diff --git a/lib/Controller/LocationController.php b/lib/Controller/LocationController.php
index 8c9f0d81..9981a5a8 100644
--- a/lib/Controller/LocationController.php
+++ b/lib/Controller/LocationController.php
@@ -53,29 +53,29 @@ class LocationController extends ApiBase
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND);
}
- // Just check bound parameters but not use them; they are used in transformation
+ // Just check bound parameters instead of using them; they are used in transformation
$minLat = $this->request->getParam('minLat');
$maxLat = $this->request->getParam('maxLat');
$minLng = $this->request->getParam('minLng');
$maxLng = $this->request->getParam('maxLng');
- if (!$minLat || !$maxLat || !$minLng || !$maxLng) {
- return new JSONResponse(['message' => 'Parameters missing'], Http::STATUS_PRECONDITION_FAILED);
+ if (!is_numeric($minLat) || !is_numeric($maxLat) || !is_numeric($minLng) || !is_numeric($maxLng)) {
+ return new JSONResponse(['message' => 'Invalid perameters'], Http::STATUS_PRECONDITION_FAILED);
}
- // Zoom level is used to determine the size of boxes
+ // Zoom level is used to determine the grid length
$zoomLevel = $this->request->getParam('zoom');
if (!$zoomLevel || !is_numeric($zoomLevel)) {
return new JSONResponse(['message' => 'Invalid zoom level'], Http::STATUS_PRECONDITION_FAILED);
}
// A tweakable parameter to determine the number of boxes in the map
- $clusterDensity = 3;
- $boxSize = 180.0 / (2 ** $zoomLevel * $clusterDensity);
+ $clusterDensity = 2;
+ $gridLength = 180.0 / (2 ** $zoomLevel * $clusterDensity);
try {
$clusters = $this->timelineQuery->getMapClusters(
- $boxSize,
+ $gridLength,
$root,
$uid,
$this->isRecursive(),
@@ -83,9 +83,66 @@ class LocationController extends ApiBase
$this->getTransformations(true),
);
+ // Merge clusters that are close together
+ $distanceThreshold = $gridLength / 3;
+ $clusters = $this->mergeClusters($clusters, $distanceThreshold);
+
return new JSONResponse($clusters);
} catch (\Exception $e) {
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
+
+ private function mergeClusters($clusters, $distanceThreshold): array
+ {
+ $valid = array_fill(0, \count($clusters), true);
+ for ($i = 0; $i < \count($clusters); ++$i) {
+ if (!$valid[$i]) {
+ continue;
+ }
+ for ($j = 0; $j < \count($clusters); ++$j) {
+ if ($i === $j) {
+ continue;
+ }
+ if (!$valid[$i] || !$valid[$j]) {
+ continue;
+ }
+ if ($this->isClose($clusters[$i], $clusters[$j], $distanceThreshold)) {
+ $this->merge($valid, $clusters, $i, $j);
+ }
+ }
+ }
+
+ $updatedClusters = [];
+ for ($i = 0; $i < \count($clusters); ++$i) {
+ if ($valid[$i]) {
+ $updatedClusters[] = $clusters[$i];
+ }
+ }
+
+ return $updatedClusters;
+ }
+
+ private function isCLose(array $cluster1, array $cluster2, float $threshold): bool
+ {
+ $deltaX = (float) $cluster1['center'][0] - (float) $cluster2['center'][0];
+ $deltaY = (float) $cluster1['center'][1] - (float) $cluster2['center'][1];
+
+ return $deltaX * $deltaX + $deltaY * $deltaY < $threshold * $threshold;
+ }
+
+ private function merge(array &$valid, array &$clusters, int $index1, int $index2): void
+ {
+ $cluster1Count = (int) $clusters[$index1]['count'];
+ $cluster1Center = $clusters[$index1]['center'];
+ $cluster2Count = (int) $clusters[$index2]['count'];
+ $cluster2Center = $clusters[$index2]['center'];
+ $newCenter = [
+ ($cluster1Count * $cluster1Center[0] + $cluster2Count * $cluster2Center[0]) / ($cluster1Count + $cluster2Count),
+ ($cluster1Count * $cluster1Center[1] + $cluster2Count * $cluster2Center[1]) / ($cluster1Count + $cluster2Count),
+ ];
+ $clusters[] = ['center' => $newCenter, 'count' => $cluster1Count + $cluster2Count];
+ $valid[] = true;
+ $valid[$index1] = $valid[$index2] = false;
+ }
}
diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php
index 6a8e166c..53f28c84 100644
--- a/lib/Db/TimelineQueryDays.php
+++ b/lib/Db/TimelineQueryDays.php
@@ -196,7 +196,7 @@ trait TimelineQueryDays
}
public function getMapClusters(
- float $boxSize,
+ float $gridLength,
TimelineRoot &$root,
string $uid,
bool $recursive,
@@ -217,7 +217,7 @@ trait TimelineQueryDays
$query = $this->joinFilecache($query, $root, $recursive, $archive);
// Group by cluster
- $groupFunction = $query->createFunction('latitude DIV '.$boxSize.', longitude DIV '.$boxSize);
+ $groupFunction = $query->createFunction('latitude DIV '.$gridLength.', longitude DIV '.$gridLength);
$query->groupBy($groupFunction);
// Apply all transformations (including map bounds)
diff --git a/src/components/top-matter/LocationTopMatter.vue b/src/components/top-matter/LocationTopMatter.vue
index 1c3c505e..f4cb1dd1 100644
--- a/src/components/top-matter/LocationTopMatter.vue
+++ b/src/components/top-matter/LocationTopMatter.vue
@@ -8,7 +8,6 @@
@zoomend="updateMapAndTimeline"
>
-
-
@@ -88,22 +86,28 @@ export default defineComponent({
async updateMapAndTimeline() {
let map = this.$refs.map as LMap;
- //let markerCluster = this.$refs.markerCluster as Vue2LeafletMarkerCluster;
let boundary = map.mapObject.getBounds();
+ let minLat = boundary.getSouth();
+ let maxLat = boundary.getNorth();
+ let minLng = boundary.getWest();
+ let maxLng = boundary.getEast();
let zoomLevel = map.mapObject.getZoom().toString();
this.$parent.$emit("updateBoundary", {
- minLat: boundary.getSouth(),
- maxLat: boundary.getNorth(),
- minLng: boundary.getWest(),
- maxLng: boundary.getEast(),
+ minLat: minLat,
+ maxLat: maxLat,
+ minLng: minLng,
+ maxLng: maxLng,
});
+ let mapWidth = maxLat - minLat;
+ let mapHeight = maxLng - minLng;
const query = new URLSearchParams();
- query.set("minLat", boundary.getSouth().toString());
- query.set("maxLat", boundary.getNorth().toString());
- query.set("minLng", boundary.getWest().toString());
- query.set("maxLng", boundary.getEast().toString());
+ // Show clusters correctly while draging the map
+ query.set("minLat", (minLat - mapWidth).toString());
+ query.set("maxLat", (maxLat + mapWidth).toString());
+ query.set("minLng", (minLng - mapHeight).toString());
+ query.set("maxLng", (maxLng + mapHeight).toString());
query.set("zoom", zoomLevel);
const url = API.Q(API.CLUSTERS(), query);