diff --git a/.gitignore b/.gitignore index c3fe41ab2..9f9255f6e 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,5 @@ build !app/frontend/.env !local-infrastructure/.env .history +realm-export.json +chefs_build \ No newline at end of file diff --git a/app/frontend/package-lock.json b/app/frontend/package-lock.json index 5b89bcac0..ca78b437b 100644 --- a/app/frontend/package-lock.json +++ b/app/frontend/package-lock.json @@ -21,6 +21,8 @@ "font-awesome": "^4.7.0", "formiojs": "^4.14.13", "keycloak-js": "^21.1.1", + "leaflet": "^1.9.4", + "leaflet-draw": "^1.0.4", "lodash": "^4.17.21", "mitt": "^3.0.0", "moment": "^2.29.4", @@ -2767,9 +2769,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/functions-have-names": { "version": "1.2.3", @@ -3690,6 +3695,16 @@ "js-sha256": "^0.9.0" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, + "node_modules/leaflet-draw": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/leaflet-draw/-/leaflet-draw-1.0.4.tgz", + "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/app/frontend/package.json b/app/frontend/package.json index 76a987ddd..3759ab75c 100644 --- a/app/frontend/package.json +++ b/app/frontend/package.json @@ -42,6 +42,8 @@ "font-awesome": "^4.7.0", "formiojs": "^4.14.13", "keycloak-js": "^21.1.1", + "leaflet": "^1.9.4", + "leaflet-draw": "^1.0.4", "lodash": "^4.17.21", "mitt": "^3.0.0", "moment": "^2.29.4", diff --git a/app/frontend/src/components/designer/FormDesigner.vue b/app/frontend/src/components/designer/FormDesigner.vue index 7abbab56c..309bc8ca8 100644 --- a/app/frontend/src/components/designer/FormDesigner.vue +++ b/app/frontend/src/components/designer/FormDesigner.vue @@ -188,6 +188,7 @@ export default { simplefile: this.form.userType !== this.ID_MODE.PUBLIC, bcaddress: true, simplebcaddress: true, + map: true, }, }, }, diff --git a/components/package-lock.json b/components/package-lock.json index 2d6935783..60df996b8 100644 --- a/components/package-lock.json +++ b/components/package-lock.json @@ -9,8 +9,13 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { + "@types/leaflet": "^1.9.12", + "@types/leaflet-draw": "^1.0.11", "autocompleter": "^7.0.1", + "css-loader": "^7.1.2", "formiojs": "^4.14.6", + "leaflet": "^1.9.4", + "leaflet-draw": "^1.0.4", "lodash": "^4.17.21", "native-promise-only": "^0.8.1", "path-browserify": "^1.0.1", @@ -603,11 +608,32 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==" }, + "node_modules/@types/leaflet": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz", + "integrity": "sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/leaflet-draw": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/leaflet-draw/-/leaflet-draw-1.0.11.tgz", + "integrity": "sha512-dyedtNm3aSmnpi6FM6VSl28cQuvP+MD7pgpXyO3Q1ZOCvrJKmzaDq0P3YZTnnBs61fQCKSnNYmbvCkDgFT9FHQ==", + "dependencies": { + "@types/leaflet": "*" + } + }, "node_modules/@types/mocha": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", @@ -1904,6 +1930,62 @@ "custom-event": "^1.0.0" } }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", @@ -3450,6 +3532,17 @@ "@babel/runtime": "^7.17.2" } }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/idb": { "version": "6.1.5", "resolved": "https://registry.npmjs.org/idb/-/idb-6.1.5.tgz", @@ -4374,6 +4467,16 @@ "node": ">= 0.10" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" + }, + "node_modules/leaflet-draw": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/leaflet-draw/-/leaflet-draw-1.0.4.tgz", + "integrity": "sha512-rsQ6saQO5ST5Aj6XRFylr5zvarWgzWnrg46zQ1MEOEIHsppdC/8hnN8qMoFvACsPvTioAuysya/TVtog15tyAQ==" + }, "node_modules/liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -6249,10 +6352,9 @@ } }, "node_modules/postcss": { - "version": "8.4.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz", - "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==", - "dev": true, + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -6268,19 +6370,90 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", @@ -7291,10 +7464,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -8330,8 +8502,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "node_modules/uuid": { "version": "3.4.0", diff --git a/components/package.json b/components/package.json index a0cb08779..636662295 100755 --- a/components/package.json +++ b/components/package.json @@ -47,8 +47,13 @@ "components" ], "dependencies": { + "@types/leaflet": "^1.9.12", + "@types/leaflet-draw": "^1.0.11", "autocompleter": "^7.0.1", + "css-loader": "^7.1.2", "formiojs": "^4.14.6", + "leaflet": "^1.9.4", + "leaflet-draw": "^1.0.4", "lodash": "^4.17.21", "native-promise-only": "^0.8.1", "path-browserify": "^1.0.1", diff --git a/components/src/components/Map/Common/Constants.d.ts b/components/src/components/Map/Common/Constants.d.ts new file mode 100644 index 000000000..0b0a10000 --- /dev/null +++ b/components/src/components/Map/Common/Constants.d.ts @@ -0,0 +1,4 @@ +export declare abstract class Constants { + static readonly DEFAULT_HELP_LINK: string; + static readonly ADV: string; +} diff --git a/components/src/components/Map/Common/Constants.js b/components/src/components/Map/Common/Constants.js new file mode 100644 index 000000000..0ca555292 --- /dev/null +++ b/components/src/components/Map/Common/Constants.js @@ -0,0 +1,4 @@ +export class Constants { + static DEFAULT_HELP_LINK = 'https://github.com/bcgov/common-hosted-form-service/wiki'; + static ADV = ''; +} diff --git a/components/src/components/Map/Component.form.ts b/components/src/components/Map/Component.form.ts new file mode 100644 index 000000000..cc9dc0133 --- /dev/null +++ b/components/src/components/Map/Component.form.ts @@ -0,0 +1,46 @@ +import baseEditForm from 'formiojs/components/_classes/component/Component.form'; +import EditData from './editForm/Component.edit.data'; +import EditDisplay from './editForm/Component.edit.display'; +import EditValidation from './editForm/Component.edit.validation'; +import SimpleApi from '../Common/Simple.edit.api'; +import SimpleConditional from '../Common/Simple.edit.conditional'; +export default function (...extend) { + return baseEditForm([ + EditDisplay, + EditData, + { + key: 'api', + ignore: true + }, + { + key: 'layout', + ignore: true + }, + { + key: 'conditional', + ignore: true + }, + { + key: 'logic', + ignore: true + }, + { + label: 'Validation', + key: 'customValidation', + weight: 20, + components: EditValidation + }, + { + label: 'API', + key: 'customAPI', + weight: 30, + components: SimpleApi + }, + { + label: 'Conditional', + key: 'customConditional', + weight: 40, + components: SimpleConditional + } + ], ...extend); +} diff --git a/components/src/components/Map/Component.ts b/components/src/components/Map/Component.ts new file mode 100644 index 000000000..e2dbed261 --- /dev/null +++ b/components/src/components/Map/Component.ts @@ -0,0 +1,148 @@ +import { Components } from 'formiojs'; +const FieldComponent = (Components as any).components.field; +import MapService from './services/MapService'; +import baseEditForm from './Component.form'; +import * as L from 'leaflet'; + +const CENTER: [number, number] = [48.41939025932759, -123.37029576301576]; // Ensure CENTER is a tuple with exactly two elements + +export default class Component extends (FieldComponent as any) { + static schema(...extend) { + return FieldComponent.schema({ + type: 'map', + label: 'Map', + key: 'map', + input: true, + ...extend, + }); + } + + static get builderInfo() { + return { + title: 'Map', + group: 'basic', + icon: 'map', + weight: 70, + schema: Component.schema(), + }; + } + + static editForm = baseEditForm; + + componentID: string; + mapService: MapService; + + constructor(component, options, data) { + super(component, options, data); + this.componentID = super.elementInfo().component.id; + } + + render() { + return super.render( + `
` + ); + } + + attach(element) { + const superAttach = super.attach(element); + this.loadMap(); + return superAttach; + } + + loadMap() { + const mapContainer = document.getElementById(`map-${this.componentID}`); + const form = document.getElementsByClassName('formio'); + let drawOptions = { + marker: false, + circlemarker: false, + polygon: false, + polyline: false, + circle: false, + rectangle: null, + }; + + // Set drawing options based on markerType + if (this.component.markerType === 'rectangle') { + drawOptions.rectangle = { showArea: false }; // fixes a bug in Leaflet.Draw + } else { + drawOptions.rectangle = false; + drawOptions[this.component.markerType] = true; // set marker type from user choice + } + + const { numPoints, defaultZoom } = this.component; + this.mapService = new MapService({ + mapContainer, + drawOptions, + center: CENTER, + form, + numPoints, + defaultZoom, + onDrawnItemsChange: this.saveDrawnItems.bind(this), + }); + + + + // Load existing data if available + if (this.dataValue) { + this.mapService.loadDrawnItems(JSON.parse(this.dataValue)); + } + } + + saveDrawnItems(drawnItems: L.Layer[]) { + const value = drawnItems.map((layer: any) => { + if (layer instanceof L.Marker) { + return { + type: 'marker', + latlng: layer.getLatLng(), + }; + } else if (layer instanceof L.Rectangle) { + return { + type: 'rectangle', + bounds: layer.getBounds(), + }; + } else if (layer instanceof L.Circle) { + return { + type: 'circle', + latlng: layer.getLatLng(), + radius: layer.getRadius(), + }; + } else if (layer instanceof L.Polygon) { + return { + type: 'polygon', + latlngs: layer.getLatLngs(), + }; + } else if (layer instanceof L.Polyline) { + return { + type: 'polyline', + latlngs: layer.getLatLngs(), + }; + } + }); + + + // Convert to JSON string + const jsonValue = + this.component.numPoints === 1 + ? JSON.stringify(value[0]) + : JSON.stringify(value); + this.setValue(jsonValue); + } + + setValue(value) { + super.setValue(value); + + // Additional logic to render the saved data on the map if necessary + if (this.mapService && value) { + try { + const parsedValue = JSON.parse(value); + this.mapService.loadDrawnItems(parsedValue); + } catch (error) { + console.error('Failed to parse value:', error); + } + } + } + + getValue() { + return this.dataValue; + } +} diff --git a/components/src/components/Map/editForm/Component.edit.data.ts b/components/src/components/Map/editForm/Component.edit.data.ts new file mode 100644 index 000000000..2251a2a51 --- /dev/null +++ b/components/src/components/Map/editForm/Component.edit.data.ts @@ -0,0 +1,49 @@ +import common from '../../Common/Simple.edit.data'; +export default + { + key: 'data', + components: [ + ...common, + { + label: "Marker Type ", + values: [ + { + label: "Point Marker", + value: "marker" + }, + { + label: "Circle", + value: "circle", + } + ], + defaultValue:"marker", + key: "markerType", + type: "simpleradios", + input: true, + }, + { + label: "How many Points per Submission?", + key: "numPoints", + type: "simplenumber", + defaultValue: 1, + input: true, + }, + + { + label: "Default Zoom Level", + description: "Zoom Levels are from 0 (Most zoomed out) to 18 (most zoomed in).", + defaultValue: 13, + delimiter: false, + requireDecimal: false, + validate: { + isUseForCopy: false, + min: 0, + max: 18 + }, + key: "defaultZoom", + type: "simplenumber", + input: true, + } + ] + } + diff --git a/components/src/components/Map/editForm/Component.edit.display.ts b/components/src/components/Map/editForm/Component.edit.display.ts new file mode 100644 index 000000000..5c0cae03f --- /dev/null +++ b/components/src/components/Map/editForm/Component.edit.display.ts @@ -0,0 +1,24 @@ +import common from '../../Common/Simple.edit.display'; +export default [ + ...common, + { + key: 'refreshOnChange', + ignore: true + }, + { + key: 'className', + ignore: true, + }, + { + key: 'prefix', + ignore: true + }, + { + key: 'suffix', + ignore: true + }, + { + key: 'labelPosition', + ignore: true, + }, +]; diff --git a/components/src/components/Map/editForm/Component.edit.validation.ts b/components/src/components/Map/editForm/Component.edit.validation.ts new file mode 100644 index 000000000..1cec573db --- /dev/null +++ b/components/src/components/Map/editForm/Component.edit.validation.ts @@ -0,0 +1,4 @@ +import common from '../../Common/Simple.edit.validation'; +export default [ + ...common, +]; diff --git a/components/src/components/Map/services/MapService.ts b/components/src/components/Map/services/MapService.ts new file mode 100644 index 000000000..cb0c39d1d --- /dev/null +++ b/components/src/components/Map/services/MapService.ts @@ -0,0 +1,156 @@ +import * as L from 'leaflet'; +import 'leaflet-draw'; +import 'leaflet/dist/leaflet.css'; +import 'leaflet-draw/dist/leaflet.draw-src.css'; + +const DEFAULT_MAP_LAYER_URL = + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; +const DEFAULT_LAYER_ATTRIBUTION = + '© OpenStreetMap contributors'; +const DEFAULT_MAP_ZOOM = 13; +const DECIMALS_LATLNG = 5; // the number of decimals of latitude and longitude to be displayed in the marker popup + +interface MapServiceOptions { + mapContainer: HTMLElement; + center: [number, number]; // Ensure center is a tuple with exactly two elements + drawOptions: any; + form: HTMLCollectionOf; + numPoints: number; + defaultZoom?: number; + onDrawnItemsChange: (items: any) => void; // Support both single and multiple items +} + +class MapService { + options: MapServiceOptions; + map: L.Map; + drawnItems: L.FeatureGroup; + + constructor(options: MapServiceOptions) { + this.options = options; + if (options.mapContainer) { + const { map, drawnItems } = this.initializeMap(options); + this.map = map; + this.drawnItems = drawnItems; + map.invalidateSize(); + + // Event listener for drawn objects + map.on('draw:created', (e: any) => { + let layer = e.layer; + if (drawnItems.getLayers().length === options.numPoints) { + L.popup() + .setLatLng(layer._latlng) + .setContent('

Only one marker for submission

') + .openOn(map); + } else { + drawnItems.addLayer(layer); + } + this.bindPopupToLayer(layer); + options.onDrawnItemsChange(drawnItems.getLayers()); + }); + } + } + + initializeMap(options: MapServiceOptions) { + let { mapContainer, center, drawOptions, form, defaultZoom } = options; + if (drawOptions.rectangle) { + drawOptions.rectangle.showArea = false; + } + const map = L.map(mapContainer).setView( + center, + defaultZoom || DEFAULT_MAP_ZOOM + ); + L.tileLayer(DEFAULT_MAP_LAYER_URL, { + attribution: DEFAULT_LAYER_ATTRIBUTION, + }).addTo(map); + + // Initialize Draw Layer + let drawnItems = new L.FeatureGroup(); + map.addLayer(drawnItems); + + // Add Drawing Controllers + let drawControl = new L.Control.Draw({ + draw: drawOptions, + edit: { + featureGroup: drawnItems, + }, + }); + + if (form && form[0]?.classList.contains('formbuilder')) { + map.dragging.disable(); + map.scrollWheelZoom.disable(); + } + + // Attach Controls to map + map.addControl(drawControl); + return { map, drawnItems }; + } + + bindPopupToLayer(layer: L.Layer) { + if (layer instanceof L.Marker) { + layer + .bindPopup( + `

