From deb101bcee1ce3ba61a3b9c0c851102c721ff331 Mon Sep 17 00:00:00 2001 From: Johan von Forstner Date: Mon, 21 Apr 2025 12:22:43 +0200 Subject: [PATCH] make local (in-memory) clustering consistent with DB clustering --- .../maps/android/clustering/Cluster.java | 32 -- .../maps/android/clustering/ClusterItem.java | 46 --- .../clustering/algo/AbstractAlgorithm.java | 39 --- .../android/clustering/algo/Algorithm.java | 85 ----- ...NonHierarchicalDistanceBasedAlgorithm.java | 314 ------------------ .../clustering/algo/StaticCluster.java | 83 ----- .../google/maps/android/geometry/Bounds.java | 61 ---- .../google/maps/android/geometry/Point.java | 35 -- .../google/maps/android/projection/Point.java | 27 -- .../SphericalMercatorProjection.java | 46 --- .../maps/android/quadtree/PointQuadTree.java | 226 ------------- .../evmap/storage/ChargeLocationsDao.kt | 8 +- .../java/net/vonforst/evmap/ui/Clustering.kt | 47 ++- .../vonforst/evmap/viewmodel/MapViewModel.kt | 6 +- 14 files changed, 28 insertions(+), 1027 deletions(-) delete mode 100644 app/src/main/java/com/google/maps/android/clustering/Cluster.java delete mode 100644 app/src/main/java/com/google/maps/android/clustering/ClusterItem.java delete mode 100644 app/src/main/java/com/google/maps/android/clustering/algo/AbstractAlgorithm.java delete mode 100644 app/src/main/java/com/google/maps/android/clustering/algo/Algorithm.java delete mode 100644 app/src/main/java/com/google/maps/android/clustering/algo/NonHierarchicalDistanceBasedAlgorithm.java delete mode 100644 app/src/main/java/com/google/maps/android/clustering/algo/StaticCluster.java delete mode 100644 app/src/main/java/com/google/maps/android/geometry/Bounds.java delete mode 100644 app/src/main/java/com/google/maps/android/geometry/Point.java delete mode 100644 app/src/main/java/com/google/maps/android/projection/Point.java delete mode 100644 app/src/main/java/com/google/maps/android/projection/SphericalMercatorProjection.java delete mode 100644 app/src/main/java/com/google/maps/android/quadtree/PointQuadTree.java diff --git a/app/src/main/java/com/google/maps/android/clustering/Cluster.java b/app/src/main/java/com/google/maps/android/clustering/Cluster.java deleted file mode 100644 index a08b9c0f..00000000 --- a/app/src/main/java/com/google/maps/android/clustering/Cluster.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.clustering; - -import com.car2go.maps.model.LatLng; - -import java.util.Collection; - -/** - * A collection of ClusterItems that are nearby each other. - */ -public interface Cluster { - LatLng getPosition(); - - Collection getItems(); - - int getSize(); -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/clustering/ClusterItem.java b/app/src/main/java/com/google/maps/android/clustering/ClusterItem.java deleted file mode 100644 index 71795d80..00000000 --- a/app/src/main/java/com/google/maps/android/clustering/ClusterItem.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.clustering; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.car2go.maps.model.LatLng; - -/** - * ClusterItem represents a marker on the map. - */ -public interface ClusterItem { - - /** - * The position of this marker. This must always return the same value. - */ - @NonNull - LatLng getPosition(); - - /** - * The title of this marker. - */ - @Nullable - String getTitle(); - - /** - * The description of this marker. - */ - @Nullable - String getSnippet(); -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/clustering/algo/AbstractAlgorithm.java b/app/src/main/java/com/google/maps/android/clustering/algo/AbstractAlgorithm.java deleted file mode 100644 index 182dc70f..00000000 --- a/app/src/main/java/com/google/maps/android/clustering/algo/AbstractAlgorithm.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.maps.android.clustering.algo; - -import com.google.maps.android.clustering.ClusterItem; - -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * Base Algorithm class that implements lock/unlock functionality. - */ -public abstract class AbstractAlgorithm implements Algorithm { - - private final ReadWriteLock mLock = new ReentrantReadWriteLock(); - - @Override - public void lock() { - mLock.writeLock().lock(); - } - - @Override - public void unlock() { - mLock.writeLock().unlock(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/clustering/algo/Algorithm.java b/app/src/main/java/com/google/maps/android/clustering/algo/Algorithm.java deleted file mode 100644 index c8dba12e..00000000 --- a/app/src/main/java/com/google/maps/android/clustering/algo/Algorithm.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.clustering.algo; - -import com.google.maps.android.clustering.Cluster; -import com.google.maps.android.clustering.ClusterItem; - -import java.util.Collection; -import java.util.Set; - -/** - * Logic for computing clusters - */ -public interface Algorithm { - - /** - * Adds an item to the algorithm - * - * @param item the item to be added - * @return true if the algorithm contents changed as a result of the call - */ - boolean addItem(T item); - - /** - * Adds a collection of items to the algorithm - * - * @param items the items to be added - * @return true if the algorithm contents changed as a result of the call - */ - boolean addItems(Collection items); - - void clearItems(); - - /** - * Removes an item from the algorithm - * - * @param item the item to be removed - * @return true if this algorithm contained the specified element (or equivalently, if this - * algorithm changed as a result of the call). - */ - boolean removeItem(T item); - - /** - * Updates the provided item in the algorithm - * - * @param item the item to be updated - * @return true if the item existed in the algorithm and was updated, or false if the item did - * not exist in the algorithm and the algorithm contents remain unchanged. - */ - boolean updateItem(T item); - - /** - * Removes a collection of items from the algorithm - * - * @param items the items to be removed - * @return true if this algorithm contents changed as a result of the call - */ - boolean removeItems(Collection items); - - Set> getClusters(float zoom); - - Collection getItems(); - - void setMaxDistanceBetweenClusteredItems(int maxDistance); - - int getMaxDistanceBetweenClusteredItems(); - - void lock(); - - void unlock(); -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/clustering/algo/NonHierarchicalDistanceBasedAlgorithm.java b/app/src/main/java/com/google/maps/android/clustering/algo/NonHierarchicalDistanceBasedAlgorithm.java deleted file mode 100644 index f9ae445e..00000000 --- a/app/src/main/java/com/google/maps/android/clustering/algo/NonHierarchicalDistanceBasedAlgorithm.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.clustering.algo; - -import com.car2go.maps.model.LatLng; -import com.google.maps.android.clustering.Cluster; -import com.google.maps.android.clustering.ClusterItem; -import com.google.maps.android.geometry.Bounds; -import com.google.maps.android.geometry.Point; -import com.google.maps.android.projection.SphericalMercatorProjection; -import com.google.maps.android.quadtree.PointQuadTree; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -/** - * A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not - * hierarchical. - *

- * High level algorithm:
- * 1. Iterate over items in the order they were added (candidate clusters).
- * 2. Create a cluster with the center of the item.
- * 3. Add all items that are within a certain distance to the cluster.
- * 4. Move any items out of an existing cluster if they are closer to another cluster.
- * 5. Remove those items from the list of candidate clusters. - *

- * Clusters have the center of the first element (not the centroid of the items within it). - */ -public class NonHierarchicalDistanceBasedAlgorithm extends AbstractAlgorithm { - private static final int DEFAULT_MAX_DISTANCE_AT_ZOOM = 100; // essentially 100 dp. - - private int mMaxDistance = DEFAULT_MAX_DISTANCE_AT_ZOOM; - - /** - * Any modifications should be synchronized on mQuadTree. - */ - private final Collection> mItems = new LinkedHashSet<>(); - - /** - * Any modifications should be synchronized on mQuadTree. - */ - private final PointQuadTree> mQuadTree = new PointQuadTree<>(0, 1, 0, 1); - - private static final SphericalMercatorProjection PROJECTION = new SphericalMercatorProjection(1); - - /** - * Adds an item to the algorithm - * - * @param item the item to be added - * @return true if the algorithm contents changed as a result of the call - */ - @Override - public boolean addItem(T item) { - boolean result; - final QuadItem quadItem = new QuadItem<>(item); - synchronized (mQuadTree) { - result = mItems.add(quadItem); - if (result) { - mQuadTree.add(quadItem); - } - } - return result; - } - - /** - * Adds a collection of items to the algorithm - * - * @param items the items to be added - * @return true if the algorithm contents changed as a result of the call - */ - @Override - public boolean addItems(Collection items) { - boolean result = false; - for (T item : items) { - boolean individualResult = addItem(item); - if (individualResult) { - result = true; - } - } - return result; - } - - @Override - public void clearItems() { - synchronized (mQuadTree) { - mItems.clear(); - mQuadTree.clear(); - } - } - - /** - * Removes an item from the algorithm - * - * @param item the item to be removed - * @return true if this algorithm contained the specified element (or equivalently, if this - * algorithm changed as a result of the call). - */ - @Override - public boolean removeItem(T item) { - boolean result; - // QuadItem delegates hashcode() and equals() to its item so, - // removing any QuadItem to that item will remove the item - final QuadItem quadItem = new QuadItem<>(item); - synchronized (mQuadTree) { - result = mItems.remove(quadItem); - if (result) { - mQuadTree.remove(quadItem); - } - } - return result; - } - - /** - * Removes a collection of items from the algorithm - * - * @param items the items to be removed - * @return true if this algorithm contents changed as a result of the call - */ - @Override - public boolean removeItems(Collection items) { - boolean result = false; - synchronized (mQuadTree) { - for (T item : items) { - // QuadItem delegates hashcode() and equals() to its item so, - // removing any QuadItem to that item will remove the item - final QuadItem quadItem = new QuadItem<>(item); - boolean individualResult = mItems.remove(quadItem); - if (individualResult) { - mQuadTree.remove(quadItem); - result = true; - } - } - } - return result; - } - - /** - * Updates the provided item in the algorithm - * - * @param item the item to be updated - * @return true if the item existed in the algorithm and was updated, or false if the item did - * not exist in the algorithm and the algorithm contents remain unchanged. - */ - @Override - public boolean updateItem(T item) { - // TODO - Can this be optimized to update the item in-place if the location hasn't changed? - boolean result; - synchronized (mQuadTree) { - result = removeItem(item); - if (result) { - // Only add the item if it was removed (to help prevent accidental duplicates on map) - result = addItem(item); - } - } - return result; - } - - @Override - public Set> getClusters(float zoom) { - final int discreteZoom = (int) zoom; - - final double zoomSpecificSpan = mMaxDistance / Math.pow(2, discreteZoom) / 256; - - final Set> visitedCandidates = new HashSet<>(); - final Set> results = new HashSet<>(); - final Map, Double> distanceToCluster = new HashMap<>(); - final Map, StaticCluster> itemToCluster = new HashMap<>(); - - synchronized (mQuadTree) { - for (QuadItem candidate : getClusteringItems(mQuadTree, zoom)) { - if (visitedCandidates.contains(candidate)) { - // Candidate is already part of another cluster. - continue; - } - - Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan); - Collection> clusterItems; - clusterItems = mQuadTree.search(searchBounds); - if (clusterItems.size() == 1) { - // Only the current marker is in range. Just add the single item to the results. - results.add(candidate); - visitedCandidates.add(candidate); - distanceToCluster.put(candidate, 0d); - continue; - } - StaticCluster cluster = new StaticCluster<>(candidate.mClusterItem.getPosition()); - results.add(cluster); - - for (QuadItem clusterItem : clusterItems) { - Double existingDistance = distanceToCluster.get(clusterItem); - double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint()); - if (existingDistance != null) { - // Item already belongs to another cluster. Check if it's closer to this cluster. - if (existingDistance < distance) { - continue; - } - // Move item to the closer cluster. - itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem); - } - distanceToCluster.put(clusterItem, distance); - cluster.add(clusterItem.mClusterItem); - itemToCluster.put(clusterItem, cluster); - } - visitedCandidates.addAll(clusterItems); - } - } - return results; - } - - protected Collection> getClusteringItems(PointQuadTree> quadTree, float zoom) { - return mItems; - } - - @Override - public Collection getItems() { - final Set items = new LinkedHashSet<>(); - synchronized (mQuadTree) { - for (QuadItem quadItem : mItems) { - items.add(quadItem.mClusterItem); - } - } - return items; - } - - @Override - public void setMaxDistanceBetweenClusteredItems(int maxDistance) { - mMaxDistance = maxDistance; - } - - @Override - public int getMaxDistanceBetweenClusteredItems() { - return mMaxDistance; - } - - private double distanceSquared(Point a, Point b) { - return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); - } - - private Bounds createBoundsFromSpan(Point p, double span) { - // TODO: Use a span that takes into account the visual size of the marker, not just its - // LatLng. - double halfSpan = span / 2; - return new Bounds( - p.x - halfSpan, p.x + halfSpan, - p.y - halfSpan, p.y + halfSpan); - } - - protected static class QuadItem implements PointQuadTree.Item, Cluster { - private final T mClusterItem; - private final Point mPoint; - private final LatLng mPosition; - private Set singletonSet; - - private QuadItem(T item) { - mClusterItem = item; - mPosition = item.getPosition(); - mPoint = PROJECTION.toPoint(mPosition); - singletonSet = Collections.singleton(mClusterItem); - } - - @Override - public Point getPoint() { - return mPoint; - } - - @Override - public LatLng getPosition() { - return mPosition; - } - - @Override - public Set getItems() { - return singletonSet; - } - - @Override - public int getSize() { - return 1; - } - - @Override - public int hashCode() { - return mClusterItem.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof QuadItem)) { - return false; - } - - return ((QuadItem) other).mClusterItem.equals(mClusterItem); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/clustering/algo/StaticCluster.java b/app/src/main/java/com/google/maps/android/clustering/algo/StaticCluster.java deleted file mode 100644 index 528cbcca..00000000 --- a/app/src/main/java/com/google/maps/android/clustering/algo/StaticCluster.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.clustering.algo; - -import com.car2go.maps.model.LatLng; -import com.google.maps.android.clustering.Cluster; -import com.google.maps.android.clustering.ClusterItem; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * A cluster whose center is determined upon creation. - */ -public class StaticCluster implements Cluster { - private final LatLng mCenter; - private final List mItems = new ArrayList(); - - public StaticCluster(LatLng center) { - mCenter = center; - } - - public boolean add(T t) { - return mItems.add(t); - } - - @Override - public LatLng getPosition() { - return mCenter; - } - - public boolean remove(T t) { - return mItems.remove(t); - } - - @Override - public Collection getItems() { - return mItems; - } - - @Override - public int getSize() { - return mItems.size(); - } - - @Override - public String toString() { - return "StaticCluster{" + - "mCenter=" + mCenter + - ", mItems.size=" + mItems.size() + - '}'; - } - - @Override - public int hashCode() { - return mCenter.hashCode() + mItems.hashCode(); - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof StaticCluster)) { - return false; - } - - return ((StaticCluster) other).mCenter.equals(mCenter) - && ((StaticCluster) other).mItems.equals(mItems); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/geometry/Bounds.java b/app/src/main/java/com/google/maps/android/geometry/Bounds.java deleted file mode 100644 index 35b582b8..00000000 --- a/app/src/main/java/com/google/maps/android/geometry/Bounds.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.geometry; - -/** - * Represents an area in the cartesian plane. - */ -public class Bounds { - public final double minX; - public final double minY; - - public final double maxX; - public final double maxY; - - public final double midX; - public final double midY; - - public Bounds(double minX, double maxX, double minY, double maxY) { - this.minX = minX; - this.minY = minY; - this.maxX = maxX; - this.maxY = maxY; - - midX = (minX + maxX) / 2; - midY = (minY + maxY) / 2; - } - - public boolean contains(double x, double y) { - return minX <= x && x <= maxX && minY <= y && y <= maxY; - } - - public boolean contains(Point point) { - return contains(point.x, point.y); - } - - public boolean intersects(double minX, double maxX, double minY, double maxY) { - return minX < this.maxX && this.minX < maxX && minY < this.maxY && this.minY < maxY; - } - - public boolean intersects(Bounds bounds) { - return intersects(bounds.minX, bounds.maxX, bounds.minY, bounds.maxY); - } - - public boolean contains(Bounds bounds) { - return bounds.minX >= minX && bounds.maxX <= maxX && bounds.minY >= minY && bounds.maxY <= maxY; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/geometry/Point.java b/app/src/main/java/com/google/maps/android/geometry/Point.java deleted file mode 100644 index 8f57a792..00000000 --- a/app/src/main/java/com/google/maps/android/geometry/Point.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.geometry; - -public class Point { - public final double x; - public final double y; - - public Point(double x, double y) { - this.x = x; - this.y = y; - } - - @Override - public String toString() { - return "Point{" + - "x=" + x + - ", y=" + y + - '}'; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/projection/Point.java b/app/src/main/java/com/google/maps/android/projection/Point.java deleted file mode 100644 index 70a56e90..00000000 --- a/app/src/main/java/com/google/maps/android/projection/Point.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.projection; - -/** - * @deprecated since 0.2. Use {@link com.google.maps.android.geometry.Point} instead. - */ -@Deprecated -public class Point extends com.google.maps.android.geometry.Point { - public Point(double x, double y) { - super(x, y); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/projection/SphericalMercatorProjection.java b/app/src/main/java/com/google/maps/android/projection/SphericalMercatorProjection.java deleted file mode 100644 index 1eba7711..00000000 --- a/app/src/main/java/com/google/maps/android/projection/SphericalMercatorProjection.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.projection; - -import com.car2go.maps.model.LatLng; - -public class SphericalMercatorProjection { - final double mWorldWidth; - - public SphericalMercatorProjection(final double worldWidth) { - mWorldWidth = worldWidth; - } - - @SuppressWarnings("deprecation") - public Point toPoint(final LatLng latLng) { - final double x = latLng.longitude / 360 + .5; - final double siny = Math.sin(Math.toRadians(latLng.latitude)); - final double y = 0.5 * Math.log((1 + siny) / (1 - siny)) / -(2 * Math.PI) + .5; - - return new Point(x * mWorldWidth, y * mWorldWidth); - } - - public LatLng toLatLng(com.google.maps.android.geometry.Point point) { - final double x = point.x / mWorldWidth - 0.5; - final double lng = x * 360; - - double y = .5 - (point.y / mWorldWidth); - final double lat = 90 - Math.toDegrees(Math.atan(Math.exp(-y * 2 * Math.PI)) * 2); - - return new LatLng(lat, lng); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/quadtree/PointQuadTree.java b/app/src/main/java/com/google/maps/android/quadtree/PointQuadTree.java deleted file mode 100644 index 7c715ed8..00000000 --- a/app/src/main/java/com/google/maps/android/quadtree/PointQuadTree.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.quadtree; - -import com.google.maps.android.geometry.Bounds; -import com.google.maps.android.geometry.Point; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -/** - * A quad tree which tracks items with a Point geometry. - * See http://en.wikipedia.org/wiki/Quadtree for details on the data structure. - * This class is not thread safe. - */ -public class PointQuadTree { - public interface Item { - Point getPoint(); - } - - /** - * The bounds of this quad. - */ - private final Bounds mBounds; - - /** - * The depth of this quad in the tree. - */ - private final int mDepth; - - /** - * Maximum number of elements to store in a quad before splitting. - */ - private final static int MAX_ELEMENTS = 50; - - /** - * The elements inside this quad, if any. - */ - private Set mItems; - - /** - * Maximum depth. - */ - private final static int MAX_DEPTH = 40; - - /** - * Child quads. - */ - private List> mChildren = null; - - /** - * Creates a new quad tree with specified bounds. - * - * @param minX - * @param maxX - * @param minY - * @param maxY - */ - public PointQuadTree(double minX, double maxX, double minY, double maxY) { - this(new Bounds(minX, maxX, minY, maxY)); - } - - public PointQuadTree(Bounds bounds) { - this(bounds, 0); - } - - private PointQuadTree(double minX, double maxX, double minY, double maxY, int depth) { - this(new Bounds(minX, maxX, minY, maxY), depth); - } - - private PointQuadTree(Bounds bounds, int depth) { - mBounds = bounds; - mDepth = depth; - } - - /** - * Insert an item. - */ - public void add(T item) { - Point point = item.getPoint(); - if (this.mBounds.contains(point.x, point.y)) { - insert(point.x, point.y, item); - } - } - - private void insert(double x, double y, T item) { - if (this.mChildren != null) { - if (y < mBounds.midY) { - if (x < mBounds.midX) { // top left - mChildren.get(0).insert(x, y, item); - } else { // top right - mChildren.get(1).insert(x, y, item); - } - } else { - if (x < mBounds.midX) { // bottom left - mChildren.get(2).insert(x, y, item); - } else { - mChildren.get(3).insert(x, y, item); - } - } - return; - } - if (mItems == null) { - mItems = new LinkedHashSet<>(); - } - mItems.add(item); - if (mItems.size() > MAX_ELEMENTS && mDepth < MAX_DEPTH) { - split(); - } - } - - /** - * Split this quad. - */ - private void split() { - mChildren = new ArrayList>(4); - mChildren.add(new PointQuadTree(mBounds.minX, mBounds.midX, mBounds.minY, mBounds.midY, mDepth + 1)); - mChildren.add(new PointQuadTree(mBounds.midX, mBounds.maxX, mBounds.minY, mBounds.midY, mDepth + 1)); - mChildren.add(new PointQuadTree(mBounds.minX, mBounds.midX, mBounds.midY, mBounds.maxY, mDepth + 1)); - mChildren.add(new PointQuadTree(mBounds.midX, mBounds.maxX, mBounds.midY, mBounds.maxY, mDepth + 1)); - - Set items = mItems; - mItems = null; - - for (T item : items) { - // re-insert items into child quads. - insert(item.getPoint().x, item.getPoint().y, item); - } - } - - /** - * Remove the given item from the set. - * - * @return whether the item was removed. - */ - public boolean remove(T item) { - Point point = item.getPoint(); - if (this.mBounds.contains(point.x, point.y)) { - return remove(point.x, point.y, item); - } else { - return false; - } - } - - private boolean remove(double x, double y, T item) { - if (this.mChildren != null) { - if (y < mBounds.midY) { - if (x < mBounds.midX) { // top left - return mChildren.get(0).remove(x, y, item); - } else { // top right - return mChildren.get(1).remove(x, y, item); - } - } else { - if (x < mBounds.midX) { // bottom left - return mChildren.get(2).remove(x, y, item); - } else { - return mChildren.get(3).remove(x, y, item); - } - } - } else { - if (mItems == null) { - return false; - } else { - return mItems.remove(item); - } - } - } - - /** - * Removes all points from the quadTree - */ - public void clear() { - mChildren = null; - if (mItems != null) { - mItems.clear(); - } - } - - /** - * Search for all items within a given bounds. - */ - public Collection search(Bounds searchBounds) { - final List results = new ArrayList(); - search(searchBounds, results); - return results; - } - - private void search(Bounds searchBounds, Collection results) { - if (!mBounds.intersects(searchBounds)) { - return; - } - - if (this.mChildren != null) { - for (PointQuadTree quad : mChildren) { - quad.search(searchBounds, results); - } - } else if (mItems != null) { - if (searchBounds.contains(mBounds)) { - results.addAll(mItems); - } else { - for (T item : mItems) { - if (searchBounds.contains(item.getPoint())) { - results.add(item); - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt index 07777e7f..1ae8a011 100644 --- a/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt +++ b/app/src/main/java/net/vonforst/evmap/storage/ChargeLocationsDao.kt @@ -28,6 +28,7 @@ import net.vonforst.evmap.api.openstreetmap.OSMReferenceData import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper import net.vonforst.evmap.model.* import net.vonforst.evmap.ui.cluster +import net.vonforst.evmap.ui.getClusterPrecision import net.vonforst.evmap.utils.crossesAntimeridian import net.vonforst.evmap.utils.splitAtAntimeridian import net.vonforst.evmap.viewmodel.Resource @@ -127,7 +128,7 @@ abstract class ChargeLocationsDao { after: Long, zoom: Float ): List { - val precision = 30000000 / 2.0.pow(zoom.roundToInt() + 1) + val precision = getClusterPrecision(zoom) val clusters = getChargeLocationClusters(lat1, lat2, lng1, lng2, dataSource, after, precision) val singleChargers = @@ -466,11 +467,10 @@ class ChargeLocationsRepository( we have to cluster even at pretty high zoom levels to make sure the map does not get laggy. Otherwise, only cluster at zoom levels <= 11. */ val useClustering = chargers.size > 500 || zoom <= 11f - val clusterDistance = getClusterDistance(zoom) - val chargersClustered = if (useClustering && clusterDistance != null) { + val chargersClustered = if (useClustering) { Dispatchers.Default.run { - cluster(chargers, zoom, clusterDistance) + cluster(chargers, zoom) } } else chargers return chargersClustered diff --git a/app/src/main/java/net/vonforst/evmap/ui/Clustering.kt b/app/src/main/java/net/vonforst/evmap/ui/Clustering.kt index 499b2002..72e54ccc 100644 --- a/app/src/main/java/net/vonforst/evmap/ui/Clustering.kt +++ b/app/src/main/java/net/vonforst/evmap/ui/Clustering.kt @@ -1,40 +1,37 @@ package net.vonforst.evmap.ui -import com.car2go.maps.model.LatLng -import com.google.maps.android.clustering.ClusterItem -import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm +import co.anbora.labs.spatia.geometry.Point import net.vonforst.evmap.model.ChargeLocation import net.vonforst.evmap.model.ChargeLocationCluster import net.vonforst.evmap.model.ChargepointListItem import net.vonforst.evmap.model.Coordinate +import net.vonforst.evmap.utils.SphericalMercatorProjection +import kotlin.math.pow +import kotlin.math.roundToInt + +fun getClusterPrecision(zoom: Float) = 30000000 / 2.0.pow(zoom.roundToInt() + 1) fun cluster( locations: List, - zoom: Float, - clusterDistance: Int + zoom: Float ): List { - val clusterItems = locations.map { ChargepointClusterItem(it) } - - val algo = NonHierarchicalDistanceBasedAlgorithm() - algo.maxDistanceBetweenClusteredItems = clusterDistance - algo.addItems(clusterItems) - return algo.getClusters(zoom).map { - if (it.size == 1) { - it.items.first().charger + val clusters = mutableMapOf, MutableSet>() + val precision = getClusterPrecision(zoom) + locations.forEach { + // snap coordinates to grid + val gridX = (it.coordinatesProjected.x / precision).roundToInt() + val gridY = (it.coordinatesProjected.y / precision).roundToInt() + clusters.getOrPut(gridX to gridY, ::mutableSetOf).add(it) + } + return clusters.map { + if (it.value.size == 1) { + it.value.first() } else { - ChargeLocationCluster( - it.size, - Coordinate(it.position.latitude, it.position.longitude), - it.items.map { it.charger }) + val centerX = it.value.map { it.coordinatesProjected.x }.average() + val centerY = it.value.map { it.coordinatesProjected.y }.average() + val centerLatLng = SphericalMercatorProjection.unproject(Point(centerX, centerY, 3857)) + ChargeLocationCluster(it.value.size, Coordinate(centerLatLng.y, centerLatLng.x)) } } -} - -private class ChargepointClusterItem(val charger: ChargeLocation) : ClusterItem { - override fun getSnippet(): String? = null - - override fun getTitle(): String = charger.name - - override fun getPosition(): LatLng = LatLng(charger.coordinates.lat, charger.coordinates.lng) } \ No newline at end of file diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt index c446e5e9..6fc2b3c1 100644 --- a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt +++ b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt @@ -411,10 +411,8 @@ class MapViewModel(application: Application, private val state: SavedStateHandle bounds.northeast.longitude ).map { it.charger } - val clusterDistance = getClusterDistance(mapPosition.zoom) - val chargersClustered = clusterDistance?.let { - cluster(chargers, mapPosition.zoom, clusterDistance) - } ?: chargers + // TODO: let DB do the clustering here + val chargersClustered = cluster(chargers, mapPosition.zoom) filteredConnectors.value = null filteredMinPower.value = null filteredChargeCards.value = null