UNPKG

@dhis2/gis-api

Version:

Maps API for DHIS2 based on Leaflet

242 lines (199 loc) 6.48 kB
import L from 'leaflet' import { scaleLog } from 'd3-scale' import clusterMarker from './ClusterMarker' import circleMarker from './CircleMarker' import polygon from './Polygon' import layerMixin from './layerMixin' import { toLatLngBounds, toLngLatBounds } from '../utils/geometry' export const ServerCluster = L.GridLayer.extend({ ...layerMixin, options: { pane: 'markerPane', tileSize: 512, clusterSize: 110, opacity: 1, domain: [1, 10000], range: [16, 40], }, initialize(opts) { const options = L.setOptions(this, { ...opts, pane: opts.id, ...(opts.bounds && {bounds: toLatLngBounds(opts.bounds)}) }) this._clusters = L.featureGroup() // Clusters shown on map this._tileClusters = {} // Cluster cache this._scale = scaleLog() .base(Math.E) .domain(options.domain) .range(options.range) .clamp(true) this._clusters.on('click', this.onClusterClick, this) }, onAdd(map) { this._levels = {} this._tiles = {} this._resetView() this._update() map.addLayer(this._clusters) map.on('zoomstart', this._onZoomStart, this) }, onRemove(map) { this._clusters.clearLayers() map.removeLayer(this._clusters) map.off('zoomstart', this._onZoomStart, this) }, // Load/add clusters within tile bounds createTile(coords) { const tileId = this._tileCoordsToKey(coords) const clusters = this._tileClusters[tileId] if (clusters) { // Add from cache clusters.forEach(cluster => this._clusters.addLayer(cluster)) return } const options = this.options const map = this._map const bounds = this._tileCoordsToBounds(coords) const params = { tileId, bbox: bounds.toBBoxString(), clusterSize: Math.round( this.getResolution(coords.z) * options.clusterSize ), includeClusterPoints: map.getZoom() === map.getMaxZoom(), } if (options.load && this._isWithinWorldBounds(bounds)) { options.load(params, L.bind(this.addClusters, this), this) } }, _addTile(coords) { const key = this._tileCoordsToKey(coords) this._tiles[key] = { coords, current: true, } this.createTile(this._wrapCoords(coords)) this.fire('tileloadstart', { key, coords, }) }, onClusterClick(evt) { const marker = evt.layer const map = this._map if (marker.getBounds) { // Is cluster if (map.getZoom() !== map.getMaxZoom()) { // Zoom to cluster bounds map.fitBounds(toLatLngBounds(marker.getBounds())) } else { // Spiderify on last zoom if (this._spider) { this._spider.unspiderify() } this._spider = marker.spiderify() } } else if (this.options.onClick) { // Is single marker const { type, latlng } = evt const coordinates = [latlng.lng, latlng.lat] const { feature } = marker this.options.onClick({ type, coordinates, feature }) } }, // Add clusters for one tile addClusters(tileId, clusters) { const tileClusters = [] clusters.forEach(d => { const cluster = this.createCluster(d) if (this._tiles[tileId]) { // If tile still present this._clusters.addLayer(cluster) } tileClusters.push(cluster) }) this._tileClusters[tileId] = tileClusters }, // Create cluster or circle marker createCluster(feature) { const { options } = this let marker if (feature.properties.point_count === 1) { marker = feature.geometry.type === 'Point' ? circleMarker(feature, options) : polygon(feature, options) } else { feature.properties.size = this._scale(feature.properties.point_count) marker = clusterMarker(feature, this.options) } return marker }, // Meters per pixel getResolution(zoom) { return ( (Math.PI * L.Projection.SphericalMercator.R * 2) / 256 / Math.pow(2, zoom) ) }, // Returns bounds for all clusters getBounds() { const bounds = this.options.bounds ? L.latLngBounds(this.options.bounds) : this._clusters.getBounds() if (bounds.isValid()) { return toLngLatBounds(bounds) } }, // Set opacity for all clusters and circle markers setOpacity(opacity) { const tileClusters = this._tileClusters let tileId let layer for (tileId in tileClusters) { // eslint-disable-line if (tileClusters.hasOwnProperty(tileId)) { for (layer of tileClusters[tileId]) { layer.setOpacity(opacity) } } } this.options.opacity = opacity }, // Remove clusters in tile _removeTile(key) { const tile = this._tiles[key] if (!tile) { return } const clusters = this._tileClusters[key] if (clusters) { clusters.forEach(layer => this._clusters.removeLayer(layer)) } delete this._tiles[key] this.fire('tileunload', { tileId: key, coords: this._keyToTileCoords(key), }) }, // Remove cluster on zoom change _onZoomStart() { this._clusters.clearLayers() }, // Disable zoom animation _animateZoom() {}, // We somtimes get cluster bounds outside valid range if requests are fired before the map dom el is properly sized _isWithinWorldBounds(bounds) { return ( bounds.getWest() >= -180 && bounds.getEast() <= 180 && bounds.getSouth() >= -90 && bounds.getNorth() <= 90 ) }, }) export default function serverCluster(options) { return new ServerCluster(options) }