UNPKG

@geogirafe/lib-geoportal

Version:

GeoGirafe is a flexible application to build online geoportals.

726 lines (722 loc) 33.7 kB
import { html as uHtml } from 'uhtml'; import Collection from 'ol/Collection'; import Feature from 'ol/Feature'; import VectorSource from 'ol/source/Vector'; import VectorLayer from 'ol/layer/Vector'; import { LineString, MultiLineString, MultiPolygon, Point, MultiPoint, Polygon } from 'ol/geom'; import { Style, Icon, Stroke, Fill } from 'ol/style'; import { buffer, getWidth, getHeight, getCenter, containsExtent } from 'ol/extent'; import GirafeColorPicker from '../../tools/utils/girafecolorpicker'; import PinIcon from './images/pin.svg'; import LayerIcon from './images/layer.svg'; import LayerGroupIcon from './images/layergroup.svg'; import SearchIcon from './images/search.svg'; import PaintbrushIcon from './images/paintbrush.svg'; import GirafeHTMLElement from '../../base/GirafeHTMLElement'; import MapManager from '../../tools/state/mapManager'; import LayerManager from '../../tools/layers/layermanager'; import { parseCoordinates } from '../../tools/geometrytools'; import ThemesHelper from '../../tools/themes/themeshelper'; import ThemeLayer from '../../models/layers/themelayer'; class SearchComponent extends GirafeHTMLElement { constructor() { super('search'); Object.defineProperty(this, "template", { enumerable: true, configurable: true, writable: true, value: () => { return uHtml `<style> *{font-family:Arial,sans-serif}.hidden{display:none!important}.gg-rotate90{transform:rotate(90deg)}.gg-rotate180{transform:rotate(180deg)}.gg-rotate270{transform:rotate(270deg)}img{filter:var(--svg-filter)}img.legend-image{filter:var(--svg-map-filter);background:var(--svg-legend-bkg)}div{scrollbar-width:thin}a{color:var(--link-color)}a:visited{color:var(--link-color)}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin-wait{0%{transform:rotate(0)}7%{transform:rotate(360deg)}100%{transform:rotate(360deg)}}.gg-spin{animation-name:spin;animation-duration:2s;animation-iteration-count:infinite;animation-timing-function:linear}.gg-spin-wait{animation-name:spin-wait;animation-duration:10s;animation-iteration-count:infinite;animation-timing-function:linear}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#999}.gg-button,.gg-input,.gg-select,.gg-textarea{background-color:var(--bkg-color);color:var(--text-color);outline:0;border-radius:3px;border:var(--app-standard-border);display:inline-block;box-sizing:border-box;padding:0 0 0 .5rem;margin:0;cursor:pointer}.gg-label{background-color:var(--bkg-color);color:var(--text-color);border:none;display:inline-block;padding:0;margin:0}.gg-button,.gg-input,.gg-label,.gg-select{min-height:calc(var(--app-standard-height)/ 1.5);max-height:calc(var(--app-standard-height)/ 1.5);line-height:calc(var(--app-standard-height) / 1.5)}.gg-textarea{padding:.5rem;height:6rem}.gg-input{cursor:text}.gg-checkbox{accent-color:var(--text-color);width:1.2rem}.gg-range{accent-color:var(--text-color)}.gg-button{padding:0 .5rem}.gg-button.active{border:solid 1px var(--text-color-grad2);background-color:var(--text-color-grad2);color:var(--bkg-color)}.gg-button:disabled{background-color:#d3d3d3;color:grey;border:none;cursor:not-allowed}.gg-button>img{vertical-align:middle}.gg-icon-button{border:none;background-color:transparent;display:flex;flex-direction:column;color:var(--text-color);padding:0;align-items:center;justify-content:center;cursor:pointer}.gg-big,.gg-big-withtext{min-width:var(--app-standard-height);min-height:var(--app-standard-height);max-height:var(--app-standard-height)}.gg-big img,.gg-big-withtext img{margin:0;width:calc(var(--app-standard-height) - 1.5rem)}.gg-big-withtext span{font-size:.9rem;font-variant:small-caps;padding:0 1rem}.gg-medium,.gg-medium-withtext{flex-direction:row;min-width:calc(var(--app-standard-height)/ 1.2);min-height:calc(var(--app-standard-height)/ 1.2);max-height:calc(var(--app-standard-height)/ 1.2)}.gg-medium img{margin:0;width:calc(var(--app-standard-height)/ 2.4)}.gg-medium-withtext img{margin-left:.5rem;width:calc(var(--app-standard-height)/ 2.4)}.gg-medium-withtext span{font-size:.9rem;padding:0 1rem;padding-left:.5rem}.gg-small,.gg-small-withtext{flex-direction:row;min-width:calc(var(--app-standard-height)/ 2);min-height:calc(var(--app-standard-height)/ 2);max-height:calc(var(--app-standard-height)/ 2)}.gg-small img{margin:0;width:calc(var(--app-standard-height)/ 3)}.gg-small-withtext img{margin-left:.5rem;width:calc(var(--app-standard-height)/ 3)}.gg-small-withtext span{font-size:.9rem;padding:0 .5rem;padding-left:.3rem}.gg-button:hover,.gg-icon-button:hover,.gg-input:hover,.gg-select:hover,.gg-textarea:hover{background-color:var(--bkg-color-grad1)}.gg-opacity{opacity:.5}.gg-opacity:hover{background-color:transparent;opacity:1}.gg-tabs{display:flex;justify-content:left;margin:.5rem auto 0;border-bottom:1px solid grey;cursor:pointer;height:2rem;align-items:end;gap:1rem;margin-bottom:1rem}.gg-tab{border:none;background:0 0;cursor:pointer;padding:.5rem;color:var(--text-color)}.gg-tab.active{border-bottom:solid 2px var(--text-color-grad2);font-weight:600}.girafe-button-big,.girafe-button-large,.girafe-button-small,.girafe-button-tiny{border:none;background-color:transparent;display:flex;flex-direction:column;color:var(--text-color)}.girafe-button-big:hover,.girafe-button-large:hover,.girafe-button-small:hover,.girafe-button-tiny:hover{background-color:var(--bkg-color-grad1);cursor:pointer}.girafe-button-big.dark,.girafe-button-large.dark,.girafe-button-small.dark,.girafe-button-tiny.dark{background-color:var(--bkg-color);filter:invert(1)}.girafe-button-big{width:var(--app-standard-height);height:var(--app-standard-height);align-items:center;padding:1rem}.girafe-button-big img{overflow:hidden}.girafe-button-large{flex-direction:row}.girafe-button-large img{margin:.3rem;height:2rem}.girafe-button-large span{line-height:2rem;height:2rem;margin:.3rem}.girafe-button-small{min-width:calc(var(--app-standard-height)/ 2);height:calc(var(--app-standard-height)/ 2);align-items:center;padding:.5rem}.girafe-button-small img{overflow:hidden}.girafe-button-small span{width:100%;text-align:left;overflow:hidden;text-overflow:ellipsis}.girafe-button-tiny{width:1rem;height:1rem;align-items:center;padding:0}.girafe-button-tiny img{overflow:hidden} </style><style> .hidden{display:none}#searchbox{background-color:var(--bkg-color);height:2.25rem;width:100%;display:flex;flex-direction:row;border:1px solid #cfd6dd;border-radius:4px}#search{font-size:1rem;outline:0;margin:0 0 0 .5rem;padding-left:.8rem;border:0;height:2.25rem;width:auto;flex:1 1 auto;-ms-flex:1 1 auto;-webkit-flex:1 1 auto;-webkit-appearance:none;background:0 0;color:var(--text-color)}.close-icon,.search-icon,.select-color-icon{width:14px;color:var(--text-color);padding:0 1rem}.close-button,.select-color-button{border:none;padding:0;background-color:transparent;cursor:pointer}#results{z-index:1000;margin-top:.1rem;margin-bottom:.5rem;position:absolute;top:3.5rem;width:36rem;max-height:31rem;background-color:var(--bkg-color);overflow-x:hidden;scrollbar-width:thin}.result,.title{display:inline-block;padding:.3rem 1rem;width:Calc(36rem - 2rem);line-height:1.3rem}.title{color:var(--text-color);font-weight:600;line-height:2.5rem;font-size:1.3rem;padding-top:1.5rem}.result{cursor:pointer;border:none;border-top:solid 1px #ccc;background:0 0;text-align:left;width:100%;color:var(--text-color)}.title img{margin-right:1rem;width:14px}.title span{text-transform:uppercase}.result.selected{background-color:#aaa!important;color:var(--text-color)} </style> <link rel="stylesheet" href="lib/vanilla-picker/vanilla-picker.csp.css" /> <link rel="stylesheet" href="styles/girafecolorpicker.css" /> <div id="searchbox"> <input id="search" class="gg-input" length="20" maxlength="1000" autocomplete="off" autocorrect="off" i18n="Search" placeholder="Search..." oninput="${(e) => this.doSearchDebounced(e)}" onfocusin="${() => this.onFocusIn()}" onfocusout="${() => this.onFocusOut()}" onkeydown="${(e) => this.onKeyDown(e)}" /> <img class="${this.allResults.length > 0 ? 'hidden' : 'search-icon'}" alt="search-icon" src="${this.searchIcon}" /> <button class="${this.allResults.length > 0 ? 'close-button' : 'hidden'}" onclick="${() => this.clearSearch(true)}"> <img class="close-icon" alt="close-icon" src="icons/close.svg" /> </button> <button id="colorPickerBtn" class="${this.paintSearchResults ? 'select-color-button' : 'hidden'}"> <img class="select-color-icon" alt="select-color-icon" src="${this.paintbrushIcon}" /> </button> <div id="results" class="${Object.keys(this.groupedResults).length === 0 || this.forceHide ? 'hidden' : ''}" onmousemove="${() => this.onMouseMove()}"> ${Object.keys(this.groupedResults).map(group => uHtml ` <div class="title"> <img alt="result icon" src="${this.getIcon(group)}" /> <span i18n="${group}"></span> </div> ${this.groupedResults[group].map(result => uHtml ` <button class="${result.selected ? 'result selected' : 'result'}" onmousedown="${() => this.onMouseDown()}" onclick="${() => this.onSelect(result)}" onmouseover="${() => this.onMouseOver(result)}" onmouseleave="${() => this.onMouseLeave()}"> ${result.properties?.label} </button> `)} `)} </div> </div> `; } }); Object.defineProperty(this, "searchIcon", { enumerable: true, configurable: true, writable: true, value: SearchIcon }); Object.defineProperty(this, "paintbrushIcon", { enumerable: true, configurable: true, writable: true, value: PaintbrushIcon }); Object.defineProperty(this, "themesHelper", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "layerManager", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "map", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "previewFeaturesCollection", { enumerable: true, configurable: true, writable: true, value: new Collection() }); Object.defineProperty(this, "previewTheme", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "previewGeoLayer", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "maxExtent", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "ignoreBlur", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "groupedResults", { enumerable: true, configurable: true, writable: true, value: {} }); Object.defineProperty(this, "allResults", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "forceHide", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "searchTermPlaceholder", { enumerable: true, configurable: true, writable: true, value: '###SEARCHTERM###' }); Object.defineProperty(this, "searchLangPlaceholder", { enumerable: true, configurable: true, writable: true, value: '###SEARCHLANG###' }); Object.defineProperty(this, "COORD_REGEX", { enumerable: true, configurable: true, writable: true, value: /^(\d+[.,]?\d*)\s*[,;/\s]\s*(\d+[.,]?\d*)$/ }); Object.defineProperty(this, "focusedResultIndex", { enumerable: true, configurable: true, writable: true, value: -1 }); Object.defineProperty(this, "focusedResult", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "selectedResult", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "searchBox", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "paintSearchResults", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "defaultSearchStrokeColor", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "defaultSearchFillColor", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "abortController", { enumerable: true, configurable: true, writable: true, value: new AbortController() }); // Keeping track of the last input timeout Object.defineProperty(this, "ongoingSearchTimeoutId", { enumerable: true, configurable: true, writable: true, value: 0 }); this.themesHelper = ThemesHelper.getInstance(); this.layerManager = LayerManager.getInstance(); this.map = MapManager.getInstance().getMap(); this.defaultSearchStrokeColor = this.configManager.Config.search.defaultStrokeColor; this.defaultSearchFillColor = this.configManager.Config.search.defaultFillColor; this.createPreviewLayer(); } createPreviewLayer() { this.configManager.loadConfig().then(() => { this.paintSearchResults = this.configManager.Config.search.paintSearchResults; this.maxExtent = this.configManager.Config.map.maxExtent?.split(',').map(Number); this.previewGeoLayer = new VectorLayer({ properties: { addToPrintedLayers: true }, source: new VectorSource({ features: this.previewFeaturesCollection }) }); this.updatePreviewLayerStyle(this.defaultSearchFillColor, this.defaultSearchStrokeColor); this.map.addLayer(this.previewGeoLayer); this.previewGeoLayer.setZIndex(1010); this.initColorPicker(); }); } onMouseDown() { this.ignoreBlur = true; } onFocusIn() { this.forceHide = false; super.render(); } onFocusOut() { if (!this.ignoreBlur) { this.forceHide = true; super.render(); } this.ignoreBlur = false; } render() { super.render(); this.searchBox = this.shadowRoot?.getElementById('search'); } connectedCallback() { this.loadConfig().then(() => { this.render(); super.girafeTranslate(); }); } clearSearch(purge = false) { if (purge) { if (this.searchBox) { this.searchBox.value = ''; } } this.forceHide = false; this.groupedResults = {}; this.allResults = []; this.clearPreview(); this.focusedResultIndex = -1; this.focusedResult = null; super.render(); } async doSearch(e) { // Cancel any previous search this.abortController.abort(); // Create a new controller for the new request const currentAbortController = new AbortController(); this.abortController = currentAbortController; const target = e.target; const term = target.value.trim(); this.clearSearch(); if (this.COORD_REGEX.test(term)) { this.displayCoordinates(term); return; } if (term.length > 0) { const url = this.configManager.Config.search.url .replace(this.searchTermPlaceholder, term) .replace(this.searchLangPlaceholder, this.state.language); try { const response = await fetch(url, { signal: this.abortController.signal }); const data = await response.json(); this.displayResults(data); } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { // Request was aborted, ignore the error console.debug('Multiple parallel search: previous request was aborted.'); return; } } } } /** * Debounce the fetch call to API to prevent sending request at every stroke. * @param e */ async doSearchDebounced(e) { if (this.ongoingSearchTimeoutId !== 0) { clearTimeout(this.ongoingSearchTimeoutId); } // The original even cannot be passed because event objects are ephemeral and // not designed to be used asynchronously (the target property would be undefined) const syntheticEvent = { target: { value: e.target && 'value' in e.target ? e.target.value : undefined } }; this.ongoingSearchTimeoutId = window.setTimeout(() => { this.ongoingSearchTimeoutId = 0; this.doSearch(syntheticEvent); }, 300); } /** * Will render the result of the search with coordinates * @param term typed string */ displayCoordinates(term) { const matches = this.COORD_REGEX.exec(term); const coord1 = parseFloat(matches[1].replace(',', '.')); const coord2 = parseFloat(matches[2].replace(',', '.')); const current_srid = this.map.getView().getProjection().getCode(); const [east_coord, north_coord] = parseCoordinates([coord1, coord2], this.maxExtent, current_srid); // Don't show result if no corresponding coordinates were parsed if (!east_coord || !north_coord) { return; } const result = { bbox: [east_coord, north_coord, east_coord, north_coord], geometry: { type: 'Point', coordinates: [east_coord, north_coord] }, properties: { label: `${coord1} ${coord2}`, layer_name: 'recenter_map' } }; this.allResults = [result]; this.groupedResults['recenter_map'] = [result]; super.render(); super.girafeTranslate(); } displayResults(results) { const uniqueLayersNames = Array.from(new Set(results.features.map((f) => f.properties?.layer_name).filter((name) => name !== undefined))); console.log('uniqueLayersNames:', uniqueLayersNames); // First, group the results results.features.forEach((result) => { let type = 'Unknown layer type'; if (result.properties) { if (result.properties.layer_name) { type = result.properties.layer_name; } else if (result.properties.actions[0].action === 'add_theme') { type = 'add_theme'; } else if (result.properties.actions[0].action === 'add_group') { type = 'add_group'; } else if (result.properties.actions[0].action === 'add_layer') { type = 'add_layer'; } } let resultList; if (type in this.groupedResults) { resultList = this.groupedResults[type]; } else { resultList = []; this.groupedResults[type] = resultList; } resultList.push(result); }); // Manage a flat list with all results this.allResults = Object.values(this.groupedResults).flatMap((results) => results); // And then rerender the results super.render(); super.girafeTranslate(); } getIcon(searchGroup) { switch (searchGroup) { case 'Group': return LayerGroupIcon; case 'Layer': return LayerIcon; default: return PinIcon; } } onMouseOver(result) { this.focusResult(result); } onMouseLeave() { // Clear preview search result, only if the result was not selected if (this.selectedResult === null) { this.clearPreview(); } } focusResultFromIndex() { const result = this.allResults[this.focusedResultIndex]; this.focusResult(result); } focusResult(result) { // Clear old selection and preview this.clearPreview(); if (this.focusedResult) { this.focusedResult.selected = false; } // Set new selected object, and activate preview this.focusedResultIndex = this.allResults.findIndex((r) => r === result); this.focusedResult = this.allResults[this.focusedResultIndex]; this.focusedResult.selected = true; this.render(); this.preview(result); // Scroll to selected div const resultHtmlElement = this.shadow.querySelectorAll('.result')[this.focusedResultIndex]; resultHtmlElement.scrollIntoView({ block: 'nearest' }); } preview(result) { if (result.bbox && this.configManager.Config.search.objectPreview) { // Result with geometry if (result.geometry) { this.addFeatureToPreview(result.geometry); this.updatePreviewLayerStyle(); } } if (result.properties?.actions[0].action === 'add_layer' && this.configManager.Config.search.layerPreview) { const layer = this.themesHelper.findLayerByName(result.properties?.actions[0].data); const clonedTheme = this.getMinimalClonedThemeForLayer(layer); if (!this.isAlreadyPresent(clonedTheme)) { // Preview layer clonedTheme.order = 0; clonedTheme.isExpanded = true; this.previewTheme = clonedTheme; this.state.layers.layersList.push(clonedTheme); this.layerManager.toggleGroupOrTheme(clonedTheme, 'on'); } } } isAlreadyPresent(theme) { return this.state.layers.layersList.some((t) => t.id === theme.id); } getMinimalClonedThemeForLayer(layer) { const hierarchy = this.getHierarchyFromLayer(layer); const theme = hierarchy[0]; const clone = theme.clone(); // Remove unnecessary clones let children = clone.children; for (let i = 1; i < hierarchy.length; ++i) { this.removeOthers(children, hierarchy[i]); children[0].isExpanded = true; children = children[0].children; } return clone; } removeOthers(layers, keep) { for (let i = layers.length - 1; i >= 0; --i) { if (layers[i].id !== keep.id) { layers.splice(i, 1); } } } getHierarchyFromLayer(layer) { if (layer instanceof ThemeLayer) { return [layer]; } if (!layer.parent) { throw new Error('A group or a layer should always have a parent.'); } const parents = this.getHierarchyFromLayer(layer.parent); return [...parents, layer]; } addFeatureToPreview(geometry) { switch (geometry.type) { case 'Point': { const feature = new Feature(new Point(geometry.coordinates)); this.previewFeaturesCollection.push(feature); return; } case 'MultiPoint': { const feature = new Feature(new MultiPoint(geometry.coordinates)); this.previewFeaturesCollection.push(feature); return; } case 'MultiLineString': { const feature = new Feature(new MultiLineString(geometry.coordinates)); this.previewFeaturesCollection.push(feature); return; } case 'LineString': { const feature = new Feature(new LineString(geometry.coordinates)); this.previewFeaturesCollection.push(feature); return; } case 'Polygon': { const feature = new Feature(new Polygon(geometry.coordinates)); this.previewFeaturesCollection.push(feature); return; } case 'MultiPolygon': { const feature = new Feature(new MultiPolygon(geometry.coordinates)); this.previewFeaturesCollection.push(feature); return; } case 'GeometryCollection': { geometry.geometries.forEach((geom) => { this.addFeatureToPreview(geom); }); return; } default: throw new Error(`Geometry type of search result is not being supported.`); } } clearPreview() { // Clear preview search result this.previewFeaturesCollection.clear(); // Clear preview layer if (this.previewTheme) { this.layerManager.toggle(this.previewTheme, 'off'); const index = this.state.layers.layersList.findIndex((l) => l.treeItemId === this.previewTheme?.treeItemId); if (index >= 0) { this.state.layers.layersList.splice(index, 1); } else { console.warn('Error while removing preview layer.'); } this.previewTheme = null; } } onSelect(result) { this.selectedResult = result; this.ignoreBlur = false; this.forceHide = true; this.previewTheme = null; super.render(); if (result.bbox) { // Result with geometry this.zoomTo(result.bbox); } else { let clonedTheme; if (result.properties?.actions[0].action === 'add_theme') { const theme = this.themesHelper.findThemeByName(result.properties?.actions[0].data); clonedTheme = theme.clone(); } else if (result.properties?.actions[0].action === 'add_group') { const group = this.themesHelper.findGroupByName(result.properties?.actions[0].data); clonedTheme = this.getMinimalClonedThemeForLayer(group); } else if (result.properties?.actions[0].action === 'add_layer') { const layer = this.themesHelper.findLayerByName(result.properties?.actions[0].data); clonedTheme = this.getMinimalClonedThemeForLayer(layer); } else { console.warn('Unsupported result type'); } if (clonedTheme && !this.isAlreadyPresent(clonedTheme)) { clonedTheme.order = 0; this.state.layers.layersList.push(clonedTheme); } } this.onFocusOut(); // Update searchbox with result if (this.searchBox && result.properties) { this.searchBox.value = result.properties.label; } } zoomTo(extent) { // We create a buffer around the extent from 50% of the width/height const bufferValue = Math.max((getWidth(extent) * 50) / 100, (getHeight(extent) * 50) / 100); const bufferedExtent = buffer(extent, bufferValue); const minResolution = this.configManager.Config.search.minResolution; const currentResolution = this.map.getView().getResolution(); const currentExtent = this.map.getView().calculateExtent(); if (minResolution) { if (currentResolution > minResolution) { // If we are in a bigger resolution as the minimal one, // Zoom to object with minResolution MapManager.getInstance().zoomToExtent(bufferedExtent, minResolution); } else if (!containsExtent(currentExtent, extent)) { // Else, if the extent is NOT already within the current extent of the map // We keep the current resolution, and just pan to object this.state.position.center = getCenter(extent); } } // Otherwise, if the searched object is already in the current map extent // We do nothing } onMouseMove() { // if the mouse moves, we activate the hover effect const results = this.shadowRoot?.querySelectorAll('.result'); for (const result of results) { result.classList.remove('active'); const htmlResult = result; htmlResult.style.removeProperty('background-color'); } } onKeyDown(e) { // clear search on escape if (e.key === 'Escape') { this.clearSearch(true); } // automatic re-open search results on enter else if (this.forceHide) { if (e.key === 'Enter') { this.onFocusIn(); } } // navigate through search results else if (!this.forceHide) { this.navigateToResult(e); } } navigateToResult(e) { switch (e.key) { case 'ArrowDown': if (this.focusedResultIndex < this.allResults.length - 1) { this.focusedResultIndex += 1; this.focusResultFromIndex(); } break; case 'ArrowUp': if (this.focusedResultIndex > 0) { this.focusedResultIndex -= 1; this.focusResultFromIndex(); } break; case 'Enter': if (this.focusedResultIndex >= 0) { this.onSelect(this.allResults[this.focusedResultIndex]); } break; } } initColorPicker() { super.render(); const colorPicker = this.shadowRoot?.getElementById('colorPickerBtn'); if (colorPicker) { const fillPicker = new GirafeColorPicker({ parent: colorPicker, color: this.configManager.Config.search.defaultStrokeColor, popup: 'right' }); fillPicker.onChange = (color) => { // The fill color should be the selected color with a bit more transparency const fillColor = [color.rgba[0], color.rgba[1], color.rgba[2], color.rgba[3] / 2]; this.updatePreviewLayerStyle(fillColor, color.hex); }; } } updatePreviewLayerStyle(fillColor, strokeColor) { // Only update style if new colors were provided via color picker or default colors have changed if (this.defaultColorHasChanged()) { this.defaultSearchFillColor = this.configManager.Config.search.defaultFillColor; this.defaultSearchStrokeColor = this.configManager.Config.search.defaultStrokeColor; } fillColor ??= this.defaultSearchFillColor; strokeColor ??= this.defaultSearchStrokeColor; this.previewGeoLayer.setStyle(new Style({ stroke: new Stroke({ color: strokeColor, width: this.configManager.Config.search.defaultStrokeWidth }), fill: new Fill({ color: fillColor }), image: new Icon({ anchor: [0.5, 1], anchorXUnits: 'fraction', anchorYUnits: 'fraction', src: this.getColoredPinIcon(strokeColor), scale: 0.3 }) })); } defaultColorHasChanged() { return (this.defaultSearchFillColor !== this.configManager.Config.search.defaultFillColor || this.defaultSearchStrokeColor !== this.configManager.Config.search.defaultStrokeColor); } getColoredPinIcon(hexColor) { const pin = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" style="fill: ${hexColor};" viewBox="0 0 384 512"> <path d="M215.7 499.2C267 435 384 279.4 384 192C384 86 298 0 192 0S0 86 0 192c0 87.4 117 243 168.3 307.2c12.3 15.3 35.1 15.3 47.4 0zM192 128a64 64 0 1 1 0 128 64 64 0 1 1 0-128z"/> </svg>`; return 'data:image/svg+xml;utf8,' + encodeURIComponent(pin); } } export default SearchComponent;