public/js/components/app.js (327 lines of code) (raw):

/* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ import { TMSService } from '@elastic/ems-client/target/node'; import { EuiCode, EuiGlobalToastList, EuiHeader, EuiHeaderLink, EuiHeaderLinks, EuiHeaderLogo, EuiHeaderSectionItem, EuiPage, EuiPageBody, EuiPageSection, EuiPanel, EuiProvider, EuiSpacer, EuiToast, EuiToolTip, } from '@elastic/eui'; import { appendIconComponentCache } from '@elastic/eui/es/components/icon/icon'; import { icon as EuiIconAlert } from '@elastic/eui/lib/components/icon/assets/alert'; import { icon as EuiIconEmsApp } from '@elastic/eui/lib/components/icon/assets/app_ems'; import { icon as EuiIconBug } from '@elastic/eui/lib/components/icon/assets/bug'; import { icon as EuiIconDocuments } from '@elastic/eui/lib/components/icon/assets/documents'; import { icon as EuiIconElastic } from '@elastic/eui/lib/components/icon/assets/logo_elastic'; import { icon as EuiIconGithub } from '@elastic/eui/lib/components/icon/assets/logo_github'; import { icon as EuiIconStop } from '@elastic/eui/lib/components/icon/assets/stop'; import React, { Component } from 'react'; import URL from 'url-parse'; import chroma from 'chroma-js'; import { FeatureTable } from './feature_table'; import { LayerDetails } from './layer_details'; import { Map } from './map'; import { TableOfContents } from './table_of_contents'; import { eui } from './theme'; const colorMode = window?.matchMedia?.('(prefers-color-scheme:dark)')?.matches ? 'dark' : 'light'; document.body.setAttribute('data-eui-theme', eui.name); document.body.setAttribute('data-eui-mode', colorMode); // One or more icons are passed in as an object of iconKey (string): IconComponent appendIconComponentCache({ emsApp: EuiIconEmsApp, alert: EuiIconAlert, logoGithub: EuiIconGithub, logoElastic: EuiIconElastic, bug: EuiIconBug, documents: EuiIconDocuments, stop: EuiIconStop }); export const supportedLanguages = [ { key: 'default', label: 'Default' }, { key: 'ar', label: 'العربية' }, { key: 'de', label: 'Deutsch' }, { key: 'en', label: 'English' }, { key: 'es', label: 'Español' }, { key: 'fr-fr', label: 'Français' }, { key: 'hi-in', label: 'हिन्दी' }, { key: 'it', label: 'Italiano' }, { key: 'ja-jp', label: '日本語' }, { key: 'ko', label: '한국어' }, { key: 'pt-pt', label: 'Português' }, { key: 'ru-ru', label: 'русский' }, { key: 'zh-cn', label: '简体中文' }, ]; export class App extends Component { constructor(props) { super(props); this.state = { selectedTileLayer: null, selectedFileLayer: null, selectedLanguage: 'default', selectedColor: null, selectedColorOp: null, selectedPercentage: null, jsonFeatures: null, initialSelection: null, toasts: [] }; this._selectFileLayer = async (fileLayerConfig, skipZoom) => { try { this._featuretable?.startLoading(); const featureCollection = await fileLayerConfig.getGeoJson(); featureCollection.features.forEach((feature, index) => { feature.properties.__id__ = index; }); this.setState({ selectedFileLayer: fileLayerConfig, jsonFeatures: featureCollection, }); this._setFileRoute(fileLayerConfig); this._map.setOverlayLayer(featureCollection, skipZoom, this.state.selectedColor); this._featuretable?.stopLoading(); } catch (error) { this._addToast( 'There was an error', <p><EuiCode>{error.message}</EuiCode></p> ); } }; this._showFeature = (feature) => { this._map.highlightFeature(feature); }; this._filterFeatures = (features) => { this._map.filterFeatures(features); }; this._getTmsSource = (cfg) => cfg.getVectorStyleSheet(); this._selectLanguage = (lang) => { this.setState({ selectedLanguage: lang }, () => { this._updateMap(); }); }; this._selectTmsLayer = async (config) => { const source = await this._getTmsSource(config); const { operation, percentage } = TMSService.colorOperationDefaults.find(c => c.style === config.getId()); this.setState({ selectedTileLayer: config, selectedColorOp: operation, selectedPercentage: percentage }, () => { this._map.setTmsLayer(source, () => { this._updateMap(); }); }); }; this._changeColor = async (color) => { this.setState({ selectedColor: color }, async () => { await this._updateMap(); if (this.state.selectedFileLayer) { this._selectFileLayer(this.state.selectedFileLayer, true); } }); }; this._addToast = (title, text) => { this.setState({ toasts: [{ id: 'error', color: 'danger', title, text }] }); }; this._removeToast = () => { this.setState({ toasts: [] }); }; this._map = null; this._toc = null; this._featuretable = null; } componentDidMount() { document.title = this.props.serviceName; if (!Map.isSupported()) { return; } const baseLayerStyle = colorMode === 'light' ? 'road_map_desaturated' : 'dark_map'; const baseLayer = this.props.layers.tms.find((service) => { return service.getId() === baseLayerStyle; }); this._toc.selectItem(`tms/${baseLayer.getId()}`, baseLayer); const vectorLayerSelection = this._readFileRoute(); if (vectorLayerSelection) { this._map.waitForStyleLoaded(() => { this._selectFileLayer(vectorLayerSelection.config); this._toc.selectItem(vectorLayerSelection.path, vectorLayerSelection.config); }); } } _readFileRoute() { const urlTokens = new URL(window.location, true); // uses layer id as ID. // This is more human readable, and seems more transferable than the machine GCP cloud storage ids. const path = urlTokens.hash.substr(1);// cut off # const tokens = path.split('/'); // this version only supports files for now if (tokens[0] !== 'file') { return; } const id = decodeURIComponent(tokens[1]); if (!id || id === 'undefined') { return; } return { path: `file/${id}`, config: this.props.layers.file.find(layer => layer.hasId(id)) }; } _setFileRoute(layerConfig) { window.location.hash = `file/${layerConfig.getId()}`; } async _updateMap() { if (!this?.state) { return; } // Getting the necessary data to update the map const { selectedTileLayer, selectedLanguage } = this.state; const source = await (selectedTileLayer.getVectorStyleSheet()); const mlMap = this._map._maplibreMap; if (!selectedTileLayer || !source || !mlMap) { return; } // Iterate over map layers to change the layout[text-field] property if (selectedLanguage) { const langKey = selectedLanguage; const lang = supportedLanguages.find(l => l.key === langKey); try { this._map.waitForStyleLoaded(async () => { if (langKey === 'default') { const defaultStyle = await this.state.selectedTileLayer.getVectorStyleSheet(); source.layers.forEach(layer => { const textField = defaultStyle?.layers.find(l => l.id === layer.id)?.layout?.['text-field']; if (textField) { mlMap.setLayoutProperty(layer.id, 'text-field', textField); } }); } else { source.layers.forEach(layer => { const textField = TMSService.transformLanguageProperty(layer, langKey); if (textField) { mlMap.setLayoutProperty(layer.id, 'text-field', textField); } }); } }); } catch (error) { this._addToast( `Error switching to ${lang.label}`, <p><EuiCode>{error.message}</EuiCode></p> ); } } const { selectedColor, selectedColorOp, selectedPercentage } = this.state; try { if (selectedColor && !chroma.valid(selectedColor)) { throw new Error(`${selectedColor} is not a valid color representation`); } source?.layers.forEach(layer => { TMSService .transformColorProperties(layer, selectedColor, selectedColorOp, selectedPercentage) .forEach(({ color, property }) => { mlMap.setPaintProperty(layer.id, property, color); }); }); if (mlMap && mlMap?.redraw === 'function') { mlMap.redraw(); } } catch (error) { this._addToast( `Error blending basemap with ${selectedColor}`, <p><EuiCode>{error.message}</EuiCode></p> ); } } render() { if (!Map.isSupported()) { return (<EuiToast title="Your browser does not support WebGL. Please turn on WebGL in order to use this application." color="danger" iconType="alert" />); } const setMap = (map) => { if (this._map === null) { this._map = map; } }; const setToc = (toc) => { if (this._toc === null) { this._toc = toc; } }; const setFeatureTable = (featuretable) => { if (this._featuretable === null) { this._featuretable = featuretable; } }; // Set up the link on the logo to go to the root or up if relative const fileApUrl = this.props.client.getFileApiUrl(); const logoLink = new URL(fileApUrl).hostname == window.location.hostname ? '../' : '/'; return ( <EuiProvider theme={eui.theme} colorMode={colorMode}> <EuiHeader> <EuiHeaderSectionItem border="right"> <EuiToolTip delay="long" content={`EMS version: ${this.props.client._emsVersion}`}> <EuiHeaderLogo href={logoLink} aria-label={`${this.props.serviceName} home`} iconType="emsApp" > {this.props.serviceName} </EuiHeaderLogo> </EuiToolTip> </EuiHeaderSectionItem> <EuiHeaderSectionItem border="none"> <EuiHeaderLinks gutterSize="xs"> <EuiHeaderLink target="_blank" iconType="logoElastic" href="https://elastic.co"> elastic.co </EuiHeaderLink> <EuiHeaderLink target="_blank" iconType="logoGithub" href="https://www.github.com/elastic/ems-landing-page"> GitHub </EuiHeaderLink> <EuiHeaderLink target="_blank" iconType="bug" href="https://www.github.com/elastic/ems-file-service/issues/new"> Report data issues </EuiHeaderLink> <EuiHeaderLink target="_blank" iconType="documents" href="https://www.elastic.co/elastic-maps-service-terms"> Terms of Service </EuiHeaderLink> </EuiHeaderLinks> </EuiHeaderSectionItem> </EuiHeader> <EuiPage> <TableOfContents layers={this.props.layers} selectedLang={this.state.selectedLanguage} onLanguageSelect={this._selectLanguage} onTmsLayerSelect={this._selectTmsLayer} onFileLayerSelect={this._selectFileLayer} ref={setToc} /> <EuiPageBody> <EuiPageSection className="mainContent"> <EuiPanel paddingSize="none"> <Map ref={setMap} /> </EuiPanel> <EuiSpacer size="l" /> <EuiPanel> <LayerDetails title="Tile Layer" layerConfig={this.state.selectedTileLayer} onLanguageChange={this._selectLanguage} onColorChange={this._changeColor} language={this.state.selectedLanguage} color={this.state.selectedColor} /> </EuiPanel> <EuiSpacer /> { (this.state.selectedFileLayer) && <EuiPanel> <LayerDetails title="Vector Layer" layerConfig={this.state.selectedFileLayer} /> <EuiSpacer size="l" /> <FeatureTable ref={setFeatureTable} jsonFeatures={this.state.jsonFeatures} config={this.state.selectedFileLayer} onShow={this._showFeature} onFilterChange={this._filterFeatures} /> </EuiPanel> } <EuiGlobalToastList toasts={this.state.toasts} dismissToast={this._removeToast} toastLifeTimeMs={3000} /> </EuiPageSection> </EuiPageBody> </EuiPage> </EuiProvider> ); } }