import { Component, createEffect, createSignal, onMount } from 'solid-js'; import { useParams, useNavigate } from '@solidjs/router'; import OlMap from 'ol/Map'; import View from 'ol/View'; import TileLayer from 'ol/layer/Tile'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import Feature from 'ol/Feature'; import Attribution from 'ol/control/Attribution'; import Rotate from 'ol/control/Rotate'; import ScaleLine from 'ol/control/ScaleLine'; import Control from 'ol/control/Control'; import { toLonLat, useGeographic as olUseGeographic } from 'ol/proj'; import DragRotate from 'ol/interaction/DragRotate'; import 'ol/ol.css'; import './Map.css'; import { Collection, VectorTile } from 'ol'; import { Point } from 'ol/geom'; import { Style, Icon, Circle, Fill } from 'ol/style'; import GetLocation, { getCurrentLocation } from '../get-location'; import ShowLocationIcon from '../get-location/ShowLocationIcon.svg'; import { Back, Forward } from '../back-forward'; import GpxImport from '../gpx-import'; import AllGpxes from '../all-gpxes'; import MapTileProvider, { mapTileProviders } from '../map-tile-provider'; import Interaction from 'ol/interaction/Interaction'; import DoubleClickZoom from 'ol/interaction/DoubleClickZoom'; import DragPan from 'ol/interaction/DragPan'; import PinchRotate from 'ol/interaction/PinchRotate'; import PinchZoom from 'ol/interaction/PinchZoom'; import KeyboardPan from 'ol/interaction/KeyboardPan'; import KeyboardZoom from 'ol/interaction/KeyboardZoom'; import MouseWheelZoom from 'ol/interaction/MouseWheelZoom'; import Cluster from 'ol/source/Cluster.js'; import * as olExtent from 'ol/extent'; import DragZoom from 'ol/interaction/DragZoom'; import Infos, { clickHandler } from '../infos'; import GpxDialog from '../gpx-dialog'; import GpxRecord from '../gpx-record'; import dispatch from '../../workers/dispatcher-main'; import { debounce, floor } from 'lodash'; import { AndroidFullScreen } from '@awesome-cordova-plugins/android-full-screen'; import Account from '../account'; import OsmFetch from '../osm-fetch'; import VectorTileLayer from 'ol/layer/VectorTile'; 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'; import clusterableVectorTileSourceProxy from '../../lib/ClusterableVectorTileSourceProxy'; const [getState, setState] = createSignal({ lon: 0, lat: 0, rotation: 0, zoom: 0, provider: 'osm', }); export { getState }; const [getMap, setMap] = createSignal(null); export { getMap }; const Map: Component = () => { const navigate = useNavigate(); const params = useParams(); // See https://stackoverflow.com/questions/71288670/how-to-make-fullscreen-ionic-capacitor-app AndroidFullScreen.isImmersiveModeSupported() .then(() => AndroidFullScreen.immersiveMode()) .catch(console.warn); if (window.plugins) { window.plugins.intentShim.registerBroadcastReceiver( { filterActions: ['android.intent.action.VIEW'], }, function (intent: any) { console.log({ caller: 'Intent broadcast receiver', intent, }); } ); window.plugins.intentShim.onIntent(function (intent: any) { console.log({ caller: 'Intent receiver', intent }); const url = new URL(intent.data); const q = url.search; const [, lat, lon] = q.match(/q=([0-9.]+),([0-9.]+)/); const zoom = Math.min(18, getState().zoom); navigate( `/map/${getState().provider}/${lon}/${lat}/${zoom}/${ getState().rotation }` ); }); } else { console.log({ caller: 'Intent', message: "window.plugins doesn't exist", window, }); } if ( params.lat === '0' && params.lon === '0' && params.provider === 'osm' && params.rotation === '0' && params.zoom === '2' ) { dispatch({ action: 'getState' }, (error, state) => { if (state !== null) { console; navigate( `/map/${state.provider}/${state.lon}/${state.lat}/${state.zoom}/${state.rotation}` ); } }); } let target: HTMLDivElement; const debouncedDbSetState = debounce((state: any) => { console.log({ caller: 'Map / debouncedDbSetState', state }); dispatch({ action: 'setState', params: state }); }, 60000); createEffect(async () => { console.log({ caller: 'Map / setState', params: { ...params, }, }); setState({ provider: params.provider, lon: +params.lon, lat: +params.lat, rotation: +params.rotation, zoom: +params.zoom, }); debouncedDbSetState(getState()); const map = getMap(); const layers = map?.getLayers(); const tileLayer = layers?.item(0) as TileLayer | undefined; if (tileLayer?.get('provider') !== params.provider) { tileLayer?.set('provider', params.provider, true); tileLayer?.setSource(mapTileProviders[params.provider].source); } const view = map?.getView(); view?.animate({ center: [getState().lon, getState().lat], rotation: getState().rotation, zoom: getState().zoom, duration: 1000, }); }); createEffect(() => { const location = getCurrentLocation(); if (location) { console.log({ caller: 'Map / updateLocation', location, }); const source = getMap() ?.getAllLayers() .at(1) ?.getSource() as VectorSource; source!.clear(true); const point = new Point([location.lon, location.lat]); const style = new Style({ image: new Icon({ // size: [20, 20], imgSize: [24, 24], declutterMode: 'obstacle', // @ts-ignore src: ShowLocationIcon, }), }); const feature = new Feature({ geometry: point, // labelPoint: point, // name: 'current location', style: style, }); feature.set('type', 'current-location'); feature.setStyle(style); source.addFeature(feature); // source.changed(); console.log({ caller: 'Map / updateLocation', location, source, style, feature, }); } }); onMount(async () => { olUseGeographic(); const changeListener = (event: any) => { const map = getMap(); const view = map?.getView(); const center = view?.getCenter(); if (center) { navigate( `/map/${getState().provider}/${center[0]}/${ center[1] }/${view?.getZoom()}/${view?.getRotation()}` ); } console.log({ caller: 'Map / changeListener', event, params: { ...params, }, map, }); }; const tileLayer = new TileLayer({ source: mapTileProviders[params.provider].source, }); tileLayer.set('provider', params.provider, true); const vectorLayer = new VectorLayer({ source: new VectorSource(), zIndex: Infinity, }); // console.log({ // caller: 'Map / projections', // vector: vectorLayer.getSource()?.getProjection(), // vectorTile: clusterableVectorTileSource.getProjection(), // }); const olMap = new OlMap({ view: new View({ center: [+getState().lon, +getState().lat], zoom: +getState().zoom, rotation: +getState().rotation, }), layers: [tileLayer, vectorLayer], target: target, controls: new Collection([ new Attribution({ collapsible: true }), new Rotate(), new ScaleLine({ bar: true }), ]), moveTolerance: 10, interactions: new Collection([ new DragRotate(), new DoubleClickZoom(), new DragPan(), new PinchRotate(), new PinchZoom(), new KeyboardPan(), new KeyboardZoom(), new MouseWheelZoom(), new DragZoom(), ]), }); olMap.on(['moveend'], changeListener); olMap.on(['singleclick'], clickHandler); // // 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 VectorTileSource({ url: 'https://geo.dyomedea.com/services/spain/tiles/{z}/{x}/{y}.pbf', format: new MVT({ featureClass: Feature }), maxZoom: 14, }); const vectorTileLayer = new VectorTileLayer({ source: vectorTileSource, style: [], declutter: false, }); const clusterableVectorTileSource = clusterableVectorTileSourceProxy(vectorTileSource); clusterableVectorTileSource.init({ featureFilter: (feature: Feature, resolution?: number) => feature.get('type') === 'poi', }); let clusterLayer = new VectorLayer({ // source: vectorTileMirrorSource, source: new Cluster({ // 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', // 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, }), zIndex: Infinity, // style, style: function (feature) { // console.log({ // caller: 'Map / Cluster / style', // feature, // }); return new Style({ image: new Circle({ radius: feature.get('features').length * 5, fill: new Fill({ color: 'black' }), }), }); }, // style: new Style({ // image: new Circle({ // radius: 20, // fill: new Fill({ color: 'black' }), // }), // }), }); olMap.addLayer(vectorTileLayer); olMap.addLayer(clusterLayer); setMap(olMap); // console.log({ // caller: 'Map / projections', // olMap, // projections: { // map: olMap.getView().getProjection(), // vectorSource: vectorLayer.getSource()?.getProjection(), // clusterSource: clusterLayer.getSource().getProjection(), // clusterSourceSource: clusterLayer // .getSource() // ?.getSource() // .getProjection(), // }, // }); }); return ( // @ts-ignore
); }; export default Map;