(${layer.getLatLng().lat.toFixed(DECIMALS_LATLNG)},${layer + .getLatLng() + .lng.toFixed(DECIMALS_LATLNG)})

` + ) + .openPopup(); + } else if (layer instanceof L.Circle) { + layer + .bindPopup( + `

(${layer.getLatLng().lat.toFixed(DECIMALS_LATLNG)},${layer + .getLatLng() + .lng.toFixed(DECIMALS_LATLNG)})

` + ) + .openPopup(); + } else if (layer instanceof L.Rectangle || layer instanceof L.Polygon) { + const bounds = layer.getBounds(); + const center = bounds.getCenter(); + layer + .bindPopup( + `

(${center.lat.toFixed(DECIMALS_LATLNG)},${center.lng.toFixed( + DECIMALS_LATLNG + )})

` + ) + .openPopup(); + } + } + + loadDrawnItems(items: any) { + const { drawnItems } = this; + + // Ensure drawnItems is defined before attempting to clear layers + if (!drawnItems) { + console.error('drawnItems is undefined'); + return; + } + + drawnItems.clearLayers(); + + // Check if items is an array + if (!Array.isArray(items)) { + items = [items]; + } + + items.forEach((item) => { + let layer; + if (item.type === 'marker') { + layer = L.marker(item.latlng); + } else if (item.type === 'rectangle') { + layer = L.rectangle(item.bounds); + } else if (item.type === 'circle') { + layer = L.circle(item.latlng, { radius: item.radius }); + } else if (item.type === 'polygon') { + layer = L.polygon(item.latlngs); + } else if (item.type === 'polyline') { + layer = L.polyline(item.latlngs); + } + + if (layer) { + drawnItems.addLayer(layer); + this.bindPopupToLayer(layer); + } + }); + } +} + +export default MapService; diff --git a/components/src/components/index.ts b/components/src/components/index.ts index 8361f59d1..ae1f2cf05 100755 --- a/components/src/components/index.ts +++ b/components/src/components/index.ts @@ -45,6 +45,7 @@ import simplesignatureadvanced from './SimpleSignatureAdvanced/Component'; import simplebuttonadvanced from './SimpleButtonAdvanced/Component'; import bcaddress from './BCAddress/Component'; import simplebcaddress from './SimpleBCAddress/Component'; +import map from './Map/Component'; export default { orgbook, @@ -93,5 +94,6 @@ export default { simplesignatureadvanced, simplebuttonadvanced, bcaddress, - simplebcaddress + simplebcaddress, + map, }; diff --git a/components/webpack.config.js b/components/webpack.config.js index c82fc573d..446560860 100755 --- a/components/webpack.config.js +++ b/components/webpack.config.js @@ -1,6 +1,14 @@ const path = require('path'); module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + use: ["css-loader"], + }, + ], + }, entry: path.join(path.resolve(__dirname, 'lib'), 'index.js'), output: { library: 'BcGovFormioComponents',