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`
`;
}
private dialogs() {
return html`
{
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();
}
}
}