Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions app/org/maproulette/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ object Config {
val KEY_SCHEDULER_SNAPSHOT_CHALLENGES_INTERVAL =
s"$SUB_GROUP_SCHEDULER.challengesSnapshot.interval"
val KEY_SCHEDULER_SNAPSHOT_CHALLENGES_START = s"$SUB_GROUP_SCHEDULER.challengesSnapshot.startTime"
val KEY_SCHEDULER_TILE_REFRESH_INTERVAL = s"$SUB_GROUP_SCHEDULER.tileRefresh.interval"

val KEY_MAPROULETTE_FRONTEND = s"$GROUP_MAPROULETTE.frontend"
val SUB_GROUP_MAPILLARY = s"$GROUP_MAPROULETTE.mapillary"
Expand Down
51 changes: 37 additions & 14 deletions app/org/maproulette/framework/controller/TaskController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ import org.maproulette.framework.service.{
NotificationService
}
import org.maproulette.framework.psql.Paging
import org.maproulette.framework.model.{
User,
TaskMarker,
TaskMarkerLocation,
TaskMarkerResponse,
OverlappingTaskMarker
}
import org.maproulette.framework.model.{User, TaskMarkerResponse}
import org.maproulette.framework.mixins.TaskJSONMixin
import org.maproulette.session.{SessionManager, SearchParameters, SearchLocation}
import play.api.mvc._
Expand Down Expand Up @@ -327,13 +321,42 @@ class TaskController @Inject() (
}
}

// for getting more detailed task marker data on individul makrers
// def getTaskMarkerData(id: Long): Action[AnyContent] = Action.async { implicit request =>
// this.sessionManager.userAwareRequest { implicit user =>
// val task = this.taskService.getTask(id)
// Ok(Json.toJson(task))
// }
// }
/**
* Get MVT (Mapbox Vector Tile) for a specific tile.
* Returns binary protobuf data for use with MapLibre vector tile sources.
*
* @param z Zoom level (0-14, MapLibre handles overzooming for 15+)
* @param x Tile X coordinate
* @param y Tile Y coordinate
* @param global Include global challenges
* @param difficulty Optional difficulty filter (1=Easy, 2=Normal, 3=Expert)
* @return Binary MVT data
*/
def getTaskTilesMvt(
z: Int,
x: Int,
y: Int,
global: Boolean,
difficulty: Option[Int],
keywords: Option[String],
location_id: Option[Long]
): Action[AnyContent] = Action.async { implicit request =>
this.sessionManager.userAwareRequest { implicit user =>
val validZoom = math.max(0, math.min(22, z))
val validDifficulty = difficulty.filter(d => d >= 1 && d <= 3)

val mvtBytes = this.serviceManager.tileAggregate.getMvtTile(
validZoom,
x,
y,
validDifficulty,
global,
keywords,
location_id
)
Ok(mvtBytes).as("application/vnd.mapbox-vector-tile")
}
}

/**
* Updates the completion responses asked in the task instructions. Request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
*/
package org.maproulette.framework.model

import org.joda.time.DateTime
import play.api.libs.json.{JsValue, Json, Reads, Writes}
import play.api.libs.json.JodaWrites._
import play.api.libs.json.JodaReads._
import play.api.libs.json.{Json, Reads, Writes}
import org.maproulette.framework.model.TaskMarkerLocation

