Skip to content

Commit

Permalink
Merge pull request #555 from geoadmin/feat_identify_geojson_feature_w…
Browse files Browse the repository at this point in the history
…ithout_openlayers

Handle identification of GeoJSON feature without the help of OpenLayers
  • Loading branch information
pakb authored Dec 4, 2023
2 parents 7f577ec + de1a797 commit 9a2f27e
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 9a2f27e

Please sign in to comment.