Commit 43e7c349 authored by Florian Schäfer's avatar Florian Schäfer

Cluster the markers of the WikiLayer where there are many markers

parent fb534a86
...@@ -4,13 +4,17 @@ package org.wikipedia.gui; ...@@ -4,13 +4,17 @@ package org.wikipedia.gui;
import java.awt.BasicStroke; import java.awt.BasicStroke;
import java.awt.Color; import java.awt.Color;
import java.awt.Graphics2D; import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point; import java.awt.Point;
import java.awt.RenderingHints; import java.awt.RenderingHints;
import java.awt.geom.Arc2D; import java.awt.geom.Arc2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.OptionalDouble;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.swing.Action; import javax.swing.Action;
import javax.swing.Icon; import javax.swing.Icon;
...@@ -29,17 +33,22 @@ import org.wikipedia.tools.WikiProperties; ...@@ -29,17 +33,22 @@ import org.wikipedia.tools.WikiProperties;
public class WikiLayer extends Layer implements ListDataListener { public class WikiLayer extends Layer implements ListDataListener {
private static final Icon LAYER_ICON = new ImageProvider("w").setMaxSize(ImageProvider.ImageSizes.LAYER).get(); private static final Icon LAYER_ICON = new ImageProvider("w").setMaxSize(ImageProvider.ImageSizes.LAYER).get();
private static final Color MARKER_FILL_COLOR = new Color(153, 0, 0, 180); private static final Color MARKER_FILL_COLOR = new Color(200, 9, 9, 180);
private static final Color MARKER_STROKE_COLOR = new Color(255, 255, 255); private static final Color MARKER_STROKE_COLOR = new Color(255, 255, 255);
private static final Color MARKER_FILL_SELECTED_COLOR = new Color(45, 204, 123, 180);
private static final Color MARKER_STROKE_SELECTED_COLOR = new Color(255, 255, 0);
private static final double MIN_MARKER_HEIGHT = 10.0; private static final double MIN_MARKER_HEIGHT = 10.0;
private static double markerHeight = Math.max(MIN_MARKER_HEIGHT, WikiProperties.WIKI_LAYER_MARKER_HEIGHT.get()); private static double markerHeight;
static { static {
WikiProperties.WIKI_LAYER_MARKER_HEIGHT.addListener(it -> markerHeight = Math.max(MIN_MARKER_HEIGHT, it.getProperty().get())); WikiProperties.WIKI_LAYER_MARKER_HEIGHT.addListener(it -> setMarkerHeight(it.getProperty().get()));
setMarkerHeight(WikiProperties.WIKI_LAYER_MARKER_HEIGHT.get());
} }
private final WikipediaToggleDialog wikiDialog; private final WikipediaToggleDialog wikiDialog;
public WikiLayer(final WikipediaToggleDialog wikiDialog) { public WikiLayer(final WikipediaToggleDialog wikiDialog) {
super("WikiLayer"); super("WikiLayer");
this.wikiDialog = wikiDialog; this.wikiDialog = wikiDialog;
...@@ -47,6 +56,18 @@ public class WikiLayer extends Layer implements ListDataListener { ...@@ -47,6 +56,18 @@ public class WikiLayer extends Layer implements ListDataListener {
wikiDialog.list.addListSelectionListener(it -> invalidate()); wikiDialog.list.addListSelectionListener(it -> invalidate());
} }
private static double getMarkerWidth() {
return markerHeight / 3 * 2;
}
private static double getMarkerHeight() {
return markerHeight;
}
private static void setMarkerHeight(final double markerHeight) {
WikiLayer.markerHeight = Math.max(MIN_MARKER_HEIGHT, markerHeight);
}
@Override @Override
public Icon getIcon() { public Icon getIcon() {
return LAYER_ICON; return LAYER_ICON;
...@@ -93,28 +114,49 @@ public class WikiLayer extends Layer implements ListDataListener { ...@@ -93,28 +114,49 @@ public class WikiLayer extends Layer implements ListDataListener {
bbox.extend(mv.getLatLon(minPoint.getX() - 10, minPoint.getY() + 30)); bbox.extend(mv.getLatLon(minPoint.getX() - 10, minPoint.getY() + 30));
final Collection<WikipediaEntry> selectedEntries = wikiDialog.list.getSelectedValuesList(); final Collection<WikipediaEntry> selectedEntries = wikiDialog.list.getSelectedValuesList();
final Collection<Point> entriesInBbox = Collections.list(wikiDialog.model.elements()).parallelStream() final Collection<Collection<Point>> entriesInBbox = Collections.list(wikiDialog.model.elements()).parallelStream()
.filter(it -> it.coordinate != null && bbox.contains(it.coordinate) && !selectedEntries.contains(it)) .filter(it -> it.coordinate != null && bbox.contains(it.coordinate) && !selectedEntries.contains(it))
.map(it -> mv.getPoint(it.coordinate)) .map(it -> mv.getPoint(it.coordinate))
.collect(Collectors.toList()); .collect(new WikiLayerClusteringCollector(getMarkerWidth(), getMarkerHeight()));
paintWikiMarkers(g, entriesInBbox, false); paintWikiMarkers(g, entriesInBbox, false);
paintWikiMarkers(g, selectedEntries.stream().map(it -> mv.getPoint(it.coordinate)).collect(Collectors.toList()), true); paintWikiMarkers(g, selectedEntries.stream().map(it -> Collections.singleton(mv.getPoint(it.coordinate))).collect(Collectors.toList()), true);
} }
private void paintWikiMarkers(final Graphics2D g, final Collection<Point> points, final boolean selected) { private void paintWikiMarkers(final Graphics2D g, final Collection<Collection<Point>> clusters, final boolean selected) {
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g.setStroke(new BasicStroke(selected ? 3 : 2)); g.setStroke(new BasicStroke(selected ? 3 : 2));
for (final Point point: points) { for (final Collection<Point> cluster: clusters) {
final Path2D path = new Path2D.Double(); if (cluster.size() <= 1) {
path.moveTo(point.getX(), point.getY()); final Point point = cluster.iterator().next();
path.append(new Arc2D.Double(point.getX() - markerHeight / 3, point.getY() - markerHeight, markerHeight / 3 * 2, markerHeight / 3 * 2, -30, 240.0, Arc2D.OPEN), true); final Path2D path = new Path2D.Double();
path.closePath(); path.moveTo(point.getX(), point.getY());
path.append(new Arc2D.Double(point.getX() - markerHeight / 3, point.getY() - markerHeight, markerHeight / 3 * 2, markerHeight / 3 * 2, -30, 240.0, Arc2D.OPEN), true);
g.setColor(MARKER_FILL_COLOR); path.closePath();
g.fill(path);
g.setColor(selected ? Color.YELLOW : MARKER_STROKE_COLOR); g.setColor(selected ? MARKER_FILL_SELECTED_COLOR : MARKER_FILL_COLOR);
g.draw(path); g.fill(path);
g.setColor(selected ? MARKER_STROKE_SELECTED_COLOR : MARKER_STROKE_COLOR);
g.draw(path);
} else {
OptionalDouble avgX = cluster.stream().mapToDouble(Point::getX).average();
OptionalDouble avgY = cluster.stream().mapToDouble(Point::getY).average();
avgX.ifPresent(x -> avgY.ifPresent(y -> {
g.setColor(selected ? MARKER_STROKE_SELECTED_COLOR : MARKER_STROKE_COLOR);
final Ellipse2D ellipse = new Ellipse2D.Double(x - getMarkerHeight() / 2, y - getMarkerHeight() / 2, getMarkerHeight(), getMarkerHeight());
g.fill(ellipse);
g.setColor(selected ? MARKER_FILL_SELECTED_COLOR : MARKER_FILL_COLOR);
g.draw(ellipse);
g.setFont(g.getFont().deriveFont((float) getMarkerWidth() * .5f));
final String label = cluster.size() + "";
final int labelWidth = g.getFontMetrics().stringWidth(label);
g.drawString(
label,
(int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, x - labelWidth / 2)),
(int) Math.max(Integer.MIN_VALUE, Math.min(Integer.MAX_VALUE, y + getMarkerWidth() * .2))
);
}));
}
} }
} }
......
// License: GPL. For details, see LICENSE file.
package org.wikipedia.gui;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import org.openstreetmap.josm.tools.Pair;
public class WikiLayerClusteringCollector implements Collector<Point, Map<Pair<Integer, Integer>, Collection<Point>>, Collection<Collection<Point>>> {
private final int gridWidth;
private final int gridHeight;
WikiLayerClusteringCollector(final double markerWidth, final double markerHeight) {
this.gridWidth = (int) Math.min(Integer.MAX_VALUE, 3 * markerWidth);
this.gridHeight = (int) Math.min(Integer.MAX_VALUE, 3 * markerHeight);
}
@Override
public Supplier<Map<Pair<Integer, Integer>, Collection<Point>>> supplier() {
return ConcurrentHashMap::new;
}
@Override
public BiConsumer<Map<Pair<Integer, Integer>, Collection<Point>>, Point> accumulator() {
return (result, p) -> {
final Pair<Integer, Integer> gridPos = Pair.create(p.x / gridWidth, p.y / gridHeight);
if (!result.containsKey(gridPos)) {
result.put(gridPos, new ArrayList<>());
}
result.get(gridPos).add(p);
};
}
@Override
public BinaryOperator<Map<Pair<Integer, Integer>, Collection<Point>>> combiner() {
return (a, b) -> {
for (final Map.Entry<Pair<Integer, Integer>, Collection<Point>> bEntry : b.entrySet()) {
if (a.containsKey(bEntry.getKey())) {
a.get(bEntry.getKey()).addAll(bEntry.getValue());
} else {
a.put(bEntry.getKey(), bEntry.getValue());
}
}
return a;
};
}
@Override
public Function<Map<Pair<Integer, Integer>, Collection<Point>>, Collection<Collection<Point>>> finisher() {
return Map::values;
}
@Override
public Set<Characteristics> characteristics() {
final HashSet<Characteristics> characteristics = new HashSet<>(2);
Collections.addAll(characteristics, Characteristics.UNORDERED);
return characteristics;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment