Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c3051c2
when querying tags, sort by label tag frequency
bella-buchanan Feb 18, 2025
5ba972f
Revert "when querying tags, sort by label tag frequency"
bella-buchanan Feb 18, 2025
8177ecc
when querying tags, sort by label tag frequency - without the mess
bella-buchanan Feb 18, 2025
c4f9ca0
ADD: group mutually exclusive tags together
bella-buchanan Feb 27, 2025
ea74def
Merge branch 'develop' into 3578-order-tags-by-frequency-of-use
bella-buchanan Apr 8, 2025
8cb20e8
use popularity to sort tags
bella-buchanan Apr 8, 2025
689f274
sort by new field
bella-buchanan Apr 8, 2025
5dab3a1
backend needs to give popularity based on mutually exclusive so they …
bella-buchanan Apr 8, 2025
60a31ee
sort tags by popularity on FE for explore page
bella-buchanan Apr 8, 2025
1008d2b
Merge branch 'develop' into 3578-order-tags-by-frequency-of-use
bella-buchanan Apr 8, 2025
24f771f
Merge branch 'develop' into 3578-order-tags-by-frequency-of-use
bella-buchanan Apr 12, 2025
85be3be
actually do the sorting on the FE
bella-buchanan Apr 14, 2025
9d1623c
change name to count and fix sorting
bella-buchanan Apr 14, 2025
aa9bc4b
add count to tag object
bella-buchanan Apr 15, 2025
65783a6
revert context menu
bella-buchanan Apr 15, 2025
9a3711d
Revert "revert context menu"
bella-buchanan Apr 15, 2025
25844a2
include count in tag writes
bella-buchanan Apr 15, 2025
83f9ba8
sort tags on FE in newValidateBeta
bella-buchanan Apr 15, 2025
dcd4401
common utility method to sort tags
bella-buchanan Apr 15, 2025
ca2c6e7
fix formatting dif
bella-buchanan Apr 15, 2025
0662c75
type annotation
bella-buchanan Apr 15, 2025
9fa6d7e
Merge branch 'develop' into 3578-order-tags-by-frequency-of-use
bella-buchanan Apr 29, 2025
7a22219
Merge branch 'develop' into 3578-order-tags-by-frequency-of-use
bella-buchanan May 20, 2025
6bfc854
detailed tag case class
bella-buchanan May 20, 2025
30fb15a
fix validate tag display
bella-buchanan May 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions app/controllers/LabelController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,25 @@ class LabelController @Inject() (implicit val env: Environment[User, SessionAuth
*/
def getLabelTags() = Action.async { implicit request =>
val tags: List[Tag] = TagTable.getTagsForCurrentCity
Future.successful(Ok(JsArray(tags.map { tag => Json.obj(
"tag_id" -> tag.tagId,
"label_type" -> LabelTypeTable.labelTypeIdToLabelType(tag.labelTypeId).get,
"tag" -> tag.tag,
"mutually_exclusive_with" -> tag.mutuallyExclusiveWith
)})))
val tagCounts: List[TagCount] = LabelTable.getTagCounts()

val tagCountMap: Map[(String, String), Int] = tagCounts.map(tc => (tc.labelType, tc.tag) -> tc.count).toMap

val tagsWithCount: Seq[JsObject] = tags.map { tag =>
val labelType = LabelTypeTable.labelTypeIdToLabelType(tag.labelTypeId).getOrElse("")
val count = tagCountMap.getOrElse((labelType, tag.tag), 0)

Json.obj(
"tag_id" -> tag.tagId,
"label_type" -> labelType,
"tag" -> tag.tag,
"mutually_exclusive_with" -> tag.mutuallyExclusiveWith,
"count" -> count
)
}
Future.successful(Ok(JsArray(tagsWithCount)))
}

}

/**
Expand Down
18 changes: 16 additions & 2 deletions app/controllers/ValidationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import formats.json.CommentSubmissionFormats._
import formats.json.LabelFormat
import models.amt.AMTAssignmentTable
import models.daos.slick.DBTableDefinitions.{DBUser, UserTable}
import models.label.{LabelTable, LabelTypeTable, LabelValidationTable, Tag, TagTable}
import models.label.{LabelTable, LabelTypeTable, LabelValidationTable, Tag, TagCount, TagTable}
import models.label.LabelTable.{AdminValidationData, LabelValidationMetadata}
import models.mission.{Mission, MissionSetProgress, MissionTable}
import models.region.{Region, RegionTable}
Expand All @@ -24,6 +24,7 @@ import play.api.libs.json._
import play.api.Logger
import play.api.i18n.Lang
import play.api.mvc._

import javax.naming.AuthenticationException
import scala.concurrent.Future
import scala.util.Try
Expand Down Expand Up @@ -114,8 +115,21 @@ class ValidationController @Inject() (implicit val env: Environment[User, Sessio
// If all went well, load the data for Admin NewValidateBeta with the specified filters.
val adminParams: AdminValidateParams = AdminValidateParams(adminVersion=true, parsedLabelTypeId.flatten, userIdsList.map(_.flatten), neighborhoodIdList.map(_.flatten))
val validationData = getDataForValidationPages(request, labelCount=10, "Visit_NewValidateBeta", adminParams)

// Add counts to corresponding tags
val tags: List[Tag] = TagTable.getTagsForCurrentCity
Future.successful(Ok(views.html.newValidateBeta("Sidewalk - NewValidateBeta", request.identity, adminParams, validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6, tags)))
val tagCounts: List[TagCount] = LabelTable.getTagCounts()

val tagCountMap: Map[(String, String), Int] = tagCounts.map(tc => (tc.labelType, tc.tag) -> tc.count).toMap

val tagsWithCounts: List[Tag] = tags.map { tag =>
val labelType = LabelTypeTable.labelTypeIdToLabelType(tag.labelTypeId).getOrElse("")
val count = tagCountMap.getOrElse((labelType, tag.tag), 0)
tag.copy(count = count)
}


Future.successful(Ok(views.html.newValidateBeta("Sidewalk - NewValidateBeta", request.identity, adminParams, validationData._1, validationData._2, validationData._3, validationData._4.numComplete, validationData._5, validationData._6, tagsWithCounts)))
}
} else {
Future.failed(new AuthenticationException("This is a beta currently only open to Admins."))
Expand Down
3 changes: 2 additions & 1 deletion app/formats/json/LabelFormat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ object LabelFormat {
(__ \ "tag_id").write[Int] and
(__ \ "label_type_id").write[Int] and
(__ \ "tag_name").write[String] and
(__ \ "mutually_exclusive_with").writeNullable[String]
(__ \ "mutually_exclusive_with").writeNullable[String] and
(__ \ "count").write[Int]
)(unlift(Tag.unapply))
}
5 changes: 3 additions & 2 deletions app/models/label/TagTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import play.api.cache.Cache
import scala.concurrent.duration.DurationInt
import scala.slick.lifted.{ForeignKeyQuery, Index}

case class Tag(tagId: Int, labelTypeId: Int, tag: String, mutuallyExclusiveWith: Option[String])
case class Tag(tagId: Int, labelTypeId: Int, tag: String, mutuallyExclusiveWith: Option[String], count: Int = 0)

class TagTable(tagParam: slick.lifted.Tag) extends Table[Tag](tagParam, "tag") {
def tagId: Column[Int] = column[Int]("tag_id", O.PrimaryKey, O.AutoInc)
def labelTypeId: Column[Int] = column[Int]("label_type_id")
def tag: Column[String] = column[String]("tag")
def mutuallyExclusiveWith: Column[Option[String]] = column[Option[String]]("mutually_exclusive_with")

def * = (tagId, labelTypeId, tag, mutuallyExclusiveWith) <> ((Tag.apply _).tupled, Tag.unapply)
def * = (tagId, labelTypeId, tag, mutuallyExclusiveWith) <> ((Tag.apply(_: Int, _: Int, _: String, _: Option[String])).tupled,
{ t: Tag => Some((t.tagId, t.labelTypeId, t.tag, t.mutuallyExclusiveWith)) })

def labelType: ForeignKeyQuery[LabelTypeTable, LabelType] =
foreignKey("tag_label_type_id_fkey", labelTypeId, TableQuery[LabelTypeTable])(_.labelTypeId)
Expand Down
5 changes: 3 additions & 2 deletions public/javascripts/SVLabel/src/SVLabel/canvas/ContextMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ function ContextMenu (uiContextMenu) {
url: "/label/tags",
type: 'get',
success: function(json) {
self.labelTags = json;
// Sort tags by popularity and group mutually exclusive tags using the common utility function
self.labelTags = util.sortTagsByPopularityAndGroupMutuallyExclusive(json, 'count', 'tag', 'mutually_exclusive_with');
},
error: function(result) {
throw result;
Expand Down Expand Up @@ -629,4 +630,4 @@ function ContextMenu (uiContextMenu) {
self.enableTagging = enableTagging;
self.isTaggingDisabled = isTaggingDisabled;
return self;
}
}
14 changes: 12 additions & 2 deletions public/javascripts/SVValidate/src/menu/RightMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ function RightMenu(menuUI) {
labelField: 'tag_name',
valueField: 'tag_name',
searchField: 'tag_name',
sortField: 'popularity', // TODO include data abt frequency of use on this server.
onFocus: function() { svv.tracker.push('Click=TagSearch'); },
onItemAdd: function (value, $item) {
let currLabel = svv.panorama.getCurrentLabel();
Expand Down Expand Up @@ -296,6 +295,7 @@ function RightMenu(menuUI) {
label.setProperty('newTags', label.getProperty('newTags').filter(t => t !== tagToRemove));
_renderTags();
}

function _renderTags() {
let label = svv.panorama.getCurrentLabel();
let allTagOptions = structuredClone(svv.tagsByLabelType[label.getAuditProperty('labelType')]);
Expand Down Expand Up @@ -338,7 +338,17 @@ function RightMenu(menuUI) {

// Clear the possible tags to add and add all appropriate options.
$tagSelect[0].selectize.clearOptions();
$tagSelect[0].selectize.addOption(allTagOptions);

// Sort tags by popularity while grouping mutually exclusive tags using the common utility function
const sortedTags = util.sortTagsByPopularityAndGroupMutuallyExclusive(allTagOptions, 'count', 'tag_name', 'mutually_exclusive_with');

// Add index order to each tag to preserve our custom sort
sortedTags.forEach((tag, index) => {
tag.$order = index;
});

// Add the sorted tags to the selectize dropdown
$tagSelect[0].selectize.addOption(sortedTags);
}

// SEVERITY SECTION.
Expand Down
58 changes: 57 additions & 1 deletion public/javascripts/common/Utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,63 @@ function escapeHTML(str) {
case '"': return '&quot;';
case "'": return '&#039;';
default: return match;
util.escapeHTML = escapeHTML;

/**
* Sorts tags by their count/popularity while keeping mutually exclusive tags grouped together.
* This ensures that related tags appear next to each other in the UI.
* @param {Array} tags - The array of tags to sort
* @param {string} countField - The name of the field containing the count/popularity value (default: 'count')
* @param {string} nameField - The name of the field containing the tag name (default: 'tag_name' or 'tag')
* @param {string} mutuallyExclusiveField - The name of the field for mutually exclusive relationships (default: 'mutually_exclusive_with')
* @returns {Array} The sorted array of tags
*/
function sortTagsByPopularityAndGroupMutuallyExclusive(tags, countField, nameField, mutuallyExclusiveField) {
// Set default field names if not provided
countField = countField || 'count';
nameField = nameField || (tags[0] && tags[0].tag_name !== undefined ? 'tag_name' : 'tag');
mutuallyExclusiveField = mutuallyExclusiveField || 'mutually_exclusive_with';

// Return early if tags array is empty
if (!tags || tags.length === 0) {
return [];
}

// Group tags by mutually exclusive relationships
const tagGroups = {};
tags.forEach(tag => {
// For mutually exclusive tags, use the same group key for both tags
// Use the tag name for grouping, or create a special key for mutually exclusive pairs
const mutuallyExclusiveValue = tag[mutuallyExclusiveField];
const tagName = tag[nameField];

const groupKey = mutuallyExclusiveValue
? [tagName, mutuallyExclusiveValue].sort()[0]
: tagName;

if (!tagGroups[groupKey]) {
tagGroups[groupKey] = [];
}
tagGroups[groupKey].push(tag);
});

// Calculate max count for each mutually exclusive group
const groupMaxCounts = {};
Object.entries(tagGroups).forEach(([groupKey, tagsInGroup]) => {
groupMaxCounts[groupKey] = Math.max(...tagsInGroup.map(t => t[countField] || 0));
});

// Sort mutually exclusive groups by their max count
const sortedGroupKeys = Object.keys(tagGroups).sort((a, b) => {
return groupMaxCounts[b] - groupMaxCounts[a];
});

// Flatten the sorted groups back into a single array
const processedTags = sortedGroupKeys.flatMap(groupKey => {
// Within each group, sort by count
return tagGroups[groupKey].sort((a, b) => (b[countField] || 0) - (a[countField] || 0));
});

return processedTags;
}
util.escapeHTML = escapeHTML;
util.sortTagsByPopularityAndGroupMutuallyExclusive = sortTagsByPopularityAndGroupMutuallyExclusive;