diff --git a/src/components/map/Map.tsx b/src/components/map/Map.tsx index 48749e0..a30f00b 100644 --- a/src/components/map/Map.tsx +++ b/src/components/map/Map.tsx @@ -52,6 +52,7 @@ import { DepartureBoard } from '@suid/icons-material'; import deTileVectorSource from '../../lib/de-tile-vector-source'; import ClusterableVectorTileSource from '../../lib/ClusterableVectorTileSource'; import clusterableVectorTileSourceProxy from '../../lib/ClusterableVectorTileSourceProxy'; +import ClusterableVectorTileSourceProxy from '../../lib/ClusterableVectorTileSourceProxy'; const [getState, setState] = createSignal({ lon: 0, @@ -278,96 +279,9 @@ const Map: Component = () => { // // 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 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, - // }); - // }); - - // 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(); - // } - // }); - - const vectorTileSource = new ClusterableVectorTileSource({ + const vectorTileSource = new VectorTileSource({ url: 'https://geo.dyomedea.com/services/spain/tiles/{z}/{x}/{y}.pbf', + format: new MVT({ featureClass: Feature }), maxZoom: 14, }); @@ -377,65 +291,55 @@ const Map: Component = () => { declutter: false, }); - // const clusterableVectorTileSource = - // clusterableVectorTileSourceProxy(vectorTileSource); + const clusterableVectorTileSource = new ClusterableVectorTileSourceProxy({ + source: vectorTileSource, + }); - // clusterableVectorTileSource.init({ - // featureFilter: (feature: Feature, resolution?: number) => - // feature.get('type') === 'poi', - // }); + const clusterSource = new Cluster({ + source: clusterableVectorTileSource, + distance: 4000000, + //minDistance: 200, + geometryFunction: (feature: Feature) => { + // console.log({ + // caller: 'Map / Cluster / geometryFunction', + // feature, + // }); + if (feature.get('sub-type') === 'tourism') { + 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', + // point, + // features, + // }); + + return new Feature({ + geometry: new Point( + toLonLat( + olExtent.getCenter(point.getExtent()), + olMap.getView().getProjection() + ) + ), + // point, + features: features, + type: 'cluster', + }); + }, + }); let clusterLayer = new VectorLayer({ - // source: vectorTileMirrorSource, - source: new Cluster({ - // source: deTileVectorSource(vectorLayer.getSource()), - source: vectorTileSource, - 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', - // point, - // features, - // }); - - //return features[0]; - - return new Feature({ - geometry: new Point( - toLonLat( - olExtent.getCenter(point.getExtent()), - olMap.getView().getProjection() - ) - ), - // point, - features: features, - type: 'cluster', - }); - }, - // distance: 100000, - // minDistance: 0, - }), + source: clusterSource, zIndex: Infinity, // style, style: function (feature) { @@ -445,17 +349,11 @@ const Map: Component = () => { // }); return new Style({ image: new Circle({ - radius: feature.get('features').length * 5, + radius: 20, fill: new Fill({ color: 'black' }), }), }); }, - // style: new Style({ - // image: new Circle({ - // radius: 20, - // fill: new Fill({ color: 'black' }), - // }), - // }), }); olMap.addLayer(vectorTileLayer); diff --git a/src/lib/ClusterableVectorTileSourceProxy.ts b/src/lib/ClusterableVectorTileSourceProxy.ts index 9f44c08..b82fb5e 100644 --- a/src/lib/ClusterableVectorTileSourceProxy.ts +++ b/src/lib/ClusterableVectorTileSourceProxy.ts @@ -1,142 +1,106 @@ -import { cloneDeep } from 'lodash'; -import { Feature } from 'ol'; import { equals, Extent } from 'ol/extent'; import { fromLonLat, Projection } from 'ol/proj'; import VectorTileSource from 'ol/source/VectorTile.js'; -const CLUSTER_PROPS = '[cluster props]'; - -type FeatureFilter = ( - feature: Feature, - resolution: number | undefined -) => boolean; - -const passAllFeatureFilter = (feature: Feature, resolution?: number) => true; - -interface Props { - extent: Extent | null; - resolution?: number; - extentProjected: Extent | null; - featuresSent: boolean; - featureFilter: FeatureFilter; -} - interface Options { - featureFilter?: FeatureFilter; + source: VectorTileSource; } -const initialProps = { - extent: null, - extentProjected: null, - featuresSent: false, - featureFilter: passAllFeatureFilter, -}; +class ClusterableVectorTileSourceProxy { + extent?: Extent; + resolution?: number; + extentProjected?: Extent; + featuresSent: boolean = false; + proxy?: typeof Proxy; -const getProps = (vectorTileSource: VectorTileSource) => - vectorTileSource.get(CLUSTER_PROPS) as Props; - -const setProps = (vectorTileSource: VectorTileSource, props: Props) => - vectorTileSource.set(CLUSTER_PROPS, props); - -const overloads = { - init: (target: VectorTileSource) => { - return (options?: Options) => { - let props = cloneDeep(initialProps); - if (options) { - props = { ...props, ...options }; - } - setProps(target, props); - }; - }, - - loadFeatures: (target: VectorTileSource) => { - return (extent: Extent, resolution: number, projection: Projection) => { - let props = getProps(target); - props.resolution = resolution; - if (props.extent === null || !equals(props.extent, extent)) { - props.extent = extent; - if (target.getProjection() != null) { - props.extentProjected = fromLonLat( - extent.slice(0, 2), - target.getProjection() - ).concat(fromLonLat(extent.slice(2), props.projection)); + overloads = { + loadFeatures: (target: VectorTileSource) => { + return (extent: Extent, resolution: number, projection: Projection) => { + this.resolution = resolution; + if (this.extent === undefined || !equals(this.extent, extent)) { + this.extent = extent; + if (target.getProjection() != null) { + this.extentProjected = fromLonLat( + extent.slice(0, 2), + target.getProjection() + ).concat(fromLonLat(extent.slice(2), this.projection)); + } } - } - if (!props.featuresSent) { - target.dispatchEvent('change'); - } - props.featuresSent = false; - setProps(target, props); - // console.log({ - // caller: 'clusterableVectorTileSourceProxy', - // overload: 'loadFeatures', - // extent, - // resolution, - // projection, - // target: target, - // props, - // }); - }; - }, - - getFeatures: (target: VectorTileSource) => { - return () => { - let props = getProps(target); - const result = ( - props.extentProjected !== null - ? target.getFeaturesInExtent(props.extentProjected) - : [] - ).filter((feature) => props.featureFilter(feature, props.resolution)); - //console.trace(); - props.featuresSent = true; - setProps(target, props); - // console.log({ - // caller: 'clusterableVectorTileSourceProxy', - // overload: 'getFeatures', - // result, - // props, - // target, - // }); - return result; - }; - }, - - addEventListener: (target: VectorTileSource) => { - return (type: string, listener: any) => { - console.log({ - caller: 'clusterableVectorTileSourceProxy', - overload: 'addEventListener', - type, - listener, - target, - }); - target.addEventListener(type, listener); - if (type === 'change') { - target.addEventListener('tileloadend', listener); - } - }; - }, -}; - -const clusterableVectorTileSourceProxy = (vectorTileSource: VectorTileSource) => - new Proxy(vectorTileSource, { - get: (target, prop, receiver) => { - let result; - if (prop in overloads) { - result = overloads[prop as keyof typeof overloads](target); - } else { - result = Reflect.get(target, prop, receiver); - } - // console.log({ - // caller: 'clusterableVectorTileSourceProxy', - // trap: 'get', - // target, - // prop, - // receiver, - // result, - // }); - return result; + if (!this.featuresSent) { + target.dispatchEvent('change'); + } + this.featuresSent = false; + // console.log({ + // caller: 'clusterableVectorTileSourceProxy', + // overload: 'loadFeatures', + // extent, + // resolution, + // projection, + // target: target, + // this, + // }); + }; }, - }); -export default clusterableVectorTileSourceProxy; + getFeatures: (target: VectorTileSource) => { + return () => { + const result = + this.extentProjected !== undefined + ? target.getFeaturesInExtent(this.extentProjected) + : []; + //console.trace(); + this.featuresSent = true; + // console.log({ + // caller: 'clusterableVectorTileSourceProxy', + // overload: 'getFeatures', + // result, + // this, + // target, + // }); + return result; + }; + }, + + addEventListener: (target: VectorTileSource) => { + return (type: string, listener: any) => { + console.log({ + caller: 'clusterableVectorTileSourceProxy', + overload: 'addEventListener', + type, + listener, + target, + }); + target.addEventListener(type, listener); + if (type === 'change') { + target.addEventListener('tileloadend', listener); + } + }; + }, + }; + + constructor(options: Options) { + const { source } = options; + this.proxy = new Proxy(source, { + get: (target, prop, receiver) => { + let result; + if (prop in this.overloads) { + result = this.overloads[prop as keyof typeof this.overloads](target); + } else { + result = Reflect.get(target, prop, receiver); + } + // console.log({ + // caller: 'clusterableVectorTileSourceProxy', + // trap: 'get', + // target, + // prop, + // receiver, + // result, + // }); + return result; + }, + }); + return this.proxy; + } +} + +export default ClusterableVectorTileSourceProxy;