Skip to content

Commit

Permalink
Merge pull request #1095 from geoadmin/feat-pb-875-ctrl-right-click-s…
Browse files Browse the repository at this point in the history
…elect-multiple-features

PB-875: CTRL-Left click selection features for vector (KML/GPX/GeoJSON) and multiple elements
  • Loading branch information
sommerfe authored Oct 24, 2024
2 parents fd26a8e + 030c55a commit 59b1a68
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 62 deletions.
103 changes: 103 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 @@ -53,9 +53,11 @@
"@turf/area": "^7.1.0",
"@turf/bbox": "^7.1.0",
"@turf/boolean-contains": "^7.1.0",
"@turf/boolean-intersects": "^7.1.0",
"@turf/boolean-point-in-polygon": "^7.1.0",
"@turf/buffer": "^7.1.0",
"@turf/centroid": "^7.1.0",
"@turf/circle": "^7.1.0",
"@turf/distance": "^7.1.0",
"@turf/explode": "^7.1.0",
"@turf/helpers": "^7.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import { booleanIntersects } from '@turf/boolean-intersects'
import { circle } from '@turf/circle'
import {
geometryCollection,
lineString,
multiLineString,
multiPoint,
multiPolygon,
point,
polygon,
} from '@turf/helpers'
import { platformModifierKeyOnly } from 'ol/events/condition'
import GeoJSON from 'ol/format/GeoJSON'
import { DragBox } from 'ol/interaction'
import { useStore } from 'vuex'

import LayerTypes from '@/api/layers/LayerTypes.enum'
import { ClickInfo } from '@/store/modules/map.store'
import { parseGpx } from '@/utils/gpxUtils'
import { parseKml } from '@/utils/kmlUtils'
import { createLayerFeature } from '@/utils/layerUtils'
import log from '@/utils/logging'

