diff --git a/examples/snowbox/index.js b/examples/snowbox/index.js index accd300b1a..53313361f8 100644 --- a/examples/snowbox/index.js +++ b/examples/snowbox/index.js @@ -9,6 +9,7 @@ import { } from '@polar/polar' import pluginAddressSearch from '@polar/polar/plugins/addressSearch' import pluginAttributions from '@polar/polar/plugins/attributions' +import pluginDraw from '@polar/polar/plugins/draw' import pluginExport from '@polar/polar/plugins/export' import pluginFilter from '@polar/polar/plugins/filter' import pluginFullscreen from '@polar/polar/plugins/fullscreen' @@ -34,6 +35,7 @@ const ausgleichsflaechen = '1454' const reports = '6059' const denkmal = 'denkmaelerWMS' const hamburgBorder = '6074' +const flurstuecke = 'flurstuecke' let colorScheme = 'light' // eslint-disable-next-line no-unused-vars @@ -120,19 +122,25 @@ const map = await createMap( name: 'Anliegen (MML)', visibility: false, }, + { + id: flurstuecke, + type: 'mask', + name: 'Flurstücke', + visibility: false, + }, { id: ausgleichsflaechen, type: 'mask', name: 'Ausgleichsflächen', styleId: 'panda', - visibility: true, + visibility: false, minZoom: 5, }, { id: denkmal, type: 'mask', name: 'Kulturdenkmale', - visibility: true, + visibility: false, options: { layers: { order: '6,24,25,4,3,2,1,0', @@ -150,6 +158,11 @@ const map = await createMap( }, }, }, + { + id: 'lgv_multipolygons', + type: 'WFS-T', + name: 'LGV WFS-T Multipolygone (P)', + }, ], layout: 'nineRegions', checkServiceAvailability: true, @@ -403,7 +416,7 @@ addPlugin( pluginIconMenu({ displayComponent: true, layoutTag: 'TOP_RIGHT', - initiallyOpen: 'layerChooser', + initiallyOpen: 'draw', menus: [ [ { @@ -478,6 +491,86 @@ addPlugin( }), }, ], + [ + { + plugin: pluginDraw({ + // showLoader: true, // defaultValue: true + // showToasts: true, // defaultValue: true + showMeasure: true, // defaultValue: false + showStyling: true, // defaultValue: false + layers: [ + { + id: 'localLayer', + name: 'Lokale Zeichenebene', + selectableDrawModes: [ + 'Point', + 'LineString', + 'Polygon', + 'Circle', + 'Text', + ], + snapTo: [flurstuecke], + style: { + fill: { + hatch: { + pattern: 'diagonal', + lineWidth: 10, + size: 30, + backgroundColor: [100, 100, 255, 0.5], + patternColor: [255, 255, 255, 1.0], + }, + }, + stroke: { + color: '#3375d4', + width: 2, + }, + imageCircle: { + radius: 7, + fill: { + color: 'rgb(51 117 212 / 100%)', + }, + }, + }, + textStyle: { + font: { + size: [10, 20, 30], + family: 'Consolas', + }, + textColor: '#16161D', + }, + lassos: [{ id: flurstuecke }], + revision: { + autofix: true, + mergeToMultiGeometries: true, + metaServices: [ + { + id: flurstuecke, + propertyNames: [ + 'land', + 'gemarkung', + 'regbezirk', + 'kreis', + 'gemeinde', + ], + aggregationMode: 'unequal', + }, + ], + validate: true, + }, + }, + + /* TODO: support WFS-T layers for selection + { + id: 'lgv_multipolygons', + name: 'LGV WFS-T Multipolygone (P)', + selectableDrawModes: ['MultiPolygon'], + saving: {}, // »if ID matches a WFS-T config, we may need some save parameters and then are good to go« – partially copyable from FDB, also use the OL methods provided; please mind that saving (esp. of styles) should be written in an adaptable fashion, i.e. adapters should be planned, since people will make up formats wherever they go + }, + */ + ], + }), + }, + ], [ { plugin: pluginGeoLocation({ @@ -578,6 +671,22 @@ subscribe( JSON.stringify(coordinates)) ) +subscribe(map, 'draw', 'featureCollection', (featureCollection) => + // eslint-disable-next-line no-console + console.info( + 'Feature Collection was updated: ', + JSON.stringify(featureCollection, null, 2) + ) +) + +subscribe(map, 'draw', 'revisedFeatureCollection', (revisedFeatureCollection) => + // eslint-disable-next-line no-console + console.info( + 'Revised Feature Collection was updated: ', + JSON.stringify(revisedFeatureCollection, null, 2) + ) +) + /* simple language switcher attached for demo purposes; * language switching is considered a global concern and * should be handled by the leading application */ diff --git a/examples/snowbox/services.js b/examples/snowbox/services.js index fb26bfacfe..be67771545 100644 --- a/examples/snowbox/services.js +++ b/examples/snowbox/services.js @@ -86,4 +86,24 @@ export default [ featureNS: 'http://www.deegree.org/app', featurePrefix: 'app', }, + { + id: 'flurstuecke', + typ: 'OAF', + name: 'Flurstueck', + url: 'https://api.hamburg.de/datasets/v1/alkis_vereinfacht', + collection: 'Flurstueck', + crs: 'http://www.opengis.net/def/crs/EPSG/0/25832', + bboxCrs: 'http://www.opengis.net/def/crs/EPSG/0/25832', + }, + // NOTE: Here be WFS-T 🐉 + { + id: 'lgv_multipolygons', + name: 'LGV WFS-T Multipolygone', + url: 'https://masterportal.schulung.deegree.pro/core-services/services/wfstlgv', + typ: 'WFS', + featurePrefix: 'sf', + featureType: 'wfstlgv', + version: '1.1.0', + featureNS: 'http://cite.opengeospatial.org/gmlsf', + }, ] diff --git a/package-lock.json b/package-lock.json index 20133547ed..ca51894d9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,18 @@ "@microsoft/api-extractor": "^7.58.12", "@pinia/testing": "^2.0.1", "@repositoryname/noop": "^1.0.6", + "@turf/boolean-contains": "^7.3.5", + "@turf/boolean-intersects": "^7.3.5", + "@turf/boolean-point-in-polygon": "^7.3.5", + "@turf/boolean-valid": "^7.3.5", + "@turf/buffer": "^7.3.5", + "@turf/center-of-mass": "^7.3.5", + "@turf/clean-coords": "^7.3.5", + "@turf/difference": "^7.3.5", + "@turf/helpers": "^7.3.5", + "@turf/line-intersect": "^7.3.5", + "@turf/union": "^7.3.5", + "@turf/unkink-polygon": "^7.3.5", "@types/geojson": "7946.0.16", "@types/node": "^24.13.3", "@vitejs/plugin-vue": "^6.0.8", @@ -39,6 +51,7 @@ "i18next-vue": "^5.4.0", "jsdom": "^30.0.1", "jspdf": "^4.2.1", + "just-compare": "^2.3.0", "magic-string": "^0.30.18", "ol": "10.9.0", "pinia": "^4.0.2", @@ -3070,6 +3083,649 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@turf/area": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.3.5.tgz", + "integrity": "sha512-sSn80wPT7XfBIDN3vurCPxhk9W4U8ozS/XImSqeLN8qveTICOxzZkhsGDMp0CuncaN+plWut4a2TdNM7mzZB6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-7.3.5.tgz", + "integrity": "sha512-oG1ya/HtBjAIg4TimbWx+nOYPbY0bCvt82Bq8tm6sBw3qqtbOyRSfDz79Sq90TnH7DXJprJ1qnVGKNtZ6jemfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-contains": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-contains/-/boolean-contains-7.3.5.tgz", + "integrity": "sha512-P4JUAHgvJkD+8ybQ6d1OHp9TBsGsjJxF5lWeXJgp0k4+Hd/D0CVy4/mhLkZdNa6QdljVdwNcfU0CTqy1WsSQig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.3.5", + "@turf/boolean-point-in-polygon": "7.3.5", + "@turf/boolean-point-on-line": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@turf/line-split": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-crosses": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-crosses/-/boolean-crosses-7.3.5.tgz", + "integrity": "sha512-o4Gmb2gbPl4j7810ZrT9GZsGigY+HRwcOI9pkkKH6BJ4ewSlQCPzP4JFruZp0+mIXS6NnPv7+Lw3J8LN3kYw1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/boolean-equal": "7.3.5", + "@turf/boolean-point-in-polygon": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@turf/line-intersect": "7.3.5", + "@turf/polygon-to-line": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-disjoint": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-disjoint/-/boolean-disjoint-7.3.5.tgz", + "integrity": "sha512-Pz1GGUC6iL6xGVqhyo+fYg35kl4j/HONMEoC1voN3DcBOCcVlzq+ljvMpvE+oQiR7Q38xLhIxZneLCqMp5YrQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/line-intersect": "7.3.5", + "@turf/meta": "7.3.5", + "@turf/polygon-to-line": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-equal": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-equal/-/boolean-equal-7.3.5.tgz", + "integrity": "sha512-xjSFi/BpxBY5tDDuSbixIRVq2AnDGcdLL8XOHzZtJWZjSa6tAi6afxSiMbQTGIhYfwwcB/sJcIUUffBaIQ2BiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/clean-coords": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@types/geojson": "^7946.0.10", + "geojson-equality-ts": "^1.0.2", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-intersects": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-intersects/-/boolean-intersects-7.3.5.tgz", + "integrity": "sha512-Z6GPYjozrmTuzWQD0x7o8RPm+4HC7hz9q23hdB3U1+Qahesv8Mtc+wo82tO4CG6/NRnJ9u79DlEhmR1DxUU4iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/boolean-disjoint": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-overlap": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-overlap/-/boolean-overlap-7.3.5.tgz", + "integrity": "sha512-DUeiPVqFSTjW79erPbo780pRcKCRad59NcscVsTYkZD/92peF/4rQvHbvAUpUDPZaQCxe+mvfeDHfUFmfVKCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@turf/line-intersect": "7.3.5", + "@turf/line-overlap": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "geojson-equality-ts": "^1.0.2", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.3.5.tgz", + "integrity": "sha512-ba7+B0wzaS9GtERZOoXUZ6oW8IcIJHNQZf3c+tiD9ESjcsPO1Q/4qIJGTKl92nBLhhracHJxMWBM/U6hAVkaRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@types/geojson": "^7946.0.10", + "point-in-polygon-hao": "^1.1.0", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-on-line": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-on-line/-/boolean-point-on-line-7.3.5.tgz", + "integrity": "sha512-TuWfrAT63W43BDzgYc94UzQ5/PjF1aTnh4AIzmQwez1YnimShYcOTwo8OIHzDaB6gbbvFsfxYMuOA5JOp942Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-valid": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/boolean-valid/-/boolean-valid-7.3.5.tgz", + "integrity": "sha512-ZezJCgFJS0JRJfj6fVSI1idcifTaWFW70NYRCUur9926DHLvSkfbtEF5i63yPQt2Zxx00FTUJPcHRzdgOiwECA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.3.5", + "@turf/boolean-crosses": "7.3.5", + "@turf/boolean-disjoint": "7.3.5", + "@turf/boolean-overlap": "7.3.5", + "@turf/boolean-point-in-polygon": "7.3.5", + "@turf/boolean-point-on-line": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@turf/line-intersect": "7.3.5", + "@types/geojson": "^7946.0.10", + "geojson-polygon-self-intersections": "^1.2.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/buffer": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/buffer/-/buffer-7.3.5.tgz", + "integrity": "sha512-TGls3nYtWzviKHT00XVBfHKa7Z2oIZKqiHN7R0xErGwMSAR7YhxVROhxq/iyIsWZjl1SlPwweZZIxWILQuxpZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.3.5", + "@turf/center": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/jsts": "^2.7.1", + "@turf/meta": "7.3.5", + "@turf/projection": "7.3.5", + "@types/geojson": "^7946.0.10", + "d3-geo": "1.7.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/center/-/center-7.3.5.tgz", + "integrity": "sha512-eub5/Kfdmn89ZqwCONHI7astmTDEtN5M6+JfOkgoSyhKKFhUJYNxUyH1F/vCtIP7j1K369Vs4L9TYiuGapvIKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.3.5", + "@turf/helpers": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center-of-mass": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/center-of-mass/-/center-of-mass-7.3.5.tgz", + "integrity": "sha512-eEzx4YKxUP55Apdjt7XXuQ906KKx4uSQWLxJw5OHE0rKOSN/qYTSdu0s7nQBAmGgAqeSC2538vuN7wEqMfYMvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/centroid": "7.3.5", + "@turf/convex": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/centroid": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-7.3.5.tgz", + "integrity": "sha512-hkWaqwGFdOn6Tf0EWfn2yn1XZ1FWE1h2C5ZWstDMu/FxYO5DB+YjlmOFPl4K6SmSOEgdV07eK2vDCyPeTHqKGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clean-coords": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/clean-coords/-/clean-coords-7.3.5.tgz", + "integrity": "sha512-e0ZTqVWiQaCI/b8EFb+HAS8lCDomWafAKxQuIv0IYks0GwOlVTDWTwsY2RX2685j2Y+QssqOsyHsPZCtAjc3YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/boolean-point-on-line": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clone": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-7.3.5.tgz", + "integrity": "sha512-qfIaHj3410QEcTpiCRnTzhq8YrUp2gWrUIPLBAEFykopNxJkq1du1VrRzvuAo36ap2UV7KppkS6wGNypbcxswQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/convex": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/convex/-/convex-7.3.5.tgz", + "integrity": "sha512-d/pq86he+/GB/2ObuBHapeT15QFsCPhhGab3uE4YjogLMbD8qtu0sRGF+cvvPRDNuCLycOBL/xgFLnf9SteYlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "concaveman": "^1.2.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/difference": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/difference/-/difference-7.3.5.tgz", + "integrity": "sha512-iDyWYCvgS3vgB8W1ai2cvfJaTq9bOt1QghiVkCNIyccF97CgtYJzenREVVUlp8qU9lJlbQ/ameyKvXPA1uOAlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/distance": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-7.3.5.tgz", + "integrity": "sha512-uQAC63zg/l91KUxzfhqio7Ii3+UXTrPOVJScIdRj6EO6+9XHI4kC+AdyIS4cPAv14sZfJLIBxzMnzcGrss+kEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/geojson-rbush": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/geojson-rbush/-/geojson-rbush-7.3.5.tgz", + "integrity": "sha512-30/hQqc+ErnlcavvDdxGfgm8VtsJDEzSYpf3mPqYxOyI976l49T6+1jCQD5xKswml6o8zZAaTSe6ZcSKF+SCNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/geojson-rbush/node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@turf/geojson-rbush/node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, + "node_modules/@turf/helpers": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.5.tgz", + "integrity": "sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.3.5.tgz", + "integrity": "sha512-ZVIvsBvjr8lO7WxC5zYNjRsjSDvyGvWkJMjuWaJjTU8x+1tmfNnw3gDX/TI2Sit83gcRYLYkNo23lB/udqx/Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/jsts": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@turf/jsts/-/jsts-2.7.2.tgz", + "integrity": "sha512-zAezGlwWHPyU0zxwcX2wQY3RkRpwuoBmhhNE9HY9kWhFDkCxZ3aWK5URKwa/SWKJbj9aztO+8vtdiBA28KVJFg==", + "dev": true, + "license": "(EDL-1.0 OR EPL-1.0)", + "dependencies": { + "jsts": "2.7.1" + } + }, + "node_modules/@turf/line-intersect": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-7.3.5.tgz", + "integrity": "sha512-2Cl4oPsjaDdfIwz/5IRDdG2fNdfp3W6atICm81vnzl/GwURoVP+CLjXJ64QWWzpzIbgX2XprJQTmamByDt5MDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@types/geojson": "^7946.0.10", + "sweepline-intersections": "^1.5.0", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-overlap": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/line-overlap/-/line-overlap-7.3.5.tgz", + "integrity": "sha512-7lZMWYHuzM6EMlL5pIIi/Nh7GLsM7ilW8/jVyHP1yGfQ0c0YAUoJd/Xy3J2+9v8OkYiZW/t2LdwVoTmCEJLWxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/boolean-point-on-line": "7.3.5", + "@turf/geojson-rbush": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@turf/line-segment": "7.3.5", + "@turf/meta": "7.3.5", + "@turf/nearest-point-on-line": "7.3.5", + "@types/geojson": "^7946.0.10", + "fast-deep-equal": "^3.1.3", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-segment": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-7.3.5.tgz", + "integrity": "sha512-TM1aCu7utM6fllAEHO8PNqBJZ/uoFJVNp2A0YI7FyWN928hPbacsvNtLeVz/Kq1ZbeqQ1ZIKRxo9FdVjaj8hGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-split": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/line-split/-/line-split-7.3.5.tgz", + "integrity": "sha512-GEuy+LdbbaqtYjHk/i1G8sK51wfCdPqTO8uH0dJZ6WlcIcZQfRcKKI4ksFm7NkVyfmw8gXWbpMJD8lO380GFBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.3.5", + "@turf/geojson-rbush": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@turf/line-intersect": "7.3.5", + "@turf/line-segment": "7.3.5", + "@turf/meta": "7.3.5", + "@turf/nearest-point-on-line": "7.3.5", + "@turf/truncate": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.3.5.tgz", + "integrity": "sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-on-line": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-on-line/-/nearest-point-on-line-7.3.5.tgz", + "integrity": "sha512-MZn6OkEFZpjS6BNUANfqiHMIbQSivu7TNji3a+OAIrnPJ71vp8cbz0N2aVEa5M7I8ipvxoxAPIV3eqg3h280Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/distance": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-to-line": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/polygon-to-line/-/polygon-to-line-7.3.5.tgz", + "integrity": "sha512-Mat5tvJcW3grpXCNFcMvjHL3d8hO4eoIgF3qYpXj25BHx/S7SJUOgyCV5x3arC0rCfM/cB71VmNDm9k57ec7bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/invariant": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/projection": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/projection/-/projection-7.3.5.tgz", + "integrity": "sha512-G4bejYKT0vCQZryMhEoS9aLmP7ThDg6nb3zi3wPzELiTrGNOd2YgkWVheQDGCk4hcqEIWZc9fI2alaRSSkkLVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/clone": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/truncate": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/truncate/-/truncate-7.3.5.tgz", + "integrity": "sha512-Qx2iv3KIqKuDAUduMfaJ5fFegEWBeRve5zePalRevS16bMUqEX+jnKPK9fWGyUuPqT61qP1Kybz0PTWPbUbljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/union": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/union/-/union-7.3.5.tgz", + "integrity": "sha512-/FSKhl+LX4+M7L/Trmiln0CDPWS8vCneGnQktt1o5XbCY/zIpH1JdxHEBFXhFZg4beAyXCz0uuxRyW9N/DH+KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/unkink-polygon": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@turf/unkink-polygon/-/unkink-polygon-7.3.5.tgz", + "integrity": "sha512-7MwKCm7sPHVF4xBO/3s08/DtA/09UEBXaLNnm8/RUq3abS5zZ9PnakLsd36J18IHDscM6RmH7IZPLSwYh4TLcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@turf/area": "7.3.5", + "@turf/boolean-point-in-polygon": "7.3.5", + "@turf/helpers": "7.3.5", + "@turf/meta": "7.3.5", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/unkink-polygon/node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@turf/unkink-polygon/node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/@tweenjs/tween.js": { "version": "25.0.0", "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", @@ -4452,6 +5108,16 @@ "require-from-string": "^2.0.2" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -4882,6 +5548,43 @@ "dev": true, "license": "MIT" }, + "node_modules/concaveman": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/concaveman/-/concaveman-1.2.1.tgz", + "integrity": "sha512-PwZYKaM/ckQSa8peP5JpVr7IMJ4Nn/MHIaWUjP4be+KoZ7Botgs8seAZGpmaOM+UZXawcdYRao/px9ycrCihHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "point-in-polygon": "^1.1.0", + "rbush": "^3.0.1", + "robust-predicates": "^2.0.4", + "tinyqueue": "^2.0.3" + } + }, + "node_modules/concaveman/node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/concaveman/node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, + "node_modules/concaveman/node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", + "dev": true, + "license": "ISC" + }, "node_modules/confbox": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", @@ -5109,6 +5812,23 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/d3-geo": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.7.1.tgz", + "integrity": "sha512-O4AempWAr+P5qbk2bC2FuN/sDW4z+dN2wDf9QV3bxQt4M5HfOEeXLgJ/UKQW0+o1Dj8BE+L5kiDbdWUMjsmQpw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -7147,6 +7867,43 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-equality-ts": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/geojson-equality-ts/-/geojson-equality-ts-1.0.2.tgz", + "integrity": "sha512-h3Ryq+0mCSN/7yLs0eDgrZhvc9af23o/QuC4aTiuuzP/MRCtd6mf5rLsLRY44jX0RPUfM8c4GqERQmlUxPGPoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.14" + } + }, + "node_modules/geojson-polygon-self-intersections": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/geojson-polygon-self-intersections/-/geojson-polygon-self-intersections-1.2.2.tgz", + "integrity": "sha512-6XRNF4CsRHYmR9z5YuIk5f/aOototnDf0dgMqYGcS7y1l57ttt6MAIAxl3rXyas6lq1HEbTuLMh4PgvO+OV42w==", + "dev": true, + "license": "MIT", + "dependencies": { + "rbush": "^2.0.1" + } + }, + "node_modules/geojson-polygon-self-intersections/node_modules/quickselect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-1.1.1.tgz", + "integrity": "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/geojson-polygon-self-intersections/node_modules/rbush": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-2.0.2.tgz", + "integrity": "sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "quickselect": "^1.0.1" + } + }, "node_modules/geotiff": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-3.0.5.tgz", @@ -8912,6 +9669,16 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jsts": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/jsts/-/jsts-2.7.1.tgz", + "integrity": "sha512-x2wSZHEBK20CY+Wy+BPE7MrFQHW6sIsdaGUMEqmGAio+3gFzQaBYPwLRonUfQf9Ak8pBieqj9tUofX1+WtAEIg==", + "dev": true, + "license": "(EDL-1.0 OR EPL-1.0)", + "engines": { + "node": ">= 12" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -8928,6 +9695,13 @@ "node": ">=4.0" } }, + "node_modules/just-compare": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz", + "integrity": "sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg==", + "dev": true, + "license": "MIT" + }, "node_modules/kdbush": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", @@ -11665,6 +12439,41 @@ "node": ">=6" } }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", + "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/point-in-polygon-hao": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz", + "integrity": "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/point-in-polygon-hao/node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/polyclip-ts": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/polyclip-ts/-/polyclip-ts-0.16.8.tgz", + "integrity": "sha512-JPtKbDRuPEuAjuTdhR62Gph7Is2BS1Szx69CFOO3g71lpJDFo78k4tFyi+qFOMVPePEzdSKkpGU3NBXPHHjvKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.1.0", + "splaytree-ts": "^1.0.2" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -12264,6 +13073,13 @@ "node": ">= 0.8.15" } }, + "node_modules/robust-predicates": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz", + "integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==", + "dev": true, + "license": "Unlicense" + }, "node_modules/rolldown": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", @@ -12680,6 +13496,13 @@ "node": ">=0.10.0" } }, + "node_modules/splaytree-ts": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/splaytree-ts/-/splaytree-ts-1.0.2.tgz", + "integrity": "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA==", + "dev": true, + "license": "BDS-3-Clause" + }, "node_modules/split2": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", @@ -13339,6 +14162,23 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, + "node_modules/sweepline-intersections": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sweepline-intersections/-/sweepline-intersections-1.5.0.tgz", + "integrity": "sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyqueue": "^2.0.0" + } + }, + "node_modules/sweepline-intersections/node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", + "dev": true, + "license": "ISC" + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index 106ec359b2..42acc02858 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,18 @@ "@microsoft/api-extractor": "^7.58.12", "@pinia/testing": "^2.0.1", "@repositoryname/noop": "^1.0.6", + "@turf/boolean-contains": "^7.3.5", + "@turf/boolean-intersects": "^7.3.5", + "@turf/boolean-point-in-polygon": "^7.3.5", + "@turf/boolean-valid": "^7.3.5", + "@turf/buffer": "^7.3.5", + "@turf/center-of-mass": "^7.3.5", + "@turf/clean-coords": "^7.3.5", + "@turf/difference": "^7.3.5", + "@turf/helpers": "^7.3.5", + "@turf/line-intersect": "^7.3.5", + "@turf/union": "^7.3.5", + "@turf/unkink-polygon": "^7.3.5", "@types/geojson": "7946.0.16", "@types/node": "^24.13.3", "@vitejs/plugin-vue": "^6.0.8", @@ -111,6 +123,7 @@ "i18next-vue": "^5.4.0", "jsdom": "^30.0.1", "jspdf": "^4.2.1", + "just-compare": "^2.3.0", "magic-string": "^0.30.18", "ol": "10.9.0", "pinia": "^4.0.2", diff --git a/src/components/PolarIconButton.ce.vue b/src/components/PolarIconButton.ce.vue index 8647dd87b3..068e52225b 100644 --- a/src/components/PolarIconButton.ce.vue +++ b/src/components/PolarIconButton.ce.vue @@ -11,6 +11,7 @@ aria-hidden="true" /> {{ hint }} + @@ -34,7 +35,7 @@ defineOptions({ position: relative; background: var(--kern-color-layout-background-default); box-shadow: var(--polar-shadow); - border: none; + border: solid transparent; &:focus, &:hover { diff --git a/src/components/PolarIconButtonSelect.ce.vue b/src/components/PolarIconButtonSelect.ce.vue new file mode 100644 index 0000000000..a103e9d329 --- /dev/null +++ b/src/components/PolarIconButtonSelect.ce.vue @@ -0,0 +1,62 @@ + + + + + + {{ label }} + + + + + + + + + + + diff --git a/src/components/PolarSelect.ce.vue b/src/components/PolarSelect.ce.vue index b9689665e0..91b66b4680 100644 --- a/src/components/PolarSelect.ce.vue +++ b/src/components/PolarSelect.ce.vue @@ -1,8 +1,9 @@ - + {{ label }} diff --git a/src/core/types/plugin.ts b/src/core/types/plugin.ts index 6326e21dc6..53296e16b4 100644 --- a/src/core/types/plugin.ts +++ b/src/core/types/plugin.ts @@ -7,6 +7,9 @@ import type { useAddressSearchStore as AddressSearchStore } from '@/plugins/addr import type { PluginId as AttributionsPluginId } from '@/plugins/attributions' import type { resourcesEn as AttributionsResources } from '@/plugins/attributions/locales' import type { useAttributionsStore as AttributionsStore } from '@/plugins/attributions/store' +import type { PluginId as DrawPluginId } from '@/plugins/draw' +import type { resourcesEn as DrawResources } from '@/plugins/draw/locales' +import type { useDrawStore as DrawStore } from '@/plugins/draw/store' import type { PluginId as ExportPluginId } from '@/plugins/export' import type { resourcesEn as ExportResources } from '@/plugins/export/locales' import type { useExportStore as ExportStore } from '@/plugins/export/store' @@ -146,6 +149,7 @@ export type PolarPluginStore< export type BundledPluginId = | typeof AddressSearchPluginId | typeof AttributionsPluginId + | typeof DrawPluginId | typeof ExportPluginId | typeof FilterPluginId | typeof FooterPluginId @@ -176,6 +180,7 @@ type GetPluginStore< export type BundledPluginStores = | GetPluginStore | GetPluginStore + | GetPluginStore | GetPluginStore | GetPluginStore | GetPluginStore @@ -219,6 +224,7 @@ export type BundledPluginLocaleResources = typeof AttributionsPluginId, typeof AttributionsResources > + | GetPluginResources | GetPluginResources | GetPluginResources | GetPluginResources diff --git a/src/lib/jsonStyleMapper.ts b/src/lib/jsonStyleMapper.ts new file mode 100644 index 0000000000..68ab51bb42 --- /dev/null +++ b/src/lib/jsonStyleMapper.ts @@ -0,0 +1,46 @@ +import PolygonStyle from '@masterportal/masterportalapi/src/vectorStyle/styles/polygon/stylePolygon' +import { Circle, Fill, Icon, Stroke, Text } from 'ol/style' + +// imitate class to match keyMap ... +function Hatch(params) { + const polygonStyle = new PolygonStyle() + return polygonStyle.getPolygonFillHatch(params)?.getContext('2d')?.fillStyle +} + +// maps keys from source to key that's actually to be written +const inputKeyToOpenLayersKey = (key: string) => + ({ + imageCircle: 'image', + imageIcon: 'image', + hatch: 'color', + })[key] || key + +const inputKeyToOpenLayersClass = { + fill: Fill, + stroke: Stroke, + text: Text, + imageCircle: Circle, + imageIcon: Icon, + hatch: Hatch, +} + +/** + * This function builds the parameter tree to an `ol/Style` instantiation. + * @param styleFragment - This may be anything (except for actual ol instances) that resides at any nesting level in `ol/Style`'s instantiation parameter, which is named `options` in ol's documentation. @see https://openlayers.org/en/latest/apidoc/module-ol_style_Style-Style.html Please mind that only a finite set of nested classes is currently supported, see above function definition and API.md file. + */ +export const buildStyleParameters = (styleFragment) => + typeof styleFragment === 'object' && !Array.isArray(styleFragment) + ? Object.entries(styleFragment).reduce((accumulator, [key, value]) => { + if (inputKeyToOpenLayersClass[key]) { + accumulator[inputKeyToOpenLayersKey(key)] = + new inputKeyToOpenLayersClass[key]({ + ...buildStyleParameters(value), + ...(key === 'text' ? { text: 'Testtext' } : {}), + }) + } else { + accumulator[inputKeyToOpenLayersKey(key)] = + buildStyleParameters(value) + } + return accumulator + }, {}) + : styleFragment diff --git a/src/plugins/draw/components/DrawGadget.ce.vue b/src/plugins/draw/components/DrawGadget.ce.vue new file mode 100644 index 0000000000..ffaa5ee155 --- /dev/null +++ b/src/plugins/draw/components/DrawGadget.ce.vue @@ -0,0 +1,179 @@ + + + + {{ $t(($) => $.label, { ns: PluginId }) }} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/draw/components/DrawHelp.ce.vue b/src/plugins/draw/components/DrawHelp.ce.vue new file mode 100644 index 0000000000..b2687474b0 --- /dev/null +++ b/src/plugins/draw/components/DrawHelp.ce.vue @@ -0,0 +1,134 @@ + + + + {{ $t(($) => $.help.text[helpText], { ns: PluginId }) }} + + + + + {{ alt ? 'Alt + ' : '' }} + + + {{ $t(($) => $.help.tooltip[label], { ns: PluginId }) }} + + + + + + + + diff --git a/src/plugins/draw/components/DrawMeasure.ce.vue b/src/plugins/draw/components/DrawMeasure.ce.vue new file mode 100644 index 0000000000..0d196394a3 --- /dev/null +++ b/src/plugins/draw/components/DrawMeasure.ce.vue @@ -0,0 +1,41 @@ + + + + + + + + + diff --git a/src/plugins/draw/components/DrawText.ce.vue b/src/plugins/draw/components/DrawText.ce.vue new file mode 100644 index 0000000000..67a73d60ab --- /dev/null +++ b/src/plugins/draw/components/DrawText.ce.vue @@ -0,0 +1,80 @@ + + + + + {{ $t(($) => $.text.size, { ns: PluginId }) }} + + + + {{ textSizes[textSizeIndex] }} + + + + + {{ $t(($) => $.text.content, { ns: PluginId }) }} + + + + + + + + + + diff --git a/src/plugins/draw/index.ts b/src/plugins/draw/index.ts new file mode 100644 index 0000000000..f737321734 --- /dev/null +++ b/src/plugins/draw/index.ts @@ -0,0 +1,33 @@ +/* eslint-disable tsdoc/syntax */ +/** + * @module \@polar/polar/plugins/draw + */ +/* eslint-enable tsdoc/syntax */ + +import type { PluginContainer, PolarPluginStore } from '@/core' +import type { DrawPluginOptions } from './types' + +import component from './components/DrawGadget.ce.vue' +import locales from './locales' +import { useDrawStore } from './store' +import { PluginId } from './types' + +/** + * Creates a plugin that lets users draw. + * + * @returns Plugin for use with {@link addPlugin}. + */ +export default function pluginDraw( + options: DrawPluginOptions +): PluginContainer { + return { + id: PluginId, + component, + locales, + icon: 'kern-icon-fill--brush', + storeModule: useDrawStore as PolarPluginStore, + options, + } +} + +export * from './types' diff --git a/src/plugins/draw/locales.ts b/src/plugins/draw/locales.ts new file mode 100644 index 0000000000..0b44a0cba8 --- /dev/null +++ b/src/plugins/draw/locales.ts @@ -0,0 +1,265 @@ +/* Allowing PascalCase keys for ease of use (using OL entity names here) */ +/* eslint-disable @typescript-eslint/naming-convention */ + +/* eslint-disable tsdoc/syntax */ +/** + * This is the documentation for the locales keys in the draw plugin. + * These locales are *NOT* exported, but documented only. + * + * @module locales/plugins/draw + */ +/* eslint-enable tsdoc/syntax */ + +import type { Locale } from '@/core' + +/** + * German locales for draw plugin. + * For overwriting these values, use the plugin's ID as namespace. + */ +export const resourcesDe = { + label: 'Zeichenwerkzeuge', + layerSelection: { + label: 'Layerauswahl', + }, + drawMode: { + label: 'Zeichnen', + Point: 'Punkt', + LineString: 'Linie', + Polygon: 'Polygon', + Circle: 'Kreis', + Text: 'Text', + }, + editMode: { + label: 'Editieren', + modify: 'Modifizieren', + translate: 'Verschieben', + duplicate: 'Duplizieren', + cut: 'Polygon auftrennen', + merge: 'Polygone kombinieren', + lasso: 'Lasso', + }, + measurements: { + label: 'Längen- und Flächenmaße', + none: 'Keine Messung', + noneArea: 'Keine Messung', + metres: 'm', + metresArea: 'm / m²', + kilometres: 'km', + kilometresArea: 'km / km²', + hectaresArea: 'km / ha', + }, + delete: { + label: 'Löschen', + }, + upload: { + label: 'Importieren', + }, + download: { + label: 'Exportieren', + geojson: 'GeoJSON', + }, + revision: { + autofix: { + errorToast: + 'Die automatische Geometrie-Reparatur ist fehlgeschlagen, da die eingegebenen Geometrien nicht gültig und nicht reparierbar waren. Dies kann z.B. passieren, wenn Punkte eines Polygons im Bearbeitungsmodus so verschoben werden, dass sie punktförmig werden.', + }, + metaInformationRetrieval: { + errorToast: + 'Der Bezug von Metadaten zur gezeichneten Geometrie ist fehlgeschlagen. Die Geometrie wird ohne Metadaten weitergereicht.', + }, + }, + text: { + size: 'Textgröße (px)', + content: 'Textinhalt', + }, + lasso: { + layerRejected: + 'Die Antwort des Layers "{{id}}" konnte nicht gelesen werden. Es wurden keine Geometrien aus diesem Layer bezogen.', + internalError: + 'Ein unerwarteter Fehler ist in der Verarbeitung der Lasso-Daten aufgetreten.', + }, + cut: { + error: { + cutFailed: + 'Das Polygon konnte aufgrund eines unbekannten Fehlers leider nicht geschnitten werden.', + }, + warn: { + unevenCut: + 'Leider konnte kein Schnitt hergestellt werden, da entweder der Start- oder Endpunkt der Schnittkante innerhalb des zu schneidenden Polygons lag, oder kein Polygon geschnitten wurde. Bitte versuchen Sie es erneut.', + }, + }, + help: { + text: { + Point: 'Punkte setzen.', + LineString: 'Linien zeichnen.', + Polygon: 'Polygone zeichnen.', + Circle: + 'Kreise zeichnen. Der erste Klick fügt einen Mittelpunkt hinzu, der zweite Klick legt den Radius fest.', + Text: 'Text hinzufügen. Bitte benutzen sie hierfür die untenstehende Eingabemaske. Es können nur Texte hinzugefügt werden, wenn diese nicht leer sind.', + modify: + 'Geometrie durch Bewegen, Hinzufügen, und Löschen von Stützpunkten bearbeiten.', + translate: 'Geometrie ganzheitlich verschieben.', + duplicate: 'Geometrie duplizieren.', + cut: 'Polygon auftrennen. Hierzu wird eine Linie gezeichnet, die das Polygon schneidet.', + merge: + 'Polygone kombinieren, indem ein weiteres Polygon gezeichnet wird. Dieses gezeichnete Polygon wird ebenfalls Teil der Kombination aller berührten Polygone.', + lasso: + 'Geometrien mit Lasso auswählen und in die Zeichenebene übernehmen.', + delete: 'Geometrie löschen.', + }, + tooltip: { + clickDraw: 'Klick – Punkt setzen', + clickStart: 'Klick – Startpunkt setzen', + clickContinue: 'Klick – Zwischenpunkt setzen', + clickEndPolygon: 'Klick auf Start – Polygon fertigstellen', + clickEndCircle: 'Klick – Kreis fertigstellen', + clickEndDouble: 'Doppelklick – Geometrie fertigstellen', + clickAlt: 'Klick – Eckpunkt entfernen', + dragLine: 'An Linie ziehen – neuen Zwischenpunkt setzen', + dragPoint: 'An Punkt ziehen – Punkt bewegen', + dragVertex: 'An Punkt ziehen – Geometrie anpassen', + dragMove: 'An Geometrie ziehen – Geometrie verschieben', + clickDuplicate: 'Klick auf Geometrie – Geometrie duplizieren', + clickCutStart: 'Klick – Startpunkt Trennlinie setzen', + clickCut: 'Klick – neuen Zwischenpunkt Trennlinie setzen', + clickCutFinish: + 'Doppelklick – Endpunkt Trennlinie setzen – Polygon auftrennen', + clickDelete: 'Klick auf Geometrie – Geometrie löschen', + }, + }, +} as const + +/** + * English locales for draw plugin. + * For overwriting these values, use the plugin's ID as namespace. + */ +export const resourcesEn = { + label: 'Drawing tools', + layerSelection: { + label: 'Layer selection', + }, + drawMode: { + label: 'Draw', + Point: 'Point', + LineString: 'Line', + Polygon: 'Polygon', + Circle: 'Circle', + Text: 'Text', + }, + editMode: { + label: 'Edit', + modify: 'Modify', + translate: 'Move', + duplicate: 'Duplicate', + cut: 'Split polygon', + merge: 'Combine polygons', + lasso: 'Lasso', + }, + measurements: { + label: 'Length and area measurements', + none: 'No measurement', + noneArea: 'No measurement', + metres: 'm', + metresArea: 'm / m²', + kilometres: 'km', + kilometresArea: 'km / km²', + hectaresArea: 'km / ha', + }, + delete: { + label: 'Delete', + }, + upload: { + label: 'Import', + }, + download: { + label: 'Export', + geojson: 'GeoJSON', + }, + revision: { + autofix: { + errorToast: + 'The automatic geometry repair failed because the entered geometries were not valid and not repairable. This can happen, for example, when points of a polygon are moved in edit mode so that they become point-shaped.', + }, + metaInformationRetrieval: { + errorToast: + 'Retrieving metadata for the drawn geometry failed. The geometry is passed on without metadata.', + }, + }, + text: { + size: 'Text size (px)', + content: 'Text content', + }, + lasso: { + layerRejected: + 'The response of layer "{{id}}" could not be read. No geometries were fetched from that layer.', + internalError: 'An unexpected error occurred while processing lasso data.', + }, + cut: { + error: { + cutFailed: 'The polygon could not be split due to an unknown error.', + }, + warn: { + unevenCut: + 'No split could be created because either the start or end point of the cutting edge was inside the polygon to split, or no polygon was split. Please try again.', + }, + }, + help: { + text: { + Point: 'Place points.', + LineString: 'Draw lines.', + Polygon: 'Draw polygons.', + Circle: + 'Draw circles. The first click adds a center point, the second click sets the radius.', + Text: 'Add text. Please use the input field below for this. Text can only be added if it is not empty.', + modify: 'Edit the geometry by moving, adding, and removing vertices.', + translate: 'Move the geometry as a whole.', + duplicate: 'Duplicate the geometry.', + cut: 'Split a polygon. To do this, a line is drawn that intersects the polygon.', + merge: + 'Combine polygons by drawing another polygon. This drawn polygon also becomes part of the combination of all touched polygons.', + lasso: + 'Select geometries with the lasso and transfer them into the drawing layer.', + delete: 'Delete the geometry.', + }, + tooltip: { + clickDraw: 'Click – place point', + clickStart: 'Click – set start point', + clickContinue: 'Click – set intermediate point', + clickEndPolygon: 'Click on start – complete polygon', + clickEndCircle: 'Click – complete circle', + clickEndDouble: 'Double-click – complete geometry', + clickAlt: 'Click – remove vertex', + dragLine: 'Drag on line – set new intermediate point', + dragPoint: 'Drag on point – move point', + dragVertex: 'Drag on point – adjust geometry', + dragMove: 'Drag on geometry – move geometry', + clickDuplicate: 'Click on geometry – duplicate geometry', + clickCutStart: 'Click – set start point of cutting line', + clickCut: 'Click – set new intermediate point of cutting line', + clickCutFinish: + 'Double-click – set end point of cutting line – split polygon', + clickDelete: 'Click on geometry – delete geometry', + }, + }, +} as const + +/** + * Draw plugin locales. + * + * @privateRemarks + * The first entry will be used as fallback. + * + * @internal + */ +const locales: Locale[] = [ + { + type: 'de', + resources: resourcesDe, + }, + { + type: 'en', + resources: resourcesEn, + }, +] + +export default locales diff --git a/src/plugins/draw/store.ts b/src/plugins/draw/store.ts new file mode 100644 index 0000000000..5941b3127c --- /dev/null +++ b/src/plugins/draw/store.ts @@ -0,0 +1,528 @@ +/* eslint-disable tsdoc/syntax */ +/** + * @module \@polar/polar/plugins/draw/store + */ +/* eslint-enable tsdoc/syntax */ + +import type { FeatureCollection } from 'geojson' +import type { Feature as OlFeature } from 'ol' +import type BaseLayer from 'ol/layer/Base' +import type { ComputedRef } from 'vue' +import type { + DownloadMode, + DrawMode, + DrawPluginOptions, + EditMode, + GeometryType, + RevisionStateFlag, + ToolMode, +} from './types' +import type { MeasureMode } from './types' + +import Style from 'ol/style/Style' +import { acceptHMRUpdate, defineStore, storeToRefs } from 'pinia' +import { computed, ref, shallowRef, watch } from 'vue' + +import { useCoreStore } from '@/core/stores' + +import { complete, error, inactive, inProgress } from './types' +import { MeasureModes, PluginId } from './types' +import { drawSourceToFeatureCollection } from './utils/conversion/drawSourceToFeatureCollection' +import { featureCollectionToDrawSource } from './utils/conversion/featureCollectionToDrawSource' +import { createDrawLayer } from './utils/drawLayer' +import { InteractionManager } from './utils/interactionManager' +import { reviseFeatures } from './utils/reviseFeatures' + +/* eslint-disable tsdoc/syntax */ +/** + * @function + * + * Plugin store for fullscreen mode detection and enablement. + */ +/* eslint-enable tsdoc/syntax */ +export const useDrawStore = defineStore('plugins/draw', () => { + const coreStore = useCoreStore() + const { map } = storeToRefs(coreStore) + + let interactionManager: InteractionManager | undefined + + const configuration = computed( + () => (coreStore.configuration[PluginId] || {}) as DrawPluginOptions + ) + + // *slaps roof of store* + const _activeTool = ref(null) + const _drawMode = ref('Point') + const _editMode = ref('modify') + const _measureMode = ref('none') + const _selectedFeature = shallowRef(null) + const _textInput = ref('') + const _textSizeIndex = ref(0) + const _downloadFormat = ref('geojson') + const _activeLayerId = ref('') + const _layerIds = ref([]) + const _layers = ref([]) + const _featureCollection = ref>({ + type: 'FeatureCollection', + features: [], + }) + + const revisedFeatureCollection = ref>({ + type: 'FeatureCollection', + features: [], + }) + const revisionStateFlag = ref(inactive) + const drawing = ref(false) + + const reinitializeInteractions = () => { + if (_activeTool.value) { + interactionManager?.initializeInteractions( + _activeTool.value, + getModeForTool(_activeTool.value), + { + measureMode: measureMode.value, + activeLassoIds: activeLassoIds.value, + text: { + textInput: textInput.value, + textSize: textSizes.value[textSizeIndex.value] ?? 12, + textStyle: { + font: activeLayerConfig.value?.textStyle?.font ?? 'sans-serif', + textColor: + activeLayerConfig.value?.textStyle?.textColor ?? '#000000', + }, + }, + selectedFeature, + drawing, + } + ) + } + } + + const featureCollectionUpdater = (drawSource) => { + drawSourceToFeatureCollection(drawSource, featureCollection) + } + + const activeLayerId = computed({ + get: () => _activeLayerId.value, + set: (value) => { + _activeTool.value = null + _activeLayerId.value = value + interactionManager?.updateDrawLayer(value) + }, + }) + + const activeLayerConfig = computed(() => + configuration.value.layers.find((layer) => layer.id === activeLayerId.value) + ) + + const layerOptions = computed(() => { + return _layers.value.map((layer) => ({ + value: layer.get('id'), + label: layer.get('name') ?? layer.get('id'), + })) + }) + + function getModeForTool(tool: ToolMode) { + switch (tool) { + case 'draw': + return _drawMode.value + case 'edit': + return _editMode.value + default: + return undefined + } + } + + const activeTool = computed({ + get: () => _activeTool.value, + set: (value) => { + _activeTool.value = value + if (_activeTool.value) { + reinitializeInteractions() + } else { + interactionManager?.removeAllInteractions() + } + }, + }) + + const drawMode = computed({ + get: () => _drawMode.value, + set: (value) => { + _drawMode.value = value + if (activeTool.value !== 'draw') { + activeTool.value = 'draw' + } else { + reinitializeInteractions() + } + }, + }) + + const drawOptions: ComputedRef = computed( + () => + activeLayerConfig.value?.selectableDrawModes ?? [ + 'Point', + 'LineString', + 'Polygon', + ] + ) + + watch(drawOptions, (newOptions) => { + if (!newOptions.includes(_drawMode.value) && newOptions[0]) { + _drawMode.value = newOptions[0] + } + }) + + const editMode = computed({ + get: () => _editMode.value, + set: (value) => { + _editMode.value = value + if (activeTool.value !== 'edit') { + activeTool.value = 'edit' + } else { + reinitializeInteractions() + } + }, + }) + + const measureMode = computed({ + get: () => _measureMode.value, + set: (value) => { + _measureMode.value = value + if (activeTool.value === 'draw') { + reinitializeInteractions() + } + }, + }) + + const measureOptions = computed(() => + MeasureModes.reduce<{ value: MeasureMode; label: string }[]>( + (accumulator, mode) => { + const isArea = drawMode.value.includes('Polygon') + if (!isArea && mode === 'hectares') { + return accumulator + } + accumulator.push({ value: mode, label: mode + (isArea ? 'Area' : '') }) + return accumulator + }, + [] + ) + ) + + watch(measureOptions, (newOptions) => { + if ( + !newOptions.map((option) => option.value).includes(_measureMode.value) + ) { + _measureMode.value = newOptions[0]?.value ?? 'none' + } + }) + + const textInput = computed({ + get: () => _textInput.value, + set: (value) => { + _textInput.value = value + if (activeTool.value === 'draw' && drawMode.value === 'Text') { + reinitializeInteractions() + } else if (textIsSelected.value) { + const style = _selectedFeature.value?.getStyle() + if (style instanceof Style) { + const text = style.getText() + if (text) { + text.setText(value) + _selectedFeature.value?.setStyle(style) + } + } + } + }, + }) + + const textSizes = computed( + // @ts-expect-error | exactly, we want undefined then ദ്ദി(˵ •̀ ᴗ - ˵ ) ✧ + () => activeLayerConfig.value?.textStyle?.font?.size ?? [] + ) + + const textSizeIndex = computed({ + get: () => (textSizes.value.length > 0 ? _textSizeIndex.value : -1), + set: (value) => { + _textSizeIndex.value = value + if (activeTool.value === 'draw' && drawMode.value === 'Text') { + reinitializeInteractions() + } else if (textIsSelected.value) { + const style = _selectedFeature.value?.getStyle() + if (style instanceof Style) { + const text = style.getText() + if (text) { + text.setFont( + text + .getFont() + ?.replace(/\d+px/, `${textSizes.value[value] ?? 12}px`) + ) + _selectedFeature.value?.setStyle(style) + } + } + } + }, + }) + + const selectedFeature = computed({ + get: () => _selectedFeature.value, + set: (value) => { + _selectedFeature.value = value + + if (value === null) { + textInput.value = '' + textSizeIndex.value = 0 + } else { + const style = value.getStyle() + if (style instanceof Style) { + const text = style.getText() + if (text) { + const textValue = text.getText() + // NOTE: no rich text in this tool, formatting ignored + textInput.value = Array.isArray(textValue) + ? textValue.filter((_, index) => index % 2 === 0).join('') + : (textValue ?? '') + const fontSize = text.getFont()?.match(/(\d+)px/)?.[1] + if (fontSize) { + const index = textSizes.value.findIndex( + (size) => size === parseInt(fontSize, 10) + ) + textSizeIndex.value = index !== -1 ? index : 0 + } + } + } + } + }, + }) + + const textIsSelected = computed(() => { + const style = _selectedFeature.value?.getStyle() + return style instanceof Style && Boolean(style.getText()) + }) + + const activeLassoIds = computed(() => + (activeLayerConfig.value?.lassos || []).reduce( + (accumulator, { id, minZoom = true }) => { + const layerConfig = coreStore.configuration.layers.find( + (layer) => id === layer.id + ) + if ( + minZoom && + layerConfig && + typeof layerConfig.minZoom !== 'undefined' && + coreStore.zoom < layerConfig.minZoom + ) { + return accumulator + } + accumulator.push(id) + return accumulator + }, + [] + ) + ) + + const downloadFormat = computed({ + get: () => _downloadFormat.value, + set: (value: DownloadMode) => { + _downloadFormat.value = value + }, + }) + + function download() { + const blob = new Blob([JSON.stringify(featureCollection.value)], { + type: 'application/geo+json', + }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.download = 'polar.geojson' + link.href = url + link.style.display = 'none' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } + + function upload() { + const drawSource = interactionManager?.getDrawSource() + if (!drawSource) { + // TODO: notifyUser to select a draw source + return + } + const input = document.createElement('input') + input.type = 'file' + input.accept = '.geojson,application/geo+json,application/json' + input.style.display = 'none' + input.addEventListener('change', () => { + const file = input.files?.[0] + if (!file) { + return + } + const reader = new FileReader() + reader.addEventListener('load', () => { + try { + featureCollectionToDrawSource( + JSON.parse( + reader.result as string + ) as FeatureCollection, + drawSource + ) + // value then goes to ref featureCollection by interactionManager callback + const extent = drawSource.getExtent() + if (extent) { + map.value.getView().fit(extent, { duration: 500 }) + } + } catch (e: unknown) { + console.error(e) + } + }) + reader.readAsText(file) + }) + document.body.appendChild(input) + input.click() + document.body.removeChild(input) + } + + function addFeatures( + featureCollection: FeatureCollection, + overwrite = false + ) { + const drawSource = interactionManager?.getDrawSource() + if (!drawSource) { + // TODO: notifyUser to select a draw source + return + } + if (overwrite) { + drawSource.clear() + } + featureCollectionToDrawSource(featureCollection, drawSource) + } + + const featureCollection = computed({ + get: () => _featureCollection.value, + set: (value: FeatureCollection) => { + _featureCollection.value = value + if (activeLayerConfig.value?.revision) { + revisionStateFlag.value = inProgress + reviseFeatures(map.value, activeLayerConfig.value.revision, value) + .then((oneRevvedBoi) => { + if (oneRevvedBoi !== null) { + revisedFeatureCollection.value = oneRevvedBoi + revisionStateFlag.value = complete + } + }) + .catch((e: unknown) => { + if (!( + // already handled + e instanceof Error && e.message.startsWith('Autofix failed:') + )) { + console.error(e) + } + revisionStateFlag.value = error + }) + } + }, + }) + + function setupPlugin() { + let syntheticDrawLayerId = 0 + + let layers = configuration.value.layers + + // Don't trust configuration, not even your own. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!layers || layers.length === 0) { + layers = [{}] + } + + layers.forEach((layerConfig) => { + if (layerConfig.id) { + const layer = map.value + .getLayers() + .getArray() + .find((l) => l.get('id') === layerConfig.id) + + if (layer) { + _layers.value.push(layer) + _layerIds.value.push(layerConfig.id) + return + } + } + + const localLayer = createDrawLayer( + layerConfig.id ?? `draw-layer-${++syntheticDrawLayerId}`, + layerConfig.style + ) + + map.value.addLayer(localLayer) + _layers.value.push(localLayer) + _layerIds.value.push(localLayer.get('id')) + }) + + activeLayerId.value = _layerIds.value[0] ?? '' + interactionManager = new InteractionManager( + map.value, + configuration.value, + activeLayerId.value, + featureCollectionUpdater + ) + } + + function teardownPlugin() { + interactionManager?.destructor() + // TODO: check for completeness + } + + return { + activeTool, + drawMode, + measureMode, + drawOptions, + editMode, + downloadFormat, + + activeLayerId, + configuration, + layerOptions, + measureOptions, + textInput, + textSizeIndex, + textSizes, + textIsSelected, + + /** True while the user is within a drawing procedure and has a half-baked geometry at hand. */ + drawing, + + /** Indicator of what's going on. */ + revisionStateFlag, + + /** GeoJSON.FeatureCollection containing the currently drawn features. */ + featureCollection, + + /** If revision is configured, this will contain the revision result. Please use the revisionStateFlag to decide if the process is done. */ + revisedFeatureCollection, + + /** + * Calling the action `addFeatures` expects an object containing a [GeoJSON](https://geojson.org/) FeatureCollection. It adds the given features from the FeatureCollection to the drawn source. It is also possible to completely overwrite them using the parameter `overwrite`. The feature type "GeometryCollection" is not supported. + * It's important to note that the GeoJSON Standard [RFC7946](https://www.rfc-editor.org/rfc/rfc7946) does not support circles. To add a circle to the map, it is assumed, that a feature being a circle has a property `radius` together with a point geometry. + * The same goes for text. In that case, a property 'text' with an accompanying 'textFont' property is expected, where 'textFont' matches e.g. "30px sans-serif". The size should match a valid configurable size. + */ + addFeatures, + + /** GeoJSON.FeatureCollection goes woo ↓ */ + download, + + /** GeoJSON.FeatureCollection goes wee ↑ */ + upload, + + /** @internal */ + setupPlugin, + + /** @internal */ + teardownPlugin, + } +}) + +// TODO: hot reloading fails a lot +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useDrawStore, import.meta.hot)) +} diff --git a/src/plugins/draw/types/common.ts b/src/plugins/draw/types/common.ts new file mode 100644 index 0000000000..c1f5d91c23 --- /dev/null +++ b/src/plugins/draw/types/common.ts @@ -0,0 +1,40 @@ +/** + * TODO: document for configuration + * @defaultValue `['Point', 'LineString', 'Polygon']` + */ +export const DrawModes = [ + 'Point', + 'LineString', + 'Polygon', + 'Circle', + 'Text', + // TODO: handling requires distinction when a multi is 'active'/'finished' + // 'MultiLineString', + // 'MultiPoint', + // 'MultiPolygon', +] as const + +/** + * TODO: document for configuration + * TODO: document defaultValue + */ +export interface DrawFontStyle { + family: string + size: number[] +} + +/** + * TODO: document for configuration + * TODO: document defaultValue + */ +export interface DrawTextStyle { + font?: string | DrawFontStyle + textColor?: string +} + +/** + * Supported geometry types for draw tool mode. + * Please mind that 'Text' and 'Circle' do not transfer to GeoJSON, and hence will only work in clients sharing the opinionated formatting of POLAR. + * To avoid this issue, do not configure these properties. + */ +export type DrawMode = (typeof DrawModes)[number] diff --git a/src/plugins/draw/types/configuration.ts b/src/plugins/draw/types/configuration.ts new file mode 100644 index 0000000000..8780f5a1a5 --- /dev/null +++ b/src/plugins/draw/types/configuration.ts @@ -0,0 +1,202 @@ +import type { PluginOptions } from '@/core' +import type { DrawMode, DrawTextStyle } from './common' + +/** + * Lasso definition to describe which layers to copy geometries from. + */ +export interface DrawPluginOptionsLayerLasso { + /** + * ID of the layer to lasso from. The layer must be a vector layer from the + * service register. + */ + id: string + + /** + * If given, the lasso will only be active when minimally this zoom level is + * used. This is to prevent overly copying geometries from layers with many + * geometries that may not load on the current zoom level. + * @defaultValue `undefined` + */ + minZoom?: number +} + +export type DrawPluginOptionsMetaServiceAggregationMode = 'unequal' | 'all' + +/** + * From all geometries of the service intersecting our geometries, properties are aggregated. + */ +export interface DrawPluginOptionsMetaService { + /** + * Id of the vector layer to make use of in the meta service. + */ + id: string + + /** + * Defaults to `'unequal'`. In mode `'unequal'`, one of each property set is kept; duplicate property sets are dropped. In mode `'all'`, all property sets are kept without further filtering. + */ + aggregationMode?: DrawPluginOptionsMetaServiceAggregationMode + + /** + * Names of the properties to build aggregations from. If left undefined, all found properties will be used. + */ + propertyNames?: string[] +} + +/** + * Controls a revision step that will produce a secondary output to the plugin, `revisedFeatureCollection`. + */ +export interface DrawPluginOptionsLayerRevision { + /** + * If `true`, an automatic attempt at repairing the given geometries is executed regarding fulfillment of the OGC Simple Feature Specification (part of [SFA](https://www.ogc.org/de/publications/standard/sfa/)). Defaults to `false`. + */ + autofix?: boolean + + /** + * If `true`, geometries will be merged into MultiGeometries where applicable. + * Defaults to `false`. + */ + mergeToMultiGeometries?: boolean + + /** + * Specification of meta services that are requested with the spatial position of each geometry. + */ + metaServices?: DrawPluginOptionsMetaService[] + + /** + * If `true`, a `sfaValidity` flag is added to each feature's attributes that indicates whether the geometry is valid respective the OGC Simple Feature Specification (part of [SFA](https://www.ogc.org/de/publications/standard/sfa/)). This will override any other `sfaValidity` property. Defaults to `false`. + */ + validate?: boolean +} + +/** + * TODO: document (This is a magic object like in @polar/client-style-preview. Currently missing docs, this should probably somehow reference OpenLayers since it heavily borrows from there.) + * TODO: document defaultValue + * + * Example: + * ``` + * { + * fill: { + * color: 'rgba(255, 255, 255, 0.5)' + * }, + * stroke: { + * color: '#e51313', + * width: 2 + * }, + * circle: { + * radius: 7, + * fillColor: '#e51313' + * }, + * // Styling for text of measurements; supports everything described at https://openlayers.org/en/v9.2.4/apidoc/module-ol_style_Text-Text.html + * measure: { + * font: '16px sans-serif', + * placement: 'line', + * fill: new Fill({ color: 'black' }), + * stroke: new Stroke({ color: 'black' }), + * offsetY: -5 + * } + * } + * ``` + */ +export interface DrawPluginOptionsLayerStyle { + [key: string]: unknown +} + +/** + * Specification of a layer to work on in the draw plugin. + */ +export interface DrawPluginOptionsLayer { + /** + * ID of the layer to work on. + * If this ID is contained in the services register, that layer must be + * a vector layer, and its features will be worked on. + * If this ID is not contained in the services register, a new vector layer + * with this ID will be created. + * If no ID is given, an ID will be generated that fits the prior case. + * @defaultValue A custom ID will be created + */ + id?: string + + /** + * Lassos allow the user to copy geometries from other layers onto the + * currently chosen draw layer. + * @defaultValue `undefined` + */ + lassos?: DrawPluginOptionsLayerLasso[] + + /** + * Display name for layer. This is only used when multiple draw layers exist, + * and is used to offer the user to choose between them. + * TODO: check if it's used + * @defaultValue Arbitrary name based on `id` + */ + name?: string + + /** + * TODO: document + */ + revision?: DrawPluginOptionsLayerRevision + + /** + * Allowed drawing modes. + * @defaultValue `['Point', 'LineString', 'Polygon']` + */ + selectableDrawModes?: DrawMode[] + + /** + * Which layers to snap to when drawing. + * Entries must be IDs of vector layers in the service register. + * Drawing layer will always snap to themselves. + * @defaultValue `[]` + */ + snapTo?: string[] + + /** + * Used as default style for all drawn geometries. + * TODO: document defaultValue + */ + style?: DrawPluginOptionsLayerStyle + + /** + * Determines font and font color for the text drawn. + * You may offer multiple options for text sizes. + * TODO: document defaultValue + */ + textStyle?: DrawTextStyle +} + +/** + * Plugin options for draw plugin. + */ +export interface DrawPluginOptions extends PluginOptions { + /** + * TODO: determine default value, there's probably something basic to provide + * @defaultValue [] + */ + layers: DrawPluginOptionsLayer[] + + /** + * Shows a loader during saving and loading. + * TODO: unused, either start using or remove + * @defaultValue true + */ + showLoader?: boolean + + /** + * Shows measure options for drawing lines and polygons. + * @defaultValue false + */ + showMeasure?: boolean + + /** + * Shows styling options for drawing geometries. + * @defaultValue false + */ + showStyling?: boolean + + /** + * Shows toast messages for effects. + * TODO: unused, either start using or remove + * @defaultValue true + */ + showToasts?: boolean +} diff --git a/src/plugins/draw/types/index.ts b/src/plugins/draw/types/index.ts new file mode 100644 index 0000000000..d286560aee --- /dev/null +++ b/src/plugins/draw/types/index.ts @@ -0,0 +1,3 @@ +export * from './configuration' +export * from './internal' +export * from './common' diff --git a/src/plugins/draw/types/internal.ts b/src/plugins/draw/types/internal.ts new file mode 100644 index 0000000000..4b85904beb --- /dev/null +++ b/src/plugins/draw/types/internal.ts @@ -0,0 +1,73 @@ +import type { Geometry, GeometryCollection } from 'geojson' +import type { Feature as OlFeature } from 'ol' +import type VectorSource from 'ol/source/Vector' +import type { StyleLike } from 'ol/style/Style' +import type { Ref, WritableComputedRef } from 'vue' +import type { DrawTextStyle } from './common' + +/** + * Plugin identifier. + */ +export const PluginId = 'draw' + +/** + * Modes in that the draw plugin can be used. + */ +export const ToolModes = ['draw', 'edit', 'delete'] as const + +/** + * Supported editing modes for edit tool mode. + * Please mind that `lasso` requires additional plugin configuration to work. + */ +export const EditModes = [ + 'modify', + 'translate', + 'duplicate', + 'cut', + 'merge', + 'lasso', +] as const + +export const DownloadModes = ['geojson'] as const + +export const MeasureModes = [ + 'none', + 'metres', + 'kilometres', + 'hectares', +] as const + +export type ToolMode = (typeof ToolModes)[number] +export type EditMode = (typeof EditModes)[number] +export type DownloadMode = (typeof DownloadModes)[number] +export type MeasureMode = (typeof MeasureModes)[number] + +export interface InteractionOptions { + drawing: Ref + selectedFeature: WritableComputedRef + activeLassoIds?: string[] + measureMode?: MeasureMode + text?: { + textInput: string + textSize: number + textStyle: DrawTextStyle + } +} + +export type GeometryType = Exclude + +/** + * The options that can be given to an ol/VectorLayer. + * TODO: Somehow the direct import from ol doesn't work. This is a copy with the things that we currently use. Fix me if you can. + */ +export interface PolarVectorOptions { + source?: VectorSource + style?: StyleLike +} + +export const inactive = 'inactive' +export const inProgress = 'inProgress' +export const complete = 'complete' +export const error = 'error' +export type RevisionStateFlag = + typeof inactive | typeof inProgress | typeof complete | typeof error diff --git a/src/plugins/draw/utils/conversion/drawSourceToFeatureCollection.ts b/src/plugins/draw/utils/conversion/drawSourceToFeatureCollection.ts new file mode 100644 index 0000000000..3ce5d57c39 --- /dev/null +++ b/src/plugins/draw/utils/conversion/drawSourceToFeatureCollection.ts @@ -0,0 +1,65 @@ +import type { Feature, FeatureCollection } from 'geojson' +import type { Circle, LineString, Point, Polygon } from 'ol/geom' +import type VectorSource from 'ol/source/Vector' +import type { Ref } from 'vue' +import type { GeometryType } from '../../types' + +import { Style } from 'ol/style' + +export function drawSourceToFeatureCollection( + drawSource: VectorSource, + featureCollection: Ref> +) { + const features = drawSource.getFeatures().map((feature) => { + const geometry = feature.getGeometry() as + Circle | LineString | Point | Polygon + const type = geometry.getType() + const style = feature.getStyle() + const text = + type === 'Point' && style instanceof Style ? style.getText() : undefined + const isCircle = type === 'Circle' + + const jsonFeature: Feature = { + type: 'Feature', + properties: Object.fromEntries( + Object.entries(feature.getProperties()).filter( + ([property]) => property !== 'geometry' + ) + ), + geometry: { + // @ts-expect-error | A LinearRing can currently not be drawn + type: isCircle ? 'Point' : type, + // @ts-expect-error | The coordinates are in the correct format + coordinates: isCircle + ? (geometry as Circle).getCenter() + : geometry.getCoordinates(), + }, + } + + // TODO: the radius property key has always been used like that, but the usage isn't safe as soon as we allow more properties / external layers. We shall then probably use _polarkeyboardcat as a prefix and hope for the best. + if (jsonFeature.properties) { + if (isCircle) { + jsonFeature.properties.radius = (geometry as Circle).getRadius() + } + + if (text) { + jsonFeature.properties.text = text.getText() + jsonFeature.properties.textFont = text.getFont() + } + + if (style) { + // TODO: reverse jsonStyleMapper on style (difficulty level: archmage) + // jsonFeature.properties.style = {} + } + + // TODO: measurement lines/polygons must be saved with measurements, should work in the same style as it's done for text + } + + return jsonFeature + }) + + featureCollection.value = { + ...featureCollection.value, + features: features as Feature[], + } +} diff --git a/src/plugins/draw/utils/conversion/featureCollectionToDrawSource.ts b/src/plugins/draw/utils/conversion/featureCollectionToDrawSource.ts new file mode 100644 index 0000000000..c2973f8dbe --- /dev/null +++ b/src/plugins/draw/utils/conversion/featureCollectionToDrawSource.ts @@ -0,0 +1,56 @@ +import type { FeatureCollection, Point } from 'geojson' +import type Feature from 'ol/Feature' +import type VectorSource from 'ol/source/Vector' +import type { GeometryType } from '../../types' + +import GeoJSON from 'ol/format/GeoJSON' +import { Circle } from 'ol/geom' + +import { createTextStyle } from '../createTextStyle' + +export function featureCollectionToDrawSource( + featureCollection: FeatureCollection, + drawSource: VectorSource +) { + const features = featureCollection.features.map((feature) => { + const style = feature.properties?.style + const isCircle = + feature.properties && typeof feature.properties.radius !== 'undefined' + const text = feature.properties?.text + const textFont = feature.properties?.textFont + + const olFeature = new GeoJSON().readFeature(feature) as Feature + + if (isCircle) { + olFeature.setGeometry( + new Circle( + (feature.geometry as Point).coordinates, + // Known due to isCircle. TypeScript, you should be able to solve this. + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + feature.properties!.radius as number + ) + ) + } + + if (style) { + // relevant when feature styling is implemented (see reverse function) + // import { buildStyleParameters } from '@/lib/jsonStyleMapper' + // olFeature.setStyle(buildStyleParameters(style)) + } + + if (text) { + const textStyle = createTextStyle(text) + if (textFont) { + textStyle.getText()?.setFont(textFont) + } + + olFeature.setStyle(textStyle) + } + + // TODO: measurement lines/polygons must be restored with measurements, should work in the same fashion as for text + + return olFeature + }) + + drawSource.addFeatures(features) +} diff --git a/src/plugins/draw/utils/createMeasureStyle.ts b/src/plugins/draw/utils/createMeasureStyle.ts new file mode 100644 index 0000000000..73b32a4ad2 --- /dev/null +++ b/src/plugins/draw/utils/createMeasureStyle.ts @@ -0,0 +1,166 @@ +import type { Feature } from 'ol' +import type { Projection } from 'ol/proj' +import type { Options, StyleFunction } from 'ol/style/Style' +import type { Options as TextOptions } from 'ol/style/Text' +import type { DrawPluginOptionsLayerStyle, MeasureMode } from '../types' + +import centerOfMass from '@turf/center-of-mass' +import { LineString, Point, Polygon } from 'ol/geom' +import { getArea, getLength } from 'ol/sphere' +import { Circle as CircleStyle, Fill, Stroke } from 'ol/style' +import Style from 'ol/style/Style' +import Text from 'ol/style/Text' + +const roundMeasurement = (measurement: number, divisor: number) => + Math.round((measurement * 100) / divisor + Number.EPSILON) / 100 + +function calculatePartialDistances( + styles: Style[], + styleOptions: Options, + textOptions: TextOptions, + feature: Feature, + unit: 'm' | 'km', + projection: Projection +) { + const geometry = feature.getGeometry() as LineString | Polygon + const coordinates = + geometry instanceof Polygon + ? geometry.getCoordinates()[0] + : geometry.getCoordinates() + + if (!coordinates) { + return styles + } + + for (let i = 1; i < coordinates.length; i++) { + const lineString = new LineString([coordinates[i - 1], coordinates[i]]) + const lengthInMetres = getLength(lineString, { + projection, + }) + const length = roundMeasurement(lengthInMetres, unit === 'km' ? 1000 : 1) + const text = `${length} ${unit}` + feature.set(`length-${i}`, roundMeasurement(lengthInMetres, 1)) + const style = new Style({ + ...styleOptions, + text: new Text({ + ...textOptions, + text, + }), + }) + style.setGeometry(lineString) + styles.push(style) + } + // This only happens once the drawing has been finished + if ( + Object.keys(feature.getProperties()).filter((key) => + key.startsWith('length-') + ).length === coordinates.length + ) { + feature.unset(`length-${coordinates.length}`) + } + + return styles +} + +function getAreaUnitAndDivisor(measureMode: Exclude) { + let areaUnit = '' + let divisor: number + if (measureMode === 'metres') { + areaUnit = 'm²' + divisor = 1 + } else if (measureMode === 'kilometres') { + areaUnit = 'km²' + divisor = 1000000 + } else { + areaUnit = 'ha' + divisor = 10000 + } + return { areaUnit, divisor } +} + +const measureStyle: ( + styleOptions: Options, + measureMode: Exclude, + projection: Projection, + measureStyleOptions?: TextOptions +) => StyleFunction = + (styleOptions, measureMode, projection, measureStyleOptions) => (feature) => { + const geometry = feature.getGeometry() + if (geometry instanceof Polygon || geometry instanceof LineString) { + const styles = [new Style(styleOptions)] + const textOptions: TextOptions = { + font: '16px sans-serif', + placement: 'line', + fill: new Fill({ color: 'black' }), + stroke: new Stroke({ color: 'black' }), + offsetY: -5, + ...measureStyleOptions, + } + if (geometry instanceof Polygon) { + const { areaUnit, divisor } = getAreaUnitAndDivisor(measureMode) + const areaInMetres = getArea(geometry, { projection }) + const area = roundMeasurement(areaInMetres, divisor) + const text = `${area} ${areaUnit}` + const style = new Style({ + text: new Text({ + ...textOptions, + placement: 'point', + text, + }), + }) + // @ts-expect-error | Features in this StyleFunction are always of type Feature + feature.set('area', roundMeasurement(areaInMetres, 1)) + style.setGeometry( + new Point( + centerOfMass({ + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: geometry.getCoordinates(), + }, + }).geometry.coordinates + ) + ) + styles.push(style) + } + return calculatePartialDistances( + styles, + styleOptions, + textOptions, + feature as Feature, + measureMode === 'metres' ? 'm' : 'km', + projection + ) + } + return new Style(styleOptions) + } + +// TODO: this was createDrawStyle; it is createMeasureStyle now. much of the code is useless and can be removed. Proposal: Instead of overriding all styles, only inject text styles onto the existing/configured style. +export function createMeasureStyle( + strokeColor: string, + measureMode: Exclude, + projection: Projection, + drawStyle?: DrawPluginOptionsLayerStyle +): Style | StyleFunction { + const defaultFillColor = 'rgba(255, 255, 255, 0.5)' + const fillColor = drawStyle?.fill?.color + ? drawStyle.fill.color + : defaultFillColor + const styleOptions: Options = { + image: new CircleStyle({ + radius: 5, + fill: new Fill({ + color: fillColor, + }), + stroke: new Stroke({ color: strokeColor }), + }), + stroke: new Stroke({ + color: strokeColor, + width: drawStyle?.stroke?.width || 2, + }), + fill: new Fill({ + color: fillColor, + }), + } + return measureStyle(styleOptions, measureMode, projection, drawStyle?.measure) +} diff --git a/src/plugins/draw/utils/createTextStyle.ts b/src/plugins/draw/utils/createTextStyle.ts new file mode 100644 index 0000000000..e5a6ccb38f --- /dev/null +++ b/src/plugins/draw/utils/createTextStyle.ts @@ -0,0 +1,38 @@ +import type { DrawTextStyle } from '../types' + +import { Circle as CircleStyle, Fill, Style, Text } from 'ol/style' + +export function createTextStyle( + drawText: string, + textStyle?: DrawTextStyle, + textSize?: number +) { + return new Style({ + image: new CircleStyle({ + radius: 5, + fill: new Fill({ color: '#ffcc3300' }), + }), + text: new Text({ + textAlign: 'center', + text: drawText, + font: createFont(textStyle, textSize), + fill: textStyle?.textColor + ? new Fill({ color: textStyle.textColor }) + : undefined, + textBaseline: 'middle', + }), + }) +} + +function createFont(textStyle?: DrawTextStyle, textSize?: number) { + if (typeof textStyle === 'undefined') { + // if empty string apply default open layers style + return '' + } + const { font } = textStyle + if (typeof font === 'string') { + return font + } + const fontFamily: string = font?.family ?? 'sans-serif' + return String(textSize ?? 12) + 'px ' + fontFamily +} diff --git a/src/plugins/draw/utils/drawLayer.ts b/src/plugins/draw/utils/drawLayer.ts new file mode 100644 index 0000000000..489557c492 --- /dev/null +++ b/src/plugins/draw/utils/drawLayer.ts @@ -0,0 +1,26 @@ +import type { DrawPluginOptionsLayerStyle, PolarVectorOptions } from '../types' + +import VectorLayer from 'ol/layer/Vector' +import VectorSource from 'ol/source/Vector' +import { Style } from 'ol/style' + +import { buildStyleParameters } from '@/lib/jsonStyleMapper' + +/** + * Creates a new VectorLayer. + * If certain style parameters, they are used instead of the default styling. + * + * @returns VectorLayer for the drawn features to reside in. + */ +export function createDrawLayer( + id: string, + style?: DrawPluginOptionsLayerStyle +) { + const options: PolarVectorOptions = { source: new VectorSource() } + if (style) { + options.style = new Style(buildStyleParameters(style)) + } + const layer = new VectorLayer(options) + layer.set('id', id) + return layer +} diff --git a/src/plugins/draw/utils/icons.ts b/src/plugins/draw/utils/icons.ts new file mode 100644 index 0000000000..876968891c --- /dev/null +++ b/src/plugins/draw/utils/icons.ts @@ -0,0 +1,46 @@ +import type { DrawMode, EditMode } from '../types' + +export const drawModeIcon = (drawMode: DrawMode) => { + switch (drawMode) { + case 'Point': + // case 'MultiPoint': + return 'kern-icon kern-icon--scatter-plot' + case 'LineString': + // case 'MultiLineString': + return 'kern-icon kern-icon--polyline' + case 'Polygon': + // case 'MultiPolygon': + return 'kern-icon kern-icon--hexagon' + case 'Circle': + return 'kern-icon kern-icon--app-badging' + case 'Text': + return 'kern-icon kern-icon--insert-text' + default: + return 'kern-icon kern-icon--brush' + } +} + +export const editModeIcon = (editMode: EditMode) => { + switch (editMode) { + case 'modify': + return 'kern-icon kern-icon--edit' + case 'translate': + return 'kern-icon kern-icon--open-with' + case 'duplicate': + return 'kern-icon kern-icon--content-copy' + case 'cut': + return 'kern-icon kern-icon--content-cut' + case 'lasso': + return 'kern-icon kern-icon--lasso-select' + case 'merge': + return 'kern-icon kern-icon--merge' + default: + return 'kern-icon kern-icon--edit' + } +} + +export const deleteIcon = 'kern-icon kern-icon--delete' + +export const downloadIcon = 'kern-icon kern-icon--download' + +export const uploadIcon = 'kern-icon kern-icon--upload' diff --git a/src/plugins/draw/utils/interactionManager/createDeleteInteractions.ts b/src/plugins/draw/utils/interactionManager/createDeleteInteractions.ts new file mode 100644 index 0000000000..b86d89f74e --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createDeleteInteractions.ts @@ -0,0 +1,62 @@ +import type { Map } from 'ol' +import type Interaction from 'ol/interaction/Interaction' +import type VectorLayer from 'ol/layer/Vector' +import type VectorSource from 'ol/source/Vector' + +import { platformModifierKeyOnly } from 'ol/events/condition' +import { DragBox, Select } from 'ol/interaction' + +// TODO: pointer cursor doesn't consequently work atm (purely cosmetical) +const pointerStyle = (map: Map, drawLayer: VectorLayer) => (e) => { + const found = map.hasFeatureAtPixel(e.pixel, { + layerFilter: (l) => l === drawLayer, + }) + + if (found) { + map.getTargetElement().setAttribute('style', 'cursor: pointer') + } else { + map.getTargetElement().setAttribute('style', '') + } +} + +export default function ( + map: Map, + drawLayer: VectorLayer, + drawSource: VectorSource +): Interaction[] { + const selectInteraction = new Select({ layers: [drawLayer] }) + const selectedFeatures = selectInteraction.getFeatures() + const dragBoxInteraction = new DragBox({ + condition: platformModifierKeyOnly, + }) + + const boundPointerStyle = pointerStyle(map, drawLayer) + map.on('pointermove', boundPointerStyle) + + dragBoxInteraction.on('boxend', () => { + const extent = dragBoxInteraction.getGeometry().getExtent() + selectedFeatures.extend( + drawSource + .getFeaturesInExtent(extent) + .filter((feature) => feature.getGeometry()?.intersectsExtent(extent)) + ) + }) + + selectedFeatures.on(['add'], () => { + selectedFeatures.forEach((feature) => { + drawSource.removeFeature(feature) + }) + selectedFeatures.clear() + }) + + // @ts-expect-error | internal hack to detect it in other plugins + selectInteraction._isDeleteSelect = true + + // @ts-expect-error | local piggyback + selectInteraction._onRemove = () => { + map.un('pointermove', boundPointerStyle) + map.getTargetElement().setAttribute('style', '') + } + + return [selectInteraction, dragBoxInteraction] +} diff --git a/src/plugins/draw/utils/interactionManager/createDrawInteractions.ts b/src/plugins/draw/utils/interactionManager/createDrawInteractions.ts new file mode 100644 index 0000000000..0e1a6fd818 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createDrawInteractions.ts @@ -0,0 +1,120 @@ +import type { Map } from 'ol' +import type Interaction from 'ol/interaction/Interaction' +import type VectorSource from 'ol/source/Vector' +import type { Ref } from 'vue' +import type { + DrawMode, + DrawPluginOptions, + DrawTextStyle, + InteractionOptions, + MeasureMode, +} from '../../types' + +import { Draw, Snap } from 'ol/interaction' + +import { createMeasureStyle } from '../createMeasureStyle' +import { createTextStyle } from '../createTextStyle' +import { getSnaps } from './getSnaps' + +function createTextDraw( + textInput: string, + textSize: number, + drawSource: VectorSource, + textStyle?: DrawTextStyle +) { + const _textStyle = createTextStyle(textInput, textStyle, textSize) + const draw = new Draw({ + source: drawSource, + type: 'Point', + style: _textStyle, + }) + // @ts-expect-error | internal hack to detect it in @polar/plugin-pins and @polar/plugin-gfi + draw._isDrawPlugin = true + draw.on('drawend', function (e) { + e.feature.setStyle(_textStyle) + e.feature.set('text', textInput) + }) + // prevent the creation of empty text features + drawSource.on('addfeature', (event) => { + if (event.feature?.get('text') && !event.feature.get('text').trim()) { + drawSource.removeFeature(event.feature) + } + }) + + return draw +} + +export function createDrawInteraction( + configuration: DrawPluginOptions['layers'][number], + drawMode: DrawMode, + measureMode: MeasureMode, + drawSource: VectorSource, + map: Map, + text: InteractionOptions['text'], + drawing: Ref +): Interaction[] { + if ( + drawMode === 'Text' && + (typeof text === 'undefined' || !text.textInput.trim()) + ) { + return [] + } + + const makeMeasurement = + measureMode !== 'none' && + (drawMode.includes('Polygon') || drawMode.includes('LineString')) + + const style = makeMeasurement + ? createMeasureStyle( + '#000000', + measureMode, + map.getView().getProjection(), + configuration.style + ) + : undefined + + const draw = + drawMode === 'Text' + ? createTextDraw( + // TS doesn't infer from check at method start + // TODO: This can be removed with refactoring through all layers. However, it adds zero value. + /* eslint-disable @typescript-eslint/no-non-null-assertion */ + text!.textInput, + text!.textSize, + drawSource, + text!.textStyle + /* eslint-enable @typescript-eslint/no-non-null-assertion */ + ) + : new Draw({ + source: drawSource, + type: drawMode, + style, + }) + + draw.on('drawstart', () => { + drawing.value = true + }) + + draw.on('drawend', () => { + drawing.value = false + }) + + draw.on('drawabort', () => { + drawing.value = false + }) + + // @ts-expect-error | internal hack to detect it in other plugins + draw._isDrawPlugin = true + + if (makeMeasurement) { + draw.on('drawend', (e) => { + e.feature.setStyle(style) + }) + } + + return [ + draw, + ...getSnaps(map, configuration.snapTo ?? []), + new Snap({ source: drawSource }), + ] +} diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/cutlery.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/cutlery.ts new file mode 100644 index 0000000000..0434c5ca51 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/cutlery.ts @@ -0,0 +1,132 @@ +import type { + Feature as GeoJsonFeature, + LineString as GeoJsonLineString, + MultiPolygon as GeoJsonMultiPolygon, + Polygon as GeoJsonPolygon, +} from 'geojson' +import type Feature from 'ol/Feature' +import type VectorSource from 'ol/source/Vector' +import type { ProjectionInfo } from './types' + +import { booleanPointInPolygon } from '@turf/boolean-point-in-polygon' +import { buffer } from '@turf/buffer' +import { difference } from '@turf/difference' +import { featureCollection, polygon } from '@turf/helpers' +import { lineIntersect } from '@turf/line-intersect' +import { unkinkPolygon } from '@turf/unkink-polygon' +import { GeoJSON } from 'ol/format' + +const polygonTypes = ['Polygon', 'MultiPolygon'] +export const converter = new GeoJSON() + +const cutCuttableWithCutter = ( + cuttable: GeoJsonFeature, + cutter: GeoJsonFeature +): GeoJsonFeature[] => { + // error by less than 4 water molecule widths deemed acceptable for surveying + const cutterAsPolygon = buffer(cutter, 0.000001, { + units: 'millimeters', + }) as GeoJsonFeature + const cuts = difference(featureCollection([cuttable, cutterAsPolygon])) + + if (cuts === null) { + const errorText = 'Cutting resulted in geometry destruction.' + console.error( + '@polar/plugin-draw: ', + errorText, + 'cuttable: ', + cuttable, + 'cutter: ', + cutter + ) + throw new Error(errorText) + } + + if (cuts.geometry.type === 'MultiPolygon') { + return cuts.geometry.coordinates + .map((coordinates) => polygon(coordinates)) + .map((cuttable) => cutCuttableWithCutter(cuttable, cutter)) + .flat(1) + } + + return [cuts as GeoJsonFeature] +} + +export const cutCuttablesWithCutter = ( + cuttables: GeoJsonFeature[], + cutter: GeoJsonFeature +): GeoJsonFeature[] => { + const unkinkedCuttables = cuttables.reduce[]>( + (accumulator, current) => { + accumulator.push(...unkinkPolygon(current).features) + return accumulator + }, + [] + ) + + return unkinkedCuttables + .map((cuttable) => cutCuttableWithCutter(cuttable, cutter)) + .flat(1) +} + +const intersectionCountImpliesCuttability = (count: number) => + count > 0 && count % 2 === 0 + +const wouldCutterCutCandidate = ( + cutter: GeoJsonFeature, + candidate: GeoJsonFeature +) => + // drawing may initially turn out to be a point (1-coordinate-line-string) + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + cutter.geometry.type === 'LineString' && + // check if candidate is actually a polygon, we don't cut anything else today + polygonTypes.includes(candidate.geometry.type) && + // start and end do not reside inside candidate (cut through, not cut in) + [ + cutter.geometry.coordinates[0], + cutter.geometry.coordinates[cutter.geometry.coordinates.length - 1], + ].every( + (coordinate) => + !booleanPointInPolygon( + // trust me bro, LineString type indicates >=2 points available + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + coordinate!, + candidate as GeoJsonFeature + ) + ) && + // cuttable is cut an even amount of times > 0, i.e. is entered and left + intersectionCountImpliesCuttability( + lineIntersect( + cutter, + candidate as GeoJsonFeature + ).features.length + ) + +export const splitByCuttability = ( + drawSource: VectorSource, + cutter: GeoJsonFeature, + projectionInfo: ProjectionInfo +): [GeoJsonFeature[], Feature[]] => + drawSource + .getFeatures() + .reduce< + [GeoJsonFeature[], Feature[]] + >( + ([cuttables, uncuttables], current) => { + const currentAsGeoJson = converter.writeFeatureObject( + current, + projectionInfo + ) + if (wouldCutterCutCandidate(cutter, currentAsGeoJson)) { + cuttables.push( + currentAsGeoJson as GeoJsonFeature< + GeoJsonPolygon | GeoJsonMultiPolygon + > + ) + } else { + uncuttables.push(current) + } + return [cuttables, uncuttables] + }, + [[], []] + ) diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/index.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/index.ts new file mode 100644 index 0000000000..57d6ff79e7 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/index.ts @@ -0,0 +1,69 @@ +import type { + Feature as GeoJsonFeature, + LineString as GeoJsonLineString, +} from 'geojson' +import type { Map } from 'ol' +import type Feature from 'ol/Feature' +import type VectorSource from 'ol/source/Vector' + +import { t } from 'i18next' + +import { notifyUser } from '@/lib/notifyUser' + +import { PluginId } from '../../../../types' +import { + converter, + cutCuttablesWithCutter, + splitByCuttability, +} from './cutlery' +import { makeDraw } from './makeDraw' + +// NOTE: Lots of "as" casting in whole module due to conversions' broad returns + +const wgs84Epsg = 'EPSG:4326' + +export const createCutInteractions = (map: Map, drawSource: VectorSource) => { + const projectionInfo = { + featureProjection: map.getView().getProjection().getCode(), + dataProjection: wgs84Epsg, + } + const draw = makeDraw(projectionInfo, drawSource) + draw.on('drawend', (e) => { + const cutter = converter.writeFeatureObject( + e.feature, + projectionInfo + ) as GeoJsonFeature + const [cuttables, uncuttables] = splitByCuttability( + drawSource, + cutter, + projectionInfo + ) + + if (cuttables.length) { + try { + const cuts = cutCuttablesWithCutter(cuttables, cutter).map( + (cut) => converter.readFeature(cut, projectionInfo) as Feature + ) + drawSource.clear() + drawSource.addFeatures([...uncuttables, ...cuts]) + } catch { + console.error(`Cut operation failed for unknown reason.`) + notifyUser( + 'error', + t(($) => $.cut.error.cutFailed, { + ns: PluginId, + }) + ) + } + } else { + notifyUser( + 'info', + t(($) => $.cut.warn.unevenCut, { + ns: PluginId, + }) + ) + } + }) + + return draw +} diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/makeDraw.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/makeDraw.ts new file mode 100644 index 0000000000..054cc96005 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/makeDraw.ts @@ -0,0 +1,33 @@ +import type { + Feature as GeoJsonFeature, + LineString as GeoJsonLineString, +} from 'geojson' +import type Feature from 'ol/Feature' +import type LineString from 'ol/geom/LineString' +import type VectorSource from 'ol/source/Vector' +import type { ProjectionInfo } from './types' + +import Draw from 'ol/interaction/Draw' + +import { converter, splitByCuttability } from './cutlery' +import { cutStyle, styleCut } from './style' + +export const makeDraw = ( + projectionInfo: ProjectionInfo, + drawSource: VectorSource +) => + new Draw({ + type: 'LineString', + stopClick: true, + style: (feature) => { + const cutter = converter.writeFeatureObject( + feature as Feature, + projectionInfo + ) as GeoJsonFeature + // if cut would have an effect, style accordingly + styleCut( + splitByCuttability(drawSource, cutter, projectionInfo)[0].length > 0 + ) + return cutStyle + }, + }) diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/style.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/style.ts new file mode 100644 index 0000000000..0edba3eee0 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/style.ts @@ -0,0 +1,44 @@ +import Circle from 'ol/style/Circle' +import Fill from 'ol/style/Fill' +import Stroke from 'ol/style/Stroke' +import Style from 'ol/style/Style' + +const colors = { + fill: '#ecf4ff', + circleStroke: '#2b3980', + strokeIneffective: '#dc3545', + strokeEffective: '#00a645', +} +const width = 5 + +const fill = new Fill({ + color: colors.fill, +}) + +const circleStroke = new Stroke({ + color: colors.circleStroke, + width: width * 0.66, +}) + +const dashStroke = new Stroke({ + color: colors.strokeIneffective, + lineDash: [10, 15], + lineCap: 'square', + width, +}) + +export const cutStyle = new Style({ + image: new Circle({ + fill, + stroke: circleStroke, + radius: width * 1.33, + }), + fill, + stroke: dashStroke, +}) + +export const styleCut = (effective: boolean) => { + dashStroke.setColor( + effective ? colors.strokeEffective : colors.strokeIneffective + ) +} diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/types.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/types.ts new file mode 100644 index 0000000000..d6cc6ea66f --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createCutInteractions/types.ts @@ -0,0 +1,5 @@ +import type { WriteOptions } from 'ol/format/Feature' + +export type ProjectionInfo = Required< + Pick +> diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createDuplicateInteractions.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createDuplicateInteractions.ts new file mode 100644 index 0000000000..6f5c16cb8b --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createDuplicateInteractions.ts @@ -0,0 +1,51 @@ +import type { Map } from 'ol' +import type VectorLayer from 'ol/layer/Vector' +import type VectorSource from 'ol/source/Vector' + +import { Select } from 'ol/interaction' + +const pointerStyle = (map: Map, drawLayer: VectorLayer) => (e) => { + const found = map.hasFeatureAtPixel(e.pixel, { + layerFilter: (l) => l === drawLayer, + }) + + if (found) { + map.getTargetElement().setAttribute('style', 'cursor: copy') + } else { + map.getTargetElement().setAttribute('style', '') + } +} + +export function createDuplicateInteractions( + map: Map, + { + drawSource, + drawLayer, + }: { + drawSource: VectorSource + drawLayer: VectorLayer + } +) { + const selectInteraction = new Select({ layers: [drawLayer], style: null }) + const selectedFeatures = selectInteraction.getFeatures() + // TODO temp solution, not actually _isDeleteSelect; normalizing these flags is part of a future effort + // @ts-expect-error | internal hack to detect it in @polar/plugin-pins and @polar/plugin-gfi + selectInteraction._isDeleteSelect = true + + const boundPointerStyle = pointerStyle(map, drawLayer) + map.on('pointermove', boundPointerStyle) + + selectedFeatures.on('add', () => { + // @ts-expect-error | We know there's a feature. It's why we're here. + drawSource.addFeature(selectedFeatures.getArray()[0].clone()) + selectedFeatures.clear() + }) + + // @ts-expect-error | local piggyback + selectInteraction._onRemove = () => { + map.un('pointermove', boundPointerStyle) + map.getTargetElement().setAttribute('style', '') + } + + return selectInteraction +} diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createLassoInteractions.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createLassoInteractions.ts new file mode 100644 index 0000000000..1c70930cc6 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createLassoInteractions.ts @@ -0,0 +1,143 @@ +import type { + Feature as GeoJsonFeature, + Geometry, + GeometryCollection, +} from 'geojson' +import type { Feature } from 'ol' +import type { Map } from 'ol' +import type { Polygon } from 'ol/geom' +import type VectorSource from 'ol/source/Vector' +import type { PolarGeoJsonFeature, PolarGeoJsonFeatureCollection } from '@/core' + +import { rawLayerList } from '@masterportal/masterportalapi' +import { booleanContains } from '@turf/boolean-contains' +import { GeoJSON } from 'ol/format' +import Draw from 'ol/interaction/Draw' + +import { getVectorFeaturesByFeatureRequest } from '@/lib/getFeatures/vector' +import { parseWfsResponse } from '@/lib/getFeatures/wfs/parse' +import { indicateLoading } from '@/lib/indicateLoading' +import { notifyUser } from '@/lib/notifyUser' + +const requestError = `@polar/plugin-draw: An error occurred on creating the lasso request: ` +const rejectedError = + '@polar/plugin-draw: The response to a lasso request indicated an error.' +const parseError = + '@polar/plugin-draw: Client failure in reading responses in lasso action.' + +const buildFeatureCollection = ( + featureCollections: PolarGeoJsonFeatureCollection[], + drawnLasso: Feature +) => { + const drawnLassoGeoJson = JSON.parse(new GeoJSON().writeFeature(drawnLasso)) + + return { + type: 'FeatureCollection', + features: featureCollections + .reduce( + (accumulator, { features }) => accumulator.concat(features), + [] + ) + .filter((feature) => { + if (feature.geometry.type.startsWith('Multi')) { + return ( + // since .type on GeometryCollection doesn't start with 'Multi' + ( + feature.geometry as Exclude + ).coordinates.every((partialCoordinates) => + booleanContains(drawnLassoGeoJson, { + type: 'Feature', + geometry: { + type: feature.geometry.type.slice(5), // un«Multi»ed + coordinates: partialCoordinates, + }, + properties: {}, + }) + ) + ) + } + return booleanContains(drawnLassoGeoJson, feature as GeoJsonFeature) + }), + } +} + +export function createLassoInteractions( + map: Map, + drawSource: VectorSource, + activeLassoIds: string[] +): Draw { + const draw = new Draw({ type: 'Polygon', freehand: true }) + + draw.on('drawend', (e) => { + const drawnLasso = e.feature as Feature // due to Draw 'type' param + const requests = activeLassoIds.reduce[]>( + (accumulator, id) => { + try { + const request = getVectorFeaturesByFeatureRequest( + null, + drawnLasso, + id, + map.getView().getProjection().getCode() + ) + accumulator.push(request) + } catch (e) { + console.error(requestError, e) + notifyUser('error', 'plugins.draw.lasso.internalError') + } + return accumulator + }, + [] + ) + + let done = false + let unindicateLoading: (() => void) | undefined + setTimeout(() => !done && (unindicateLoading = indicateLoading()), 100) + + Promise.allSettled(requests) + .then((settledRequests) => + Promise.all( + ( + settledRequests.filter((promiseSettledResult) => { + if (promiseSettledResult.status === 'rejected') { + console.error(rejectedError, promiseSettledResult.reason) + notifyUser('error', 'plugins.draw.lasso.layerRejected') + return false + } + return true + }) as PromiseFulfilledResult[] + ).map(async (result, index) => + rawLayerList.getLayerWhere({ id: activeLassoIds[index] }).typ === + 'WFS' + ? await parseWfsResponse( + result.value, + undefined, + false, + map.getView().getProjection().getCode() + ) + : ((await result.value.json()) as PolarGeoJsonFeatureCollection) + ) + ) + ) + .then((featureCollections) => { + const featureCollection = buildFeatureCollection( + featureCollections, + drawnLasso + ) + const olFeatures = new GeoJSON().readFeatures(featureCollection, { + dataProjection: map.getView().getProjection().getCode(), + featureProjection: map.getView().getProjection().getCode(), + }) + drawSource.addFeatures(olFeatures) + }) + .catch((error: unknown) => { + console.error(parseError, error) + notifyUser('error', 'plugins.draw.lasso.internalError') + }) + .finally(() => { + done = true + unindicateLoading?.() + }) + }) + + return draw +} diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createMergeInteractions.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createMergeInteractions.ts new file mode 100644 index 0000000000..486e1aaa28 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createMergeInteractions.ts @@ -0,0 +1,56 @@ +import type { Feature as GeoJsonFeature, MultiPolygon, Polygon } from 'geojson' +import type Feature from 'ol/Feature' +import type VectorSource from 'ol/source/Vector' + +import { booleanIntersects } from '@turf/boolean-intersects' +import { union } from '@turf/union' +import { GeoJSON } from 'ol/format' +import Draw from 'ol/interaction/Draw' + +const converter = new GeoJSON() + +export const createMergeInteractions = (drawSource: VectorSource) => { + const draw = new Draw({ type: 'Polygon' }) + // @ts-expect-error | internal hack to detect it in @polar/plugin-pins and @polar/plugin-gfi + draw._isDrawPlugin = true + + draw.on('drawend', (e) => { + const mergePolygon = converter.writeFeatureObject( + e.feature + ) as GeoJsonFeature + const drawFeatures = drawSource.getFeatures() + + // sort everything into a.) things to combine b.) things not to touch + const intersectedPolygons: GeoJsonFeature[] = [] + const nextFeatures = drawFeatures.reduce( + (accumulator, drawFeature) => { + const type = drawFeature.getGeometry()?.getType() ?? '' + const drawFeatureAsGeoJson = converter.writeFeatureObject(drawFeature) + if ( + ['Polygon', 'MultiPolygon'].includes(type) && + booleanIntersects(mergePolygon, drawFeatureAsGeoJson) + ) { + intersectedPolygons.push( + drawFeatureAsGeoJson as GeoJsonFeature + ) + } else { + accumulator.push(drawFeature) + } + return accumulator + }, + [] + ) + + const mergedFeature = union({ + type: 'FeatureCollection', + features: [mergePolygon, ...intersectedPolygons], + }) + + nextFeatures.push(converter.readFeature(mergedFeature) as Feature) + + drawSource.clear() + drawSource.addFeatures(nextFeatures) + }) + + return draw +} diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createModifyInteractions.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createModifyInteractions.ts new file mode 100644 index 0000000000..ffad2dfeec --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createModifyInteractions.ts @@ -0,0 +1,35 @@ +import type { Feature, Map } from 'ol' +import type VectorLayer from 'ol/layer/Vector' + +import { Collection } from 'ol' +import { Modify } from 'ol/interaction' + +import { makeLocalSelector } from '../localSelector' + +// TODO: doesn't properly work on mobile, difficult/impossible to get the vertices +export const createModifyInteractions = (map: Map, drawLayer: VectorLayer) => { + const activeContainer = { active: false } + const features: Collection = new Collection() + const modify = new Modify({ features }) + modify.set('_isPolarDragLikeInteraction', true, true) + modify.on('modifystart', () => { + activeContainer.active = true + }) + modify.on('modifyend', () => { + activeContainer.active = false + }) + + const localSelector = makeLocalSelector( + map, + activeContainer, + features, + drawLayer + ) + map.on('pointermove', localSelector) + // @ts-expect-error | "un on removal" riding piggyback as _onRemove + modify._onRemove = () => { + map.un('pointermove', localSelector) + } + + return modify +} diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/createTranslateInteractions.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/createTranslateInteractions.ts new file mode 100644 index 0000000000..5f0baca7d1 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/createTranslateInteractions.ts @@ -0,0 +1,37 @@ +import type { Feature, Map } from 'ol' +import type VectorLayer from 'ol/layer/Vector' + +import { Collection } from 'ol' +import { Translate } from 'ol/interaction' + +import { makeLocalSelector } from '../localSelector' + +export const createTranslateInteractions = ( + map: Map, + drawLayer: VectorLayer +) => { + const activeContainer = { active: false } + const features: Collection = new Collection() + const translate = new Translate({ features }) + translate.set('_isPolarDragLikeInteraction', true, true) + translate.on('translatestart', () => { + activeContainer.active = true + }) + translate.on('translateend', () => { + activeContainer.active = false + }) + + const localSelector = makeLocalSelector( + map, + activeContainer, + features, + drawLayer + ) + map.on('pointermove', localSelector) + // @ts-expect-error | "un on removal" riding piggyback as _onRemove + translate._onRemove = () => { + map.un('pointermove', localSelector) + } + + return translate +} diff --git a/src/plugins/draw/utils/interactionManager/createEditInteractions/index.ts b/src/plugins/draw/utils/interactionManager/createEditInteractions/index.ts new file mode 100644 index 0000000000..b477f8bcf7 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/createEditInteractions/index.ts @@ -0,0 +1,88 @@ +import type { Map } from 'ol' +import type { Feature as OlFeature } from 'ol' +import type Interaction from 'ol/interaction/Interaction' +import type VectorLayer from 'ol/layer/Vector' +import type VectorSource from 'ol/source/Vector' +import type { Ref } from 'vue' +import type { DrawPluginOptions, EditMode } from '../../../types' + +import { Select, Snap } from 'ol/interaction' +import Style from 'ol/style/Style' + +import { getSnaps } from '../getSnaps' +import { createCutInteractions } from './createCutInteractions' +import { createDuplicateInteractions } from './createDuplicateInteractions' +import { createLassoInteractions } from './createLassoInteractions' +import { createMergeInteractions } from './createMergeInteractions' +import { createModifyInteractions } from './createModifyInteractions' +import { createTranslateInteractions } from './createTranslateInteractions' + +const makeModifySelect = ( + drawLayer: VectorLayer, + drawSource: VectorSource, + selectedFeature: Ref +) => { + const select = new Select({ + layers: [drawLayer], + style: null, + hitTolerance: 50, + }) + let lastSelectedFeature: OlFeature | null = null + select.on('select', (event) => { + const selected = event.selected[event.selected.length - 1] ?? null + if (selected) { + lastSelectedFeature = selected + selectedFeature.value = selected + } else { + // empty text features are considered deleted + if (lastSelectedFeature) { + const style = lastSelectedFeature.getStyle() + if (style instanceof Style && style.getText()?.getText() === '') { + drawSource.removeFeature(lastSelectedFeature) + } + } + selectedFeature.value = null + } + }) + return select +} + +export function createEditInteractions( + configuration: DrawPluginOptions['layers'][number], + drawMode: EditMode, + drawLayer: VectorLayer, + drawSource: VectorSource, + map: Map, + activeLassoIds: string[] = [], + selectedFeature: Ref +): Interaction[] { + switch (drawMode) { + case 'modify': + return [ + createModifyInteractions(map, drawLayer), + ...getSnaps(map, configuration.snapTo ?? []), + new Snap({ source: drawSource }), + makeModifySelect(drawLayer, drawSource, selectedFeature), + ] + case 'translate': + return [ + createTranslateInteractions(map, drawLayer), + ...getSnaps(map, configuration.snapTo ?? []), + new Snap({ source: drawSource }), + ] + case 'duplicate': + return [createDuplicateInteractions(map, { drawSource, drawLayer })] + case 'cut': + return [createCutInteractions(map, drawSource)] + case 'merge': + return [ + createMergeInteractions(drawSource), + ...getSnaps(map, configuration.snapTo ?? []), + new Snap({ source: drawSource }), + ] + case 'lasso': + return [createLassoInteractions(map, drawSource, activeLassoIds)] + default: + return [] + } +} diff --git a/src/plugins/draw/utils/interactionManager/getSnaps.ts b/src/plugins/draw/utils/interactionManager/getSnaps.ts new file mode 100644 index 0000000000..66891942bd --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/getSnaps.ts @@ -0,0 +1,32 @@ +import type { Map } from 'ol' +import type VectorLayer from 'ol/layer/Vector' + +import { Snap } from 'ol/interaction' +import VectorSource from 'ol/source/Vector' + +export const getSnaps = (map: Map, snapIds: string[]): Snap[] => + snapIds.reduce((accumulator, layerId) => { + const layer = map + .getLayers() + .getArray() + .find((layer) => layer.get('id') === layerId) as VectorLayer + const source = layer.getSource() + if (source instanceof VectorSource) { + const snap = new Snap({ source }) + const visibilityToggler = () => { + snap.setActive(layer.getVisible()) + } + layer.on('propertychange', visibilityToggler) + visibilityToggler() + // @ts-expect-error | riding piggyback + snap._onRemove = () => { + layer.un('propertychange', visibilityToggler) + } + accumulator.push(snap) + } else { + console.warn( + `@polar/plugin-draw: Layer with ID "${layerId}" configured for 'snapTo', but it has no source to snap to. The layer does probably not hold any vector data.` + ) + } + return accumulator + }, []) diff --git a/src/plugins/draw/utils/interactionManager/index.ts b/src/plugins/draw/utils/interactionManager/index.ts new file mode 100644 index 0000000000..7fda45d484 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/index.ts @@ -0,0 +1,160 @@ +import type { EventsKey } from 'ol/events' +import type Interaction from 'ol/interaction/Interaction' +import type VectorLayer from 'ol/layer/Vector' +import type Map from 'ol/Map' +import type VectorSource from 'ol/source/Vector' +import type { + DrawMode, + DrawPluginOptions, + EditMode, + InteractionOptions, + ToolMode, +} from '../../types' + +import { unByKey } from 'ol/Observable' + +import createDeleteInteractions from './createDeleteInteractions' +import { createDrawInteraction } from './createDrawInteractions' +import { createEditInteractions } from './createEditInteractions' + +// This is a class since it works indepently of vue and its framework +// Don't make the next migration harder than it has to be +export class InteractionManager { + #map: Map + #drawLayerId: string + #drawLayer: VectorLayer | null = null + #drawSource: VectorSource | null = null + #currentInteractions = new Set() + #configuration: DrawPluginOptions + #sourceChangeCallback: ((drawSource: VectorSource) => void) | undefined + #uns: EventsKey[] = [] + + constructor( + map: Map, + configuration: DrawPluginOptions, + drawLayerId: string, + sourceChangeCallback?: (drawSource: VectorSource) => void + ) { + this.#map = map + this.#configuration = configuration + this.#drawLayerId = drawLayerId + if (sourceChangeCallback) { + this.#sourceChangeCallback = sourceChangeCallback + } + this.determineDrawSource(drawLayerId) + } + + destructor() { + this.removeAllInteractions() + if (this.#uns.length > 0) { + this.#uns.forEach((key) => { + unByKey(key) + }) + this.#uns = [] + } + } + + updateDrawLayer(drawLayerId: string) { + this.removeAllInteractions() + this.determineDrawSource(drawLayerId) + } + + determineDrawSource(drawLayerId: string) { + const drawLayer = this.#map + .getLayers() + .getArray() + .find((layer) => layer.get('id') === drawLayerId) + + if (!drawLayer) { + throw new Error(`Draw layer with id ${drawLayerId} not found`) + } + + if (this.#uns.length > 0) { + this.#uns.forEach((key) => { + unByKey(key) + }) + this.#uns = [] + } + + this.#drawLayer = drawLayer as VectorLayer + this.#drawSource = this.#drawLayer.getSource() + this.#drawLayerId = drawLayerId + + if (this.#drawSource) { + this.#uns = this.#drawSource.on( + ['addfeature', 'changefeature', 'removefeature'], + () => { + this.#sourceChangeCallback?.(this.#drawSource as VectorSource) + } + ) + } + } + + getDrawSource() { + return this.#drawSource + } + + initializeInteractions( + tool: ToolMode, + entry: string | undefined, + { + selectedFeature, + measureMode, + text, + activeLassoIds, + drawing, + }: InteractionOptions + ) { + this.removeAllInteractions() + + if (!this.#drawSource || !this.#drawLayer) { + throw new Error('No draw layer or source available.') + } + + let interactions: Interaction[] = [] + + if (tool === 'draw') { + interactions = createDrawInteraction( + this.#configuration.layers.find((l) => l.id === this.#drawLayerId) ?? + {}, + entry as DrawMode, + measureMode ?? 'none', + this.#drawSource, + this.#map, + text, + drawing + ) + } else if (tool === 'edit') { + interactions = createEditInteractions( + this.#configuration.layers.find((l) => l.id === this.#drawLayerId) ?? + {}, + entry as EditMode, + this.#drawLayer, + this.#drawSource, + this.#map, + activeLassoIds, + selectedFeature + ) + } else /* assume (tool === 'delete') */ { + interactions = createDeleteInteractions( + this.#map, + this.#drawLayer, + this.#drawSource + ) + } + + interactions.forEach((interaction) => { + this.#map.addInteraction(interaction) + this.#currentInteractions.add(interaction) + }) + } + + removeAllInteractions() { + this.#currentInteractions.forEach((interaction) => { + this.#map.removeInteraction(interaction) + // @ts-expect-error | "un on removal" riding piggyback as _onRemove + interaction._onRemove?.() + }) + this.#currentInteractions.clear() + } +} diff --git a/src/plugins/draw/utils/interactionManager/localSelector.ts b/src/plugins/draw/utils/interactionManager/localSelector.ts new file mode 100644 index 0000000000..1a37577230 --- /dev/null +++ b/src/plugins/draw/utils/interactionManager/localSelector.ts @@ -0,0 +1,30 @@ +import type { Collection, Feature, Map, MapBrowserEvent } from 'ol' +import type VectorLayer from 'ol/layer/Vector' + +/* sets the topmost hovered feature as singleton feature in collection */ +export const makeLocalSelector = + ( + map: Map, + activeContainer: { active: boolean }, + features: Collection, + drawLayer: VectorLayer + ) => + // bound event processor + (e: MapBrowserEvent) => { + if (activeContainer.active) { + return + } + const hoveredFeature = map.forEachFeatureAtPixel( + e.pixel, + (f) => f as Feature, + { + layerFilter: (l) => l === drawLayer, + } + ) + if (!hoveredFeature) { + // required to update on text deletion + features.clear() + } else if (hoveredFeature !== features.item(0)) { + features.setAt(0, hoveredFeature) + } + } diff --git a/src/plugins/draw/utils/reviseFeatures/autofix.ts b/src/plugins/draw/utils/reviseFeatures/autofix.ts new file mode 100644 index 0000000000..9318df921c --- /dev/null +++ b/src/plugins/draw/utils/reviseFeatures/autofix.ts @@ -0,0 +1,30 @@ +import type { + FeatureCollection, + Feature as GeoJsonFeature, + MultiPolygon, + Polygon, +} from 'geojson' +import type { GeometryType } from '../../types' + +import { cleanCoords } from '@turf/clean-coords' +import { unkinkPolygon } from '@turf/unkink-polygon' + +export const autofixFeatureCollection = ( + revisedFeatureCollection: FeatureCollection +) => ({ + ...revisedFeatureCollection, + features: revisedFeatureCollection.features.reduce< + GeoJsonFeature[] + >((accumulator, feature) => { + if (['Polygon', 'MultiPolygon'].includes(feature.geometry.type)) { + accumulator.push( + ...unkinkPolygon( + cleanCoords(feature) as GeoJsonFeature + ).features + ) + } else { + accumulator.push(cleanCoords(feature)) + } + return accumulator + }, []), +}) diff --git a/src/plugins/draw/utils/reviseFeatures/cloneFeatureCollection.ts b/src/plugins/draw/utils/reviseFeatures/cloneFeatureCollection.ts new file mode 100644 index 0000000000..f001f2820d --- /dev/null +++ b/src/plugins/draw/utils/reviseFeatures/cloneFeatureCollection.ts @@ -0,0 +1,9 @@ +import type { FeatureCollection } from 'geojson' +import type { GeometryType } from '../../types' + +// FeatureCollection is compatible to gangsta clone +export const cloneFeatureCollection = ( + // No GeometryCollection from Draw, hence the + featureCollection: FeatureCollection +): FeatureCollection => + JSON.parse(JSON.stringify(featureCollection)) diff --git a/src/plugins/draw/utils/reviseFeatures/enrichWithMetaServices.ts b/src/plugins/draw/utils/reviseFeatures/enrichWithMetaServices.ts new file mode 100644 index 0000000000..ff188f8c4b --- /dev/null +++ b/src/plugins/draw/utils/reviseFeatures/enrichWithMetaServices.ts @@ -0,0 +1,110 @@ +import type { FeatureCollection, Feature as GeoJsonFeature } from 'geojson' +import type { Feature, Map } from 'ol' +import type { DrawPluginOptionsMetaService, GeometryType } from '../../types' + +import { rawLayerList } from '@masterportal/masterportalapi' +import { booleanIntersects } from '@turf/boolean-intersects' +import compare from 'just-compare' +import { GeoJSON } from 'ol/format' + +import { getVectorFeaturesByFeatureRequest } from '@/lib/getFeatures/vector' +import { parseWfsResponse } from '@/lib/getFeatures/wfs/parse' + +const reader = new GeoJSON() + +type Aggregator = ( + propertiesArray: Exclude[] +) => GeoJsonFeature['properties'][] | GeoJsonFeature['properties'] + +const aggregators: Record< + Required['aggregationMode'], + Aggregator +> = { + all: (x) => x, + unequal: (propertiesArray) => + propertiesArray.reduce((accumulator, current) => { + if (accumulator.every((entry) => !compare(entry, current))) { + accumulator.push(current) + } + return accumulator + }, []), +} + +const filterProperties = ( + properties: Exclude, + propertyNames?: string[] +) => + propertyNames && propertyNames.length + ? Object.fromEntries( + Object.entries(properties).filter(([key]) => + propertyNames.includes(key) + ) + ) + : properties + +const aggregateProperties = ( + propertiesArray: GeoJsonFeature['properties'][], + propertyNames?: string[], + mode: DrawPluginOptionsMetaService['aggregationMode'] = 'unequal' +) => + aggregators[mode]( + propertiesArray + .filter((properties) => properties !== null) + .map((properties) => filterProperties(properties, propertyNames)) + ) + +/** @throws */ +export const enrichWithMetaServices = ( + featureCollection: FeatureCollection, + map: Map, + metaServices: DrawPluginOptionsMetaService[], + signal: AbortSignal +): Promise[]> => + Promise.all( + featureCollection.features.map(async (feature) => ({ + ...feature, + properties: { + ...feature.properties, + metaProperties: { + ...(feature.properties?.metaProperties || {}), + ...Object.fromEntries( + await Promise.all( + metaServices.map(({ id, propertyNames, aggregationMode }) => + getVectorFeaturesByFeatureRequest( + signal, + reader.readFeature( + JSON.stringify(feature) + ) as unknown as Feature, + id, + map.getView().getProjection().getCode() + ) + .then((response) => + rawLayerList.getLayerWhere({ id }).typ === 'WFS' + ? (parseWfsResponse( + response, + undefined, + false, + map.getView().getProjection().getCode() + ) as Promise) + : (response.json() as Promise) + ) + .then((featuresFromBbox) => { + const applicableProperties = featuresFromBbox.features + .filter((featureFromBbox) => + booleanIntersects(featureFromBbox, feature) + ) + .map(({ properties }) => properties) + const aggregatedProperties = aggregateProperties( + applicableProperties, + propertyNames, + aggregationMode + ) + return [id, aggregatedProperties] + }) + ) + ) + ), + }, + }, + })) + ) diff --git a/src/plugins/draw/utils/reviseFeatures/index.ts b/src/plugins/draw/utils/reviseFeatures/index.ts new file mode 100644 index 0000000000..ec72c1fa7f --- /dev/null +++ b/src/plugins/draw/utils/reviseFeatures/index.ts @@ -0,0 +1,71 @@ +import type { FeatureCollection } from 'geojson' +import type Map from 'ol/Map' +import type { DrawPluginOptionsLayerRevision, GeometryType } from '../../types' + +import { t } from 'i18next' + +import { notifyUser } from '@/lib/notifyUser' + +import { PluginId } from '../../types' +import { autofixFeatureCollection } from './autofix' +import { cloneFeatureCollection } from './cloneFeatureCollection' +import { enrichWithMetaServices } from './enrichWithMetaServices' +import { validateGeoJson } from './validateGeoJson' + +let abortController: AbortController | null = null + +export const reviseFeatures = async ( + map: Map, + revisionConfiguration: DrawPluginOptionsLayerRevision, + featureCollection: FeatureCollection +) => { + if (abortController) { + abortController.abort() + } + const thisController = (abortController = new AbortController()) + + const { autofix, validate, metaServices } = revisionConfiguration + + // clone to prevent accidentally messing with the draw tool's data + let revisedFeatureCollection = cloneFeatureCollection(featureCollection) + + if (autofix) { + try { + revisedFeatureCollection = autofixFeatureCollection( + revisedFeatureCollection + ) + } catch (error) { + notifyUser('error', () => + t(($) => $.revision.autofix.errorToast, { ns: PluginId }) + ) + throw new Error('Autofix failed: ' + (error as Error).message) + } + } + + if (validate) { + revisedFeatureCollection = validateGeoJson(revisedFeatureCollection) + } + + if (metaServices?.length) { + try { + revisedFeatureCollection.features = await enrichWithMetaServices( + revisedFeatureCollection, + map, + metaServices, + abortController.signal + ) + } catch (error) { + if (thisController.signal.aborted) { + return null + } + console.error(error) + notifyUser('warning', () => + t(($) => $.revision.metaInformationRetrieval.errorToast, { + ns: PluginId, + }) + ) + } + } + + return thisController.signal.aborted ? null : revisedFeatureCollection +} diff --git a/src/plugins/draw/utils/reviseFeatures/validateGeoJson.ts b/src/plugins/draw/utils/reviseFeatures/validateGeoJson.ts new file mode 100644 index 0000000000..d4356b0f25 --- /dev/null +++ b/src/plugins/draw/utils/reviseFeatures/validateGeoJson.ts @@ -0,0 +1,17 @@ +import type { FeatureCollection } from 'geojson' +import type { GeometryType } from '../../types' + +import { booleanValid } from '@turf/boolean-valid' + +export const validateGeoJson = ( + featureCollection: FeatureCollection +): FeatureCollection => ({ + ...featureCollection, + features: featureCollection.features.map((feature) => ({ + ...feature, + properties: { + ...(feature.properties ?? {}), + sfaValidity: booleanValid(feature), + }, + })), +}) diff --git a/vite.config.ts b/vite.config.ts index 6246ce4229..5636bc34b3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -70,6 +70,9 @@ export default defineConfig(({ mode }) => ({ }, server: { port: 1234, + hmr: { + overlay: false, + }, }, optimizeDeps: { entries: ['!vue2'], diff --git a/vue2/packages/plugins/Draw/CHANGELOG.md b/vue2/packages/plugins/Draw/CHANGELOG.md deleted file mode 100644 index b9010594a7..0000000000 --- a/vue2/packages/plugins/Draw/CHANGELOG.md +++ /dev/null @@ -1,50 +0,0 @@ -# CHANGELOG - -## 3.2.0 - -- Feature: Add new `revision` parameter that, if set, adds another export with modifications and additional information to the drawn geometries. -- Feature: Add new mode `'cut'` to allow users to cut polygons in parts. -- Feature: Add new mode `'merge'` to allow users to merge drawn polygons with a then-to-be-drawn merge polygon. -- Feature: Add new mode `'duplicate'` to allow users to copy drawn features. -- Fix: Measurements were off. The calculation has been fixed. -- Chore: Updated the README.md's state description to be in table format. - -## 3.1.0 - -- Feature: Add a `"translate"` mode that allows moving drawn features as they are. -- Feature: Add a `"snapTo"` key to the configuration that allows defining vector layers to snap to while drawing, editing, and translating. Snapping will only pertain to configured layers while they're visible. -- Feature: Add a lasso mode that allows copying up features from a vector layer that are contained within the user's hand drawn polygon. This also adds the fields `addLoading`, `removeLoading`, and `toastAction` for usage in the `lassos`. -- Feature: Interactions requiring dragging are now marked with the POLAR flag `_isPolarDragLikeInteraction`. -- Feature: Show a "pointer" cursor when the delete interaction would delete a feature on click. -- Feature: An action `setInteractions` has been added to allow clients to bring their own geometry operations. -- Feature: Expose the `getSnaps` function. Intended to be used together with the `setInteractions` action only. -- Feature: Expose `Mode` type of Draw plugin for client-side extensions. -- Fix: Configured font colours were not used on Text draw. This has been resolved. -- Fix: Make `setMode` and `setDrawMode` `async` actions to prevent race conditions if they are called in short succession. - -## 3.0.0 - -- Breaking: Upgrade peerDependency `ol` from `^9.2.4` to `^10.3.1`. -- Feature: Add new configuration parameter `measureOptions` to allow users to select a measurement mode when drawing a feature. This way, a length / area in the selected unit is added to the drawn feature. -- Fix: Update initial value of `drawMode` to a selectable value if the default `Point` is not a drawable option. -- Fix: Adjust type `DrawGetters` regarding its keys `selectableDrawModes` and `selectableModes` to correctly reflect that they represent objects. -- Fix: Stacked geometries can be separated with the "Edit" operation again. [Thanks to mike-000](https://github.com/openlayers/openlayers/issues/16593#issuecomment-2624257614). -- Chore: Add `@polar/core` as a dependency as the component `RadioCard.vue` has been moved from this package to `@polar/core`. - -## 2.0.0 - -- Breaking: Upgrade peerDependency `ol` from `^7.1.0` to `^9.2.4`. -- Fix: Adjust documentation to properly describe optionality of configuration parameters. -- Feature: Make the stroke color for drawn geometry features selectable and editable. - -## 1.1.0 - -- Feature: Improved implementation to make plugin SPA-ready. - -## 1.0.1 - -- Fix: Documentation error regarding plugin state. - -## 1.0.0 - -Initial release. diff --git a/vue2/packages/plugins/Draw/LICENSE b/vue2/packages/plugins/Draw/LICENSE deleted file mode 100644 index c29ce2f835..0000000000 --- a/vue2/packages/plugins/Draw/LICENSE +++ /dev/null @@ -1,287 +0,0 @@ - EUROPEAN UNION PUBLIC LICENCE v. 1.2 - EUPL © the European Union 2007, 2016 - -This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined -below) which is provided under the terms of this Licence. Any use of the Work, -other than as authorised under this Licence is prohibited (to the extent such -use is covered by a right of the copyright holder of the Work). - -The Work is provided under the terms of this Licence when the Licensor (as -defined below) has placed the following notice immediately following the -copyright notice for the Work: - - Licensed under the EUPL - -or has expressed by any other means his willingness to license under the EUPL. - -1. Definitions - -In this Licence, the following terms have the following meaning: - -- ‘The Licence’: this Licence. - -- ‘The Original Work’: the work or software distributed or communicated by the - Licensor under this Licence, available as Source Code and also as Executable - Code as the case may be. - -- ‘Derivative Works’: the works or software that could be created by the - Licensee, based upon the Original Work or modifications thereof. This Licence - does not define the extent of modification or dependence on the Original Work - required in order to classify a work as a Derivative Work; this extent is - determined by copyright law applicable in the country mentioned in Article 15. - -- ‘The Work’: the Original Work or its Derivative Works. - -- ‘The Source Code’: the human-readable form of the Work which is the most - convenient for people to study and modify. - -- ‘The Executable Code’: any code which has generally been compiled and which is - meant to be interpreted by a computer as a program. - -- ‘The Licensor’: the natural or legal person that distributes or communicates - the Work under the Licence. - -- ‘Contributor(s)’: any natural or legal person who modifies the Work under the - Licence, or otherwise contributes to the creation of a Derivative Work. - -- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of - the Work under the terms of the Licence. - -- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, - renting, distributing, communicating, transmitting, or otherwise making - available, online or offline, copies of the Work or providing access to its - essential functionalities at the disposal of any other natural or legal - person. - -2. Scope of the rights granted by the Licence - -The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, -sublicensable licence to do the following, for the duration of copyright vested -in the Original Work: - -- use the Work in any circumstance and for all usage, -- reproduce the Work, -- modify the Work, and make Derivative Works based upon the Work, -- communicate to the public, including the right to make available or display - the Work or copies thereof to the public and perform publicly, as the case may - be, the Work, -- distribute the Work or copies thereof, -- lend and rent the Work or copies thereof, -- sublicense rights in the Work or copies thereof. - -Those rights can be exercised on any media, supports and formats, whether now -known or later invented, as far as the applicable law permits so. - -In the countries where moral rights apply, the Licensor waives his right to -exercise his moral right to the extent allowed by law in order to make effective -the licence of the economic rights here above listed. - -The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to -any patents held by the Licensor, to the extent necessary to make use of the -rights granted on the Work under this Licence. - -3. Communication of the Source Code - -The Licensor may provide the Work either in its Source Code form, or as -Executable Code. If the Work is provided as Executable Code, the Licensor -provides in addition a machine-readable copy of the Source Code of the Work -along with each copy of the Work that the Licensor distributes or indicates, in -a notice following the copyright notice attached to the Work, a repository where -the Source Code is easily and freely accessible for as long as the Licensor -continues to distribute or communicate the Work. - -4. Limitations on copyright - -Nothing in this Licence is intended to deprive the Licensee of the benefits from -any exception or limitation to the exclusive rights of the rights owners in the -Work, of the exhaustion of those rights or of other applicable limitations -thereto. - -5. Obligations of the Licensee - -The grant of the rights mentioned above is subject to some restrictions and -obligations imposed on the Licensee. Those obligations are the following: - -Attribution right: The Licensee shall keep intact all copyright, patent or -trademarks notices and all notices that refer to the Licence and to the -disclaimer of warranties. The Licensee must include a copy of such notices and a -copy of the Licence with every copy of the Work he/she distributes or -communicates. The Licensee must cause any Derivative Work to carry prominent -notices stating that the Work has been modified and the date of modification. - -Copyleft clause: If the Licensee distributes or communicates copies of the -Original Works or Derivative Works, this Distribution or Communication will be -done under the terms of this Licence or of a later version of this Licence -unless the Original Work is expressly distributed only under this version of the -Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee -(becoming Licensor) cannot offer or impose any additional terms or conditions on -the Work or Derivative Work that alter or restrict the terms of the Licence. - -Compatibility clause: If the Licensee Distributes or Communicates Derivative -Works or copies thereof based upon both the Work and another work licensed under -a Compatible Licence, this Distribution or Communication can be done under the -terms of this Compatible Licence. For the sake of this clause, ‘Compatible -Licence’ refers to the licences listed in the appendix attached to this Licence. -Should the Licensee's obligations under the Compatible Licence conflict with -his/her obligations under this Licence, the obligations of the Compatible -Licence shall prevail. - -Provision of Source Code: When distributing or communicating copies of the Work, -the Licensee will provide a machine-readable copy of the Source Code or indicate -a repository where this Source will be easily and freely available for as long -as the Licensee continues to distribute or communicate the Work. - -Legal Protection: This Licence does not grant permission to use the trade names, -trademarks, service marks, or names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the copyright notice. - -6. Chain of Authorship - -The original Licensor warrants that the copyright in the Original Work granted -hereunder is owned by him/her or licensed to him/her and that he/she has the -power and authority to grant the Licence. - -Each Contributor warrants that the copyright in the modifications he/she brings -to the Work are owned by him/her or licensed to him/her and that he/she has the -power and authority to grant the Licence. - -Each time You accept the Licence, the original Licensor and subsequent -Contributors grant You a licence to their contributions to the Work, under the -terms of this Licence. - -7. Disclaimer of Warranty - -The Work is a work in progress, which is continuously improved by numerous -Contributors. It is not a finished work and may therefore contain defects or -‘bugs’ inherent to this type of development. - -For the above reason, the Work is provided under the Licence on an ‘as is’ basis -and without warranties of any kind concerning the Work, including without -limitation merchantability, fitness for a particular purpose, absence of defects -or errors, accuracy, non-infringement of intellectual property rights other than -copyright as stated in Article 6 of this Licence. - -This disclaimer of warranty is an essential part of the Licence and a condition -for the grant of any rights to the Work. - -8. Disclaimer of Liability - -Except in the cases of wilful misconduct or damages directly caused to natural -persons, the Licensor will in no event be liable for any direct or indirect, -material or moral, damages of any kind, arising out of the Licence or of the use -of the Work, including without limitation, damages for loss of goodwill, work -stoppage, computer failure or malfunction, loss of data or any commercial -damage, even if the Licensor has been advised of the possibility of such damage. -However, the Licensor will be liable under statutory product liability laws as -far such laws apply to the Work. - -9. Additional agreements - -While distributing the Work, You may choose to conclude an additional agreement, -defining obligations or services consistent with this Licence. However, if -accepting obligations, You may act only on your own behalf and on your sole -responsibility, not on behalf of the original Licensor or any other Contributor, -and only if You agree to indemnify, defend, and hold each Contributor harmless -for any liability incurred by, or claims asserted against such Contributor by -the fact You have accepted any warranty or additional liability. - -10. Acceptance of the Licence - -The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ -placed under the bottom of a window displaying the text of this Licence or by -affirming consent in any other similar way, in accordance with the rules of -applicable law. Clicking on that icon indicates your clear and irrevocable -acceptance of this Licence and all of its terms and conditions. - -Similarly, you irrevocably accept this Licence and all of its terms and -conditions by exercising any rights granted to You by Article 2 of this Licence, -such as the use of the Work, the creation by You of a Derivative Work or the -Distribution or Communication by You of the Work or copies thereof. - -11. Information to the public - -In case of any Distribution or Communication of the Work by means of electronic -communication by You (for example, by offering to download the Work from a -remote location) the distribution channel or media (for example, a website) must -at least provide to the public the information requested by the applicable law -regarding the Licensor, the Licence and the way it may be accessible, concluded, -stored and reproduced by the Licensee. - -12. Termination of the Licence - -The Licence and the rights granted hereunder will terminate automatically upon -any breach by the Licensee of the terms of the Licence. - -Such a termination will not terminate the licences of any person who has -received the Work from the Licensee under the Licence, provided such persons -remain in full compliance with the Licence. - -13. Miscellaneous - -Without prejudice of Article 9 above, the Licence represents the complete -agreement between the Parties as to the Work. - -If any provision of the Licence is invalid or unenforceable under applicable -law, this will not affect the validity or enforceability of the Licence as a -whole. Such provision will be construed or reformed so as necessary to make it -valid and enforceable. - -The European Commission may publish other linguistic versions or new versions of -this Licence or updated versions of the Appendix, so far this is required and -reasonable, without reducing the scope of the rights granted by the Licence. New -versions of the Licence will be published with a unique version number. - -All linguistic versions of this Licence, approved by the European Commission, -have identical value. Parties can take advantage of the linguistic version of -their choice. - -14. Jurisdiction - -Without prejudice to specific agreement between parties, - -- any litigation resulting from the interpretation of this License, arising - between the European Union institutions, bodies, offices or agencies, as a - Licensor, and any Licensee, will be subject to the jurisdiction of the Court - of Justice of the European Union, as laid down in article 272 of the Treaty on - the Functioning of the European Union, - -- any litigation arising between other parties and resulting from the - interpretation of this License, will be subject to the exclusive jurisdiction - of the competent court where the Licensor resides or conducts its primary - business. - -15. Applicable Law - -Without prejudice to specific agreement between parties, - -- this Licence shall be governed by the law of the European Union Member State - where the Licensor has his seat, resides or has his registered office, - -- this licence shall be governed by Belgian law if the Licensor has no seat, - residence or registered office inside a European Union Member State. - -Appendix - -‘Compatible Licences’ according to Article 5 EUPL are: - -- GNU General Public License (GPL) v. 2, v. 3 -- GNU Affero General Public License (AGPL) v. 3 -- Open Software License (OSL) v. 2.1, v. 3.0 -- Eclipse Public License (EPL) v. 1.0 -- CeCILL v. 2.0, v. 2.1 -- Mozilla Public Licence (MPL) v. 2 -- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 -- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for - works other than software -- European Union Public Licence (EUPL) v. 1.1, v. 1.2 -- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong - Reciprocity (LiLiQ-R+). - -The European Commission may update this Appendix to later versions of the above -licences without producing a new version of the EUPL, as long as they provide -the rights granted in Article 2 of this Licence and protect the covered Source -Code from exclusive appropriation. - -All other changes or additions to this Appendix require the production of a new -EUPL version. \ No newline at end of file diff --git a/vue2/packages/plugins/Draw/README.md b/vue2/packages/plugins/Draw/README.md deleted file mode 100644 index 53c6ed7adc..0000000000 --- a/vue2/packages/plugins/Draw/README.md +++ /dev/null @@ -1,323 +0,0 @@ -# Draw - -## Scope - -The draw plugin allows users to draw features on the map. Drawn features may be edited and deleted. - -Currently supported OpenLayers geometry types: - -- `'Circle''` -- `'LineString'` -- `'Point'` -- `'Polygon'` - -Also, `'Text'` is supported which is modeled as an OpenLayers `'Point'`. This is no default feature, so it must be specified in the configuration to use it. - -## User instructions for Text Mode - -The interaction with text features is not intuitive, which is why the text feature should come with instructions for the users: - -### Edit - -To edit the text or the placement of the text feature, the user must click on the center of the text to select the point geometry below it. After selecting it, the user can move the point by keeping the left mouse button pressed, or edit the text in the input field that opens with selecting the feature. If more than one text size is specified in the configuration, the user can change the text size with a slider. - -### Delete - -To delete the text, the user must either click on the point at the center of the text or use CTRL + left mouse button to open a box over all features that he or she wants to delete. - -## Configuration - -The styling of the drawn features can be configured to overwrite the default ol-style. Configuration is oriented around the [OpenLayers styles](https://openlayers.org/en/latest/apidoc/module-ol_style_Style.html#~StyleLike). - -### draw - -| fieldName | type | description | -| - | - | - | -| addLoading | string? | Expects the path to a mutation within the store. This mutation is committed with a plugin-specific loading key as payload when starting asynchronous procedures that are intended to be communicated to the user. | -| enableOptions | boolean? | If `true`, draw options are displayed, like choosing and changing stroke color. Not available for texts features. Defaults to `false`. | -| lassos | lasso[]? | Allows configuring lasso options. The lasso function allows free-hand drawing a geometry on the map; features completely fitting into that geometry will be copied up to the draw layer from all configured layers. UI-wise, it is not intuitive for users do understand what a "Lasso" does. This feature currently requires further instructions by the outlying UI on what one is supposed to do with it. | -| removeLoading | string? | Expects the path to a mutation within the store. This mutation is committed with a plugin-specific loading key as payload when finishing asynchronous procedures that are intended to be communicated to the user. | -| measureOptions | measureOptions? | If set, an additional radio is being shown to the user to be able to let the (then) drawn features display their length and / or area. See [draw.measureOptions](#drawmeasureoptions) for further information. Not shown by default. | -| revision | revision? | If set, a modified copy of the drawn features is provided as export with configurable properties. | -| selectableDrawModes | string[]? | List 'Point', 'LineString', 'Circle', 'Text' and/or 'Polygon' as desired. All besides 'Text' are selectable by default. | -| snapTo | string[]? | Accepts an array of layer IDs. If these layers are active, they are used as snapping material for geometry manipulation. The Draw layer will also always snap to its own features regardless. Please mind that used layers must provide vector data. The layers referred to must be configured in `mapConfiguration.layers`. | -| style | style? | Please see example below for styling options. Defaults to standard OpenLayers styling. | -| textStyle | textStyle? | Use this object with properties 'font' and 'textColor' to style text feature. | -| toastAction | string? | This string will be used as action to send a toast information to the user to clarify why something happened in edge cases. If this is not defined, the information will only be printed to the console for debugging purposes instead. | - -For details on the `displayComponent` attribute, refer to the [Global Plugin Parameters](../../core/README.md#global-plugin-parameters) section of `@polar/core`. - - -Example configuration - -```js -draw: { - selectableDrawModes: ['Circle', 'LineString', 'Point', 'Polygon', 'Text'], - textStyle: { - font: { - size: [10, 20, 30], - family: 'Arial', - }, - }, - style: { - fill: { - color: 'rgba(255, 255, 255, 0.5)' - }, - stroke: { - color: '#e51313', - width: 2, - }, - circle: { - radius: 7, - fillColor: '#e51313', - }, - }, -}, -``` - - - -#### draw.lasso - -| fieldName | type | description | -| - | - | - | -| id | string | The layer id of a vector layer to copy up vector features from. | -| minZoom | boolean | Defaults to `true`. If a boolean is given, the `minZoom` (if configured) of the `LayerConfiguration` in `mapConfiguration.layers` will be adhered to when copying up geometries from the source. This is to prevent the client from overly burdening feature-rich vector services on accident. | - -#### draw.measureOptions - -| fieldName | type | description | -| - | - | - | -| metres | boolean? | Whether to show the measure option `'m / m²'` to the user. `false` by default. | -| kilometres | boolean? | Whether to show the measure option `'km / km²'` to the user. `false` by default. | -| hectares | boolean? | Whether to show the measure option `'km / ha‚'` to the user. `false` by default. | -| initialOption | 'none' \| 'meters' \| 'kilometres' \| 'hectares' | The initial measure option to be selected. Defaults to `'none'`. | - -#### draw.textStyle - -| fieldName | type | description | -| - | - | - | -| font | object \| string | Style the font of the text feature with either css font properties or use font as an object with properties 'size' and 'family'. | -| textColor | string? | Define text color in hex or rgb / rgba code. | - -Example configuration: -```js -textStyle: { - font: '16px sans-serif' - textColor: '#e51313' -} -``` - -##### draw.textStyle.font - -| fieldName | type | description | -| - | - | - | -| family | string? | Font family. | -| size | number[]? | Array with numbers that define the available text sizes that a user can choose from | - -Example configuration: -```js -font: { - size: [10.5, 20, 30.5, 35], - family: 'serif' -}, -``` - -#### draw.revision - -| fieldName | type | description | -| - | - | - | -| autofix | boolean? | If `true`, an automatic attempt at repairing the given geometries is executed regarding fulfillment of the OGC Simple Feature Specification (part of [SFA](https://www.ogc.org/de/publications/standard/sfa/)). Defaults to `false`. | -| mergeToMultiGeometries | boolean? | Defaults to `false`. If `true`, the exported FeatureCollection in getter `revisedFeatureCollection` will have merged geometries; that is, instead of Points, Lines, and Polygons, only MultiPoints, MultiLines, and MultiPolygons will be featured, created by merging the features of their respective geometry. All geometry types that are enabled may occur. This step is executed before geometry validation and meta service usage. | -| metaServices | metaService[]? | Specification of meta services that are requested with the spatial position of each geometry. | -| validate | boolean? | If `true`, a `sfaValidity` flag is added to each feature's attributes that indicates whether the geometry is valid respective the OGC Simple Feature Specification (part of [SFA](https://www.ogc.org/de/publications/standard/sfa/)). This will override any other `sfaValidity` property. Defaults to `false`. | - -#### draw.revision.metaService - -| fieldName | type | description | -| - | - | - | -| id | string | Id of the vector layer to make use of in the meta service. | -| aggregationMode | enum['unequal', 'all']? | Defaults to `'unequal'`. In mode `'unequal'`, one of each property set is kept; duplicate property sets are dropped. In mode `'all'`, all property sets are kept without further filtering. | -| propertyNames | string[]? | Names of the properties to build aggregations from. If left undefined, all found properties will be used. | - -From all geometries of the service intersecting our geometries, properties are aggregated. - -Example: Our drawing feature touches these features in the layer with id `"metaSourceExampleId"`: - -```json -{ - "type": "Feature", - "geometry": "...", - "properties": { "a": 0, "b": 0 } -}, -{ - "type": "Feature", - "geometry": "...", - "properties": { "a": 0, "b": 1 } -}, -{ - "type": "Feature", - "geometry": "...", - "properties": { "a": 0, "b": 1 } -}, -{ - "type": "Feature", - "geometry": "...", - "properties": { "a": 1, "b": 1 } -} -``` - -The feature will then have the following properties: - -In mode `'unequal'`: - -```json -{ - "type": "Feature", - "geometry": "...", - "properties": { - "metaProperties": { - "metaSourceExampleId": [ - { "a": 0, "b": 0 }, - { "a": 0, "b": 1 }, - { "a": 1, "b": 1 } - ] - } - } -} -``` - -In mode `'all'`: - -```json -{ - "type": "Feature", - "geometry": "...", - "properties": { - "metaProperties": { - "metaSourceExampleId": [ - { "a": 0, "b": 0 }, - { "a": 0, "b": 1 }, - { "a": 0, "b": 1 }, - { "a": 1, "b": 1 } - ] - } - } -} -``` - -#### draw.style (by example) - -The `@masterportal/masterportalapi` has vectorStyles in development. As soon as that's done, we shall use its styling syntax and methods. - -For the time being, please use this example as a rough reference as to what can currently be done. - -```js -{ - draw: { - enableOptions: true, - style: { - fill: { - color: 'rgba(255, 255, 255, 0.5)' - }, - stroke: { - color: '#e51313', - width: 2 - }, - circle: { - radius: 7, - fillColor: '#e51313' - }, - // Styling for text of measurements; supports everything described at https://openlayers.org/en/v9.2.4/apidoc/module-ol_style_Text-Text.html - measure: { - font: '16px sans-serif', - placement: 'line', - fill: new Fill({ color: 'black' }), - stroke: new Stroke({ color: 'black' }), - offsetY: -5 - } - } - } -} -``` - -## Store - -### State - -| fieldName | type | description | -| - | - | - | -| `'plugin/draw/featureCollection'` | FeatureCollection | A [GeoJSON](https://geojson.org/) FeatureCollection of all drawn features (including possible measurements in meters with two decimals precision). It updates on changes. | -| `'plugin/draw/revisedFeatureCollection'` | FeatureCollection | A [GeoJSON](https://geojson.org/) FeatureCollection after the `draw.revision` configuration has been applied. If it is not set, this FeatureCollection will stay empty. | -| `'plugin/draw/featureCollectionRevisionState'` | enum['inactive', 'inProgress', 'complete', 'error'] | An indicator for asynchronous revisions. If `draw.revision` is not set, this will stay `'inactive'`. While an asynchronous operation is running, it is `'inProgress'`, after that `'finished'`. If the revision failed for any reason, an `'error'` flag is set. | - -```js -map.subscribe('plugin/draw/featureCollection', (featureCollection) => { - /* Your code. */ -}) -``` - -### Actions - -#### addFeatures - -```js -map.$store.dispatch('plugin/draw/addFeatures', { - geoJSON: { - type: 'FeatureCollection', - features: [ - { - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: [484000, 5885000], - }, - }, - ], - }, - overwrite: true, // defaults to false -}) -``` - -Calling the action `addFeatures` expects an object containing the parameter `geoJSON`, which is a [GeoJSON](https://geojson.org/) FeatureCollection. -It adds the given features from the FeatureCollection to the drawn source. It is also possible to completely overwrite them using the parameter `overwrite`. - -It's important to note that the GeoJSON Standard [RFC7946](https://www.rfc-editor.org/rfc/rfc7946) does not support circles. -To add a circle to the map, it is assumed, that a feature being a circle has a property `radius` together with a point geometry. - -#### setInteractions - ->⚠️ This is a complex action and can not be used without further implementation in a client. - -```js -// `yourInteractions` is of type `ol/interaction[]` -map.$store.dispatch('plugin/draw/setInteractions', yourInteractions) -``` - -Allows interactions of other sources to take precedence without overlapping with the Draw interactions. By using this action, it is ensured both the draw interactions are cleared and the outside interactions are clearable by the draw tool. With this, you may write client-specific geometry operations of arbitrarily specific nature. Please mind that the Draw tool will display that no draw mode is active during this time, requiring you to provide a different UI. - -When setting drag-like interactions, add `_isPolarDragLikeInteraction` to the interaction. Regarding this, refer to the chapter "Special Flags" in the core documentation. - -If you need additional code executed on removal, you may add a method to `yourInteraction._onRemove`. It will be called on clean-up without parameters. - -#### zoomToFeature - -```js -map.$store.dispatch('plugin/draw/zoomToFeature', { - index: 42, // defaults to 0 - margin: 420, // defaults to 20 -}) -``` - -Calling the action `zoomToFeature` zooms to the feature with position `index`, if given, and fits the map view around it with a padding of size `margin` in every direction of the feature. - -#### zoomToAllFeatures - -```js -map.$store.dispatch('plugin/draw/zoomToAllFeatures', { - margin: 420, // defaults to 20 -}) -``` - -Calling the action `zoomToFeature` zooms to all drawn features, fits them in the map view with a padding of size `margin` in every direction of the features. diff --git a/vue2/packages/plugins/Draw/package.json b/vue2/packages/plugins/Draw/package.json deleted file mode 100644 index 056aa2c228..0000000000 --- a/vue2/packages/plugins/Draw/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "@polar/plugin-draw", - "version": "3.2.0", - "description": "Draw plugin for POLAR that adds draw interactions to the map, allowing users to place various shapes and texts.", - "keywords": [ - "OpenLayers", - "ol", - "POLAR", - "plugin", - "Draw", - "interaction" - ], - "license": "EUPL-1.2", - "type": "module", - "author": "Dataport AöR ", - "main": "src/index.ts", - "repository": { - "type": "git", - "url": "git+https://github.com/Dataport/polar.git", - "directory": "packages/plugins/Draw" - }, - "files": [ - "src/**/*", - "CHANGELOG.md" - ], - "dependencies": { - "@turf/boolean-contains": "^7.2.0", - "@turf/boolean-intersects": "^7.2.0", - "@turf/boolean-point-in-polygon": "^7.2.0", - "@turf/boolean-valid": "^7.2.0", - "@turf/buffer": "^7.2.0", - "@turf/center-of-mass": "^7.2.0", - "@turf/clean-coords": "^7.2.0", - "@turf/difference": "^7.2.0", - "@turf/helpers": "^7.2.0", - "@turf/line-intersect": "^7.2.0", - "@turf/union": "^7.2.0", - "@turf/unkink-polygon": "^7.2.0", - "@polar/lib-get-features": "^3.0.0" - }, - "peerDependencies": { - "@masterportal/masterportalapi": "2.48.0", - "@polar/core": "^3.2.1", - "@repositoryname/vuex-generators": "^1.1.2", - "i18next": "^23.11.5", - "ol": "^10.4.0", - "vue": "^2.6.14", - "vuex": "^3.6.2" - }, - "devDependencies": { - "@polar/lib-custom-types": "^2.2.0" - } -} diff --git a/vue2/packages/plugins/Draw/src/components/Draw.vue b/vue2/packages/plugins/Draw/src/components/Draw.vue deleted file mode 100644 index 92103f3cbb..0000000000 --- a/vue2/packages/plugins/Draw/src/components/Draw.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - - - - - - {{ - $t('plugins.draw.label.textSize') - }} - - - - - - - - - diff --git a/vue2/packages/plugins/Draw/src/components/DrawOptions.vue b/vue2/packages/plugins/Draw/src/components/DrawOptions.vue deleted file mode 100644 index 8e5f97411f..0000000000 --- a/vue2/packages/plugins/Draw/src/components/DrawOptions.vue +++ /dev/null @@ -1,79 +0,0 @@ - - - {{ $t('plugins.draw.title.options') }} - - - - {{ $t('plugins.draw.options.stroke') }} - - {{ - isColorPickerVisible - ? 'fa-solid fa-chevron-up' - : 'fa-solid fa-chevron-down' - }} - - - - - - - - - - - - - diff --git a/vue2/packages/plugins/Draw/src/components/index.ts b/vue2/packages/plugins/Draw/src/components/index.ts deleted file mode 100644 index 9cb51c45d7..0000000000 --- a/vue2/packages/plugins/Draw/src/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as Draw } from './Draw.vue' diff --git a/vue2/packages/plugins/Draw/src/index.ts b/vue2/packages/plugins/Draw/src/index.ts deleted file mode 100644 index e565043324..0000000000 --- a/vue2/packages/plugins/Draw/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Vue from 'vue' -import { DrawConfiguration } from '@polar/lib-custom-types' - -import { Draw } from './components' -import locales from './locales' -import { makeStoreModule } from './store' - -export { getSnaps } from './store/createInteractions/getSnaps' -export type { Mode } from './types' - -// NOTE: Currently no options are specified here, variable is kept for integrity until options are needed -export default (options: DrawConfiguration) => (instance: Vue) => - instance.$store.dispatch('addComponent', { - name: 'draw', - plugin: Draw, - locales, - storeModule: makeStoreModule(), - options, - }) diff --git a/vue2/packages/plugins/Draw/src/locales.ts b/vue2/packages/plugins/Draw/src/locales.ts deleted file mode 100644 index 873bb60f95..0000000000 --- a/vue2/packages/plugins/Draw/src/locales.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { Locale } from '@polar/lib-custom-types' - -export const resourcesDe = { - plugins: { - draw: { - mode: { - none: 'Keine Interaktion', - draw: 'Zeichnen', - measure: 'Zeichnen und Messen', - write: 'Zeichnen und Schreiben', - writeAndMeasure: 'Zeichnen, Schreiben und Messen', - lasso: 'Lasso', - duplicate: 'Duplizieren', - cut: 'Polygone auftrennen', - merge: 'Polygone kombinieren', - edit: 'Bearbeiten', - translate: 'Verschieben', - delete: 'Löschen', - }, - drawMode: { - circle: 'Kreis', - lineString: 'Linie', - point: 'Punkt', - polygon: 'Polygon', - text: 'Text', - }, - measureMode: { - none: 'Keine Messung', - metres: 'm', - metresArea: 'm / m²', - kilometres: 'km', - kilometresArea: 'km / km²', - hectares: 'km / ha', - }, - metaInformationRetrieval: { - errorToast: - 'Der Bezug von Metadaten zur gezeichneten Geometrie ist fehlgeschlagen. Die Geometrie wird ohne Metadaten weitergereicht.', - }, - options: { - stroke: 'Linienfarbe', - }, - title: { - drawMode: 'Zeichenmodus', - mode: 'Modus', - measureMode: 'Messmodus', - options: 'Zeichenoptionen', - }, - label: { - textSize: 'Textgröße (px) wählen:', - }, - lasso: { - layerRejected: - 'Die Antwort des Layers "{{id}}" konnte nicht gelesen werden. Es wurden keine Geometrien aus diesem Layer bezogen.', - internalError: - 'Ein unerwarteter Fehler ist in der Verarbeitung der Lasso-Daten aufgetreten.', - }, - cut: { - error: { - cutFailed: - 'Das Polygon konnte aufgrund eines unbekannten Fehlers leider nicht geschnitten werden.', - }, - warn: { - unevenCut: - 'Leider konnte kein Schnitt hergestellt werden, da entweder der Start- oder Endpunkt der Schnittkante innerhalb des zu schneidenden Polygons lag, oder kein Polygon geschnitten wurde. Bitte versuchen Sie es erneut.', - }, - }, - }, - }, -} as const - -export const resourcesEn = { - plugins: { - draw: { - mode: { - none: 'No interaction', - draw: 'Draw', - measure: 'Draw and measure', - write: 'Draw and write', - writeAndMeasure: 'Draw, write and measure', - lasso: 'Lasso', - duplicate: 'Duplicate', - cut: 'Cut polygons', - merge: 'Merge polygons', - edit: 'Edit', - translate: 'Translate', - delete: 'Delete', - }, - drawMode: { - circle: 'Circle', - lineString: 'Line', - point: 'Point', - polygon: 'Polygon', - text: 'Text', - }, - measureMode: { - none: 'No measure', - metres: 'm', - metresArea: 'm / m²', - kilometres: 'km', - kilometresArea: 'km / km²', - hectares: 'km / ha', - }, - options: { - stroke: 'Stroke color', - }, - title: { - drawMode: 'Drawing mode', - mode: 'Mode', - measureMode: 'Measure mode', - options: 'Draw options', - }, - label: { - textSize: 'Choose text size (px):', - }, - lasso: { - layerRejected: - 'The response of layer "{{id}}" could not be read. No geometries were fetched from that layer.', - internalError: - 'An unexpected error occured in the processing of lasso data.', - }, - cut: { - error: { - cutFailed: 'The polygon could not be cut due to unknown reasons.', - }, - warn: { - unevenCut: - 'No cut could be produced since either the start or end point of the cut line is within the polygon to cut, or no polygon was cut. Please try again.', - }, - }, - }, - }, -} as const - -const locales: Locale[] = [ - { - type: 'de', - resources: resourcesDe, - }, - { - type: 'en', - resources: resourcesEn, - }, -] - -export default locales diff --git a/vue2/packages/plugins/Draw/src/store/actions.ts b/vue2/packages/plugins/Draw/src/store/actions.ts deleted file mode 100644 index abedf7b32b..0000000000 --- a/vue2/packages/plugins/Draw/src/store/actions.ts +++ /dev/null @@ -1,230 +0,0 @@ -import VectorSource from 'ol/source/Vector' -import { Interaction, Select } from 'ol/interaction' -import { DrawMode, MeasureMode, PolarActionTree } from '@polar/lib-custom-types' -import GeoJSON from 'ol/format/GeoJSON' -import { Circle, Point } from 'ol/geom' -import createDrawLayer from '../utils/createDrawLayer' -import { DrawGetters, DrawState, Mode } from '../types' -import { createTextStyle } from '../utils/createTextStyle' -import createDrawStyle from '../utils/createDrawStyle' -import createInteractions from './createInteractions' -import createModifyInteractions from './createInteractions/createModifyInteractions' -import createTextInteractions from './createInteractions/createTextInteractions' -import createTranslateInteractions from './createInteractions/createTranslateInteractions' -import createLassoInteractions from './createInteractions/createLassoInteractions' -import modifyDrawStyle from './createInteractions/modifyDrawStyle' -import modifyTextStyle from './createInteractions/modifyTextStyle' -import createDrawInteractions from './createInteractions/createDrawInteractions' -import createDeleteInteractions from './createInteractions/createDeleteInteractions' -import { createDuplicateInteractions } from './createInteractions/createDuplicateInteractions' -import { createCutInteractions } from './createInteractions/createCutInteractions' -import { createMergeInteractions } from './createInteractions/createMergeInteractions' -import { reviseFeatures } from './reviseFeatures' -import { complete } from './reviseFeatures/revisionStates' - -export const makeActions = () => { - let interactions: Interaction[] = [] - let drawLayer - const drawSource = new VectorSource() - - const actions: PolarActionTree = { - createInteractions, - createDuplicateInteractions, - createCutInteractions, - createMergeInteractions, - createDrawInteractions, - createLassoInteractions, - createModifyInteractions, - createTranslateInteractions, - createDeleteInteractions, - createTextInteractions, - modifyDrawStyle, - modifyTextStyle, - reviseFeatures, - setupModule({ commit, dispatch, getters, rootGetters: { map } }) { - dispatch('initializeConfigStyle') - drawSource.on(['addfeature', 'changefeature', 'removefeature'], () => - commit('updateFeatures') - ) - drawLayer = createDrawLayer(drawSource, getters.configuration?.style) - - map.addLayer(drawLayer) - dispatch('updateInteractions') - - const drawModes = Object.keys(getters.selectableDrawModes) - if (!drawModes.includes('Point')) { - commit('setDrawMode', drawModes[0]) - } - if (getters.measureOptions.initialOption) { - commit('setMeasureMode', getters.measureOptions.initialOption) - } - - if (getters.configuration.revision) { - // not inactive, and initially complete due to still being empty - commit('setFeatureCollectionRevisionState', complete) - this.watch( - () => getters.featureCollection, - () => dispatch('reviseFeatures') - ) - } - }, - async setDrawMode({ commit, dispatch }, drawMode: DrawMode) { - commit('setDrawMode', drawMode) - await dispatch('updateInteractions') - }, - /** Please consult the README.md before usage. */ - async setInteractions( - { dispatch, rootGetters }, - newInteractions: Interaction[] - ) { - dispatch('setMode', 'none') - await dispatch('updateInteractions') - interactions = newInteractions - interactions.forEach((interaction) => - rootGetters.map.addInteraction(interaction) - ) - }, - setMeasureMode({ commit, dispatch }, measureMode: MeasureMode) { - commit('setMeasureMode', measureMode) - dispatch('updateInteractions') - }, - async setMode({ commit, dispatch }, mode: Mode) { - commit('setMode', mode) - await dispatch('updateInteractions') - }, - setTextInput( - { - commit, - dispatch, - rootGetters: { configuration }, - getters: { selectedFeature, mode, textSize }, - }, - textInput - ) { - commit('setTextInput', textInput) - if (mode === 'draw') { - dispatch('updateInteractions') - } else if (selectedFeature) { - selectedFeature.setStyle( - createTextStyle(textInput, configuration.draw?.textStyle, textSize) - ) - selectedFeature.set('text', textInput) - commit('updateFeatures') - } - }, - setSelectedStrokeColor( - { - commit, - dispatch, - getters: { configuration, measureMode, mode, selectedFeature }, - rootGetters: { map }, - }, - selectedStrokeColor - ) { - const featureStyle = selectedFeature?.getStyle() - if (mode === 'draw') { - commit('setSelectedStrokeColor', selectedStrokeColor) - dispatch('updateInteractions') - } else if (selectedFeature && featureStyle) { - const style = createDrawStyle( - selectedFeature.getGeometry()?.getType() || mode, - selectedStrokeColor, - measureMode, - map.getView().getProjection(), - configuration?.style - ) - selectedFeature.setStyle(style) - } - }, - setSelectedSize( - { - commit, - dispatch, - rootGetters: { configuration }, - getters: { selectedFeature, mode, textInput, fontSizes }, - }, - selectedSize - ) { - commit('setSelectedSize', selectedSize) - // textSize must be recalculated with new selectedSize - const textSize = fontSizes[selectedSize] - if (mode === 'draw') { - dispatch('updateInteractions') - } else if (selectedFeature) { - selectedFeature.setStyle( - createTextStyle(textInput, configuration.draw?.textStyle, textSize) - ) - commit('updateFeatures') - } - }, - async updateInteractions({ - commit, - dispatch, - getters: { selectedFeature }, - rootGetters: { map }, - }) { - interactions.forEach( - (interaction) => - // @ts-expect-error | "un on removal" riding piggyback as _onRemove - map.removeInteraction(interaction) && interaction._onRemove?.() - ) - if (interactions.some((interaction) => interaction instanceof Select)) { - if (selectedFeature && selectedFeature.get('text') === '') { - // text nodes without text are considered deleted - drawSource.removeFeature(selectedFeature) - commit('updateFeatures') - } - commit('setSelectedFeature', null) // select removal = deselect - commit('setTextInput', '') - } - // await is needed as dispatch *may* return a Promise - interactions = await dispatch('createInteractions', { - drawSource, - drawLayer, - }) - interactions.forEach((interaction) => map.addInteraction(interaction)) - }, - zoomToFeature({ dispatch }, { index = 0, margin }) { - const feature = drawSource.getFeatures()[index] // TODO parameter is weird, one has to know in which order the features where added - if (feature) { - dispatch('zoomTo', { geometryOrExtent: feature.getGeometry(), margin }) - } - }, - zoomToAllFeatures({ dispatch }, { margin }) { - dispatch('zoomTo', { geometryOrExtent: drawSource.getExtent(), margin }) - }, - /** - * Zoom to one or all features of the draw layer. - */ - zoomTo({ rootGetters }, { geometryOrExtent, margin }) { - const m = margin || 20 - rootGetters.map.getView().fit(geometryOrExtent, { padding: [m, m, m, m] }) - }, - addFeatures({ commit }, { geoJSON, overwrite = false }) { - const features = new GeoJSON().readFeatures(geoJSON).map((feature) => { - const geometry = feature.getGeometry() - const radius = feature.getProperties().radius - - if (geometry?.getType() === 'Point' && radius) { - feature.setGeometry( - new Circle((geometry as Point).getCoordinates(), radius) - ) - } - return feature - }) - - if (overwrite) { - drawSource.clear() - } - drawSource.addFeatures(features) - commit('updateFeatures') - }, - initializeConfigStyle: ({ commit, getters: { configuration } }) => { - if (configuration?.style?.stroke?.color) { - commit('setSelectedStrokeColor', configuration.style.stroke.color) - } - }, - } - - return { actions, drawSource } -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/cutlery.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/cutlery.ts deleted file mode 100644 index 0ce82d76f7..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/cutlery.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { polygon, featureCollection } from '@turf/helpers' -import { buffer } from '@turf/buffer' -import { difference } from '@turf/difference' -import { unkinkPolygon } from '@turf/unkink-polygon' -import { - Feature as GeoJsonFeature, - Polygon as GeoJsonPolygon, - MultiPolygon as GeoJsonMultiPolygon, - LineString as GeoJsonLineString, -} from 'geojson' -import { booleanPointInPolygon } from '@turf/boolean-point-in-polygon' -import { lineIntersect } from '@turf/line-intersect' -import VectorSource from 'ol/source/Vector' -import Feature from 'ol/Feature' -import { GeoJSON } from 'ol/format' -import { ProjectionInfo } from './types' - -const polygonTypes = ['Polygon', 'MultiPolygon'] -export const converter = new GeoJSON() - -const cutCuttableWithCutter = ( - cuttable: GeoJsonFeature, - cutter: GeoJsonFeature -): GeoJsonFeature[] => { - // error by less than 4 water molecule widths deemed acceptable for surveying - const cutterAsPolygon = buffer(cutter, 0.000001, { - units: 'millimeters', - }) as GeoJsonFeature - const cuts = difference(featureCollection([cuttable, cutterAsPolygon])) - - if (cuts === null) { - const errorText = 'Cutting resulted in geometry destruction.' - console.error( - '@polar/plugin-draw: ', - errorText, - 'cuttable: ', - cuttable, - 'cutter: ', - cutter - ) - throw new Error(errorText) - } - - if (cuts.geometry.type === 'MultiPolygon') { - return cuts.geometry.coordinates - .map((coordinates) => polygon(coordinates)) - .map((cuttable) => cutCuttableWithCutter(cuttable, cutter)) - .flat(1) - } - - return [cuts as GeoJsonFeature] -} - -export const cutCuttablesWithCutter = ( - cuttables: GeoJsonFeature[], - cutter: GeoJsonFeature -): GeoJsonFeature[] => { - const unkinkedCuttables = cuttables.reduce((accumulator, current) => { - accumulator.push(...unkinkPolygon(current).features) - return accumulator - }, [] as GeoJsonFeature[]) - - return unkinkedCuttables - .map((cuttable) => cutCuttableWithCutter(cuttable, cutter)) - .flat(1) -} - -const intersectionCountImpliesCuttability = (count: number) => - count > 0 && count % 2 === 0 - -const wouldCutterCutCandidate = ( - cutter: GeoJsonFeature, - candidate: GeoJsonFeature -) => - // drawing may initially turn out to be a point (1-coordinate-line-string) - cutter.geometry.type === 'LineString' && - // check if candidate is actually a polygon, we don't cut anything else today - polygonTypes.includes(candidate.geometry.type) && - // start and end do not reside inside candidate (cut through, not cut in) - [ - cutter.geometry.coordinates[0], - cutter.geometry.coordinates[cutter.geometry.coordinates.length - 1], - ].every( - (coordinate) => - !booleanPointInPolygon( - coordinate, - candidate as GeoJsonFeature - ) - ) && - // cuttable is cut an even amount of times > 0, i.e. is entered and left - intersectionCountImpliesCuttability( - lineIntersect( - cutter, - candidate as GeoJsonFeature - ).features.length - ) - -export const splitByCuttability = ( - drawSource: VectorSource, - cutter: GeoJsonFeature, - projectionInfo: ProjectionInfo -): [GeoJsonFeature[], Feature[]] => - drawSource.getFeatures().reduce( - ([cuttables, uncuttables], current) => { - const currentAsGeoJson = converter.writeFeatureObject( - current, - projectionInfo - ) - if (wouldCutterCutCandidate(cutter, currentAsGeoJson)) { - cuttables.push( - currentAsGeoJson as GeoJsonFeature< - GeoJsonPolygon | GeoJsonMultiPolygon - > - ) - } else { - uncuttables.push(current) - } - return [cuttables, uncuttables] - }, - [[], []] as [ - GeoJsonFeature[], - Feature[] - ] - ) diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/index.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/index.ts deleted file mode 100644 index c5c00cf972..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { PolarActionContext } from '@polar/lib-custom-types' -import Feature from 'ol/Feature' -import { - Feature as GeoJsonFeature, - LineString as GeoJsonLineString, -} from 'geojson' -import { - CreateInteractionsPayload, - DrawState, - DrawGetters, -} from '../../../types' -import { - converter, - cutCuttablesWithCutter, - splitByCuttability, -} from './cutlery' -import { makeDraw } from './makeDraw' - -// NOTE: Lots of "as" casting in whole module due to conversions' broad returns - -const wgs84Epsg = 'EPSG:4326' - -export const createCutInteractions = ( - { - dispatch, - rootGetters, - getters, - }: PolarActionContext, - { drawSource }: CreateInteractionsPayload -) => { - const projectionInfo = { - featureProjection: rootGetters.map.getView().getProjection().getCode(), - dataProjection: wgs84Epsg, - } - const draw = makeDraw(projectionInfo, drawSource) - draw.on('drawend', (e) => { - const cutter = converter.writeFeatureObject( - e.feature, - projectionInfo - ) as GeoJsonFeature - const [cuttables, uncuttables] = splitByCuttability( - drawSource, - cutter, - projectionInfo - ) - - if (cuttables.length) { - try { - const cuts = cutCuttablesWithCutter(cuttables, cutter).map( - (cut) => converter.readFeature(cut, projectionInfo) as Feature - ) - drawSource.clear() - drawSource.addFeatures([...uncuttables, ...cuts]) - } catch { - console.error( - `@polar/plugin-draw: Cut operation failed for unknown reason.` - ) - if (getters.toastAction) { - dispatch( - getters.toastAction, - { - type: 'error', - text: 'draw.cut.error.cutFailed', - }, - { root: true } - ) - } - } - } else if (getters.toastAction) { - dispatch( - getters.toastAction, - { - type: 'info', - text: 'draw.cut.warn.unevenCut', - }, - { root: true } - ) - } - }) - - return [draw] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/makeDraw.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/makeDraw.ts deleted file mode 100644 index 5003bb2c65..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/makeDraw.ts +++ /dev/null @@ -1,31 +0,0 @@ -import Draw from 'ol/interaction/Draw' -import LineString from 'ol/geom/LineString' -import Feature from 'ol/Feature' -import { - Feature as GeoJsonFeature, - LineString as GeoJsonLineString, -} from 'geojson' -import VectorSource from 'ol/source/Vector' -import { cutStyle, styleCut } from './style' -import { ProjectionInfo } from './types' -import { converter, splitByCuttability } from './cutlery' - -export const makeDraw = ( - projectionInfo: ProjectionInfo, - drawSource: VectorSource -) => - new Draw({ - type: 'LineString', - stopClick: true, - style: (feature) => { - const cutter = converter.writeFeatureObject( - feature as Feature, - projectionInfo - ) as GeoJsonFeature - // if cut would have an effect, style accordingly - styleCut( - splitByCuttability(drawSource, cutter, projectionInfo)[0].length > 0 - ) - return cutStyle - }, - }) diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/style.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/style.ts deleted file mode 100644 index f7164bea81..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/style.ts +++ /dev/null @@ -1,43 +0,0 @@ -import Style from 'ol/style/Style' -import Stroke from 'ol/style/Stroke' -import Fill from 'ol/style/Fill' -import Circle from 'ol/style/Circle' - -const colors = { - fill: '#ecf4ff', - circleStroke: '#2b3980', - strokeIneffective: '#dc3545', - strokeEffective: '#00a645', -} -const width = 5 - -const fill = new Fill({ - color: colors.fill, -}) - -const circleStroke = new Stroke({ - color: colors.circleStroke, - width: width * 0.66, -}) - -const dashStroke = new Stroke({ - color: colors.strokeIneffective, - lineDash: [10, 15], - lineCap: 'square', - width, -}) - -export const cutStyle = new Style({ - image: new Circle({ - fill, - stroke: circleStroke, - radius: width * 1.33, - }), - fill, - stroke: dashStroke, -}) - -export const styleCut = (effective: boolean) => - dashStroke.setColor( - effective ? colors.strokeEffective : colors.strokeIneffective - ) diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/types.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/types.ts deleted file mode 100644 index 053baa5086..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createCutInteractions/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { WriteOptions } from 'ol/format/Feature' - -export type ProjectionInfo = Required< - Pick -> diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createDeleteInteractions.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createDeleteInteractions.ts deleted file mode 100644 index 2a82871b43..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createDeleteInteractions.ts +++ /dev/null @@ -1,58 +0,0 @@ -import VectorLayer from 'ol/layer/Vector' -import Interaction from 'ol/interaction/Interaction' -import { DragBox, Select } from 'ol/interaction' -import { platformModifierKeyOnly } from 'ol/events/condition' -import { PolarActionContext } from '@polar/lib-custom-types' -import { Map } from 'ol' -import { CreateInteractionsPayload, DrawGetters, DrawState } from '../../types' - -const pointerStyle = (map: Map, drawLayer: VectorLayer) => (e) => { - const found = map.hasFeatureAtPixel(e.pixel, { - layerFilter: (l) => l === drawLayer, - }) - - if (found) { - map.getTargetElement().setAttribute('style', 'cursor: pointer') - } else { - map.getTargetElement().setAttribute('style', '') - } -} - -export default function ( - { rootGetters }: PolarActionContext, - { drawSource, drawLayer }: CreateInteractionsPayload -): Interaction[] { - const selectInteraction = new Select({ layers: [drawLayer] }) - const selectedFeatures = selectInteraction.getFeatures() - const dragBoxInteraction = new DragBox({ - condition: platformModifierKeyOnly, - }) - - const boundPointerStyle = pointerStyle(rootGetters.map, drawLayer) - rootGetters.map.on('pointermove', boundPointerStyle) - - dragBoxInteraction.on('boxend', () => { - const extent = dragBoxInteraction.getGeometry().getExtent() - selectedFeatures.extend( - drawSource - .getFeaturesInExtent(extent) - .filter((feature) => feature.getGeometry()?.intersectsExtent(extent)) - ) - }) - - selectedFeatures.on(['add'], () => { - selectedFeatures.forEach((feature) => drawSource.removeFeature(feature)) - selectedFeatures.clear() - }) - - // @ts-expect-error | internal hack to detect it in other plugins - selectInteraction._isDeleteSelect = true - - // @ts-expect-error | local piggyback - selectInteraction._onRemove = () => { - rootGetters.map.un('pointermove', boundPointerStyle) - rootGetters.map.getTargetElement().setAttribute('style', '') - } - - return [selectInteraction, dragBoxInteraction] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createDrawInteractions.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createDrawInteractions.ts deleted file mode 100644 index 46f531baaa..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createDrawInteractions.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Draw, Snap } from 'ol/interaction' -import Interaction from 'ol/interaction/Interaction' -import { PolarActionContext } from '@polar/lib-custom-types' -import { CreateInteractionsPayload, DrawGetters, DrawState } from '../../types' -import createDrawStyle from '../../utils/createDrawStyle' -import { getSnaps } from './getSnaps' - -export default function ( - { - dispatch, - rootGetters, - getters: { - configuration, - drawMode, - measureMode, - selectedStrokeColor, - textInput, - textSize, - }, - }: PolarActionContext, - { drawSource }: CreateInteractionsPayload -): Promise | Interaction[] { - if (drawMode === 'Text') { - return dispatch('createTextInteractions', { - textInput, - textSize, - drawSource, - drawConfiguration: configuration, - }) - } - const style = createDrawStyle( - drawMode, - selectedStrokeColor, - measureMode, - rootGetters.map.getView().getProjection(), - configuration?.style - ) - const draw = new Draw({ - source: drawSource, - type: drawMode, - style, - }) - // @ts-expect-error | internal hack to detect it in @polar/plugin-pins and @polar/plugin-gfi - draw._isDrawPlugin = true - draw.on('drawend', (e) => e.feature.setStyle(style)) - return [ - draw, - ...getSnaps(rootGetters.map, rootGetters.configuration?.draw?.snapTo || []), - new Snap({ source: drawSource }), - ] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createDuplicateInteractions.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createDuplicateInteractions.ts deleted file mode 100644 index 679abb90b6..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createDuplicateInteractions.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PolarActionContext } from '@polar/lib-custom-types' -import { Map } from 'ol' -import { Select } from 'ol/interaction' -import { Vector } from 'ol/layer' -import { CreateInteractionsPayload, DrawGetters, DrawState } from '../../types' - -const pointerStyle = (map: Map, drawLayer: Vector) => (e) => { - const found = map.hasFeatureAtPixel(e.pixel, { - layerFilter: (l) => l === drawLayer, - }) - - if (found) { - map.getTargetElement().setAttribute('style', 'cursor: copy') - } else { - map.getTargetElement().setAttribute('style', '') - } -} - -export function createDuplicateInteractions( - { rootGetters }: PolarActionContext, - { drawSource, drawLayer }: CreateInteractionsPayload -) { - const selectInteraction = new Select({ layers: [drawLayer], style: null }) - const selectedFeatures = selectInteraction.getFeatures() - // TODO temp solution, not actually _isDeleteSelect; normalizing these flags is part of a future effort - // @ts-expect-error | internal hack to detect it in @polar/plugin-pins and @polar/plugin-gfi - selectInteraction._isDeleteSelect = true - - const boundPointerStyle = pointerStyle(rootGetters.map, drawLayer) - rootGetters.map.on('pointermove', boundPointerStyle) - - selectedFeatures.on('add', () => { - drawSource.addFeature(selectedFeatures.getArray()[0].clone()) - selectedFeatures.clear() - }) - - // @ts-expect-error | local piggyback - selectInteraction._onRemove = () => { - rootGetters.map.un('pointermove', boundPointerStyle) - rootGetters.map.getTargetElement().setAttribute('style', '') - } - - return [selectInteraction] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createLassoInteractions.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createLassoInteractions.ts deleted file mode 100644 index f9f340f022..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createLassoInteractions.ts +++ /dev/null @@ -1,149 +0,0 @@ -import Draw from 'ol/interaction/Draw' -import i18next from 'i18next' -import { GeoJSON } from 'ol/format' -import { booleanContains } from '@turf/boolean-contains' -import { rawLayerList } from '@masterportal/masterportalapi' -import { PolarActionContext } from '@polar/lib-custom-types' -import { Feature } from 'ol' -import { Polygon } from 'ol/geom' -import { - parseWfsResponse, - getVectorFeaturesByFeatureRequest, -} from '@polar/lib-get-features' -import { - FeatureCollection, - Feature as GeoJsonFeature, - Geometry, - GeometryCollection, -} from 'geojson' -import { DrawGetters, DrawState } from '../../types' - -let loaderKeyCounter = 0 -const loaderKeyBase = 'draw-lasso-load' - -const requestError = `@polar/plugin-draw: An error occurred on creating the lasso request: ` -const rejectedError = - '@polar/plugin-draw: The response to a lasso request indicated an error.' -const parseError = - '@polar/plugin-draw: Client failure in reading responses in lasso action.' -const internalError = () => ({ - type: 'error', - text: i18next.t('plugins.draw.lasso.internalError'), -}) - -const buildAddFeaturesPayload = ( - featureCollections: FeatureCollection[], - drawnLasso: Feature -) => { - const drawnLassoGeoJson = JSON.parse(new GeoJSON().writeFeature(drawnLasso)) - - return { - geoJSON: { - type: 'FeatureCollection', - features: featureCollections - .reduce( - (accumulator, { features }) => accumulator.concat(features), - [] as GeoJsonFeature[] - ) - .filter((feature) => { - if (feature.geometry.type.startsWith('Multi')) { - return ( - // since .type on GeometryCollection doesn't start with 'Multi' - ( - feature.geometry as Exclude - ).coordinates.every((partialCoordinates) => - booleanContains(drawnLassoGeoJson, { - type: 'Feature', - geometry: { - type: feature.geometry.type.slice(5), // un«Multi»ed - coordinates: partialCoordinates, - }, - properties: {}, - }) - ) - ) - } - return booleanContains(drawnLassoGeoJson, feature) - }), - }, - } -} - -export default function ({ - rootGetters, - getters, - commit, - dispatch, -}: PolarActionContext) { - const draw = new Draw({ type: 'Polygon', freehand: true }) - - draw.on('drawend', (e) => { - const toast = (toastObject) => - getters.toastAction && - dispatch(getters.toastAction, toastObject, { root: true }) - const drawnLasso = e.feature as Feature // due to Draw 'type' param - const requests = getters.activeLassoIds.reduce((accumulator, id) => { - try { - const request = getVectorFeaturesByFeatureRequest({ - feature: drawnLasso, - fetchLayerId: id, - projectionCode: rootGetters.map.getView().getProjection().getCode(), - }) - accumulator.push(request) - } catch (e) { - console.error(requestError, e) - toast(internalError()) - } - return accumulator - }, [] as Promise[]) - - let loaderKey - if (getters.configuration.addLoading) { - loaderKey = `${loaderKeyBase}-${loaderKeyCounter++}` - commit(getters.configuration.addLoading, loaderKey, { root: true }) - } - - Promise.allSettled(requests) - .then((settledRequests) => - Promise.all( - ( - settledRequests.filter((promiseSettledResult, index) => { - if (promiseSettledResult.status === 'rejected') { - console.error(rejectedError, promiseSettledResult.reason) - toast({ - type: 'error', - text: i18next.t('plugins.draw.lasso.layerRejected', { - id: getters.activeLassoIds[index], - }), - }) - return false - } - return true - }) as PromiseFulfilledResult[] - ).map(async (result, index) => - rawLayerList.getLayerWhere({ id: getters.activeLassoIds[index] }) - .typ === 'WFS' - ? await parseWfsResponse(result.value, undefined, false) - : ((await result.value.json()) as FeatureCollection) - ) - ) - ) - .then((featureCollections) => - dispatch( - 'addFeatures', - buildAddFeaturesPayload(featureCollections, drawnLasso) - ) - ) - .catch((error) => { - console.error(parseError, error) - toast(internalError()) - }) - .finally( - () => - getters.configuration.removeLoading && - commit(getters.configuration.removeLoading, loaderKey, { root: true }) - ) - }) - - return [draw] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createMergeInteractions.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createMergeInteractions.ts deleted file mode 100644 index aab0f4c2b8..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createMergeInteractions.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { PolarActionContext } from '@polar/lib-custom-types' -import { getSnaps } from '@polar/plugin-draw' -import Draw from 'ol/interaction/Draw' -import Snap from 'ol/interaction/Snap' -import { Feature as GeoJsonFeature, MultiPolygon, Polygon } from 'geojson' -import { booleanIntersects } from '@turf/boolean-intersects' -import { union } from '@turf/union' -import { GeoJSON } from 'ol/format' -import Feature from 'ol/Feature' -import { CreateInteractionsPayload, DrawState, DrawGetters } from '../../types' - -const converter = new GeoJSON() - -export const createMergeInteractions = ( - { rootGetters }: PolarActionContext, - { drawSource }: CreateInteractionsPayload -) => { - const draw = new Draw({ type: 'Polygon' }) - // @ts-expect-error | internal hack to detect it in @polar/plugin-pins and @polar/plugin-gfi - draw._isDrawPlugin = true - - draw.on('drawend', (e) => { - const mergePolygon = converter.writeFeatureObject( - e.feature - ) as GeoJsonFeature - const drawFeatures = drawSource.getFeatures() - - // sort everything into a.) things to combine b.) things not to touch - const intersectedPolygons: GeoJsonFeature[] = [] - const nextFeatures = drawFeatures.reduce((accumulator, drawFeature) => { - const type = drawFeature.getGeometry()?.getType() ?? '' - const drawFeatureAsGeoJson = converter.writeFeatureObject(drawFeature) - if ( - ['Polygon', 'MultiPolygon'].includes(type) && - booleanIntersects(mergePolygon, drawFeatureAsGeoJson) - ) { - intersectedPolygons.push( - drawFeatureAsGeoJson as GeoJsonFeature - ) - } else { - accumulator.push(drawFeature) - } - return accumulator - }, [] as Feature[]) - - const mergedFeature = union({ - type: 'FeatureCollection', - features: [mergePolygon, ...intersectedPolygons], - }) - - nextFeatures.push(converter.readFeature(mergedFeature) as Feature) - - drawSource.clear() - drawSource.addFeatures(nextFeatures) - }) - - return [ - draw, - ...getSnaps(rootGetters.map, rootGetters.configuration?.draw?.snapTo || []), - new Snap({ source: drawSource }), - ] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createModifyInteractions.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createModifyInteractions.ts deleted file mode 100644 index 02913301cc..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createModifyInteractions.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Modify, Select, Snap } from 'ol/interaction' -import Interaction from 'ol/interaction/Interaction' -import { PolarActionContext } from '@polar/lib-custom-types' -import { Collection, Feature, Map } from 'ol' -import { CreateInteractionsPayload, DrawGetters, DrawState } from '../../types' -import { makeLocalSelector } from './localSelector' -import { getSnaps } from './getSnaps' - -const createModify = ( - map: Map, - drawLayer: CreateInteractionsPayload['drawLayer'] -) => { - const activeContainer = { active: false } - const features: Collection = new Collection() - const modify = new Modify({ features }) - modify.set('_isPolarDragLikeInteraction', true, true) - modify.on('modifystart', () => { - activeContainer.active = true - }) - modify.on('modifyend', () => { - activeContainer.active = false - }) - - const localSelector = makeLocalSelector( - map, - activeContainer, - features, - drawLayer - ) - map.on('pointermove', localSelector) - // @ts-expect-error | "un on removal" riding piggyback as _onRemove - modify._onRemove = () => map.un('pointermove', localSelector) - - return modify -} - -export default function ( - { - commit, - dispatch, - getters, - rootGetters, - }: PolarActionContext, - { drawSource, drawLayer }: CreateInteractionsPayload -): Interaction[] { - const { drawMode } = getters - // clear input - no feature selected initially - commit('setTextInput', '') - const select = new Select({ - layers: [drawLayer], - style: null, - hitTolerance: 50, - }) - let lastSelectedFeature - select.on('select', (event) => { - if (event.selected.length > 0) { - lastSelectedFeature = event.selected[event.selected.length - 1] - commit('setSelectedFeature', lastSelectedFeature) - const featureStyle = lastSelectedFeature.getStyle() - dispatch( - featureStyle && 'getText' in featureStyle && featureStyle.getText() - ? 'modifyTextStyle' - : 'modifyDrawStyle', - featureStyle - ) - } else if (event.selected.length === 0) { - if (lastSelectedFeature && lastSelectedFeature.get('text') === '') { - drawSource.removeFeature(lastSelectedFeature) - commit('updateFeatures') - } - if (drawMode === 'Text') { - commit('setTextInput', '') - commit('setSelectedFeature', null) - } - } - }) - return [ - createModify(rootGetters.map, drawLayer), - ...getSnaps(rootGetters.map, rootGetters.configuration?.draw?.snapTo || []), - new Snap({ source: drawSource }), - select, - ] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createTextInteractions.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createTextInteractions.ts deleted file mode 100644 index acbf2f7034..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createTextInteractions.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Draw, Snap } from 'ol/interaction' -import Interaction from 'ol/interaction/Interaction' -import VectorSource from 'ol/source/Vector' -import { DrawConfiguration, PolarActionContext } from '@polar/lib-custom-types' -import { DrawGetters, DrawState } from '../../types' -import { createTextStyle } from '../../utils/createTextStyle' -import { getSnaps } from './getSnaps' - -interface CreateTextInteractionsPayload { - textInput: DrawState['textInput'] - textSize: DrawGetters['textSize'] - drawSource: VectorSource - drawConfiguration?: DrawConfiguration -} - -export default function ( - { rootGetters }: PolarActionContext, - { - textInput, - textSize, - drawSource, - drawConfiguration, - }: CreateTextInteractionsPayload -): Interaction[] { - if (!textInput) { - // nothing to draw yet - return [] - } - const textStyle = createTextStyle( - textInput, - drawConfiguration?.textStyle, - textSize - ) - const draw = new Draw({ - source: drawSource, - type: 'Point', - style: textStyle, - }) - // @ts-expect-error | internal hack to detect it in @polar/plugin-pins and @polar/plugin-gfi - draw._isDrawPlugin = true - draw.on('drawend', function (e) { - e.feature.setStyle(textStyle) - e.feature.set('text', textInput) - }) - // prevent the creation of empty text features - drawSource.on('addfeature', (event) => { - if (event.feature?.get('text') && !event.feature.get('text').trim()) { - drawSource.removeFeature(event.feature) - } - }) - return [ - draw, - ...getSnaps(rootGetters.map, rootGetters.configuration?.draw?.snapTo || []), - new Snap({ source: drawSource }), - ] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/createTranslateInteractions.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/createTranslateInteractions.ts deleted file mode 100644 index de143cdfae..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/createTranslateInteractions.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Translate, Snap } from 'ol/interaction' -import Interaction from 'ol/interaction/Interaction' -import { PolarActionContext } from '@polar/lib-custom-types' -import { Collection, Feature, Map } from 'ol' -import { CreateInteractionsPayload, DrawGetters, DrawState } from '../../types' -import { makeLocalSelector } from './localSelector' -import { getSnaps } from './getSnaps' - -const createTranslate = ( - map: Map, - drawLayer: CreateInteractionsPayload['drawLayer'] -) => { - const activeContainer = { active: false } - const features: Collection = new Collection() - const translate = new Translate({ features }) - translate.set('_isPolarDragLikeInteraction', true, true) - translate.on('translatestart', () => { - activeContainer.active = true - }) - translate.on('translateend', () => { - activeContainer.active = false - }) - - const localSelector = makeLocalSelector( - map, - activeContainer, - features, - drawLayer - ) - map.on('pointermove', localSelector) - // @ts-expect-error | "un on removal" riding piggyback as _onRemove - translate._onRemove = () => map.un('pointermove', localSelector) - - return translate -} - -export default function ( - { rootGetters }: PolarActionContext, - { drawSource, drawLayer }: CreateInteractionsPayload -): Interaction[] { - return [ - createTranslate(rootGetters.map, drawLayer), - ...getSnaps(rootGetters.map, rootGetters.configuration?.draw?.snapTo || []), - new Snap({ source: drawSource }), - ] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/getSnaps.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/getSnaps.ts deleted file mode 100644 index 27c849bc22..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/getSnaps.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Map } from 'ol' -import { Snap } from 'ol/interaction' -import VectorLayer from 'ol/layer/Vector' -import VectorSource from 'ol/source/Vector' - -export const getSnaps = (map: Map, snapIds: string[]): Snap[] => - snapIds.reduce((accumulator, layerId) => { - const layer = map - .getLayers() - .getArray() - .find((layer) => layer.get('id') === layerId) as VectorLayer - const source = layer?.getSource?.() - if (source instanceof VectorSource) { - const snap = new Snap({ source }) - const visibilityToggler = () => snap.setActive(layer.getVisible()) - layer.on('propertychange', visibilityToggler) - visibilityToggler() - // @ts-expect-error | riding piggyback - snap._onRemove = () => layer.un('propertychange', visibilityToggler) - accumulator.push(snap) - } else { - console.warn( - `@polar/plugin-draw: Layer with ID "${layerId}" configured for 'snapTo', but it has no source to snap to. The layer does probably not hold any vector data.` - ) - } - return accumulator - }, [] as Snap[]) diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/index.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/index.ts deleted file mode 100644 index 740471d23f..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Interaction from 'ol/interaction/Interaction' -import { PolarActionContext } from '@polar/lib-custom-types' -import { CreateInteractionsPayload, DrawGetters, DrawState } from '../../types' - -export default function ( - { dispatch, getters: { mode } }: PolarActionContext, - { drawSource, drawLayer }: CreateInteractionsPayload -): Interaction[] | Promise { - if (mode === 'draw') { - return dispatch('createDrawInteractions', { drawSource }) - } else if (mode === 'edit') { - return dispatch('createModifyInteractions', { drawSource, drawLayer }) - } else if (mode === 'translate') { - return dispatch('createTranslateInteractions', { drawSource, drawLayer }) - } else if (mode === 'delete') { - return dispatch('createDeleteInteractions', { drawSource, drawLayer }) - } else if (mode === 'lasso') { - return dispatch('createLassoInteractions') - } else if (mode === 'duplicate') { - return dispatch('createDuplicateInteractions', { drawSource, drawLayer }) - } else if (mode === 'cut') { - return dispatch('createCutInteractions', { drawSource }) - } else if (mode === 'merge') { - return dispatch('createMergeInteractions', { drawSource }) - } - return [] -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/localSelector.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/localSelector.ts deleted file mode 100644 index d282b29039..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/localSelector.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Collection, Feature, Map, MapBrowserEvent } from 'ol' -import { CreateInteractionsPayload } from '../../types' - -/* sets the topmost hovered feature as singleton feature in collection */ -export const makeLocalSelector = - ( - map: Map, - activeContainer: { active: boolean }, - features: Collection, - drawLayer: CreateInteractionsPayload['drawLayer'] - ) => - // bound event processor - (e: MapBrowserEvent) => { - if (!activeContainer.active) { - map.forEachFeatureAtPixel( - e.pixel, - (f) => { - if (f !== features.item(0)) { - features.setAt(0, f as Feature) - } - return true - }, - { - layerFilter: (l) => l === drawLayer, - } - ) - } - } diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/modifyDrawStyle.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/modifyDrawStyle.ts deleted file mode 100644 index 218bc7d8a6..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/modifyDrawStyle.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PolarActionContext } from '@polar/lib-custom-types' -import { Style } from 'ol/style' -import { Color } from 'ol/color' -import { ColorLike } from 'ol/colorlike' -import { DrawGetters, DrawState } from '../../types' - -export default function ( - { commit }: PolarActionContext, - featureStyle: Style -): void { - let strokeColor: Color | ColorLike - if (featureStyle && 'getImage' in featureStyle && featureStyle.getImage()) { - // @ts-expect-error | For some reason getStroke is not defined on the type but is callable. - strokeColor = featureStyle.getImage().getStroke().getColor() - } else { - strokeColor = featureStyle.getStroke()?.getColor() || 'black' - } - commit('setSelectedStrokeColor', strokeColor) -} diff --git a/vue2/packages/plugins/Draw/src/store/createInteractions/modifyTextStyle.ts b/vue2/packages/plugins/Draw/src/store/createInteractions/modifyTextStyle.ts deleted file mode 100644 index 1759cf094a..0000000000 --- a/vue2/packages/plugins/Draw/src/store/createInteractions/modifyTextStyle.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PolarActionContext } from '@polar/lib-custom-types' -import { Style } from 'ol/style' -import { DrawGetters, DrawState } from '../../types' - -export default function ( - { commit, getters }: PolarActionContext, - featureStyle: Style -): void { - const featureText = featureStyle.getText()?.getText() - const font = featureStyle.getText()?.getFont() as string - // set selectedSize of feature to prevent unintentional size change - const fontSize = font.match(/\b\d+(?:.\d+)?/) - commit('setSelectedSize', getters.fontSizes.indexOf(Number(fontSize))) - commit('setTextInput', featureText) -} diff --git a/vue2/packages/plugins/Draw/src/store/index.ts b/vue2/packages/plugins/Draw/src/store/index.ts deleted file mode 100644 index f2d338eff9..0000000000 --- a/vue2/packages/plugins/Draw/src/store/index.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { - generateSimpleGetters, - generateSimpleMutations, -} from '@repositoryname/vuex-generators' -import { Feature } from 'geojson' -import { PolarModule } from '@polar/lib-custom-types' -import noop from '@repositoryname/noop' -import { Circle, LineString, Point, Polygon } from 'ol/geom' -import { DrawGetters, DrawMutations, DrawState } from '../types' -import { makeActions } from './actions' -import { inactive } from './reviseFeatures/revisionStates' - -const getInitialState = (): DrawState => ({ - mode: 'none', - drawMode: 'Point', - textInput: '', - selectedSize: 0, - featureCollection: { - type: 'FeatureCollection', - features: [], - }, - revisedFeatureCollection: { - type: 'FeatureCollection', - features: [], - }, - featureCollectionRevisionState: inactive, - selectedFeature: 1, - selectedStrokeColor: '#000000', - measureMode: 'none', -}) - -export const makeStoreModule = () => { - // NOTE hack to keep complex objects out of vuex - let selectedFeature = null - - const { actions, drawSource } = makeActions() - - const storeModule: PolarModule = { - namespaced: true, - state: getInitialState(), - actions, - getters: { - ...generateSimpleGetters(getInitialState()), - drawSource() { - return drawSource - }, - selectableDrawModes(_, { configuration }) { - /* eslint-disable @typescript-eslint/naming-convention */ - // NOTE: Keys are directly used as technical keys for ol/interaction/Draw and are allowed to differ from the naming scheme. - const allSelectableDrawModes = { - Circle: 'plugins.draw.drawMode.circle', - LineString: 'plugins.draw.drawMode.lineString', - Point: 'plugins.draw.drawMode.point', - Polygon: 'plugins.draw.drawMode.polygon', - Text: 'plugins.draw.drawMode.text', - } - /* eslint-enable @typescript-eslint/naming-convention */ - - // TODO: Do something different than eslint ignore - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { Text, ...defaultSelectableDrawModes } = allSelectableDrawModes - - return configuration.selectableDrawModes - ? configuration.selectableDrawModes.reduce( - (acc, curr) => ({ ...acc, [curr]: allSelectableDrawModes[curr] }), - {} - ) - : defaultSelectableDrawModes - }, - selectedFeature: (state) => { - noop(state.selectedFeature) - return selectedFeature - }, - selectableModes(_, { configuration }) { - const includesWrite = - configuration.selectableDrawModes?.includes('Text') - const includesMeasure = configuration.measureOptions !== undefined - let drawLabel = 'draw' - if (includesWrite && includesMeasure) { - drawLabel = 'writeAndMeasure' - } else if (includesWrite) { - drawLabel = 'write' - } else if (includesMeasure) { - drawLabel = 'measure' - } - const modes = [ - ['none', 'plugins.draw.mode.none'], - ['draw', `plugins.draw.mode.${drawLabel}`], - ['edit', 'plugins.draw.mode.edit'], - ['translate', 'plugins.draw.mode.translate'], - ['duplicate', 'plugins.draw.mode.duplicate'], - ['cut', 'plugins.draw.mode.cut'], - ['merge', 'plugins.draw.mode.merge'], - ['delete', 'plugins.draw.mode.delete'], - ] - if (configuration.lassos) { - modes.splice(4, 0, ['lasso', 'plugins.draw.mode.lasso']) - } - return Object.fromEntries(modes) - }, - activeLassoIds: (_, { configuration }, __, rootGetters) => - (configuration.lassos || []).reduce( - (accumulator, { id, minZoom = true }) => { - const layerConfig = rootGetters.configuration.layers?.find( - (layer) => id === layer.id - ) - if ( - minZoom && - layerConfig && - typeof layerConfig.minZoom !== 'undefined' && - rootGetters.zoomLevel < layerConfig.minZoom - ) { - return accumulator - } - accumulator.push(id) - return accumulator - }, - [] as string[] - ), - toastAction: (_, { configuration }) => configuration.toastAction || '', - measureOptions: (_, { configuration }) => - configuration.measureOptions || {}, - selectableMeasureModes: (_, { drawMode, measureOptions }) => - (drawMode === 'LineString' - ? Object.entries(measureOptions).filter( - ([option]) => option !== 'hectares' - ) - : Object.entries(measureOptions) - ) - .filter((option) => option[1] === true) - .reduce( - (acc, [option]) => ({ - ...acc, - [option]: - `plugins.draw.measureMode.${option}` + - (drawMode === 'Polygon' && option !== 'hectares' ? 'Area' : ''), - }), - { none: 'plugins.draw.measureMode.none' } - ), - showMeasureOptions: ({ drawMode, mode }, { measureOptions }) => - measureOptions && - Object.values(measureOptions).some((option) => option === true) && - mode === 'draw' && - ['LineString', 'Polygon'].includes(drawMode), - showTextInput({ drawMode, mode }, { selectedFeature }) { - return ( - (drawMode === 'Text' && mode === 'draw') || - (mode === 'edit' && - selectedFeature && - typeof selectedFeature.get('text') === 'string') - ) - }, - showDrawOptions( - { mode }, - { configuration, showTextInput, selectedFeature } - ) { - return ( - configuration.enableOptions && - !showTextInput && - (mode === 'draw' || (mode === 'edit' && selectedFeature)) - ) - }, - configuration(_, __, ___, rootGetters) { - return rootGetters.configuration.draw || {} - }, - fontSizes(_, { configuration }) { - const { textStyle } = configuration - if (textStyle?.font) { - if (typeof textStyle.font === 'string') { - return [] - } - return textStyle.font.size ? textStyle.font.size.slice(0, 5) : [10] - } - return [] - }, - showSizeSlider(_, { fontSizes, showTextInput }) { - return showTextInput && fontSizes.length > 1 - }, - textSize(_, { fontSizes, selectedSize }) { - return fontSizes[selectedSize] - }, - }, - mutations: { - ...generateSimpleMutations(getInitialState()), - updateFeatures(state) { - const features = drawSource.getFeatures().map((feature) => { - const geometry = feature.getGeometry() as - | Circle - | LineString - | Point - | Polygon - const type = geometry.getType() - const isCircle = type === 'Circle' - const jsonFeature: Feature = { - type: 'Feature', - properties: Object.fromEntries( - Object.entries(feature.getProperties()).filter( - ([property]) => property !== 'geometry' - ) - ), - geometry: { - // @ts-expect-error | A LinearRing can currently not be drawn - type: isCircle ? 'Point' : type, - // @ts-expect-error | The coordinates are in the correct format - coordinates: isCircle - ? (geometry as Circle).getCenter() - : geometry.getCoordinates(), - }, - } - // NOTE: If one is checking if properties exists (which it clearly does), TS complains - // "TS2531: Object is possibly 'null'.". This is due to the structure of the type GeoJsonProperties. - if (isCircle && jsonFeature.properties) { - jsonFeature.properties.radius = (geometry as Circle).getRadius() - } - return jsonFeature - }) - state.featureCollection = { ...state.featureCollection, features } - }, - setSelectedFeature: (state, payload) => { - selectedFeature = payload - state.selectedFeature = state.selectedFeature + 1 - }, - } as DrawMutations, - } - - return storeModule -} diff --git a/vue2/packages/plugins/Draw/src/store/reviseFeatures/autofix.ts b/vue2/packages/plugins/Draw/src/store/reviseFeatures/autofix.ts deleted file mode 100644 index 77ac53505e..0000000000 --- a/vue2/packages/plugins/Draw/src/store/reviseFeatures/autofix.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - FeatureCollection, - Feature as GeoJsonFeature, - MultiPolygon, - Polygon, -} from 'geojson' -import { cleanCoords } from '@turf/clean-coords' -import { unkinkPolygon } from '@turf/unkink-polygon' -import { GeometryType } from '../../types' - -export const autofixFeatureCollection = ( - revisedFeatureCollection: FeatureCollection -) => ({ - ...revisedFeatureCollection, - features: revisedFeatureCollection.features.reduce((accumulator, feature) => { - if (['Polygon', 'MultiPolygon'].includes(feature.geometry.type)) { - accumulator.push( - ...unkinkPolygon( - cleanCoords(feature) as GeoJsonFeature - ).features - ) - } else { - accumulator.push(cleanCoords(feature)) - } - return accumulator - }, [] as GeoJsonFeature[]), -}) diff --git a/vue2/packages/plugins/Draw/src/store/reviseFeatures/cloneFeatureCollection.ts b/vue2/packages/plugins/Draw/src/store/reviseFeatures/cloneFeatureCollection.ts deleted file mode 100644 index db499f17f5..0000000000 --- a/vue2/packages/plugins/Draw/src/store/reviseFeatures/cloneFeatureCollection.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FeatureCollection } from 'geojson' -import { GeometryType } from '../../types' - -// FeatureCollection is compatible to stupid clone -export const cloneFeatureCollection = ( - // No GeometryCollection from Draw, hence the - featureCollection: FeatureCollection -): FeatureCollection => - JSON.parse(JSON.stringify(featureCollection)) diff --git a/vue2/packages/plugins/Draw/src/store/reviseFeatures/enrichWithMetaServices.ts b/vue2/packages/plugins/Draw/src/store/reviseFeatures/enrichWithMetaServices.ts deleted file mode 100644 index a229baeae4..0000000000 --- a/vue2/packages/plugins/Draw/src/store/reviseFeatures/enrichWithMetaServices.ts +++ /dev/null @@ -1,106 +0,0 @@ -import compare from 'just-compare' -import { FeatureCollection, Feature as GeoJsonFeature } from 'geojson' -import { Feature, Map } from 'ol' -import { GeoJSON } from 'ol/format' -import { - getVectorFeaturesByFeatureRequest, - parseWfsResponse, -} from '@polar/lib-get-features' -import { rawLayerList } from '@masterportal/masterportalapi' -import { booleanIntersects } from '@turf/boolean-intersects' -import { DrawMetaService } from '@polar/lib-custom-types' -import { GeometryType } from '../../types' - -const reader = new GeoJSON() - -type Aggregator = ( - propertiesArray: Exclude[] -) => GeoJsonFeature['properties'][] | GeoJsonFeature['properties'] - -const aggregators: Record< - Required['aggregationMode'], - Aggregator -> = { - all: (x) => x, - unequal: (propertiesArray) => - propertiesArray.reduce((accumulator, current) => { - if (accumulator.every((entry) => !compare(entry, current))) { - accumulator.push(current) - } - return accumulator - }, []), -} - -const filterProperties = ( - properties: Exclude, - propertyNames?: string[] -) => - propertyNames && propertyNames.length - ? Object.fromEntries( - Object.entries(properties).filter(([key]) => - propertyNames.includes(key) - ) - ) - : properties - -const aggregateProperties = ( - propertiesArray: GeoJsonFeature['properties'][], - propertyNames?: string[], - mode: DrawMetaService['aggregationMode'] = 'unequal' -) => - aggregators[mode]( - propertiesArray - .filter((properties) => properties !== null) - .map((properties) => filterProperties(properties, propertyNames)) - ) - -/** @throws */ -export const enrichWithMetaServices = ( - featureCollection: FeatureCollection, - map: Map, - metaServices: DrawMetaService[], - signal: AbortSignal -): Promise[]> => - Promise.all( - featureCollection.features.map(async (feature) => ({ - ...feature, - properties: { - ...feature.properties, - metaProperties: { - ...(feature.properties?.metaProperties || {}), - ...Object.fromEntries( - await Promise.all( - metaServices.map(({ id, propertyNames, aggregationMode }) => - getVectorFeaturesByFeatureRequest({ - feature: reader.readFeature( - JSON.stringify(feature) - ) as unknown as Feature, - fetchLayerId: id, - projectionCode: map.getView().getProjection().getCode(), - signal, - }) - .then((response) => - rawLayerList.getLayerWhere({ id }).typ === 'WFS' - ? parseWfsResponse(response, undefined, false) - : (response.json() as Promise) - ) - .then((featuresFromBbox) => { - const applicableProperties = featuresFromBbox.features - .filter((featureFromBbox) => - booleanIntersects(featureFromBbox, feature) - ) - .map(({ properties }) => properties) - const aggregatedProperties = aggregateProperties( - applicableProperties, - propertyNames, - aggregationMode - ) - return [id, aggregatedProperties] - }) - ) - ) - ), - }, - }, - })) - ) diff --git a/vue2/packages/plugins/Draw/src/store/reviseFeatures/index.ts b/vue2/packages/plugins/Draw/src/store/reviseFeatures/index.ts deleted file mode 100644 index 62c7763752..0000000000 --- a/vue2/packages/plugins/Draw/src/store/reviseFeatures/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { PolarActionContext } from '@polar/lib-custom-types' -import type { FeatureCollection } from 'geojson' -import type { DrawGetters, DrawState, GeometryType } from '../../types' -import { complete, error, inProgress } from './revisionStates' -import { autofixFeatureCollection } from './autofix' -import { cloneFeatureCollection } from './cloneFeatureCollection' -import { enrichWithMetaServices } from './enrichWithMetaServices' -import { mergeToMultiGeometries } from './mergeToMultiGeometries' -import { validateGeoJson } from './validateGeoJson' - -let abortController: AbortController | null = null - -export const reviseFeatures = async ({ - commit, - dispatch, - rootGetters, - getters, -}: PolarActionContext) => { - const { revision } = getters.configuration - if (!revision) { - return - } - - commit('setFeatureCollectionRevisionState', inProgress) - - if (abortController) { - abortController.abort() - } - const thisController = (abortController = new AbortController()) - - // clone to prevent accidentally messing with the draw tool's data - let revisedFeatureCollection = cloneFeatureCollection( - getters.featureCollection as FeatureCollection - ) - - if (revision.autofix) { - try { - revisedFeatureCollection = autofixFeatureCollection( - revisedFeatureCollection - ) - } catch { - commit('setFeatureCollectionRevisionState', error) - console.warn( - `@polar/plugin-draw: Autofix failed since entered geometries were not valid and fixable. This may e.g. result from pulling points of a polygon in edit mode together until they're point-shaped.` - ) - return - } - } - - // merge first; relevant for both follow-up steps - if (revision.mergeToMultiGeometries) { - // TODO: turf provides "union" and "combine" methods, probably just use them - revisedFeatureCollection = mergeToMultiGeometries(revisedFeatureCollection) - } - - if (revision.validate) { - revisedFeatureCollection = validateGeoJson(revisedFeatureCollection) - } - - if (revision.metaServices?.length) { - try { - revisedFeatureCollection.features = await enrichWithMetaServices( - revisedFeatureCollection, - rootGetters.map, - revision.metaServices, - abortController.signal - ) - } catch (e) { - if (thisController.signal.aborted) { - return - } - console.error( - '@polar/plugin-draw: An error occurred when trying to fetch meta service data for the given feature collection.', - e - ) - if (getters.toastAction) { - dispatch( - getters.toastAction, - { - type: 'warning', - text: 'plugins.draw.metaInformationRetrieval.errorToast', - timeout: 10000, - }, - { root: true } - ) - } - } - } - - if (!thisController.signal.aborted) { - commit('setRevisedFeatureCollection', revisedFeatureCollection) - commit('setFeatureCollectionRevisionState', complete) - } -} diff --git a/vue2/packages/plugins/Draw/src/store/reviseFeatures/mergeToMultiGeometries.ts b/vue2/packages/plugins/Draw/src/store/reviseFeatures/mergeToMultiGeometries.ts deleted file mode 100644 index a6f1a8384b..0000000000 --- a/vue2/packages/plugins/Draw/src/store/reviseFeatures/mergeToMultiGeometries.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - Feature, - FeatureCollection, - GeoJsonTypes, - Geometry, - GeometryCollection, -} from 'geojson' - -type GeometryType = Exclude - -const isMulti = (type: GeometryType['type']) => type.startsWith('Multi') -const multi = (type: GeometryType['type']) => - (isMulti(type) ? type : `Multi${type}`) as - | 'MultiPoint' - | 'MultiLineString' - | 'MultiPolygon' - -const mergeBin = (features: Feature[]): Feature[] => - !features.length - ? [] - : [ - { - ...features[0], - geometry: { - type: multi(features[0].geometry.type), - coordinates: [ - ...features - .map(({ geometry }) => - isMulti(geometry.type) - ? geometry.coordinates - : [geometry.coordinates] - ) - .flat(1), - ], - }, - } as Feature, - ] - -const getGeometryBin = (type: GeoJsonTypes) => - type.endsWith('Point') - ? 'points' - : type.endsWith('LineString') - ? 'lineStrings' - : type.endsWith('Polygon') - ? 'polygons' - : '' - -export const mergeToMultiGeometries = ( - featureCollection: FeatureCollection -): FeatureCollection => { - const bins = featureCollection.features.reduce( - (accumulator, current) => { - const bin = getGeometryBin(current.geometry.type) - if (bin) { - accumulator[bin].push(current) - } else { - console.warn( - `@polar/plugin-draw: Unsupported geometry input "${current.geometry.type}" in multi geometry merge skipped.` - ) - } - return accumulator - }, - { - points: [], - lineStrings: [], - polygons: [], - } as Record<'points' | 'lineStrings' | 'polygons', Feature[]> - ) - - return { - ...featureCollection, - features: [ - ...mergeBin(bins.points), - ...mergeBin(bins.lineStrings), - ...mergeBin(bins.polygons), - ], - } -} diff --git a/vue2/packages/plugins/Draw/src/store/reviseFeatures/revisionStates.ts b/vue2/packages/plugins/Draw/src/store/reviseFeatures/revisionStates.ts deleted file mode 100644 index 38312fe245..0000000000 --- a/vue2/packages/plugins/Draw/src/store/reviseFeatures/revisionStates.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const inactive = 'inactive' -export const inProgress = 'inProgress' -export const complete = 'complete' -export const error = 'error' diff --git a/vue2/packages/plugins/Draw/src/store/reviseFeatures/validateGeoJson.ts b/vue2/packages/plugins/Draw/src/store/reviseFeatures/validateGeoJson.ts deleted file mode 100644 index a34ab11c30..0000000000 --- a/vue2/packages/plugins/Draw/src/store/reviseFeatures/validateGeoJson.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FeatureCollection } from 'geojson' -import { booleanValid } from '@turf/boolean-valid' -import { GeometryType } from '../../types' - -export const validateGeoJson = ( - featureCollection: FeatureCollection -): FeatureCollection => ({ - ...featureCollection, - features: featureCollection.features.map((feature) => ({ - ...feature, - properties: { - ...(feature.properties ?? {}), - sfaValidity: booleanValid(feature), - }, - })), -}) diff --git a/vue2/packages/plugins/Draw/src/types.ts b/vue2/packages/plugins/Draw/src/types.ts deleted file mode 100644 index c66e785175..0000000000 --- a/vue2/packages/plugins/Draw/src/types.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Mutation, MutationTree } from 'vuex' -import { FeatureCollection, Geometry, GeometryCollection } from 'geojson' -import { Feature } from 'ol' -import VectorLayer from 'ol/layer/Vector' -import VectorSource from 'ol/source/Vector' -import { StyleLike } from 'ol/style/Style' -import { - DrawConfiguration, - DrawMode, - MeasureMode, - MeasureOptions, -} from '@polar/lib-custom-types' -import { - inactive, - inProgress, - complete, - error, -} from './store/reviseFeatures/revisionStates' - -export type GeometryType = Exclude - -// The options that can be given to an ol/VectorLayer. Somehow the direct import from ol doesn't work. -// This is a copy with the things that we currently use -export interface PolarVectorOptions { - source?: VectorSource - style?: StyleLike -} - -export type Mode = - | 'none' - | 'draw' - | 'edit' - | 'translate' - | 'delete' - | 'lasso' - | 'duplicate' - | 'merge' - | 'cut' - -export interface CreateInteractionsPayload { - drawSource: VectorSource - drawLayer: VectorLayer -} - -export interface DrawState { - mode: Mode - drawMode: DrawMode - textInput: string - selectedStrokeColor: string - /* index in array of available sizes */ - selectedSize: number - featureCollection: FeatureCollection - selectedFeature: number - measureMode: MeasureMode - revisedFeatureCollection: FeatureCollection - featureCollectionRevisionState: - | typeof inactive - | typeof inProgress - | typeof complete - | typeof error -} - -export interface DrawGetters extends Omit { - drawSource: VectorSource - selectableDrawModes: { [k in DrawMode]?: string } - selectedFeature: Feature - selectableModes: { [k in Mode]: string } - selectableMeasureModes: { [k in MeasureMode]?: string } - showTextInput: boolean - configuration: DrawConfiguration - fontSizes: number[] - measureOptions: MeasureOptions - showSizeSlider: boolean - /* actual text size to use */ - textSize: number - activeLassoIds: string[] - toastAction: string - showDrawOptions: boolean - showMeasureOptions: boolean -} - -export interface DrawMutations extends MutationTree { - setDrawMode: Mutation - setMode: Mutation - updateFeatures: Mutation -} diff --git a/vue2/packages/plugins/Draw/src/utils/createDrawLayer.ts b/vue2/packages/plugins/Draw/src/utils/createDrawLayer.ts deleted file mode 100644 index 0644155d11..0000000000 --- a/vue2/packages/plugins/Draw/src/utils/createDrawLayer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import VectorLayer from 'ol/layer/Vector' -import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style' -import { DrawStyle } from '@polar/lib-custom-types' -import { PolarVectorOptions } from '../types' - -/** - * Creates a new VectorLayer. - * If certain style parameters, they are used instead of the default styling. - * - * @returns VectorLayer for the drawn features to reside in. - */ -export default function (source, style?: DrawStyle) { - const options: PolarVectorOptions = { source } - if (style) { - // TODO: The styling for Stroke and Circle have many more options. - // Thus, it could be interesting to divide this function even further - // so that every possible option can be used. - const { fill, stroke, circle } = style - options.style = new Style({ - fill: new Fill(fill), - stroke: new Stroke(stroke), - image: new CircleStyle({ - radius: circle.radius, - fill: new Fill({ color: circle.fillColor }), - }), - }) - } - return new VectorLayer(options) -} diff --git a/vue2/packages/plugins/Draw/src/utils/createDrawStyle.ts b/vue2/packages/plugins/Draw/src/utils/createDrawStyle.ts deleted file mode 100644 index 0c40b60fbe..0000000000 --- a/vue2/packages/plugins/Draw/src/utils/createDrawStyle.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { type DrawStyle, type MeasureMode } from '@polar/lib-custom-types' -import centerOfMass from '@turf/center-of-mass' -import { type Color } from 'ol/color' -import { type ColorLike } from 'ol/colorlike' -import { Feature } from 'ol' -import { LineString, Point, Polygon } from 'ol/geom' -import { type Projection } from 'ol/proj' -import { getArea, getLength } from 'ol/sphere' -import { Circle as CircleStyle, Fill, Stroke } from 'ol/style' -import Style, { type Options, StyleFunction } from 'ol/style/Style' -import Text, { type Options as TextOptions } from 'ol/style/Text' - -const roundMeasurement = (measurement: number, divisor: number) => - Math.round((measurement * 100) / divisor + Number.EPSILON) / 100 - -function calculatePartialDistances( - styles: Style[], - styleOptions: Options, - textOptions: TextOptions, - feature: Feature, - unit: 'm' | 'km', - projection: Projection -) { - const geometry = feature.getGeometry() as LineString | Polygon - const coordinates = - geometry instanceof Polygon - ? geometry.getCoordinates()[0] - : geometry.getCoordinates() - for (let i = 1; i < coordinates.length; i++) { - const lineString = new LineString([coordinates[i - 1], coordinates[i]]) - const lengthInMetres = getLength(lineString, { - projection, - }) - const length = roundMeasurement(lengthInMetres, unit === 'km' ? 1000 : 1) - const text = `${length} ${unit}` - feature.set(`length-${i}`, roundMeasurement(lengthInMetres, 1)) - const style = new Style({ - ...styleOptions, - text: new Text({ - ...textOptions, - text, - }), - }) - style.setGeometry(lineString) - styles.push(style) - } - // This only happens once the drawing has been finished - if ( - Object.keys(feature.getProperties()).filter((key) => - key.startsWith('length-') - ).length === coordinates.length - ) { - feature.unset(`length-${coordinates.length}`) - } - - return styles -} - -function getAreaUnitAndDivisor(measureMode: Exclude) { - let areaUnit = '' - let divisor: number - if (measureMode === 'metres') { - areaUnit = 'm²' - divisor = 1 - } else if (measureMode === 'kilometres') { - areaUnit = 'km²' - divisor = 1000000 - } else { - areaUnit = 'ha' - divisor = 10000 - } - return { areaUnit, divisor } -} - -const measureStyle: ( - styleOptions: Options, - measureMode: Exclude, - projection: Projection, - measureStyleOptions?: TextOptions -) => StyleFunction = - (styleOptions, measureMode, projection, measureStyleOptions) => (feature) => { - const geometry = feature.getGeometry() - if (geometry instanceof Polygon || geometry instanceof LineString) { - const styles = [new Style(styleOptions)] - const textOptions: TextOptions = { - font: '16px sans-serif', - placement: 'line', - fill: new Fill({ color: 'black' }), - stroke: new Stroke({ color: 'black' }), - offsetY: -5, - ...measureStyleOptions, - } - if (geometry instanceof Polygon) { - const { areaUnit, divisor } = getAreaUnitAndDivisor(measureMode) - const areaInMetres = getArea(geometry, { projection }) - const area = roundMeasurement(areaInMetres, divisor) - const text = `${area} ${areaUnit}` - const style = new Style({ - text: new Text({ - ...textOptions, - placement: 'point', - text, - }), - }) - // @ts-expect-error | Features in this StyleFunction are always of type Feature - feature.set('area', roundMeasurement(areaInMetres, 1)) - style.setGeometry( - new Point( - centerOfMass({ - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: geometry.getCoordinates(), - }, - }).geometry.coordinates - ) - ) - styles.push(style) - } - return calculatePartialDistances( - styles, - styleOptions, - textOptions, - feature as Feature, - measureMode === 'metres' ? 'm' : 'km', - projection - ) - } - return new Style(styleOptions) - } - -export default function ( - drawMode: string, - strokeColor: string, - measureMode: MeasureMode, - projection: Projection, - drawStyle?: DrawStyle -): Style | StyleFunction { - const defaultFillColor = 'rgba(255, 255, 255, 0.5)' - if (drawMode === 'Point') { - return createPointStyle( - strokeColor, - drawStyle?.circle?.fillColor - ? drawStyle.circle.fillColor - : defaultFillColor, - drawStyle?.circle?.radius - ) - } - const fillColor = drawStyle?.fill?.color - ? drawStyle.fill.color - : defaultFillColor - const styleOptions: Options = { - image: new CircleStyle({ - radius: 5, - fill: new Fill({ - color: fillColor, - }), - stroke: new Stroke({ color: strokeColor }), - }), - stroke: new Stroke({ - color: strokeColor, - width: drawStyle?.stroke?.width || 2, - }), - fill: new Fill({ - color: fillColor, - }), - } - return measureMode === 'none' - ? new Style(styleOptions) - : measureStyle(styleOptions, measureMode, projection, drawStyle?.measure) -} - -function createPointStyle( - strokeColor: string, - fillColor: Color | ColorLike, - radius = 5 -) { - return new Style({ - image: new CircleStyle({ - radius, - fill: new Fill({ - color: fillColor, - }), - stroke: new Stroke({ color: strokeColor }), - }), - }) -} diff --git a/vue2/packages/plugins/Draw/src/utils/createTextStyle.ts b/vue2/packages/plugins/Draw/src/utils/createTextStyle.ts deleted file mode 100644 index 9b8e0d80cf..0000000000 --- a/vue2/packages/plugins/Draw/src/utils/createTextStyle.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { TextStyle } from '@polar/lib-custom-types' -import { Circle as CircleStyle, Fill, Style, Text } from 'ol/style' - -export function createTextStyle( - drawText: string, - textStyle?: TextStyle, - textSize?: number -) { - return new Style({ - image: new CircleStyle({ - radius: 5, - fill: new Fill({ color: '#ffcc3300' }), - }), - text: new Text({ - textAlign: 'center', - text: drawText, - font: createFont(textStyle, textSize), - fill: textStyle?.textColor - ? new Fill({ color: textStyle.textColor }) - : undefined, - textBaseline: 'middle', - }), - }) -} - -function createFont(textStyle?: TextStyle, textSize?: number) { - if (typeof textStyle === 'undefined' || textStyle === null) { - // if empty string apply default open layers style - return '' - } - const { font } = textStyle - if (typeof font === 'string') { - return font - } - const fontFamily = font.family ? font.family : 'sans-serif' - return textSize + 'px ' + fontFamily -} diff --git a/vue2/packages/plugins/Draw/vite.config.js b/vue2/packages/plugins/Draw/vite.config.js deleted file mode 100644 index 0d2ec38a15..0000000000 --- a/vue2/packages/plugins/Draw/vite.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { getCodeConfig } from '../../../viteConfigs' - -export default getCodeConfig() diff --git a/vue2/packages/types/custom/core.ts b/vue2/packages/types/custom/core.ts index 36e4e3071d..86f90bc666 100644 --- a/vue2/packages/types/custom/core.ts +++ b/vue2/packages/types/custom/core.ts @@ -1,10 +1,6 @@ import { Feature, Map } from 'ol' import { Options as Fill } from 'ol/style/Fill' import { Options as Stroke } from 'ol/style/Stroke' -import { type Options as TextOptions } from 'ol/style/Text' -import { Size } from 'ol/size' -import { Color } from 'ol/color' -import { ColorLike } from 'ol/colorlike' import { Feature as GeoJsonFeature } from 'geojson' import { VueConstructor } from 'vue' @@ -14,75 +10,6 @@ import { VueConstructor } from 'vue' * */ -export interface PolarCircleStyle { - fillColor?: Color | ColorLike - radius: number - strokeColor?: Color | ColorLike - displacement?: number[] - scale?: number | Size - rotation?: number -} - -export interface DrawStyle { - fill: Fill - stroke: Stroke - circle: PolarCircleStyle - measure?: TextOptions -} - -export interface TextStyle { - font: string | FontStyle - textColor?: Color | ColorLike -} - -export interface FontStyle { - size: number[] - family: string -} - -export type DrawMode = 'Circle' | 'LineString' | 'Point' | 'Polygon' | 'Text' - -export type MeasureMode = 'none' | 'metres' | 'kilometres' | 'hectares' - -export interface MeasureOptions { - metres?: boolean - kilometres?: boolean - hectares?: boolean - initialOption?: MeasureMode -} - -export interface Lasso { - id: string - minZoom: boolean -} - -export interface DrawConfiguration extends Partial { - addLoading?: string - enableOptions?: boolean - lassos?: Lasso[] - measureOptions?: MeasureOptions - removeLoading?: string - revision?: DrawRevision - selectableDrawModes?: DrawMode[] - snapTo?: string[] - style?: DrawStyle - textStyle?: TextStyle - toastAction?: string -} - -export interface DrawRevision { - autofix?: boolean - mergeToMultiGeometries?: boolean - metaServices?: DrawMetaService[] - validate?: boolean -} - -export interface DrawMetaService { - id: string - aggregationMode?: 'unequal' | 'all' - propertyNames?: string[] -} - /** Configuration of GFI feature regarding a specific layer */ export interface GfiLayerConfiguration { /**
+ {{ $t(($) => $.help.text[helpText], { ns: PluginId }) }} +