Skip to content
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

New Release v1.31.0 - #minor #947

Merged
merged 19 commits into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7d93a84
PB-669 : separate html popup request from getFeature
pakb Jun 20, 2024
0fe6672
PB-669 : mimic backend response by sending MultiPoint for single point
pakb Jun 20, 2024
a415f3d
PB-669 : add back optional params for feature detail and HTML popup
pakb Jun 20, 2024
7566663
Merge pull request #944 from geoadmin/feat_PB-669_less_get_feature_re…
pakb Jun 20, 2024
e67702b
PB-681 : add support for group of external layers getFeatureInfo
pakb Jun 21, 2024
92cd225
Merge pull request #946 from geoadmin/bug_PB-681_identify_on_external…
pakb Jun 21, 2024
412872e
PB-675: Clean up TimeSlider code
ltshb Jun 20, 2024
0d15a13
PB-675: Removed the preview year in layer component
ltshb Jun 20, 2024
d4f96b3
PB-675: Fix initial preview year
ltshb Jun 20, 2024
2dea9e9
PB-675: Fix layers url parameter parsing in e2e tests
ltshb Jun 20, 2024
1110e27
PB-675: Fix legacy timestamp conversion
ltshb Jun 20, 2024
83b2142
PB-675: Fix external maps with invalid timestamp
ltshb Jun 20, 2024
7fe5988
PB-590: Fix @year=none for non timeEnabled layers and external layers
ltshb Jun 20, 2024
a4b34a9
PB-675: Fix e2e tests
ltshb Jun 20, 2024
1383068
PB-675: Allow @updateDelay and @features on external layers
ltshb Jun 21, 2024
f113824
PB-675: Fix timeSlider reaction when new layer is added/visible
ltshb Jun 21, 2024
dd77a7f
Fix typo
ltshb Jun 21, 2024
c95ea38
PB-675: Fix crash due to previous commit
ltshb Jun 21, 2024
e8aa05a
Merge pull request #942 from geoadmin/bug-PB-590-time-slider-no-data
ltshb Jun 21, 2024
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
285 changes: 200 additions & 85 deletions src/api/features/features.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { WMSGetFeatureInfo } from 'ol/format'
import GeoJSON from 'ol/format/GeoJSON'

