diff --git a/package-lock.json b/package-lock.json
index b4cbffc67..2b30d38e5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,8 @@
"@geoblocks/ol-maplibre-layer": "^0.1.2",
"@ivanv/vue-collapse-transition": "^1.0.2",
"@popperjs/core": "^2.11.8",
+ "@turf/distance": "^6.5.0",
+ "@turf/helpers": "^6.5.0",
"animate.css": "^4.1.1",
"axios": "^1.6.2",
"bootstrap": "^5.3.2",
@@ -1000,6 +1002,37 @@
"node": ">= 10"
}
},
+ "node_modules/@turf/distance": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-6.5.0.tgz",
+ "integrity": "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg==",
+ "dependencies": {
+ "@turf/helpers": "^6.5.0",
+ "@turf/invariant": "^6.5.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
+ "node_modules/@turf/helpers": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
+ "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
+ "node_modules/@turf/invariant": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz",
+ "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==",
+ "dependencies": {
+ "@turf/helpers": "^6.5.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/turf"
+ }
+ },
"node_modules/@tweenjs/tween.js": {
"version": "21.0.0",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-21.0.0.tgz",
diff --git a/package.json b/package.json
index dcf595048..6664e342b 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,8 @@
"@geoblocks/ol-maplibre-layer": "^0.1.2",
"@ivanv/vue-collapse-transition": "^1.0.2",
"@popperjs/core": "^2.11.8",
+ "@turf/distance": "^6.5.0",
+ "@turf/helpers": "^6.5.0",
"animate.css": "^4.1.1",
"axios": "^1.6.2",
"bootstrap": "^5.3.2",
diff --git a/src/modules/infobox/components/FeatureList.vue b/src/modules/infobox/components/FeatureList.vue
index 4410e5a34..4b80be917 100644
--- a/src/modules/infobox/components/FeatureList.vue
+++ b/src/modules/infobox/components/FeatureList.vue
@@ -45,7 +45,7 @@ export default {
}
-
diff --git a/src/modules/map/components/cesium/CesiumMap.vue b/src/modules/map/components/cesium/CesiumMap.vue
index c9c732fae..9f35f94a0 100644
--- a/src/modules/map/components/cesium/CesiumMap.vue
+++ b/src/modules/map/components/cesium/CesiumMap.vue
@@ -91,7 +91,7 @@ import { ClickInfo, ClickType } from '@/store/modules/map.store'
import { UIModes } from '@/store/modules/ui.store'
import { WEBMERCATOR, WGS84 } from '@/utils/coordinates/coordinateSystems'
import CustomCoordinateSystem from '@/utils/coordinates/CustomCoordinateSystem.class'
-import { createGeoJSONFeature } from '@/utils/layerUtils'
+import { identifyGeoJSONFeatureAt } from '@/utils/identifyOnVectorLayer'
import log from '@/utils/logging'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import '@geoblocks/cesium-compass'
@@ -461,25 +461,19 @@ export default {
)
let objects = this.viewer.scene.drillPick(event.position)
- const geoJsonFeatures = {}
const kmlFeatures = {}
// if there is a GeoJSON layer currently visible, we will find it and search for features under the mouse cursor
this.visiblePrimitiveLayers
.filter((l) => l instanceof GeoAdminGeoJsonLayer)
.forEach((geoJSonLayer) => {
- objects
- .filter((obj) => obj.primitive?.olLayer?.get('id') === geoJSonLayer.getID())
- .forEach((obj) => {
- const feature = obj.primitive.olFeature
- if (!geoJsonFeatures[feature.getId()]) {
- geoJsonFeatures[feature.getId()] = createGeoJSONFeature(
- obj.primitive.olFeature,
- geoJSonLayer,
- feature.getGeometry()
- )
- }
- })
- features.push(...Object.values(geoJsonFeatures))
+ features.push(
+ ...identifyGeoJSONFeatureAt(
+ geoJSonLayer,
+ event.position,
+ this.projection,
+ this.resolution
+ )
+ )
})
this.visiblePrimitiveLayers
.filter((l) => l instanceof KMLLayer)
diff --git a/src/modules/map/components/common/mouse-click.composable.js b/src/modules/map/components/common/mouse-click.composable.js
index 27e07b3d6..75ed61de1 100644
--- a/src/modules/map/components/common/mouse-click.composable.js
+++ b/src/modules/map/components/common/mouse-click.composable.js
@@ -1,5 +1,6 @@
import LayerTypes from '@/api/layers/LayerTypes.enum'
import { ClickInfo, ClickType } from '@/store/modules/map.store'
+import { identifyGeoJSONFeatureAt, identifyKMLFeatureAt } from '@/utils/identifyOnVectorLayer'
import { computed } from 'vue'
import { useStore } from 'vuex'
@@ -18,6 +19,8 @@ export function useMouseOnMap() {
const visibleKMLLayers = computed(() =>
store.getters.visibleLayers.filter((layer) => layer.type === LayerTypes.KML)
)
+ const currentMapResolution = computed(() => store.getters.resolution)
+ const currentProjection = computed(() => store.state.position.projection)
/**
* @param {[Number, Number]} screenPosition
@@ -42,12 +45,26 @@ export function useMouseOnMap() {
if (!hasPointerDownTriggeredLocationPopup && isStillOnStartingPosition) {
const features = []
// if there is a GeoJSON layer currently visible, we will find it and search for features under the mouse cursor
- visibleGeoJsonLayers.value.forEach((_geoJSonLayer) => {
- // TODO: implements OpenLayers-free feature identification
+ visibleGeoJsonLayers.value.forEach((geoJSonLayer) => {
+ features.push(
+ ...identifyGeoJSONFeatureAt(
+ geoJSonLayer,
+ coordinate,
+ currentProjection.value,
+ currentMapResolution.value
+ )
+ )
})
// same for KML layers
- visibleKMLLayers.value.forEach((_kmlLayer) => {
- // TODO: implements OpenLayers-free feature identification
+ visibleKMLLayers.value.forEach((kmlLayer) => {
+ features.push(
+ ...identifyKMLFeatureAt(
+ kmlLayer.kmlData,
+ coordinate,
+ currentProjection.value,
+ currentMapResolution.value
+ )
+ )
})
store.dispatch(
'click',
diff --git a/src/store/modules/features.store.js b/src/store/modules/features.store.js
index a6259c62b..83a07ad9e 100644
--- a/src/store/modules/features.store.js
+++ b/src/store/modules/features.store.js
@@ -7,7 +7,7 @@ const getSelectedFeatureWithId = (state, featureId) => {
export default {
state: {
- /** @type Array */
+ /** @type Array */
selectedFeatures: [],
},
getters: {
@@ -22,7 +22,8 @@ export default {
* tells the store which features are selected (it does not select the features by itself)
*
* @param commit
- * @param {Feature[]} features A list of feature we want to highlight/select on the map
+ * @param {SelectableFeature[]} features A list of feature we want to highlight/select on
+ * the map
*/
setSelectedFeatures({ commit }, features) {
if (Array.isArray(features)) {
diff --git a/src/utils/geoJsonUtils.js b/src/utils/geoJsonUtils.js
index fddd1d2f7..02153b4d8 100644
--- a/src/utils/geoJsonUtils.js
+++ b/src/utils/geoJsonUtils.js
@@ -27,7 +27,7 @@ export default function reprojectGeoJsonData(geoJsonData, toProjection, fromProj
}
} else if (toProjection instanceof CoordinateSystem) {
// according to the IETF reference, if nothing is said about the projection used, it should be WGS84
- reprojectedGeoJSON = reproject(this.geojsonData, WGS84.epsg, toProjection.epsg)
+ reprojectedGeoJSON = reproject(geoJsonData, WGS84.epsg, toProjection.epsg)
}
return reprojectedGeoJSON
}
diff --git a/src/utils/identifyOnVectorLayer.js b/src/utils/identifyOnVectorLayer.js
new file mode 100644
index 000000000..86d3c61fa
--- /dev/null
+++ b/src/utils/identifyOnVectorLayer.js
@@ -0,0 +1,108 @@
+import { LayerFeature } from '@/api/features.api'
+import { WGS84 } from '@/utils/coordinates/coordinateSystems'
+import reprojectGeoJsonData from '@/utils/geoJsonUtils'
+import log from '@/utils/logging'
+import distance from '@turf/distance'
+import { point } from '@turf/helpers'
+import proj4 from 'proj4'
+
+const pixelToleranceForIdentify = 10
+
+/**
+ * Finds and returns all features, from the given GeoJSON layer, that are under or close to the
+ * given coordinate (we require the map resolution as input, so that we may calculate a 10-pixels
+ * tolerance for feature identification)
+ *
+ * This means we do not require OpenLayers to perform this search anymore, and that this code can be
+ * used in any mapping framework.
+ *
+ * @param {GeoAdminGeoJsonLayer} geoJsonLayer The GeoJSON layer in which we want to find feature at
+ * the given coordinate. This layer must have its geoJsonData loaded in order for this
+ * identification of feature to work properly (this function will not load the data if it is
+ * missing)
+ * @param {[Number, Number]} coordinate Where we want to find features ([x, y])
+ * @param {CoordinateSystem} projection The projection used to describe the coordinate where we want
+ * to search for feature
+ * @param {Number} resolution The current map resolution, in meters/pixel. Used to calculate a
+ * tolerance of 10 pixels around the given coordinate.
+ * @returns {SelectableFeature[]} The feature found at the coordinate, or an empty array if none
+ * were found
+ */
+export function identifyGeoJSONFeatureAt(geoJsonLayer, coordinate, projection, resolution) {
+ const features = []
+ // if there is a GeoJSON layer currently visible, we will find it and search for features under the mouse cursor
+ const coordinateWGS84 = point(proj4(projection.epsg, WGS84.epsg, coordinate))
+ // to use turf functions, we need to have lat/lon (WGS84) coordinates
+ const reprojectedGeoJSON = reprojectGeoJsonData(geoJsonLayer.geoJsonData, WGS84, projection)
+ if (!reprojectedGeoJSON) {
+ log.error(
+ `Unable to reproject GeoJSON data in order to find features at coordinates`,
+ geoJsonLayer.getID(),
+ coordinate
+ )
+ return []
+ }
+ const matchingFeatures = reprojectedGeoJSON.features
+ .filter((feature) => {
+ const distanceWithClick = distance(
+ coordinateWGS84,
+ point(feature.geometry.coordinates),
+ {
+ units: 'meters',
+ }
+ )
+ return distanceWithClick <= pixelToleranceForIdentify * resolution
+ })
+ .map((feature) => {
+ // back to the starting projection
+ feature.geometry.coordinates = proj4(
+ WGS84.epsg,
+ projection.epsg,
+ feature.geometry.coordinates
+ )
+ return new LayerFeature(
+ geoJsonLayer,
+ feature.id,
+ feature.properties.station_name || feature.id,
+ ``,
+ proj4(WGS84.epsg, projection.epsg, feature.geometry.coordinates),
+ null,
+ feature.geometry
+ )
+ })
+ if (matchingFeatures?.length > 0) {
+ features.push(...matchingFeatures)
+ }
+ return features
+}
+
+/**
+ * Finds and returns all features, from the given KML layer, that are under or close to the given
+ * coordinate (we require the map resolution as input, so that we may calculate a 10-pixels
+ * tolerance for feature identification)
+ *
+ * This means we do not require OpenLayers to perform this search anymore, and that this code can be
+ * used in any mapping framework.
+ *
+ * @param {KMLLayer} _kmlLayer The KML layer in which we want to find feature at the given
+ * coordinate. This layer must have its kmlData loaded in order for this identification of feature
+ * to work properly (this function will not load the data if it is missing)
+ * @param {[Number, Number]} _coordinate Where we want to find features ([x, y])
+ * @param {CoordinateSystem} _projection The projection used to describe the coordinate where we
+ * want to search for feature
+ * @param {Number} _resolution The current map resolution, in meters/pixel. Used to calculate a
+ * tolerance of 10 pixels around the given coordinate.
+ * @returns {SelectableFeature[]} The feature found at the coordinate, or an empty array if none
+ * were found
+ */
+export function identifyKMLFeatureAt(_kmlLayer, _coordinate, _projection, _resolution) {
+ // TODO : implement KML layer feature identification
+ return []
+}
diff --git a/src/utils/layerUtils.js b/src/utils/layerUtils.js
index 3566e6331..80fe871eb 100644
--- a/src/utils/layerUtils.js
+++ b/src/utils/layerUtils.js
@@ -1,7 +1,5 @@
-import { YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA } from '@/api/layers/LayerTimeConfigEntry.class'
import GeoAdminWMTSLayer from '@/api/layers/GeoAdminWMTSLayer.class'
-import { LayerFeature } from '@/api/features.api'
-import log from '@/utils/logging'
+import { YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA } from '@/api/layers/LayerTimeConfigEntry.class'
export class ActiveLayerConfig {
/**
@@ -43,37 +41,3 @@ export function getTimestampFromConfig(config, previewYear) {
}
return config instanceof GeoAdminWMTSLayer ? null : ''
}
-
-/**
- * Describes a GeoJSON feature from the backend
- *
- * For GeoJSON features, there's a catch as they only provide us with the inner tooltip content we
- * have to wrap it around the "usual" wrapper from the backend (not very fancy but otherwise the
- * look and feel is different from a typical backend tooltip)
- *
- * @param feature
- * @param geoJsonLayer
- * @param [geometry]
- * @returns {LayerFeature}
- */
-export function createGeoJSONFeature(feature, geoJsonLayer, geometry) {
- const featureGeometry = feature.getGeometry()
- const geoJsonFeature = new LayerFeature(
- geoJsonLayer,
- geoJsonLayer.getID(),
- geoJsonLayer.name,
- ``,
- featureGeometry.flatCoordinates,
- featureGeometry.getExtent(),
- geometry
- )
- log.debug('GeoJSON feature found', geoJsonFeature)
- return geoJsonFeature
-}