Add example for dynamic clusters
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
"jsts": false,
|
"jsts": false,
|
||||||
"JSZip": false,
|
"JSZip": false,
|
||||||
"mapboxgl": false,
|
"mapboxgl": false,
|
||||||
|
"monotoneChainConvexHull": false,
|
||||||
"NumpyLoader": false,
|
"NumpyLoader": false,
|
||||||
"saveAs": false,
|
"saveAs": false,
|
||||||
"toastr": false,
|
"toastr": false,
|
||||||
|
|||||||
16
examples/clusters-dynamic.html
Normal file
16
examples/clusters-dynamic.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
layout: example.html
|
||||||
|
title: Dynamic clusters
|
||||||
|
shortdesc: Clusters with zoom-to-cluster, hull view, and uncluttering of overlapping features.
|
||||||
|
docs: >
|
||||||
|
<p>This example shows open data of subsidized photovoltaic installations in Vienna. Different style functions
|
||||||
|
are used for cluster display, single feature display, convex hull of a cluster, and an expanded view of
|
||||||
|
overlapping features. Hovering over a cluster shows its convex hull. Clicking on a cluster zooms to the
|
||||||
|
extent of the contained features. Clicking on a cluster consisting of features that are very close to each other reveals an expanded view
|
||||||
|
of the features, along a circle around the cluster.</p>
|
||||||
|
<p>Features are styled differently depending on the power of the photovoltaic installation.</p>
|
||||||
|
tags: "marker, cluster, vector, style, convex hull"
|
||||||
|
resources:
|
||||||
|
- https://unpkg.com/monotone-chain-convex-hull@1.0.0/lib/index.js
|
||||||
|
---
|
||||||
|
<div id="map" class="map"></div>
|
||||||
278
examples/clusters-dynamic.js
Normal file
278
examples/clusters-dynamic.js
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import Feature from '../src/ol/Feature.js';
|
||||||
|
import GeoJSON from '../src/ol/format/GeoJSON.js';
|
||||||
|
import Map from '../src/ol/Map.js';
|
||||||
|
import View from '../src/ol/View.js';
|
||||||
|
import {
|
||||||
|
Circle as CircleStyle,
|
||||||
|
Fill,
|
||||||
|
Icon,
|
||||||
|
Stroke,
|
||||||
|
Style,
|
||||||
|
Text,
|
||||||
|
} from '../src/ol/style.js';
|
||||||
|
import {Cluster, Vector as VectorSource, XYZ} from '../src/ol/source.js';
|
||||||
|
import {LineString, Point, Polygon} from '../src/ol/geom.js';
|
||||||
|
import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
|
||||||
|
import {createEmpty, extend, getWidth} from '../src/ol/extent.js';
|
||||||
|
import {fromLonLat} from '../src/ol/proj.js';
|
||||||
|
|
||||||
|
const circleDistanceMultiplier = 1;
|
||||||
|
const circleFootSeparation = 28;
|
||||||
|
const circleStartAngle = Math.PI / 2;
|
||||||
|
|
||||||
|
const convexHullFill = new Fill({
|
||||||
|
color: 'rgba(255, 153, 0, 0.4)',
|
||||||
|
});
|
||||||
|
const convexHullStroke = new Stroke({
|
||||||
|
color: 'rgba(204, 85, 0, 1)',
|
||||||
|
width: 1.5,
|
||||||
|
});
|
||||||
|
const outerCircleFill = new Fill({
|
||||||
|
color: 'rgba(255, 153, 102, 0.3)',
|
||||||
|
});
|
||||||
|
const innerCircleFill = new Fill({
|
||||||
|
color: 'rgba(255, 165, 0, 0.7)',
|
||||||
|
});
|
||||||
|
const textFill = new Fill({
|
||||||
|
color: '#fff',
|
||||||
|
});
|
||||||
|
const textStroke = new Stroke({
|
||||||
|
color: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
width: 3,
|
||||||
|
});
|
||||||
|
const innerCircle = new CircleStyle({
|
||||||
|
radius: 14,
|
||||||
|
fill: innerCircleFill,
|
||||||
|
});
|
||||||
|
const outerCircle = new CircleStyle({
|
||||||
|
radius: 20,
|
||||||
|
fill: outerCircleFill,
|
||||||
|
});
|
||||||
|
const darkIcon = new Icon({
|
||||||
|
src: 'data/icons/emoticon-cool.svg',
|
||||||
|
});
|
||||||
|
const lightIcon = new Icon({
|
||||||
|
src: 'data/icons/emoticon-cool-outline.svg',
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single feature style, users for clusters with 1 feature and cluster circles.
|
||||||
|
* @param {Feature} clusterMember A feature from a cluster.
|
||||||
|
* @return {Style} An icon style for the cluster member's location.
|
||||||
|
*/
|
||||||
|
function clusterMemberStyle(clusterMember) {
|
||||||
|
return new Style({
|
||||||
|
geometry: clusterMember.getGeometry(),
|
||||||
|
image: clusterMember.get('LEISTUNG') > 5 ? darkIcon : lightIcon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let clickFeature, clickResolution;
|
||||||
|
/**
|
||||||
|
* Style for clusters with features that are too close to each other, activated on click.
|
||||||
|
* @param {Feature} cluster A cluster with overlapping members.
|
||||||
|
* @param {number} resolution The current view resolution.
|
||||||
|
* @return {Style} A style to render an expanded view of the cluster members.
|
||||||
|
*/
|
||||||
|
function clusterCircleStyle(cluster, resolution) {
|
||||||
|
if (cluster !== clickFeature || resolution !== clickResolution) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clusterMembers = cluster.get('features');
|
||||||
|
const centerCoordinates = cluster.getGeometry().getCoordinates();
|
||||||
|
return generatePointsCircle(
|
||||||
|
clusterMembers.length,
|
||||||
|
cluster.getGeometry().getCoordinates(),
|
||||||
|
resolution
|
||||||
|
).reduce((styles, coordinates, i) => {
|
||||||
|
const point = new Point(coordinates);
|
||||||
|
const line = new LineString([centerCoordinates, coordinates]);
|
||||||
|
styles.unshift(
|
||||||
|
new Style({
|
||||||
|
geometry: line,
|
||||||
|
stroke: convexHullStroke,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
styles.push(
|
||||||
|
clusterMemberStyle(
|
||||||
|
new Feature({
|
||||||
|
...clusterMembers[i].getProperties(),
|
||||||
|
geometry: point,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return styles;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From
|
||||||
|
* https://github.com/Leaflet/Leaflet.markercluster/blob/31360f2/src/MarkerCluster.Spiderfier.js#L55-L72
|
||||||
|
* Arranges points in a circle around the cluster center, with a line pointing from the center to
|
||||||
|
* each point.
|
||||||
|
* @param {number} count Number of cluster members.
|
||||||
|
* @param {Array<number>} clusterCenter Center coordinate of the cluster.
|
||||||
|
* @param {number} resolution Current view resolution.
|
||||||
|
* @return {Array<Array<number>>} An array of coordinates representing the cluster members.
|
||||||
|
*/
|
||||||
|
function generatePointsCircle(count, clusterCenter, resolution) {
|
||||||
|
const circumference =
|
||||||
|
circleDistanceMultiplier * circleFootSeparation * (2 + count);
|
||||||
|
let legLength = circumference / (Math.PI * 2); //radius from circumference
|
||||||
|
const angleStep = (Math.PI * 2) / count;
|
||||||
|
const res = [];
|
||||||
|
let angle;
|
||||||
|
|
||||||
|
legLength = Math.max(legLength, 35) * resolution; // Minimum distance to get outside the cluster icon.
|
||||||
|
|
||||||
|
for (let i = 0; i < count; ++i) {
|
||||||
|
// Clockwise, like spiral.
|
||||||
|
angle = circleStartAngle + i * angleStep;
|
||||||
|
res.push([
|
||||||
|
clusterCenter[0] + legLength * Math.cos(angle),
|
||||||
|
clusterCenter[1] + legLength * Math.sin(angle),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hoverFeature;
|
||||||
|
/**
|
||||||
|
* Style for convex hulls of clusters, activated on hover.
|
||||||
|
* @param {Feature} cluster The cluster feature.
|
||||||
|
* @return {Style} Polygon style for the convex hull of the cluster.
|
||||||
|
*/
|
||||||
|
function clusterHullStyle(cluster) {
|
||||||
|
if (cluster !== hoverFeature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const originalFeatures = cluster.get('features');
|
||||||
|
const points = originalFeatures.map((feature) =>
|
||||||
|
feature.getGeometry().getCoordinates()
|
||||||
|
);
|
||||||
|
return new Style({
|
||||||
|
geometry: new Polygon([monotoneChainConvexHull(points)]),
|
||||||
|
fill: convexHullFill,
|
||||||
|
stroke: convexHullStroke,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clusterStyle(feature) {
|
||||||
|
const size = feature.get('features').length;
|
||||||
|
if (size > 1) {
|
||||||
|
return [
|
||||||
|
new Style({
|
||||||
|
image: outerCircle,
|
||||||
|
}),
|
||||||
|
new Style({
|
||||||
|
image: innerCircle,
|
||||||
|
text: new Text({
|
||||||
|
text: size.toString(),
|
||||||
|
fill: textFill,
|
||||||
|
stroke: textStroke,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
const originalFeature = feature.get('features')[0];
|
||||||
|
return clusterMemberStyle(originalFeature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const vectorSource = new VectorSource({
|
||||||
|
format: new GeoJSON(),
|
||||||
|
url: 'data/geojson/photovoltaic.json',
|
||||||
|
});
|
||||||
|
|
||||||
|
const clusterSource = new Cluster({
|
||||||
|
attributions:
|
||||||
|
'Data: <a href="https://www.data.gv.at/auftritte/?organisation=stadt-wien">Stadt Wien</a>',
|
||||||
|
distance: 35,
|
||||||
|
source: vectorSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layer displaying the convex hull of the hovered cluster.
|
||||||
|
const clusterHulls = new VectorLayer({
|
||||||
|
source: clusterSource,
|
||||||
|
style: clusterHullStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layer displaying the clusters and individual features.
|
||||||
|
const clusters = new VectorLayer({
|
||||||
|
source: clusterSource,
|
||||||
|
style: clusterStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Layer displaying the expanded view of overlapping cluster members.
|
||||||
|
const clusterCircles = new VectorLayer({
|
||||||
|
source: clusterSource,
|
||||||
|
style: clusterCircleStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
const raster = new TileLayer({
|
||||||
|
source: new XYZ({
|
||||||
|
attributions:
|
||||||
|
'Base map: <a target="_blank" href="https://basemap.at/">basemap.at</a>',
|
||||||
|
url: 'https://maps{1-4}.wien.gv.at/basemap/bmapgrau/normal/google3857/{z}/{y}/{x}.png',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const map = new Map({
|
||||||
|
layers: [raster, clusterHulls, clusters, clusterCircles],
|
||||||
|
target: 'map',
|
||||||
|
view: new View({
|
||||||
|
center: [0, 0],
|
||||||
|
zoom: 2,
|
||||||
|
maxZoom: 19,
|
||||||
|
extent: [
|
||||||
|
...fromLonLat([16.1793, 48.1124]),
|
||||||
|
...fromLonLat([16.5559, 48.313]),
|
||||||
|
],
|
||||||
|
showFullExtent: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on('pointermove', (event) => {
|
||||||
|
clusters.getFeatures(event.pixel).then((features) => {
|
||||||
|
if (features[0] !== hoverFeature) {
|
||||||
|
// Display the convex hull on hover.
|
||||||
|
hoverFeature = features[0];
|
||||||
|
clusterHulls.setStyle(clusterHullStyle);
|
||||||
|
// Change the cursor style to indicate that the cluster is clickable.
|
||||||
|
map.getTargetElement().style.cursor =
|
||||||
|
hoverFeature && hoverFeature.get('features').length > 1
|
||||||
|
? 'pointer'
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on('click', (event) => {
|
||||||
|
clusters.getFeatures(event.pixel).then((features) => {
|
||||||
|
if (features.length > 0) {
|
||||||
|
const clusterMembers = features[0].get('features');
|
||||||
|
if (clusterMembers.length > 1) {
|
||||||
|
// Calculate the extent of the cluster members.
|
||||||
|
const extent = createEmpty();
|
||||||
|
clusterMembers.forEach((feature) =>
|
||||||
|
extend(extent, feature.getGeometry().getExtent())
|
||||||
|
);
|
||||||
|
const view = map.getView();
|
||||||
|
const resolution = map.getView().getResolution();
|
||||||
|
if (
|
||||||
|
view.getZoom() === view.getMaxZoom() ||
|
||||||
|
(getWidth(extent) < resolution && getWidth(extent) < resolution)
|
||||||
|
) {
|
||||||
|
// Show an expanded view of the cluster members.
|
||||||
|
clickFeature = features[0];
|
||||||
|
clickResolution = resolution;
|
||||||
|
clusterCircles.setStyle(clusterCircleStyle);
|
||||||
|
} else {
|
||||||
|
// Zoom to the extent of the cluster members.
|
||||||
|
view.fit(extent, {duration: 500, padding: [50, 50, 50, 50]});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
1
examples/data/geojson/photovoltaic.json
Normal file
1
examples/data/geojson/photovoltaic.json
Normal file
File diff suppressed because one or more lines are too long
1
examples/data/icons/emoticon-cool-outline.svg
Normal file
1
examples/data/icons/emoticon-cool-outline.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M19,10C19,11.38 16.88,12.5 15.5,12.5C14.12,12.5 12.75,11.38 12.75,10H11.25C11.25,11.38 9.88,12.5 8.5,12.5C7.12,12.5 5,11.38 5,10H4.25C4.09,10.64 4,11.31 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12C20,11.31 19.91,10.64 19.75,10H19M12,4C9.04,4 6.45,5.61 5.07,8H18.93C17.55,5.61 14.96,4 12,4M22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2A10,10 0 0,1 22,12M12,17.23C10.25,17.23 8.71,16.5 7.81,15.42L9.23,14C9.68,14.72 10.75,15.23 12,15.23C13.25,15.23 14.32,14.72 14.77,14L16.19,15.42C15.29,16.5 13.75,17.23 12,17.23Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 812 B |
1
examples/data/icons/emoticon-cool.svg
Normal file
1
examples/data/icons/emoticon-cool.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3.22,7.22C4.91,4.11 8.21,2 12,2C15.79,2 19.09,4.11 20.78,7.22L20,8H4L3.22,7.22M21.4,8.6C21.78,9.67 22,10.81 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12C2,10.81 2.22,9.67 2.6,8.6L4,10H5C5,11.38 7.12,12.5 8.5,12.5C9.88,12.5 11.25,11.38 11.25,10H12.75C12.75,11.38 14.12,12.5 15.5,12.5C16.88,12.5 19,11.38 19,10H20L21.4,8.6M16.19,15.42L14.77,14C14.32,14.72 13.25,15.23 12,15.23C10.75,15.23 9.68,14.72 9.23,14L7.81,15.42C8.71,16.5 10.25,17.23 12,17.23C13.75,17.23 15.29,16.5 16.19,15.42Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 775 B |
Reference in New Issue
Block a user