Skip to content

Handle identification of GeoJSON feature without the help of OpenLayers #555

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}