diff --git a/CHANGELOG.md b/CHANGELOG.md index 070d580..70579a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +### [5.0.0](https://github.com/eea/volto-eea-map/compare/4.1.0...5.0.0) - 25 July 2024 + +#### :house: Internal changes + +- style: Automated code fix [eea-jenkins - [`aadf771`](https://github.com/eea/volto-eea-map/commit/aadf771b40e0a190c29de42d99466d2e6ef440e7)] +- style: Automated code fix [eea-jenkins - [`edfae3b`](https://github.com/eea/volto-eea-map/commit/edfae3b8ab4cdef23ae23ed7ea008043601dd783)] +- style: Automated code fix [eea-jenkins - [`fd6c49f`](https://github.com/eea/volto-eea-map/commit/fd6c49f6d0d5081a230f9b4427844eace0321361)] +- style: Automated code fix [eea-jenkins - [`5bcee12`](https://github.com/eea/volto-eea-map/commit/5bcee12ab667ad89096baae214ade24fa1c98fd1)] +- style: Automated code fix [eea-jenkins - [`9bf0079`](https://github.com/eea/volto-eea-map/commit/9bf0079694c16fa20c857f896e303850cf67da43)] +- style: Automated code fix [eea-jenkins - [`3d9adb6`](https://github.com/eea/volto-eea-map/commit/3d9adb6cee7ff1c45ae88d816e7f17de4bbaba37)] +- style: Automated code fix [eea-jenkins - [`f310427`](https://github.com/eea/volto-eea-map/commit/f3104279ad719b16a149a357c0eab2f8b7fd8ef7)] +- style: Automated code fix [eea-jenkins - [`fcf5adf`](https://github.com/eea/volto-eea-map/commit/fcf5adf32a73d388d97597dd9c88c3da362f90b8)] + +#### :hammer_and_wrench: Others + +- remove uneeded logo [Miu Razvan - [`759a07b`](https://github.com/eea/volto-eea-map/commit/759a07b587b8298d88800346a230fd55304aadf9)] +- fix bugs [Miu Razvan - [`ccccc8a`](https://github.com/eea/volto-eea-map/commit/ccccc8a1fac321d3968ffdc0843ff87138748476)] +- fix infinite rerenders [Miu Razvan - [`8dbb501`](https://github.com/eea/volto-eea-map/commit/8dbb5016741f2518668cfe1d40abe1484b53bfc3)] +- lint fix [Miu Razvan - [`56c2138`](https://github.com/eea/volto-eea-map/commit/56c2138099d7425e21a566cb13fee814acdaea68)] +- merge origin/develop into revamp branch [Miu Razvan - [`01ea06c`](https://github.com/eea/volto-eea-map/commit/01ea06ccad2b337a28f4e5847116a41bdda749e3)] +- fix zoom to layer extent [Miu Razvan - [`28bc084`](https://github.com/eea/volto-eea-map/commit/28bc08456fbaa66938e8d0bf5f25ceaff753fae3)] +- finalize eea-map refactoring [Miu Razvan - [`3c6abf9`](https://github.com/eea/volto-eea-map/commit/3c6abf998e3fdca55b1e81a0553749ee90bfbcd6)] +- update [Miu Razvan - [`65205bc`](https://github.com/eea/volto-eea-map/commit/65205bc494f1d3c1b381ed6f9af01ed022e87c75)] +- Update [Miu Razvan - [`13468a3`](https://github.com/eea/volto-eea-map/commit/13468a3c6738e612115535813e64b0a6af4c97d7)] +- wip [Miu Razvan - [`901fcc3`](https://github.com/eea/volto-eea-map/commit/901fcc318c618c0307a64081002ab4a048be70a7)] +- Update [Miu Razvan - [`d70dcf2`](https://github.com/eea/volto-eea-map/commit/d70dcf293b1395741ccbc524fa19145c829f72a9)] +- Update [Miu Razvan - [`a35b941`](https://github.com/eea/volto-eea-map/commit/a35b941700d985c6f3b53247cb60ecc103c4f339)] +- Widget component + other [Miu Razvan - [`291f5ad`](https://github.com/eea/volto-eea-map/commit/291f5ad97945cacb1bc4c6d12d177f232b907184)] +- update [Miu Razvan - [`b073166`](https://github.com/eea/volto-eea-map/commit/b073166de8be0b709e5b2ec397ba9d768c8c2fc8)] ### [4.1.0](https://github.com/eea/volto-eea-map/compare/4.0.0...4.1.0) - 7 June 2024 #### :hammer_and_wrench: Others diff --git a/Jenkinsfile b/Jenkinsfile index a04b5df..725c343 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,7 +9,7 @@ pipeline { environment { GIT_NAME = "volto-eea-map" NAMESPACE = "@eeacms" - SONARQUBE_TAGS = "volto.eea.europa.eu,forest.eea.europa.eu,climate-adapt.eea.europa.eu,biodiversity.europa.eu,demo-www.eea.europa.eu,www.eea.europa.eu-en,water.europa.eu-freshwater,water.europa.eu-marine" + SONARQUBE_TAGS = "volto.eea.europa.eu,forest.eea.europa.eu,climate-adapt.eea.europa.eu,biodiversity.europa.eu,demo-www.eea.europa.eu,www.eea.europa.eu-en,water.europa.eu-freshwater,water.europa.eu-marine,insitu.copernicus.eu" DEPENDENCIES = "@eeacms/volto-embed" BACKEND_PROFILES = "eea.kitkat:testing" BACKEND_ADDONS = "" diff --git a/package.json b/package.json index dffe4c0..bfba0f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@eeacms/volto-eea-map", - "version": "4.1.0", + "version": "5.0.0", "description": "@eeacms/volto-eea-map: Volto add-on", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", @@ -21,16 +21,19 @@ "@eeacms/volto-object-widget" ], "dependencies": { + "@arcgis/core": "4.29.10", "@eeacms/volto-embed": "*", "@eeacms/volto-object-widget": "*", "@plone/scripts": "*", "esri-loader": "3.6.0", + "jsoneditor": "10.1.0", "lodash": "4.17.21", "razzle-plugin-scss": "4.2.18", "react-color": "~2.19.3", - "react-querybuilder": "4.2.3" + "react-querybuilder": "6.5.5" }, "devDependencies": { + "@babel/preset-env": "7.24.6", "@cypress/code-coverage": "^3.10.0", "@plone/scripts": "*", "babel-plugin-transform-class-properties": "^6.24.1", diff --git a/src/Arcgis/Editor/Editor.jsx b/src/Arcgis/Editor/Editor.jsx new file mode 100644 index 0000000..2718a79 --- /dev/null +++ b/src/Arcgis/Editor/Editor.jsx @@ -0,0 +1,130 @@ +import { memo, useRef, useState, useMemo } from 'react'; +import { isNil } from 'lodash'; + +import SidebarGroup from './SidebarGroup'; + +import _MapBuilder from '../Map/MapBuilder'; + +import { + StructureBaseLayerPanel, + StructureLayersPanel, + StructureWidgetsPanel, + SettingsGeneralPanel, + SettingsLayersPanel, +} from './Panels'; + +import EditorContext from './EditorContext'; + +import 'react-querybuilder/dist/query-builder.css'; +import 'jsoneditor/dist/jsoneditor.min.css'; + +const MapBuilder = memo(_MapBuilder); + +const panels = { + structure: [ + { + title: 'Base layer', + Panel: StructureBaseLayerPanel, + }, + { + title: 'Layers', + Panel: StructureLayersPanel, + }, + { + title: 'Widgets', + Panel: StructureWidgetsPanel, + }, + ], + settings: [ + { title: 'General', Panel: SettingsGeneralPanel }, + { title: 'Layers', Panel: SettingsLayersPanel }, + ], +}; + +function useApi() { + const [data, setData] = useState({}); + const [loading, setLoading] = useState({}); + const [loaded, setLoaded] = useState({}); + const [error, setError] = useState({}); + + const load = async (url, opts) => { + if (data[url]) return data[url]; + let response, result; + setLoading((prev) => ({ ...prev, [url]: true })); + try { + response = await fetch(`${url}?f=json`, opts); + } catch { + response = { ok: false, statusText: 'Unexpected error' }; + } + try { + result = await response.json(); + } catch { + result = response.ok + ? { + code: 500, + message: 'Unexpected error', + } + : { + code: response.status, + message: response.statusText, + }; + } + + if (!response.ok || (!isNil(result.code) && result.code !== 200)) { + setData((prev) => ({ ...prev, [url]: null })); + setError((prev) => ({ ...prev, [url]: result })); + setLoading((prev) => ({ ...prev, [url]: false })); + setLoaded((prev) => ({ ...prev, [url]: false })); + return; + } + setData((prev) => ({ ...prev, [url]: result })); + setError((prev) => ({ ...prev, [url]: null })); + setLoading((prev) => ({ ...prev, [url]: false })); + setLoaded((prev) => ({ ...prev, [url]: true })); + }; + + return { data, loading, loaded, error, load }; +} + +export default function Editor({ value, properties, onChangeValue }) { + const $map = useRef(null); + const [active, setActive] = useState({ + sidebar: 'structure', + panel: panels.structure[0], + }); + const servicesApi = useApi(); + const layersApi = useApi(); + + const Panel = useMemo(() => active.panel.Panel, [active]); + + return ( + +
+
+
+ {Object.keys(panels).map((panel) => ( + + ))} +
+
+ +
+
+
+ +
+
+
+ ); +} diff --git a/src/Arcgis/Editor/EditorContext.jsx b/src/Arcgis/Editor/EditorContext.jsx new file mode 100644 index 0000000..66c19a4 --- /dev/null +++ b/src/Arcgis/Editor/EditorContext.jsx @@ -0,0 +1,2 @@ +import { createContext } from 'react'; +export default createContext(null); diff --git a/src/Arcgis/Editor/Fold/Fold.jsx b/src/Arcgis/Editor/Fold/Fold.jsx new file mode 100644 index 0000000..6ffaedc --- /dev/null +++ b/src/Arcgis/Editor/Fold/Fold.jsx @@ -0,0 +1,56 @@ +import { useState } from 'react'; +import cx from 'classnames'; + +import { Icon } from '@plone/volto/components'; + +import clearSVG from '@plone/volto/icons/clear.svg'; +import upKeySVG from '@plone/volto/icons/up-key.svg'; + +export default function Fold({ + children, + title, + icon, + foldable, + deletable, + onDelete, +}) { + const [fold, setFold] = useState(false); + + const isfolded = foldable && fold; + + return ( +
+
setFold(!fold), + onKeyDown: () => {}, + } + : {})} + > +
+ {foldable && ( + + )} + {!!icon && } + {!!title &&
{title}
} +
+ {deletable && ( + { + onDelete(e); + e.stopPropagation(); + }} + /> + )} +
+ {!isfolded &&
{children}
} +
+ ); +} diff --git a/src/Arcgis/Editor/Panels/Panel.jsx b/src/Arcgis/Editor/Panels/Panel.jsx new file mode 100644 index 0000000..32df1b4 --- /dev/null +++ b/src/Arcgis/Editor/Panels/Panel.jsx @@ -0,0 +1,8 @@ +export default function Panel({ header, content }) { + return ( +
+ {!!header &&
{header}
} + {!!content &&
{content}
} +
+ ); +} diff --git a/src/Arcgis/Editor/Panels/SettingsGeneralPanel.jsx b/src/Arcgis/Editor/Panels/SettingsGeneralPanel.jsx new file mode 100644 index 0000000..c0ed6d8 --- /dev/null +++ b/src/Arcgis/Editor/Panels/SettingsGeneralPanel.jsx @@ -0,0 +1,217 @@ +import { toNumber } from 'lodash'; +import { InlineForm } from '@plone/volto/components'; +import Panel from './Panel'; +import Fold from '../Fold/Fold'; +import { getDefaultWidgets } from '@eeacms/volto-eea-map/constants'; + +const mapSchema = { + title: '', + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['dimension'], + }, + ], + properties: { + dimension: { + title: 'Dimension', + choices: [ + ['2d', '2D'], + ['3d', '3D'], + ], + }, + }, + required: [], +}; + +const getViewConstraintsSchema = ({ $map }) => { + return { + title: 'Map View', + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['initialExtent'], + }, + { + id: 'constraints', + title: 'Constraints', + fields: [ + 'minScale', + 'maxScale', + 'minZoom', + 'maxZoom', + 'rotationEnabled', + ], + }, + ], + properties: { + initialExtent: { + title: 'Initial viewpoint', + widget: 'arcgis_viewpoint', + $map, + }, + minScale: { + title: 'Min scale', + type: 'number', + minimum: 0, + }, + maxScale: { + title: 'Max scale', + type: 'number', + minimum: 0, + }, + minZoom: { + title: 'Min zoom', + type: 'number', + minimum: 0, + }, + maxZoom: { + title: 'Max zoom', + type: 'number', + minimum: 0, + }, + rotationEnabled: { + title: 'Rotation enabled', + type: 'boolean', + }, + }, + required: [], + }; +}; + +export default function SettingsGeneralPanel({ $map, value, onChangeValue }) { + const viewConstraints = value.settings?.view?.constraints || {}; + const dimension = value.settings?.map?.dimension || '2d'; + + return ( + + + { + e.stopPropagation(); + }} + style={{ color: '#fff' }} + target="_blank" + rel="noopener noreferrer" + > + Map + + + } + foldable + > + { + let $fieldValue = fieldValue; + + onChangeValue({ + ...value, + ...(id === 'dimension' + ? { + widgets: getDefaultWidgets($fieldValue), + } + : {}), + settings: { + ...(value.settings || {}), + map: { + ...(value.settings?.map || {}), + [id]: $fieldValue, + }, + }, + }); + }} + /> + + {dimension === '2d' && ( + + { + e.stopPropagation(); + }} + style={{ color: '#fff' }} + target="_blank" + rel="noopener noreferrer" + > + View + + + } + foldable + > + { + let $fieldValue = fieldValue; + + if ( + ['minScale', 'maxScale', 'minZoom', 'maxZoom'].includes(id) + ) { + $fieldValue = Math.max(toNumber(fieldValue) || 0, 0); + } + + if (id === 'initialExtent') { + const center = [ + $fieldValue.longitude || 0, + $fieldValue.latitude || 0, + ]; + const zoom = $fieldValue.zoom; + + onChangeValue({ + ...value, + settings: { + ...(value.settings || {}), + view: { + ...(value.settings?.view || {}), + center, + zoom, + }, + }, + }); + return; + } + + onChangeValue({ + ...value, + settings: { + ...(value.settings || {}), + view: { + ...(value.settings?.view || {}), + constraints: { + ...(value.settings?.view?.constraints || {}), + [id]: $fieldValue, + }, + }, + }, + }); + }} + /> + + )} + + } + /> + ); +} diff --git a/src/Arcgis/Editor/Panels/SettingsLayersPanel.jsx b/src/Arcgis/Editor/Panels/SettingsLayersPanel.jsx new file mode 100644 index 0000000..69692f8 --- /dev/null +++ b/src/Arcgis/Editor/Panels/SettingsLayersPanel.jsx @@ -0,0 +1,216 @@ +import { useState, useEffect, useContext, useMemo } from 'react'; +import { Segment, Dimmer, Loader } from 'semantic-ui-react'; +import { isNil, toNumber } from 'lodash'; +import { v4 as uuid } from 'uuid'; +import { InlineForm } from '@plone/volto/components'; +import { + debounce, + getLayers, + getLayerDefaults, +} from '@eeacms/volto-eea-map/Arcgis/helpers'; +import { blendModes } from '@eeacms/volto-eea-map/constants'; +import EditorContext from '@eeacms/volto-eea-map/Arcgis/Editor/EditorContext'; +import Panel from './Panel'; +import Fold from '../Fold/Fold'; + +function Layer({ $map, layer, layers, index, value, onChangeValue }) { + const uid = useState(uuid()); + const [isReady, setIsReady] = useState(false); + const { layersApi } = useContext(EditorContext); + + const layerPath = useMemo( + () => + !isNil(layer.url) && !isNil(layer.id) ? `${layer.url}/${layer.id}` : null, + [layer.url, layer.id], + ); + + const $layer = useMemo( + () => ({ + data: layersApi.data[layerPath], + error: layersApi.error[layerPath], + loading: layersApi.loading[layerPath], + loaded: layersApi.loaded[layerPath], + load: layersApi.load, + }), + [layersApi, layerPath], + ); + + const schema = useMemo( + () => ({ + title: 'General', + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['blendMode', 'minScale', 'maxScale', 'opacity', 'renderer'], + }, + ], + properties: { + blendMode: { + title: 'Blend mode', + choices: blendModes, + }, + minScale: { + title: 'Min scale', + type: 'number', + minimum: 0, + }, + maxScale: { + title: 'Max scale', + type: 'number', + minimum: 0, + }, + opacity: { + title: 'Opacity', + widget: 'arcgis_slider', + }, + renderer: { + title: 'Renderer', + widget: 'arcgis_renderer', + $map, + }, + }, + required: [], + }), + [$map], + ); + + useEffect(() => { + if (!$layer.loaded && !$layer.loading && !$layer.error && layerPath) { + debounce( + () => { + $layer.load(layerPath); + }, + 300, + `fetch:${uid}:${layerPath}`, + ); + } + }, [uid, $layer, layerPath]); + + useEffect(() => { + if (!$layer.loaded || $layer.data.id !== layer.id) return; + const defaults = getLayerDefaults($layer.data); + if (JSON.stringify(defaults) !== JSON.stringify(layer.defaults)) { + onChangeValue({ + ...value, + layers: layers.map((layer, i) => { + if (i !== index) return layer; + return { + ...layer, + defaults, + }; + }), + }); + } + setIsReady(true); + }, [index, layer, $layer, layers, value, onChangeValue]); + + if (!isReady) return null; + + return ( + <> + + + + + { + let $fieldValue = fieldValue; + + if ( + ['minScale', 'maxScale', 'minZoom', 'maxZoom', 'opacity'].includes( + id, + ) + ) { + $fieldValue = Math.max(toNumber(fieldValue) || 0, 0); + } + + onChangeValue({ + ...value, + layers: layers.map((layer, i) => { + if (i !== index) return layer; + return { + ...layer, + [id]: $fieldValue, + }; + }), + }); + }} + /> + {layer.defaults && ( + + + + )} + + ); +} + +export default function SettingsLayersPanel({ + $map, + value, + properties, + onChangeValue, +}) { + const data_query = properties?.data_query; + + const layers = useMemo( + () => + getLayers( + { layers: value.layers, styles: value.styles, data_query }, + false, + ), + [value.layers, value.styles, data_query], + ); + + return ( + + {layers.map((layer, index) => ( + + + + ))} + + } + /> + ); +} diff --git a/src/Arcgis/Editor/Panels/StructureBaseLayerPanel.jsx b/src/Arcgis/Editor/Panels/StructureBaseLayerPanel.jsx new file mode 100644 index 0000000..56d2afd --- /dev/null +++ b/src/Arcgis/Editor/Panels/StructureBaseLayerPanel.jsx @@ -0,0 +1,60 @@ +import { useMemo } from 'react'; +import { InlineForm } from '@plone/volto/components'; +import { basemaps } from '@eeacms/volto-eea-map/constants'; +import { getBasemap } from '@eeacms/volto-eea-map/Arcgis/helpers'; +import Fold from '../Fold/Fold'; + +import Panel from './Panel'; + +const schema = { + title: 'Base layer', + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['name', 'url_template'], + }, + ], + properties: { + name: { + title: 'Name', + choices: basemaps, + }, + url_template: { + title: 'Custom basemap', + widget: 'textarea', + }, + }, + required: [], +}; + +export default function StructureBaseLayerPanel({ value, onChangeValue }) { + const basemap = useMemo( + () => getBasemap({ basemap: value.basemap, base: value.base }) || {}, + [value.basemap, value.base], + ); + + return ( + + { + const $value = { ...value }; + delete $value.base; // not needed (backward compatibility) + onChangeValue({ + ...$value, + basemap: { + ...basemap, + [id]: fieldValue, + }, + }); + }} + /> + + } + /> + ); +} diff --git a/src/Arcgis/Editor/Panels/StructureLayersPanel.jsx b/src/Arcgis/Editor/Panels/StructureLayersPanel.jsx new file mode 100644 index 0000000..513a4fd --- /dev/null +++ b/src/Arcgis/Editor/Panels/StructureLayersPanel.jsx @@ -0,0 +1,394 @@ +import { memo, useState, useEffect, useContext, useMemo } from 'react'; +import { compose } from 'redux'; +import { isNil, toNumber } from 'lodash'; +import { v4 as uuid } from 'uuid'; +import { QueryBuilder, Rule as QBRule, useRule } from 'react-querybuilder'; +import { Dimmer, Loader } from 'semantic-ui-react'; +import { Icon, InlineForm } from '@plone/volto/components'; +import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable'; +import addSVG from '@plone/volto/icons/add.svg'; +import { + debounce, + getLayers, + getLayerDefaults, +} from '@eeacms/volto-eea-map/Arcgis/helpers'; +import EditorContext from '@eeacms/volto-eea-map/Arcgis/Editor/EditorContext'; +import Panel from './Panel'; +import Fold from '../Fold/Fold'; + +function getLayersChoices(layers = []) { + return layers.map((layer) => [ + layer.id.toString(), + `${ + (layer.parentLayerId > -1 ? `${layer.parentLayerId}.` : '') + layer.id + } - ${layer.name} (${layer.type})`, + ]); +} + +function getSublayers(subLayerIds, data) { + return subLayerIds?.reduce((acc, subLayerId) => { + const subLayer = data?.layers.find((layer) => layer.id === subLayerId); + if (!subLayer) return acc; + acc.push({ + ...(subLayer || {}), + id: subLayerId, + subLayers: getSublayers(subLayer?.subLayerIds, data), + }); + return acc; + }, []); +} + +const RuleComponent = memo((props) => { + const [inputValue, setInputValue] = useState(''); + const r = useRule(props); + + const dataQuery = props.rule.dataQuery; + + function handleKeyDown(event) { + if (!inputValue) return; + switch (event.key) { + case 'Enter': + case 'Tab': + if (dataQuery?.includes(inputValue)) { + setInputValue(''); + event.preventDefault(); + break; + } + r.generateOnChangeHandler('dataQuery')([ + ...(dataQuery || []), + event.target.value, + ]); + setInputValue(''); + event.preventDefault(); + break; + default: + break; + } + } + + const Select = props.reactSelectCreateable.default; + + return ( +
+
+ +

+ When using page level parameters to filter the map, please specify the + corresponding name +

+ { + return onChange(id, { + autocast: true, + type: + selectedOption && selectedOption.value !== 'no-value' + ? selectedOption.value + : undefined, + ...(selectedOption.value === 'simple' + ? { symbol: simpleFillSymbol } + : {}), + }); + }} + /> + +
+ + {open && ( + { + onChange(id, newValue); + }} + onClose={() => { + setOpen(false); + }} + /> + )} +
+ + ); +} + +export const ArcgisRendererWidgetComponent = injectIntl(ArcgisRendererWidget); + +export default compose(injectLazyLibs(['reactSelect']))( + ArcgisRendererWidgetComponent, +); diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/ClassBreaks.jsx b/src/Widgets/ArcgisRendererWidget/RendererEditor/ClassBreaks.jsx new file mode 100644 index 0000000..ea17844 --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/ClassBreaks.jsx @@ -0,0 +1,8 @@ +export default function ClassBreaks() { + return ( +

+ Renderer visual editor under work. Please edit it through json editor by + pressing the "Renderer" button +

+ ); +} diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/Dictionary.jsx b/src/Widgets/ArcgisRendererWidget/RendererEditor/Dictionary.jsx new file mode 100644 index 0000000..ca28570 --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/Dictionary.jsx @@ -0,0 +1,8 @@ +export default function Dictionary() { + return ( +

+ Renderer visual editor under work. Please edit it through json editor by + pressing the "Renderer" button +

+ ); +} diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/DotDensity.jsx b/src/Widgets/ArcgisRendererWidget/RendererEditor/DotDensity.jsx new file mode 100644 index 0000000..88a59be --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/DotDensity.jsx @@ -0,0 +1,8 @@ +export default function DotDensity() { + return ( +

+ Renderer visual editor under work. Please edit it through json editor by + pressing the "Renderer" button +

+ ); +} diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/Heatmap.jsx b/src/Widgets/ArcgisRendererWidget/RendererEditor/Heatmap.jsx new file mode 100644 index 0000000..efd2602 --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/Heatmap.jsx @@ -0,0 +1,8 @@ +export default function Heatmap() { + return ( +

+ Renderer visual editor under work. Please edit it through json editor by + pressing the "Renderer" button +

+ ); +} diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/PieChart.jsx b/src/Widgets/ArcgisRendererWidget/RendererEditor/PieChart.jsx new file mode 100644 index 0000000..147c19d --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/PieChart.jsx @@ -0,0 +1,8 @@ +export default function PieChart() { + return ( +

+ Renderer visual editor under work. Please edit it through json editor by + pressing the "Renderer" button +

+ ); +} diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/Simple.jsx b/src/Widgets/ArcgisRendererWidget/RendererEditor/Simple.jsx new file mode 100644 index 0000000..46f6f03 --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/Simple.jsx @@ -0,0 +1,109 @@ +import { toNumber } from 'lodash'; + +import { InlineForm } from '@plone/volto/components'; + +import { simpleSymbols as simpleSymbolsOptions } from '@eeacms/volto-eea-map/constants'; + +import { simpleSymbols } from '../RendererEditor/_defaults'; + +export default function Simple(props) { + const { $map, value, id, onChange } = props; + + const symbol = value?.symbol || {}; + + return ( + { + let $fieldValue = fieldValue; + + if (['outline_width', 'size', 'width'].includes(symbolId)) { + $fieldValue = Math.max(toNumber(fieldValue) || 0, 0); + } + + onChange(id, { + ...(value || {}), + ...(symbolId === 'type' ? { autocast: true } : {}), + symbol: { + ...(symbolId !== 'type' ? symbol : {}), + ...(symbolId === 'type' ? simpleSymbols[fieldValue] || {} : {}), + ...(['outline_color', 'outline_width'].includes(symbolId) + ? { + outline: { + ...(symbol.outline || {}), + [symbolId.replace('outline_', '')]: $fieldValue, + }, + } + : { [symbolId]: $fieldValue }), + }, + }); + }} + /> + ); +} diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/UniqueValue.jsx b/src/Widgets/ArcgisRendererWidget/RendererEditor/UniqueValue.jsx new file mode 100644 index 0000000..6f84aac --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/UniqueValue.jsx @@ -0,0 +1,8 @@ +export default function UniqueValue() { + return ( +

+ Renderer visual editor under work. Please edit it through json editor by + pressing the "Renderer" button +

+ ); +} diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/_Editor.jsx b/src/Widgets/ArcgisRendererWidget/RendererEditor/_Editor.jsx new file mode 100644 index 0000000..789a168 --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/_Editor.jsx @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; + +import Simple from './Simple'; +import UniqueValue from './UniqueValue'; +import Heatmap from './Heatmap'; +import ClassBreaks from './ClassBreaks'; +import Dictionary from './Dictionary'; +import DotDensity from './DotDensity'; +import PieChart from './PieChart'; + +const types = { + simle: Simple, + 'unique-value': UniqueValue, + heatmap: Heatmap, + 'class-breaks': ClassBreaks, + dictionary: Dictionary, + 'dot-density': DotDensity, + 'pie-chart': PieChart, +}; + +function getRendererByType(type) { + return types[type] || Simple; +} + +export default function Editor(props) { + const Renderer = useMemo(() => getRendererByType(props.type), [props.type]); + + return ; +} diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/_EditorModal.jsx b/src/Widgets/ArcgisRendererWidget/RendererEditor/_EditorModal.jsx new file mode 100644 index 0000000..b3b8f69 --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/_EditorModal.jsx @@ -0,0 +1,88 @@ +import { useEffect, useRef } from 'react'; +import { Modal, Button } from 'semantic-ui-react'; +import { toast } from 'react-toastify'; +import { Toast } from '@plone/volto/components'; +import { + initEditor, + destroyEditor, + validateEditor, + onPasteEditor, +} from '@eeacms/volto-eea-map/jsoneditor'; + +export default function EditorModal(props) { + const { value, onClose, onChange } = props; + const editor = useRef(); + const initailValue = useRef(props.value); + + async function getValue() { + const valid = await validateEditor(editor); + if (!valid) { + throw new Error('Invalid JSON'); + } + try { + return editor.current.get(); + } catch { + throw new Error('Invalid JSON'); + } + } + + useEffect(() => { + initEditor({ + el: 'jsoneditor-plotlyjson', + editor, + options: { + schema: undefined, + }, + dflt: initailValue.current, + }); + + const editorCurr = editor.current; + + return () => { + destroyEditor(editorCurr); + }; + }, []); + + return ( + + +
{ + onPasteEditor(editor); + }} + /> + + + + + + + ); +} diff --git a/src/Widgets/ArcgisRendererWidget/RendererEditor/_defaults.js b/src/Widgets/ArcgisRendererWidget/RendererEditor/_defaults.js new file mode 100644 index 0000000..15761a7 --- /dev/null +++ b/src/Widgets/ArcgisRendererWidget/RendererEditor/_defaults.js @@ -0,0 +1,30 @@ +export const simpleFillSymbol = { + type: 'simple-fill', + color: [17, 157, 255, 0.5], + outline: { + color: [17, 157, 255, 0.6], + width: 0.5, + }, +}; + +export const simpleMarkerSymbol = { + type: 'simple-marker', + size: 8, + color: [17, 157, 255, 0.2], + outline: { + color: [17, 157, 255, 0.8], + width: 0.5, + }, +}; + +export const simpleLineSymbol = { + type: 'simple-line', + color: [17, 157, 255, 0.5], + width: 1, +}; + +export const simpleSymbols = { + 'simple-fill': simpleFillSymbol, + 'simple-line': simpleLineSymbol, + 'simple-marker': simpleMarkerSymbol, +}; diff --git a/src/Widgets/ArcgisSliderWidget.jsx b/src/Widgets/ArcgisSliderWidget.jsx new file mode 100644 index 0000000..b6e6243 --- /dev/null +++ b/src/Widgets/ArcgisSliderWidget.jsx @@ -0,0 +1,79 @@ +/** + * ArcgisSliderWidget component. + * @module components/manage/Widgets/ArcgisSliderWidget + */ + +import React, { Component } from 'react'; + +import { FormFieldWrapper } from '@plone/volto/components'; + +/** + * The simple slider widget. + * + * It is the default fallback widget, so if no other widget is found based on + * passed field properties, it will be used. + */ +class ArcgisSliderWidget extends Component { + /** + * Component did mount lifecycle method + * @method componentDidMount + * @returns {undefined} + */ + componentDidMount() { + if (this.props.focus) { + this.node.focus(); + } + } + + /** + * Render method. + * @method render + * @returns {string} Markup for the component. + */ + render() { + const { id, value, onChange, min = 0, max = 1, step = 0.1 } = this.props; + + return ( + +
+ { + if (target.value < min) { + target.value = min; + } + if (target.value > max) { + target.value = max; + } + onChange(id, target.value === '' ? undefined : target.value); + }} + /> + { + if (target.value < min) { + target.value = min; + } + if (target.value > max) { + target.value = max; + } + onChange(id, target.value === '' ? undefined : target.value); + }} + /> +
+
+ ); + } +} + +export default ArcgisSliderWidget; diff --git a/src/Widgets/ArcgisViewpointWidget.jsx b/src/Widgets/ArcgisViewpointWidget.jsx new file mode 100644 index 0000000..3051643 --- /dev/null +++ b/src/Widgets/ArcgisViewpointWidget.jsx @@ -0,0 +1,112 @@ +import { useEffect, useState } from 'react'; +import { toNumber } from 'lodash'; +import { FormFieldWrapper, InlineForm } from '@plone/volto/components'; + +export default function ArcgisViewpointWidget(props) { + const [watchViewpoint, setWatchViewpoint] = useState(false); + const { $map, id, value, onChange } = props; + + useEffect(() => { + if (!$map.current?.isReady) return; + const homeWidget = $map.current.view.ui.find('Home'); + if (!homeWidget) return; + homeWidget.viewpoint = new $map.current.modules.AgViewpoint({ + center: [value.longitude, value.latitude], + zoom: value.zoom, + }); + }, [$map, value]); + + useEffect(() => { + if (watchViewpoint && $map.current?.isReady) { + const reactiveUtils = $map.current.modules.agReactiveUtils; + + reactiveUtils.when( + () => $map.current.view.stationary, + (stationary) => { + if (stationary) { + const { longitude, latitude } = $map.current.view.center; + const zoom = $map.current.view.zoom; + onChange(id, { + ...value, + longitude, + latitude, + zoom, + }); + } + setWatchViewpoint(false); + }, + { + once: true, + }, + ); + } + }, [$map, watchViewpoint, id, value, onChange]); + + return ( + <> + +
+ +
+
+
+ { + let $fieldValue = fieldValue; + + if (['longitude', 'latitude', 'zoom'].includes(fieldId)) { + $fieldValue = Math.max(toNumber($fieldValue) || 0, 0); + } + + onChange(id, { + ...value, + [fieldId]: $fieldValue, + }); + }} + /> +
+ + ); +} diff --git a/src/components/visualization/VisualizationViewWidget.jsx b/src/Widgets/VisualizationViewWidget.jsx similarity index 54% rename from src/components/visualization/VisualizationViewWidget.jsx rename to src/Widgets/VisualizationViewWidget.jsx index 8a65093..d86c2cd 100644 --- a/src/components/visualization/VisualizationViewWidget.jsx +++ b/src/Widgets/VisualizationViewWidget.jsx @@ -1,25 +1,24 @@ import { connect } from 'react-redux'; import { pickMetadata } from '@eeacms/volto-embed/helpers'; -import Webmap from '../Webmap'; -import ExtraViews from '../ExtraViews'; +import MapBuilder from '@eeacms/volto-eea-map/Arcgis/Map/MapBuilder'; +import Toolbar from '../Toolbar/Toolbar'; function VisualizationViewWidget(props) { - const { value: map_visualization_data = {} } = props; + const { value: mapData = {}, content } = props; return ( <> - - + diff --git a/src/Widgets/VisualizationWidget.jsx b/src/Widgets/VisualizationWidget.jsx new file mode 100644 index 0000000..f950ca4 --- /dev/null +++ b/src/Widgets/VisualizationWidget.jsx @@ -0,0 +1,200 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Modal, Button, Grid } from 'semantic-ui-react'; +import { toast } from 'react-toastify'; + +import { FormFieldWrapper, Icon, Toast } from '@plone/volto/components'; + +import MapBuilder from '@eeacms/volto-eea-map/Arcgis/Map/MapBuilder'; +import { + initEditor, + destroyEditor, + validateEditor, + onPasteEditor, +} from '@eeacms/volto-eea-map/jsoneditor'; + +import editSVG from '@plone/volto/icons/editing.svg'; + +import '@eeacms/volto-eea-map/styles/editor.less'; +import MapEditor from '../Arcgis/Editor/Editor'; + +function JsonEditorModal(props) { + const { value, onClose, onChange } = props; + const editor = useRef(); + const initailValue = useRef(props.value); + + async function getValue() { + const valid = await validateEditor(editor); + if (!valid) { + throw new Error('Invalid JSON'); + } + try { + return editor.current.get(); + } catch { + throw new Error('Invalid JSON'); + } + } + + useEffect(() => { + initEditor({ + el: 'jsoneditor-plotlyjson', + editor, + options: { + schema: undefined, + }, + dflt: initailValue.current, + }); + + const editorCurr = editor.current; + + return () => { + destroyEditor(editorCurr); + }; + }, []); + + return ( + + +
{ + onPasteEditor(editor); + }} + /> + + + + + + + ); +} + +function MapEditorModal(props) { + const [value, setValue] = useState(props.value); + const [open, setOpen] = useState(false); + + const properties = props.formData; + + return ( + <> + + + { + setValue(value); + }} + /> + + + + + + +
+ + +
+
+
+
+
+
+ {open && ( + setOpen(false)} + /> + )} + + ); +} + +const VisualizationWidget = (props) => { + const { id, title, description, value } = props; + const [showMapEditor, setShowMapEditor] = useState(false); + + if (__SERVER__) return ''; + + return ( + +
+ + +
+ {description &&

{description}

} + + {showMapEditor && ( + setShowMapEditor(false)} + /> + )} +
+ ); +}; + +export default VisualizationWidget; diff --git a/src/arcgis.js b/src/arcgis.js new file mode 100644 index 0000000..6472470 --- /dev/null +++ b/src/arcgis.js @@ -0,0 +1,48 @@ +export default function arcgis(version = '4.29') { + if (__SERVER__) return null; + + const getId = (type) => { + return `arcgis${type}-${version}`; + }; + + const linkId = getId('css'); + const scriptId = getId('js'); + + function loadCss() { + let link = document.getElementById(linkId); + if (!link) { + link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = `https://js.arcgis.com/${version}/esri/themes/light/main.css`; + link.id = getId('css'); + document.head.appendChild(link); + link.addEventListener('load', () => { + loadScript(); + }); + } + } + + function loadScript() { + let script = document.getElementById(scriptId); + if (!script) { + script = document.createElement('script'); + script.src = `https://js.arcgis.com/${version}/init.js`; + script.id = scriptId; + document.body.appendChild(script); + script.addEventListener('load', () => { + if (window.$arcgis) { + window.postMessage({ + type: 'arcgis-loaded', + }); + } + }); + } + } + + if (window.$arcgis) { + return window.$arcgis; + } else { + loadCss(); + return null; + } +} diff --git a/src/components/Blocks/EmbedEEAMap/Edit.jsx b/src/components/Blocks/EmbedEEAMap/Edit.jsx deleted file mode 100644 index b321e09..0000000 --- a/src/components/Blocks/EmbedEEAMap/Edit.jsx +++ /dev/null @@ -1,161 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { Message } from 'semantic-ui-react'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { SidebarPortal } from '@plone/volto/components'; -import { getContent } from '@plone/volto/actions'; -import { flattenToAppURL } from '@plone/volto/helpers'; -import BlockDataForm from '@plone/volto/components/manage/Form/BlockDataForm'; -import Webmap from '@eeacms/volto-eea-map/components/Webmap'; -import ExtraViews from '@eeacms/volto-eea-map/components/ExtraViews'; - -import { Schema } from './Schema'; -import { applyQueriesToMapLayers } from '@eeacms/volto-eea-map/utils'; -import { deepUpdateDataQueryParams, getMapVisualizationData } from './helpers'; -import { isEqual } from 'lodash'; - -const Edit = (props) => { - const { - id, - block, - onChangeBlock, - selected, - data, - getContent, - connected_data_parameters, // page level queries live from widget - data_query, // page level queries - mapContent, - } = props; - - const { - data_query_params, //block level queries - enable_queries, - show_legend = true, - show_note = true, - show_more_info = true, - show_share = true, - dataprotection = { enabled: true }, - height = '', - } = data; - - const schema = Schema(props); - const [mapData, setMapData] = React.useState(''); - - const vis_url = useMemo(() => flattenToAppURL(data.vis_url), [data.vis_url]); - - const map_visualization_data = useMemo( - () => getMapVisualizationData({ mapContent, data }), - [mapContent, data], - ); - - const effectiveQueryParams = - connected_data_parameters && connected_data_parameters.length > 0 - ? connected_data_parameters - : data_query; - - React.useEffect(() => { - deepUpdateDataQueryParams( - block, - props.data, - effectiveQueryParams, - onChangeBlock, - ); - }, [block, props.data, effectiveQueryParams, onChangeBlock]); - - useEffect(() => { - const mapVisId = flattenToAppURL(map_visualization_data['@id'] || ''); - if (!map_visualization_data?.error && vis_url && vis_url !== mapVisId) { - getContent(vis_url, null, id); - } - if (!vis_url) { - setMapData(''); - } - }, [id, getContent, vis_url, map_visualization_data]); - - useEffect(() => { - const mergedQueries = - connected_data_parameters && - connected_data_parameters.length > 0 && - connected_data_parameters.map((unsavedQuery, index) => { - const correspondingQuery = - data_query_params && data_query_params[index]; - return { ...unsavedQuery, alias: correspondingQuery?.alias }; - }); - const queriesToUse = - mergedQueries && mergedQueries.length > 0 - ? mergedQueries - : data_query_params; - - const updatedMapData = applyQueriesToMapLayers( - map_visualization_data, - queriesToUse, - enable_queries, - ); - - if (!isEqual(mapData, updatedMapData)) { - setMapData(updatedMapData); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - map_visualization_data, - data_query_params, - enable_queries, - connected_data_parameters, - ]); - - return ( - <> - {!vis_url && ( - Please select a visualization from block editor. - )} - {!!vis_url && mapData && ( -
- - -
- )} - - { - onChangeBlock(block, { - ...data, - [id]: value, - }); - }} - formData={data} - /> - - - ); -}; - -export default compose( - connect( - (state, props) => { - const pathname = flattenToAppURL(state.content.data['@id']); - return { - mapContent: state.content.subrequests?.[props.id]?.data, - data_query: state.content.data.data_query, - connected_data_parameters: - state?.connected_data_parameters?.byContextPath && - state.connected_data_parameters?.byContextPath[pathname], - }; - }, - { getContent }, - ), -)(Edit); diff --git a/src/components/Blocks/EmbedEEAMap/Schema.js b/src/components/Blocks/EmbedEEAMap/Schema.js deleted file mode 100644 index 20bf39f..0000000 --- a/src/components/Blocks/EmbedEEAMap/Schema.js +++ /dev/null @@ -1,161 +0,0 @@ -const ProtectionSchema = () => ({ - title: 'Data Protection', - - fieldsets: [ - { - id: 'default', - title: 'Default', - fields: [ - 'privacy_statement', - 'privacy_cookie_key', - 'enabled', - 'background_image', - ], - }, - ], - - properties: { - privacy_statement: { - title: 'Privacy statement', - description: 'Defined in template. Change only if necessary', - widget: 'slate_richtext', - className: 'slate-Widget', - defaultValue: [ - { - children: [ - { - text: 'This map is hosted by a third party, Environmental Systems Research Institute. By showing the external content you accept the terms and conditions of ', - }, - { - type: 'a', - url: 'https://www.esri.com', - children: [ - { - text: 'esri.com', - }, - ], - }, - { - text: '. This includes their cookie policies, which we have no control over.', - }, - ], - }, - ], - }, - privacy_cookie_key: { - title: 'Privacy cookie key', - description: 'Use default for Esri maps, otherwise change', - defaultValue: 'esri-maps', - }, - enabled: { - title: 'Data protection disclaimer enabled', - description: 'Enable/disable the privacy protection', - type: 'boolean', - }, - background_image: { - title: 'Static map preview image', - widget: 'file', - required: true, - }, - }, - - required: ['background_image'], -}); - -export const Schema = (props) => { - return { - title: 'Embed Map layers (ArcGis)', - fieldsets: [ - { - id: 'default', - title: 'Default', - fields: [ - 'vis_url', - 'description', - 'height', - 'enable_queries', - ...(props.data.enable_queries ? ['data_query_params'] : []), - ], - }, - { - id: 'toolbar', - title: 'Toolbar', - fields: [ - 'show_legend', - 'show_viewer', - 'show_note', - 'show_more_info', - 'show_share', - ], - }, - { - fields: ['dataprotection'], - title: 'Data Protection', - }, - ], - properties: { - vis_url: { - widget: 'internal_url', - title: 'Visualization', - }, - height: { - title: 'Height', - description: - 'Map block height in px. Default is 500px. Change only if necessary', - type: 'number', - }, - description: { - title: 'Description', - widget: 'slate', - }, - show_note: { - title: 'Show note', - type: 'boolean', - defaultValue: true, - }, - show_sources: { - title: 'Show sources', - description: 'Will show sources set in this page Data provenance', - type: 'boolean', - defaultValue: true, - }, - show_more_info: { - title: 'Show more info', - type: 'boolean', - defaultValue: true, - }, - show_share: { - title: 'Show share button', - type: 'boolean', - defaultValue: true, - }, - show_legend: { - title: 'Show legend', - type: 'boolean', - }, - show_viewer: { - title: 'Show API link', - description: 'Open the map on ArcGIS js service', - type: 'boolean', - }, - enable_queries: { - title: 'Enable queries', - description: - 'Will import Criteria from content-type and try to query map layer fields.', - type: 'boolean', - }, - data_query_params: { - title: 'Query parameters', - description: - 'When using page level parameters to filter the map, please map those to the corresponding field name from the ArcGIS service', - widget: 'data_query_widget', - }, - dataprotection: { - widget: 'object', - schema: ProtectionSchema(), - default: { enabled: true }, - }, - }, - required: [], - }; -}; diff --git a/src/components/Blocks/EmbedEEAMap/View.jsx b/src/components/Blocks/EmbedEEAMap/View.jsx deleted file mode 100644 index 3a73a60..0000000 --- a/src/components/Blocks/EmbedEEAMap/View.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { useMemo } from 'react'; -import { PrivacyProtection } from '@eeacms/volto-embed'; -import Webmap from '@eeacms/volto-eea-map/components/Webmap'; -import ExtraViews from '@eeacms/volto-eea-map/components/ExtraViews'; -import { applyQueriesToMapLayers } from '@eeacms/volto-eea-map/utils'; - -import { getMapVisualizationData } from './helpers'; - -const View = (props) => { - const { data } = props; - const { - data_query_params, - enable_queries, - show_legend = true, - show_note = true, - show_more_info = true, - show_share = true, - dataprotection = { enabled: true }, - height = '', - } = data; - - const map_visualization_data = useMemo( - () => getMapVisualizationData(props), - [props], - ); - - const [mapData, setMapData] = React.useState(''); - - React.useEffect(() => { - const updatedMapData = applyQueriesToMapLayers( - map_visualization_data, - data_query_params, - enable_queries, - ); - setMapData(updatedMapData); - }, [map_visualization_data, data_query_params, enable_queries]); - - const mapUrl = map_visualization_data?.layers?.map_layers[0]?.map_layer - ?.map_service_url - ? `${map_visualization_data.layers.map_layers[0].map_layer.map_service_url}?f=jsapi` - : ''; - - if (map_visualization_data?.error) { - return ( -

- ); - } - - return ( - - {!!mapData && ( - <> - - - - )} - {!mapData && ( -

No map view to show. Set visualization in block configuration.

- )} - - ); -}; - -export default View; diff --git a/src/components/Blocks/EmbedEEAMap/helpers.js b/src/components/Blocks/EmbedEEAMap/helpers.js deleted file mode 100644 index 74e7672..0000000 --- a/src/components/Blocks/EmbedEEAMap/helpers.js +++ /dev/null @@ -1,45 +0,0 @@ -import { pickMetadata } from '@eeacms/volto-embed/helpers'; -// import { updateBlockQueryFromPageQuery } from '@eeacms/volto-eea-map/utils'; - -const deepUpdateDataQueryParams = ( - block, - data, - effectiveQueryParams, - onChangeBlock, -) => { - const updatedQueryParams = - effectiveQueryParams && - effectiveQueryParams.length > 0 && - effectiveQueryParams.map((param) => { - // Find the matching query in the block's current data_query_params - const existingParam = - data?.data_query_params && - data.data_query_params.find((p) => p.i === param.i); - - // If found, merge it with the effective query parameter, preserving the alias - return existingParam ? { ...param, alias: existingParam.alias } : param; - }); - - // Update the block data if there are changes - if ( - JSON.stringify(data.data_query_params) !== - JSON.stringify(updatedQueryParams) - ) { - onChangeBlock(block, { - ...data, - data_query_params: updatedQueryParams, - }); - } -}; - -function getMapVisualizationData({ mapContent, data }) { - const content = mapContent || {}; - const map_visualization_data = - content.map_visualization_data || data?.map_visualization_data || {}; - return { - ...pickMetadata(content), - ...map_visualization_data, - }; -} - -export { deepUpdateDataQueryParams, getMapVisualizationData }; diff --git a/src/components/LegendView.jsx b/src/components/LegendView.jsx deleted file mode 100644 index 1abd559..0000000 --- a/src/components/LegendView.jsx +++ /dev/null @@ -1,150 +0,0 @@ -import React from 'react'; -import { Button, Grid } from 'semantic-ui-react'; -import cx from 'classnames'; -import { fetchArcGISData, setLegendColumns } from '../utils'; -import { serializeNodes } from '@plone/volto-slate/editor/render'; - -import { withDeviceSize } from '@eeacms/volto-eea-map/hocs'; - -import codeSVG from '@eeacms/volto-eea-map/static/code-line.svg'; - -const LayerLegend = ({ data, show_viewer }) => { - const [legendRows, setLegendRows] = React.useState([]); - - const map_service_url = - data && data.map_service_url ? data.map_service_url : ''; - - const layer = data && data.layer ? data.layer : {}; - const { id, name } = layer || {}; - - const fetchLegend = async (url, activeLayerID) => { - let legendData = await fetchArcGISData(url); - - const { layers = [] } = legendData; - const selectedLayer = layers.filter((l, i) => l.layerId === activeLayerID); - setLegendRows(selectedLayer[0].legend); - }; - - React.useEffect(() => { - if (data?.map_service_url && id !== undefined) { - fetchLegend(`${data.map_service_url}/legend`, id); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id, data]); - - return ( - -
-
{name}
- {show_viewer && map_service_url && ( - - - - )} -
- {data?.description && serializeNodes(data.description)} -
- {legendRows.length > 0 && - legendRows.map((item, i) => { - return ( - - alt - - {item.label} - - - ); - })} -
-
- ); -}; - -const LegendView = (props) => { - const data = React.useMemo(() => props.data, [props.data]); - const { device = '', show_viewer = false } = props; - - const [expand, setExpand] = React.useState(true); - - const { layers = {} } = data; - - const visible_layers = - layers && layers.map_layers && layers.map_layers.length - ? layers.map_layers.filter((l) => !l?.map_layer?.hide) - : ''; - - const map_layers = - visible_layers && visible_layers.length > 0 && visible_layers.length > 3 - ? visible_layers.slice(0, 3) - : visible_layers; - - const legendColumns = - map_layers && setLegendColumns(map_layers.length, device); - - return ( -
- - {expand && ( - - - {(!map_layers || map_layers.length === 0) && ( - -

- No layer found for legend. Please add a map layer from editor. -

-
- )} - {map_layers && - map_layers.length > 0 && - map_layers?.map((l, i) => ( - - ))} -
-
- )} -
- ); -}; - -export default withDeviceSize(React.memo(LegendView)); diff --git a/src/components/Webmap.jsx b/src/components/Webmap.jsx deleted file mode 100644 index eb1e717..0000000 --- a/src/components/Webmap.jsx +++ /dev/null @@ -1,371 +0,0 @@ -/* eslint-disable */ -import React from 'react'; -import { withDeviceSize } from '../hocs'; -import { loadModules } from 'esri-loader'; - -const MODULES = [ - 'esri/Map', - 'esri/views/MapView', - 'esri/layers/FeatureLayer', - 'esri/layers/MapImageLayer', - 'esri/layers/GroupLayer', - 'esri/layers/WebTileLayer', - 'esri/Basemap', - 'esri/widgets/Legend', - 'esri/widgets/Expand', - 'esri/widgets/Print', - 'esri/widgets/Zoom', - 'esri/widgets/ScaleBar', - 'esri/widgets/Fullscreen', -]; - -const Webmap = (props) => { - const editMode = props && props.editMode ? props.editMode : false; - const height = props && props.height ? props.height : ''; - const id = props && props.id ? props.id : ''; - const device = props && props.device ? props.device : {}; - const data = props && props.data ? props.data : {}; - - const layers = - props && props.data && props.data.layers ? props.data.layers : {}; - const base = props && props.data && props.data.base ? props.data.base : {}; - const general = - props && props.data && props.data.general ? props.data.general : {}; - const styles = - props && props.data && props.data.styles ? props.data.styles : {}; - const base_layer = base && base.base_layer ? base.base_layer : ''; - - const map_layers = - layers && - layers.map_layers && - layers.map_layers - .filter(({ map_layer }) => map_layer) - .map((l, i) => l.map_layer); - - const mapRef = React.useRef(); - const [modules, setModules] = React.useState({}); - const [reactQueryBuilder, setReactQueryBuilder] = React.useState(null); - - const modules_loaded = React.useRef(false); - - React.useEffect(() => { - if (!modules_loaded.current) { - modules_loaded.current = true; - import( - /* webpackChunkName: "react-querybuilder" */ 'react-querybuilder' - ).then((module) => { - setReactQueryBuilder(module); - }); - - loadModules(MODULES, { - css: true, - }).then((modules) => { - const [ - MapBlock, - MapView, - FeatureLayer, - MapImageLayer, - GroupLayer, - WebTileLayer, - Basemap, - Legend, - Expand, - Print, - Zoom, - ScaleBar, - Fullscreen, - ] = modules; - setModules({ - MapBlock, - MapView, - FeatureLayer, - MapImageLayer, - GroupLayer, - WebTileLayer, - Basemap, - Legend, - Expand, - Print, - Zoom, - ScaleBar, - Fullscreen, - }); - }); - } - }, [setModules]); - - var customFeatureLayerRenderer = { - type: 'simple', // autocasts as new SimpleRenderer() - symbol: { - type: 'simple-fill', // autocasts as new SimpleFillSymbol() - color: styles?.symbol_color - ? styles?.symbol_color?.rgb - : { - r: 0, - g: 0, - b: 0, - a: 1, - }, - //color: 'rgba(255,255,255,0.4)', - style: 'solid', - outline: { - // autocasts as new SimpleLineSymbol() - color: styles?.outline_color - ? styles?.outline_color?.rgb - : { - r: 0, - g: 0, - b: 0, - a: 1, - }, - width: styles?.outline_width ? styles?.outline_width : 1, - }, - }, - }; - //eslint-disable-next-line no-unused-vars - const esri = React.useMemo(() => { - if (Object.keys(modules).length === 0) return {}; - const { - MapBlock, - MapView, - FeatureLayer, - MapImageLayer, - GroupLayer, - WebTileLayer, - Basemap, - Legend, - Expand, - Print, - Zoom, - ScaleBar, - Fullscreen, - } = modules; - let layers = - map_layers && map_layers.length > 0 - ? map_layers - .filter(({ map_service_url, layer }) => map_service_url && layer) - .map( - ( - { - map_service_url = '', - layer, - fullLayer, - query = '', - opacity = 1, - maxScaleOverride = '', - minScaleOverride = '', - }, - index, - ) => { - const url = `${map_service_url}/${layer?.id}`; - let mapLayer; - switch (layer.type) { - case 'Raster Layer': - mapLayer = new MapImageLayer({ - url: map_service_url, - sublayers: [ - { - id: layer.id, - minScale: minScaleOverride - ? minScaleOverride - : layer?.minScale, - maxScale: maxScaleOverride - ? maxScaleOverride - : layer?.maxScale, - opacity: opacity ? parseFloat(opacity) : 1, - definitionExpression: query - ? reactQueryBuilder.formatQuery(query, 'sql') - : '', - }, - ], - }); - break; - case 'Feature Layer': - mapLayer = new FeatureLayer({ - layerId: layer.id, - url, - definitionExpression: query - ? reactQueryBuilder.formatQuery(query, 'sql') - : '', - minScale: minScaleOverride - ? minScaleOverride - : layer?.minScale, - maxScale: maxScaleOverride - ? maxScaleOverride - : layer?.maxScale, - opacity: opacity ? parseFloat(opacity) : 1, - ...(styles?.override_styles && { - renderer: customFeatureLayerRenderer, - }), - }); - break; - default: - break; - } - return mapLayer; - }, - ) - : []; - - const generateMapBaselayer = (compositeType) => { - return new WebTileLayer({ - urlTemplate: `https://gisco-services.ec.europa.eu/maps/tiles/OSM${compositeType}Composite/EPSG3857/{level}/{col}/{row}.png`, - }); - }; - - // Create a Basemap with the WebTileLayer. - - const positronCompositeBasemap = new Basemap({ - baseLayers: [generateMapBaselayer('Positron')], - title: 'Positron Composite', - id: 'positron-composite', - thumbnailUrl: - 'https://gisco-services.ec.europa.eu/maps/tiles/OSMPositronComposite/EPSG3857/0/0/0.png', - }); - - const blossomCompositeBasemap = new Basemap({ - baseLayers: [generateMapBaselayer('Blossom')], - title: 'Blossom Composite', - id: 'blossom-composite', - thumbnailUrl: - 'https://gisco-services.ec.europa.eu/maps/tiles/OSMBlossomComposite/EPSG3857/0/0/0.png', - }); - - const setBasemap = (basemap) => { - if (basemap === 'positron-composite') { - return positronCompositeBasemap; - } - if (basemap === 'blossom-composite') { - return blossomCompositeBasemap; - } - if (!basemap) { - return 'hybrid'; - } - return basemap; - }; - - const setCustomBasemap = (urlTemplate) => { - const mapBaseLayer = new WebTileLayer({ - urlTemplate, - }); - - // Create a Basemap with the WebTileLayer. - const customBase = new Basemap({ - baseLayers: [mapBaseLayer], - title: 'Custom Base Layer', - id: 'custom-base', - }); - return customBase; - }; - - const map = new MapBlock({ - basemap: - data?.base?.use_custom_base && data?.base?.custom_base_layer - ? setCustomBasemap(data?.base?.custom_base_layer) - : setBasemap(base_layer), - layers, - }); - const view = new MapView({ - container: mapRef.current, - map, - center: - general?.long && general?.lat ? [general.long, general.lat] : [0, 40], - zoom: general?.zoom_level ? general?.zoom_level : 2, - ui: { - components: ['attribution'], - }, - }); - if (general && general.scalebar) { - const scaleBarWidget = new ScaleBar({ - view: view, - unit: general.scalebar, - }); - - view.ui.add(scaleBarWidget, { - position: 'bottom-left', - }); - } - - const fullscreenWidget = new Fullscreen({ - view: view, - }); - - view.ui.add(fullscreenWidget, 'top-right'); - - //detect when fullscreen is on - - if (layers && layers[0] && general && general.centerOnExtent) { - const firstLayer = layers[0]; - if (firstLayer.type === 'feature') { - firstLayer - .when(() => { - return firstLayer.queryExtent(); - }) - .then((response) => { - view.goTo(response.extent); - }); - } - if (firstLayer.type === 'map-image') { - firstLayer.when(() => { - view.goTo(firstLayer.fullExtent); - }); - } - } - - const zoomPosition = - general && general.zoom_position ? general.zoom_position : ''; - - if (zoomPosition) { - const zoomWidget = new Zoom({ - view: view, - }); - view.ui.add(zoomWidget, zoomPosition); - } - const printPosition = - general && general.print_position ? general.print_position : ''; - - if (printPosition) { - const printWidget = new Expand({ - content: new Print({ - view: view, - }), - view: view, - expanded: false, - expandIconClass: 'esri-icon-printer', - expandTooltip: 'Print', - }); - view.ui.add(printWidget, printPosition); - } - - if (layers && layers.length > 0) { - layers.forEach((layer) => { - view.whenLayerView(layer).then((layerView) => { - layerView.watch('updating', (val) => {}); - }); - }); - } - return { view, map }; - }, [modules, data, data.layers, map_layers]); - - const heightPx = - height && !editMode - ? `${height}px` - : device === 'tablet' || device === 'mobile' - ? '300px' - : '500px'; - - const dynamicStyle = ` - .esri-map { - height: ${heightPx} !important - } - `; - - return ( -
- -
-
- ); -}; - -export default withDeviceSize(React.memo(Webmap)); diff --git a/src/components/index.js b/src/components/index.js deleted file mode 100644 index 4317dbc..0000000 --- a/src/components/index.js +++ /dev/null @@ -1,6 +0,0 @@ -export { default as Webmap } from './Webmap'; -export { default as ExtraViews } from './ExtraViews'; -export { default as LegendView } from './LegendView'; - -export { default as EmbedMapView } from './Blocks/EmbedEEAMap/View'; -export { default as EmbedMapEdit } from './Blocks/EmbedEEAMap/Edit'; diff --git a/src/components/visualization/VisualizationEditorWidget.jsx b/src/components/visualization/VisualizationEditorWidget.jsx deleted file mode 100644 index 0c63f23..0000000 --- a/src/components/visualization/VisualizationEditorWidget.jsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import { Modal, Button, Grid } from 'semantic-ui-react'; - -import { FormFieldWrapper, InlineForm } from '@plone/volto/components'; - -import PanelsSchema from './panelsSchema'; -import Webmap from '../Webmap'; - -const VisualizationEditorWidget = (props) => { - const [open, setOpen] = React.useState(false); - const { onChange = {}, id } = props; - const block = React.useMemo(() => props.block, [props.block]); - const value = React.useMemo(() => props.value || {}, [props.value]); - - const [intValue, setIntValue] = React.useState(value); - - const dataForm = { map_data: intValue }; - const handleApplyChanges = () => { - onChange(id, intValue); - setOpen(false); - }; - - const handleClose = () => { - setIntValue(value); - setOpen(false); - }; - - const handleChangeField = (val) => { - setIntValue(val); - }; - - let schema = PanelsSchema({ data: dataForm }); - - React.useEffect(() => { - if (!intValue?.general) { - setIntValue({ - ...intValue, - general: { - print_position: 'top-right', - zoom_position: 'top-right', - centerOnExtent: true, - scalebar: 'metric', - }, - }); - } - if (!intValue?.base) { - setIntValue({ - ...intValue, - base: { - base_layer: 'gray-vector', - }, - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [intValue]); - - return ( - -
- -
- - {open && ( - - - - - { - handleChangeField(value); - }} - formData={dataForm} - /> - - -
- -
-
-
-
- - - -
- - -
-
-
-
-
- )} - {!open && } -
- ); -}; - -export default VisualizationEditorWidget; diff --git a/src/components/visualization/panelsSchema.js b/src/components/visualization/panelsSchema.js deleted file mode 100644 index 6b70bdf..0000000 --- a/src/components/visualization/panelsSchema.js +++ /dev/null @@ -1,229 +0,0 @@ -import { base_layers } from '../../constants'; - -const customBaselayers = [ - ['positron-composite', 'positron-composite'], - ['blossom-composite', 'blossom-composite'], -]; - -const BaseLayerSchema = ({ data = {} }) => { - const useCustomBase = data?.map_data?.base?.use_custom_base; - return { - title: 'Base Layer', - fieldsets: [ - { - id: 'base', - title: 'Base Layer', - fields: [ - 'use_custom_base', - ...(useCustomBase ? ['custom_base_layer'] : ['base_layer']), - ], - }, - ], - properties: { - use_custom_base: { - title: 'Use custom Base Layer', - description: - 'Switch between default base layers and defining your custom service base layer', - type: 'boolean', - }, - base_layer: { - title: 'Base Layer', - choices: [...customBaselayers, ...base_layers], - }, - custom_base_layer: { - title: 'Base Layer', - description: - 'Add an URL Template to import your own base layer from an external service', - }, - }, - required: [], - }; -}; - -const LayerSchema = { - title: 'Layer', - fieldsets: [ - { - id: 'default', - title: 'Layer', - fields: ['map_layer'], - }, - ], - properties: { - map_layer: { - title: 'Map layer configuration', - widget: 'map_layers_widget', - }, - }, - required: [], -}; - -const MapLayersSchema = { - title: 'Map Layers', - fieldsets: [ - { - id: 'default', - title: 'Map Data', - fields: ['map_layers'], - }, - ], - properties: { - map_layers: { - title: 'Map Layers', - description: 'Add/Edit Map Layers', - widget: 'object_list', - schema: LayerSchema, - }, - }, - required: [], -}; - -//style changes work only for Feature layers atm -// TODO: apply style for individual layers -const StylesLayersSchema = ({ data = {} }) => { - return { - title: 'Styles Layers', - fieldsets: [ - { - id: 'default', - title: 'Map Data', - fields: [ - 'override_styles', - ...(data?.map_data?.styles?.override_styles - ? ['symbol_color', 'outline_color', 'outline_width'] - : []), - ], - }, - ], - properties: { - override_styles: { - title: 'Override layers style', - description: 'Will override imported layers styling', - type: 'boolean', - }, - symbol_color: { - title: 'Fill color', - widget: 'simple_color_picker_widget', - }, - outline_color: { - title: 'Outline color', - widget: 'simple_color_picker_widget', - }, - outline_width: { - title: 'Outline width', - type: 'number', - }, - }, - required: [], - }; -}; - -const GeneralSchema = ({ data = {} }) => { - const centerOnExtent = data?.map_data?.general?.centerOnExtent; - - return { - title: 'General', - fieldsets: [ - { - id: 'default', - title: 'Zoom', - fields: [ - 'print_position', - 'zoom_position', - 'scalebar', - 'centerOnExtent', - ...(!centerOnExtent ? ['zoom_level', 'long', 'lat'] : []), - ], - }, - ], - properties: { - centerOnExtent: { - title: 'Center on extent', - type: 'boolean', - description: - 'This will override latitude/longitude/zoom level and will lock zoom/moving the map.', - }, - scalebar: { - title: 'Scalebar', - choices: ['metric', 'non-metric', 'dual'].map((n) => { - return [n, n]; - }), - }, - zoom_position: { - title: 'Zoom position', - choices: ['bottom-right', 'bottom-left', 'top-right', 'top-left'].map( - (n) => { - return [n, n]; - }, - ), - }, - zoom_level: { - title: 'Zoom level', - type: 'number', - }, - long: { - title: 'Longitude', - type: 'number', - description: `Will set the map center long coordinate. See: https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html#center`, - }, - lat: { - title: 'Latitude', - type: 'number', - description: `Will set the map center lat coordinate. See: https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html#center`, - }, - - print_position: { - title: 'Print position', - choices: ['bottom-right', 'bottom-left', 'top-right', 'top-left'].map( - (n) => { - return [n, n]; - }, - ), - }, - }, - required: [], - }; -}; - -const PanelsSchema = ({ data = {} }) => { - const generalSchema = GeneralSchema({ data }); - const baseLayerSchema = BaseLayerSchema({ data }); - const stylesLayerSchema = StylesLayersSchema({ data }); - return { - title: 'Map Editor', - fieldsets: [ - { - id: 'default', - title: 'Map Editor Sections', - fields: ['map_data'], - }, - ], - properties: { - map_data: { - title: 'Panels', - widget: 'object_types_widget', - schemas: [ - { - id: 'general', - schema: generalSchema, - }, - { - id: 'base', - schema: baseLayerSchema, - }, - { - id: 'layers', - schema: MapLayersSchema, - }, - { - id: 'styles', - schema: stylesLayerSchema, - }, - ], - }, - }, - required: [], - }; -}; - -export default PanelsSchema; diff --git a/src/components/widgets/DataQueryWidget.jsx b/src/components/widgets/DataQueryWidget.jsx deleted file mode 100644 index 1f8d831..0000000 --- a/src/components/widgets/DataQueryWidget.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { FormFieldWrapper, Field } from '@plone/volto/components'; -import { Accordion, Segment } from 'semantic-ui-react'; - -const DataQueryWidget = (props) => { - const { value, onChange, id } = props; - - const onChangeAlias = (fieldId, fieldValue) => { - let altValue = value; - value[fieldId] = { ...value[fieldId], alias: fieldValue }; - onChange(id, altValue); - }; - - return ( -
- -
- {value && value.length > 0 ? ( - value.map((param, i) => ( - - - -

- {param?.i}:{' '} - {param?.v && param.v.join(', ')} -

- -
-
-
- )) - ) : ( -

No parameters set

- )} -
-
- ); -}; - -export default DataQueryWidget; diff --git a/src/components/widgets/LayerSelectWidget.jsx b/src/components/widgets/LayerSelectWidget.jsx deleted file mode 100644 index 7d1105a..0000000 --- a/src/components/widgets/LayerSelectWidget.jsx +++ /dev/null @@ -1,463 +0,0 @@ -import React from 'react'; -import { Icon } from '@plone/volto/components'; -import { Input, Select, Button, Grid, Checkbox } from 'semantic-ui-react'; - -import { flattenToAppURL } from '@plone/volto/helpers'; -import RichTextWidget from '@plone/volto-slate/widgets/RichTextWidget'; - -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { getContent } from '@plone/volto/actions'; - -import checkSVG from '@plone/volto/icons/check.svg'; -import closeSVG from '@plone/volto/icons/clear.svg'; -import aheadSVG from '@plone/volto/icons/ahead.svg'; -import resetSVG from '@plone/volto/icons/reset.svg'; - -import { fetchArcGISData } from '../../utils'; - -import loadable from '@loadable/component'; - -const QueryBuilder = loadable(() => import('react-querybuilder'), { - resolveComponent: (components) => components.QueryBuilder, -}); - -const LayerSelectWidget = (props) => { - const { onChange, id, data_query } = props; - - const value = React.useMemo(() => props.value || {}, [props.value]); - - const { - available_layers, - map_data, - map_service_url, - layer, - fields = [], - query = '', - description = '', - hide = false, - } = value; - - const [mapData, setMapData] = React.useState(map_data); - const [checkColor, setCheckColor] = React.useState(''); - const [serviceUrlError, setServiceUrlError] = React.useState(''); - const [serviceUrl, setServiceUrl] = React.useState(map_service_url); - const [selectedLayer, setSelectedLayer] = React.useState(layer); - - const [availableLayers, setAvailableLayers] = - React.useState(available_layers); - - const [builtQuery, setBuiltQuery] = React.useState(query); - - const handleServiceUrlCheck = async () => { - // fetch url, save it, populate layers options - try { - let mapData = await fetchArcGISData(serviceUrl); - setCheckColor('green'); - setMapData(mapData); - setServiceUrlError(''); - if (mapData.layers && mapData.layers.length > 0) { - const mappedLayers = mapData.layers - .filter( - (layer) => layer && layer.type && layer.type !== 'Group Layer', - ) - .map((layer, i) => { - return { - key: layer.id, - value: layer, - text: `${layer.name} (${layer.type})`, - }; - }); - setAvailableLayers(mappedLayers); - } - onChange(id, { - ...value, - layer: selectedLayer, - map_service_url: serviceUrl, - available_layers: availableLayers, - map_data: mapData, - description, - hide, - }); - } catch (e) { - setCheckColor('youtube'); - setServiceUrlError({ error: e.message, status: e.status }); - } - }; - - React.useEffect(() => { - import( - /* webpackChunkName: "react-querybuilder-css" */ 'react-querybuilder/dist/query-builder.css' - ); - props.getContent('', null, id); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - React.useEffect(() => { - if (query && !builtQuery) { - setBuiltQuery(query); - } - }, [query, builtQuery]); - - React.useEffect(() => { - //detect existing queries in block content. If it exists. Apply matching queries to layer on fresh layer load - if ( - map_service_url && - layer && - !query && - data_query && - data_query.length > 0 - ) { - let autoQuery = { - combinator: 'or', - rules: [], - }; - data_query.forEach((param, i) => { - if ( - fields && - fields.length > 0 && - fields.filter( - (field, j) => field.name === param.alias || field.name === param.i, - ).length > 0 - ) { - autoQuery.rules = [ - ...autoQuery.rules, - { field: param.alias || param.i, operator: '=', value: param.v[0] }, - ]; - } - }); - if (autoQuery.rules.length > 0) { - onChange(id, { - ...value, - query: autoQuery, - }); - setBuiltQuery(autoQuery); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [map_service_url, layer, query, data_query, fields]); - - const handleLayerFetch = async (service_url, id) => { - try { - let fullLayer = await fetchArcGISData(`${service_url}/${id}`); - return fullLayer; - } catch (e) {} - }; - - const handleSelectLayer = async (layer) => { - const fullLayer = await handleLayerFetch(serviceUrl, layer.id); - setSelectedLayer(layer); - onChange(id, { - ...value, - layer, - fullLayer, - fields: fullLayer.fields, - map_service_url: serviceUrl, - available_layers: availableLayers, - map_data: mapData, - query: '', - }); - setBuiltQuery(''); - }; - - const handleQueryLayer = () => { - if (builtQuery) { - onChange(id, { - ...value, - query: builtQuery, - }); - } - }; - - const handleChangeDescription = (val) => { - if (val) { - onChange(id, { - ...value, - description: val, - }); - } - }; - - const handleOpacityChange = (val) => { - onChange(id, { - ...value, - opacity: val, - }); - }; - - const handleChangeServiceUrl = (value) => { - setServiceUrlError(''); - setCheckColor(''); - setAvailableLayers(''); - setBuiltQuery(''); - setSelectedLayer(''); - setMapData(''); - - setServiceUrl(value); - }; - - const handleMinScaleChange = (minScaleOverride) => { - onChange(id, { - ...value, - minScaleOverride, - }); - }; - - const handleMaxScaleChange = (maxScaleOverride) => { - onChange(id, { - ...value, - maxScaleOverride, - }); - }; - - const handleReset = () => { - setServiceUrlError(''); - setServiceUrl(map_service_url); - setCheckColor(''); - setAvailableLayers(available_layers); - setBuiltQuery(''); - setSelectedLayer(layer); - setMapData(map_data); - }; - - const handleHideInLegend = (v) => { - onChange(id, { - ...value, - hide: v, - }); - }; - - return ( -
- -
- -
- Service URL -
-
- - - handleChangeServiceUrl(value)} - style={{ width: '100%' }} - error={serviceUrlError} - value={serviceUrl} - > - - - {serviceUrlError.error} - - - {serviceUrl && ( - - {serviceUrl !== map_service_url && ( - - )} - - - )} -
- {mapData && mapData.mapName && ( -
- -
- Map name: -
-

{mapData.mapName}

-
-
- )} - {availableLayers && availableLayers.length > 0 && ( -
- -
- Layer -
-
- - handleOpacityChange(e.target.value)} - /> - handleOpacityChange(e.target.value)} - /> - - -
- Min scale: -
- handleMinScaleChange(e.target.value)} - /> -
- -
- Max scale: -
- handleMaxScaleChange(e.target.value)} - /> -
- -
- Description -
-
- -
- { - handleChangeDescription(value); - }} - value={value.description} - placeholder="Set Description" - /> -
-
- -
Hide in legend:
{' '} - handleHideInLegend(checked)} - /> -
-
- )} - {availableLayers && fields && fields.length > 0 && ( -
-
- Query Layer -
- - { - return { name: fi.name, label: fi.name }; - })} - query={builtQuery} - onQueryChange={(q) => setBuiltQuery(q)} - enableDragAndDrop={false} - /> - - {builtQuery && ( - - - - )} - -

