Skip to content

Commit 41e4b94

Browse files
committed
[POC] Support on-the-fly LV95 reprojection
copying geoblocks/ol-maplibre-layer/ locally to be able to edit it and add the on the fly capabilities (will create a PR there when done) Making our coordinate system calculate resolution without threshold (especially for LV95) so that it can then be used to calculate a mercator zoom level
1 parent 2559300 commit 41e4b94

15 files changed

Lines changed: 517 additions & 66 deletions

src/config/map.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { WEBMERCATOR } from '@/utils/coordinates/coordinateSystems'
1+
import { LV95 } from '@/utils/coordinates/coordinateSystems'
22

33
/**
44
* Default projection to be used throughout the application
55
*
66
* @type {CoordinateSystem}
77
*/
8-
export const DEFAULT_PROJECTION = WEBMERCATOR
8+
export const DEFAULT_PROJECTION = LV95
99

1010
/**
1111
* Default tile size to use when requesting WMS tiles with our internal WMSs (512px)

src/modules/map/components/openlayers/OpenLayersVectorLayer.vue

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
* Most of the specific code found bellow, plus import of layer ID should be removed then.
1010
*/
1111
12-
import { MapLibreLayer } from '@geoblocks/ol-maplibre-layer'
1312
import { Source } from 'ol/source'
1413
import { computed, inject, toRefs, watch } from 'vue'
14+
import { useStore } from 'vuex'
1515
1616
import GeoAdminVectorLayer from '@/api/layers/GeoAdminVectorLayer.class'
17+
import MapLibreLayer from '@/modules/map/components/openlayers/utils/ol-maplibre-layer/MapLibreLayer'
1718
import useAddLayerToMap from '@/modules/map/components/openlayers/utils/useAddLayerToMap.composable'
19+
import SwissCoordinateSystem from '@/utils/coordinates/SwissCoordinateSystem.class'
1820
1921
const props = defineProps({
2022
vectorLayerConfig: {
@@ -32,6 +34,9 @@ const props = defineProps({
3234
})
3335
const { vectorLayerConfig, parentLayerOpacity, zIndex } = toRefs(props)
3436
37+
const store = useStore()
38+
const currentProjection = computed(() => store.state.position.projection)
39+
3540
// extracting useful info from what we've linked so far
3641
const layerId = computed(() => vectorLayerConfig.value.vectorStyleId)
3742
const opacity = computed(() => parentLayerOpacity.value ?? vectorLayerConfig.value.opacity)
@@ -47,6 +52,12 @@ const layer = new MapLibreLayer({
4752
source: new Source({
4853
attribution: [vectorLayerConfig.value.attribution],
4954
}),
55+
translateZoom: (zoom) => {
56+
if (currentProjection.value instanceof SwissCoordinateSystem) {
57+
return currentProjection.value.transformCustomZoomLevelToStandard(zoom)
58+
}
59+
return zoom
60+
},
5061
})
5162
5263
const olMap = inject('olMap')

src/modules/map/components/openlayers/OpenLayersWMTSLayer.vue

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,13 @@ function getTransformedXYZUrl() {
8585
function createTileGridForProjection() {
8686
const maxResolutionIndex = indexOfMaxResolution(projection.value, maxResolution.value)
8787
let resolutions = projection.value.getResolutions()
88-
let matrixIds = projection.value.getMatrixIds()
8988
if (resolutions.length > maxResolutionIndex) {
9089
resolutions = resolutions.slice(0, maxResolutionIndex + 1)
91-
matrixIds = matrixIds.slice(0, maxResolutionIndex + 1)
9290
}
9391
return new WMTSTileGrid({
94-
resolutions,
92+
resolutions: resolutions.map((resolution) => resolution.resolution),
9593
origin: projection.value.getTileOrigin(),
96-
matrixIds,
94+
matrixIds: resolutions.map((_, index) => index),
9795
extent: projection.value.bounds.flatten,
9896
})
9997
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { MapOptions, QueryRenderedFeaturesOptions } from 'maplibre-gl'
2+
import { Map as MapLibreMap } from 'maplibre-gl'
3+
import type { Map } from 'ol'
4+
import type { Options as LayerOptions } from 'ol/layer/Layer.js'
5+
import Layer from 'ol/layer/Layer.js'
6+
import type { EventsKey } from 'ol/events.js'
7+
import BaseEvent from 'ol/events/Event.js'
8+
import { unByKey } from 'ol/Observable.js'
9+
import { Source } from 'ol/source.js'
10+
import MapLibreLayerRenderer from './MapLibreLayerRenderer.js'
11+
import getMapLibreAttributions from './getMapLibreAttributions.js'
12+
13+
export type MapLibreOptions = Omit<MapOptions, 'container'>;
14+
15+
export type MapLibreLayerOptions = LayerOptions & {
16+
mapLibreOptions: MapLibreOptions;
17+
queryRenderedFeaturesOptions?: QueryRenderedFeaturesOptions;
18+
translateZoom?: Function
19+
};
20+
21+
export default class MapLibreLayer extends Layer {
22+
mapLibreMap?: MapLibreMap;
23+
24+
loaded: boolean = false;
25+
26+
private olListenersKeys: EventsKey[] = [];
27+
28+
constructor(options: MapLibreLayerOptions) {
29+
super({
30+
source: new Source({
31+
attributions: () => {
32+
return getMapLibreAttributions(this.mapLibreMap);
33+
},
34+
}),
35+
...options,
36+
});
37+
}
38+
39+
override disposeInternal() {
40+
unByKey(this.olListenersKeys);
41+
this.loaded = false;
42+
if (this.mapLibreMap) {
43+
// Some asynchronous repaints are triggered even if the MapLibreMap has been removed,
44+
// to avoid display of errors we set an empty function.
45+
this.mapLibreMap.triggerRepaint = () => {};
46+
this.mapLibreMap.remove();
47+
}
48+
super.disposeInternal();
49+
}
50+
51+
override setMapInternal(map: Map) {
52+
super.setMapInternal(map);
53+
if (map) {
54+
this.loadMapLibreMap();
55+
} else {
56+
// TODO: I'm not sure if it's the right call
57+
this.dispose();
58+
}
59+
}
60+
61+
private loadMapLibreMap() {
62+
this.loaded = false;
63+
const map = this.getMapInternal();
64+
if (map) {
65+
this.olListenersKeys.push(
66+
map.on('change:target', this.loadMapLibreMap.bind(this)),
67+
);
68+
}
69+
70+
if (!map?.getTargetElement()) {
71+
return;
72+
}
73+
74+
if (!this.getVisible()) {
75+
// On next change of visibility we load the map
76+
this.olListenersKeys.push(
77+
this.once('change:visible', this.loadMapLibreMap.bind(this)),
78+
);
79+
return;
80+
}
81+
82+
const container = document.createElement('div');
83+
container.style.position = 'absolute';
84+
container.style.width = '100%';
85+
container.style.height = '100%';
86+
87+
const mapLibreOptions = this.get('mapLibreOptions') as MapLibreOptions;
88+
89+
this.mapLibreMap = new MapLibreMap(
90+
Object.assign({}, mapLibreOptions, {
91+
container: container,
92+
attributionControl: false,
93+
interactive: false,
94+
trackResize: false,
95+
}),
96+
);
97+
98+
this.mapLibreMap.on('sourcedata', () => {
99+
this.getSource()?.refresh(); // Refresh attribution
100+
});
101+
102+
this.mapLibreMap.once('load', () => {
103+
this.loaded = true;
104+
this.dispatchEvent(new BaseEvent('load'));
105+
});
106+
}
107+
108+
override createRenderer(): MapLibreLayerRenderer {
109+
const translateZoom = this.get('translateZoom') as Function | undefined;
110+
return new MapLibreLayerRenderer(this, translateZoom);
111+
}
112+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import type { MapGeoJSONFeature, QueryRenderedFeaturesOptions } from 'maplibre-gl'
2+
import type { FrameState } from 'ol/Map.js'
3+
import { toDegrees } from 'ol/math.js'
4+
import { toLonLat } from 'ol/proj.js'
5+
import LayerRenderer from 'ol/renderer/Layer.js'
6+
import GeoJSON from 'ol/format/GeoJSON.js'
7+
import type { Coordinate } from 'ol/coordinate.js'
8+
import type { FeatureCallback } from 'ol/renderer/vector.js'
9+
import type { Feature } from 'ol'
10+
import type { Geometry } from 'ol/geom.js'
11+
import { SimpleGeometry } from 'ol/geom.js'
12+
import type { Pixel } from 'ol/pixel.js'
13+
import type MapLibreLayer from './MapLibreLayer.js'
14+
15+
const VECTOR_TILE_FEATURE_PROPERTY = 'vectorTileFeature';
16+
17+
const formats: {
18+
[key: string]: GeoJSON;
19+
} = {
20+
'EPSG:3857': new GeoJSON({
21+
featureProjection: 'EPSG:3857',
22+
}),
23+
};
24+
25+
/**
26+
* This class is a renderer for MapLibre Layer to be able to use the native ol
27+
* functionalities like map.getFeaturesAtPixel or map.hasFeatureAtPixel.
28+
*/
29+
export default class MapLibreLayerRenderer extends LayerRenderer<MapLibreLayer> {
30+
private readonly translateZoom: Function | undefined
31+
32+
constructor(layer: MapLibreLayer, translateZoom: Function | undefined) {
33+
super(layer)
34+
this.translateZoom = translateZoom
35+
}
36+
37+
getFeaturesAtCoordinate(
38+
coordinate: Coordinate | undefined,
39+
hitTolerance: number = 5
40+
): Feature<Geometry>[] {
41+
const pixels = this.getMapLibrePixels(coordinate, hitTolerance);
42+
43+
if (!pixels) {
44+
return [];
45+
}
46+
47+
const queryRenderedFeaturesOptions =
48+
(this.getLayer().get(
49+
'queryRenderedFeaturesOptions',
50+
) as QueryRenderedFeaturesOptions) || {};
51+
52+
// At this point we get GeoJSON MapLibre feature, we transform it to an OpenLayers
53+
// feature to be consistent with other layers.
54+
const features = this.getLayer()
55+
.mapLibreMap?.queryRenderedFeatures(pixels, queryRenderedFeaturesOptions)
56+
.map((feature) => {
57+
return this.toOlFeature(feature);
58+
});
59+
60+
return features || [];
61+
}
62+
63+
override prepareFrame(): boolean {
64+
return true;
65+
}
66+
67+
override renderFrame(frameState: FrameState): HTMLElement {
68+
const layer = this.getLayer();
69+
const {mapLibreMap} = layer;
70+
const map = layer.getMapInternal();
71+
if (!layer || !map || !mapLibreMap) {
72+
return null;
73+
}
74+
75+
const mapLibreCanvas = mapLibreMap.getCanvas();
76+
const {viewState} = frameState;
77+
// adjust view parameters in MapLibre
78+
mapLibreMap.jumpTo({
79+
center: toLonLat(viewState.center, viewState.projection) as [number, number],
80+
zoom: (this.translateZoom ? this.translateZoom(viewState.zoom) : viewState.zoom) - 1 ,
81+
bearing: toDegrees(-viewState.rotation),
82+
});
83+
84+
const opacity = layer.getOpacity().toString();
85+
if (mapLibreCanvas && opacity !== mapLibreCanvas.style.opacity) {
86+
mapLibreCanvas.style.opacity = opacity;
87+
}
88+
89+
if (!mapLibreCanvas.isConnected) {
90+
// The canvas is not connected to the DOM, request a map rendering at the next animation frame
91+
// to set the canvas size.
92+
map.render();
93+
} else if (!sameSize(mapLibreCanvas, frameState)) {
94+
mapLibreMap.resize();
95+
}
96+
97+
mapLibreMap.redraw();
98+
99+
return mapLibreMap.getContainer();
100+
}
101+
102+
override getFeatures(pixel: Pixel): Promise<Feature<Geometry>[]> {
103+
const coordinate = this.getLayer()
104+
.getMapInternal()
105+
?.getCoordinateFromPixel(pixel);
106+
return Promise.resolve(this.getFeaturesAtCoordinate(coordinate));
107+
}
108+
109+
override forEachFeatureAtCoordinate<Feature>(
110+
coordinate: Coordinate,
111+
_frameState: FrameState,
112+
hitTolerance: number,
113+
callback: FeatureCallback<Feature>,
114+
): Feature | undefined {
115+
const features = this.getFeaturesAtCoordinate(coordinate, hitTolerance);
116+
features.forEach((feature) => {
117+
const geometry = feature.getGeometry();
118+
if (geometry instanceof SimpleGeometry) {
119+
callback(feature, this.getLayer(), geometry);
120+
}
121+
});
122+
return features?.[0] as Feature;
123+
}
124+
125+
private getMapLibrePixels(
126+
coordinate?: Coordinate,
127+
hitTolerance?: number,
128+
): [[number, number], [number, number]] | [number, number] | undefined {
129+
if (!coordinate) {
130+
return undefined;
131+
}
132+
133+
const pixel = this.getLayer().mapLibreMap?.project(
134+
toLonLat(coordinate) as [number, number],
135+
);
136+
137+
if (pixel?.x === undefined || pixel?.y === undefined) {
138+
return undefined;
139+
}
140+
141+
let pixels: [[number, number], [number, number]] | [number, number] = [
142+
pixel.x,
143+
pixel.y,
144+
];
145+
146+
if (hitTolerance) {
147+
const [x, y] = pixels as [number, number];
148+
pixels = [
149+
[x - hitTolerance, y - hitTolerance],
150+
[x + hitTolerance, y + hitTolerance],
151+
];
152+
}
153+
return pixels;
154+
}
155+
156+
private toOlFeature(feature: MapGeoJSONFeature): Feature<Geometry> {
157+
const layer = this.getLayer();
158+
const map = layer.getMapInternal();
159+
160+
const projection =
161+
map?.getView()?.getProjection()?.getCode() || 'EPSG:3857';
162+
163+
if (!formats[projection]) {
164+
formats[projection] = new GeoJSON({
165+
featureProjection: projection,
166+
});
167+
}
168+
169+
const olFeature = formats[projection].readFeature(feature) as Feature;
170+
if (olFeature) {
171+
// We save the original MapLibre feature to avoid losing information
172+
// potentially needed for others functionalities like highlighting
173+
// (id, layer id, source, sourceLayer ...)
174+
olFeature.set(VECTOR_TILE_FEATURE_PROPERTY, feature, true);
175+
}
176+
return olFeature;
177+
}
178+
}
179+
180+
function sameSize(canvas: HTMLCanvasElement, frameState: FrameState): boolean {
181+
return (
182+
canvas.width === Math.floor(frameState.size[0] * frameState.pixelRatio) &&
183+
canvas.height === Math.floor(frameState.size[1] * frameState.pixelRatio)
184+
);
185+
}

0 commit comments

Comments
 (0)