Skip to content

Commit

Permalink
Handle identification of GeoJSON feature without the help of OpenLayers
Browse files Browse the repository at this point in the history
this way we can re-use the same logic for Cesium (and will then be able to use the mouse-click Composable when we rewrite the CesiumMap component into Composition API)
  • Loading branch information
pakb committed Dec 4, 2023
1 parent f052d2d commit de1a797
Show file tree
Hide file tree
Showing 9 changed files with 182 additions and 63 deletions.
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions src/modules/infobox/components/FeatureList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default {
}
</script>

<style lang="scss">
<style lang="scss" scoped>
@import 'src/scss/media-query.mixin';
.feature-list {
Expand Down Expand Up @@ -82,18 +82,18 @@ export default {
}
// Styling for external HTML content
.htmlpopup-container {
:global(.htmlpopup-container) {
width: 100%;
font-size: 11px;
text-align: start;
}
.htmlpopup-header {
:global(.htmlpopup-header) {
background-color: #e9e9e9;
padding: 7px;
margin-bottom: 7px;
font-weight: 700;
}
.htmlpopup-content {
:global(.htmlpopup-content) {
padding: 7px;
}
</style>
24 changes: 9 additions & 15 deletions src/modules/map/components/cesium/CesiumMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 21 additions & 4 deletions src/modules/map/components/common/mouse-click.composable.js
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
Expand All @@ -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',
Expand Down
5 changes: 3 additions & 2 deletions src/store/modules/features.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const getSelectedFeatureWithId = (state, featureId) => {

export default {
state: {
/** @type Array<Feature> */
/** @type Array<SelectableFeature> */
selectedFeatures: [],
},
getters: {
Expand All @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/geoJsonUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
108 changes: 108 additions & 0 deletions src/utils/identifyOnVectorLayer.js
Original file line number Diff line number Diff line change
@@ -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,
`<div class="htmlpopup-container">
<div class="htmlpopup-header">
<span>${geoJsonLayer.name}</span>
</div>
<div class="htmlpopup-content">
${feature.properties.description}
</div>
</div>`,
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 []
}
38 changes: 1 addition & 37 deletions src/utils/layerUtils.js
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -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,
`<div class="htmlpopup-container">
<div class="htmlpopup-header">
<span>${geoJsonLayer.name}</span>
</div>
<div class="htmlpopup-content">
${feature.get('description')}
</div>
</div>`,
featureGeometry.flatCoordinates,
featureGeometry.getExtent(),
geometry
)
log.debug('GeoJSON feature found', geoJsonFeature)
return geoJsonFeature
}

0 comments on commit de1a797

Please sign in to comment.