import { LitElementWw } from '@webwriter/lit'; import { LitElement, PropertyValueMap, html } from 'lit'; import { customElement, property, query } from 'lit/decorators.js'; import { localized, msg } from '@lit/localize'; import LOCALIZE from './localization/generated'; import { styleMap } from 'lit/directives/style-map.js'; import { style } from './ww-map.css.js'; import { leafletStyles } from './leaflet/leaflet.css.js'; import { icons } from './icons.js'; import { faArrowPointer, faBan, faBorderBottomRight, faBorderTopLeft, faCircle, faCompress, faDrawPolygon, faExpand, faEye, faEyeSlashed, faLocationCrosshairs, faLocationDot, faMagnifyingGlassLocation, faMagnifyingGlassMinus, faMagnifyingGlassPlus, faMapLocationDot, faSlash, faSquareMinus, faSquarePlus, faStreetView, faTrash, faVectorSquare, } from './fontawesome.css.js'; import SlButton from '@shoelace-style/shoelace/dist/components/button/button.component.js'; import SlDetails from '@shoelace-style/shoelace/dist/components/details/details.component.js'; import SlInput from '@shoelace-style/shoelace/dist/components/input/input.component.js'; import SlCheckbox from '@shoelace-style/shoelace/dist/components/checkbox/checkbox.component.js'; import SlTooltip from '@shoelace-style/shoelace/dist/components/tooltip/tooltip.component.js'; import SlButtonGroup from '@shoelace-style/shoelace/dist/components/button-group/button-group.component.js'; import SlIcon from '@shoelace-style/shoelace/dist/components/icon/icon.component.js'; import SlDialog from '@shoelace-style/shoelace/dist/components/dialog/dialog.component.js'; import SlMenu from '@shoelace-style/shoelace/dist/components/menu/menu.component.js'; import SlMenuItem from '@shoelace-style/shoelace/dist/components/menu-item/menu-item.component.js'; import SlDropdown from '@shoelace-style/shoelace/dist/components/dropdown/dropdown.component.js'; import SlRange from '@shoelace-style/shoelace/dist/components/range/range.component.js'; import SlProgressBar from '@shoelace-style/shoelace/dist/components/progress-bar/progress-bar.component.js'; import SlCard from '@shoelace-style/shoelace/dist/components/card/card.component.js'; import SlDivider from '@shoelace-style/shoelace/dist/components/divider/divider.component.js'; import SlSwitch from '@shoelace-style/shoelace/dist/components/switch/switch.component.js'; import SlColorPicker from '@shoelace-style/shoelace/dist/components/color-picker/color-picker.component.js'; import '@shoelace-style/shoelace/dist/themes/light.css'; // import leafletStyles from './leaflet/leaflet.css.js'; import L from './leaflet/leaflet.js'; import 'fa-icons'; /** * Geographical map with different terrain options including custom tiling, and GeoJSON support. */ @customElement('webwriter-map') @localized() export class WwMap extends LitElementWw { // styles = [leafletStyles]; private styles = [style, leafletStyles]; protected localize = LOCALIZE; @query('#map') private accessor mapElement!: HTMLElement; @query('#pinDialog') private accessor pinDialog!: SlDialog; @query('#switchStudentPanning') private accessor switchStudentPanning!: SlSwitch @property({ type: Object }) private accessor map: L.Map | undefined; /** Initial center position of the map.
Expected value: object { lat: number, lng: number } (e.g. { lat: 51, lng: 19 }).
Optional; when set via attribute, pass a JSON string (e.g. '{"lat":51,"lng":19}'). */ @property({ type: Object, attribute: true, reflect: true }) accessor initialPos: { lat: number; lng: number; } = { lat: 51, lng: 19, }; /** Maximum bounding box for panning the map.
Expected value: Leaflet LatLngBoundsExpression (e.g. [[northLat, westLng], [southLat, eastLng]]).
Optional; when set via attribute, pass a JSON string (e.g. '[[51,6],[50,7]]'). */ @property({ type: Object, attribute: true, reflect: true }) accessor mapBounds: L.LatLngBoundsExpression; /** Maximum zoom level allowed when `boundsActive` is true.
Expected value: number (Leaflet zoom level).
Optional. */ @property({ type: Number, attribute: true, reflect: true }) accessor maxZoom: number; /** Minimum zoom level allowed.
Expected value: number (Leaflet zoom level).
Optional. */ @property({ type: Number, attribute: true, reflect: true }) accessor minZoom: number; /** Initial zoom level when the map is created.
Expected value: number (Leaflet zoom level).
Optional. */ @property({ type: Number, attribute: true, reflect: true }) accessor initialZoom = 13; /** Fixed zoom level to enforce when panning is not allowed for viewers (non-edit contexts).
Expected value: number (Leaflet zoom level).
Optional. */ @property({ type: Number, attribute: true}) accessor fixedZoom = 1; /** Static pin markers to display on the map.
Expected value: array of { lat: number, lng: number, title?: string }.
Optional; when set via attribute, pass a JSON string. */ @property({ type: Array, attribute: true, reflect: true }) accessor markers = []; /** Persisted drawing objects (rectangles, circles, polygons, polylines), keyed by id.
Expected value: map id -> { id, type, latlngs, radius?, borderColor, fillColor, borderOpacity, fillOpacity, label? }.
Optional; when set via attribute, pass a JSON string. */ @property({ type: Object, attribute: true, reflect: true }) accessor objects = {}; /** Custom tile URL template to use for the base map layer.
Expected value: string URL template containing {z}/{x}/{y}.
Optional; when empty, the default base layer is used. */ @property({ type: String, attribute: true, reflect: true }) accessor customTileUrl = ''; /** GeoJSON overlay to render on the map.
Expected value: stringified GeoJSON (Feature or FeatureCollection).
Optional; when empty/falsy, no GeoJSON overlay is shown. */ @property({ type: String, attribute: true, reflect: true }) accessor geoJSON = ''; /** Map container width, as a percentage of the host element's width.
Expected value: number (0–100). Applied as CSS width: `${mapWidth}%`.
Optional. */ @property({ type: Number, attribute: true, reflect: true }) accessor mapWidth = 100; /** Map container height in pixels.
Expected value: number (pixels). Applied as CSS height: `${mapHeight}px`.
Optional. */ @property({ type: Number, attribute: true, reflect: true }) accessor mapHeight = 500; /** Whether to enforce `mapBounds` and `maxZoom` constraints on the map.
Expected value: boolean; when true and `mapBounds` is set, panning is constrained to those bounds.
Optional. */ @property({ type: Boolean, attribute: true, reflect: true }) accessor boundsActive = true; @property({ type: Number }) private accessor inputLat = 0; @property({ type: Number }) private accessor inputLng = 0; @property({ type: Number }) private accessor inputZoom = 0; @property({ type: String }) private accessor inputBorderColor = '#000000ff'; @property({ type: String }) private accessor inputFillColor = '#000000ff'; @property({ type: String }) private accessor inputDrawObjectLabel = ''; @property({ type: String }) private accessor pinTitle = ''; @property({ type: String }) private accessor mapMode = 'view'; @property({ type: Object }) private accessor mouseMarker: L.Marker | undefined; @property({ type: Boolean }) private accessor showBounds = false; @property({ type: Object }) private accessor showBoundsLayer: L.Rectangle | undefined; @property({ type: Object }) private accessor editObject; @property({ type: Array }) private accessor editObjectMarkers = []; @property({ type: Object }) private accessor layerControl; @property({ type: Object }) private accessor drawObject; @property({ type: Number }) private accessor heightBuffer; @property({ type: Boolean, reflect: true }) private accessor allowPanning; /** @internal */ static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }; /** @internal */ static get scopedElements() { return { 'sl-button-group': SlButtonGroup, 'sl-button': SlButton, 'sl-icon': SlIcon, 'sl-input': SlInput, 'sl-checkbox': SlCheckbox, 'sl-details': SlDetails, 'sl-range': SlRange, 'sl-progress-bar': SlProgressBar, 'sl-card': SlCard, 'sl-divider': SlDivider, 'sl-switch': SlSwitch, 'sl-menu': SlMenu, 'sl-menu-item': SlMenuItem, 'sl-dropdown': SlDropdown, 'sl-tooltip': SlTooltip, 'sl-dialog': SlDialog, 'sl-color-picker': SlColorPicker, }; } connectedCallback(): void { // console.log('connectedCallback'); super.connectedCallback(); } disconnectedCallback(): void { // console.log('disconnectedCallback'); super.disconnectedCallback(); } protected update(changedProperties: PropertyValueMap | Map): void { // console.log('update'); super.update(changedProperties); } protected updated(changedProperties: PropertyValueMap | Map): void { // console.log('updated'); super.updated(changedProperties); if (this.map && changedProperties.has('customTileUrl')) { this.map.eachLayer((layer) => { if (layer instanceof L.TileLayer) this.map.removeLayer(layer); }); if (this.layerControl) { this.map.removeControl(this.layerControl); } if (this.customTileUrl) { L.tileLayer(this.customTileUrl, { attribution: '', }).addTo(this.map); } else { const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', }); const otm = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { attribution: '© OpenTopoMap contributors', }); const sat = L.tileLayer( 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: '© Esri contributors', } ); const baseLayers = { OpenStreetMap: osm, OpenTopoMap: otm, Satellite: sat, }; this.layerControl = L.control.layers(baseLayers).addTo(this.map); osm.addTo(this.map); } this.markers?.forEach((marker) => { const m = L.marker([marker.lat, marker.lng], { icon: icons.RED }).addTo(this.map); m.bindPopup(marker.title); }); } if (this.map && changedProperties.has('geoJSON')) { this.map.eachLayer((layer) => { if (layer instanceof L.GeoJSON) this.map.removeLayer(layer); }); if (this.geoJSON) { L.geoJSON(JSON.parse(this.geoJSON)).addTo(this.map); } this.markers?.forEach((marker) => { const m = L.marker([marker.lat, marker.lng], { icon: icons.RED }).addTo(this.map); m.bindPopup(marker.title); }); } if (this.map && changedProperties.has('mapBounds')) { if (this.mapBounds && this.boundsActive) { this.map.setMaxBounds(this.mapBounds); } else { this.map.setMaxBounds(undefined); } } if (this.map && changedProperties.has('maxZoom')) { if (this.maxZoom && this.boundsActive) { this.map.setMaxZoom(this.maxZoom); } else { this.map.setMaxZoom(Infinity); } } if (this.map && changedProperties.has('minZoom')) { if (this.minZoom) { this.map.setMinZoom(this.minZoom); } else { this.map.setMinZoom(0); } } if (this.map && changedProperties.has('boundsActive')) { if (this.boundsActive) { this.map.setMaxBounds(this.mapBounds); } else { this.map.setMaxBounds(undefined); } } if (this.map && changedProperties.has('editable')) { this.clearEditObject(); } } protected shouldUpdate(changedProperties: PropertyValueMap | Map): boolean { // console.log('shouldUpdate'); return super.shouldUpdate(changedProperties); } protected willUpdate(changedProperties: PropertyValueMap | Map): void { // console.log('willUpdate'); super.willUpdate(changedProperties); } protected firstUpdated(_changedProperties: PropertyValueMap | Map): void { // console.log('firstUpdated'); super.firstUpdated(_changedProperties); this.addEventListener("fullscreenchange", () => this.requestUpdate()) // console.log(this.styles); this.map = L.map(this.mapElement).setView([this.initialPos.lat, this.initialPos.lng], this.initialZoom); if (this.customTileUrl) { L.tileLayer(this.customTileUrl, { attribution: '', }).addTo(this.map); } else { L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', }).addTo(this.map); } if (this.geoJSON) { L.geoJSON(JSON.parse(this.geoJSON)).addTo(this.map); } this.markers?.forEach((marker) => { const m = L.marker([marker.lat, marker.lng], { icon: icons.RED }).addTo(this.map); m.bindPopup(marker.title); }); this.map.on('move', this.onMapMove.bind(this)); this.map.on('click', this.onMapClick.bind(this)); this.inputLat = this.initialPos.lat; this.inputLng = this.initialPos.lng; this.inputZoom = this.initialZoom; this.loadObjects() if(!this.allowPanning){ if(!this.hasAttribute("contenteditable")){ this.map.touchZoom.disable(); this.map.doubleClickZoom.disable(); this.map.scrollWheelZoom.disable(); this.map.boxZoom.disable(); this.map.keyboard.disable(); this.mapElement.style.pointerEvents = "none" this.fixedZoom = this.initialZoom }else{ this.switchStudentPanning.removeAttribute("checked") } }else{ if(this.hasAttribute("contenteditable")){ this.switchStudentPanning.setAttribute("checked", "") } } setInterval(() => { this.setInitialPosition() if(!this.allowPanning && !this.hasAttribute("contenteditable")){ this.map.setZoom(this.fixedZoom) } }, 250); } private isEditable() { return this.contentEditable === 'true' || this.contentEditable === ''; } private onMapMove() { // this.setInitialPosition() } private onMapClick(e: L.LeafletMouseEvent) { // console.log('onMapClick'); if (this.mapMode === 'mouseSelect') { if (this.mouseMarker) { this.map?.removeLayer(this.mouseMarker); } this.mouseMarker = L.marker(e.latlng, { icon: icons.YELLOW }).addTo(this.map); this.inputLat = e.latlng.lat; this.inputLng = e.latlng.lng; this.inputZoom = this.map?.getZoom() || 0; } } render() { return html` ${this.isEditable() ? this.toolbox() : ''}
{ if(this.ownerDocument.fullscreenElement === this){ this.ownerDocument.exitFullscreen() this.style.setProperty("height", this.heightBuffer+"px") this.mapHeight = this.heightBuffer }else{ this.heightBuffer = this.mapHeight this.requestFullscreen() this.style.height = "100%" setTimeout(() => { window.dispatchEvent(new Event('resize')); this.mapHeight = this.getBoundingClientRect().height }, 250); } }}> ${!(this.ownerDocument.fullscreenElement === this) ? html`` : html``}
${this.isEditable() ? this.dialogs() : ''} `; } private toolbox() { return html`
${faSquarePlus} ${faSquareMinus} {this.allowPanning = this.switchStudentPanning.checked}}>${msg('Allow student movement')} ${faSquarePlus} ${faSquareMinus}