import LayerFeature from '@/api/features/LayerFeature.class'
import ExternalGroupOfLayers from '@/api/layers/ExternalGroupOfLayers.class'
import ExternalLayer from '@/api/layers/ExternalLayer.class'
import ExternalWMSLayer from '@/api/layers/ExternalWMSLayer.class'
import GeoAdminLayer from '@/api/layers/GeoAdminLayer.class'
import { YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA } from '@/api/layers/LayerTimeConfigEntry.class'
import {
ALL_YEARS_TIMESTAMP,
CURRENT_YEAR_TIMESTAMP,
} from '@/api/layers/LayerTimeConfigEntry.class'
import { API_BASE_URL, DEFAULT_FEATURE_COUNT_SINGLE_POINT } from '@/config'
import allCoordinateSystems, { LV95 } from '@/utils/coordinates/coordinateSystems'
import { projExtent } from '@/utils/coordinates/coordinateUtils'
Expand All @@ -32,7 +36,7 @@ function getApi3TimeInstantParam(layer) {
// timestamp therefore we need to set it to null in this case.
if (
layer.timeConfig?.currentYear &&
layer.timeConfig.currentYear !== YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA
![ALL_YEARS_TIMESTAMP, CURRENT_YEAR_TIMESTAMP].includes(layer.timeConfig.currentYear)
) {
return layer.timeConfig.currentYear
}
Expand Down Expand Up @@ -158,23 +162,24 @@ export async function identifyOnGeomAdminLayer({
},
}
)
// firing a getFeature (async/parallel) on each identified feature
const featureRequests = []
// firing a getHtmlPopup (async/parallel) on each identified feature
const features = []
if (identifyResponse.data?.results?.length > 0) {
// for each feature that has been identified, we will now load their metadata and tooltip content
identifyResponse.data.results.forEach((feature) => {
featureRequests.push(
getFeature(layer, feature.id, projection, {
for (const feature of identifyResponse.data.results) {
const featureData = await getFeatureHtmlPopup(layer, feature.id, {
lang,
screenWidth,
screenHeight,
mapExtent,
})
features.push(
parseGeomAdminFeature(layer, feature, featureData, projection, {
lang,
coordinates: coordinate.join(','),
mapExtent: mapExtent.join(','),
imageDisplay,
})
)
})
}
}
// waiting on the result of all parallel getFeature requests
return await Promise.all(featureRequests)
return features
}

/**
Expand Down Expand Up @@ -260,6 +265,27 @@ async function identifyOnExternalLayer(config) {
tolerance,
outputProjection: projection,
})
} else if (layer instanceof ExternalGroupOfLayers) {
// firing one request per sub-layer
const allRequests = [
layer.layers.map((subLayer) =>
identifyOnExternalLayer({
...config,
layer: subLayer,
})
),
]
const allResponses = await Promise.allSettled(allRequests)
// logging any error
allResponses
.filter((response) => response.status !== 'fulfilled')
.forEach((failedResponse) => {
log.error('Error while identify an external sub-layer', failedResponse)
})
return allResponses
.filter((response) => response.status === 'fulfilled' && response.value)
.map((response) => response.value)
.flat()
} else {
throw new GetFeatureInfoError(
`Unsupported external layer type to build getFeatureInfo request: ${layer.type}`
Expand Down Expand Up @@ -361,7 +387,8 @@ async function identifyOnExternalWmsLayer(config) {
// there might exist more implementation of WMS, but I stopped there looking for more
// (please add more if you think one of our customer/external layer providers uses another flavor of WMS)
}
if (layer.timeConfig?.currentYear) {
// In WMS "all" years mean no TIME parameter
if (layer.timeConfig?.currentYear && layer.timeConfig.currentYear !== ALL_YEARS_TIMESTAMP) {
params.TIME = layer.timeConfig.currentYear
}
// WMS 1.3.0 uses i,j to describe pixel coordinate where we want feature info
Expand Down Expand Up @@ -529,108 +556,196 @@ export const identify = (config) => {
})
}

/**
* @param {GeoAdminLayer} layer The layer from which the feature is part of
* @param {String | Number} featureId The feature ID in the BGDI
* @returns {string}
*/
function generateFeatureUrl(layer, featureId) {
return `${API_BASE_URL}rest/services/${layer.getTopicForIdentifyAndTooltipRequests()}/MapServer/${layer.id}/${featureId}`
}

/**
* Generates parameters used to request endpoint to get a single feature's data and endpoint to get
* a single feature's HTML popup. As some layers have a resolution dependent answer, we have to give
* the map extent and the current screen size with each request.
*
* @param {Object} [options]
* @param {String} [options.lang='en'] ISO code of the current lang. Default is `'en'`
* @param {Number} [options.screenWidth] Current screen width in pixels
* @param {Number} [options.screenHeight] Current screen height in pixels
* @param {[Number, Number, Number, Number]} [options.mapExtent]
*/
function generateFeatureParams(options = {}) {
const { lang = 'en', screenWidth = null, screenHeight = null, mapExtent = null } = options
let imageDisplay = null
if (screenWidth && screenHeight) {
imageDisplay = `${screenWidth},${screenHeight},96`
}
return {
sr: LV95.epsgNumber,
lang,
imageDisplay,
mapExtent: mapExtent?.join(',') ?? null,
}
}

/**
* @param {GeoAdminLayer} layer The layer to which this feature belongs to
* @param {Object} featureMetadata The backend response (either identify, or feature-resource) for
* this feature
* @param featureHtmlPopup The backend response for the getHtmlPopup endpoint for this feature
* @param {CoordinateSystem} outputProjection In which projection the feature should be in.
* @param {Object} [options]
* @param {String} [options.lang] The lang the title of the feature should be look up. Some features
* do provide a title per lang, instead of an all-purpose title. In this case we need the lang ISO
* code to be able to decide which title the feature will have. Default is `en`
* @returns {LayerFeature}
*/
function parseGeomAdminFeature(
layer,
featureMetadata,
featureHtmlPopup,
outputProjection,
options = {}
) {
const { lang = 'en' } = options
const featureGeoJSONGeometry = featureMetadata.geometry
let featureExtent = []
if (featureMetadata.bbox) {
featureExtent.push(...featureMetadata.bbox)
}
let featureName = featureMetadata.id
if (featureMetadata.properties) {
const { name = null, title = null, label = null } = featureMetadata.properties
const titleInCurrentLang = featureMetadata.properties[`title_${lang}`]
if (label) {
featureName = label
} else if (name) {
featureName = name
} else if (title) {
featureName = title
} else if (titleInCurrentLang) {
featureName = titleInCurrentLang
}
}

if (outputProjection.epsg !== LV95.epsg) {
if (featureExtent.length === 4) {
featureExtent = projExtent(LV95, outputProjection, featureExtent)
}
}

return new LayerFeature({
layer,
id: featureMetadata.id,
name: featureName,
data: featureHtmlPopup,
coordinates: getGeoJsonFeatureCoordinates(featureGeoJSONGeometry, LV95, outputProjection),
extent: featureExtent,
geometry: featureGeoJSONGeometry,
})
}

/**
* Loads a feature metadata and tooltip content from this two endpoint of the backend
*
* - http://api3.geo.admin.ch/services/sdiservices.html#identify-features
* - https://api3.geo.admin.ch/services/sdiservices.html#feature-resource
* - http://api3.geo.admin.ch/services/sdiservices.html#htmlpopup-resource
*
* @param {GeoAdminLayer} layer The layer from which the feature is part of
* @param {String | Number} featureID The feature ID in the BGDI
* @param {String | Number} featureId The feature ID in the BGDI
* @param {CoordinateSystem} outputProjection Projection in which the coordinates (and possible
* extent) of the features should be expressed
* @param {String} lang The language for the HTML popup
* @param {Object} [options]
* @param {String} [options.lang] The language for the HTML popup. Default is `en`.
* @param {Number} [options.screenWidth] Width of the screen in pixels
* @param {Number} [options.screenHeight] Height of the screen in pixels
* @param {[Number, Number, Number, Number]} [options.mapExtent] Current extent of the map,
* described in LV95.
* @returns {Promise<LayerFeature>}
*/
const getFeature = (layer, featureID, outputProjection, options = {}) => {
const { lang = 'en', coordinates = null, imageDisplay = null, mapExtent = null } = options
const getFeature = (layer, featureId, outputProjection, options = {}) => {
return new Promise((resolve, reject) => {
if (!layer?.id) {
reject('Needs a valid layer with an ID')
}
if (!featureID) {
if (!featureId) {
reject('Needs a valid feature ID')
}
// combining the two requests in one promise
const topic = layer.getTopicForIdentifyAndTooltipRequests()
const featureUrl = `${API_BASE_URL}rest/services/${topic}/MapServer/${layer.id}/${featureID}`
const params = {
sr: LV95.epsgNumber,
lang: lang,
}
if (coordinates) {
params.coord = coordinates
}
if (imageDisplay) {
params.imageDisplay = imageDisplay
}
if (mapExtent) {
params.mapExtent = mapExtent
if (!outputProjection) {
reject('An output projection is required')
}
axios
.all([
axios.get(featureUrl, {
axios.get(generateFeatureUrl(layer, featureId), {
params: {
geometryFormat: 'geojson',
...params,
...generateFeatureParams(options),
},
}),
axios.get(`${featureUrl}/htmlPopup`, {
params,
}),
getFeatureHtmlPopup(layer, featureId, options),
])
.then((responses) => {
const featureMetadata = responses[0].data.feature
? responses[0].data.feature
: responses[0].data
const featureHtmlPopup = responses[1].data
const featureGeoJSONGeometry = featureMetadata.geometry
let featureExtent = []
if (featureMetadata.bbox) {
featureExtent.push(...featureMetadata.bbox)
}
let featureName = featureID
if (featureMetadata.properties) {
const { name = null, title = null, label = null } = featureMetadata.properties
const titleInCurrentLang = featureMetadata.properties[`title_${lang}`]
if (label) {
featureName = label
} else if (name) {
featureName = name
} else if (title) {
featureName = title
} else if (titleInCurrentLang) {
featureName = titleInCurrentLang
}
}

if (outputProjection.epsg !== LV95.epsg) {
if (featureExtent.length === 4) {
featureExtent = projExtent(LV95, outputProjection, featureExtent)
}
}

.then(([getFeatureResponse, featureHtmlPopup]) => {
const featureMetadata = getFeatureResponse.data.feature ?? getFeatureResponse.data
resolve(
new LayerFeature({
parseGeomAdminFeature(
layer,
id: featureID,
name: featureName,
data: featureHtmlPopup,
coordinates: getGeoJsonFeatureCoordinates(
featureGeoJSONGeometry,
LV95,
outputProjection
),
extent: featureExtent,
geometry: featureGeoJSONGeometry,
})
featureMetadata,
featureHtmlPopup,
outputProjection,
options
)
)
})
.catch((error) => {
log.error(
'Error while requesting a feature to the backend',
layer,
featureID,
featureId,
error
)
reject(error)
})
})
}

/**
* Retrieves the HTML popup of a feature (the backend builds it for us).
*
* As the request's outcome is dependent on the resolution, we have to give the screen size and map
* extent with the request.
*
* @param {GeoAdminLayer} layer
* @param {String} featureId
* @param {Object} options
* @param {String} [options.lang] The language for the HTML popup. Default is `en`.
* @param {Number} [options.screenWidth] Width of the screen in pixels
* @param {Number} [options.screenHeight] Height of the screen in pixels
* @param {[Number, Number, Number, Number]} [options.mapExtent] Current extent of the map,
* described in LV95.
* @returns {Promise<String>}
*/
export function getFeatureHtmlPopup(layer, featureId, options) {
return new Promise((resolve, reject) => {
if (!layer?.id) {
reject('Needs a valid layer with an ID')
}
if (!featureId) {
reject('Needs a valid feature ID')
}
axios
.get(`${generateFeatureUrl(layer, featureId)}/htmlPopup`, {
params: generateFeatureParams(options),
})
.then((response) => {
resolve(response.data)
})
.catch((error) => {
log.error(
'Error while requesting a the HTML popup of a feature to the backend',
layer,
featureId,
error
)
reject(error)
Expand Down
13 changes: 4 additions & 9 deletions src/api/layers/LayerTimeConfig.class.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import LayerTimeConfigEntry, {
ALL_YEARS_WMS_TIMESTAMP,
YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA,
} from '@/api/layers/LayerTimeConfigEntry.class'
import LayerTimeConfigEntry, { ALL_YEARS_TIMESTAMP } from '@/api/layers/LayerTimeConfigEntry.class'

/**
* @class
Expand All @@ -25,8 +22,8 @@ export default class LayerTimeConfig {
this.behaviour = behaviour
/** @type {LayerTimeConfigEntry[]} */
this.timeEntries = [...timeEntries]
if (this.behaviour === ALL_YEARS_WMS_TIMESTAMP) {
this.timeEntries.splice(0, 0, new LayerTimeConfigEntry(ALL_YEARS_WMS_TIMESTAMP))
if (this.behaviour === ALL_YEARS_TIMESTAMP) {
this.timeEntries.splice(0, 0, new LayerTimeConfigEntry(ALL_YEARS_TIMESTAMP))
}
/*
* Here we will define what is the first "currentTimeEntry" for this configuration.
Expand Down Expand Up @@ -57,9 +54,7 @@ export default class LayerTimeConfig {
this.updateCurrentTimeEntry(behaviour)
}

this.years = this.timeEntries
.map((entry) => entry.year)
.filter((year) => year !== YEAR_TO_DESCRIBE_ALL_OR_CURRENT_DATA)
this.years = this.timeEntries.map((entry) => entry.year)
}

/**
Expand Down
Loading
Loading