- Available Fields: -

-
- {fields.map((field, id) => ( -

- {field.alias} ({field.type}) -

- ))} -
- )} -
-
- ); -}; - -export default compose( - connect( - (state) => { - const pathname = flattenToAppURL(state.content.data['@id']); - return { - content: state.content.data, - data_query: state.connected_data_parameters.byContextPath[pathname], - }; - }, - { - getContent, - }, - ), -)(LayerSelectWidget); diff --git a/src/components/widgets/LayersPanelWidget.jsx b/src/components/widgets/LayersPanelWidget.jsx deleted file mode 100644 index 59e0ab4..0000000 --- a/src/components/widgets/LayersPanelWidget.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { Button } from 'semantic-ui-react'; -import LayerSelectWidget from './LayerSelectWidget'; - -const LayersPanelWidget = ({ data, onChange, block }) => { - const map_layers = React.useMemo( - () => data.map_layers || [], - [data.map_layers], - ); - - React.useEffect(() => { - if (!data.map_layers) { - onChange('map_data', { - ...data, - map_layers: [ - { - map_service_url: '', - layer: '', - available_layers: [], - map_data: {}, - }, - ], - }); - } - }, [data, block, onChange]); - - const handleAddLayer = () => { - onChange('map_data', { - ...data, - map_layers: [ - ...data.map_layers, - { map_service_url: '', layer: '', available_layers: [], map_data: {} }, - ], - }); - }; - - return ( -
- {map_layers && - map_layers.length > 0 && - map_layers.map((layer, i) => ( - - ))} - - -
- ); -}; - -export default LayersPanelWidget; diff --git a/src/components/widgets/SimpleColorPickerWidget.jsx b/src/components/widgets/SimpleColorPickerWidget.jsx deleted file mode 100644 index 9f114a5..0000000 --- a/src/components/widgets/SimpleColorPickerWidget.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; -import { FormFieldWrapper, Icon } from '@plone/volto/components'; -import { Button } from 'semantic-ui-react'; -import loadable from '@loadable/component'; -import clearSVG from '@plone/volto/icons/clear.svg'; -import checkSVG from '@plone/volto/icons/check.svg'; - -const ReactColor = loadable.lib(() => import('react-color')); - -const SimpleColorPickerWidget = (props) => { - const { id, value, onChange, available_colors } = props; - const [showPicker, setShowPicker] = React.useState(false); - - const [color, setColor] = React.useState(value); - - const handleChangeColor = (valColor) => { - setColor(valColor); - }; - - const handleConfirmColor = () => { - onChange(id, color); - setShowPicker(false); - }; - - const handleAbortColor = () => { - setColor(value); - setShowPicker(false); - }; - - return ( - -
- - - - {showPicker ? ( -
- - {({ SketchPicker }) => { - return ( - { - handleChangeColor(value); - }} - > - ); - }} - - - - - -
- ) : ( - '' - )} -
-
-
- ); -}; - -export default SimpleColorPickerWidget; diff --git a/src/constants.js b/src/constants.js index c96ab61..3cc6844 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,10 +1,17 @@ -const positions = ['bottom-right', 'bottom-left', 'top-right', 'top-left'].map( - (n) => { - return { key: n, value: n, text: n }; - }, -); +import { capitalize } from 'lodash'; + +export const positions = [ + 'top-left', + 'top-right', + 'bottom-right', + 'bottom-left', +].map((n) => { + return [n, capitalize(n.replaceAll('-', ' '))]; +}); -const base_layers = [ +export const basemaps = [ + 'positron-composite', + 'blossom-composite', 'dark-gray', 'dark-gray-vector', 'gray', @@ -22,8 +29,219 @@ const base_layers = [ 'terrain', 'topo', 'topo-vector', +].map((n) => { + return [n, capitalize(n.replaceAll('-', ' '))]; +}); + +export const widgets = [ + 'AreaMeasurement2D', + 'AreaMeasurement3D', + 'Attachments', + 'Attribution', + 'BasemapGallery', + 'BasemapLayerList', + 'BasemapToggle', + 'Bookmarks', + 'BuildingExplorer', + 'Compass', + 'CoordinateConversion', + 'Daylight', + 'DirectLineMeasurement3D', + 'DirectionalPad', + 'Directions', + 'DistanceMeasurement2D', + 'Editor', + 'ElevationProfile', + 'Feature', + 'FeatureForm', + 'FeatureTable', + 'FeatureTemplates', + 'Features', + 'FloorFilter', + 'Fullscreen', + 'Histogram', + 'HistogramRangeSlider', + 'Home', + 'LayerList', + 'Legend', + 'LineOfSight', + 'Locate', + 'Measurement', + 'NavigationToggle', + 'OrientedImageryViewer', + 'Print', + 'ScaleBar', + 'ScaleRangeSlider', + 'Search', + 'ShadowCast', + 'Sketch', + 'Slice', + 'Slider', + 'TableList', + 'TimeSlider', + 'TimeZoneLabel', + 'Track', + 'UtilityNetworkAssociations', + 'UtilityNetworkTrace', + 'UtilityNetworkValidateTopology', + 'ValuePicker', + 'Weather', + 'Zoom', ].map((n) => { return [n, n]; }); -export { positions, base_layers }; +export const blendModes = [ + 'average', + 'color-burn', + 'color-dodge', + 'color', + 'darken', + 'destination-atop', + 'destination-in', + 'destination-out', + 'destination-over', + 'difference', + 'exclusion', + 'hard-light', + 'hue', + 'invert', + 'lighten', + 'lighter', + 'luminosity', + 'minus', + 'multiply', + 'normal', + 'overlay', + 'plus', + 'reflect', + 'saturation', + 'screen', + 'soft-light', + 'source-atop', + 'source-in', + 'source-out', + 'vivid-light', + 'xor', +].map((n) => { + return [n, capitalize(n.replaceAll('-', ' '))]; +}); + +export const geometryTypes = [ + 'point', + 'multipoint', + 'polyline', + 'polygon', + 'multipatch', + 'mesh', +].map((n) => { + return [n, capitalize(n.replaceAll('-', ' '))]; +}); + +export const rendererTypes = [ + 'simple', + 'unique-value', + 'heatmap', + 'class-breaks', + 'dictionary', + 'dot-density', + 'pie-chart', +].map((n) => ({ value: n, label: capitalize(n.replaceAll('-', ' ')) })); + +export const simpleSymbols = [ + 'simple-fill', + 'simple-marker', + 'simple-line', +].map((n) => { + return [n, capitalize(n.replaceAll('-', ' '))]; +}); + +export const expandKeys = ['expandTooltip']; + +export const getDefaultWidgets = (dimension = '2d') => [ + { name: 'Zoom', position: 'top-left' }, + ...(dimension === '3d' + ? [{ name: 'NavigationToggle', position: 'top-left' }] + : []), + { + name: 'Home', + position: 'top-left', + }, + { + name: 'Compass', + position: 'top-left', + }, + { + name: 'LayerList', + position: 'top-right', + expand: true, + ExpandProperties: { + expandTooltip: 'Layers', + }, + }, + { + name: 'Print', + position: 'top-right', + expand: true, + ExpandProperties: { + expandTooltip: 'Print', + }, + }, + { + name: 'Fullscreen', + position: 'top-right', + }, + { + name: 'Legend', + position: 'bottom-left', + expand: true, + respectLayerVisibility: false, + ExpandProperties: { + expandTooltip: 'Legend', + }, + }, + { + name: 'ScaleBar', + position: 'bottom-right', + unit: 'dual', + }, +]; + +export const widgetsSchema = { + ScaleBar: { + unit: { + title: 'Unit', + choices: [ + ['metric', 'Metric'], + ['imperial', 'Imperial'], + ['dual', 'Dual'], + ['non-metric', 'Non metric'], + ], + }, + }, + Legend: { + respectLayerVisibility: { + title: 'Respect layer visibility', + type: 'boolean', + }, + }, +}; + +export const withSublayers = ['MapImageLayer']; + +export const layersMapping = { + 'Raster Layer': 'MapImageLayer', +}; + +export const geometryTypeMapping = { + esriGeometryPoint: 'point', + esriGeometryMultipoint: 'multipoint', + esriGeometryPolyline: 'polyline', + esriGeometryPolygon: 'polygon', + esriGeometryMultipatch: 'multipatch', + esriGeometryMesh: 'mesh', +}; + +export const renderersMapping = { + uniqueValue: 'unique-value', +}; diff --git a/src/hocs/index.js b/src/hocs/index.js deleted file mode 100644 index 8a5a293..0000000 --- a/src/hocs/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import withDeviceSize from './withDeviceSize'; - -export { withDeviceSize }; diff --git a/src/hocs/withArcgis.jsx b/src/hocs/withArcgis.jsx new file mode 100644 index 0000000..8f8abbb --- /dev/null +++ b/src/hocs/withArcgis.jsx @@ -0,0 +1,27 @@ +import React, { forwardRef, useEffect, useState } from 'react'; +import loadArcgis from '@eeacms/volto-eea-map/arcgis'; + +export default function withArcgis(WrappedComponent) { + return forwardRef((props, ref) => { + const [agLoaded, setAgLoaded] = useState(false); + + const interceptArcgis = (event) => { + if (event.type === 'message' && event.data.type === 'arcgis-loaded') { + setAgLoaded(true); + } + }; + + useEffect(() => { + if (window.$arcgis) { + setAgLoaded(true); + } + loadArcgis(); + window.addEventListener('message', interceptArcgis); + return () => { + window.removeEventListener('message', interceptArcgis); + }; + }, []); + + return ; + }); +} diff --git a/src/hocs/withDeviceSize.jsx b/src/hocs/withDeviceSize.jsx deleted file mode 100644 index eb1b6c6..0000000 --- a/src/hocs/withDeviceSize.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; - -export default function withDeviceSize(WrappedComponent) { - return (props) => { - const [device, setDevice] = React.useState(null); - - const updateScreenSize = () => { - if (__CLIENT__) { - const screenWidth = - window.innerWidth || - document.documentElement.clientWidth || - document.body.clientWidth || - 0; - - setDevice(getDeviceConfig(screenWidth)); - } - }; - - const getDeviceConfig = (width) => { - // semantic ui breakpoints - if (width < 320) { - return 'mobile'; - } else if (width >= 320 && width < 768) { - return 'tablet'; - } else if (width >= 768 && width < 992) { - return 'computer'; - } else if (width >= 992 && width < 1280) { - return 'large'; - } else if (width >= 1280) { - return 'widescreen'; - } - }; - - React.useEffect(() => { - updateScreenSize(); - window.addEventListener('resize', updateScreenSize); - return () => { - window.removeEventListener('resize', updateScreenSize); - }; - /* eslint-disable-next-line */ - }, []); - - return ; - }; -} diff --git a/src/hooks/useChangedProps.jsx b/src/hooks/useChangedProps.jsx new file mode 100644 index 0000000..df249b8 --- /dev/null +++ b/src/hooks/useChangedProps.jsx @@ -0,0 +1,24 @@ +import { useEffect, useRef } from 'react'; +import { isFunction, isEqual } from 'lodash'; + +export default function useChangedEffect(callback, props) { + const prevPropsRef = useRef(props); + + useEffect(() => { + const currentChangedProps = Object.keys(props).reduce((acc, key) => { + if ( + !['children'].includes(key) && + !isEqual(props[key], prevPropsRef.current[key]) + ) { + acc[key] = props[key]; + } + return acc; + }, {}); + + if (isFunction(callback)) { + callback(currentChangedProps); + } + + prevPropsRef.current = props; + }, [callback, props]); +} diff --git a/src/hooks/useClass.jsx b/src/hooks/useClass.jsx new file mode 100644 index 0000000..79cdcb9 --- /dev/null +++ b/src/hooks/useClass.jsx @@ -0,0 +1,17 @@ +import { useEffect, useRef } from 'react'; + +export default function useClass(Class, ...props) { + const refObject = useRef(null); + + useEffect(() => { + return () => { + refObject.current = null; + }; + }, []); + + if (refObject.current === null) { + refObject.current = new Class(...props); + } + + return refObject.current; +} diff --git a/src/hooks/useCopyToClipboard.jsx b/src/hooks/useCopyToClipboard.jsx new file mode 100644 index 0000000..c66b485 --- /dev/null +++ b/src/hooks/useCopyToClipboard.jsx @@ -0,0 +1,25 @@ +import { useState, useEffect, useCallback } from 'react'; + +const useCopyToClipboard = (text) => { + const [copyStatus, setCopyStatus] = useState('inactive'); + const copy = useCallback(() => { + navigator.clipboard.writeText(text).then( + () => setCopyStatus('copied'), + () => setCopyStatus('failed'), + ); + }, [text]); + + useEffect(() => { + if (copyStatus === 'inactive') { + return; + } + + const timeout = setTimeout(() => setCopyStatus('inactive'), 3000); + + return () => clearTimeout(timeout); + }, [copyStatus]); + + return [copyStatus, copy]; +}; + +export default useCopyToClipboard; diff --git a/src/index.js b/src/index.js index 44c3e9c..40afd8f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,20 +1,20 @@ import { uniqBy } from 'lodash'; -import EmbedMapView from './components/Blocks/EmbedEEAMap/View'; -import EmbedMapEdit from './components/Blocks/EmbedEEAMap/Edit'; +import EmbedMapView from './Blocks/EmbedEEAMap/View'; +import EmbedMapEdit from './Blocks/EmbedEEAMap/Edit'; import world from '@plone/volto/icons/world.svg'; -import DataQueryWidget from './components/widgets/DataQueryWidget'; -import LayerSelectWidget from './components/widgets/LayerSelectWidget'; +import VisualizationWidget from './Widgets/VisualizationWidget'; -import VisualizationEditorWidget from './components/visualization/VisualizationEditorWidget'; -import VisualizationViewWidget from './components/visualization/VisualizationViewWidget'; -import VisualizationView from './components/visualization/VisualizationView'; +import ArcgisRendererWidget from './Widgets/ArcgisRendererWidget/ArcgisRendererWidget'; +import ArcgisColorPickerWidget from './Widgets/ArcgisColorPickerWidget'; +import ArcgisSliderWidget from './Widgets/ArcgisSliderWidget'; +import ArcgisViewpointWidget from './Widgets/ArcgisViewpointWidget'; -import SimpleColorPickerWidget from './components/widgets/SimpleColorPickerWidget'; +import VisualizationViewWidget from './Widgets/VisualizationViewWidget'; -import './less/global.less'; +import VisualizationView from './Views/VisualizationView'; const applyConfig = (config) => { config.settings.allowed_cors_destinations = [ @@ -64,15 +64,15 @@ const applyConfig = (config) => { 'id', ); - config.widgets.widget.map_layers_widget = LayerSelectWidget; - config.widgets.widget.data_query_widget = DataQueryWidget; - config.widgets.widget.simple_color_picker_widget = SimpleColorPickerWidget; + config.views.contentTypesViews.map_visualization = VisualizationView; + + config.widgets.widget.arcgis_renderer = ArcgisRendererWidget; + config.widgets.widget.arcgis_color_picker = ArcgisColorPickerWidget; + config.widgets.widget.arcgis_slider = ArcgisSliderWidget; + config.widgets.widget.arcgis_viewpoint = ArcgisViewpointWidget; - //map editor for the visualization(content-type) - config.widgets.id.map_visualization_data = VisualizationEditorWidget; + config.widgets.id.map_visualization_data = VisualizationWidget; config.widgets.views.id.map_visualization_data = VisualizationViewWidget; - //map viewer for the visualization(content-type) - config.views.contentTypesViews.map_visualization = VisualizationView; return config; }; diff --git a/src/jsoneditor.js b/src/jsoneditor.js new file mode 100644 index 0000000..c3b5903 --- /dev/null +++ b/src/jsoneditor.js @@ -0,0 +1,72 @@ +import { isString } from 'lodash'; +import { toast } from 'react-toastify'; +import { Toast } from '@plone/volto/components'; + +import loadable from '@loadable/component'; + +const LoadableJsonEditor = loadable.lib(() => import('jsoneditor')); + +const jsoneditor = __CLIENT__ && LoadableJsonEditor; + +export async function initEditor({ el, editor, dflt, options, onInit }) { + if (!jsoneditor) return; + const module = await jsoneditor.load(); + const { default: JSONEditor } = module; + // create the editor + const container = isString(el) ? document.getElementById(el) : el; + + if (!container) { + return; + } + + container.innerHTML = ''; + + const _options = { + mode: 'code', + enableTransform: false, + schema: { + type: 'array', + items: { + type: 'object', + }, + }, + ...options, + }; + + editor.current = new JSONEditor(container, _options); + // set initial json + editor.current.set(dflt); + + if (onInit) onInit(); +} + +export function destroyEditor(editor) { + if (editor) { + editor.destroy(); + editor = null; + } +} + +export async function validateEditor(editor) { + const err = await editor.current.validate(); + + if (err.length) { + toast.warn( + , + ); + return false; + } + + return true; +} + +export function onPasteEditor(editor) { + try { + editor.current.repair(); + editor.current.format(); + } catch {} +} diff --git a/src/less/global.less b/src/less/global.less deleted file mode 100644 index 1980ce1..0000000 --- a/src/less/global.less +++ /dev/null @@ -1,253 +0,0 @@ -@import (multiple, reference, optional) '../../theme.config'; - -/* Enables customization of addons */ -.loadAddonOverrides() { - @import (optional) - '@{siteFolder}/../addons/@{addon}/@{addontype}s/@{addonelement}.overrides'; -} - -/* Helper to load variables */ -.loadAddonVariables() { - @import (optional) '@{addonelement}.variables'; - @import (optional) - '@{siteFolder}/../addons/@{addon}/@{addontype}s/@{addonelement}.variables'; -} - -@import './variables.less'; - -.esri-map-wrapper { - margin-bottom: 1rem; -} - -// TODO: pull out colors and dimensions into variables -.map-edit-container { - display: flex; -} - -.map-modal-trigger-button { - margin-bottom: 10px !important; -} - -#map-editor-modal { - top: auto; - left: auto !important; - width: 95% !important; -} - -#map-widget-toggle { - color: blue !important; -} - -.map-text-view { - display: flex; - padding: 1rem 0; -} - -.map-editor-modal { - width: '95% !important'; -} - -.webmap-container { - position: sticky; - top: 10px; -} - -.map-edit-actions-container { - padding: 5px 0; - margin-right: 10px; - margin-left: auto; -} - -.layer-reset-button { - margin-top: 5px !important; - animation: fadeDown 0.2s ease-in; -} - -.layer-check-button { - margin-top: 5px !important; - margin-left: auto !important; - animation: fadeDown 0.2s ease-in; -} - -.layer-submit-button { - margin-top: 5px !important; - margin-left: auto !important; - animation: fadeDown 0.2s ease-in; -} - -.ruleGroup { - border: 1px solid lightgray !important; - margin: 10px 0; - background-color: transparent !important; -} - -.ruleGroup-addRule { - padding: 7px 10px !important; - border-radius: 5px; - background-color: darkgray !important; - color: white; - cursor: pointer; - font-size: 12px; - font-weight: bold; -} - -.ruleGroup-addGroup { - padding: 7px 10px !important; - border-radius: 5px; - background-color: darkgray !important; - color: white; - cursor: pointer; - font-size: 12px; - font-weight: bold; -} - -.ruleGroup-combinators { - padding: 0.3em 0 !important; -} - -.rule-operators { - padding: 0.3em 0 !important; -} - -.rule-value { - padding: 0.3em 0 !important; -} - -.rule-fields { - padding: 0.3em 0 !important; -} - -.rule-remove { - padding: 1px 6px !important; - border: 2px solid #d02144 !important; - border-radius: 50px; - color: #d02144; - cursor: pointer; -} - -.map_source_title { - font-weight: bold; -} - -.map_source_description { - margin-left: 5px; -} - -.data-query-widget-field { - margin: 10px 0; -} - -.spaced-row { - width: 100%; - /* border-bottom: 1px solid lightgray; */ - - padding-bottom: 10px; -} - -.legend-title { - display: flex; - width: fit-content; - align-items: center; - margin: 0 !important; -} - -.data-param-title { - padding-bottom: 15px; - border-bottom: 1px solid lightgray; - font-size: 16px; -} - -.data-param-values { - padding-bottom: 15px; - border-bottom: 1px solid lightgray; -} - -.map-layer-description-field { - width: 100%; -} - -.map-layer-description-field - > .slate_wysiwyg - > .grid - > .stretched.row - > .four.wide.column { - display: none !important; -} - -.map-layer-description-field - > .slate_wysiwyg - > .grid - > .stretched.row - > .eight.wide.column { - width: 100% !important; - padding: 10px 0 !important; -} - -.legend-container { - display: flex; - flex-direction: column; -} - -.map_source_param_container { - display: flex; -} - -.extra-view-external-icon { - color: inherit; - font-size: 10px; -} - -.extra-view-external-button { - padding: 2px 15px !important; -} - -.map-number-input { - padding: 5px !important; -} - -.map-range-input { - width: 100%; - margin: 7px 0; - color: green; -} - -.eea-map-info { - display: flex; - flex-direction: row; - - > * { - padding: 0 0.5rem; - border-collapse: collapse; - } - - > *:first-child { - padding-left: 0; - } - - > *:last-child { - padding-right: 0; - } - - > *:not(:last-child) { - border-right: 1px solid @textColor; - } -} - -@keyframes fadeDown { - from { - opacity: 0; - transform: translate3d(0, -20%, 0); - } - - to { - opacity: 1; - transform: translate3d(0, 0, 0); - } -} - -@media print { - .layer-legend-item-color { - max-width: 0.2cm; - height: 0.2cm; - } -} diff --git a/src/less/variables.less b/src/less/variables.less deleted file mode 100644 index 532b4b1..0000000 --- a/src/less/variables.less +++ /dev/null @@ -1,5 +0,0 @@ -@type: extra; - -@element: custom; - -@grey-1: #f9f9f9; diff --git a/src/styles/editor.less b/src/styles/editor.less new file mode 100644 index 0000000..7ec361e --- /dev/null +++ b/src/styles/editor.less @@ -0,0 +1,446 @@ +.arcgis-map { + &__editor { + --color-background-light: #f3f6fa; + --color-background-top: #fff; + --color-border-default: #c8d4e3; + --color-border-light: #dfe8f3; + --color-text-active: #2a3f5f; + --color-accent: #119dff; + --color-accent-shade: #0d76bf; + --color-accent-shade-mid: #0d76bf; + --color-button-primary-base-fill: var(--color-accent); + --color-button-primary-base-border: var(--color-accent-shade); + --color-button-primary-hover-fill: var(--color-accent-shade-mid); + --color-button-primary-hover-border: var(--color-accent-shade); + --color-button-primary-base-text: #fff; + --color-button-primary-hover-text: #fff; + --text-shadow-dark-color: rgba(42, 63, 95, 0.7); + --border-default: 1px solid var(--color-border-default); + --border-light: 1px solid var(--color-border-light); + --border-radius: 5px; + --spacing-half-unit: 12px; + --spacing-quarter-unit: 6px; + --sidebar-background: #fff; + --sidebar-group-background-base: var(--sidebar-background); + --sidebar-item-background-base: var(--color-background-light); + --sidebar-width: 100px; + --panel-width: 500px; + --panel-background: #ebf0f8; + --editor-width: calc(var(--sidebar-width) + var(--panel-width) + 1px); + --font-weight-normal: 400; + --font-weight-semibold: 600; + --font-size-small: 12px; + --font-size-base: 13px; + --font-size-medium: 14px; + --color-text-base: #506784; + --text-shadow-dark-ui: 0 1px 2px var(--text-shadow-dark-color); + --text-shadow-dark-ui-inactive: 0 1px 1px rgba(42, 63, 95, 0.4); + --box-shadow-base-color: rgba(80, 103, 132, 0.2); + --box-shadow-base: 0px 2px 9px var(--box-shadow-base-color); + --fold-header-background-base: #506784; + --fold-header-border-color-base: #506784; + --fold-header-text-color-closed: #fff; + --fold-header-text-color-base: #fff; + --fold-header-border-color-closed: #2a3f5f; + --fold-header-background-closed: #2a3f5f; + + display: flex; + width: 100%; + height: 100%; + max-height: 100%; + flex-grow: 1; + + button { + display: inline-flex; + height: 36px; + align-items: center; + padding: var(--spacing-quarter-unit) var(--spacing-half-unit); + border-width: 1px; + border-style: solid; + border-color: transparent; + border-radius: var(--border-radius); + column-gap: 0.25rem; + cursor: pointer; + font-size: var(--font-size-medium); + font-weight: var(--font-weight-semibold); + letter-spacing: 0.5px; + line-height: 1; + outline: none; + text-align: center; + transition: all 0.15s ease-in-out; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &:first-letter { + text-transform: capitalize; + } + + &.btn-primary { + border-color: var(--color-button-primary-base-border); + background-color: var(--color-button-primary-base-fill); + color: var(--color-button-primary-base-text); + text-shadow: var(--text-shadow-dark-ui); + + &:hover { + border: 1px solid var(--color-button-primary-hover-border); + background-color: var(--color-button-primary-hover-fill); + color: var(--color-button-primary-hover-text); + } + } + } + + .ruleGroup .rule .rule-value:has(.rule-value-list-item) { + input { + width: 50%; + } + } + + .ruleGroup .ruleGroup-body { + gap: 1rem; + } + + .ruleGroup .custom-rule { + display: flex; + flex-flow: column; + gap: 0.5rem; + + .react-select { + .react-select__control { + width: 100%; + min-height: auto; + } + + .react-select__value-container { + padding: 0.3em !important; + } + + .react-select__indicator { + padding: 0.3em !important; + } + } + } + } + + &__view { + .esri-view { + --ag-map-height: 100% !important; + } + } + + &__controls { + position: relative; + display: flex; + overflow: hidden; + width: var(--editor-width); + flex-shrink: 0; + margin-right: 1rem; + } + + &__view { + position: sticky; + top: 10px; + width: 100%; + height: 100%; + } + + &__sidebar { + width: var(--sidebar-width); + min-width: var(--sidebar-width); + max-width: var(--sidebar-width); + height: 100%; + flex-grow: 1; + border-right: var(--border-default); + background: var(--sidebar-background); + float: left; + -webkit-overflow-scrolling: touch; + overflow-x: hidden; + overflow-y: auto; + text-align: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + .sidebar-group { + &--title { + position: relative; + display: flex; + width: 100%; + align-items: center; + padding: var(--spacing-half-unit) 0; + border-bottom: var(--border-light); + background-color: var(--sidebar-group-background-base); + + color: var(--color-text-base); + cursor: pointer; + font-size: var(--font-size-medium); + font-weight: var(--font-weight-normal); + text-align: left; + text-transform: capitalize; + + &__icon { + height: 20px; + + svg { + rotate: 90deg; + } + } + } + + &--item { + position: relative; + overflow: hidden; + padding: 10px; + padding-right: var(--spacing-quarter-unit); + padding-left: 18px; + border-bottom: var(--border-light); + background-color: var(--sidebar-item-background-base); + color: var(--color-text-base); + cursor: pointer; + font-size: var(--font-size-medium); + font-weight: var(--font-weight-normal); + line-height: var(--font-size-medium); + text-align: left; + text-transform: capitalize; + + &::before { + position: absolute; + top: 0; + left: 0; + width: 5px; + height: 100%; + background-color: var(--color-accent); + content: ''; + transform: scaleX(0); + transform-origin: left center; + transition: all 0.15s ease-in-out; + will-change: transform; + } + + &__is-active { + color: var(--color-text-active); + cursor: default; + font-weight: var(--font-weight-semibold); + + &::before { + transform: none; + } + } + } + + &--expanded { + .sidebar-group--title { + position: relative; + z-index: 4; + box-shadow: var(--box-shadow-base); + + &__icon svg { + rotate: 180deg; + } + } + } + + &--is-active { + .sidebar-group--title { + color: var(--color-text-active); + font-weight: var(--font-weight-semibold); + + &__icon svg { + fill: var(--color-accent) !important; + } + } + } + } + } + + &__panel { + position: relative; + display: flex; + width: var(--panel-width); + box-sizing: border-box; + flex-direction: column; + flex-grow: 1; + padding: var(--spacing-half-unit); + border-right: var(--border-default); + background-color: var(--panel-background); + -webkit-overflow-scrolling: touch; + overflow-x: hidden; + overflow-y: auto; + + .panel { + &--header { + display: flex; + flex-shrink: 0; + margin-bottom: var(--spacing-half-unit); + } + } + + .fold { + position: relative; + width: 100%; + margin-bottom: var(--spacing-half-unit); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &.foldable { + .fold--top { + cursor: pointer; + } + } + + &--top { + display: flex; + height: auto; + justify-content: space-between; + padding: var(--spacing-quarter-unit) var(--spacing-half-unit); + border: 1px solid var(--fold-header-border-color-closed); + border-radius: var(--border-radius); + background-color: var(--fold-header-background-closed); + clear: both; + color: var(--fold-header-text-color-closed); + font-size: var(--font-size-base); + text-shadow: var(--text-shadow-dark-ui); + transition: + background-color 0.1s ease-in-out, + color 0.1s ease-in-out, + border 0.1s ease-in-out; + + &__content { + display: flex; + flex-grow: 1; + } + + &__fold { + rotate: 90deg; + } + + &__title { + overflow: hidden; + max-width: 230px; + margin-left: calc(var(--spacing-half-unit) / 3); + font-size: var(--font-size-medium); + font-weight: var(--font-weight-semibold); + letter-spacing: 0.2px; + line-height: var(--font-size-medium); + text-overflow: ellipsis; + transform: translateY(1px); + white-space: nowrap; + } + } + + &--content { + padding: 0.5rem; + border: var(--border-default); + border-width: 0 1px 1px 1px; + background: var(--color-background-top); + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } + + &__open { + .fold--top { + border: 1px solid var(--fold-header-border-color-base); + border-radius: var(--border-radius) var(--border-radius) 0 0; + background-color: var(--fold-header-background-base); + color: var(--fold-header-text-color-base); + text-shadow: var(--text-shadow-dark-ui-inactive); + } + + .fold--top__fold { + rotate: 180deg; + } + } + } + } +} + +.arcgis-map__editor { + .queryBuilder { + font-size: var(--font-size-small); + + button { + height: auto !important; + font-size: var(--font-size-small); + } + + select.rule-fields { + max-width: 100px; + } + + input.rule-value { + width: 100%; + } + } + + .ui.segment.form.attached .field:last-child { + margin-bottom: 0; + } + + .ui.form .field .wrapper > label, + .inline.field { + font-size: var(--font-size-small); + } + + .ui.form.accordion .title { + display: flex; + height: auto; + flex-flow: row-reverse; + align-items: center; + justify-content: flex-end; + padding: var(--spacing-quarter-unit) var(--spacing-half-unit); + border: 1px solid var(--fold-header-border-color-base); + border-radius: var(--border-radius) var(--border-radius) 0 0; + background-color: var(--fold-header-background-base) !important; + clear: both; + color: var(--fold-header-text-color-base) !important; + column-gap: calc(var(--spacing-half-unit) / 3); + font-size: var(--font-size-medium); + text-shadow: var(--text-shadow-dark-ui-inactive); + transition: + background-color 0.1s ease-in-out, + color 0.1s ease-in-out, + border 0.1s ease-in-out; + + svg { + height: 16px !important; + rotate: 180deg !important; + } + + &:not(.active) { + border: 1px solid var(--fold-header-border-color-closed) !important; + border-radius: var(--border-radius) !important; + background-color: var(--fold-header-background-closed) !important; + color: var(--fold-header-text-color-closed) !important; + text-shadow: var(--text-shadow-dark-ui) !important; + + svg { + rotate: -90deg !important; + } + } + } + + #blockform-fieldset-default { + .ui.segment.form:empty { + display: none; + } + } +} + +.arcgis-renderer-editor { + padding: 1rem 0.625rem; + font-size: var(--font-size-small); + + .ui.form.segment { + padding: 0; + } +} + +.arcgis-viewpoint-editor { + .ui.form.segment { + padding: 0; + } +} diff --git a/src/styles/map.less b/src/styles/map.less new file mode 100644 index 0000000..28b2539 --- /dev/null +++ b/src/styles/map.less @@ -0,0 +1,3 @@ +.esri-view { + height: var(--ag-map-height, 600px); +} diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 337a9fb..0000000 --- a/src/utils.js +++ /dev/null @@ -1,151 +0,0 @@ -/* eslint-disable no-throw-literal */ -import React from 'react'; -import { getBaseUrl } from '@plone/volto/helpers'; - -const setLegendColumns = (legendsNo, device) => { - switch (device) { - case 'widescreen': - return legendsNo ? legendsNo : 1; - case 'large': - return legendsNo ? legendsNo : 1; - case 'computer': - return legendsNo ? legendsNo : 1; - case 'tablet': - return 1; - case 'mobile': - return 1; - default: - return 1; - } -}; - -const fetchArcGISData = async (url) => { - const res = await fetch(`${getBaseUrl('')}/cors-proxy/${url}?f=json`); - if (res.status !== 200) { - const error = await res.json(); - throw { message: error.message, status: error.cod }; - } - const data = await res.json(); - if (data.error && data.error.code === 400) { - throw { message: data.error.message.message, status: data.status }; - } - return data; -}; - -const applyQueriesToMapLayers = ( - map_visualization, - block_data_query_params, - enable_queries, -) => { - //break reference to the original map_visualization object - //so i safely manipulate data - var altMapData = map_visualization - ? JSON.parse(JSON.stringify(map_visualization)) - : ''; - - var rules = []; - if ( - enable_queries && - block_data_query_params && - block_data_query_params.length > 0 && - altMapData.layers && - altMapData.layers.map_layers && - altMapData.layers.map_layers.length > 0 - ) { - altMapData.layers.map_layers.forEach((l, j) => { - block_data_query_params.forEach((param, i) => { - const matchingFields = - l.map_layer && l.map_layer.fields && l.map_layer.fields.length > 0 - ? l.map_layer.fields.filter( - (field, k) => - field.name === param.alias || field.name === param.i, - ) - : []; - matchingFields.forEach((m, i) => { - const newRules = param.v - ? param.v.map((paramVal, i) => { - return { - field: m.name, - operator: '=', - value: paramVal, - }; - }) - : []; - const concatRules = rules.concat(newRules); - const filteredRules = concatRules.filter( - (v, i, a) => - a.findLastIndex( - (v2) => v2.field === v.field && v2.value === v.value, - ) === i, - ); - rules = filteredRules; - }); - }); - let autoQuery = { - combinator: 'or', - rules, - }; - altMapData.layers.map_layers[j].map_layer.query = autoQuery; - }); - } - return altMapData; -}; - -const updateBlockQueryFromPageQuery = (data_query, data_query_params) => { - var pageDataQuery = JSON.parse(JSON.stringify(data_query)); - var blockDataQuery = data_query_params - ? JSON.parse(JSON.stringify(data_query_params)) - : ''; - var newDataQuery = pageDataQuery.map((parameter, index) => { - //check if the parameter exists in data and has value - // then get its alias value and update it - //check if data_query param value is changed - //and change it in block data - if ( - blockDataQuery && - blockDataQuery[index] && - parameter.i && - blockDataQuery[index].i && - parameter.i === blockDataQuery[index].i - ) { - return { - ...parameter, - alias: blockDataQuery[index]?.alias ? blockDataQuery[index]?.alias : '', - v: parameter?.v ? parameter?.v : '', - }; - } - - return parameter; - }); - return newDataQuery; -}; - -const useCopyToClipboard = (text) => { - const [copyStatus, setCopyStatus] = React.useState('inactive'); - const copy = React.useCallback(() => { - navigator.clipboard.writeText(text).then( - () => setCopyStatus('copied'), - () => setCopyStatus('failed'), - ); - }, [text]); - - React.useEffect(() => { - if (copyStatus === 'inactive') { - return; - } - - const timeout = setTimeout(() => setCopyStatus('inactive'), 3000); - - return () => clearTimeout(timeout); - }, [copyStatus]); - - return [copyStatus, copy]; -}; - -export { - setLegendColumns, - fetchArcGISData, - applyQueriesToMapLayers, - updateBlockQueryFromPageQuery, - useCopyToClipboard, -};