From 31da180bae6330d23ccc30ee96a76d38c406e0a0 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 15 Nov 2024 16:20:56 +0100 Subject: [PATCH 01/45] feat(feature-info): display basic feature info in default template --- .env | 3 +- .env.development | 3 +- .env.e2e | 3 +- .env.staging | 3 +- .../info/feature-info.composable.ts | 31 ++ src/components/info/feature-info.model.ts | 38 ++ src/components/info/feature-info.service.ts | 372 ++++++++++++++++++ src/components/info/feature-info.vue | 55 +++ src/components/info/info-panel.vue | 9 +- .../info/templates/default-template.vue | 128 ++++++ .../info/templates/lignes-bus-template.vue | 59 +++ .../info/templates/template-utilities.ts | 191 +++++++++ src/components/map/map-container.vue | 5 +- src/stores/feature-info.store.ts | 33 ++ 14 files changed, 927 insertions(+), 6 deletions(-) create mode 100644 src/components/info/feature-info.composable.ts create mode 100644 src/components/info/feature-info.model.ts create mode 100644 src/components/info/feature-info.service.ts create mode 100644 src/components/info/feature-info.vue create mode 100644 src/components/info/templates/default-template.vue create mode 100644 src/components/info/templates/lignes-bus-template.vue create mode 100644 src/components/info/templates/template-utilities.ts create mode 100644 src/stores/feature-info.store.ts diff --git a/.env b/.env index 311c0632..8e4c4d78 100644 --- a/.env +++ b/.env @@ -3,10 +3,11 @@ VITE_USE_PROXYURL=true VITE_PROXYURL_WMS="/ogcproxywms" VITE_PROXYURL_REMOTE="/httpsproxy" -# Urls for metadata, metadata links and legends +# Urls for metadata, metadata links, legends, feature info VITE_GEONETWORK_URL="https://geocatalogue.geoportail.lu/geonetwork/srv" VITE_GET_LEGENDS_URL="/legends/get_html" VITE_GET_METADATA_URL="/getMetadata" +VITE_GET_INFO_SERVICE_URL="/getfeatureinfo" # Paths for symbols VITE_SYMBOL_ICONS_URL="/mymaps" diff --git a/.env.development b/.env.development index 485175f7..d520915b 100644 --- a/.env.development +++ b/.env.development @@ -3,10 +3,11 @@ VITE_USE_PROXYURL=true VITE_PROXYURL_WMS="https://migration.geoportail.lu/ogcproxywms" VITE_PROXYURL_REMOTE="https://migration.geoportail.lu/httpsproxy" -# Urls for metadata, metadata links and legends +# Urls for metadata, metadata links, legends, feature info VITE_GEONETWORK_URL="https://geocatalogue.geoportail.lu/geonetwork/srv" VITE_GET_LEGENDS_URL="https://migration.geoportail.lu/legends/get_html" VITE_GET_METADATA_URL="https://migration.geoportail.lu/getMetadata" +VITE_GET_INFO_SERVICE_URL="https://migration.geoportail.lu/getfeatureinfo" # Paths for symbols VITE_SYMBOL_ICONS_URL="https://map.geoportail.lu/mymaps" # !!! use prod because of CORS diff --git a/.env.e2e b/.env.e2e index 99830da6..b5c76e50 100644 --- a/.env.e2e +++ b/.env.e2e @@ -3,10 +3,11 @@ VITE_USE_PROXYURL=true VITE_PROXYURL_WMS="https://map.geoportail.lu/ogcproxywms" VITE_PROXYURL_REMOTE="https://map.geoportail.lu/httpsproxy" -# Urls for metadata, metadata links and legends +# Urls for metadata, metadata links, legends, feature info VITE_GEONETWORK_URL="https://geocatalogue.geoportail.lu/geonetwork/srv" VITE_GET_LEGENDS_URL="https://migration.geoportail.lu/legends/get_html" VITE_GET_METADATA_URL="https://migration.geoportail.lu/getMetadata" +VITE_GET_INFO_SERVICE_URL="https://migration.geoportail.lu/getfeatureinfo" # Paths for symbols VITE_SYMBOL_ICONS_URL="https://map.geoportail.lu/mymaps" # !!! use prod because of CORS diff --git a/.env.staging b/.env.staging index 1a021b8c..071f1b9b 100644 --- a/.env.staging +++ b/.env.staging @@ -3,10 +3,11 @@ VITE_USE_PROXYURL=true VITE_PROXYURL_WMS="https://migration.geoportail.lu/ogcproxywms" VITE_PROXYURL_REMOTE="https://migration.geoportail.lu/httpsproxy" -# Urls for metadata, metadata links and legends +# Urls for metadata, metadata links, legends, feature info VITE_GEONETWORK_URL="https://geocatalogue.geoportail.lu/geonetwork/srv" VITE_GET_LEGENDS_URL="https://migration.geoportail.lu/legends/get_html" VITE_GET_METADATA_URL="https://migration.geoportail.lu/getMetadata" +VITE_GET_INFO_SERVICE_URL="https://migration.geoportail.lu/getfeatureinfo" # Paths for symbols VITE_SYMBOL_ICONS_URL="https://migration.geoportail.lu/mymaps" diff --git a/src/components/info/feature-info.composable.ts b/src/components/info/feature-info.composable.ts new file mode 100644 index 00000000..620a7424 --- /dev/null +++ b/src/components/info/feature-info.composable.ts @@ -0,0 +1,31 @@ +import { useAppStore } from '@/stores/app.store' +import useMap from '@/composables/map/map.composable' +import { listen } from 'ol/events' +import { FeatureInfoService } from './feature-info.service' +import { useFeatureInfoStore } from '@/stores/feature-info.store' +import { FeatureInfo } from './feature-info.model' +import { MapBrowserEvent } from 'ol' + +export default function useFeatureInfo() { + const map = useMap().getOlMap() + const { toggleInfoOpen } = useAppStore() + const { setContent } = useFeatureInfoStore() + + function init() { + const featureInfoService = new FeatureInfoService(map) + + listen(map, 'pointerup', event => { + ;(async () => { + const mapBrowserEvent = event as MapBrowserEvent + const content: FeatureInfo[] | undefined = + await featureInfoService.singleclickEvent(mapBrowserEvent, false) + if (content) { + setContent(content) + toggleInfoOpen(true) + } + })() + }) + } + + return { init } +} diff --git a/src/components/info/feature-info.model.ts b/src/components/info/feature-info.model.ts new file mode 100644 index 00000000..ccc30770 --- /dev/null +++ b/src/components/info/feature-info.model.ts @@ -0,0 +1,38 @@ +import { Geometry } from 'ol/geom' + +export interface Feature { + type: string + geometry: Geometry + fid: string + id: string + attributes: Attributes + alias: Record +} + +// interface Geometry { +// type: string +// coordinates: number[][][] +// } + +export interface Attributes { + name: string + canton: string + district: string + showProfile: { active: boolean } + profile: any + label: string + [key: string]: any + value: string +} + +export interface FeatureInfo { + features: Feature[] + remote_template: boolean + template: string + layer: string + ordered: boolean + has_profile: boolean + total_features_count: number + features_count: number + layerLabel: string +} diff --git a/src/components/info/feature-info.service.ts b/src/components/info/feature-info.service.ts new file mode 100644 index 00000000..3440dfd0 --- /dev/null +++ b/src/components/info/feature-info.service.ts @@ -0,0 +1,372 @@ +import { Map, MapBrowserEvent } from 'ol' +import VectorLayer from 'ol/layer/Vector.js' +import { transform } from 'ol/proj.js' +import VectorSource from 'ol/source/Vector.js' +import CircleStyle from 'ol/style/Circle.js' +import Fill from 'ol/style/Fill.js' +import Stroke from 'ol/style/Stroke.js' +import Style from 'ol/style/Style.js' +import { FeatureInfo } from './feature-info.model' +import { Geometry } from 'ol/geom' +import { FeatureLike } from 'ol/Feature' + +const INFO_SERVICE_URL = import.meta.env.VITE_GET_INFO_SERVICE_URL + +export class FeatureInfoService { + constructor( + // appThemes, + map: Map + // http, + // appGetDevice, + // getLayerFunc, + // getInfoServiceUrl + ) { + // this.appThemes_ = appThemes + this.map = map + // this.http_ = http + // this.appGetDevice_ = appGetDevice + // this.getLayerFunc_ = getLayerFunc + // this.getInfoServiceUrl_ = getInfoServiceUrl + // this.content = [] + // this.QUERYPANEL_ = 'queryPanel' + this.createFeatureLayer() + } + map: Map + featureLayer: VectorLayer> + content: FeatureInfo[] + isQuerying = false + responses: FeatureInfo[] = [] + + createFeatureLayer() { + this.featureLayer = new VectorLayer({ + source: new VectorSource(), + zIndex: 1000, + // altitudeMode: 'clampToGround', + }) + const defaultFill = new Fill({ + color: [255, 255, 0, 0.6], + }) + const circleStroke = new Stroke({ + color: [255, 155, 55, 1], + width: 3, + }) + + const image = new CircleStyle({ + radius: 10, + fill: defaultFill, + stroke: circleStroke, + }) + + this.featureLayer.setStyle( + /** + * @param {ol.Feature|ol.render.Feature} feature Feature. + * @param {number} resolution Resolution. + * @return {Array.} Array of styles. + */ + function (feature: FeatureLike) { + const lineColor = feature.get('color') || '#ffcc33' + const lineWidth = /** @type {number} */ feature.get('width') || 3 + const defaultStyle = [ + new Style({ + fill: new Fill({ + color: [255, 255, 0, 0.6], + }), + }), + new Style({ + stroke: new Stroke({ + color: '#ffffff', + width: 5, + }), + }), + new Style({ + stroke: new Stroke({ + color: lineColor, + width: lineWidth, + }), + }), + ] + + const geometryType = feature.getGeometry()?.getType() + return geometryType == 'Point' || geometryType == 'MultiPoint' + ? [new Style({ image: image })] + : defaultStyle + } + ) + this.map.addLayer(this.featureLayer) + } + + // getFeatureInfoById(fid) { + // const fids = fid.split(',') + // fids.forEach(async curFid => { + // const splittedFid = curFid.split('_') + // const layersList = [splittedFid[0]] + // const layerLabel = {} + + // try { + // const flatCatalogue = await this.appThemes_.getFlatCatalog() + + // if (layersList.length > 0) { + // this.isQuerying_ = true + // this.map_.getViewport().style.cursor = 'wait' + // this.content = [] + + // try { + // const resp = await this.http_.get(this.getInfoServiceUrl_, { + // params: { fid: curFid }, + // }) + + // this.appSelector = this.QUERYPANEL_ + // let showInfo = false + // if (!this.appGetDevice_.testEnv('xs')) { + // showInfo = true + // this.hiddenContent = false + // } else { + // this.hiddenContent = true + // } + + // const node = flatCatalogue.find( + // catItem => catItem.id == splittedFid[0] + // ) + // if (node !== undefined && node !== null) { + // const layer = this.getLayerFunc_(node) + // const foundLayer = this.map_ + // .getLayers() + // .getArray() + // .find( + // curLayer => + // curLayer.get('queryable_id') === layer.get('queryable_id') + // ) + // const idx = this.map_.getLayers().getArray().indexOf(foundLayer) + // if (idx === -1) { + // this.map_.addLayer(layer) + // } + // layerLabel[splittedFid[0]] = layer.get('label') + // } + // this.showInfo(true, resp, layerLabel, showInfo, true) + // } catch (error) { + // this.clearQueryResult(this.QUERYPANEL_) + // this.infoOpen = false + // this.map_.getViewport().style.cursor = '' + // this.isQuerying_ = false + // } + // } + // } catch (error) { + // console.error('Error fetching flat catalog:', error) + // } + // }) + // } + + async singleclickEvent( + evt: MapBrowserEvent, + infoMymaps: boolean + ): Promise { + const layers = this.map.getLayers().getArray() + const layersList = [] + const layerLabel: { [key: string]: string } = {} + + for (let i = layers.length - 1; i >= 0; i--) { + const metadata = layers[i].get('metadata') + if (metadata !== undefined && metadata !== null) { + if ( + metadata['is_queryable'] && + layers[i].getVisible() && + layers[i].getOpacity() > 0 + ) { + const queryableId = layers[i].get('queryable_id') + layersList.push(queryableId) + layerLabel[queryableId] = layers[i].get('label') + } + } + } + + if (layersList.length > 0) { + const resolution = this.map.getView().getResolution() + let bigBuffer = 0 + let smallBuffer = 0 + if (resolution) { + bigBuffer = 20 * resolution + smallBuffer = 1 * resolution + } + + const point = transform( + evt.coordinate, + this.map.getView().getProjection(), + 'EPSG:2169' + ) + const big_box = [ + [point[0] - bigBuffer, point[1] + bigBuffer], + [point[0] + bigBuffer, point[1] - bigBuffer], + ] + const small_box = [ + [point[0] - smallBuffer, point[1] + smallBuffer], + [point[0] + smallBuffer, point[1] - smallBuffer], + ] + + this.isQuerying = true + this.map.getViewport().style.cursor = 'wait' + + this.content = [] + + const size = this.map.getSize() || [0, 0] + const extent = this.map.getView().calculateExtent(size) + const bbox = extent.join(',') + const params = { + layers: layersList.join(), + box1: big_box.join(), + box2: small_box.join(), + srs: 'EPSG:3857', + zoom: this.map.getView().getZoom(), + .../*(!this.map.get('ol3dm').is3dEnabled() &&*/ { + BBOX: bbox, + WIDTH: size[0], + HEIGHT: size[1], + X: evt.pixel[0], + Y: evt.pixel[1], + }, + } + + const url = new URL(INFO_SERVICE_URL) + Object.keys(params).forEach(key => + url.searchParams.append(key, (params as Record)[key]) + ) + + try { + const resp = await fetch(url.toString()) + if (resp.ok) { + const data = await resp.json() + if (data.length > 0) { + //added return here + return this.showInfo( + evt.originalEvent.shiftKey, + data, + layerLabel + // true, + // false + ) + } else { + this.isQuerying = false + this.map.getViewport().style.cursor = '' + // this.lastHighlightedFeatures_ = [] + // this.highlightFeatures_(this.lastHighlightedFeatures_, false) + // this.clearQueryResult(this.QUERYPANEL_) + // if (infoMymaps) { + // if (!this.selectMymapsFeature_(evt.pixel)) { + // this['infoOpen'] = false + // } + // } else { + // this['infoOpen'] = false + // } + } + } else { + throw new Error('Network response was not ok') + } + } catch (error) { + // this.clearQueryResult(this.QUERYPANEL_) + // this['infoOpen'] = false + // this.map.getViewport().style.cursor = '' + // this.isQuerying = false + } + } else { + if (infoMymaps) { + // this.selectMymapsFeature_(evt.pixel) + } + } + } + + showInfo( + shiftKey: boolean, + data: FeatureInfo[], + layerLabel: { [key: string]: string } + // openInfoPanel: boolean + // fit: boolean + ) { + if (shiftKey) { + data.forEach(item => { + item['layerLabel'] = layerLabel[item.layer] + let found = false + for (let iLayer = 0; iLayer < this.responses.length; iLayer++) { + if (this.responses[iLayer].layer == item.layer) { + found = true + let removed = false + for (let iItem = 0; iItem < item.features.length; iItem++) { + for ( + let iFeatures = 0; + iFeatures < this.responses[iLayer].features.length; + iFeatures++ + ) { + if ( + this.responses[iLayer].features[iFeatures]['fid'] == + item.features[iItem]['fid'] + ) { + removed = true + this.responses[iLayer].features.splice(iFeatures, 1) + break + } + } + if (!removed) { + this.responses[iLayer].features = this.responses[ + iLayer + ].features.concat(item.features[iItem]) + } + } + break + } + } + if (!found) { + this.responses.push(item) + } + }) + } else { + this.responses = data + this.responses.forEach(item => { + item['layerLabel'] = layerLabel[item.layer] + }) + } + + this.responses.forEach(item => { + if (item['has_profile']) { + // TODO: integrate profile + // item.features.forEach(feature => { + // const validGeom = this.filterValidProfileFeatures_(feature) + // if (validGeom.geom.getLineStrings().length > 0) { + // feature['attributes']['showProfile'] = { active: true } + // this.getProfile_(validGeom.geom, validGeom.id).then(profile => { + // feature['attributes']['showProfile'] = { active: true } + // feature['attributes']['profile'] = profile + // }) + // } + // }) + } + }) + + // this.clearQueryResult(this.QUERYPANEL_) + // content to be displayed in the info panel + this.content = this.responses.filter(item => { + return 'features' in item && item.features.length > 0 + }) + + // this['infoOpen'] = this.responses.length > 0 ? openInfoPanel : false + + // this.lastHighlightedFeatures_ = [] + // for (let i = 0; i < this.responses.length; i++) { + // this.lastHighlightedFeatures_.push(...this.responses[i].features) + // } + // this.highlightFeatures_(this.lastHighlightedFeatures_, fit) + this.isQuerying = false + this.map.getViewport().style.cursor = '' + //added return here + return this.content + } + + clearQueryResult(/*appSelector*/) { + // this['appSelector'] = appSelector + this.content = [] + this.clearFeatures() + } + + clearFeatures() { + this.featureLayer.getSource()?.clear() + } +} + +// export const featureInfoService = new FeatureInfoService() diff --git a/src/components/info/feature-info.vue b/src/components/info/feature-info.vue new file mode 100644 index 00000000..fb8d7f13 --- /dev/null +++ b/src/components/info/feature-info.vue @@ -0,0 +1,55 @@ + + diff --git a/src/components/info/info-panel.vue b/src/components/info/info-panel.vue index f828a2dd..829fcf4b 100644 --- a/src/components/info/info-panel.vue +++ b/src/components/info/info-panel.vue @@ -2,9 +2,13 @@ import { useTranslation } from 'i18next-vue' import SidePanelLayout from '@/components/common/side-panel-layout.vue' import { useAppStore } from '@/stores/app.store' +import { storeToRefs } from 'pinia' +import { useFeatureInfoStore } from '@/stores/feature-info.store' +import FeatureInfo from '@/components/info/feature-info.vue' const { t } = useTranslation() const appStore = useAppStore() +const { content } = storeToRefs(useFeatureInfoStore()) diff --git a/src/components/info/templates/default-template.vue b/src/components/info/templates/default-template.vue new file mode 100644 index 00000000..bf65369e --- /dev/null +++ b/src/components/info/templates/default-template.vue @@ -0,0 +1,128 @@ + + diff --git a/src/components/info/templates/lignes-bus-template.vue b/src/components/info/templates/lignes-bus-template.vue new file mode 100644 index 00000000..710dd4b9 --- /dev/null +++ b/src/components/info/templates/lignes-bus-template.vue @@ -0,0 +1,59 @@ + + diff --git a/src/components/info/templates/template-utilities.ts b/src/components/info/templates/template-utilities.ts new file mode 100644 index 00000000..9f9adfa6 --- /dev/null +++ b/src/components/info/templates/template-utilities.ts @@ -0,0 +1,191 @@ +import { Feature } from '../feature-info.model' + +export function prefixKeys( + attributes: { [key: string]: any }, + prefix: string +): { + key: string + value: any +}[] { + return Object.entries(attributes) + .filter(([key]) => key !== 'showProfile') + .map(([key, value]) => ({ key: prefix + key, value })) +} + +export function hasAttributes(feature: Feature): boolean { + return ( + feature.attributes !== undefined && + Object.keys(feature.attributes).length > 0 + ) +} + +export function isEmpty(value: string | undefined | null): boolean { + return value === undefined || value === null || value.length === 0 +} + +export function isLink(value: any): boolean { + const strValue = String(value).toLowerCase() + return strValue.startsWith('http://') || strValue.startsWith('https://') +} + +export function showAttributesByLang( + elem: { [key: string]: any }, + layerid: string, + language: string +): boolean { + const { key } = elem + let ok = false + + if ( + layerid === '2407' && + (key === 'f_Cl_erosion' || + key === 'f_Erosion_kl' || + key === 'f_Erosion_Cl' || + key === 'f_Erosioun_K') + ) { + if (language === 'fr' && key === 'f_Cl_erosion') { + ok = true + } else if (language === 'de' && key === 'f_Erosion_kl') { + ok = true + } else if (language === 'en' && key === 'f_Erosion_Cl') { + ok = true + } else if (language === 'lb' && key === 'f_Erosioun_K') { + ok = true + } + } else if ( + layerid === '2714' && + (key === 'f_LABEL_eng' || + key === 'f_LC_class_name_fr' || + key === 'f_LC_class_name_de' || + key === 'f_LC_class_name') + ) { + if (language === 'fr' && key === 'f_LC_class_name_fr') { + ok = true + } else if (language === 'de' && key === 'f_LC_class_name_de') { + ok = true + } else if (language === 'en' && key === 'f_LABEL_eng') { + ok = true + } else if (language === 'lb' && key === 'f_LC_class_name') { + ok = true + } + } else if ( + layerid === '2444' && + (key === 'f_Betriebsform' || + key === 'f_Forme_d_entreprise' || + key === 'f_Operation_Form' || + key === 'f_Form_vun_Geschaeft') + ) { + if (language === 'fr' && key === 'f_Forme_d_entreprise') { + ok = true + } else if (language === 'de' && key === 'f_Betriebsform') { + ok = true + } else if (language === 'en' && key === 'f_Operation_Form') { + ok = true + } else if (language === 'lb' && key === 'f_Form_vun_Geschaeft') { + ok = true + } + } else if ( + key.endsWith('_FR') || + key.endsWith('_DE') || + key.endsWith('_EN') || + key.endsWith('_LU') || + key.endsWith('_LB') + ) { + if (language === 'fr' && key.endsWith('_FR')) { + ok = true + } else if (language === 'de' && key.endsWith('_DE')) { + ok = true + } else if (language === 'en' && key.endsWith('_EN')) { + ok = true + } else if ( + language === 'lb' && + (key.endsWith('_LB') || key.endsWith('_LU')) + ) { + ok = true + } + } else { + ok = true + } + + return ok +} + +export function hasValidFID(feature: { fid?: string }): boolean { + return 'fid' in feature && isFIDValid(feature.fid) +} + +export function isFIDValid(fid: string | undefined): boolean { + if (fid === undefined || fid === null) { + return false + } + + const fids = fid.split(',') + return fids.every(curFid => !!curFid && curFid.split('_').length >= 2) +} + +// TODO: use vue utilities for this + +// export function trustAsHtml(content: string, sce: SceService): any { +// if (typeof content !== 'string') { +// return content +// } +// return sce.trustAsHtml(content) +// } + +// export function getTrustedUrlByLang( +// urlFr: string, +// urlDe: string | null | undefined, +// urlEn: string | null | undefined, +// urlLb: string | null | undefined, +// language: string, +// sce: SceService +// ): any { +// switch (language) { +// case 'fr': +// return sce.trustAsResourceUrl(urlFr) +// case 'de': +// return sce.trustAsResourceUrl(urlDe ?? urlFr) +// case 'en': +// return sce.trustAsResourceUrl(urlEn ?? urlFr) +// case 'lb': +// return sce.trustAsResourceUrl(urlLb ?? urlFr) +// default: +// return sce.trustAsResourceUrl(urlFr) +// } +// } + +// TODO: call v4 exports + +// export function exportGpx( +// feature: Feature, +// name: string | undefined, +// isTrack: boolean, +// map: MapService, +// appExport: ExportService +// ): void { +// const encOpt: ExportOptions = { +// dataProjection: 'EPSG:2169', +// featureProjection: map.getView().getProjection(), +// } + +// if (feature.attributes && !feature.properties) { +// feature.properties = feature.attributes +// } + +// const activeFeature = new olFormatGeoJSON().readFeature(feature, encOpt) +// if (name === undefined) { +// name = 'kml' +// } +// appExport.exportGpx([activeFeature], name, isTrack) +// } + +// export function exportKml( +// feature: Feature, +// name: string | undefined, +// appExport: ExportService +// ): void { +// if (name === undefined) { +// name = 'kml' +// } +// appExport.exportKml(feature, name) +// } diff --git a/src/components/map/map-container.vue b/src/components/map/map-container.vue index 5804e213..e212de96 100644 --- a/src/components/map/map-container.vue +++ b/src/components/map/map-container.vue @@ -16,6 +16,7 @@ import ZoomControl from '../map-controls/zoom-control.vue' import ZoomToExtentControl from '../map-controls/zoom-to-extent-control.vue' import useDraw from '@/composables/draw/draw.composable' import useDrawSelect from '@/composables/draw/draw-select.composable' +import useFeatureInfo from '../info/feature-info.composable' const appStore = useAppStore() const { embedded } = storeToRefs(appStore) @@ -33,12 +34,14 @@ const props = withDefaults( ) // add draw layer after map init to allow restoring draw features (not in v3 for now) -// TODO: remove v4_standalone condition once v4 draw is used in v3 +// TODO: remove v4_standalone condition or move calls outside of it, once v4 draw or feature info is used in v3 if (props.v4_standalone) { const draw = useDraw() draw.addDrawLayer(olMap) // initialise map listeners for feature selection useDrawSelect() + const featureInfo = useFeatureInfo() + featureInfo.init() } const DEFAULT_EXTENT = [ diff --git a/src/stores/feature-info.store.ts b/src/stores/feature-info.store.ts new file mode 100644 index 00000000..806ed32a --- /dev/null +++ b/src/stores/feature-info.store.ts @@ -0,0 +1,33 @@ +import { FeatureInfo } from '@/components/info/feature-info.model' +import { acceptHMRUpdate, defineStore } from 'pinia' +import { shallowRef, ShallowRef } from 'vue' + +export const useFeatureInfoStore = defineStore( + 'featureInfo', + () => { + const content: ShallowRef = shallowRef() + + function setContent(value: FeatureInfo[]) { + if (!value || value.length === 0) { + content.value = undefined + } else { + content.value = [...value] + } + } + + function clearContent() { + content.value = undefined + } + + return { + content, + setContent, + clearContent, + } + }, + {} +) + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useFeatureInfoStore, import.meta.hot)) +} From fa385b1868186939dbf8bb8415e62c418315926c Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Mon, 18 Nov 2024 11:10:04 +0100 Subject: [PATCH 02/45] feat(feature-info): adapt style for default template --- src/assets/main.css | 16 ++++++++++++++++ .../info/templates/default-template.vue | 17 +++++++---------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/assets/main.css b/src/assets/main.css index 1f853ec9..fc0f54aa 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -485,6 +485,22 @@ .lux-legend hr { @apply my-2; } + + .lux-poi-title { + @apply text-title-xl mt-[22px] mb-[11px]; + } + .lux-poi-feature { + @apply bg-white bg-opacity-50 m-[5px] p-[10px]; + } + .lux-poi-feature h4 { + @apply my-[11px] text-[20px]; + } + .lux-poi-feature label { + @apply font-bold mb-[5px]; + } + .lux-poi-feature a { + @apply text-black underline; + } } .fa-solid { diff --git a/src/components/info/templates/default-template.vue b/src/components/info/templates/default-template.vue index bf65369e..d90db520 100644 --- a/src/components/info/templates/default-template.vue +++ b/src/components/info/templates/default-template.vue @@ -20,13 +20,13 @@ defineProps({ const { t } = useTranslation() diff --git a/src/components/footer/footer-bar.vue b/src/components/footer/footer-bar.vue index 7c9336ab..f0968fd4 100644 --- a/src/components/footer/footer-bar.vue +++ b/src/components/footer/footer-bar.vue @@ -89,10 +89,11 @@ function onClickInfoIcon() {
  • From 882943a2612be3c2d30d96568f84e36136529299 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 6 Dec 2024 09:56:09 +0100 Subject: [PATCH 31/45] review(feature-info): rename infoPanelHidden to displayStarOnMobile --- src/components/footer/footer-bar.vue | 8 ++++---- src/composables/info/feature-info.composable.ts | 6 +++--- src/stores/feature-info.store.ts | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/footer/footer-bar.vue b/src/components/footer/footer-bar.vue index f0968fd4..25d6ab6d 100644 --- a/src/components/footer/footer-bar.vue +++ b/src/components/footer/footer-bar.vue @@ -47,12 +47,12 @@ watch(drawToolbarOpen, isOpen => isOpen && (measureToolbarOpen.value = false)) watch(measureToolbarOpen, isOpen => isOpen && (drawToolbarOpen.value = false)) const featureInfoStore = useFeatureInfoStore() -const { infoPanelHidden } = storeToRefs(featureInfoStore) -const { setInfoPanelHidden } = featureInfoStore +const { displayStarOnMobile } = storeToRefs(featureInfoStore) +const { setDisplayStarOnMobile } = featureInfoStore function onClickInfoIcon() { toggleInfoOpen() - setInfoPanelHidden(false) + setDisplayStarOnMobile(false) } @@ -90,7 +90,7 @@ function onClickInfoIcon() {
  • = shallowRef() const isLoading: ShallowRef = shallowRef(false) - const infoPanelHidden: ShallowRef = shallowRef(false) + const displayStarOnMobile: ShallowRef = shallowRef(false) function setFeatureInfoPanelContent(value: FeatureInfoJSON[]) { if (!value || value.length === 0) { @@ -33,20 +33,20 @@ export const useFeatureInfoStore = defineStore( isLoading.value = value } - function setInfoPanelHidden(value: boolean) { - infoPanelHidden.value = value + function setDisplayStarOnMobile(value: boolean) { + displayStarOnMobile.value = value } return { featureInfoPanelContent, fid, isLoading, - infoPanelHidden, + displayStarOnMobile, setFeatureInfoPanelContent, clearContent, setFid, setLoading, - setInfoPanelHidden, + setDisplayStarOnMobile, } }, {} From a3f5fd4d0d035ccf95ed6c43ddc330b2773ec83a Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 6 Dec 2024 10:01:14 +0100 Subject: [PATCH 32/45] review(imports): use @ instead of relative path --- src/components/info/feature-info.vue | 4 ++-- src/components/info/profile-feature-info.vue | 2 +- src/components/info/templates/default-template.vue | 2 +- src/components/info/templates/template-utilities.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/info/feature-info.vue b/src/components/info/feature-info.vue index ea102051..8ee9ab00 100644 --- a/src/components/info/feature-info.vue +++ b/src/components/info/feature-info.vue @@ -5,8 +5,8 @@ import LignesBusTemplate from './templates/lignes-bus-template.vue' import { FeatureInfoJSON, FeatureJSON, -} from '../../services/info/feature-info.model' -import { featureInfoLayerService } from '../../services/info/feature-info-layer.service' +} from '@/services/info/feature-info.model' +import { featureInfoLayerService } from '@/services/info/feature-info-layer.service' import { exportFeatureService, ExportFormat, diff --git a/src/components/info/profile-feature-info.vue b/src/components/info/profile-feature-info.vue index 21fe9459..6dd3e457 100644 --- a/src/components/info/profile-feature-info.vue +++ b/src/components/info/profile-feature-info.vue @@ -1,7 +1,7 @@