Skip to content

Commit

Permalink
PB-1023: select searched feature
Browse files Browse the repository at this point in the history
  • Loading branch information
sommerfe committed Oct 11, 2024
1 parent 5ff9cbe commit 41b6a72
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 32 deletions.
62 changes: 37 additions & 25 deletions src/api/search.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,22 +280,22 @@ async function searchLayerFeatures(outputProjection, queryString, layer, lang, c
}

/**
* Searches for the query string in the layer inside the provided search fields
* Searches for the query string in the feature inside the provided search fields
*
* @param layer
* @param feature
* @param {String} queryString
* @param {String} searchFields
* @returns {Boolean}
*/
function isQueryInLayer(layer, queryString, searchFields) {
function isQueryInFeature(feature, queryString, searchFields) {
const queryStringClean = queryString
.trim()
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
return searchFields.some((field) => {
const value = layer[field]?.toString()
return value && value.trim().toLowerCase().includes(queryStringClean)
const value = feature.values_[field]?.toString()
return !!value && value.trim().toLowerCase().includes(queryStringClean)
})
}

Expand All @@ -305,19 +305,24 @@ function isQueryInLayer(layer, queryString, searchFields) {
* @param {CoordinateSystem} outputProjection
* @param {String} queryString
* @param layer
* @returns {SearchResult}
* @returns {SearchResult[]}
*/
async function searchKmlLayerFeatures(outputProjection, queryString, layer) {
try {
const searchFields = ['name', 'description', 'id', 'kmlFileUrl']
const isInLayer = isQueryInLayer(layer, queryString, searchFields)
if (!isInLayer) return []
const searchFields = ['name', 'description', 'id']
const features = parseKml(layer, outputProjection, [])
if (!features || !features.length) return []

const includedFeatures = features.filter((feature) =>
isQueryInFeature(feature, queryString, searchFields)
)
if (!includedFeatures.length) return []

const extent = getKmlExtent(layer.kmlData)

return createSearchResultFromLayer(layer, features[0], extent, outputProjection)
return includedFeatures.map((feature) =>
createSearchResultFromLayer(layer, feature, extent, outputProjection)
)
} catch (error) {
log.error(
`Failed to search layer features for layer ${layer.id}, fallback to empty result`,
Expand All @@ -333,19 +338,24 @@ async function searchKmlLayerFeatures(outputProjection, queryString, layer) {
* @param {CoordinateSystem} outputProjection
* @param {String} queryString
* @param layer
* @returns {SearchResult}
* @returns {SearchResult[]}
*/
async function searchGpxLayerFeatures(outputProjection, queryString, layer) {
try {
const searchFields = ['name', 'description', 'id', 'baseUrl', 'gpxFileUrl']
const isInLayer = isQueryInLayer(layer, queryString, searchFields)
if (!isInLayer) return []
const searchFields = ['name', 'description', 'id']
const features = parseGpx(layer.gpxData, outputProjection, [])
if (!features || !features.length) return []

const includedFeatures = features.filter((feature) =>
isQueryInFeature(feature, queryString, searchFields)
)
if (!includedFeatures.length) return []

const extent = getGpxExtent(layer.gpxData)

return createSearchResultFromLayer(layer, features[0], extent, outputProjection)
return includedFeatures.map((feature) =>
createSearchResultFromLayer(layer, feature, extent, outputProjection)
)
} catch (error) {
log.error(
`Failed to search layer features for layer ${layer.id}, fallback to empty result`,
Expand All @@ -365,27 +375,28 @@ async function searchGpxLayerFeatures(outputProjection, queryString, layer) {
* @returns {SearchResult}
*/
function createSearchResultFromLayer(layer, feature, extent, outputProjection) {
const layerName = layer.name ?? ''
const featureName = feature.values_.name || layer.name || ''
const coordinates = extractOlFeatureCoordinates(feature)
const zoom = outputProjection.get1_25000ZoomLevel()
// TODO is this the correct way we should handle this? It works fine for polygons but for lines it might be a bit off
const point = points(coordinates)
const centerPoint = center(point)

const featureId = feature.id_ ?? feature.ol_uid
const layerContent = {
resultType: SearchResultTypes.LAYER,
id: layer.id,
title: layerName,
sanitizedTitle: sanitizeTitle(layerName),
title: layer.name ?? '',
sanitizedTitle: sanitizeTitle(layer.name),
description: layer.description ?? '',
layerId: layer.id,
}
const locationContent = {
resultType: SearchResultTypes.LOCATION,
id: layer.id,
title: layerName,
sanitizedTitle: sanitizeTitle(layerName),
description: layer.description ?? '',
featureId: layer.id ?? layer.description,
id: featureId,
title: featureName,
sanitizedTitle: sanitizeTitle(featureName),
description: feature.values_.description ?? '',
featureId: featureId,
coordinate: centerPoint.geometry.coordinates,
extent,
zoom,
Expand All @@ -394,7 +405,7 @@ function createSearchResultFromLayer(layer, feature, extent, outputProjection) {
...layerContent,
...locationContent,
resultType: SearchResultTypes.FEATURE,
title: layerName,
title: featureName,
layer,
}
}
Expand Down Expand Up @@ -455,6 +466,7 @@ export default async function search(config) {
.map((layer) => searchKmlLayerFeatures(outputProjection, queryString, layer))
)
}

if (layersToSearch.some((layer) => layer.type === LayerTypes.GPX)) {
allRequests.push(
...layersToSearch
Expand Down
4 changes: 2 additions & 2 deletions src/store/modules/features.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const getEditableFeatureWithId = (state, featureId) => {
)
}

function getFeatureCountForCoordinate(coordinate) {
export function getFeatureCountForCoordinate(coordinate) {
return coordinate.length === 2
? DEFAULT_FEATURE_COUNT_SINGLE_POINT
: DEFAULT_FEATURE_COUNT_RECTANGLE_SELECTION
Expand All @@ -58,7 +58,7 @@ function getFeatureCountForCoordinate(coordinate) {
* @returns {Promise<LayerFeature[]>} A promise that will contain all feature identified by the
* different requests (won't be grouped by layer)
*/
const runIdentify = (config) => {
export const runIdentify = (config) => {
const {
layers,
coordinate,
Expand Down
49 changes: 44 additions & 5 deletions src/store/modules/search.store.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import GeoJSON from 'ol/format/GeoJSON'

import getFeature from '@/api/features/features.api'
import LayerFeature from '@/api/features/LayerFeature.class'
import LayerTypes from '@/api/layers/LayerTypes.enum'
import reframe from '@/api/lv03Reframe.api'
import search, { SearchResultTypes } from '@/api/search.api'
import { isWhat3WordsString, retrieveWhat3WordsLocation } from '@/api/what3words.api'
Expand All @@ -7,7 +11,9 @@ import { STANDARD_ZOOM_LEVEL_1_25000_MAP } from '@/utils/coordinates/CoordinateS
import { LV03 } from '@/utils/coordinates/coordinateSystems'
import { reprojectAndRound } from '@/utils/coordinates/coordinateUtils'
import { flattenExtent } from '@/utils/coordinates/coordinateUtils'
import { normalizeExtent } from '@/utils/coordinates/coordinateUtils'
import CustomCoordinateSystem from '@/utils/coordinates/CustomCoordinateSystem.class'
import { parseGpx } from '@/utils/gpxUtils'
import { parseKml } from '@/utils/kmlUtils'
import log from '@/utils/logging'

Expand Down Expand Up @@ -189,12 +195,20 @@ const actions = {
})
})
} else {
const features = parseKml(entry.layer, rootState.position.projection, [])
// TODO
// fix set selectedFeatures
// For imported KML and GPX files
let features = []
if (entry.layer.type === LayerTypes.KML) {
features = parseKml(entry.layer, rootState.position.projection, [])
}
if (entry.layer.type === LayerTypes.GPX) {
features = parseGpx(entry.layer, rootState.position.projection, [])
}
// TODO see if there is a better way of finding the feature
const layerFeatures = features
.map((feature) => createLayerFeature(feature, entry.layer))
.filter((feature) => !!feature && feature.data.title === entry.title)
dispatch('setSelectedFeatures', {
features,
paginationSize: features.length,
features: layerFeatures,
dispatcher,
})
}
Expand All @@ -211,6 +225,31 @@ const actions = {
},
}

function createLayerFeature(olFeature, layer) {
if (!olFeature.getGeometry()) return null
return new LayerFeature({
layer: layer,
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()),
})
}

const mutations = {
setSearchQuery: (state, { query }) => (state.query = query),
setSearchResults: (state, { results }) => (state.results = results ?? []),
Expand Down
62 changes: 62 additions & 0 deletions tests/cypress/tests-e2e/importToolFile.cy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/// <reference types="cypress" />

import proj4 from 'proj4'

import { proxifyUrl } from '@/api/file-proxy.api.js'
import { DEFAULT_PROJECTION } from '@/config/map.config'
import { WGS84 } from '@/utils/coordinates/coordinateSystems'

describe('The Import File Tool', () => {
it('Import KML file', () => {
Expand Down Expand Up @@ -196,9 +200,67 @@ describe('The Import File Tool', () => {
}
})

// Test the search for a feature in the local KML file
const expectedSecondCenterEpsg4326 = [8.117189, 46.852375] // lon/lat
const expectedCenterEpsg4326 = [9.74921, 46.707841] // lon/lat
const expectedSecondCenterDefaultProjection = proj4(
WGS84.epsg,
DEFAULT_PROJECTION.epsg,
expectedSecondCenterEpsg4326
)
const expectedCenterDefaultProjection = proj4(
WGS84.epsg,
DEFAULT_PROJECTION.epsg,
expectedCenterEpsg4326
)
const expectedLayerId = 'external-kml-file.kml'
const expectedOnlineLayerId = 'https://example.com/second-valid-kml-file.kml'
const acceptedDelta = 0.1
const checkLocation = (expected, result) => {
expect(result).to.be.an('Array')
expect(result.length).to.eq(2)
expect(result[0]).to.approximately(expected[0], acceptedDelta)
expect(result[1]).to.approximately(expected[1], acceptedDelta)
}

cy.log('Test search for a feature in the local KML file')
cy.closeMenuIfMobile()
cy.get('[data-cy="searchbar"]').paste('placemark')
cy.get('[data-cy="search-results"]').should('be.visible')
cy.get('[data-cy="search-result-entry"]').as('layerSearchResults').should('have.length', 3)
cy.get('@layerSearchResults').invoke('text').should('contain', 'Sample Placemark')
cy.get('@layerSearchResults').first().trigger('mouseenter')
cy.readStoreValue('getters.visibleLayers').should((visibleLayers) => {
const visibleIds = visibleLayers.map((layer) => layer.id)
expect(visibleIds).to.contain(expectedLayerId)
})
cy.get('@layerSearchResults').first().realClick()
// checking that the view has centered on the feature
cy.readStoreValue('state.position.center').should((center) =>
checkLocation(expectedCenterDefaultProjection, center)
)

cy.log('Test search for a feature in the online KML file')
cy.get('[data-cy="searchbar-clear"]').click()
cy.get('[data-cy="searchbar"]').paste('another sample')
cy.get('[data-cy="search-results"]').should('be.visible')
cy.get('[data-cy="search-result-entry"]').as('layerSearchResults').should('have.length', 1)
cy.get('@layerSearchResults').invoke('text').should('contain', 'Another Sample Placemark')
cy.get('@layerSearchResults').first().trigger('mouseenter')
cy.readStoreValue('getters.visibleLayers').should((visibleLayers) => {
const visibleIds = visibleLayers.map((layer) => layer.id)
expect(visibleIds).to.contain(expectedOnlineLayerId)
})
cy.get('@layerSearchResults').first().realClick()
// checking that the view has centered on the feature
cy.readStoreValue('state.position.center').should((center) =>
checkLocation(expectedSecondCenterDefaultProjection, center)
)

//---------------------------------------------------------------------
// Test the disclaimer
cy.log('Test the external layer disclaimer')
cy.openMenuIfMobile()
cy.get('[data-cy="menu-section-active-layers"]')
.children()
.find('[data-cy="menu-external-disclaimer-icon-hard-drive"]:visible')
Expand Down

0 comments on commit 41b6a72

Please sign in to comment.