diff --git a/cSpell.json b/cSpell.json index b9c8591..90e11da 100644 --- a/cSpell.json +++ b/cSpell.json @@ -1,3 +1,11 @@ { - "overrides": [{ "filename": "**/fr.*", "language": "fr" }] + "overrides": [ + { + "filename": "**/fr.*", + "language": "fr" + } + ], + "words": [ + "Clusterable" + ] } diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index bf0b8fb..0d5b006 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -49,6 +49,8 @@ import VectorTileSource from 'ol/source/VectorTile.js'; import MVT from 'ol/format/MVT.js'; import style from '../gpx/styles'; import { DepartureBoard } from '@suid/icons-material'; +import deTileVectorSource from '../../lib/de-tile-vector-source'; +import ClusterableVectorTileSource from '../../lib/ClusterableVectorTileSource'; const [getState, setState] = createSignal({ lon: 0, @@ -238,8 +240,7 @@ const Map: Component = () => { zIndex: Infinity, }); - const vectorTileSource = new VectorTileSource({ - format: new MVT({ featureClass: Feature }), + const clusterableVectorTileSource = new ClusterableVectorTileSource({ url: 'https://geo.dyomedea.com/services/spain/tiles/{z}/{x}/{y}.pbf', maxZoom: 14, }); @@ -247,11 +248,11 @@ const Map: Component = () => { console.log({ caller: 'Map / projections', vector: vectorLayer.getSource()?.getProjection(), - vectorTile: vectorTileSource.getProjection(), + vectorTile: clusterableVectorTileSource.getProjection(), }); const vectorTileLayer = new VectorTileLayer({ - source: vectorTileSource, + source: clusterableVectorTileSource, style: [], declutter: false, }); @@ -262,7 +263,7 @@ const Map: Component = () => { zoom: +getState().zoom, rotation: +getState().rotation, }), - layers: [tileLayer, vectorTileLayer, vectorLayer], + layers: [tileLayer, vectorLayer, vectorTileLayer], target: target, controls: new Collection([ new Attribution({ collapsible: true }), @@ -285,114 +286,123 @@ const Map: Component = () => { olMap.on(['moveend'], changeListener); olMap.on(['singleclick'], clickHandler); - // cf https://stackoverflow.com/questions/55161380/openlayers-cluster-with-mvt-vectortilesource-impossible + // // cf https://stackoverflow.com/questions/55161380/openlayers-cluster-with-mvt-vectortilesource-impossible - const vectorTileMirrorSource = new VectorSource(); - let featuresForZ: Feature[] = []; - let viewZ = vectorTileLayer - .getSource() - .getTileGrid() - .getZForResolution(olMap.getView().getResolution()); + // const vectorTileMirrorSource = new VectorSource(); + // let featuresForZ: Feature[] = []; + // let viewZ = vectorTileLayer + // .getSource() + // .getTileGrid() + // .getZForResolution(olMap.getView().getResolution()); - const vectorMirrorRefresh = () => { - console.log({ - caller: 'Map / Cluster / vectorMirrorRefresh', - olMap, - vectorMirrorLayer, - viewZ, - featuresForZ, - }); - vectorTileMirrorSource.clear(); - if (featuresForZ[viewZ]) { - vectorTileMirrorSource.addFeatures(featuresForZ[viewZ]); - // vectorMirrorLayer.getSource()?.refresh(); - } - //vectorMirrorLayer.changed(); - }; + // const vectorMirrorRefresh = () => { + // console.log({ + // caller: 'Map / Cluster / vectorMirrorRefresh', + // olMap, + // vectorMirrorLayer, + // viewZ, + // featuresForZ, + // }); + // vectorTileMirrorSource.clear(); + // if (featuresForZ[viewZ]) { + // vectorTileMirrorSource.addFeatures(featuresForZ[viewZ]); + // // vectorMirrorLayer.getSource()?.refresh(); + // } + // //vectorMirrorLayer.changed(); + // }; - vectorTileLayer.getSource()?.on('tileloadend', (evt) => { - const z = evt.tile.getTileCoord()[0]; - // const features = evt.tile.getFeatures(); - // features.forEach((feature: Feature) => { - // feature.setId(undefined); - // }); - const features = evt.tile - .getFeatures() - .filter((feature: Feature) => feature.get('type') === 'poi') - .map((feature: Feature) => { - const center = olExtent.getCenter(feature.getGeometry().getExtent()); - const centerLonLat = toLonLat( - center, - olMap.getView().getProjection() - ); - const newFeature = feature.clone(); - newFeature.setGeometry(new Point(centerLonLat)); - // console.log({ - // caller: 'Map / Cluster / on tileloadend / new feature', - // feature, - // center, - // centerLonLat, - // newFeature, - // }); - return newFeature; - }); - if (!Array.isArray(featuresForZ[z])) { - featuresForZ[z] = []; - } - featuresForZ[z] = featuresForZ[z].concat(features); - // evt.tile.setFeatures([]); - if (z === viewZ) { - vectorMirrorRefresh(); - } - console.log({ - caller: 'Map / Cluster / on tileloadend', - olMap, - z, - viewZ, - features, - vectorMirrorLayer, - featuresForZ, - }); - }); + // vectorTileLayer.getSource()?.on('tileloadend', (evt) => { + // const z = evt.tile.getTileCoord()[0]; + // // const features = evt.tile.getFeatures(); + // // features.forEach((feature: Feature) => { + // // feature.setId(undefined); + // // }); + // const features = evt.tile + // .getFeatures() + // .filter((feature: Feature) => feature.get('type') === 'poi') + // .map((feature: Feature) => { + // const center = olExtent.getCenter(feature.getGeometry().getExtent()); + // const centerLonLat = toLonLat( + // center, + // olMap.getView().getProjection() + // ); + // const newFeature = feature.clone(); + // newFeature.setGeometry(new Point(centerLonLat)); + // // console.log({ + // // caller: 'Map / Cluster / on tileloadend / new feature', + // // feature, + // // center, + // // centerLonLat, + // // newFeature, + // // }); + // return newFeature; + // }); + // if (!Array.isArray(featuresForZ[z])) { + // featuresForZ[z] = []; + // } + // featuresForZ[z] = featuresForZ[z].concat(features); + // // evt.tile.setFeatures([]); + // if (z === viewZ) { + // vectorMirrorRefresh(); + // } + // console.log({ + // caller: 'Map / Cluster / on tileloadend', + // olMap, + // z, + // viewZ, + // features, + // vectorMirrorLayer, + // featuresForZ, + // }); + // }); - olMap.getView().on('change:resolution', function () { - // use VT features from the tile z level corresponding to view resolution - const newZ = vectorTileLayer - .getSource() - .getTileGrid() - .getZForResolution(olMap.getView().getResolution()); - console.log({ - caller: 'Map / Cluster / on change:resolution', - olMap, - newZ, - viewZ, - vectorMirrorLayer, - featuresForZ, - }); - if (newZ !== viewZ) { - viewZ = newZ; - vectorMirrorRefresh(); - } - }); + // olMap.getView().on('change:resolution', function () { + // // use VT features from the tile z level corresponding to view resolution + // const newZ = vectorTileLayer + // .getSource() + // .getTileGrid() + // .getZForResolution(olMap.getView().getResolution()); + // console.log({ + // caller: 'Map / Cluster / on change:resolution', + // olMap, + // newZ, + // viewZ, + // vectorMirrorLayer, + // featuresForZ, + // }); + // if (newZ !== viewZ) { + // viewZ = newZ; + // vectorMirrorRefresh(); + // } + // }); - let vectorMirrorLayer = new VectorLayer({ + let clusterLayer = new VectorLayer({ // source: vectorTileMirrorSource, source: new Cluster({ - source: vectorTileMirrorSource, - // geometryFunction: (feature: Feature) => { - // // console.log({ - // // caller: 'Map / Cluster / geometryFunction', - // // feature, - // // }); - // // test data is linestrings - // // return new Point( - // // olExtent.getCenter(feature.getGeometry().getExtent()) - // // ); - // if (feature.get('type') === 'poi') { - // return feature.getGeometry(); - // } - // return null; - // }, + // source: deTileVectorSource(vectorLayer.getSource()), + source: clusterableVectorTileSource, + geometryFunction: (feature: Feature) => { + // console.log({ + // caller: 'Map / Cluster / geometryFunction', + // feature, + // }); + // test data is linestrings + // return new Point( + // olExtent.getCenter(feature.getGeometry().getExtent()) + // ); + if (feature.get('type') === 'poi') { + return new Point( + olExtent.getCenter(feature.getGeometry().getExtent()) + ); + // return new Point( + // toLonLat( + // olExtent.getCenter(feature.getGeometry().getExtent()), + // olMap.getView().getProjection() + // ) + // ); + } + return null; + }, createCluster: (point: Point, features: Feature[]) => { // console.log({ // caller: 'Map / Cluster / createCluster', @@ -400,18 +410,24 @@ const Map: Component = () => { // features, // }); - // return features[0]; + //return features[0]; return new Feature({ - geometry: point, + geometry: new Point( + toLonLat( + olExtent.getCenter(point.getExtent()), + olMap.getView().getProjection() + ) + ), features: features, + type: 'cluster', }); }, - distance: 100, - minDistance: 10, + distance: 100000, + minDistance: 0, }), zIndex: Infinity, - style, + // style, style: function (feature) { // console.log({ // caller: 'Map / Cluster / style', @@ -431,19 +447,25 @@ const Map: Component = () => { // }), // }), }); - olMap.addLayer(vectorMirrorLayer); + olMap.addLayer(clusterLayer); + + + + setMap(olMap); console.log({ caller: 'Map / projections', olMap, - map: olMap.getView().getProjection(), - vectorSource: vectorLayer.getSource()?.getProjection(), - vectorTileMirrorSource: vectorTileMirrorSource.getProjection(), - vectorTileSource: vectorTileSource.getProjection(), - clusterSource: vectorMirrorLayer.getSource()?.getProjection(), + projections: { + map: olMap.getView().getProjection(), + vectorSource: vectorLayer.getSource()?.getProjection(), + clusterSource: clusterLayer.getSource().getProjection(), + clusterSourceSource: clusterLayer + .getSource() + ?.getSource() + .getProjection(), + }, }); - - setMap(olMap); }); return ( diff --git a/src/lib/ClusterableVectorTileSource.ts b/src/lib/ClusterableVectorTileSource.ts new file mode 100644 index 0000000..63be463 --- /dev/null +++ b/src/lib/ClusterableVectorTileSource.ts @@ -0,0 +1,112 @@ +import { Feature } from 'ol'; +import { equals, Extent } from 'ol/extent'; +import MVT from 'ol/format/MVT'; +import { fromLonLat, Projection } from 'ol/proj'; +import VectorTileSource from 'ol/source/VectorTile.js'; + +class ClusterableVectorTileSource extends VectorTileSource { + extent: Extent | null = null; + extentProjected: Extent | null = null; + featuresSent: boolean = false; + + changeHandler = () => { + this.featuresSent = false; + console.log({ + caller: 'ClusterableVectorTileSource', + method: 'changeHandler', + this: this, + }); + }; + + constructor(properties: any) { + properties.format = new MVT({ featureClass: Feature }); + super(properties); + this.addEventListener('change', this.changeHandler); + console.log({ + caller: 'ClusterableVectorTileSource', + method: 'constructor', + this: this, + }); + } + + loadFeatures = ( + extent: Extent, + resolution: number, + projection: Projection + ) => { + console.log({ + caller: 'ClusterableVectorTileSource', + method: 'loadFeatures', + extent, + resolution, + projection, + this: this, + }); + if (this.extent === null || !equals(this.extent, extent)) { + this.extent = extent; + if (this.projection != null) { + this.extentProjected = fromLonLat( + extent.slice(0, 2), + this.projection + ).concat(fromLonLat(extent.slice(2), this.projection)); + } + } + if (!this.featuresSent) { + super.dispatchEvent('change'); + } + this.featuresSent = false; + }; + + getFeatures = () => { + const result = + this.extentProjected !== null + ? super.getFeaturesInExtent(this.extentProjected) + : []; + console.log({ + caller: 'ClusterableVectorTileSource', + method: 'getFeatures', + result, + this: this, + }); + //console.trace(); + this.featuresSent = true; + return result; + }; + + getFeaturesInExtent = (extent: Extent) => { + const features = super.getFeaturesInExtent(extent); + console.log({ + caller: 'ClusterableVectorTileSource', + method: 'getFeaturesInExtent', + extent, + features, + this: this, + }); + return features; + // if (this.projection === null) { + // return super.getFeaturesInExtent(extent); + // } + // return super.getFeaturesInExtent( + // (this.extentProjected = fromLonLat( + // extent.slice(0, 2), + // this.projection + // ).concat(fromLonLat(extent.slice(2), this.projection))) + // ); + }; + + addEventListener = (type: string, listener: any) => { + console.log({ + caller: 'ClusterableVectorTileSource', + method: 'addEventListener', + type, + listener, + this: this, + }); + super.addEventListener(type, listener); + if (type === 'change') { + super.addEventListener('tileloadend', listener); + } + }; +} + +export default ClusterableVectorTileSource;