Add example for dynamic clusters
This commit is contained in:
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]});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user