${msg('Map style')}

{ this.customTileUrl = undefined; }} >${msg('User select')} { this.customTileUrl = 'https://tile.openstreetmap.de/{z}/{x}/{y}.png'; }} >OpenStreetMapDE { this.customTileUrl = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; }} >OpenTopoMap { this.customTileUrl = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'; }} >WorldImagery { this.customTileUrl = e.target.value; }} > { this.geoJSON = e.target.value; }} >
`; } private dialogs() { return html`
${msg('Add Pin')}
{ this.pinTitle = e.target.value; }} > { this.addLabel(); }} >${msg('Add')}
`; } private addRectangel() { //disable map dragging this.map?.dragging.disable(); this.mapMode = 'awaitDrawingRectangel'; //clear map events this.map?.off('mousemove'); this.map?.off('mousedown'); //on mouse down const onMapDivMouseDown = this.map.on('mousedown', (e: any) => { if (this.mapMode === 'awaitDrawingRectangel') { this.mapMode = 'drawingRectangle'; this.drawObject = L.rectangle([e.latlng, e.latlng], { color: this.inputBorderColor, fillColor: this.inputFillColor, opacity: this.getOpacity(this.inputBorderColor), fillOpacity: this.getOpacity(this.inputFillColor), }).addTo(this.map); this.map.off('mousedown', onMapDivMouseDown); } }); const onMapDivMove = this.map.on('mousemove', (e: any) => { if (this.mapMode === 'drawingRectangle') { this.drawObject?.setBounds(L.latLngBounds(this.drawObject.getBounds().getNorthWest(), e.latlng)); } }); const onMapDivMouseUp = this.map.on('mouseup', (e: any) => { if (this.mapMode === 'drawingRectangle') { this.mapMode = 'view'; this.map.off('mousemove', onMapDivMove); this.map.off('mouseup', onMapDivMouseUp); this.drawObject.on('click', (e: any) => { this.onRectangleClick(e); }); if (this.inputDrawObjectLabel) { this.drawObject.bindTooltip(this.inputDrawObjectLabel, { direction: 'center', }); } this.map?.dragging.enable(); this.saveObject(this.drawObject); } }); } private addCircle() { //disable map dragging this.map?.dragging.disable(); this.mapMode = 'awaitDrawingCircle'; //clear map events this.map?.off('mousemove'); this.map?.off('mousedown'); //on mouse down const onMapDivMouseDown = this.map.on('mousedown', (e: any) => { if (this.mapMode === 'awaitDrawingCircle') { this.mapMode = 'drawingCircle'; this.drawObject = L.circle(e.latlng, { color: this.inputBorderColor, fillColor: this.inputFillColor, opacity: this.getOpacity(this.inputBorderColor), fillOpacity: this.getOpacity(this.inputFillColor), }).addTo(this.map); this.map.off('mousedown', onMapDivMouseDown); } }); const onMapDivMove = this.map.on('mousemove', (e: any) => { if (this.mapMode === 'drawingCircle') { this.drawObject?.setRadius(this.drawObject.getLatLng().distanceTo(e.latlng)); } }); const onMapDivMouseUp = this.map.on('mouseup', (e: any) => { if (this.mapMode === 'drawingCircle') { this.mapMode = 'view'; this.map.off('mousemove'); this.map.off('mouseup'); this.drawObject.on('click', (e: any) => { this.onCircleClick(e); }); if (this.inputDrawObjectLabel) { this.drawObject.bindTooltip(this.inputDrawObjectLabel, { direction: 'center', }); } this.map?.dragging.enable(); this.saveObject(this.drawObject); } }); } private addPolygon() { //disable map dragging this.map?.dragging.disable(); this.mapMode = 'drawingPolygon'; this.drawObject = undefined; //clear map events this.map?.off('mousemove'); this.map?.off('mousedown'); //on map click const onMapDivMouseDown = this.map.on('click', (e: any) => { if (this.mapMode === 'drawingPolygon') { if (this.drawObject) { this.drawObject.addLatLng(e.latlng); } else { this.drawObject = L.polygon([e.latlng], { color: this.inputBorderColor, fillColor: this.inputFillColor, opacity: this.getOpacity(this.inputBorderColor), fillOpacity: this.getOpacity(this.inputFillColor), }).addTo(this.map); } } }); } private addPolyline() { //disable map dragging this.map?.dragging.disable(); this.mapMode = 'drawingPolyline'; this.drawObject = undefined; //clear map events this.map?.off('mousemove'); this.map?.off('mousedown'); //on map click const onMapDivMouseDown = this.map.on('click', (e: any) => { if (this.mapMode === 'drawingPolyline') { if (this.drawObject) { this.drawObject.addLatLng(e.latlng); } else { this.drawObject = L.polyline([e.latlng], { color: this.inputBorderColor, fillColor: this.inputFillColor, opacity: this.getOpacity(this.inputBorderColor), fillOpacity: this.getOpacity(this.inputFillColor), }).addTo(this.map); } } }); } private getPolygonPoints(n: number) { const points = []; const centerLat = this.inputLat; const centerLng = this.inputLng; // Create n sided polygon for (let i = 0; i < n; i++) { const x = centerLat + 0.01 * Math.cos((2 * Math.PI * i) / n); const y = centerLng + 0.01 * Math.sin((2 * Math.PI * i) / n); points.push([x, y]); } return points; } private getPolylinePoints(n: number) { const points = []; const centerLat = this.inputLat; const centerLng = this.inputLng; for (let i = 0; i < n; i++) { const x = centerLat + 0.01 * i; const y = centerLng + 0.01 * i; points.push([x, y]); } return points; } private onRectangleClick(e: any) { if (!this.isEditable()) return; this.clearEditObject(); this.editObject = e.target; const markerTL = L.marker(this.editObject.getBounds().getNorthWest(), { draggable: true, icon: icons.GREEN, }).addTo(this.map); const markerTR = L.marker(this.editObject.getBounds().getNorthEast(), { draggable: true, icon: icons.GREEN, }).addTo(this.map); const markerBL = L.marker(this.editObject.getBounds().getSouthWest(), { draggable: true, icon: icons.GREEN, }).addTo(this.map); const markerBR = L.marker(this.editObject.getBounds().getSouthEast(), { draggable: true, icon: icons.GREEN, }).addTo(this.map); this.editObjectMarkers.push(markerTL); this.editObjectMarkers.push(markerTR); this.editObjectMarkers.push(markerBL); this.editObjectMarkers.push(markerBR); markerTL.on('drag', (e: any) => { this.editObject?.setBounds(L.latLngBounds(e.target.getLatLng(), markerBR.getLatLng())); this.editObjectMarkers[1].setLatLng(this.editObject.getBounds().getNorthEast()); this.editObjectMarkers[2].setLatLng(this.editObject.getBounds().getSouthWest()); this.editObjectMarkers[3].setLatLng(this.editObject.getBounds().getSouthEast()); }); markerTL.once('dragend', (e: any) => { this.saveObject(this.editObject, this.editObject.id); }); markerTR.on('drag', (e: any) => { this.editObject?.setBounds( L.latLngBounds( [e.target.getLatLng().lat, markerTL.getLatLng().lng], [markerBL.getLatLng().lat, e.target.getLatLng().lng] ) ); this.editObjectMarkers[0].setLatLng(this.editObject.getBounds().getNorthWest()); this.editObjectMarkers[2].setLatLng(this.editObject.getBounds().getSouthWest()); this.editObjectMarkers[3].setLatLng(this.editObject.getBounds().getSouthEast()); }); markerTR.once('dragend', (e: any) => { this.saveObject(this.editObject, this.editObject.id); }); markerBL.on('drag', (e: any) => { this.editObject?.setBounds( L.latLngBounds( [markerTL.getLatLng().lat, e.target.getLatLng().lng], [e.target.getLatLng().lat, markerBR.getLatLng().lng] ) ); this.editObjectMarkers[0].setLatLng(this.editObject.getBounds().getNorthWest()); this.editObjectMarkers[1].setLatLng(this.editObject.getBounds().getNorthEast()); this.editObjectMarkers[3].setLatLng(this.editObject.getBounds().getSouthEast()); }); markerBL.once('dragend', (e: any) => { this.saveObject(this.editObject, this.editObject.id); }); markerBR.on('drag', (e: any) => { this.editObject?.setBounds(L.latLngBounds(markerTL.getLatLng(), e.target.getLatLng())); this.editObjectMarkers[0].setLatLng(this.editObject.getBounds().getNorthWest()); this.editObjectMarkers[1].setLatLng(this.editObject.getBounds().getNorthEast()); this.editObjectMarkers[2].setLatLng(this.editObject.getBounds().getSouthWest()); }); markerBR.once('dragend', (e: any) => { this.saveObject(this.editObject, this.editObject.id); }); } private onCircleClick(e: any) { if (!this.isEditable()) return; if (this.editObjectMarkers.length > 0) { this.editObjectMarkers.forEach((marker) => { this.map?.removeLayer(marker); }); this.editObjectMarkers = []; } this.editObject = e.target; const markerCenter = L.marker(this.editObject.getLatLng(), { draggable: true, icon: icons.GREEN, }).addTo(this.map); const markerRadius = L.marker( [ this.editObject.getBounds().getNorthEast().lat, this.editObject.getBounds().getNorthWest().lng + Math.abs( this.editObject.getBounds().getNorthEast().lng - this.editObject.getBounds().getNorthWest().lng ) / 2, ], { draggable: true, icon: icons.GREEN, } ).addTo(this.map); this.editObjectMarkers.push(markerCenter); this.editObjectMarkers.push(markerRadius); markerCenter.on('drag', (e: any) => { this.editObject?.setLatLng(e.target.getLatLng()); this.editObjectMarkers[1].setLatLng([ this.editObject.getBounds().getNorthEast().lat, this.editObject.getBounds().getNorthWest().lng + Math.abs( this.editObject.getBounds().getNorthEast().lng - this.editObject.getBounds().getNorthWest().lng ) / 2, ]); }); markerCenter.once('dragend', (e: any) => { this.saveObject(this.editObject, this.editObject.id); }); markerRadius.on('drag', (e: any) => { this.editObject?.setRadius(e.target.getLatLng().distanceTo(markerCenter.getLatLng())); this.editObjectMarkers[0].setLatLng(this.editObject.getLatLng()); }); markerRadius.once('dragend', (e: any) => { this.saveObject(this.editObject, this.editObject.id); }); } private onPolygonClick(e: any) { if (!this.isEditable()) return; if (this.editObjectMarkers.length > 0) { this.editObjectMarkers.forEach((marker) => { this.map?.removeLayer(marker); }); this.editObjectMarkers = []; } this.editObject = e.target; this.editObject.getLatLngs()[0].forEach((point: any) => { const marker = L.marker(point, { draggable: true, icon: icons.GREEN, }).addTo(this.map); this.editObjectMarkers.push(marker); marker.on('drag', (e: any) => { const index = this.editObjectMarkers.indexOf(e.target); const latlngs = this.editObject.getLatLngs()[0]; latlngs[index] = e.target.getLatLng(); this.editObject.setLatLngs(latlngs); }); marker.once('dragend', (e: any) => { this.saveObject(this.editObject, this.editObject.id); }); }); } private onPolylineClick(e: any) { if (!this.isEditable()) return; if (this.editObjectMarkers.length > 0) { this.editObjectMarkers.forEach((marker) => { this.map?.removeLayer(marker); }); this.editObjectMarkers = []; } this.editObject = e.target; this.editObject.getLatLngs().forEach((point: any) => { const marker = L.marker(point, { draggable: true, icon: icons.GREEN, }).addTo(this.map); this.editObjectMarkers.push(marker); marker.on('drag', (e: any) => { const index = this.editObjectMarkers.indexOf(e.target); const latlngs = this.editObject.getLatLngs(); latlngs[index] = e.target.getLatLng(); this.editObject.setLatLngs(latlngs); }); marker.once('dragend', (e: any) => { this.saveObject(this.editObject, this.editObject.id); }); }); } private setInitialPosition() { this.loadMapPosition() this.initialPos = { lat: this.inputLat, lng: this.inputLng, }; this.initialZoom = this.inputZoom; } private loadMapPosition() { this.inputLat = this.map?.getCenter().lat || 0; this.inputLng = this.map?.getCenter().lng || 0; this.inputZoom = this.map?.getZoom() || 0; } private loadGeoLocation() { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition((position) => { this.map?.setView([position.coords.latitude, position.coords.longitude], 13); this.loadMapPosition(); }); } } private addLabel() { if (this.pinTitle) { this.pinDialog.hide(); const marker = L.marker([this.inputLat, this.inputLng], { icon: icons.RED }) .addTo(this.map) .bindPopup(this.pinTitle) .openPopup(); this.markers.push({ lat: this.inputLat, lng: this.inputLng, title: this.pinTitle, }); this.markers = [...this.markers]; this.pinTitle = ''; } } private clearEditObject() { if (this.editObjectMarkers.length > 0) { this.editObjectMarkers.forEach((marker) => { this.map?.removeLayer(marker); }); this.editObjectMarkers = []; } if (this.editObject) { this.editObject = undefined; } } private saveObject(o, id: string = undefined) { const checkIfObjectExists = this.objects.hasOwnProperty(id); // console.log('SAVE DRAW OBJECT', this.objects, checkIfObjectExists); if (!id) { id = crypto.randomUUID(); o.id = id; } this.objects[id] = { id: id, type: o instanceof L.Rectangle ? 'rectangle' : o instanceof L.Circle ? 'circle' : o instanceof L.Polygon ? 'polygon' : o instanceof L.Polyline ? 'polyline' : 'unknown', latlngs: o instanceof L.Rectangle ? o.getBounds() : o instanceof L.Circle ? o.getLatLng() : o instanceof L.Polygon ? o.getLatLngs() : o instanceof L.Polyline ? o.getLatLngs() : undefined, radius: o instanceof L.Circle ? o.getRadius() : undefined, borderColor: o.options.color, fillColor: o.options.fillColor, borderOpacity: o.options.opacity, fillOpacity: o.options.fillOpacity, label: this.inputDrawObjectLabel, }; this.objects = { ...this.objects }; } private deleteObject(id: string) { const checkIfObjectExists = this.objects.hasOwnProperty(id); if (checkIfObjectExists) { delete this.objects[id]; this.objects = { ...this.objects }; } } private loadObjects() { for (let key in this.objects) { const o = this.objects[key]; switch (o.type) { case 'rectangle': const rectangle = L.rectangle( [ [o.latlngs._northEast.lat, o.latlngs._northEast.lng], [o.latlngs._southWest.lat, o.latlngs._southWest.lng], ], { color: o.borderColor, fillColor: o.fillColor, opacity: o.borderOpacity, fillOpacity: o.fillOpacity, } ).addTo(this.map); rectangle.id = o.id; rectangle.on('click', (e: any) => { this.onRectangleClick(e); }); if (o.label) { rectangle.bindTooltip(o.label, { direction: 'center', }); } break; case 'circle': const circle = L.circle([o.latlngs.lat, o.latlngs.lng], { radius: o.radius, color: o.borderColor, fillColor: o.fillColor, opacity: o.borderOpacity, fillOpacity: o.fillOpacity, }).addTo(this.map); circle.id = o.id; circle.on('click', (e: any) => { this.onCircleClick(e); }); if (o.label) { circle.bindTooltip(o.label, { direction: 'center', }); } break; case 'polygon': const polygon = L.polygon(o.latlngs, { color: o.borderColor, fillColor: o.fillColor, opacity: o.borderOpacity, fillOpacity: o.fillOpacity, }).addTo(this.map); polygon.id = o.id; polygon.on('click', (e: any) => { this.onPolygonClick(e); }); if (o.label) { polygon.bindTooltip(o.label, { direction: 'center', }); } break; case 'polyline': const polyline = L.polyline(o.latlngs, { color: o.borderColor, fillColor: o.fillColor, opacity: o.borderOpacity, fillOpacity: o.fillOpacity, }).addTo(this.map); polyline.id = o.id; polyline.on('click', (e: any) => { this.onPolylineClick(e); }); if (o.label) { polyline.bindTooltip(o.label, { direction: 'center', }); } break; default: break; } } } private getOpacity(hex) { const a = parseInt(hex.substring(6, 8), 16); return Math.round((a / 255) * 100); } private deleteSelectedObject() { if (this.editObject) { this.map?.removeLayer(this.editObject); this.deleteObject(this.editObject.id); this.clearEditObject(); } } }