case class SingleTaskMarker(
Expand Down
5 changes: 1 addition & 4 deletions app/org/maproulette/framework/model/TaskMarker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
*/
package org.maproulette.framework.model

import org.joda.time.DateTime
import play.api.libs.json.{JsValue, Json, Reads, Writes}
import play.api.libs.json.JodaWrites._
import play.api.libs.json.JodaReads._
import play.api.libs.json.{Json, Reads, Writes}

/**
* @author cuthbertm
Expand Down
135 changes: 135 additions & 0 deletions app/org/maproulette/framework/model/TileAggregate.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright (C) 2020 MapRoulette contributors (see CONTRIBUTORS.md).
* Licensed under the Apache License, Version 2.0 (see LICENSE).
*/
package org.maproulette.framework.model
import play.api.libs.json._

case class ClusterPoint(
lat: Double,
lng: Double,
count: Int
)

object ClusterPoint {
implicit val clusterPointFormat: Format[ClusterPoint] = Json.format[ClusterPoint]
}

case class FilterCounts(
d1_gf: Int = 0,
d1_gt: Int = 0,
d2_gf: Int = 0,
d2_gt: Int = 0,
d3_gf: Int = 0,
d3_gt: Int = 0,
d0_gf: Int = 0,
d0_gt: Int = 0
) {

/**
* Get count for specific difficulty and global filters
*
* @param difficulty Optional difficulty filter (1, 2, 3)
* @param global Whether to include global challenges (true = all, false = non-global only)
* @return Filtered count
*/
def getFilteredCount(difficulty: Option[Int], global: Boolean): Int = {
difficulty match {
case Some(1) => if (global) d1_gf + d1_gt else d1_gf
case Some(2) => if (global) d2_gf + d2_gt else d2_gf
case Some(3) => if (global) d3_gf + d3_gt else d3_gf
case Some(_) =>
// Unknown difficulty values use d0 (unset/other) bucket
if (global) d0_gf + d0_gt else d0_gf
case None =>
// No filter: sum all difficulty levels
if (global) d1_gf + d1_gt + d2_gf + d2_gt + d3_gf + d3_gt + d0_gf + d0_gt
else d1_gf + d2_gf + d3_gf + d0_gf
}
}

/**
* Get total count (all combinations)
*/
def total: Int = d1_gf + d1_gt + d2_gf + d2_gt + d3_gf + d3_gt + d0_gf + d0_gt
}

object FilterCounts {
implicit val filterCountsFormat: Format[FilterCounts] = Json.format[FilterCounts]

def fromJson(json: JsValue): FilterCounts = {
FilterCounts(
d1_gf = (json \ "d1_gf").asOpt[Int].getOrElse(0),
d1_gt = (json \ "d1_gt").asOpt[Int].getOrElse(0),
d2_gf = (json \ "d2_gf").asOpt[Int].getOrElse(0),
d2_gt = (json \ "d2_gt").asOpt[Int].getOrElse(0),
d3_gf = (json \ "d3_gf").asOpt[Int].getOrElse(0),
d3_gt = (json \ "d3_gt").asOpt[Int].getOrElse(0),
d0_gf = (json \ "d0_gf").asOpt[Int].getOrElse(0),
d0_gt = (json \ "d0_gt").asOpt[Int].getOrElse(0)
)
}
}

/**
* Represents a pre-computed task group at any zoom level (0-14).
*
* Zoom 0-13: One cluster per tile (group_type=2, no task_ids)
* - As zoom increases, tiles get smaller, clusters naturally split
* - Frontend displays these as cluster markers
*
* Zoom 14: One entry per overlap group (group_type=0 or 1, with task_ids)
* - Frontend handles clustering for zoom levels 14-22
* - Returns individual task markers and overlapping task markers
*
* @param id Database ID
* @param z Zoom level (0-14)
* @param x Tile X coordinate
* @param y Tile Y coordinate
* @param groupType 0=single task, 1=overlapping tasks, 2=cluster
* @param centroidLat Centroid latitude of the group
* @param centroidLng Centroid longitude of the group
* @param taskIds List of task IDs (empty for clusters at zoom 0-13)
* @param taskCount Number of tasks in this group
* @param countsByFilter Counts broken down by difficulty × global for filtering
*/
case class TileTaskGroup(
id: Long,
z: Int,
x: Int,
y: Int,
groupType: Int,
centroidLat: Double,
centroidLng: Double,
taskIds: List[Long],
taskCount: Int,
countsByFilter: FilterCounts
) {

/**
* Get the filtered count for this group based on difficulty and global filters
*/
def getFilteredCount(difficulty: Option[Int], global: Boolean): Int = {
countsByFilter.getFilteredCount(difficulty, global)
}

/**
* Check if this is a single task (zoom 14 only)
*/
def isSingle: Boolean = groupType == 0

/**
* Check if this is an overlapping group (zoom 14 only)
*/
def isOverlapping: Boolean = groupType == 1

/**
* Check if this is a cluster (zoom 0-13)
*/
def isCluster: Boolean = groupType == 2
}

object TileTaskGroup {
implicit val tileTaskGroupWrites: Writes[TileTaskGroup] = Json.writes[TileTaskGroup]
implicit val tileTaskGroupReads: Reads[TileTaskGroup] = Json.reads[TileTaskGroup]
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import java.sql.Connection

import anorm.~
import anorm._
import anorm.SqlParser.{double, get, int, long, str}
import anorm.SqlParser.{get, int, str}
import javax.inject.{Inject, Singleton}
import org.maproulette.session.{SearchParameters, SearchLocation}
import org.maproulette.framework.psql.{Query, Order, Paging}
Expand Down
Loading