From 51298f7e56df41d4eff10a27725365a0d0404b7e Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 12 Apr 2023 13:01:15 +0200 Subject: [PATCH] feature: draw as core control (#1714) * Added draw as control * Update stylewindow.js * Refactored code for measure control and added modify function * Update draw.js * Fixes for draw * Lots of stuff * Clean up permalink * Some styling fixes * Lint * Update viewer.js * Fix * stylewindow fix * Styling tweaks * Small fixes for print * Update stylewindow.js * Fix for labels and buffer measure * Forgot about measure in mapstate * Added projection for measures * drawHandler as component * Moved styling to layer level * Added screen/menu button placement * Initial state for save button * Added import layer functionality * Update viewer.js * Update drawtools.js * Unique layer names and new icon * Update draw.js * Fixed scaling when printing * Fix default function * Make group draggable * Fixed bugs --- css/svg/material-icons.svg | 2 + package-lock.json | 12 +- package.json | 2 +- scss/_draw.scss | 56 ++ scss/origo.scss | 1 + scss/ui/_flex.scss | 4 + src/controls.js | 1 + src/controls/draganddrop.js | 35 +- src/controls/draw.js | 674 ++++++++++++++++++++++ src/controls/draw/drawhandler.js | 556 +++++++++++++++++++ src/controls/draw/drawtools.js | 131 +++++ src/controls/draw/shapes.js | 18 + src/controls/legend/overlay.js | 10 +- src/controls/measure.js | 766 ++++++++------------------ src/controls/print/print-component.js | 4 + src/controls/print/print-legend.js | 4 +- src/controls/print/print-resize.js | 22 +- src/dropdown.js | 1 + src/layer.js | 5 + src/layer/geojson.js | 118 ++-- src/layer/vector.js | 19 +- src/permalink/permalinkparser.js | 14 +- src/permalink/permalinkstore.js | 2 +- src/style/drawstyles.js | 473 ++++++++++++++++ src/style/hextorgba.js | 21 + src/style/measure.js | 91 --- src/style/stylefunctions/default.js | 21 +- src/style/styletemplate.js | 104 ++++ src/style/styletypes.js | 2 +- src/style/stylewindow.js | 509 +++++++++++++++++ src/templates/featureinfotemplate.js | 2 +- src/ui/input.js | 5 + src/ui/modal.js | 13 +- src/utils/escapequotes.js | 3 + src/utils/exporttofile.js | 9 + src/utils/templatehelpers.js | 2 +- src/utils/validate.js | 9 + src/viewer.js | 11 +- 38 files changed, 3017 insertions(+), 715 deletions(-) create mode 100644 scss/_draw.scss create mode 100644 src/controls/draw.js create mode 100644 src/controls/draw/drawhandler.js create mode 100644 src/controls/draw/drawtools.js create mode 100644 src/controls/draw/shapes.js create mode 100644 src/style/drawstyles.js create mode 100644 src/style/hextorgba.js delete mode 100644 src/style/measure.js create mode 100644 src/style/styletemplate.js create mode 100644 src/style/stylewindow.js create mode 100644 src/utils/escapequotes.js diff --git a/css/svg/material-icons.svg b/css/svg/material-icons.svg index 50d26a394..9cc5d6245 100644 --- a/css/svg/material-icons.svg +++ b/css/svg/material-icons.svg @@ -59,5 +59,7 @@ + + diff --git a/package-lock.json b/package-lock.json index 71d7dfc90..5bc78805c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1810,9 +1810,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.4.329", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.329.tgz", - "integrity": "sha512-dcwPzNUG4+reo5z+wHnrl2eZMu4kz+nLQEeepxLEDTLDC7Mi7AVTM4NXWct1TZyu3G4oQgygaAfbByaBtPqw2Q==", + "version": "1.4.332", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.332.tgz", + "integrity": "sha512-c1Vbv5tuUlBFp0mb3mCIjw+REEsgthRgNE8BlbEDKmvzb8rxjcVki6OkQP83vLN34s0XCxpSkq7AZNep1a6xhw==", "dev": true }, "elm-pep": { @@ -6463,9 +6463,9 @@ "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==" }, "webpack": { - "version": "5.76.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", - "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", + "version": "5.76.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.2.tgz", + "integrity": "sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", diff --git a/package.json b/package.json index 781945930..259dd7ca8 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "run-sequence": "^2.2.1", "source-map-loader": "^3.0.0", "terser-webpack-plugin": "^5.2.4", - "webpack": "^5.76.0", + "webpack": "^5.76.2", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.1", "webpack-dev-server": "^4.7.3", diff --git a/scss/_draw.scss b/scss/_draw.scss new file mode 100644 index 000000000..d36b8bf28 --- /dev/null +++ b/scss/_draw.scss @@ -0,0 +1,56 @@ +.o-draw-toolbar { + bottom: 0; + left: 50%; + margin-bottom: .5rem !important; + max-width: 80%; + position: relative; + -ms-transform: translate(-50%, 0); + -webkit-transform: translate(-50%, 0); + transform: translate(-50%, 0); + z-index: 10; +} + +.o-draw-stylewindow { + font-family: Arial,sans-serif; + width: 250px; + position:absolute; + right: 1rem; + -ms-transform: translate(0, -50%); + -webkit-transform: translate(0, -50%); + transform: translate(0, -50%); + top:50%; + height: calc(100% - 9rem); +} + +.o-draw-stylewindow input[type="radio"] { + display:none; +} + +.o-draw-stylewindow input[type="radio"] + label span { + display:inline-block; + width:24px; + height:24px; + vertical-align:middle; + cursor:pointer; + -moz-border-radius: 50%; + border-radius: 50%; + box-shadow: 0 0 0 2px white, 0 0 0 4px white; +} + +.o-draw-stylewindow input[type="radio"]:checked + label span{ + box-shadow: 0 0 0 2px white, 0 0 0 4px #ababab; +} + +.o-draw-stylewindow input[type="radio"] ~ label span:hover { + box-shadow: 0 0 0 2px white, 0 0 0 4px #cdcdcd; +} + +.o-draw-stylewindow input[type="radio"] ~ label { + display:inline-block; + vertical-align:middle; + padding:5px; +} + +.o-draw-stylewindow ul li { + float:left; +} diff --git a/scss/origo.scss b/scss/origo.scss index 9c84b55b6..adef8d848 100644 --- a/scss/origo.scss +++ b/scss/origo.scss @@ -32,6 +32,7 @@ @import 'viewer'; @import 'rotate'; @import 'mapmenu'; + @import 'draw'; @import 'editor'; @import 'editor-toolbar'; @import 'position'; diff --git a/scss/ui/_flex.scss b/scss/ui/_flex.scss index 7d27913fa..feeba75f0 100644 --- a/scss/ui/_flex.scss +++ b/scss/ui/_flex.scss @@ -114,4 +114,8 @@ $flex-grow-all: 2; .basis-100 { flex-basis: 100%; } + + .small-gap { + gap: 0.2rem; + } } diff --git a/src/controls.js b/src/controls.js index 7be970928..590bb335d 100644 --- a/src/controls.js +++ b/src/controls.js @@ -2,6 +2,7 @@ export { default as About } from './controls/about'; export { default as Attribution } from './controls/attribution'; export { default as Bookmarks } from './controls/bookmarks'; export { default as Draganddrop } from './controls/draganddrop'; +export { default as Draw } from './controls/draw'; export { default as Editor } from './controls/editor'; export { default as Fullscreen } from './controls/fullscreen'; export { default as Geoposition } from './controls/geoposition'; diff --git a/src/controls/draganddrop.js b/src/controls/draganddrop.js index ae0e0fef1..919cb25f7 100644 --- a/src/controls/draganddrop.js +++ b/src/controls/draganddrop.js @@ -4,11 +4,8 @@ import GeoJSONFormat from 'ol/format/GeoJSON'; import IGCFormat from 'ol/format/IGC'; import KMLFormat from 'ol/format/KML'; import TopoJSONFormat from 'ol/format/TopoJSON'; -import VectorSource from 'ol/source/Vector'; -import VectorLayer from 'ol/layer/Vector'; import Style from '../style'; import { Component, InputFile, Button, Element as El } from '../ui'; -import { getStylewindowStyle } from './editor/stylewindow'; const DragAndDrop = function DragAndDrop(options = {}) { let dragAndDrop; @@ -118,8 +115,6 @@ const DragAndDrop = function DragAndDrop(options = {}) { } }] }; - let vectorSource; - let vectorLayer; const vectorStyles = Style.createGeometryStyle(featureStyles); dragAndDrop = new olDragAndDrop({ formatConstructors: [ @@ -147,30 +142,26 @@ const DragAndDrop = function DragAndDrop(options = {}) { i += 1; } } - vectorSource = new VectorSource({ - features: event.features - }); - vectorSource.forEachFeature((feature) => { - if (feature.get('style') && styleByAttribute) { - const featureStyle = getStylewindowStyle(feature, feature.get('style')); - feature.setStyle(featureStyle); - } - }); if (!viewer.getGroup(groupName)) { viewer.addGroup({ title: groupTitle, name: groupName, expanded: true, draggable }); } - vectorLayer = new VectorLayer({ - source: vectorSource, - name: layerName, + const layerOptions = { group: groupName, + name: layerName, title: layerTitle, + zIndex: 6, + styleByAttribute, queryable: true, removable: true, - style: vectorStyles[event.features[0].getGeometry().getType()] - }); - - map.addLayer(vectorLayer); - map.getView().fit(vectorSource.getExtent()); + visible: true, + source: 'none', + type: 'GEOJSON', + features: event.features + }; + if (!styleByAttribute) { + layerOptions.style = vectorStyles[event.features[0].getGeometry().getType()]; + } + viewer.addLayer(layerOptions); }); this.render(); }, diff --git a/src/controls/draw.js b/src/controls/draw.js new file mode 100644 index 000000000..bb7e99468 --- /dev/null +++ b/src/controls/draw.js @@ -0,0 +1,674 @@ +import { Button, dom, Component, Element as El, Input, InputFile, Modal } from '../ui'; +import DrawHandler from './draw/drawhandler'; +import drawExtraTools from './draw/drawtools'; +import exportToFile from '../utils/exporttofile'; +import validate from '../utils/validate'; + +const Draw = function Draw(options = {}) { + const { + buttonText = 'Rita', + placement = ['menu'], + icon = '#fa-pencil', + annotation, + showAttributeButton = false, + showDownloadButton = false, + showSaveButton = false, + multipleLayers = false + } = options; + + let { + isActive = false + } = options; + + const drawDefaults = { + layerTitle: 'Ritlager', + groupName: 'none', + groupTitle: 'Ritlager', + visible: true, + styleByAttribute: true, + queryable: false, + removable: true, + exportable: true, + drawlayer: true, + draggable: true + }; + + let map; + let viewer; + let drawTools; + let mapTools; + let screenButtonContainer; + let screenButton; + let mapMenu; + let menuItem; + let stylewindow; + let saveButton; + let layerAttributeButton; + let thisComponent; + let drawHandler; + + const drawOptions = Object.assign({}, drawDefaults, options); + + function setActive(state) { + if (state === true) { + document.getElementById(thisComponent.getId()).classList.remove('o-hidden'); + if (screenButton) { + screenButton.setState('active'); + } + isActive = true; + } else { + document.getElementById(thisComponent.getId()).classList.add('o-hidden'); + thisComponent.dispatch('toggleDraw', { tool: 'cancel' }); + stylewindow.dispatch('showStylewindow', false); + if (screenButton) { + screenButton.setState('initial'); + } + isActive = false; + } + } + + function onEnableInteraction(e) { + if (e.detail.name === 'draw' && e.detail.active) { + setActive(true); + } else { + setActive(false); + } + } + + function toggleState(tool, state) { + if (state === false) { + tool.setState('initial'); + } else { + tool.setState('active'); + } + } + + function changeDrawState(detail) { + const tools = Object.getOwnPropertyNames(drawTools); + tools.forEach((tool) => { + if (tool === detail.tool) { + toggleState(drawTools[tool], detail.active); + } else { + toggleState(drawTools[tool], false); + } + }); + } + + const attributeForm = Component({ // Add attribute to feature + name: 'attributeForm', + show() { + if (drawHandler.getSelection().getArray().length > 0) { + const feature = drawHandler.getSelection().getArray()[0]; + const val = feature.get('popuptext') || ''; + + const formSaveButton = Button({ + cls: 'margin-small icon-smaller light box-shadow', + style: 'border-radius: 3px', + icon: '#ic_save_24px', + text: 'Spara' + }); + + const cancelButton = Button({ + cls: 'margin-small icon-smaller light box-shadow', + style: 'border-radius: 3px', + icon: '#ic_close_24px', + text: 'Avbryt' + }); + + const inputEl = Input({ + cls: 'no-margin', + placeholderText: 'Ange popuptext', + value: val + }); + + const modalContent = El({ + cls: 'padding-small flex row wrap align-start justify-space-evenly', + components: [inputEl, formSaveButton, cancelButton] + }); + + const modal = Modal({ + title: 'Popuptext', + contentCmp: modalContent, + target: viewer.getId() + }); + + formSaveButton.on('click', () => { + const inputVal = inputEl.getValue() || ''; + feature.set('popuptext', inputVal); + modal.closeModal(); + }); + + cancelButton.on('click', () => { + modal.closeModal(); + }); + + modal.show(); + } + } + }); + + const onLayerDelete = function onLayerDelete(evt) { + const activeLayer = drawHandler.getActiveLayer(); + const removedLayer = evt.element; + if (activeLayer === removedLayer) { + const drawLayers = drawHandler.getDrawLayers(); + if (drawLayers.length > 0) { + drawHandler.setActiveLayer(drawLayers[drawLayers.length - 1]); + } else { + drawHandler.setActiveLayer(null); + } + } + }; + + const layerForm = Component({ // Handle draw layers + name: 'layerForm', + show() { + const drawLayers = drawHandler.getDrawLayers(); + const activeLayer = drawHandler.getActiveLayer(); + const components = []; + let modal; + const thisForm = this; + + drawLayers.reverse().forEach(drawLayer => { + const layerTitle = drawLayer.get('title'); + const layerName = drawLayer.get('name'); + + const inputEl = Input({ + cls: 'margin-right', + placeholderText: 'Ange lagernamn', + value: layerTitle + }); + + inputEl.on('focusout', (e) => { + drawLayer.set('title', e.value); + }); + + const activeButton = Button({ + cls: 'margin-right-small padding-small icon-smaller round light box-shadow relative o-tooltip', + icon: '#ic_check_24px', + state: drawLayer === activeLayer ? 'active' : 'initial', + click() { + drawHandler.setActiveLayer(drawLayer); + thisForm.dispatch('activeLayerChange', { layername: layerName }); + }, + tooltipText: 'Aktivera ritlager', + tooltipPlacement: 'west' + }); + + this.on('activeLayerChange', (e) => { + activeButton.setState(e.layername === layerName ? 'active' : 'initial'); + }); + + const deleteButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative o-tooltip', + icon: '#ic_delete_24px', + async click() { + if (window.confirm('Vill du radera det här ritlagret?') === true) { + await map.removeLayer(drawLayer); + modal.closeModal(); + thisForm.show(); + } + }, + tooltipText: 'Radera ritlager', + tooltipPlacement: 'west' + }); + + const downloadLayerButton = Button({ + cls: 'margin-right-small padding-small icon-smaller round light box-shadow relative o-tooltip', + icon: '#ic_download_24px', + click() { + const features = drawLayer.getSource().getFeatures(); + exportToFile(features, 'geojson', { + featureProjection: viewer.getProjection().getCode(), + filename: drawLayer.get('title') || 'export' + }); + }, + tooltipText: 'Ladda ner ritlager', + tooltipPlacement: 'west' + }); + + const layerRow = El({ + cls: 'flex row align-start justify-space-evenly', + components: [inputEl, activeButton, downloadLayerButton, deleteButton], + tagName: 'div' + }); + + components.push(layerRow); + }); + + const addLayerButton = Button({ + cls: 'icon-smaller light box-shadow', + style: 'border-radius: 3px', + icon: '#ic_add_24px', + text: 'Nytt ritlager', + async click() { + let title = drawOptions.layerTitle; + if (viewer.getLayersByProperty('title', title).length > 0) { + let i = 1; + while (i <= drawLayers.length) { + if (viewer.getLayersByProperty('title', `${title} ${i}`).length === 0) { + title = `${title} ${i}`; + break; + } + i += 1; + } + } + const addedLayer = await drawHandler.addLayer({ layerTitle: title }); + drawHandler.setActiveLayer(addedLayer); + modal.closeModal(); + layerForm.show(); + } + }); + + const fileInput = InputFile({ + labelCls: 'hidden', + inputCls: 'hidden', + change(e) { + const file = e.target.files[0]; + const fileName = file.name.substring(0, file.name.lastIndexOf('.')) || file.name; + const reader = new FileReader(); + reader.addEventListener( + 'loadend', + async () => { + if (validate.json(reader.result)) { + const features = reader.result; + const addedLayer = await drawHandler.addLayer({ features, layerTitle: fileName }); + drawHandler.setActiveLayer(addedLayer); + modal.closeModal(); + thisForm.show(); + } + }, + false + ); + if (file) { + reader.readAsText(file); + } + } + }); + + const openBtn = Button({ + cls: 'icon-smaller light box-shadow', + style: 'border-radius: 3px', + icon: '#ic_add_24px', + click() { + const inputEl = document.getElementById(fileInput.getId()); + inputEl.value = null; + inputEl.click(); + }, + text: 'Importera ritlager', + ariaLabel: 'Importera ritlager' + }); + + const okButton = Button({ + cls: 'icon-smaller light box-shadow', + style: 'border-radius: 3px', + icon: '#ic_check_24px', + text: 'OK', + click() { + modal.closeModal(); + } + }); + + const buttonRow = El({ + cls: 'flex row align-start justify-space-evenly margin-top-large', + components: [okButton, addLayerButton, fileInput, openBtn], + tagName: 'div' + }); + + components.push(buttonRow); + + const modalContent = El({ + cls: 'padding-small align-start justify-space-evenly', + components, + tagName: 'div' + }); + + modal = Modal({ + title: 'Ritlager', + contentCmp: modalContent, + target: viewer.getId(), + style: 'max-width:100%;width:400px;' + }); + + modal.show(); + } + }); + + const toolbarButtons = []; + + const pointButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + if (this.getState() !== 'disabled') { + thisComponent.dispatch('toggleDraw', { tool: 'Point' }); + } + }, + icon: '#ic_place_24px', + tooltipText: 'Punkt', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;', + state: 'inactive' + }); + + toolbarButtons.push(pointButton); + + const lineButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + if (this.getState() !== 'disabled') { + thisComponent.dispatch('toggleDraw', { tool: 'LineString' }); + } + }, + icon: '#ic_timeline_24px', + tooltipText: 'Linje', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;' + }); + + toolbarButtons.push(lineButton); + + const polygonButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + if (this.getState() !== 'disabled') { + thisComponent.dispatch('toggleDraw', { tool: 'Polygon' }); + } + }, + icon: '#o_polygon_24px', + tooltipText: 'Polygon', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;' + }); + + toolbarButtons.push(polygonButton); + + const textButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + if (this.getState() !== 'disabled') { + thisComponent.dispatch('toggleDraw', { tool: 'Text' }); + } + }, + icon: '#ic_title_24px', + tooltipText: 'Text', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;' + }); + + toolbarButtons.push(textButton); + + if (showAttributeButton) { + layerAttributeButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + attributeForm.show(); + }, + icon: '#ic_textsms_24px', + tooltipText: 'Attribut', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;', + state: 'disabled' + }); + + toolbarButtons.push(layerAttributeButton); + } + + const stylewindowButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + const stylewindowEl = document.getElementById(stylewindow.getId()); + stylewindowEl.classList.toggle('hidden'); + if (this.getState() === 'active') { + this.setState('initial'); + } else { this.setState('active'); } + }, + icon: '#ic_palette_24px', + tooltipText: 'Stil', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;' + }); + + toolbarButtons.push(stylewindowButton); + + if (multipleLayers) { + const layerButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + layerForm.show(); + }, + icon: '#ic_layers_24px', + tooltipText: 'Lager', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;' + }); + + toolbarButtons.push(layerButton); + } + + if (showSaveButton) { + saveButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + thisComponent.dispatch('saveFeatures', true); + }, + icon: '#ic_save_24px', + tooltipText: 'Spara', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;', + state: 'disabled' + }); + + toolbarButtons.push(saveButton); + } + + if (showDownloadButton) { + const downloadButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + const drawLayer = drawHandler.getActiveLayer(); + const features = drawLayer.getSource().getFeatures(); + exportToFile(features, 'geojson', { + featureProjection: viewer.getProjection().getCode(), + filename: drawLayer.get('title') || 'export' + }); + }, + icon: '#ic_download_24px', + tooltipText: 'Ladda ner', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;' + }); + toolbarButtons.push(downloadButton); + } + + const deleteFeatureButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + if (drawHandler.getSelection().getLength() > 0 && window.confirm('Vill du radera det här objektet?')) { + thisComponent.dispatch('toggleDraw', { tool: 'delete' }); + } + }, + icon: '#ic_delete_24px', + tooltipText: 'Ta bort', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;', + state: 'disabled' + }); + + toolbarButtons.push(deleteFeatureButton); + + const closeToolbarButton = Button({ + cls: 'padding-small icon-smaller round light box-shadow relative', + click() { + const stylewindowEl = document.getElementById(stylewindow.getId()); + stylewindowEl.classList.add('hidden'); + stylewindowButton.setState('initial'); + viewer.dispatch('toggleClickInteraction', { name: 'draw', active: false }); + }, + icon: '#ic_close_24px', + tooltipText: 'Stäng', + tooltipPlacement: 'south', + tooltipStyle: 'bottom:-5px;' + }); + + toolbarButtons.push(closeToolbarButton); + + const drawToolbarElement = El({ + cls: 'flex fixed bottom-center divider-horizontal bg-inverted z-index-ontop-high no-print small-gap', + style: 'height: 2rem;', + components: toolbarButtons + }); + + return Component({ + name: 'draw', + attributeForm, + getDrawHandler() { + return drawHandler; + }, + saveButton, + getSelection() { + return drawHandler.getSelection(); + }, + getDrawOptions() { + return drawOptions; + }, + getState() { + return drawHandler.getState(); + }, + isActive() { + return isActive; + }, + onInit() { + thisComponent = this; + this.on('render', this.onRender); + }, + onRender() { + drawTools = { + Point: pointButton, + LineString: lineButton, + Polygon: polygonButton, + Text: textButton + }; + const extraTools = drawOptions.drawTools || []; + drawExtraTools(extraTools, viewer, drawTools); + }, + onAdd(evt) { + viewer = evt.target; + map = viewer.getMap(); + stylewindow = viewer.getStylewindow(); + stylewindow.on('showStylewindow', function showStylewindow(e) { + if (e) { + stylewindowButton.setState('active'); + const stylewindowEl = document.getElementById(this.getId()); + stylewindowEl.classList.remove('hidden'); + } else { + stylewindowButton.setState('initial'); + const stylewindowEl = document.getElementById(this.getId()); + stylewindowEl.classList.add('hidden'); + } + }); + + if (placement.indexOf('screen') > -1) { + mapTools = `${viewer.getMain().getMapTools().getId()}`; + screenButtonContainer = El({ + tagName: 'div', + cls: 'flex column' + }); + screenButton = Button({ + cls: 'o-print padding-small margin-bottom-smaller icon-smaller round light box-shadow', + click() { + if (!isActive) { + viewer.dispatch('toggleClickInteraction', { name: 'draw', active: true }); + } else { + viewer.dispatch('toggleClickInteraction', { name: 'draw', active: false }); + } + }, + icon, + tooltipText: buttonText, + tooltipPlacement: 'east' + }); + this.addComponent(screenButton); + } + if (placement.indexOf('menu') > -1) { + mapMenu = viewer.getControlByName('mapmenu'); + menuItem = mapMenu.MenuItem({ + click() { + if (!isActive) { + viewer.dispatch('toggleClickInteraction', { name: 'draw', active: true }); + } else { + viewer.dispatch('toggleClickInteraction', { name: 'draw', active: false }); + } + mapMenu.close(); + }, + icon, + title: buttonText + }); + this.addComponent(menuItem); + } + + this.addComponent(drawToolbarElement); + if (showAttributeButton) { this.addComponent(attributeForm); } + if (multipleLayers) { this.addComponent(layerForm); } + drawHandler = DrawHandler({ + viewer, + annotation, + drawCmp: this, + stylewindow + }); + drawHandler.restoreState(viewer.getUrlParams()); + this.render(); + viewer.on('toggleClickInteraction', (detail) => { + onEnableInteraction({ detail }); + }); + map.getLayers().on('remove', onLayerDelete.bind(this)); + drawHandler.on('changeDraw', changeDrawState); + drawHandler.on('selectionChange', (detail) => { + if (deleteFeatureButton) { + const state = detail.features.getLength() > 0 ? 'initial' : 'disabled'; + deleteFeatureButton.setState(state); + } + if (showAttributeButton) { + const state = detail.features.getLength() > 0 ? 'initial' : 'disabled'; + layerAttributeButton.setState(state); + } + }); + drawHandler.on('changeButtonState', (detail) => { + const state = detail.state; + textButton.setState(state); + polygonButton.setState(state); + pointButton.setState(state); + lineButton.setState(state); + }); + this.on('toggleDraw', drawHandler.toggleDraw); + if (isActive) { + viewer.dispatch('toggleClickInteraction', { name: 'draw', active: true }); + } + }, + render() { + if (placement.indexOf('screen') > -1) { + let htmlString = `${screenButtonContainer.render()}`; + let el = dom.html(htmlString); + document.getElementById(mapTools).appendChild(el); + htmlString = screenButton.render(); + el = dom.html(htmlString); + document.getElementById(screenButtonContainer.getId()).appendChild(el); + } + if (placement.indexOf('menu') > -1) { + mapMenu.appendMenuItem(menuItem); + } + const targetElement = document.getElementById(viewer.getMain().getId()); + const htmlString = ` +
+
+ + ${drawToolbarElement.render()} +
+
+ `; + + targetElement.appendChild(dom.html(htmlString)); + this.dispatch('render'); + } + }); +}; + +export default Draw; diff --git a/src/controls/draw/drawhandler.js b/src/controls/draw/drawhandler.js new file mode 100644 index 000000000..4a1796a98 --- /dev/null +++ b/src/controls/draw/drawhandler.js @@ -0,0 +1,556 @@ +import Draw from 'ol/interaction/Draw'; +import Select from 'ol/interaction/Select'; +import Modify from 'ol/interaction/Modify'; +import DoubleClickZoom from 'ol/interaction/DoubleClickZoom'; +import GeoJSONFormat from 'ol/format/GeoJSON'; +import Feature from 'ol/Feature'; +import shapes from './shapes'; +import generateUUID from '../../utils/generateuuid'; +import { Component } from '../../ui'; + +const DrawHandler = function DrawHandler(options = {}) { + const { + stylewindow, + drawCmp, + viewer + } = options; + let map; + let drawSource; + let drawLayer; + let draw; + let activeTool; + let select; + let modify; + let annotationField; + let drawOptions; + let thisComponent; + + function disableDoubleClickZoom(evt) { + const featureType = evt.feature.getGeometry().getType(); + const interactionsToBeRemoved = []; + + if (featureType === 'Point') { + return; + } + + map.getInteractions().forEach((interaction) => { + if (interaction instanceof DoubleClickZoom) { + interactionsToBeRemoved.push(interaction); + } + }); + if (interactionsToBeRemoved.length > 0) { + map.removeInteraction(interactionsToBeRemoved[0]); + } + } + + function onDrawStart(evt) { + if (evt.feature.getGeometry().getType() !== 'Point') { + disableDoubleClickZoom(evt); + } + } + + function setActive(drawType) { + switch (drawType) { + case 'draw': + modify.setActive(true); + if (select) { + select.getFeatures().clear(); + select.setActive(false); + } + break; + default: + activeTool = undefined; + map.removeInteraction(draw); + modify.setActive(true); + if (select) { + select.getFeatures().clear(); + select.setActive(true); + } + break; + } + } + + function onTextEnd(feature, textVal) { + // Remove the feature if no text is set + if (textVal === '') { + drawLayer.getSource().removeFeature(feature); + } else { + feature.set(annotationField, textVal); + } + setActive(); + activeTool = undefined; + const details = { + feature, + layerName: drawLayer.get('name'), + action: 'insert', + tool: 'Text', + active: false + }; + thisComponent.dispatch('changeDraw', details); + } + + function addDoubleClickZoomInteraction() { + const allDoubleClickZoomInteractions = []; + map.getInteractions().forEach((interaction) => { + if (interaction instanceof DoubleClickZoom) { + allDoubleClickZoomInteractions.push(interaction); + } + }); + if (allDoubleClickZoomInteractions.length < 1) { + map.addInteraction(new DoubleClickZoom()); + } + } + + function enableDoubleClickZoom() { + setTimeout(() => { + addDoubleClickZoomInteraction(); + }, 100); + } + + function onDrawEnd(evt) { + const feature = evt.feature; + if (activeTool === 'Text') { + onTextEnd(feature, 'Text'); + stylewindow.dispatch('showStylewindow', true); + } else { + setActive(); + activeTool = undefined; + } + enableDoubleClickZoom(evt); + if (drawLayer) { + feature.setId(generateUUID()); + const styleObject = stylewindow.getStyleObject(feature); + feature.set('origostyle', styleObject); + } + const details = { + feature, + layerName: drawLayer.get('name'), + action: 'insert', + status: 'pending', + tool: feature.getGeometry().getType(), + active: false + }; + thisComponent.dispatch('changeDraw', details); + if (select) { + select.getFeatures().clear(); + select.getFeatures().push(feature); + } + } + + function setDraw(tool, drawType) { + let geometryType = tool; + drawSource = drawLayer.getSource(); + activeTool = tool; + + if (activeTool === 'Text') { + geometryType = 'Point'; + } + + const opt = { + source: drawSource, + type: geometryType + }; + + if (drawType) { + Object.assign(opt, shapes(drawType)); + } + + map.removeInteraction(draw); + draw = new Draw(opt); + map.addInteraction(draw); + const details = { + tool, + active: true + }; + thisComponent.dispatch('changeDraw', details); + + draw.on('drawend', onDrawEnd, this); + draw.on('drawstart', onDrawStart, this); + } + + function onDeleteSelected() { + const features = select.getFeatures(); + let source; + if (features.getLength()) { + source = drawLayer.getSource(); + features.forEach((feature) => { + const details = { + feature, + layerName: drawLayer.get('name'), + action: 'delete', + status: 'pending' + }; + thisComponent.dispatch('changeDraw', details); + source.removeFeature(feature); + }); + select.getFeatures().clear(); + } + } + + function onModifyEnd(evt) { + const feature = evt.features.item(0); + const details = { + feature, + layerName: drawLayer.get('name'), + action: 'update', + status: 'pending' + }; + thisComponent.dispatch('changeDraw', details); + } + + function cancelDraw(tool) { + setActive(); + activeTool = undefined; + const details = { + tool, + active: false + }; + thisComponent.dispatch('changeDraw', details); + } + + function isActive() { + if (modify === undefined || select === undefined) { + return false; + } + return true; + } + + function removeInteractions() { + if (isActive()) { + map.removeInteraction(modify); + map.removeInteraction(select); + map.removeInteraction(draw); + modify = undefined; + select = undefined; + draw = undefined; + } + } + + function toggleDraw(detail) { + if (detail.clearTool) { + activeTool = undefined; + } + if (detail.tool === 'delete') { + onDeleteSelected(); + } else if (detail.tool === 'cancel' && isActive()) { + cancelDraw(detail.tool); + removeInteractions(); + } else if (detail.tool === activeTool) { + cancelDraw(detail.tool); + } else if (detail.tool === 'Polygon' || detail.tool === 'LineString' || detail.tool === 'Point' || detail.tool === 'Text') { + if (activeTool) { + cancelDraw(activeTool); + } + setActive('draw'); + setDraw(detail.tool, detail.drawType); + } + } + + function getFeaturesByIds(type, layer, ids) { + const source = layer.getSource(); + const features = []; + if (type === 'delete') { + ids.forEach((id) => { + const dummy = new Feature(); + dummy.setId(id); + features.push(dummy); + }); + } else { + ids.forEach((id) => { + let feature; + if (source.getFeatureById(id)) { + feature = source.getFeatureById(id); + feature.unset('bbox'); + features.push(feature); + } + }); + } + + return features; + } + + const getSelection = () => select.getFeatures(); + + const getActiveLayer = () => drawLayer; + + function getDrawLayers() { + const drawLayersArray = viewer.getLayersByProperty('drawlayer', true); + return drawLayersArray; + } + + function onSelectChange() { + thisComponent.dispatch('selectionChange', { features: getSelection() }); + } + + function onSelectAdd(e) { + onSelectChange(e); + if (e.target) { + const feature = e.target.item(0); + const s = feature.get('origostyle') || {}; + s.selected = true; + feature.set('origostyle', s); + feature.changed(); + stylewindow.updateStylewindow(feature); + } + } + + function onSelectRemove(e) { + onSelectChange(e); + const feature = e.element; + const s = feature.get('origostyle') || {}; + s.selected = false; + feature.set('origostyle', s); + feature.changed(); + stylewindow.restoreStylewindow(); + } + + function removeDrawInteractions() { + if (select) { + select.getFeatures().clear(); + map.removeInteraction(select); + } + if (modify) { + map.removeInteraction(modify); + } + thisComponent.dispatch('changeButtonState', { state: 'disabled' }); + } + + function addDrawInteractions() { + removeDrawInteractions(); + select = new Select({ + layers: [drawLayer], + style: null, + hitTolerance: 5 + }); + modify = new Modify({ + features: select.getFeatures() + }); + map.addInteraction(select); + map.addInteraction(modify); + select.getFeatures().on('add', onSelectAdd, this); + select.getFeatures().on('remove', onSelectRemove, this); + select.getFeatures().on('change', onSelectChange, this); + modify.on('modifyend', onModifyEnd, this); + setActive(); + if (drawLayer.getVisible()) { + thisComponent.dispatch('changeButtonState', { state: 'initial' }); + } + } + + function onChangeVisible() { + if (drawCmp.isActive()) { + if (drawLayer.getVisible()) { + addDrawInteractions(); + } else { + removeDrawInteractions(); + } + } + } + + function setActiveLayer(layer) { + if (layer) { + if (drawLayer) { + drawLayer.un('change:visible', onChangeVisible); + } + drawLayer = layer; + drawLayer.on('change:visible', onChangeVisible); + onChangeVisible(); + } else { + drawLayer = null; + removeDrawInteractions(); + } + } + + function onUpdate(feature, layerName) { + const details = { + feature, + layerName, + action: 'update', + status: 'pending' + }; + thisComponent.dispatch('changeDraw', details); + } + + function addLayer(layerParams = {}) { + const layerOptions = Object.assign({}, drawOptions, layerParams); + const { + layerTitle, + groupName, + groupTitle, + draggable, + layerId = generateUUID(), + layer, + features, + source, + visible, + styleByAttribute, + queryable, + removable, + exportable, + drawlayer + } = layerOptions; + let newLayer; + if (layer) { // Should maybe be handled differently, where does the layer come from? + newLayer = layer; + map.addLayer(newLayer); + } else { + if (!viewer.getGroup(groupName) && groupName !== 'none' && groupName !== 'root') { + viewer.addGroup({ title: groupTitle, name: groupName, expanded: true, draggable }); + } + const newLayerOptions = { + group: groupName, + id: layerId, + name: layerId, + title: layerTitle, + zIndex: 7, + styleByAttribute, + visible, + queryable, + removable, + exportable, + drawlayer, + type: 'GEOJSON', + attributes: [ + { + title: '', + name: 'popuptext', + type: 'text' + } + ] + }; + if (source) { + newLayerOptions.source = source; + } + if (features) { + newLayerOptions.features = features; + } + newLayer = viewer.addLayer(newLayerOptions); + } + newLayer.getSource().forEachFeature((e) => { + e.on('change:origostyle', () => { + onUpdate(e, newLayer.get('name')); + }); + e.on('change:popuptext', () => { + onUpdate(e, newLayer.get('name')); + }); + }); + newLayer.getSource().on('addfeature', (e) => { + e.feature.on('change:origostyle', () => { + onUpdate(e, newLayer.get('name')); + }); + e.feature.on('change:popuptext', () => { + onUpdate(e, newLayer.get('name')); + }); + }); + return newLayer; + } + + async function onEnableInteraction(e) { + if (e.detail.name === 'draw' && e.detail.active) { + if (drawLayer === undefined) { + const addedLayer = await thisComponent.addLayer(); + setActiveLayer(addedLayer); + } + addDrawInteractions(); + } + } + + const getActiveTool = () => activeTool; + + function getState() { + if (select) { + select.getFeatures().clear(); + } + const drawLayers = getDrawLayers(); + const layerArr = []; + drawLayers.forEach(layer => { + const source = layer.getSource(); + const layerId = layer.get('name') || layer.get('id') || generateUUID(); + const layerTitle = layer.get('title') || drawOptions.layerTitle || 'Ritlager'; + const visible = layer.get('visible') || layer.getVisible(); + const features = source.getFeatures(); + const geojson = new GeoJSONFormat(); + layerArr.push({ id: layerId, title: layerTitle, visible, features: geojson.writeFeatures(features) }); + }); + if (layerArr.length > 0) { + return { + layers: layerArr + }; + } + return undefined; + } + + function restoreState(urlParams) { + if (urlParams && urlParams.controls && urlParams.controls.draw) { + const state = urlParams.controls.draw; + // TODO: Sanity/data check + let activeLayer; + if (state.layers && state.layers.length > 0) { + state.layers.forEach(layer => { + const layerId = layer.id || generateUUID(); + const layerTitle = layer.title || 'Ritlager'; + const visible = layer.visible; + const features = layer.features; + const newLayer = thisComponent.addLayer({ layerId, layerTitle, visible }); + const source = newLayer.getSource(); + source.addFeatures(features); + activeLayer = newLayer; + }); + } else if (state.features && state.features.length > 0) { + const features = state.features; + features.forEach(feature => { + const layerId = feature.get('layer'); + if (layerId) { + const layer = viewer.getLayer(layerId); + if (layer) { + const source = layer.getSource(); + source.addFeature(feature); + } else { + let layerTitle; + const newLayer = thisComponent.addLayer({ layerId, layerTitle }); + const source = newLayer.getSource(); + source.addFeature(feature); + activeLayer = newLayer; + } + } else { + if (drawLayer === undefined) { + drawLayer = thisComponent.addLayer(); + } + const source = drawLayer.getSource(); + source.addFeature(feature); + activeLayer = drawLayer; + } + }); + } + if (activeLayer) { + setActiveLayer(activeLayer); + } + } + } + return Component({ + name: 'drawHandler', + getDrawLayers, + getActiveLayer, + setActiveLayer, + addLayer, + getSelection, + getFeaturesByIds, + getState, + restoreState, + getActiveTool, + isActive, + toggleDraw, + onInit() { + thisComponent = this; + map = viewer.getMap(); + annotationField = 'annotation'; + drawOptions = drawCmp.getDrawOptions(); + activeTool = undefined; + viewer.on('toggleClickInteraction', (detail) => { + onEnableInteraction({ detail }); + }); + } + }); +}; + +export default DrawHandler; diff --git a/src/controls/draw/drawtools.js b/src/controls/draw/drawtools.js new file mode 100644 index 000000000..dfaeec900 --- /dev/null +++ b/src/controls/draw/drawtools.js @@ -0,0 +1,131 @@ +import dropDown from '../../dropdown'; +import utils from '../../utils'; + +const createElement = utils.createElement; + +let viewer; + +const drawToolsSelector = function drawToolsSelector(extraTools, v, toolCmps) { + const toolNames = { + Polygon: 'Polygon', + Point: 'Punkt', + LineString: 'Linje', + box: 'Rektangel', + freehand: 'Frihandsläge' + }; + viewer = v; + const drawCmp = viewer.getControlByName('draw'); + let drawTools; + const map = viewer.getMap(); + let active = false; + const activeCls = 'o-active'; + const target = 'draw-toolbar-dropdown'; + let activeTool; + + function selectionModel() { + const selectOptions = drawTools.map((drawTool) => { + const obj = {}; + obj.name = toolNames[drawTool]; + obj.value = drawTool; + return obj; + }); + return selectOptions; + } + + function createDropDownOptions(dropDownTarget) { + return { + target: dropDownTarget, + selectOptions: selectionModel(drawTools), + activeTool: drawTools[0] + }; + } + + function close() { + if (active) { + // eslint-disable-next-line no-use-before-define + setActive(drawTools[0], false); + } + } + + function changeDrawType(e) { + drawCmp.dispatch('toggleDraw', { tool: activeTool, drawType: e.detail.dataAttribute, clearTool: true }); + close(); + } + + function addDropDown(options) { + dropDown(options.target, options.selectOptions, { + dataAttribute: 'shape', + active: options.activeTool + }); + activeTool = options.activeTool; + document.getElementById(options.target).addEventListener('changeDropdown', changeDrawType); + } + + function setActive(tool, state) { + if (state) { + if (drawTools.length > 1) { + active = true; + + if (document.querySelector(`#${target}-${tool} > ul`)) { + document.querySelector(`#${target}-${tool} > ul`).remove(); + } + addDropDown(createDropDownOptions(`${target}-${tool}`)); + document.getElementById(`${target}-${tool}`).classList.add(activeCls); + map.once('click', close); + } + } else { + active = false; + if (tool && tool !== 'cancel' && document.querySelector(`[id^="${target}-${tool}"]`)) { + document.querySelector(`[id^="${target}-${tool}"]`).classList.remove(activeCls); + } + map.un('click', close); + } + } + + function render() { + // eslint-disable-next-line no-restricted-syntax + for (const tool in extraTools) { + if (Object.prototype.hasOwnProperty.call(extraTools, tool)) { + const popover = createElement('div', '', { + id: `${target}-${tool}`, + cls: 'o-popover' + }); + const { body: popoverHTML } = new DOMParser().parseFromString(popover, 'text/html'); + document.getElementById(toolCmps[tool].getId()).appendChild(popoverHTML.firstElementChild); + setActive(tool, false); + } + } + } + + function setDrawTools(tool) { + if (extraTools[tool]) { + drawTools = extraTools[tool] ? extraTools[tool].slice(0) : []; + drawTools.unshift(tool); + } else { + drawTools = [tool]; + } + } + + function onChangeEdit(e) { + const { tool, active: state } = e; + if (state === true) { + setDrawTools(tool); + setActive(tool, true); + } else if (state === false) { + setActive(tool, false); + } + } + + function addListener() { + drawCmp.getDrawHandler().on('changeDraw', onChangeEdit); + } + + function init() { + render(); + addListener(); + } + + init(); +}; + +export default drawToolsSelector; diff --git a/src/controls/draw/shapes.js b/src/controls/draw/shapes.js new file mode 100644 index 000000000..06bbac099 --- /dev/null +++ b/src/controls/draw/shapes.js @@ -0,0 +1,18 @@ +import { createBox } from 'ol/interaction/Draw'; + +export default (drawType) => { + const types = { + box: { + type: 'Circle', + geometryFunction: createBox() + }, + freehand: { + freehand: true + } + }; + + if (Object.prototype.hasOwnProperty.call(types, drawType)) { + return types[drawType]; + } + return {}; +}; diff --git a/src/controls/legend/overlay.js b/src/controls/legend/overlay.js index 37c6e3903..2c4b0e23f 100644 --- a/src/controls/legend/overlay.js +++ b/src/controls/legend/overlay.js @@ -199,7 +199,7 @@ const OverlayLayer = function OverlayLayer(options) { const features = layer.getSource().getFeatures(); exportToFile(features, format, { featureProjection: viewer.getProjection().getCode(), - filename: title || 'export' + filename: layer.get('title') || 'export' }); e.preventDefault(); }); @@ -323,6 +323,11 @@ const OverlayLayer = function OverlayLayer(options) { layerIcon.dispatch('change', { icon: newIcon }); }; + const onLayerTitleChange = function onLayerTitleChange(newTitle) { + const labelEl = document.getElementById(label.getId()); + labelEl.innerHTML = newTitle; + }; + return Component({ name, getLayer, @@ -364,6 +369,9 @@ const OverlayLayer = function OverlayLayer(options) { layer.on('change:style', () => { onLayerStyleChange(); }); + layer.on('change:title', (e) => { + onLayerTitleChange(e.target.get('title')); + }); }, render() { let extendedLegendHtml = ''; diff --git a/src/controls/measure.js b/src/controls/measure.js index 0f9054acd..4efeb4f32 100644 --- a/src/controls/measure.js +++ b/src/controls/measure.js @@ -1,22 +1,14 @@ -import { getArea, getLength } from 'ol/sphere'; -import VectorSource from 'ol/source/Vector'; -import VectorLayer from 'ol/layer/Vector'; -import DrawInteraction from 'ol/interaction/Draw'; -import Overlay from 'ol/Overlay'; import Feature from 'ol/Feature'; -import Polygon from 'ol/geom/Polygon'; -import Circle from 'ol/geom/Circle'; -import LineString from 'ol/geom/LineString'; -import Point from 'ol/geom/Point'; +import { Polygon, LineString, Point, Circle } from 'ol/geom'; +import { Draw, Modify, Snap } from 'ol/interaction'; +import { Vector as VectorLayer } from 'ol/layer'; import Projection from 'ol/proj/Projection'; -import * as Extent from 'ol/extent'; -import { Snap } from 'ol/interaction'; +import { Vector as VectorSource } from 'ol/source'; import { Collection } from 'ol'; import LayerGroup from 'ol/layer/Group'; import { unByKey } from 'ol/Observable'; import { Component, Icon, Element as El, Button, dom, Modal } from '../ui'; -import Style from '../style'; -import StyleTypes from '../style/styletypes'; +import * as drawStyles from '../style/drawstyles'; import replacer from '../utils/replacer'; const Measure = function Measure({ @@ -33,26 +25,14 @@ const Measure = function Measure({ snapLayers, snapRadius = 15 } = {}) { - const style = Style; - const styleTypes = StyleTypes(); - let map; let activeButton; let defaultButton; let measure; let type; let sketch; - let prevSketchLength = 0; - let measureTooltip; - let measureTooltipElement; - let measureStyleOptions; - let helpTooltip; - let helpTooltipElement; let markerIcon; let markerElement; - let vector; - let source; - let label; let lengthTool; let areaTool; let elevationTool; @@ -62,7 +42,6 @@ const Measure = function Measure({ let isActive = false; let tempOverlayArray = []; const overlayArray = []; - let viewer; let measureElement; let measureButton; @@ -83,96 +62,83 @@ const Measure = function Measure({ let snapCollection; let snapEventListenerKeys; let snapActive = snapIsActive; - - function createStyle(feature) { - const featureType = feature.getGeometry().getType(); - const measureStyle = featureType === 'LineString' ? style.createStyleRule(measureStyleOptions.linestring) : style.createStyleRule(measureStyleOptions.polygon); - - return measureStyle; - } - function setActive(state) { - isActive = state; - } - - function createHelpTooltip() { - if (helpTooltipElement) { - helpTooltipElement.parentNode.removeChild(helpTooltipElement); - } - - helpTooltipElement = document.createElement('div'); - helpTooltipElement.className = 'o-tooltip o-tooltip-measure'; - - helpTooltip = new Overlay({ - element: helpTooltipElement, - offset: [15, 0], - positioning: 'center-left' - }); - - tempOverlayArray.push(helpTooltip); - map.addOverlay(helpTooltip); - } - - function createMeasureTooltip() { - if (measureTooltipElement) { - measureTooltipElement.parentNode.removeChild(measureTooltipElement); + let tipPoint; + let projection; + + const tipStyle = drawStyles.tipStyle; + const modifyStyle = drawStyles.modifyStyle; + const measureStyle = drawStyles.measureStyle; + const source = new VectorSource(); + const modify = new Modify({ source, style: modifyStyle }); + + function styleFunction(feature, segments, drawType, tip) { + const styleScale = feature.get('styleScale') || 1; + const labelStyle = drawStyles.getLabelStyle(styleScale); + let styles = [measureStyle(styleScale)]; + const geometry = feature.getGeometry(); + const geomType = geometry.getType(); + let point; let line; let label; + if (!drawType || drawType === geomType) { + if (geomType === 'Polygon') { + point = geometry.getInteriorPoint(); + label = drawStyles.formatArea(geometry, useHectare, projection); + line = new LineString(geometry.getCoordinates()[0]); + } else if (geomType === 'LineString') { + point = new Point(geometry.getLastCoordinate()); + label = drawStyles.formatLength(geometry, projection); + line = geometry; + } + } + if (segments && line) { + const segmentLabelStyle = drawStyles.getSegmentLabelStyle(line, projection); + styles = styles.concat(segmentLabelStyle); + } + if (label) { + labelStyle.setGeometry(point); + labelStyle.getText().setText(label); + styles.push(labelStyle); + } + if ( + tip + && geomType === 'Point' + && !modify.getOverlay().getSource().getFeatures().length + ) { + tipPoint = geometry; + tipStyle.getText().setText(tip); + styles.push(tipStyle); + } + return styles; + } + + const vector = new VectorLayer({ + group: 'none', + name: 'measure', + title: 'Measure', + source, + zIndex: 8, + styleName: 'origoStylefunction', + style(feature) { + return styleFunction(feature, showSegmentLabels); } + }); - measureTooltipElement = document.createElement('div'); - measureTooltipElement.className = 'o-tooltip o-tooltip-measure'; - - measureTooltip = new Overlay({ - element: measureTooltipElement, - offset: [0, -15], - positioning: 'bottom-center', - stopEvent: false - }); - - tempOverlayArray.push(measureTooltip); - map.addOverlay(measureTooltip); - } - - function formatLength(line) { - const projection = map.getView().getProjection(); - const length = getLength(line, { - projection - }); - let output; - - if (length > 1000) { - output = `${Math.round((length / 1000) * 100) / 100} km`; - } else { - output = `${Math.round(length * 100) / 100} m`; + function centerSketch() { + if (sketch) { + const geom = (sketch.getGeometry()); + if (geom instanceof Polygon) { + const sketchCoord = geom.getCoordinates()[0]; + sketchCoord.splice(-2, 1, map.getView().getCenter()); + sketch.getGeometry().setCoordinates([sketchCoord]); + } else if (geom instanceof LineString) { + const sketchCoord = geom.getCoordinates(); + sketchCoord.splice(-1, 1, map.getView().getCenter()); + sketch.getGeometry().setCoordinates(sketchCoord); + } } - - return output; } - function formatArea(polygon) { - const projection = map.getView().getProjection(); - const area = getArea(polygon, { - projection - }); - let output; - - if (area > 10000000) { - output = `${Math.round((area / 1000000) * 100) / 100} km2`; - } else if (area > 10000 && useHectare) { - output = `${Math.round((area / 10000) * 100) / 100} ha`; - } else { - output = `${Math.round(area * 100) / 100} m2`; - } - - const htmlElem = document.createElement('span'); - htmlElem.innerHTML = output; - - [].forEach.call(htmlElem.children, (element) => { - const el = element; - if (el.tagName === 'SUP') { - el.innerHTML = String.fromCharCode(el.innerHTML.charCodeAt(0) + 128); - } - }); - - return htmlElem.textContent; + function setActive(state) { + isActive = state; } function getElevationAttribute(path, obj = {}) { @@ -187,10 +153,6 @@ const Measure = function Measure({ start: '{', end: '}' }; - if (feature.getStyle() === null) { - feature.setStyle(style.createStyleRule(measureStyleOptions.interaction)); - source.addFeature(feature); - } if (elevationTargetProjection && elevationTargetProjection !== viewer.getProjection().getCode()) { const clone = feature.getGeometry().clone(); @@ -224,51 +186,28 @@ const Measure = function Measure({ easting, northing }, options); - - feature.setStyle(createStyle(feature)); - feature.getStyle()[0].getText().setText('Hämtar höjd...'); + const styleScale = feature.get('styleScale') || 1; + const featureStyle = drawStyles.getLabelStyle(styleScale); + feature.setStyle(featureStyle); + feature.getStyle().getText().setText('Hämtar höjd...'); fetch(url).then(response => response.json({ cache: false })).then((data) => { const elevation = getElevationAttribute(elevationAttribute, data); - feature.getStyle()[0].getText().setText(`${elevation.toFixed(1)} m`); + feature.getStyle().getText().setText(`${elevation.toFixed(1)} m`); source.changed(); }); } function addBuffer(feature, radius = 0) { - if (feature.getStyle() === null) { - feature.setStyle(style.createStyleRule(measureStyleOptions.interaction)); - source.addFeature(feature); - } - // Mark the central point of the circle - feature.getStyle()[0].getText().setText('o'); - if (radius !== 0) { + if (radius > 0) { bufferSize = radius; } - function addBufferToFeature() { - const pointCenter = feature.getGeometry().getCoordinates(); - // Create a buffer around the point which was clicked on. - const bufferCircle = new Circle(pointCenter, bufferSize); - const bufferedFeature = new Feature(bufferCircle); - // Create a new point at top of the circle to add a text with radius information - const radiusText = new Point([pointCenter[0], bufferCircle.getExtent()[3]]); - const radiusFeature = new Feature(radiusText); - const featStyle = createStyle(feature); - radiusFeature.setStyle(featStyle); - // Remove stroke and fill only to leave the text styling from default measure style - radiusFeature.getStyle()[0].setStroke(null); - radiusFeature.getStyle()[0].setFill(null); - // Offset the text so it dont't cover the circle - radiusFeature.getStyle()[0].getText().setOffsetY(-10); - radiusFeature.getStyle()[0].getText().setPlacement('line'); - radiusFeature.getStyle()[0].getText().setText(`${bufferSize} m`); - vector.getSource().addFeature(bufferedFeature); - vector.getSource().addFeature(radiusFeature); - } - - addBufferToFeature(); + const pointCenter = feature.getGeometry().getCoordinates(); + const bufferCircle = new Circle(pointCenter, bufferSize); + feature.setGeometry(bufferCircle); + feature.setStyle((feat) => drawStyles.bufferStyleFunction(feat)); } function clearSnapInteractions() { @@ -278,199 +217,6 @@ const Measure = function Measure({ snapEventListenerKeys.clear(); } - function placeMeasurementLabel(segment, coords) { - const aa = segment.getExtent(); - const oo = Extent.getCenter(aa); - measureElement = document.createElement('div'); - measureElement.className = 'o-tooltip o-tooltip-measure'; - measureElement.id = `measure_${coords.length}`; - const labelOverlay = new Overlay({ - element: measureElement, - positioning: 'center-center', - stopEvent: true - }); - tempOverlayArray.push(labelOverlay); - labelOverlay.setPosition(oo); - measureElement.innerHTML = formatLength(/** @type {LineString} */(segment)); - map.addOverlay(labelOverlay); - if (coords.length < 6 && showSegmentLengths) { - switch (type) { - case 'LineString': - if (coords.length === 3) { - document.getElementById('measure_3').style.display = 'none'; - if (showSegmentLabels) { - document.getElementById('measure_3').style.display = 'block'; - } - } - break; - case 'Polygon': - if (coords.length === 4) { - document.getElementById('measure_4').style.display = 'none'; - if (showSegmentLabels) { - document.getElementById('measure_4').style.display = 'block'; - } - } - break; - case 'Point': - if (showSegmentLabels) { - document.getElementById('measure_2').style.display = 'block'; - } else { - document.getElementById('measure_2').style.display = 'none'; - } - break; - default: - break; - } - } - if (!showSegmentLabels) { - measureElement.style.display = 'none'; - } - } - - // Takes a Polygon as input and adds area measurements on it - function addArea(area) { - const tempFeature = new Feature(area); - const areaLabel = formatArea(area); - tempFeature.setStyle(style.createStyleRule(measureStyleOptions.polygon)); - source.addFeature(tempFeature); - const flatCoords = area.getCoordinates(); - for (let i = 0; i < flatCoords[0].length; i += 1) { - if (i < flatCoords[0].length - 1) { - const tempSegment = new LineString([flatCoords[0][i], flatCoords[0][i + 1]]); - placeMeasurementLabel(tempSegment, flatCoords[0][i]); - } - } - const totalLength = formatLength(new LineString(flatCoords[0])); - tempFeature.getStyle()[0].getText().setText(`${areaLabel}\n${totalLength}`); - } - - // Takes a LineString as input and adds length measurements on it - function addLength(line) { - const tempFeature = new Feature(line); - const totalLength = formatLength(line); - tempFeature.setStyle(style.createStyleRule(measureStyleOptions.linestring)); - source.addFeature(tempFeature); - const flatCoords = line.getCoordinates(); - for (let i = 0; i < flatCoords.length; i += 1) { - if (i < flatCoords.length - 1) { - const tempSegment = new LineString([flatCoords[i], flatCoords[i + 1]]); - placeMeasurementLabel(tempSegment, flatCoords[i]); - } - } - tempFeature.getStyle()[0].getText().setText(totalLength); - } - - function centerSketch() { - if (sketch) { - const geom = (sketch.getGeometry()); - if (geom instanceof Polygon) { - const sketchCoord = geom.getCoordinates()[0]; - sketchCoord.splice(-2, 1, map.getView().getCenter()); - sketch.getGeometry().setCoordinates([sketchCoord]); - } else if (geom instanceof LineString) { - const sketchCoord = geom.getCoordinates(); - sketchCoord.splice(-1, 1, map.getView().getCenter()); - sketch.getGeometry().setCoordinates(sketchCoord); - } - } - } - - // Display and move tooltips with pointer - function pointerMoveHandler(evt) { - const helpMsg = 'Klicka för att börja mäta'; - let tooltipCoord = evt.coordinate; - - if (sketch) { - const geom = (sketch.getGeometry()); - let output = ''; - let coords; - let area; - let newNode; - label = ''; - - if (geom instanceof Polygon) { - area = formatArea(/** @type {Polygon} */(geom)); - tooltipCoord = geom.getInteriorPoint().getCoordinates(); - coords = geom.getCoordinates()[0]; - newNode = coords.length > prevSketchLength && coords.length !== 3; - prevSketchLength = coords.length; - } else if (geom instanceof LineString) { - tooltipCoord = geom.getLastCoordinate(); - coords = geom.getCoordinates(); - newNode = coords.length > prevSketchLength; - prevSketchLength = coords.length; - } - - let totalLength = 0; - if (!(geom instanceof Point)) { - totalLength = formatLength(/** @type {LineString} */(geom)); - } - if (showSegmentLengths && !(geom instanceof Point)) { - let lengthLastSegment = 0; // totalLength; - let lastSegment; - if (coords.length >= 1) { - if (geom instanceof Polygon && coords.length > 2) { - if (evt.type !== 'drawend') { - // If this is a polygon in the progress of being drawn OL creates a extra vertices back to start that we need to ignore - lastSegment = new LineString([coords[coords.length - 2], coords[coords.length - 3]]); - const polygonAsLineString = /** @type {LineString} */ (geom); - const lineStringWithoutLastSegment = new LineString(polygonAsLineString.getCoordinates()[0].slice(0, -1)); - totalLength = formatLength(lineStringWithoutLastSegment); - } else { - // Finish the polygon and put a label on the last verticies as well - lastSegment = new LineString([coords[coords.length - 1], coords[coords.length - 2]]); - placeMeasurementLabel(lastSegment, coords); - } - } else { // Draw segment while drawing is in progress - lastSegment = new LineString([coords[coords.length - 1], coords[coords.length - 2]]); - } - // Create a label for the last drawn vertices and place it in the middle of it. - lengthLastSegment = formatLength(/** @type {LineString} */(lastSegment)); - if ((newNode && evt.type !== 'drawend') && coords.length > 2) { - let secondToLastSegment; - if (geom instanceof Polygon && coords.length > 3) { - secondToLastSegment = new LineString([coords[coords.length - 3], coords[coords.length - 4]]); - } else { - secondToLastSegment = new LineString([coords[coords.length - 2], coords[coords.length - 3]]); - } - if (secondToLastSegment) { - placeMeasurementLabel(secondToLastSegment, coords); - } - } - } - if (area) { - output = `${area}
`; - label = `${area}\n`; - } - output += `${lengthLastSegment} (Totalt: ${totalLength})`; - label += totalLength; - } else if (area) { - output = area; - label = area; - } else { - output = totalLength; - label += totalLength; - } - - measureTooltipElement.innerHTML = output; - measureTooltip.setPosition(tooltipCoord); - } - - if (evt.type === 'pointermove') { - helpTooltipElement.innerHTML = helpMsg; - helpTooltip.setPosition(evt.coordinate); - } - } - - function resetSketch() { - // unset sketch - sketch = null; - // unset tooltip so that a new one can be created - measureTooltipElement = null; - helpTooltipElement = null; - viewer.removeOverlays(tempOverlayArray); - } - function renderMarker() { markerIcon = Icon({ icon: '#o_centerposition_24px', @@ -493,6 +239,9 @@ const Measure = function Measure({ target: viewer.getId(), style: 'width: auto;' }); + modal.on('closed', () => { + source.removeFeature(feature); + }); const bufferradiusEl = document.getElementById('bufferradius'); bufferradiusEl.focus(); const bufferradiusBtn = document.getElementById('bufferradiusBtn'); @@ -514,92 +263,6 @@ const Measure = function Measure({ }); } - function disableInteraction() { - if (activeButton) { - document.getElementById(activeButton.getId()).classList.remove('active'); - } - document.getElementById(measureButton.getId()).classList.remove('active'); - if (lengthTool) { - document.getElementById(lengthToolButton.getId()).classList.add('hidden'); - } - if (areaTool) { - document.getElementById(areaToolButton.getId()).classList.add('hidden'); - } - if (lengthTool || areaTool) { - document.getElementById(undoButton.getId()).classList.add('hidden'); - } - if (elevationTool) { - document.getElementById(elevationToolButton.getId()).classList.add('hidden'); - } - if (bufferTool) { - document.getElementById(bufferToolButton.getId()).classList.add('hidden'); - } - if (snap) { - document.getElementById(toggleSnapButton.getId()).classList.add('hidden'); - } - document.getElementById(measureButton.getId()).classList.add('tooltip'); - document.getElementById(clearButton.getId()).classList.add('hidden'); - if (showSegmentLengths) { - document.getElementById(showSegmentLabelButton.getId()).classList.add('hidden'); - } - if (touchMode && isActive) { - document.getElementById(addNodeButton.getId()).classList.add('hidden'); - const markerIconElement = document.getElementById(`${markerIcon.getId()}`); - markerIconElement.parentNode.removeChild(markerIconElement); - } - setActive(false); - map.un('pointermove', pointerMoveHandler); - map.removeInteraction(measure); - if (snap) { - clearSnapInteractions(); - } - if (typeof helpTooltipElement !== 'undefined' && helpTooltipElement !== null) { - if (helpTooltipElement.parentNode !== null) { - helpTooltipElement.outerHTML = ''; - } - } - if (typeof measureTooltipElement !== 'undefined' && measureTooltipElement !== null) { - if (measureTooltipElement.parentNode !== null) { - measureTooltipElement.outerHTML = ''; - } - } - setActive(false); - resetSketch(); - } - - function enableInteraction() { - document.getElementById(measureButton.getId()).classList.add('active'); - if (lengthTool) { - document.getElementById(lengthToolButton.getId()).classList.remove('hidden'); - } - if (areaTool) { - document.getElementById(areaToolButton.getId()).classList.remove('hidden'); - } - if (elevationTool) { - document.getElementById(elevationToolButton.getId()).classList.remove('hidden'); - } - if (bufferTool) { - document.getElementById(bufferToolButton.getId()).classList.remove('hidden'); - } - if (snap) { - document.getElementById(toggleSnapButton.getId()).classList.remove('hidden'); - } - document.getElementById(measureButton.getId()).classList.remove('tooltip'); - document.getElementById(clearButton.getId()).classList.remove('hidden'); - document.getElementById(defaultButton.getId()).click(); - if (touchMode) { - document.getElementById(addNodeButton.getId()).classList.remove('hidden'); - renderMarker(); - } - if (showSegmentLengths) { - document.getElementById(showSegmentLabelButton.getId()).classList.remove('hidden'); - if (showSegmentLabelButtonState) { - document.getElementById(showSegmentLabelButton.getId()).classList.add('active'); - } - } - setActive(true); - } - function createSnapInteractionForVectorLayer(layer) { const state = layer.getLayerState(); // Using ol_uid because the Origo layer id is unreliable @@ -672,80 +335,138 @@ const Measure = function Measure({ } function addInteraction() { - measure = new DrawInteraction({ + const drawType = type || 'LineString'; + const activeTip = ''; + const idleTip = 'Klicka för att börja mäta'; + let tip = idleTip; + measure = new Draw({ source, - type, - style: style.createStyleRule(measureStyleOptions.interaction), - condition(evt) { - return evt.originalEvent.pointerType !== 'touch'; + type: drawType, + style(feature) { + return styleFunction(feature, showSegmentLabels, drawType, tip); } }); - - map.addInteraction(measure); - if (snap) { - addSnapInteractions(); - } - createMeasureTooltip(); - createHelpTooltip(); - if (!touchMode) { - map.on('pointermove', pointerMoveHandler); - } - - measure.on('drawstart', (evt) => { - measure.getOverlay().getSource().getFeatures()[1].setStyle([]); - sketch = evt.feature; - sketch.on('change', pointerMoveHandler); + measure.on('drawstart', (e) => { + sketch = e.feature; + modify.setActive(false); + tip = activeTip; if (touchMode) { map.getView().on('change:center', centerSketch); - } else { - pointerMoveHandler(evt); } - document.getElementsByClassName('o-tooltip-measure')[1].remove(); - - if (type === 'LineString' || type === 'Polygon') { + if (drawType === 'LineString' || drawType === 'Polygon') { document.getElementById(undoButton.getId()).classList.remove('hidden'); } - }, this); - + }); measure.on('drawend', (evt) => { const feature = evt.feature; - sketch.un('change', pointerMoveHandler); + modifyStyle.setGeometry(tipPoint); + modify.setActive(true); + map.once('pointermove', () => { + modifyStyle.setGeometry(); + }); + if (touchMode) { map.getView().un('change:center', centerSketch); } - pointerMoveHandler(evt); - feature.setStyle(createStyle(feature)); - feature.getStyle()[0].getText().setText(label); - document.getElementsByClassName('o-tooltip-measure')[0].remove(); - overlayArray.push(...tempOverlayArray); - tempOverlayArray = []; - resetSketch(); - createMeasureTooltip(); - createHelpTooltip(); - + tip = idleTip; document.getElementById(undoButton.getId()).classList.add('hidden'); - if (feature.getGeometry().getType() === 'Point') { - if (bufferTool) { - if (document.getElementById(bufferToolButton.getId()).classList.contains('active')) { - feature.getStyle()[0].getText().setText(''); - createRadiusModal(evt.feature); - } else { - feature.getStyle()[0].getText().setText(label); - getElevation(evt.feature); - } - } else { - feature.getStyle()[0].getText().setText(label); - getElevation(evt.feature); - } + if (activeButton.data.tool === 'buffer') { + feature.set('tool', 'buffer'); + createRadiusModal(feature); + } else if (activeButton.data.tool === 'elevation') { + feature.set('tool', 'elevation'); + getElevation(feature); } - }, this); + }); + + modify.on('modifyend', (evt) => { + evt.features.getArray().forEach(feat => { + if (feat.get('tool') === 'elevation') { + getElevation(feat); + } + }); + }); + + modify.setActive(true); + map.addInteraction(measure); + map.addInteraction(modify); + if (snap) { + addSnapInteractions(); + } } - function abort() { - measure.abortDrawing(); - resetSketch(); - createMeasureTooltip(); - createHelpTooltip(); + function disableInteraction() { + if (activeButton) { + document.getElementById(activeButton.getId()).classList.remove('active'); + } + document.getElementById(measureButton.getId()).classList.remove('active'); + if (lengthTool) { + document.getElementById(lengthToolButton.getId()).classList.add('hidden'); + } + if (areaTool) { + document.getElementById(areaToolButton.getId()).classList.add('hidden'); + } + if (lengthTool || areaTool) { + document.getElementById(undoButton.getId()).classList.add('hidden'); + } + if (elevationTool) { + document.getElementById(elevationToolButton.getId()).classList.add('hidden'); + } + if (bufferTool) { + document.getElementById(bufferToolButton.getId()).classList.add('hidden'); + } + if (snap) { + document.getElementById(toggleSnapButton.getId()).classList.add('hidden'); + } + document.getElementById(measureButton.getId()).classList.add('tooltip'); + document.getElementById(clearButton.getId()).classList.add('hidden'); + if (showSegmentLengths) { + document.getElementById(showSegmentLabelButton.getId()).classList.add('hidden'); + } + if (touchMode && isActive) { + document.getElementById(addNodeButton.getId()).classList.add('hidden'); + const markerIconElement = document.getElementById(`${markerIcon.getId()}`); + markerIconElement.parentNode.removeChild(markerIconElement); + } + setActive(false); + map.removeInteraction(measure); + map.removeInteraction(modify); + if (snap) { + clearSnapInteractions(); + } + } + + function enableInteraction() { + document.getElementById(measureButton.getId()).classList.add('active'); + if (lengthTool) { + document.getElementById(lengthToolButton.getId()).classList.remove('hidden'); + } + if (areaTool) { + document.getElementById(areaToolButton.getId()).classList.remove('hidden'); + } + if (elevationTool) { + document.getElementById(elevationToolButton.getId()).classList.remove('hidden'); + } + if (bufferTool) { + document.getElementById(bufferToolButton.getId()).classList.remove('hidden'); + } + if (snap) { + document.getElementById(toggleSnapButton.getId()).classList.remove('hidden'); + } + document.getElementById(measureButton.getId()).classList.remove('tooltip'); + document.getElementById(clearButton.getId()).classList.remove('hidden'); + document.getElementById(defaultButton.getId()).click(); + if (touchMode) { + document.getElementById(addNodeButton.getId()).classList.remove('hidden'); + renderMarker(); + } + if (showSegmentLengths) { + document.getElementById(showSegmentLabelButton.getId()).classList.remove('hidden'); + if (showSegmentLabelButtonState) { + document.getElementById(showSegmentLabelButton.getId()).classList.add('active'); + } + } + setActive(true); } function toggleMeasure() { @@ -764,7 +485,7 @@ const Measure = function Measure({ document.getElementById(undoButton.getId()).classList.add('hidden'); activeButton = button; map.removeInteraction(measure); - resetSketch(); + map.removeInteraction(modify); addInteraction(); } @@ -804,17 +525,11 @@ const Measure = function Measure({ } function undoLastPoint() { - if ((type === 'LineString' && sketch.getGeometry().getCoordinates().length === 2) || (type === 'Polygon' && sketch.getGeometry().getCoordinates()[0].length <= 3)) { - document.getElementsByClassName('o-tooltip-measure')[0].remove(); - document.getElementById(undoButton.getId()).classList.add('hidden'); - abort(); - } else { - if (showSegmentLengths) document.getElementsByClassName('o-tooltip-measure')[1].remove(); - measure.removeLastPoint(); - if (touchMode) { - centerSketch(); - } + measure.removeLastPoint(); + if (touchMode) { + centerSketch(); } + // TODO: Remove undo button when feature has no geometry } function toggleSnap() { @@ -845,11 +560,13 @@ const Measure = function Measure({ area.push(feature.getGeometry().getCoordinates()); break; case 'Point': - if (feature.getStyle()[0].getText().getText() === 'o') { - buffer.push(feature.getGeometry().getCoordinates()); - } else if (feature.getStyle()[0].getText().getPlacement() === 'line') { - bufferRadius.push(feature.getStyle()[0].getText().getText()); - } else { + case 'Circle': + if (feature.get('tool') === 'buffer') { + const radius = feature.getGeometry().getRadius(); + const center = feature.getGeometry().getCenter(); + bufferRadius.push(radius); + buffer.push(center); + } else if (feature.get('tool') === 'elevation') { elevation.push(feature.getGeometry().getCoordinates()); } break; @@ -886,13 +603,16 @@ const Measure = function Measure({ function restoreState(params) { if (params && params.controls && params.controls.measure) { if (params.controls.measure.measureState.isActive) { - enableInteraction(); + isActive = false; + toggleMeasure(); } // Restore areas if (params.controls.measure.measureState && params.controls.measure.measureState.area && params.controls.measure.measureState.area.length > 0) { if (Array.isArray(params.controls.measure.measureState.area)) { params.controls.measure.measureState.area.forEach((item) => { - addArea(new Polygon(item)); + source.addFeature(new Feature({ + geometry: new Polygon(item) + })); }); } } @@ -900,7 +620,9 @@ const Measure = function Measure({ if (params.controls.measure.measureState && params.controls.measure.measureState.length && params.controls.measure.measureState.length.length > 0) { if (Array.isArray(params.controls.measure.measureState.length)) { params.controls.measure.measureState.length.forEach((item) => { - addLength(new LineString(item)); + source.addFeature(new Feature({ + geometry: new LineString(item) + })); }); } } @@ -908,9 +630,12 @@ const Measure = function Measure({ if (params.controls.measure.measureState && params.controls.measure.measureState.buffer && params.controls.measure.measureState.buffer.length > 0) { if (Array.isArray(params.controls.measure.measureState.buffer)) { for (let i = 0; i < params.controls.measure.measureState.buffer.length; i += 1) { - let radius = params.controls.measure.measureState.bufferRadius[i]; - radius = radius.replace(' m', ''); - addBuffer(new Feature(new Point(params.controls.measure.measureState.buffer[i]), Number(radius)), Number(radius)); + const radius = params.controls.measure.measureState.bufferRadius[i]; + const point = params.controls.measure.measureState.buffer[i]; + const feature = new Feature(new Point(point)); + feature.set('tool', 'buffer'); + source.addFeature(feature); + addBuffer(feature, radius); } } } @@ -918,7 +643,10 @@ const Measure = function Measure({ if (params.controls.measure.measureState && params.controls.measure.measureState.elevation && params.controls.measure.measureState.elevation.length > 0) { if (Array.isArray(params.controls.measure.measureState.elevation)) { for (let i = 0; i < params.controls.measure.measureState.elevation.length; i += 1) { - getElevation(new Feature(new Point(params.controls.measure.measureState.elevation[i]))); + const feature = new Feature(new Point(params.controls.measure.measureState.elevation[i])); + feature.set('tool', 'elevation'); + source.addFeature(feature); + getElevation(feature); } } } @@ -947,6 +675,7 @@ const Measure = function Measure({ }, onAdd(evt) { viewer = evt.target; + projection = viewer.getProjection().getCode(); touchMode = 'ontouchstart' in document.documentElement; if (touchMode) { addNodeButton = Button({ @@ -970,6 +699,8 @@ const Measure = function Measure({ cls: 'o-measure-segment-label padding-small margin-bottom-smaller icon-smaller round light box-shadow hidden', click() { toggleSegmentLabels(); + vector.changed(); + measure.getOverlay().changed(); }, icon: '#ic_linear_scale_24px', tooltipText: 'Visa delsträckor', @@ -978,24 +709,10 @@ const Measure = function Measure({ buttons.push(showSegmentLabelButton); } target = `${viewer.getMain().getMapTools().getId()}`; - map = viewer.getMap(); - source = new VectorSource(); - measureStyleOptions = styleTypes.getStyle('measure'); - - // Drawn features - vector = new VectorLayer({ - group: 'none', - source, - name: 'measure', - visible: true, - zIndex: 6 - }); - map.addLayer(vector); this.addComponents(buttons); this.render(); - restoreState(viewer.getUrlParams()); viewer.on('toggleClickInteraction', (detail) => { if (detail.name === 'measure' && detail.active) { enableInteraction(); @@ -1003,6 +720,7 @@ const Measure = function Measure({ disableInteraction(); } }); + restoreState(viewer.getUrlParams()); }, onInit() { lengthTool = measureTools.indexOf('length') >= 0; @@ -1038,6 +756,7 @@ const Measure = function Measure({ type = 'LineString'; toggleType(this); }, + data: { tool: 'length' }, icon: '#ic_timeline_24px', tooltipText: 'Längd', tooltipPlacement: 'east' @@ -1053,6 +772,7 @@ const Measure = function Measure({ type = 'Polygon'; toggleType(this); }, + data: { tool: 'area' }, icon: '#o_polygon_24px', tooltipText: 'Yta', tooltipPlacement: 'east' @@ -1067,6 +787,7 @@ const Measure = function Measure({ type = 'Point'; toggleType(this); }, + data: { tool: 'elevation' }, icon: '#ic_height_24px', tooltipText: 'Höjd', tooltipPlacement: 'east' @@ -1081,6 +802,7 @@ const Measure = function Measure({ type = 'Point'; toggleType(this); }, + data: { tool: 'buffer' }, icon: '#ic_adjust_24px', tooltipText: 'Buffer', tooltipPlacement: 'east' @@ -1115,7 +837,7 @@ const Measure = function Measure({ clearButton = Button({ cls: 'o-measure-clear padding-small margin-bottom-smaller icon-smaller round light box-shadow hidden', click() { - abort(); + measure.abortDrawing(); vector.getSource().clear(); viewer.removeOverlays(overlayArray); }, diff --git a/src/controls/print/print-component.js b/src/controls/print/print-component.js index 3136a8444..2a732e76d 100644 --- a/src/controls/print/print-component.js +++ b/src/controls/print/print-component.js @@ -669,6 +669,10 @@ const PrintComponent = function PrintComponent(options = {}) { if (draganddropControl) draganddropControl.addInteraction(); }, render() { + viewer.dispatch('toggleClickInteraction', { + name: 'featureinfo', + active: true + }); if (deviceOnIos) { // If user is on iOS we have to make sure the canvas ain't too heavy and make the browser crash // eslint-disable-next-line no-underscore-dangle diff --git a/src/controls/print/print-legend.js b/src/controls/print/print-legend.js index 8508f743b..9e89f098b 100644 --- a/src/controls/print/print-legend.js +++ b/src/controls/print/print-legend.js @@ -266,7 +266,9 @@ const LayerRows = function LayerRows(options) { const overlayEls = []; overlays.forEach((layer) => { - overlayEls.push(LayerRow({ layer, viewer })); + if (!layer.get('drawlayer')) { + overlayEls.push(LayerRow({ layer, viewer })); + } }); const layerListCmp = Component({ async render() { diff --git a/src/controls/print/print-resize.js b/src/controls/print/print-resize.js index 64846568c..488bc47d7 100644 --- a/src/controls/print/print-resize.js +++ b/src/controls/print/print-resize.js @@ -46,7 +46,7 @@ export default function PrintResize(options = {}) { // Resize features when DPI changes const resizeFeature = function resizeFeature(style, feature, styleScale) { - if (!Array.isArray(style)) { + if (style && !Array.isArray(style)) { const image = style.getImage(); if (image) { if (!(feature.ol_uid in imageSavedScale)) { @@ -76,7 +76,7 @@ export default function PrintResize(options = {}) { // Reset features that was resized in DPI changes const resetFeature = function resetFeature(style, layer, feature) { - if (!Array.isArray(style)) { + if (style && !Array.isArray(style)) { const image = style.getImage(); if (image) { if (typeof layersSaveStyle[layer.get('name')].imageScale[feature.ol_uid].scale !== 'undefined') { @@ -404,8 +404,10 @@ export default function PrintResize(options = {}) { } else if (features) { features.forEach(feature => { const featureStyle = feature.getStyle(); - if (featureStyle) { - const styleScale = multiplyByFactor(1.5); + const styleScale = multiplyByFactor(1.5); + if (styleName === 'origoStylefunction' || styleName === 'default') { + feature.set('styleScale', styleScale); + } else if (featureStyle) { if (Array.from(featureStyle).length === 0) { resizeFeature(featureStyle, feature, styleScale); } else { @@ -439,11 +441,11 @@ export default function PrintResize(options = {}) { const source = layer.getSource(); if (isVector(layer)) { const features = source.getFeatures(); - - let style = viewer.getStyle(layer.get('styleName')); + const styleName = layer.get('styleName'); + let style = viewer.getStyle(); const clusterStyleName = layer.get('clusterStyle') ? layer.get('clusterStyle') : undefined; - if (typeof layer.get('styleName') !== 'undefined') { + if (typeof layer.get('styleName') !== 'undefined' && layer.get('styleName') !== 'origoStylefunction' && layer.get('styleName') !== 'default') { style = Style.createStyle({ style: layer.get('styleName'), viewer, clusterStyleName }); } if (style) { @@ -451,7 +453,11 @@ export default function PrintResize(options = {}) { } else if (features) { features.forEach(feature => { const featureStyle = feature.getStyle(); - if (featureStyle) { + console.log(featureStyle); + console.log(styleName); + if (styleName === 'origoStylefunction' || styleName === 'default') { + feature.set('styleScale', 1); + } else if (featureStyle) { if (Array.from(featureStyle).length === 0) { resetFeature(featureStyle, layer, feature); } else { diff --git a/src/dropdown.js b/src/dropdown.js index bddb5d45a..3942ec173 100644 --- a/src/dropdown.js +++ b/src/dropdown.js @@ -55,6 +55,7 @@ export default function dropDown(target, items, options) { targetEl.dispatchEvent(dropdownEvent); toggleActive(activeEl); + e.stopPropagation(e); }); } diff --git a/src/layer.js b/src/layer.js index 4894ffcdb..905108a66 100644 --- a/src/layer.js +++ b/src/layer.js @@ -46,6 +46,11 @@ const Layer = function Layer(optOptions, viewer) { layerOptions.extent = layerOptions.extent || viewer.getExtent(); layerOptions.sourceName = layerOptions.source; layerOptions.styleName = layerOptions.style; + if (typeof layerOptions.style === 'function') { + layerOptions.styleName = 'stylefunction'; + } else { + layerOptions.styleName = layerOptions.style; + } if (layerOptions.id === undefined) { layerOptions.id = name.split('__').shift(); } diff --git a/src/layer/geojson.js b/src/layer/geojson.js index 4440d71e5..6724dbdb1 100644 --- a/src/layer/geojson.js +++ b/src/layer/geojson.js @@ -1,43 +1,84 @@ import VectorSource from 'ol/source/Vector'; import GeoJSON from 'ol/format/GeoJSON'; +import Feature from 'ol/Feature'; import vector from './vector'; import isurl from '../utils/isurl'; -import { getStylewindowStyle } from '../controls/editor/stylewindow'; +import validate from '../utils/validate'; function createSource(options) { - const vectorSource = new VectorSource({ - attributions: options.attribution, - loader() { - fetch(options.url, { headers: options.headers }).then(response => response.json()).then((data) => { - vectorSource.addFeatures(vectorSource.getFormat().readFeatures(data)); - const numFeatures = vectorSource.getFeatures().length; - for (let i = 0; i < numFeatures; i += 1) { - vectorSource.forEachFeature((feature) => { - if (!feature.getGeometry().intersectsExtent(options.customExtent)) { - vectorSource.removeFeature(feature); - } - if (!feature.getId()) { - if (feature.get(options.idField)) { - feature.setId(feature.get(options.idField)); - } else { - feature.setId(1000000 + i); - } - } - if (feature.get('style') && options.styleByAttribute) { - const featureStyle = getStylewindowStyle(feature, feature.get('style')); - feature.setStyle(featureStyle); + const formatOptions = { + featureProjection: options.projectionCode, + dataProjection: options.dataProjection + }; + if (options.url) { + const vectorSource = new VectorSource({ + attributions: options.attribution, + loader() { + fetch(options.url, { headers: options.headers }).then(response => response.json()).then((data) => { + if (data.features) { + vectorSource.addFeatures(vectorSource.getFormat().readFeatures(data, formatOptions)); + const numFeatures = vectorSource.getFeatures().length; + for (let i = 0; i < numFeatures; i += 1) { + vectorSource.forEachFeature((feature) => { + if (!feature.getGeometry().intersectsExtent(options.customExtent)) { + vectorSource.removeFeature(feature); + } + if (!feature.getId()) { + if (feature.get(options.idField)) { + feature.setId(feature.get(options.idField)); + } else { + feature.setId(1000000 + i); + } + } + i += 1; + }); } - i += 1; - }); + } + }).catch(error => console.warn(error)); + }, + format: new GeoJSON() + }); + return vectorSource; + } else if (options.features) { + let features = options.features; + let featureArray = []; + + if (typeof features === 'string' && validate.json(features)) { // JSON-string + features = JSON.parse(features); + } + + if (typeof features === 'object' && (features.type === 'FeatureCollection' || features.type === 'Feature')) { // GeoJSON-object + featureArray = new GeoJSON().readFeatures(features, formatOptions); + } else if (Array.isArray(features)) { + for (let j = features.length - 1; j >= 0; j -= 1) { + let item = features[j]; + if (typeof item === 'string' && validate.json(item)) { // JSON-string + item = JSON.parse(item); + } + if (typeof item === 'object' && (item.type === 'FeatureCollection' || item.type === 'Feature')) { // GeoJSON-object + const readFeatures = new GeoJSON().readFeatures(item, formatOptions); + featureArray.push(...readFeatures); + } else if (item instanceof Feature) { // Real OpenLayers feature + featureArray.push(item); } - }).catch(error => console.warn(error)); - }, - format: new GeoJSON({ - dataProjection: options.dataProjection, - featureProjection: options.projectionCode - }) - }); - return vectorSource; + } + } + + featureArray.forEach((element, index) => { + if (!element.getId()) { + if (element.get(options.idField)) { + element.setId(element.get(options.idField)); + } else if (element.get('id')) { + element.setId(element.get('id')); + } else { + element.setId(1000000 + index); + } + } + }); + + return new VectorSource({ features: featureArray }); + } + return new VectorSource({}); } const geojson = function geojson(layerOptions, viewer) { @@ -55,20 +96,21 @@ const geojson = function geojson(layerOptions, viewer) { sourceOptions.styleByAttribute = geojsonOptions.styleByAttribute; if (geojsonOptions.projection) { sourceOptions.dataProjection = geojsonOptions.projection; - } else if (sourceOptions.projection) { - sourceOptions.dataProjection = sourceOptions.projection; - } else { - sourceOptions.dataProjection = viewer.getProjectionCode(); } sourceOptions.sourceName = layerOptions.source; if (isurl(geojsonOptions.source)) { sourceOptions.url = geojsonOptions.source; - } else { + } else if (geojsonOptions.source && viewer.getMapSource()[geojsonOptions.source]) { + geojsonOptions.sourceName = geojsonOptions.source; + sourceOptions.url = viewer.getMapSource()[geojsonOptions.source].url; + } else if (geojsonOptions.source && geojsonOptions.source !== 'none') { geojsonOptions.sourceName = geojsonOptions.source; sourceOptions.url = geojsonOptions.source; + } else if (geojsonOptions.features) { + sourceOptions.features = geojsonOptions.features; } - sourceOptions.headers = layerOptions.headers; + sourceOptions.headers = layerOptions.headers; const geojsonSource = createSource(sourceOptions); return vector(geojsonOptions, geojsonSource, viewer); }; diff --git a/src/layer/vector.js b/src/layer/vector.js index 600f8f0f9..eb44bcb3d 100644 --- a/src/layer/vector.js +++ b/src/layer/vector.js @@ -5,6 +5,8 @@ import ClusterSource from 'ol/source/Cluster'; import Style from '../style'; export default function vector(opt, src, viewer) { + const stylewindow = viewer.getStylewindow(); + const stylefunction = stylewindow.getStyleFunction; const options = opt; const source = src; const distance = 60; @@ -14,11 +16,20 @@ export default function vector(opt, src, viewer) { switch (options.layerType) { case 'vector': { + if (opt.styleByAttribute) { + const projection = source.projection || viewer.getProjectionCode(); + options.style = (feat) => stylefunction(feat, {}, projection); + options.styleName = 'origoStylefunction'; + } else if (typeof opt.style === 'function') { + options.style = opt.style; + } else { + options.style = Style.createStyle({ + style: options.style, + viewer + }); + } + options.source = source; - options.style = Style.createStyle({ - style: options.style, - viewer - }); vectorLayer = new VectorLayer(options); break; } diff --git a/src/permalink/permalinkparser.js b/src/permalink/permalinkparser.js index 48b0c11ca..24699e884 100644 --- a/src/permalink/permalinkparser.js +++ b/src/permalink/permalinkparser.js @@ -90,8 +90,18 @@ const controls = function controls(controlsStr) { }; const controlDraw = function controlDraw(drawState) { - const features = new GeoJSON().readFeatures(drawState.features); - return { features }; + const drawLayers = drawState.layers || []; + const layerArr = []; + let features = []; + drawLayers.forEach((element) => { + const layer = element; + layer.features = new GeoJSON().readFeatures(element.features); + layerArr.push(layer); + }); + if (drawState.features) { + features = new GeoJSON().readFeatures(drawState.features); + } + return { features, layers: layerArr }; }; const legend = function legend(stateStr) { diff --git a/src/permalink/permalinkstore.js b/src/permalink/permalinkstore.js index 680beda12..349680745 100644 --- a/src/permalink/permalinkstore.js +++ b/src/permalink/permalinkstore.js @@ -15,7 +15,7 @@ permalinkStore.getSaveLayers = function getSaveLayers(layers) { if (layer.get('defaultStyle') && layer.get('defaultStyle') !== layer.get('styleName')) saveLayer.sn = layer.get('altStyleIndex'); if (saveLayer.s || saveLayer.v) { saveLayer.name = layer.get('name'); - if (saveLayer.name !== 'measure') { + if (saveLayer.name !== 'measure' && !layer.get('drawlayer')) { saveLayers.push(urlparser.stringify(saveLayer, { topmost: 'name' })); diff --git a/src/style/drawstyles.js b/src/style/drawstyles.js new file mode 100644 index 000000000..024b43071 --- /dev/null +++ b/src/style/drawstyles.js @@ -0,0 +1,473 @@ +import { + Circle as CircleStyle, + Fill, + RegularShape, + Stroke, + Style, + Text +} from 'ol/style'; +import { getArea, getLength } from 'ol/sphere'; +import { LineString, MultiPoint, Point } from 'ol/geom'; + +function createRegularShape(type, size, fill, stroke) { + let style; + switch (type) { + case 'square': + style = new Style({ + image: new RegularShape({ + fill, + stroke, + points: 4, + radius: size, + angle: Math.PI / 4 + }) + }); + break; + + case 'triangle': + style = new Style({ + image: new RegularShape({ + fill, + stroke, + points: 3, + radius: size, + rotation: 0, + angle: 0 + }) + }); + break; + + case 'star': + style = new Style({ + image: new RegularShape({ + fill, + stroke, + points: 5, + radius: size, + radius2: size / 2.5, + angle: 0 + }) + }); + break; + + case 'cross': + style = new Style({ + image: new RegularShape({ + fill, + stroke, + points: 4, + radius: size, + radius2: 0, + angle: 0 + }) + }); + break; + + case 'x': + style = new Style({ + image: new RegularShape({ + fill, + stroke, + points: 4, + radius: size, + radius2: 0, + angle: Math.PI / 4 + }) + }); + break; + + case 'circle': + style = new Style({ + image: new CircleStyle({ + fill, + stroke, + radius: size + }) + }); + break; + + default: + style = new Style({ + image: new CircleStyle({ + fill, + stroke, + radius: size + }) + }); + } + return style; +} + +function formatLength(line, projection) { + const length = getLength(line, { projection }); + let output; + if (length > 1000) { + output = `${Math.round((length / 1000) * 100) / 100} km`; + } else { + output = `${Math.round(length * 100) / 100} m`; + } + return output; +} + +function formatArea(polygon, useHectare, projection) { + const area = getArea(polygon, { projection }); + let output; + if (area > 10000000) { + output = `${Math.round((area / 1000000) * 100) / 100} km\xB2`; + } else if (area > 10000 && useHectare) { + output = `${Math.round((area / 10000) * 100) / 100} ha`; + } else { + output = `${Math.round(area * 100) / 100} m\xB2`; + } + return output; +} + +function formatRadius(feat) { + let output; + const length = feat.getGeometry().getRadius(); + if (length > 10000) { + output = `${Math.round((length / 1000) * 100) / 100} km`; + } else if (length > 100) { + output = `${Math.round(length)} m`; + } else { + output = `${Math.round(length * 100) / 100} m`; + } + return output; +} + +const selectionStyle = new Style({ + image: new CircleStyle({ + radius: 5, + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.7)' + }), + fill: new Fill({ + color: 'rgba(0, 153, 255, 0.8)' + }) + }), + geometry(feature) { + let coords; + let pointGeometry; + const type = feature.getGeometry().getType(); + if (type === 'Polygon') { + coords = feature.getGeometry().getCoordinates()[0]; + pointGeometry = new MultiPoint(coords); + } else if (type === 'LineString') { + coords = feature.getGeometry().getCoordinates(); + pointGeometry = new MultiPoint(coords); + } else if (type === 'Point') { + coords = feature.getGeometry().getCoordinates(); + pointGeometry = new Point(coords); + } + return pointGeometry; + } +}); + +const measureStyle = function measureStyle(scale = 1) { + return new Style({ + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.4)' + }), + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.8)', + lineDash: [10 * scale, 10 * scale], + width: 2 * scale + }), + image: new CircleStyle({ + radius: 5 * scale, + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.7)' + }), + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }) + }) + }); +}; + +const labelStyle = function labelStyle(scale = 1) { + return new Style({ + text: new Text({ + font: `${14 * scale}px Calibri,sans-serif`, + fill: new Fill({ + color: 'rgba(255, 255, 255, 1)' + }), + backgroundFill: new Fill({ + color: 'rgba(0, 0, 0, 0.7)' + }), + padding: [3 * scale, 3 * scale, 3 * scale, 3 * scale], + textBaseline: 'bottom', + offsetY: -15 * scale + }), + image: new RegularShape({ + radius: 8 * scale, + points: 3, + angle: Math.PI, + displacement: [0, 10 * scale], + fill: new Fill({ + color: 'rgba(0, 0, 0, 0.7)' + }) + }) + }); +}; + +function getLabelStyle(scale = 1) { + return labelStyle(scale).clone(); +} + +const tipStyle = new Style({ + text: new Text({ + font: '12px Calibri,sans-serif', + fill: new Fill({ + color: 'rgba(255, 255, 255, 1)' + }), + backgroundFill: new Fill({ + color: 'rgba(0, 0, 0, 0.4)' + }), + padding: [2, 2, 2, 2], + textAlign: 'left', + offsetX: 15 + }) +}); + +const modifyStyle = new Style({ + image: new CircleStyle({ + radius: 5, + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.7)' + }), + fill: new Fill({ + color: 'rgba(0, 153, 255, 0.8)' + }) + }), + text: new Text({ + text: 'Dra för att ändra', + font: '12px Calibri,sans-serif', + fill: new Fill({ + color: 'rgba(255, 255, 255, 1)' + }), + backgroundFill: new Fill({ + color: 'rgba(0, 0, 0, 0.7)' + }), + padding: [2, 2, 2, 2], + textAlign: 'left', + offsetX: 15 + }) +}); + +const segmentStyle = function segmentStyle(scale = 1) { + return new Style({ + text: new Text({ + font: `${12 * scale}px Calibri,sans-serif`, + fill: new Fill({ + color: 'rgba(255, 255, 255, 1)' + }), + backgroundFill: new Fill({ + color: 'rgba(0, 0, 0, 0.4)' + }), + padding: [2 * scale, 2 * scale, 2 * scale, 2 * scale], + textBaseline: 'bottom', + offsetY: -12 * scale + }), + image: new RegularShape({ + radius: 6 * scale, + points: 3, + angle: Math.PI, + displacement: [0, 8 * scale], + fill: new Fill({ + color: 'rgba(0, 0, 0, 0.4)' + }) + }) + }); +}; + +function getBufferLabelStyle(label = '', scale = 1) { + return new Style({ + text: new Text({ + font: `${14 * scale}px Calibri,sans-serif`, + fill: new Fill({ + color: 'rgba(255, 255, 255, 1)' + }), + backgroundFill: new Fill({ + color: 'rgba(0, 0, 0, 0.7)' + }), + padding: [3 * scale, 3 * scale, 3 * scale, 3 * scale], + textBaseline: 'bottom', + offsetY: -15 * scale, + text: label + }), + image: new RegularShape({ + radius: 8 * scale, + points: 3, + angle: Math.PI, + displacement: [0, 10 * scale], + fill: new Fill({ + color: 'rgba(0, 0, 0, 0.7)' + }) + }), + geometry: (feat) => { + const coordinates = [feat.getGeometry().getCenter()[0], feat.getGeometry().getExtent()[3]]; + return new Point(coordinates); + } + }); +} + +function getSegmentLabelStyle(line, projection, scale = 1, segmentStyles = []) { + let count = 0; + const style = []; + line.forEachSegment((a, b) => { + const segment = new LineString([a, b]); + const segmentLabel = formatLength(segment, projection); + if (segmentStyles.length - 1 < count) { + segmentStyles.push(segmentStyle(scale).clone()); + } + const segmentPoint = new Point(segment.getCoordinateAt(0.5)); + segmentStyles[count].setGeometry(segmentPoint); + segmentStyles[count].getText().setText(segmentLabel); + style.push(segmentStyles[count]); + count += 1; + }); + return style; +} + +function getBufferPointStyle(scale = 1) { + return new Style({ + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }), + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.5)', + lineDash: [10 * scale, 10 * scale], + width: 2 * scale + }), + image: new CircleStyle({ + radius: 5 * scale, + stroke: new Stroke({ + color: 'rgba(0, 0, 0, 0.7)' + }), + fill: new Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }) + }), + geometry: (feat) => { + const coordinates = feat.getGeometry().getCenter(); + return new Point(coordinates); + } + }); +} + +function bufferStyleFunction(feature) { + const styleScale = feature.get('styleScale') || 1; + const bufferLabelStyle = getBufferLabelStyle(`${formatRadius(feature)}`, styleScale); + const pointStyle = getBufferPointStyle(styleScale); + return [measureStyle(styleScale), bufferLabelStyle, pointStyle]; +} + +const measure = { + linestring: [{ + geometry: 'endPoint', + circle: { + fill: { + color: [0, 153, 255, 1] + }, + stroke: { + color: [0, 153, 255, 1], + width: 1 + }, + radius: 3 + }, + text: { + font: 'bold 13px "Helvetica Neue", Helvetica, Arial, sans-serif', + textBaseline: 'bottom', + textAlign: 'center', + offsetY: -4, + fill: { + color: [0, 153, 255, 1] + }, + stroke: { + color: [255, 255, 255, 0.8], + width: 4 + } + } + }, + { + stroke: { + color: [0, 153, 255, 1], + width: 2 + } + } + ], + polygon: [{ + fill: { + color: [255, 255, 255, 0.4] + }, + stroke: { + color: [0, 153, 255, 1], + width: 2 + }, + text: { + font: 'bold 13px "Helvetica Neue", Helvetica, Arial, sans-serif', + textBaseline: 'middle', + textAlign: 'center', + overflow: 'true', + fill: { + color: [0, 153, 255, 1] + }, + stroke: { + color: [255, 255, 255, 0.8], + width: 4 + } + } + }], + interaction: [{ + fill: { + color: [255, 255, 255, 0.2] + }, + stroke: { + color: [0, 0, 0, 0.5], + lineDash: [10, 10], + width: 2 + }, + circle: { + radius: 5, + stroke: { + color: [0, 0, 0, 0.7] + }, + fill: { + color: [255, 255, 255, 0.2] + } + }, + text: { + font: 'bold 13px "Helvetica Neue", Helvetica, Arial, sans-serif', + textBaseline: 'middle', + textAlign: 'center', + overflow: 'true', + fill: { + color: [0, 153, 255, 1] + }, + stroke: { + color: [255, 255, 255, 0.8], + width: 4 + } + } + }] +}; + +export { + bufferStyleFunction, + createRegularShape, + formatLength, + formatArea, + formatRadius, + getBufferLabelStyle, + getBufferPointStyle, + getLabelStyle, + getSegmentLabelStyle, + labelStyle, + measure, + measureStyle, + modifyStyle, + segmentStyle, + selectionStyle, + tipStyle +}; diff --git a/src/style/hextorgba.js b/src/style/hextorgba.js new file mode 100644 index 000000000..cea0ac494 --- /dev/null +++ b/src/style/hextorgba.js @@ -0,0 +1,21 @@ +const isValidHex = (hex) => /^#([A-Fa-f0-9]{3,4}){1,2}$/.test(hex); + +const getChunksFromString = (st, chunkSize) => st.match(new RegExp(`.{${chunkSize}}`, 'g')); + +const convertHexUnitTo256 = (hexStr) => parseInt(hexStr.repeat(2 / hexStr.length), 16); + +const getAlphafloat = (a, alpha) => { + if (typeof a !== 'undefined') { return a / 255; } + if ((typeof alpha !== 'number') || alpha < 0 || alpha > 1) { + return 1; + } + return alpha; +}; + +export default function hexToRGBA(hex, alpha) { + if (!isValidHex(hex)) { throw new Error('Invalid HEX'); } + const chunkSize = Math.floor((hex.length - 1) / 3); + const hexArr = getChunksFromString(hex.slice(1), chunkSize); + const [r, g, b, a] = hexArr.map(convertHexUnitTo256); + return `rgba(${r}, ${g}, ${b}, ${getAlphafloat(a, alpha)})`; +} diff --git a/src/style/measure.js b/src/style/measure.js deleted file mode 100644 index 43fa69cb4..000000000 --- a/src/style/measure.js +++ /dev/null @@ -1,91 +0,0 @@ -const measure = { - linestring: [{ - geometry: 'endPoint', - circle: { - fill: { - color: [0, 153, 255, 1] - }, - stroke: { - color: [0, 153, 255, 1], - width: 1 - }, - radius: 3 - }, - text: { - font: 'bold 13px "Helvetica Neue", Helvetica, Arial, sans-serif', - textBaseline: 'bottom', - textAlign: 'center', - offsetY: -4, - fill: { - color: [0, 153, 255, 1] - }, - stroke: { - color: [255, 255, 255, 0.8], - width: 4 - } - } - }, - { - stroke: { - color: [0, 153, 255, 1], - width: 2 - } - } - ], - polygon: [{ - fill: { - color: [255, 255, 255, 0.4] - }, - stroke: { - color: [0, 153, 255, 1], - width: 2 - }, - text: { - font: 'bold 13px "Helvetica Neue", Helvetica, Arial, sans-serif', - textBaseline: 'middle', - textAlign: 'center', - overflow: 'true', - fill: { - color: [0, 153, 255, 1] - }, - stroke: { - color: [255, 255, 255, 0.8], - width: 4 - } - } - }], - interaction: [{ - fill: { - color: [255, 255, 255, 0.2] - }, - stroke: { - color: [0, 0, 0, 0.5], - lineDash: [10, 10], - width: 2 - }, - circle: { - radius: 5, - stroke: { - color: [0, 0, 0, 0.7] - }, - fill: { - color: [255, 255, 255, 0.2] - } - }, - text: { - font: 'bold 13px "Helvetica Neue", Helvetica, Arial, sans-serif', - textBaseline: 'middle', - textAlign: 'center', - overflow: 'true', - fill: { - color: [0, 153, 255, 1] - }, - stroke: { - color: [255, 255, 255, 0.8], - width: 4 - } - } - }] -}; - -export default measure; diff --git a/src/style/stylefunctions/default.js b/src/style/stylefunctions/default.js index 87d495984..cf166350b 100644 --- a/src/style/stylefunctions/default.js +++ b/src/style/stylefunctions/default.js @@ -32,44 +32,47 @@ export default function defaultStyle() { const styles = []; return function style(feature) { + const styleScale = feature.get('styleScale') || 1; polygon.setZIndex(1); line.setZIndex(10); let length = 0; + const width = 1 * styleScale; + const radius = 5 * styleScale; const geom = feature.getGeometry().getType(); switch (geom) { case 'Polygon': stroke.setColor(getColor('blue')); - stroke.setWidth(1); + stroke.setWidth(width); fill.setColor(getColor('blue', 0.8)); styles[length] = strokedPolygon; length += 1; break; case 'MultiPolygon': stroke.setColor(getColor('blue')); - stroke.setWidth(1); + stroke.setWidth(width); fill.setColor(getColor('blue', 0.8)); styles[length] = strokedPolygon; length += 1; break; case 'LineString': stroke.setColor(getColor('red')); - stroke.setWidth(1); + stroke.setWidth(width); styles[length] = line; length += 1; break; case 'MultiLineString': stroke.setColor(getColor('red')); - stroke.setWidth(1); + stroke.setWidth(width); styles[length] = line; length += 1; break; case 'Point': stroke.setColor(getColor('blue')); - stroke.setWidth(1); + stroke.setWidth(width); fill.setColor(getColor('blue', 0.8)); point = new Style({ image: new Circle({ - radius: 5, + radius, fill, stroke }), @@ -80,11 +83,11 @@ export default function defaultStyle() { break; case 'MultiPoint': stroke.setColor(getColor('blue')); - stroke.setWidth(1); + stroke.setWidth(width); fill.setColor(getColor('blue', 0.8)); point = new Style({ image: new Circle({ - radius: 5, + radius, fill, stroke }), @@ -95,7 +98,7 @@ export default function defaultStyle() { break; default: stroke.setColor(getColor('blue')); - stroke.setWidth(1); + stroke.setWidth(width); fill.setColor(getColor('blue', 0.8)); styles[length] = strokedPolygon; length += 1; diff --git a/src/style/styletemplate.js b/src/style/styletemplate.js new file mode 100644 index 000000000..785dc19ed --- /dev/null +++ b/src/style/styletemplate.js @@ -0,0 +1,104 @@ +export default function styleTemplate(palette, swStyle) { + const colorArray = palette; + let fillHtml = '
Fyllning
    '; + if (!colorArray.includes(swStyle.fillColor)) { + colorArray.push(swStyle.fillColor); + } + if (!colorArray.includes(swStyle.strokeColor)) { + colorArray.push(swStyle.strokeColor); + } + for (let i = 0; i < colorArray.length; i += 1) { + const checked = colorArray[i] === swStyle.fillColor ? ' checked=true' : ''; + fillHtml += `
  • + + +
  • `; + } + + fillHtml += `
+ +
+ 5% + Opacitet + 100% +
+
`; + + let strokeHtml = '
Kantlinje
    '; + for (let i = 0; i < colorArray.length; i += 1) { + const checked = colorArray[i] === swStyle.strokeColor ? ' checked=true' : ''; + strokeHtml += `
  • + + +
  • `; + } + + strokeHtml += `
+ +
+ 5% + Opacitet + 100% +
+
+
+ +
+ 1px + Linjebredd + 10px +
+
+
+ +
`; + + const pointHtml = `
Punkt
+ +
+ 1px + Punktstorlek + 50px +
+
+
+ +
`; + + const textHtml = `
Text
+ +
+ 8px + Textstorlek + 128px +
+
+
+ +
`; + + const measureHtml = `
Mått
+
+ + +
+
+ + +
+
`; + + return textHtml + pointHtml + fillHtml + strokeHtml + measureHtml; +} diff --git a/src/style/styletypes.js b/src/style/styletypes.js index ea3164e84..9fcff5903 100644 --- a/src/style/styletypes.js +++ b/src/style/styletypes.js @@ -1,5 +1,5 @@ import pin from './pin'; -import measure from './measure'; +import { measure } from './drawstyles'; import multiselection from './multiselection'; export default function styletypes() { diff --git a/src/style/stylewindow.js b/src/style/stylewindow.js new file mode 100644 index 000000000..222c63701 --- /dev/null +++ b/src/style/stylewindow.js @@ -0,0 +1,509 @@ +import { LineString, Point } from 'ol/geom'; +import Select from 'ol/interaction/Select'; +import Fill from 'ol/style/Fill'; +import Stroke from 'ol/style/Stroke'; +import Style from 'ol/style/Style'; +import Text from 'ol/style/Text'; + +import * as drawStyles from './drawstyles'; +import styleTemplate from './styletemplate'; +import hexToRgba from './hextorgba'; +import { Component, Button, Element, dom } from '../ui'; + +const Stylewindow = function Stylewindow(optOptions = {}) { + const { + title = 'Anpassa stil', + cls = 'control overflow-hidden hidden', + css = '', + viewer, + closeIcon = '#ic_close_24px', + palette = ['rgb(166,206,227)', 'rgb(31,120,180)', 'rgb(178,223,138)', 'rgb(51,160,44)', 'rgb(251,154,153)', 'rgb(227,26,28)', 'rgb(253,191,111)'] + } = optOptions; + + let annotationField; + let swStyle = {}; + let mapProjection; + const swDefaults = { + fillColor: 'rgb(0,153,255)', + fillOpacity: 0.75, + strokeColor: 'rgb(0,153,255)', + strokeOpacity: 1, + strokeWidth: 2, + strokeType: 'line', + pointSize: 10, + pointType: 'circle', + textSize: 20, + textString: 'Text', + textFont: '"Helvetica Neue", Helvetica, Arial, sans-serif', + showMeasureSegments: false, + showMeasure: false, + selected: false + }; + + function escapeQuotes(s) { + return s.replace(/'/g, "''"); + } + + function rgbToArray(colorString, opacity = 1) { + const colorArray = colorString.replace(/[^\d,.]/g, '').split(','); + colorArray[3] = opacity; + return colorArray; + } + + function rgbToRgba(colorString, opacity = 1) { + const colorArray = colorString.replace(/[^\d,.]/g, '').split(','); + return `rgba(${colorArray[0]},${colorArray[1]},${colorArray[2]},${opacity})`; + } + + function rgbaToRgb(colorString) { + const colorArray = colorString.replace(/[^\d,.]/g, '').split(','); + return `rgb(${colorArray[0]},${colorArray[1]},${colorArray[2]})`; + } + + function rgbaToOpacity(colorString) { + const colorArray = colorString.replace(/[^\d,.]/g, '').split(','); + return colorArray[3]; + } + + function stringToRgba(colorString, opacity) { + if (typeof colorString === 'string') { + if (colorString.toLowerCase().startsWith('rgba(')) { return colorString; } + if (colorString.startsWith('#')) { + return hexToRgba(colorString, opacity || 1); + } else if (colorString.toLowerCase().startsWith('rgb(')) { + return rgbToRgba(colorString, opacity || 1); + } + } + return rgbToRgba(swDefaults.fillColor, swDefaults.fillOpacity); + } + + function setFillColor(color) { + swStyle.fillColor = rgbToRgba(color, swStyle.fillOpacity); + } + + function setStrokeColor(color) { + swStyle.strokeColor = rgbToRgba(color, swStyle.strokeOpacity); + } + + function getStyleObject(feature, selected = false) { + let geometryType = feature.getGeometry().getType(); + let styleObject = {}; + if (feature.get(annotationField)) { + geometryType = 'TextPoint'; + } + switch (geometryType) { + case 'LineString': + case 'MultiLineString': + styleObject = { + strokeColor: rgbToRgba(swStyle.strokeColor, swStyle.strokeOpacity), + strokeWidth: swStyle.strokeWidth, + strokeType: swStyle.strokeType, + showMeasureSegments: swStyle.showMeasureSegments, + showMeasure: swStyle.showMeasure, + selected + }; + break; + case 'Polygon': + case 'MultiPolygon': + styleObject = { + fillColor: rgbToRgba(swStyle.fillColor, swStyle.fillOpacity), + strokeColor: rgbToRgba(swStyle.strokeColor, swStyle.strokeOpacity), + strokeWidth: swStyle.strokeWidth, + strokeType: swStyle.strokeType, + showMeasureSegments: swStyle.showMeasureSegments, + showMeasure: swStyle.showMeasure, + selected + }; + break; + case 'Point': + case 'MultiPoint': + styleObject = { + fillColor: rgbToRgba(swStyle.fillColor, swStyle.fillOpacity), + strokeColor: rgbToRgba(swStyle.strokeColor, swStyle.strokeOpacity), + strokeWidth: swStyle.strokeWidth, + strokeType: swStyle.strokeType, + pointSize: swStyle.pointSize, + pointType: swStyle.pointType, + selected + }; + break; + case 'TextPoint': + styleObject = { + fillColor: rgbToRgba(swStyle.fillColor, swStyle.fillOpacity), + textSize: swStyle.textSize, + textString: swStyle.textString, + textFont: swStyle.textFont, + selected + }; + break; + default: + styleObject = swStyle; + styleObject.fillColor = rgbToRgba(swStyle.fillColor, swStyle.fillOpacity); + break; + } + return Object.assign({}, styleObject); + } + + function restoreStylewindow() { + document.getElementById('o-draw-style-fill').classList.remove('hidden'); + document.getElementById('o-draw-style-stroke').classList.remove('hidden'); + document.getElementById('o-draw-style-point').classList.remove('hidden'); + document.getElementById('o-draw-style-text').classList.remove('hidden'); + document.getElementById('o-draw-style-measure').classList.remove('hidden'); + } + + function updateStylewindow(feature) { + const featureStyle = feature.get('origostyle') || {}; + featureStyle.fillColor = stringToRgba(featureStyle.fillColor, featureStyle.fillOpacity); + featureStyle.strokeColor = stringToRgba(featureStyle.strokeColor, featureStyle.strokeOpacity); + let geometryType = feature.getGeometry().getType(); + swStyle = Object.assign({}, swStyle, featureStyle); + if (feature.get(annotationField)) { + geometryType = 'TextPoint'; + } + switch (geometryType) { + case 'LineString': + case 'MultiLineString': + document.getElementById('o-draw-style-fill').classList.add('hidden'); + document.getElementById('o-draw-style-point').classList.add('hidden'); + document.getElementById('o-draw-style-text').classList.add('hidden'); + break; + case 'Polygon': + case 'MultiPolygon': + document.getElementById('o-draw-style-point').classList.add('hidden'); + document.getElementById('o-draw-style-text').classList.add('hidden'); + break; + case 'Point': + case 'MultiPoint': + document.getElementById('o-draw-style-text').classList.add('hidden'); + document.getElementById('o-draw-style-measure').classList.add('hidden'); + break; + case 'TextPoint': + document.getElementById('o-draw-style-stroke').classList.add('hidden'); + document.getElementById('o-draw-style-point').classList.add('hidden'); + document.getElementById('o-draw-style-measure').classList.add('hidden'); + break; + default: + break; + } + document.getElementById('o-draw-style-pointSizeSlider').value = swStyle.pointSize; + document.getElementById('o-draw-style-pointType').value = swStyle.pointType; + document.getElementById('o-draw-style-textSizeSlider').value = swStyle.textSize; + document.getElementById('o-draw-style-textString').value = swStyle.textString; + swStyle.strokeOpacity = rgbaToOpacity(swStyle.strokeColor); + swStyle.strokeColor = rgbaToRgb(swStyle.strokeColor); + const strokeEl = document.getElementById('o-draw-style-strokeColor'); + const strokeInputEl = strokeEl.querySelector(`input[value = "${swStyle.strokeColor}"]`); + if (strokeInputEl) { + strokeInputEl.checked = true; + } else { + const checkedEl = document.querySelector('input[name = "strokeColorRadio"]:checked'); + if (checkedEl) { + checkedEl.checked = false; + } + } + document.getElementById('o-draw-style-strokeWidthSlider').value = swStyle.strokeWidth; + document.getElementById('o-draw-style-strokeOpacitySlider').value = swStyle.strokeOpacity; + document.getElementById('o-draw-style-strokeType').value = swStyle.strokeType; + + const fillEl = document.getElementById('o-draw-style-fillColor'); + swStyle.fillOpacity = rgbaToOpacity(swStyle.fillColor); + swStyle.fillColor = rgbaToRgb(swStyle.fillColor); + const fillInputEl = fillEl.querySelector(`input[value = "${swStyle.fillColor}"]`); + if (fillInputEl) { + fillInputEl.checked = true; + } else { + const checkedEl = document.querySelector('input[name = "fillColorRadio"]:checked'); + if (checkedEl) { + checkedEl.checked = false; + } + } + document.getElementById('o-draw-style-fillOpacitySlider').value = swStyle.fillOpacity; + document.getElementById('o-draw-style-showMeasure').checked = swStyle.showMeasure; + document.getElementById('o-draw-style-showMeasureSegments').checked = swStyle.showMeasureSegments; + } + + function getStyleFunction(feature, inputStyle = {}, projection = mapProjection) { + if (!feature.get('origostyle') && feature.get('style') && typeof feature.get('style') === 'object') { + feature.set('origostyle', feature.get('style')); + } + const featureStyle = feature.get('origostyle') || {}; + const styleScale = feature.get('styleScale') || 1; + const newStyleObj = Object.assign({}, swDefaults, featureStyle, inputStyle); + newStyleObj.fillColor = stringToRgba(newStyleObj.fillColor, newStyleObj.fillOpacity); + newStyleObj.strokeColor = stringToRgba(newStyleObj.strokeColor, newStyleObj.strokeOpacity); + newStyleObj.strokeWidth *= styleScale; + newStyleObj.textSize *= styleScale; + newStyleObj.pointSize *= styleScale; + const geom = feature.getGeometry(); + let geometryType = feature.getGeometry().getType(); + if (feature.get(annotationField)) { + geometryType = 'TextPoint'; + } + let style = []; + let lineDash; + if (newStyleObj.strokeType === 'dash') { + lineDash = [3 * newStyleObj.strokeWidth, 3 * newStyleObj.strokeWidth]; + } else if (newStyleObj.strokeType === 'dash-point') { + lineDash = [3 * newStyleObj.strokeWidth, 3 * newStyleObj.strokeWidth, 0.1, 3 * newStyleObj.strokeWidth]; + } else if (newStyleObj.strokeType === 'point') { + lineDash = [0.1, 3 * newStyleObj.strokeWidth]; + } else { + lineDash = false; + } + + const stroke = new Stroke({ + color: newStyleObj.strokeColor, + width: newStyleObj.strokeWidth, + lineDash + }); + const fill = new Fill({ + color: newStyleObj.fillColor + }); + const font = `${newStyleObj.textSize}px ${newStyleObj.textFont}`; + switch (geometryType) { + case 'LineString': + case 'MultiLineString': + style[0] = new Style({ + stroke + }); + if (newStyleObj.showMeasureSegments) { + const segmentLabelStyle = drawStyles.getSegmentLabelStyle(geom, projection, styleScale); + style = style.concat(segmentLabelStyle); + } + if (newStyleObj.showMeasure) { + const label = drawStyles.formatLength(geom, projection); + const point = new Point(geom.getLastCoordinate()); + const labelStyle = drawStyles.getLabelStyle(styleScale); + labelStyle.setGeometry(point); + labelStyle.getText().setText(label); + style = style.concat(labelStyle); + } + break; + case 'Polygon': + case 'MultiPolygon': + style[0] = new Style({ + fill, + stroke + }); + if (newStyleObj.showMeasureSegments) { + const line = new LineString(geom.getCoordinates()[0]); + const segmentLabelStyle = drawStyles.getSegmentLabelStyle(line, projection, styleScale); + style = style.concat(segmentLabelStyle); + } + if (newStyleObj.showMeasure) { + const label = drawStyles.formatArea(geom, true, projection); + const point = geom.getInteriorPoint(); + const labelStyle = drawStyles.getLabelStyle(styleScale); + labelStyle.setGeometry(point); + labelStyle.getText().setText(label); + style = style.concat(labelStyle); + } + break; + case 'Point': + case 'MultiPoint': + style[0] = drawStyles.createRegularShape(newStyleObj.pointType, newStyleObj.pointSize, fill, stroke); + break; + case 'TextPoint': + style[0] = new Style({ + text: new Text({ + text: newStyleObj.textString || 'Text', + font, + fill + }) + }); + feature.set(annotationField, newStyleObj.textString || 'Text'); + break; + default: + style[0] = drawStyles.createRegularShape(newStyleObj.pointType, newStyleObj.pointSize, fill, stroke); + break; + } + if (newStyleObj.selected) { + style.push(drawStyles.selectionStyle); + } + return style; + } + + function getSelectedFeatures() { + let features = []; + + viewer.getMap().getInteractions().forEach((interaction) => { + if (interaction instanceof Select) { + features = interaction.getFeatures(); + } + }); + + return features; + } + + function styleFeature(feature, selected = false) { + const styleObject = getStyleObject(feature, selected); + feature.set('origostyle', styleObject); + } + + function styleSelectedFeatures() { + getSelectedFeatures().forEach((feature) => { + styleFeature(feature, true); + }); + } + + function bindUIActions() { + let matches; + const fillColorEl = document.getElementById('o-draw-style-fillColor'); + const strokeColorEl = document.getElementById('o-draw-style-strokeColor'); + + matches = fillColorEl.querySelectorAll('span'); + for (let i = 0; i < matches.length; i += 1) { + matches[i].addEventListener('click', function e() { + setFillColor(this.style.backgroundColor); + styleSelectedFeatures(); + }); + } + + matches = strokeColorEl.querySelectorAll('span'); + for (let i = 0; i < matches.length; i += 1) { + matches[i].addEventListener('click', function e() { + setStrokeColor(this.style.backgroundColor); + styleSelectedFeatures(); + }); + } + + document.getElementById('o-draw-style-fillOpacitySlider').addEventListener('input', function e() { + swStyle.fillOpacity = escapeQuotes(this.value); + setFillColor(swStyle.fillColor); + styleSelectedFeatures(); + }); + + document.getElementById('o-draw-style-strokeOpacitySlider').addEventListener('input', function e() { + swStyle.strokeOpacity = escapeQuotes(this.value); + setStrokeColor(swStyle.strokeColor); + styleSelectedFeatures(); + }); + + document.getElementById('o-draw-style-strokeWidthSlider').addEventListener('input', function e() { + swStyle.strokeWidth = escapeQuotes(this.value); + styleSelectedFeatures(); + }); + + document.getElementById('o-draw-style-strokeType').addEventListener('change', function e() { + swStyle.strokeType = escapeQuotes(this.value); + styleSelectedFeatures(); + }); + + document.getElementById('o-draw-style-pointType').addEventListener('change', function e() { + swStyle.pointType = escapeQuotes(this.value); + styleSelectedFeatures(); + }); + + document.getElementById('o-draw-style-showMeasure').addEventListener('change', function e() { + swStyle.showMeasure = this.checked; + styleSelectedFeatures(); + }); + + document.getElementById('o-draw-style-showMeasureSegments').addEventListener('change', function e() { + swStyle.showMeasureSegments = this.checked; + styleSelectedFeatures(); + }); + + document.getElementById('o-draw-style-pointSizeSlider').addEventListener('input', function e() { + swStyle.pointSize = escapeQuotes(this.value); + styleSelectedFeatures(); + }); + + document.getElementById('o-draw-style-textString').addEventListener('input', function e() { + swStyle.textString = escapeQuotes(this.value); + styleSelectedFeatures(); + }); + + document.getElementById('o-draw-style-textSizeSlider').addEventListener('input', function e() { + swStyle.textSize = escapeQuotes(this.value); + styleSelectedFeatures(); + }); + } + + annotationField = optOptions.annotation || 'annotation'; + swStyle = Object.assign(swDefaults, optOptions.swDefaults); + + let stylewindowEl; + let titleEl; + let headerEl; + let contentEl; + let closeButton; + + palette.forEach((item, index) => { + const colorArr = rgbToArray(palette[index]); + palette[index] = `rgb(${colorArr[0]},${colorArr[1]},${colorArr[2]})`; + }); + + const closeWindow = function closeWindow() { + stylewindowEl.classList.add('hidden'); + }; + + return Component({ + closeWindow, + getStyleFunction, + getStyleObject, + restoreStylewindow, + updateStylewindow, + onInit() { + mapProjection = viewer.getProjection().getCode(); + const headerCmps = []; + const thisComponent = this; + titleEl = Element({ + cls: 'flex row justify-start margin-y-small margin-left text-weight-bold', + style: 'width: 100%;', + innerHTML: `${title}` + }); + headerCmps.push(titleEl); + + closeButton = Button({ + cls: 'small round margin-top-small margin-right-small margin-bottom-auto margin-right icon-smaller grey-lightest no-shrink', + icon: closeIcon, + validStates: ['initial', 'hidden'], + click() { + closeWindow(); + thisComponent.dispatch('showStylewindow', false); + } + }); + headerCmps.push(closeButton); + + headerEl = Element({ + cls: 'flex justify-end grey-lightest', + components: headerCmps + }); + + contentEl = Element({ + cls: 'o-draw-stylewindow-content overflow-auto', + innerHTML: `${styleTemplate(palette, swStyle)}` + }); + + this.addComponent(headerEl); + this.addComponent(contentEl); + + this.on('render', this.onRender); + const target = optOptions.target || viewer.getId(); + document.getElementById(target).appendChild(dom.html(this.render())); + this.dispatch('render'); + bindUIActions(); + }, + onRender() { + stylewindowEl = document.getElementById(this.getId()); + }, + render() { + let addStyle; + if (css !== '') { + addStyle = `style="${css}"`; + } else { + addStyle = ''; + } + return `
+
+ ${headerEl.render()} + ${contentEl.render()} +
+
`; + } + }); +}; + +export default Stylewindow; diff --git a/src/templates/featureinfotemplate.js b/src/templates/featureinfotemplate.js index fc609107f..2b7d7466b 100644 --- a/src/templates/featureinfotemplate.js +++ b/src/templates/featureinfotemplate.js @@ -1,6 +1,6 @@ import helpers from '../utils/templatehelpers'; export default (properties) => { - const els = `${helpers.each(properties, obj => `
  • ${obj.prop} : ${obj.value}
  • `)}`; + const els = `${helpers.each(properties, obj => `
  • ${obj.prop}: ${obj.value}
  • `)}`; return els; }; diff --git a/src/ui/input.js b/src/ui/input.js index b8152664c..fda7c4634 100644 --- a/src/ui/input.js +++ b/src/ui/input.js @@ -17,11 +17,16 @@ export default function Input(options = {}) { onRender() { const el = document.getElementById(this.getId()); el.addEventListener('keyup', this.onChange.bind(this)); + el.addEventListener('focusout', this.onFocusOut.bind(this)); }, onChange(evt) { value = evt.target.value; this.dispatch('change', { value }); }, + onFocusOut(evt) { + value = evt.target.value; + this.dispatch('focusout', { value }); + }, render() { return ` diff --git a/src/ui/modal.js b/src/ui/modal.js index 8bca8993c..3c01c1e53 100644 --- a/src/ui/modal.js +++ b/src/ui/modal.js @@ -22,6 +22,7 @@ export default function Modal(options = {}) { title = '', content = '', contentElement, + contentCmp, cls = '', isStatic = options.static, target, @@ -95,11 +96,13 @@ export default function Modal(options = {}) { cls: 'flex row justify-end grey-lightest', components: headerCmps }); - - contentEl = Element({ - cls: 'o-modal-content', - innerHTML: `${content}` - }); + const elOptions = { cls: 'o-modal-content' }; + if (contentCmp) { + elOptions.components = [contentCmp]; + } else if (content) { + elOptions.innerHTML = `${content}`; + } + contentEl = Element(elOptions); this.addComponent(screenEl); this.addComponent(headerEl); diff --git a/src/utils/escapequotes.js b/src/utils/escapequotes.js new file mode 100644 index 000000000..744923a53 --- /dev/null +++ b/src/utils/escapequotes.js @@ -0,0 +1,3 @@ +export default function escapeQuotes(s) { + return s.replace(/'/g, "''"); +} diff --git a/src/utils/exporttofile.js b/src/utils/exporttofile.js index 79129785a..51c81efb9 100644 --- a/src/utils/exporttofile.js +++ b/src/utils/exporttofile.js @@ -42,6 +42,15 @@ const exportToFile = function exportToFile(features, format, opts = {}) { featureProjection }; + // Set selected attribute if origostyle is present + features.forEach((feature) => { + if (feature.get('origostyle')) { + const style = feature.get('origostyle'); + style.selected = false; + feature.set('origostyle', style); + } + }); + // Convert features to the specified format using the provided parameters const bytes = formatter.writeFeatures(features, formatterOptions); diff --git a/src/utils/templatehelpers.js b/src/utils/templatehelpers.js index 60944a26a..4b523b672 100644 --- a/src/utils/templatehelpers.js +++ b/src/utils/templatehelpers.js @@ -4,7 +4,7 @@ const templateHelpers = { const props = Object.keys(obj); const propsIncluded = []; props.forEach(element => { - if (typeof obj[element] !== 'undefined' && obj[element] !== null && obj[element] !== '') { + if (typeof obj[element] !== 'undefined' && obj[element] !== null && obj[element] !== '' && element !== 'style') { propsIncluded.push(element); } }); diff --git a/src/utils/validate.js b/src/utils/validate.js index 31e5eca85..3e2a06bb7 100644 --- a/src/utils/validate.js +++ b/src/utils/validate.js @@ -86,4 +86,13 @@ validate.color = (color) => { return false; }; +validate.json = (str) => { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; +}; + export default validate; diff --git a/src/viewer.js b/src/viewer.js index 0ba04e1ac..7a0cd6837 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -17,13 +17,16 @@ import CenterMarker from './components/centermarker'; import flattenGroups from './utils/flattengroups'; import getcenter from './geometry/getcenter'; import isEmbedded from './utils/isembedded'; +import generateUUID from './utils/generateuuid'; import permalink from './permalink/permalink'; +import Stylewindow from './style/stylewindow'; const Viewer = function Viewer(targetOption, options = {}) { let map; let tileGrid; let featureinfo; let selectionmanager; + let stylewindow; const { breakPoints, @@ -51,7 +54,8 @@ const Viewer = function Viewer(targetOption, options = {}) { source = {}, clusterOptions = {}, tileGridOptions = {}, - url + url, + palette } = options; let { @@ -138,6 +142,8 @@ const Viewer = function Viewer(targetOption, options = {}) { const getSelectionManager = () => selectionmanager; + const getStylewindow = () => stylewindow; + const getCenter = () => getcenter; const getMapUtils = () => maputils; @@ -517,6 +523,7 @@ const Viewer = function Viewer(targetOption, options = {}) { })); tileGrid = maputils.tileGrid(tileGridSettings); + stylewindow = Stylewindow({ palette, viewer: this }); setMap(Map(Object.assign(options, { projection, center, zoom, target: this.getId() }))); @@ -677,8 +684,10 @@ const Viewer = function Viewer(targetOption, options = {}) { setStyle, zoomToExtent, getSelectionManager, + getStylewindow, getEmbedded, permalink, + generateUUID, centerMarker }); };