const dispatcher = {
dispatcher: 'useDragBoxSelect.composable',
Expand All @@ -20,16 +37,104 @@ export function useDragBoxSelect() {
)
dragBoxSelect.on('boxend', () => {
const selectExtent = dragBoxSelect.getGeometry()?.getExtent()
if (selectExtent?.length !== 4) {
return
}
const dragBoxCoordinates = dragBoxSelect.getGeometry()?.getCoordinates()

if (selectExtent?.length === 4) {
store.dispatch('click', {
clickInfo: new ClickInfo({ coordinate: selectExtent }),
...dispatcher,
})
if (
!Array.isArray(dragBoxCoordinates) ||
!dragBoxCoordinates.every(
(coord) =>
Array.isArray(coord) &&
coord.every(
(point) =>
Array.isArray(point) &&
point.length === 2 &&
point.every(Number.isFinite)
)
)
) {
log.error('Invalid dragBoxCoordinates:', dragBoxCoordinates)
return
}

const dragBox = polygon(dragBoxCoordinates)
const visibleLayers = store.getters.visibleLayers.filter((layer) =>
[LayerTypes.GEOJSON, LayerTypes.GPX, LayerTypes.KML].includes(layer.type)
)
const vectorFeatures = visibleLayers
.flatMap((layer) => {
if (layer.type === LayerTypes.KML) {
const kmlFeatures = parseKml(layer, store.state.position.projection, [])
return kmlFeatures.map((feature) => ({ feature: feature, layer }))
}
if (layer.type === LayerTypes.GPX) {
const gpxFeatures = parseGpx(layer.gpxData, store.state.position.projection, [])
return gpxFeatures.map((feature) => ({ feature: feature, layer }))
}
if (layer.type === LayerTypes.GEOJSON) {
const geojsonFormat = new GeoJSON()
const olFeatures = geojsonFormat.readFeatures(layer.geoJsonData, {
featureProjection: store.state.position.projection.epsg,
})
return olFeatures.map((feature) => ({ feature: feature, layer }))
}
})
.filter((result) => {
const geometry = fromOlGeometryToTurfGeometry(result.feature.getGeometry())
return geometry && dragBox && booleanIntersects(dragBox, geometry)
})
.map(({ feature, layer }) => createLayerFeature(feature, layer))

store.dispatch('click', {
clickInfo: new ClickInfo({ coordinate: selectExtent, features: vectorFeatures }),
...dispatcher,
})
})

return {
dragBoxSelect,
}
}

/**
* Converts an OpenLayers geometry object to a Turf.js geometry object.
*
* @param {ol.geom.Geometry} olGeometry - The OpenLayers geometry object to convert.
* @returns {Object | null} The corresponding Turf.js geometry object, or null if the geometry type
* is not supported.
*/
function fromOlGeometryToTurfGeometry(olGeometry) {
if (!olGeometry || typeof olGeometry.getCoordinates !== 'function') {
log.error('Invalid OpenLayers geometry provided.', olGeometry)
return null
}

// Mapping OpenLayers geometry types to Turf.js functions
const geometryMapping = {
Point: point,
MultiPoint: multiPoint,
LineString: lineString,
MultiLineString: multiLineString,
Polygon: polygon,
MultiPolygon: multiPolygon,
GeometryCollection: geometryCollection,
Circle: function (olGeometry) {
const center = olGeometry.getCenter()
const radius = olGeometry.getRadius()
return circle(center, radius)
},
}

const olGeometryType = olGeometry.getType()

const turfGeometryFunction = geometryMapping[olGeometryType]

if (turfGeometryFunction) {
return turfGeometryFunction(olGeometry.getCoordinates())
} else {
log.error('Unsupported geometry type:', olGeometryType)
return null
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import GeoJSON from 'ol/format/GeoJSON'
import { DragPan, MouseWheelZoom } from 'ol/interaction'
import DoubleClickZoomInteraction from 'ol/interaction/DoubleClickZoom'
import { computed, onBeforeUnmount, watch } from 'vue'
import { useStore } from 'vuex'

import LayerFeature from '@/api/features/LayerFeature.class'
import LayerTypes from '@/api/layers/LayerTypes.enum'
import { DRAWING_HIT_TOLERANCE } from '@/config/map.config'
import { IS_TESTING_WITH_CYPRESS } from '@/config/staging.config'
import { useDragBoxSelect } from '@/modules/map/components/openlayers/utils/useDragBoxSelect.composable'
import { handleFileContent } from '@/modules/menu/components/advancedTools/ImportFile/utils'
import { ClickInfo, ClickType } from '@/store/modules/map.store'
import { normalizeExtent, OutOfBoundsError } from '@/utils/coordinates/coordinateUtils'
import { OutOfBoundsError } from '@/utils/coordinates/coordinateUtils'
import ErrorMessage from '@/utils/ErrorMessage.class'
import { EmptyGPXError } from '@/utils/gpxUtils'
import { EmptyKMLError } from '@/utils/kmlUtils'
import { createLayerFeature } from '@/utils/layerUtils'
import log from '@/utils/logging'

const dispatcher = {
Expand Down Expand Up @@ -138,32 +137,7 @@ export default function useMapInteractions(map) {
layerFilter: (layer) => layer.get('id') === olLayer.get('id'),
hitTolerance: DRAWING_HIT_TOLERANCE,
})
.map(
(olFeature) =>
new LayerFeature({
layer: vectorLayer,
id: olFeature.getId(),
name:
olFeature.get('label') ??
// exception for MeteoSchweiz GeoJSONs, we use the station name instead of the ID
// some of their layers are
// - ch.meteoschweiz.messwerte-niederschlag-10min
// - ch.meteoschweiz.messwerte-lufttemperatur-10min
olFeature.get('station_name') ??
// GPX track feature don't have an ID but have a name !
olFeature.get('name') ??
olFeature.getId(),
data: {
title: olFeature.get('name'),
description: olFeature.get('description'),
},
coordinates: olFeature.getGeometry().getCoordinates(),
geometry: new GeoJSON().writeGeometryObject(
olFeature.getGeometry()
),
extent: normalizeExtent(olFeature.getGeometry().getExtent()),
})
)
.map((olFeature) => createLayerFeature(olFeature, vectorLayer))
// unique filter on features (OL sometimes return twice the same features)
.filter(
(feature, index, self) =>
Expand Down
Loading

0 comments on commit 59b1a68

Please sign in to comment.