vue-geojson-view-ts 1.3.19 → 1.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,715 +1,1054 @@
1
- <template>
2
- <div class="map-container" :style="`height:${configurationMap?.height}`">
3
- <div :id="idMap" style="height: 100%"></div>
4
- </div>
5
- </template>
6
-
7
- <script lang="ts">
8
- import { defineComponent } from "vue";
9
- import { markerDefault } from "../helpers/imgBase64";
10
- import homeIconUrl from "../assets/home.png";
11
- import endIconUrl from "../assets/end.png";
12
- import trackingIconUrl from "../assets/tracking.png";
13
- import * as L from "leaflet";
14
- import "leaflet-draw";
15
- import "leaflet/dist/leaflet.css";
16
- import "leaflet-draw/dist/leaflet.draw.css";
17
- import drawLocales from "leaflet-draw-locales";
18
- import axios from "axios";
19
- import "leaflet.fullscreen/Control.FullScreen.css";
20
- import "leaflet.fullscreen";
21
- import gpsIcon from "../assets/gps.svg";
22
- import sateliteIcon from "../assets/satelite.svg";
23
-
24
- declare global {
25
- interface Window {
26
- type: boolean;
27
- }
28
- }
29
-
30
- L.Edit.Circle = L.Edit.CircleMarker.extend({
31
- _createResizeMarker: function () {
32
- var center = this._shape.getLatLng(),
33
- resizemarkerPoint = this._getResizeMarkerPoint(center);
34
-
35
- this._resizeMarkers = [];
36
- this._resizeMarkers.push(
37
- this._createMarker(resizemarkerPoint, this.options.resizeIcon)
38
- );
39
- },
40
-
41
- _getResizeMarkerPoint: function (latlng: any) {
42
- var delta = this._shape._radius * Math.cos(Math.PI / 4),
43
- point = this._map.project(latlng);
44
- return this._map.unproject([point.x + delta, point.y - delta]);
45
- },
46
-
47
- _resize: function (latlng: any) {
48
- var moveLatLng = this._moveMarker.getLatLng();
49
- var radius;
50
-
51
- if (L.GeometryUtil.isVersion07x()) {
52
- radius = moveLatLng.distanceTo(latlng);
53
- } else {
54
- radius = this._map.distance(moveLatLng, latlng);
55
- }
56
-
57
- // **** This fixes the cicle resizing ****
58
- this._shape.setRadius(radius);
59
-
60
- this._map.fire(L.Draw.Event.EDITRESIZE, { layer: this._shape });
61
- },
62
- });
63
-
64
- export default defineComponent({
65
- name: "MapView",
66
- props: {
67
- loadPolygon: Boolean,
68
- reverseCoordinatesPolygon: Boolean,
69
- dataPolygon: Object,
70
- configurationMap: Object as any,
71
- coordinatesMap: Array,
72
- getGeoJSON: Function,
73
- getCoodMarker: Function,
74
- isSatelite: Boolean,
75
- },
76
- data() {
77
- return {
78
- idMap: "",
79
- mapRender: null as L.Map | null,
80
- markerRender: null as L.Marker | null,
81
- renderCoordinates: this.coordinatesMap,
82
- renderGeojson: this.dataPolygon as any,
83
- markerIcon: {
84
- iconUrl: markerDefault,
85
- iconSize: [25, 41],
86
- iconAnchor: [12, 41],
87
- },
88
- layersFeatureGroup: null as any,
89
- featuresData: null as any,
90
- currentTileLayer: null as L.TileLayer | null,
91
- isSatelliteView: false,
92
- };
93
- },
94
- mounted() {
95
- this.makeid(10);
96
- this.renderMap();
97
- },
98
- methods: {
99
- createSatelliteControl(): L.Control {
100
- const SatelliteControl = L.Control.extend({
101
- onAdd: (map: L.Map) => {
102
- const container = L.DomUtil.create(
103
- "div",
104
- "leaflet-control-draw leaflet-bar leaflet-control"
105
- );
106
-
107
- const satelliteButton = L.DomUtil.create(
108
- "a",
109
- "leaflet-draw-draw-satellite",
110
- container
111
- );
112
-
113
- satelliteButton.href = "#";
114
- satelliteButton.title = this.isSatelliteView
115
- ? "Vista Normal"
116
- : "Vista Satélite";
117
-
118
- // Usar el mismo estilo que los otros botones de draw
119
- satelliteButton.style.backgroundImage = this.isSatelliteView
120
- ? `url(${gpsIcon})`
121
- : `url(${sateliteIcon})`;
122
- satelliteButton.style.backgroundPosition = "center";
123
- satelliteButton.style.backgroundRepeat = "no-repeat";
124
- satelliteButton.style.backgroundSize = "20px 20px";
125
-
126
- L.DomEvent.disableClickPropagation(satelliteButton);
127
- L.DomEvent.on(
128
- satelliteButton,
129
- "click",
130
- this.toggleSatelliteView,
131
- this
132
- );
133
-
134
- return container;
135
- },
136
- });
137
-
138
- return new SatelliteControl({ position: "topleft" });
139
- },
140
- toggleSatelliteView(): void {
141
- if (!this.mapRender || !this.currentTileLayer) return;
142
-
143
- this.isSatelliteView = !this.isSatelliteView;
144
-
145
- // Remover la capa actual
146
- this.mapRender.removeLayer(this.currentTileLayer as unknown as L.Layer);
147
-
148
- // Agregar la nueva capa
149
- if (this.isSatelliteView) {
150
- this.currentTileLayer = L.tileLayer(
151
- "https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=t8mWT2ozs1JWBqMZOnZr",
152
- {
153
- attribution:
154
- '&copy; <a href="https://www.maptiler.com/">MapTiler</a> &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
155
- }
156
- );
157
- } else {
158
- this.currentTileLayer = L.tileLayer(
159
- "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
160
- {
161
- attribution:
162
- '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
163
- }
164
- );
165
- }
166
-
167
- this.currentTileLayer.addTo(this.mapRender as L.Map);
168
-
169
- // Actualizar el ícono del control satelital
170
- const satelliteButtons = document.querySelectorAll(
171
- ".leaflet-draw-draw-satellite"
172
- );
173
- satelliteButtons.forEach((button) => {
174
- const htmlButton = button as HTMLElement;
175
- htmlButton.style.backgroundImage = this.isSatelliteView
176
- ? `url(${gpsIcon})`
177
- : `url(${sateliteIcon})`;
178
- htmlButton.title = this.isSatelliteView
179
- ? "Vista Normal"
180
- : "Vista Satélite";
181
- });
182
- },
183
- getLayersFeaturesInGeoJson() {
184
- const geojson = L.geoJSON().addTo(this.mapRender as any);
185
- this.featuresData.eachLayer((layer: any) => {
186
- geojson.addLayer(layer);
187
- });
188
- const geojsonString = [geojson.toGeoJSON()]; //JSON.stringify([geojson.toGeoJSON()]);
189
- return geojsonString;
190
- },
191
- triggerSaveEdit(): void {
192
- const buttons =
193
- document.getElementsByClassName(
194
- "leaflet-draw-actions leaflet-draw-actions-top"
195
- ) ||
196
- document.getElementsByClassName(
197
- "leaflet-draw-actions leaflet-draw-actions-top leaflet-draw-actions-bottom"
198
- );
199
- buttons[0]?.querySelector("li")?.querySelector("a")?.click();
200
- },
201
- makeid(length: number): void {
202
- let result = "";
203
- const characters = "abcdefghijklmnopqrstuvwxyz";
204
- const charactersLength = characters.length;
205
- let counter = 0;
206
- while (counter < length) {
207
- result += characters.charAt(
208
- Math.floor(Math.random() * charactersLength)
209
- );
210
- counter += 1;
211
- }
212
- this.idMap = result;
213
- },
214
- renderMap(): void {
215
- drawLocales("es");
216
- setTimeout(() => {
217
- if (this.loadPolygon) {
218
- this.viewMap();
219
- } else {
220
- this.createMap();
221
- }
222
- }, 500);
223
- },
224
- createMap(): void {
225
- const iconDefaultMarket = this.configurationMap
226
- ? this.configurationMap.iconMarker
227
- ? this.configurationMap.iconMarker
228
- : this.markerIcon
229
- : this.markerIcon;
230
- window.type = true;
231
- const map = L.map(this.idMap, { fullscreenControl: true } as any).setView(
232
- [
233
- this.renderCoordinates ? (this.renderCoordinates[0] as number) : 0,
234
- this.renderCoordinates ? (this.renderCoordinates[1] as number) : 0,
235
- ],
236
- this.renderCoordinates ? (this.renderCoordinates[2] as number) : 0
237
- );
238
- this.mapRender = map;
239
-
240
- // Inicializar con la vista correcta según el prop
241
- this.isSatelliteView = this.isSatelite || false;
242
- if (this.isSatelliteView) {
243
- this.currentTileLayer = L.tileLayer(
244
- "https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=t8mWT2ozs1JWBqMZOnZr",
245
- {
246
- attribution:
247
- '&copy; <a href="https://www.maptiler.com/">MapTiler</a> &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
248
- }
249
- );
250
- } else {
251
- this.currentTileLayer = L.tileLayer(
252
- "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
253
- {
254
- attribution:
255
- '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
256
- }
257
- );
258
- }
259
- this.currentTileLayer.addTo(map);
260
-
261
- // Agregar el control satelital si está habilitado
262
- if (this.isSatelite !== undefined) {
263
- const satelliteControl = this.createSatelliteControl();
264
- map.addControl(satelliteControl);
265
- }
266
-
267
- map.setZoom(this.configurationMap?.maxZoom);
268
- if (this.configurationMap.renderMarker) {
269
- const marker = L.marker(
270
- [
271
- this.renderCoordinates ? (this.renderCoordinates[0] as number) : 0,
272
- this.renderCoordinates ? (this.renderCoordinates[1] as number) : 0,
273
- ],
274
- {
275
- icon: L.icon(iconDefaultMarket),
276
- draggable: this.configurationMap.dragMarker,
277
- }
278
- ).addTo(map);
279
- this.markerRender = marker;
280
- if (this.getCoodMarker) {
281
- marker.on("dragend", (event) => {
282
- const updatedCoordinates = event.target.getLatLng();
283
- const lat = updatedCoordinates.lat;
284
- const lng = updatedCoordinates.lng;
285
- if (this.getCoodMarker) this.getCoodMarker(lat, lng);
286
- });
287
- }
288
- }
289
- const featuresData = L.featureGroup();
290
- this.featuresData = featuresData;
291
- const featureGroup = featuresData.addTo(map);
292
- let contextEdit = {
293
- featureGroup: featureGroup, // Crea un nuevo grupo de capas para los polígonos
294
- edit: false,
295
- remove: this.configurationMap?.editFigures.remove,
296
- };
297
- if (this.configurationMap?.editFigures.edit) {
298
- contextEdit = {
299
- featureGroup: featureGroup, // Crea un nuevo grupo de capas para los polígonos
300
- ...this.configurationMap?.editFigures.edit,
301
- remove: this.configurationMap?.editFigures.remove,
302
- };
303
- }
304
- const drawControl = new L.Control.Draw({
305
- position: "topleft",
306
- draw: {
307
- polygon: this.configurationMap?.createFigures.polygon,
308
- circle: false, //this.configurationMap?.createFigures.circle,
309
- rectangle: this.configurationMap?.createFigures.rectangle
310
- ? {
311
- shapeOptions: {
312
- color: "blue",
313
- },
314
- }
315
- : false,
316
- marker: this.configurationMap?.createFigures.marker
317
- ? {
318
- icon: L.icon(iconDefaultMarket),
319
- }
320
- : false,
321
- polyline: this.configurationMap?.createFigures.polyline
322
- ? {
323
- shapeOptions: {
324
- color: "blue",
325
- },
326
- }
327
- : false,
328
- circlemarker: this.configurationMap?.createFigures.multipoint,
329
- },
330
- edit: contextEdit as any,
331
- });
332
- map.addControl(drawControl);
333
-
334
- map.on("draw:created", (event: any) => {
335
- const layer = event.layer;
336
- featureGroup.addLayer(layer);
337
- let geojson = layer.toGeoJSON();
338
- // *** AGREGADO: Manejo especial para CircleMarker (convertir a MultiPoint) ***
339
- if (event.layerType === "circlemarker") {
340
- // Convertir CircleMarker a formato MultiPoint para consistencia
341
- geojson = {
342
- type: "Feature",
343
- geometry: {
344
- type: "MultiPoint",
345
- coordinates: [geojson.geometry.coordinates],
346
- },
347
- properties: {
348
- ...geojson.properties,
349
- pointType: "multipoint",
350
- style: {
351
- color:
352
- this.configurationMap?.createFigures?.multipoint?.color ||
353
- "green",
354
- fillColor:
355
- this.configurationMap?.createFigures?.multipoint?.fillColor ||
356
- "green",
357
- fillOpacity:
358
- this.configurationMap?.createFigures?.multipoint
359
- ?.fillOpacity || 0.8,
360
- radius:
361
- this.configurationMap?.createFigures?.multipoint?.radius || 6,
362
- weight:
363
- this.configurationMap?.createFigures?.multipoint?.weight || 2,
364
- },
365
- },
366
- };
367
- }
368
- if (this.getGeoJSON) this.getGeoJSON([geojson]);
369
- });
370
-
371
- map.on("draw:edited", (event: any) => {
372
- const layers = event.layers;
373
- layers.eachLayer((layer: any) => {
374
- const geojson = layer.toGeoJSON();
375
- if (this.getGeoJSON) this.getGeoJSON([geojson]);
376
- });
377
- });
378
- },
379
- viewMap(): void {
380
- const iconDefaultMarket = this.configurationMap
381
- ? this.configurationMap.iconMarker
382
- ? this.configurationMap.iconMarker
383
- : this.markerIcon
384
- : this.markerIcon;
385
- window.type = true;
386
- const map = L.map(this.idMap, { fullscreenControl: true } as any).setView(
387
- [
388
- this.renderCoordinates ? (this.renderCoordinates[0] as number) : 0,
389
- this.renderCoordinates ? (this.renderCoordinates[1] as number) : 0,
390
- ],
391
- this.renderCoordinates ? (this.renderCoordinates[2] as number) : 0
392
- );
393
- this.mapRender = map;
394
-
395
- // Inicializar con la vista correcta según el prop
396
- this.isSatelliteView = this.isSatelite || false;
397
- if (this.isSatelliteView) {
398
- this.currentTileLayer = L.tileLayer(
399
- "https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=t8mWT2ozs1JWBqMZOnZr",
400
- {
401
- attribution:
402
- '&copy; <a href="https://www.maptiler.com/">MapTiler</a> &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
403
- }
404
- );
405
- } else {
406
- this.currentTileLayer = L.tileLayer(
407
- "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
408
- {
409
- attribution:
410
- '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
411
- }
412
- );
413
- }
414
- this.currentTileLayer.addTo(map);
415
-
416
- // Agregar el control satelital si está habilitado
417
- if (this.isSatelite !== undefined) {
418
- const satelliteControl = this.createSatelliteControl();
419
- map.addControl(satelliteControl);
420
- }
421
-
422
- map.setZoom(this.configurationMap?.maxZoom);
423
- const featuresData = L.featureGroup();
424
- this.featuresData = featuresData;
425
- const featureGroup = featuresData.addTo(map);
426
- let contextEdit = {
427
- featureGroup: featureGroup, // Crea un nuevo grupo de capas para los polígonos
428
- edit: false,
429
- remove: this.configurationMap?.editFigures.remove,
430
- };
431
- if (this.configurationMap?.editFigures.edit) {
432
- contextEdit = {
433
- featureGroup: featureGroup, // Crea un nuevo grupo de capas para los polígonos
434
- ...this.configurationMap?.editFigures.edit,
435
- remove: this.configurationMap?.editFigures.remove,
436
- };
437
- }
438
-
439
- const renderGeojson = this.renderGeojson;
440
- if (renderGeojson && renderGeojson.length) {
441
- renderGeojson.forEach((item: any) => {
442
- const self = this;
443
- if (item.type === "FeatureCollection") {
444
- item.features.forEach((feature: any) => {
445
- if (
446
- feature.geometry.type === "Polygon" ||
447
- feature.geometry.type === "MultiPolygon" ||
448
- feature.geometry.type === "LineString" ||
449
- feature.geometry.type === "MultiLineString" ||
450
- feature.geometry.type === "Point"
451
- ) {
452
- L.geoJson(feature, {
453
- pointToLayer: this.getPointToLayer.bind(this),
454
- onEachFeature: function (feature, layer) {
455
- featureGroup.addLayer(layer);
456
- // Mostrar popup para Point
457
- if (feature.geometry.type === "Point") {
458
- var popupContent = self.getPopupContent(feature);
459
- layer.bindPopup(popupContent);
460
- }
461
- // Mostrar popup para Polygon y MultiPolygon al hacer clic
462
- else if (
463
- feature.geometry.type === "Polygon" ||
464
- feature.geometry.type === "MultiPolygon"
465
- ) {
466
- var tooltipContent =
467
- self.getPolygonTooltipContent(feature);
468
- layer.bindPopup(tooltipContent);
469
- }
470
- },
471
- });
472
- }
473
- });
474
- } else if (item.type === "Feature") {
475
- if (
476
- item.geometry.type === "Polygon" ||
477
- item.geometry.type === "MultiPolygon" ||
478
- item.geometry.type === "LineString" ||
479
- item.geometry.type === "MultiLineString" ||
480
- item.geometry.type === "Point"
481
- ) {
482
- L.geoJson(item, {
483
- pointToLayer: this.getPointToLayer.bind(this),
484
- onEachFeature: function (feature, layer) {
485
- featureGroup.addLayer(layer);
486
- // Mostrar popup para Point
487
- if (feature.geometry.type === "Point") {
488
- var popupContent = self.getPopupContent(feature);
489
- layer.bindPopup(popupContent);
490
- }
491
- // Mostrar popup para Polygon y MultiPolygon al hacer clic
492
- else if (
493
- feature.geometry.type === "Polygon" ||
494
- feature.geometry.type === "MultiPolygon"
495
- ) {
496
- var tooltipContent = self.getPolygonTooltipContent(feature);
497
- layer.bindPopup(tooltipContent);
498
- }
499
- },
500
- });
501
- }
502
- }
503
- });
504
- const bounds = featureGroup.getBounds();
505
- map.fitBounds(bounds);
506
- }
507
- const drawControl = new L.Control.Draw({
508
- position: "topleft",
509
- draw: {
510
- polygon: this.configurationMap?.createFigures.polygon,
511
- circle: false, //this.configurationMap?.createFigures.circle,
512
- rectangle: this.configurationMap?.createFigures.rectangle
513
- ? {
514
- shapeOptions: {
515
- color: "blue",
516
- },
517
- }
518
- : false,
519
- marker: this.configurationMap?.createFigures.marker
520
- ? {
521
- icon: L.icon(iconDefaultMarket),
522
- }
523
- : false,
524
- polyline: this.configurationMap?.createFigures.polyline
525
- ? {
526
- shapeOptions: {
527
- color: "blue",
528
- },
529
- }
530
- : false,
531
- circlemarker: this.configurationMap?.createFigures.multipoint,
532
- },
533
- edit: contextEdit as any,
534
- });
535
- map.addControl(drawControl);
536
-
537
- map.on("draw:created", (event: any) => {
538
- const layer = event.layer;
539
- featureGroup.addLayer(layer);
540
- let geojson = layer.toGeoJSON();
541
- // *** AGREGADO: Manejo especial para CircleMarker (convertir a MultiPoint) ***
542
- if (event.layerType === "circlemarker") {
543
- // Convertir CircleMarker a formato MultiPoint para consistencia
544
- geojson = {
545
- type: "Feature",
546
- geometry: {
547
- type: "MultiPoint",
548
- coordinates: [geojson.geometry.coordinates],
549
- },
550
- properties: {
551
- ...geojson.properties,
552
- pointType: "multipoint",
553
- style: {
554
- color:
555
- this.configurationMap?.createFigures?.multipoint?.color ||
556
- "green",
557
- fillColor:
558
- this.configurationMap?.createFigures?.multipoint?.fillColor ||
559
- "green",
560
- fillOpacity:
561
- this.configurationMap?.createFigures?.multipoint
562
- ?.fillOpacity || 0.8,
563
- radius:
564
- this.configurationMap?.createFigures?.multipoint?.radius || 6,
565
- weight:
566
- this.configurationMap?.createFigures?.multipoint?.weight || 2,
567
- },
568
- },
569
- };
570
- }
571
- if (this.getGeoJSON) this.getGeoJSON(geojson);
572
- });
573
-
574
- map.on("draw:edited", (event: any) => {
575
- const layers = event.layers;
576
- layers.eachLayer((layer: any) => {
577
- const geojson = layer.toGeoJSON();
578
- if (this.getGeoJSON) this.getGeoJSON(geojson);
579
- });
580
- });
581
- },
582
- getPointToLayer(feature: any, latlng: any) {
583
- const geometryType = feature.geometry?.type;
584
-
585
- // Point: icono según properties.tipo
586
- if (geometryType === "Point") {
587
- const tipo = feature.properties?.tipo;
588
- const firstIcon = L.icon({
589
- iconUrl: homeIconUrl,
590
- iconSize: [38, 38],
591
- iconAnchor: [16, 41],
592
- });
593
- const lastIcon = L.icon({
594
- iconUrl: endIconUrl,
595
- iconSize: [38, 38],
596
- iconAnchor: [16, 41],
597
- });
598
- const pointerIcon = L.icon({
599
- iconUrl: trackingIconUrl,
600
- iconSize: [38, 38],
601
- iconAnchor: [16, 41],
602
- });
603
-
604
- let icon = pointerIcon;
605
- if (tipo === 0) icon = firstIcon;
606
- else if (tipo === 1) icon = lastIcon;
607
-
608
- return L.marker(latlng, { icon });
609
- }
610
-
611
- // Otros tipos: icono por defecto
612
- return L.marker(latlng);
613
- },
614
- async searchAddress(query: string) {
615
- const address = await axios.get(
616
- location.protocol + "//nominatim.openstreetmap.org/search?",
617
- {
618
- params: {
619
- //query
620
- q: query,
621
- limit: 1,
622
- format: "json",
623
- },
624
- }
625
- );
626
- if (address.data.length === 1) {
627
- const lat = parseFloat(address.data[0].lat);
628
- const lng = parseFloat(address.data[0].lon);
629
- const coord = { lng, lat };
630
- this.setCoordinates({ ...coord, moveMarker: true });
631
- return address.data[0];
632
- }
633
- return address.data;
634
- },
635
- setCoordinates({
636
- lat,
637
- lng,
638
- }: {
639
- lat: number;
640
- lng: number;
641
- moveMarker?: boolean;
642
- }): void {
643
- if (this.mapRender) {
644
- const newCenter = L.latLng(lat, lng);
645
- this.mapRender.panTo(newCenter, { animate: true, duration: 0.5 });
646
- }
647
- },
648
- getPopupContent(feature: any): string {
649
- // Puedes personalizar el contenido del popup aquí
650
- const props = feature.properties || {};
651
- // Solo toma en cuenta descripcion y fechaHoraLlegada
652
- const descripcion = props.descripcion || "";
653
- const fechaHoraLlegada = props.fechaHoraLlegada || "";
654
- // url de la foto
655
- const fotoId = props.fotoId || "";
656
-
657
- const formatIsoPreserve = (iso: string): string => {
658
- const d = new Date(iso);
659
- if (isNaN(d.getTime())) return iso; // fallback: devolver la cadena original si no es válida
660
- const pad = (n: number) => n.toString().padStart(2, "0");
661
- // Usar métodos UTC para evitar que el navegador convierta a la zona local
662
- const day = pad(d.getUTCDate());
663
- const month = pad(d.getUTCMonth() + 1);
664
- const year = d.getUTCFullYear();
665
- const hours = pad(d.getUTCHours());
666
- const minutes = pad(d.getUTCMinutes());
667
- const seconds = pad(d.getUTCSeconds());
668
- return `${day}-${month}-${year} ${hours}:${minutes}:${seconds}`;
669
- };
670
-
671
- // Formatear fecha a dd-MM-yyyy hh:mm:ss
672
- let fechaFormateada = "";
673
- if (fechaHoraLlegada) {
674
- /* const fecha = new Date(fechaHoraLlegada);
675
- const pad = (n: number) => n.toString().padStart(2, "0");
676
- fechaFormateada = `${pad(fecha.getDate())}-${pad(
677
- fecha.getMonth() + 1
678
- )}-${fecha.getFullYear()} ${pad(fecha.getHours())}:${pad(
679
- fecha.getMinutes()
680
- )}:${pad(fecha.getSeconds())}`; */
681
- fechaFormateada = formatIsoPreserve(fechaHoraLlegada);
682
- }
683
- let html = "<div>";
684
- if (fotoId) {
685
- html += `<img src="${fotoId}" style="width: 200px; height: auto; margin-bottom: 5px;"><br>`;
686
- }
687
- html += `<b>Descripción:</b> ${descripcion}<br>`;
688
- if (fechaFormateada) {
689
- html += `<b>Fecha Hora Llegada:</b> ${fechaFormateada}<br>`;
690
- }
691
- html += "</div>";
692
- return html;
693
- },
694
- getPolygonTooltipContent(feature: any): string {
695
- const props = feature.properties || {};
696
- const descripcion = props.descripcion || props.name || "Sin descripción";
697
-
698
- return `<div style="font-weight: bold; padding: 5px;">${descripcion}</div>`;
699
- },
700
- },
701
- watch: {
702
- coordinatesMap(newVal) {
703
- this.renderCoordinates = newVal;
704
- if (this.mapRender && this.markerRender) {
705
- const newLatLng = L.latLng([newVal[0], newVal[1]]);
706
- this.mapRender.setView([newVal[0], newVal[1]], newVal[2]);
707
- this.markerRender.setLatLng(newLatLng);
708
- }
709
- },
710
- dataPolygon(newVal) {
711
- this.renderGeojson = newVal;
712
- },
713
- },
714
- });
715
- </script>
1
+ <template>
2
+ <div class="map-container" :style="`height:${configurationMap?.height}`">
3
+ <div :id="idMap" style="height: 100%"></div>
4
+ </div>
5
+ </template>
6
+
7
+ <script lang="ts">
8
+ import { defineComponent } from "vue";
9
+ import { markerDefault } from "../helpers/imgBase64";
10
+ import homeIconUrl from "../assets/home.png";
11
+ import endIconUrl from "../assets/end.png";
12
+ import trackingIconUrl from "../assets/tracking.png";
13
+ import origenIconUrl from "../assets/origen.png";
14
+ import destinoIconUrl from "../assets/destino.png";
15
+ import llegadaIconUrl from "../assets/llegada.png";
16
+ import DestinoIconReferencia from "../assets/address.svg";
17
+ import LLegadaSalidaIconReferencia from "../assets/entidad.svg";
18
+ import * as L from "leaflet";
19
+ import "leaflet-draw";
20
+ import "leaflet/dist/leaflet.css";
21
+ import "leaflet-draw/dist/leaflet.draw.css";
22
+ import drawLocales from "leaflet-draw-locales";
23
+ import axios from "axios";
24
+ import "leaflet.fullscreen/Control.FullScreen.css";
25
+ import "leaflet.fullscreen";
26
+ import gpsIcon from "../assets/gps.svg";
27
+ import sateliteIcon from "../assets/satelite.svg";
28
+
29
+ declare global {
30
+ interface Window {
31
+ type: boolean;
32
+ }
33
+ }
34
+
35
+ L.Edit.Circle = L.Edit.CircleMarker.extend({
36
+ _createResizeMarker: function () {
37
+ var center = this._shape.getLatLng(),
38
+ resizemarkerPoint = this._getResizeMarkerPoint(center);
39
+
40
+ this._resizeMarkers = [];
41
+ this._resizeMarkers.push(
42
+ this._createMarker(resizemarkerPoint, this.options.resizeIcon)
43
+ );
44
+ },
45
+
46
+ _getResizeMarkerPoint: function (latlng: any) {
47
+ var delta = this._shape._radius * Math.cos(Math.PI / 4),
48
+ point = this._map.project(latlng);
49
+ return this._map.unproject([point.x + delta, point.y - delta]);
50
+ },
51
+
52
+ _resize: function (latlng: any) {
53
+ var moveLatLng = this._moveMarker.getLatLng();
54
+ var radius;
55
+ if (L.GeometryUtil.isVersion07x()) {
56
+ radius = moveLatLng.distanceTo(latlng);
57
+ } else {
58
+ radius = moveLatLng.distanceTo(latlng);
59
+ }
60
+ this._shape.setRadius(radius);
61
+ },
62
+ });
63
+
64
+ export default defineComponent({
65
+ props: {
66
+ configurationMap: { type: Object, required: false },
67
+ coordinatesMap: { type: [Array, Function], required: false },
68
+ dataPolygon: { type: [Array, Object], required: false },
69
+ isSatelite: { type: Boolean, required: false },
70
+ getCoodMarker: { type: Function, required: false },
71
+ getGeoJSON: { type: Function, required: false },
72
+ loadPolygon: { type: Boolean, required: false },
73
+ },
74
+ data() {
75
+ return {
76
+ idMap: "",
77
+ mapRender: null as L.Map | null,
78
+ markerRender: null as L.Marker | null,
79
+ renderCoordinates: Array.isArray(this.coordinatesMap)
80
+ ? (this.coordinatesMap as number[])
81
+ : [],
82
+ renderGeojson: this.dataPolygon as any,
83
+ markerIcon: {
84
+ iconUrl: markerDefault,
85
+ iconSize: [25, 41],
86
+ iconAnchor: [12, 41],
87
+ },
88
+ layersFeatureGroup: null as any,
89
+ featuresData: null as any,
90
+ currentTileLayer: null as L.TileLayer | null,
91
+ isSatelliteView: false,
92
+ };
93
+ },
94
+ mounted() {
95
+ this.makeid(10);
96
+ this.renderMap();
97
+ },
98
+ methods: {
99
+ createSatelliteControl(): L.Control {
100
+ const SatelliteControl = L.Control.extend({
101
+ onAdd: (map: L.Map) => {
102
+ const container = L.DomUtil.create(
103
+ "div",
104
+ "leaflet-control-draw leaflet-bar leaflet-control"
105
+ );
106
+
107
+ const satelliteButton = L.DomUtil.create(
108
+ "a",
109
+ "leaflet-draw-draw-satellite",
110
+ container
111
+ );
112
+
113
+ satelliteButton.href = "#";
114
+ satelliteButton.title = this.isSatelliteView
115
+ ? "Vista Normal"
116
+ : "Vista Satélite";
117
+
118
+ // Usar el mismo estilo que los otros botones de draw
119
+ satelliteButton.style.backgroundImage = this.isSatelliteView
120
+ ? `url(${gpsIcon})`
121
+ : `url(${sateliteIcon})`;
122
+ satelliteButton.style.backgroundPosition = "center";
123
+ satelliteButton.style.backgroundRepeat = "no-repeat";
124
+ satelliteButton.style.backgroundSize = "20px 20px";
125
+
126
+ L.DomEvent.disableClickPropagation(satelliteButton);
127
+ L.DomEvent.on(
128
+ satelliteButton,
129
+ "click",
130
+ this.toggleSatelliteView,
131
+ this
132
+ );
133
+
134
+ return container;
135
+ },
136
+ });
137
+
138
+ return new SatelliteControl({ position: "topleft" });
139
+ },
140
+ toggleSatelliteView(): void {
141
+ if (!this.mapRender || !this.currentTileLayer) return;
142
+
143
+ this.isSatelliteView = !this.isSatelliteView;
144
+
145
+ // Remover la capa actual
146
+ this.mapRender.removeLayer(this.currentTileLayer as unknown as L.Layer);
147
+
148
+ // Agregar la nueva capa
149
+ if (this.isSatelliteView) {
150
+ this.currentTileLayer = L.tileLayer(
151
+ "https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=t8mWT2ozs1JWBqMZOnZr",
152
+ {
153
+ attribution:
154
+ '&copy; <a href="https://www.maptiler.com/">MapTiler</a> &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
155
+ }
156
+ );
157
+ } else {
158
+ this.currentTileLayer = L.tileLayer(
159
+ "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
160
+ {
161
+ attribution:
162
+ '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
163
+ }
164
+ );
165
+ }
166
+
167
+ this.currentTileLayer.addTo(this.mapRender as L.Map);
168
+
169
+ // Actualizar el ícono del control satelital
170
+ const satelliteButtons = document.querySelectorAll(
171
+ ".leaflet-draw-draw-satellite"
172
+ );
173
+ satelliteButtons.forEach((button) => {
174
+ const htmlButton = button as HTMLElement;
175
+ htmlButton.style.backgroundImage = this.isSatelliteView
176
+ ? `url(${gpsIcon})`
177
+ : `url(${sateliteIcon})`;
178
+ htmlButton.title = this.isSatelliteView
179
+ ? "Vista Normal"
180
+ : "Vista Satélite";
181
+ });
182
+ },
183
+ getLayersFeaturesInGeoJson() {
184
+ const geojson = L.geoJSON().addTo(this.mapRender as any);
185
+ this.featuresData.eachLayer((layer: any) => {
186
+ geojson.addLayer(layer);
187
+ });
188
+ const geojsonString = [geojson.toGeoJSON()]; //JSON.stringify([geojson.toGeoJSON()]);
189
+ return geojsonString;
190
+ },
191
+ triggerSaveEdit(): void {
192
+ const buttons =
193
+ document.getElementsByClassName(
194
+ "leaflet-draw-actions leaflet-draw-actions-top"
195
+ ) ||
196
+ document.getElementsByClassName(
197
+ "leaflet-draw-actions leaflet-draw-actions-top leaflet-draw-actions-bottom"
198
+ );
199
+ buttons[0]?.querySelector("li")?.querySelector("a")?.click();
200
+ },
201
+ makeid(length: number): void {
202
+ let result = "";
203
+ const characters = "abcdefghijklmnopqrstuvwxyz";
204
+ const charactersLength = characters.length;
205
+ let counter = 0;
206
+ while (counter < length) {
207
+ result += characters.charAt(
208
+ Math.floor(Math.random() * charactersLength)
209
+ );
210
+ counter += 1;
211
+ }
212
+ this.idMap = result;
213
+ },
214
+ renderMap(): void {
215
+ drawLocales("es");
216
+ setTimeout(() => {
217
+ if (this.loadPolygon) {
218
+ this.viewMap();
219
+ } else {
220
+ this.createMap();
221
+ }
222
+ }, 500);
223
+ },
224
+ createMap(): void {
225
+ const iconDefaultMarket = this.configurationMap
226
+ ? this.configurationMap.iconMarker
227
+ ? this.configurationMap.iconMarker
228
+ : this.markerIcon
229
+ : this.markerIcon;
230
+ window.type = true;
231
+ const map = L.map(this.idMap, { fullscreenControl: true } as any).setView(
232
+ [
233
+ this.renderCoordinates ? (this.renderCoordinates[0] as number) : 0,
234
+ this.renderCoordinates ? (this.renderCoordinates[1] as number) : 0,
235
+ ],
236
+ this.renderCoordinates ? (this.renderCoordinates[2] as number) : 0
237
+ );
238
+ this.mapRender = map;
239
+
240
+ // Inicializar con la vista correcta según el prop
241
+ this.isSatelliteView = this.isSatelite || false;
242
+ if (this.isSatelliteView) {
243
+ this.currentTileLayer = L.tileLayer(
244
+ "https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=t8mWT2ozs1JWBqMZOnZr",
245
+ {
246
+ attribution:
247
+ '&copy; <a href="https://www.maptiler.com/">MapTiler</a> &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
248
+ }
249
+ );
250
+ } else {
251
+ this.currentTileLayer = L.tileLayer(
252
+ "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
253
+ {
254
+ attribution:
255
+ '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
256
+ }
257
+ );
258
+ }
259
+ this.currentTileLayer.addTo(map);
260
+
261
+ // Agregar el control satelital si está habilitado
262
+ if (this.isSatelite !== undefined) {
263
+ const satelliteControl = this.createSatelliteControl();
264
+ map.addControl(satelliteControl);
265
+ }
266
+
267
+ map.setZoom(this.configurationMap?.maxZoom);
268
+ if (this.configurationMap?.renderMarker) {
269
+ const marker = L.marker(
270
+ [
271
+ this.renderCoordinates ? (this.renderCoordinates[0] as number) : 0,
272
+ this.renderCoordinates ? (this.renderCoordinates[1] as number) : 0,
273
+ ],
274
+ {
275
+ icon: L.icon(iconDefaultMarket),
276
+ draggable: this.configurationMap?.dragMarker,
277
+ }
278
+ ).addTo(map);
279
+ this.markerRender = marker;
280
+ if (this.getCoodMarker) {
281
+ marker.on("dragend", (event) => {
282
+ const updatedCoordinates = event.target.getLatLng();
283
+ const lat = updatedCoordinates.lat;
284
+ const lng = updatedCoordinates.lng;
285
+ if (this.getCoodMarker) this.getCoodMarker(lat, lng);
286
+ });
287
+ }
288
+ }
289
+ const featuresData = L.featureGroup();
290
+ this.featuresData = featuresData;
291
+ const featureGroup = featuresData.addTo(map);
292
+ let contextEdit = {
293
+ featureGroup: featureGroup, // Crea un nuevo grupo de capas para los polígonos
294
+ edit: false,
295
+ remove: this.configurationMap?.editFigures.remove,
296
+ };
297
+ if (this.configurationMap?.editFigures.edit) {
298
+ contextEdit = {
299
+ featureGroup: featureGroup, // Crea un nuevo grupo de capas para los polígonos
300
+ ...this.configurationMap?.editFigures.edit,
301
+ remove: this.configurationMap?.editFigures.remove,
302
+ };
303
+ }
304
+ const drawControl = new L.Control.Draw({
305
+ position: "topleft",
306
+ draw: {
307
+ polygon: this.configurationMap?.createFigures.polygon,
308
+ circle: false, //this.configurationMap?.createFigures.circle,
309
+ rectangle: this.configurationMap?.createFigures.rectangle
310
+ ? {
311
+ shapeOptions: {
312
+ color: "blue",
313
+ },
314
+ }
315
+ : false,
316
+ marker: this.configurationMap?.createFigures.marker
317
+ ? {
318
+ icon: L.icon(iconDefaultMarket),
319
+ }
320
+ : false,
321
+ polyline: this.configurationMap?.createFigures.polyline
322
+ ? {
323
+ shapeOptions: {
324
+ color: "blue",
325
+ },
326
+ }
327
+ : false,
328
+ circlemarker: this.configurationMap?.createFigures.multipoint,
329
+ },
330
+ edit: contextEdit as any,
331
+ });
332
+ map.addControl(drawControl);
333
+
334
+ map.on("draw:created", (event: any) => {
335
+ const layer = event.layer;
336
+ featureGroup.addLayer(layer);
337
+ let geojson = layer.toGeoJSON();
338
+ // *** AGREGADO: Manejo especial para CircleMarker (convertir a MultiPoint) ***
339
+ if (event.layerType === "circlemarker") {
340
+ // Convertir CircleMarker a formato MultiPoint para consistencia
341
+ geojson = {
342
+ type: "Feature",
343
+ geometry: {
344
+ type: "MultiPoint",
345
+ coordinates: [geojson.geometry.coordinates],
346
+ },
347
+ properties: {
348
+ ...geojson.properties,
349
+ pointType: "multipoint",
350
+ style: {
351
+ color:
352
+ this.configurationMap?.createFigures?.multipoint?.color ||
353
+ "green",
354
+ fillColor:
355
+ this.configurationMap?.createFigures?.multipoint?.fillColor ||
356
+ "green",
357
+ fillOpacity:
358
+ this.configurationMap?.createFigures?.multipoint
359
+ ?.fillOpacity || 0.8,
360
+ radius:
361
+ this.configurationMap?.createFigures?.multipoint?.radius || 6,
362
+ weight:
363
+ this.configurationMap?.createFigures?.multipoint?.weight || 2,
364
+ },
365
+ },
366
+ };
367
+ }
368
+ if (this.getGeoJSON) this.getGeoJSON([geojson]);
369
+ });
370
+
371
+ map.on("draw:edited", (event: any) => {
372
+ const layers = event.layers;
373
+ layers.eachLayer((layer: any) => {
374
+ const geojson = layer.toGeoJSON();
375
+ if (this.getGeoJSON) this.getGeoJSON([geojson]);
376
+ });
377
+ });
378
+ },
379
+ viewMap(): void {
380
+ const iconDefaultMarket = this.configurationMap
381
+ ? this.configurationMap.iconMarker
382
+ ? this.configurationMap.iconMarker
383
+ : this.markerIcon
384
+ : this.markerIcon;
385
+ window.type = true;
386
+ const map = L.map(this.idMap, { fullscreenControl: true } as any).setView(
387
+ [
388
+ this.renderCoordinates ? (this.renderCoordinates[0] as number) : 0,
389
+ this.renderCoordinates ? (this.renderCoordinates[1] as number) : 0,
390
+ ],
391
+ this.renderCoordinates ? (this.renderCoordinates[2] as number) : 0
392
+ );
393
+ this.mapRender = map;
394
+
395
+ // Inicializar con la vista correcta según el prop
396
+ this.isSatelliteView = this.isSatelite || false;
397
+ if (this.isSatelliteView) {
398
+ this.currentTileLayer = L.tileLayer(
399
+ "https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=t8mWT2ozs1JWBqMZOnZr",
400
+ {
401
+ attribution:
402
+ '&copy; <a href="https://www.maptiler.com/">MapTiler</a> &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
403
+ }
404
+ );
405
+ } else {
406
+ this.currentTileLayer = L.tileLayer(
407
+ "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
408
+ {
409
+ attribution:
410
+ '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
411
+ }
412
+ );
413
+ }
414
+ this.currentTileLayer.addTo(map);
415
+
416
+ // Agregar el control satelital si está habilitado
417
+ if (this.isSatelite !== undefined) {
418
+ const satelliteControl = this.createSatelliteControl();
419
+ map.addControl(satelliteControl);
420
+ }
421
+
422
+ map.setZoom(this.configurationMap?.maxZoom);
423
+ const featuresData = L.featureGroup();
424
+ this.featuresData = featuresData;
425
+ const featureGroup = featuresData.addTo(map);
426
+ let contextEdit = {
427
+ featureGroup: featureGroup, // Crea un nuevo grupo de capas para los polígonos
428
+ edit: false,
429
+ remove: this.configurationMap?.editFigures.remove,
430
+ };
431
+ if (this.configurationMap?.editFigures.edit) {
432
+ contextEdit = {
433
+ featureGroup: featureGroup, // Crea un nuevo grupo de capas para los polígonos
434
+ ...this.configurationMap?.editFigures.edit,
435
+ remove: this.configurationMap?.editFigures.remove,
436
+ };
437
+ }
438
+
439
+ const vm: any = this;
440
+ const renderGeojson = this.renderGeojson;
441
+ if (renderGeojson && renderGeojson.length) {
442
+ // Collect Point features to cluster (we'll render non-Point features normally)
443
+ const pointsToCluster: Array<any> = [];
444
+ // LayerGroup to hold cluster markers so we can clear on zoom
445
+ const clusterLayer = L.layerGroup().addTo(map);
446
+
447
+ renderGeojson.forEach((item: any) => {
448
+ const self = this;
449
+ if (item.type === "FeatureCollection") {
450
+ item.features.forEach((feature: any) => {
451
+ if (
452
+ feature.geometry.type === "Polygon" ||
453
+ feature.geometry.type === "MultiPolygon" ||
454
+ feature.geometry.type === "LineString" ||
455
+ feature.geometry.type === "MultiLineString" ||
456
+ feature.geometry.type === "Point"
457
+ ) {
458
+ // For Point features, collect them for clustering instead of adding directly
459
+ if (feature.geometry.type === "Point") {
460
+ const coords = feature.geometry.coordinates;
461
+ pointsToCluster.push({
462
+ lat: coords[1],
463
+ lng: coords[0],
464
+ properties: feature.properties || {},
465
+ feature,
466
+ });
467
+ } else {
468
+ // Non-point features: render normally
469
+ L.geoJson(feature, {
470
+ pointToLayer: this.getPointToLayer.bind(this),
471
+ onEachFeature: function (feature, layer) {
472
+ featureGroup.addLayer(layer);
473
+ // Mostrar popup para Polygon y MultiPolygon al hacer clic
474
+ if (
475
+ feature.geometry.type === "Polygon" ||
476
+ feature.geometry.type === "MultiPolygon"
477
+ ) {
478
+ var tooltipContent =
479
+ self.getPolygonTooltipContent(feature);
480
+ layer.bindPopup(tooltipContent, {
481
+ maxHeight: 360,
482
+ maxWidth: 320,
483
+ className: "limit-popup",
484
+ });
485
+ }
486
+ },
487
+ });
488
+ }
489
+ // Agregar markers y flechas de dirección para LineString
490
+ if (
491
+ feature.geometry.type === "LineString" &&
492
+ Array.isArray(feature.geometry.coordinates)
493
+ ) {
494
+ const coords = feature.geometry.coordinates;
495
+ const coordProps =
496
+ feature.properties &&
497
+ Array.isArray(feature.properties.coordProperties)
498
+ ? feature.properties.coordProperties
499
+ : [];
500
+ // Only render per-vertex markers/arrows when coordProperties exist and are non-empty
501
+ if (Array.isArray(coordProps) && coordProps.length) {
502
+ for (let i = 0; i < coords.length; i++) {
503
+ // Unificar círculo y flecha en un solo divIcon
504
+ let iconHtml =
505
+ '<div style="display:flex;align-items:center;justify-content:center;width:14px;height:14px;">';
506
+ // Círculo azul con padding, envuelve el SVG
507
+ let arrowSvg = "";
508
+ if (
509
+ i < coords.length - 1 &&
510
+ coordProps[i] &&
511
+ coordProps[i + 1] &&
512
+ coordProps[i].fecha &&
513
+ coordProps[i + 1].fecha &&
514
+ new Date(coordProps[i + 1].fecha) >
515
+ new Date(coordProps[i].fecha)
516
+ ) {
517
+ // Calcular ángulo (bearing)
518
+ const toRad = (deg: number) => (deg * Math.PI) / 180;
519
+ const toDeg = (rad: number) => (rad * 180) / Math.PI;
520
+ const lat1 = toRad(coords[i][1]);
521
+ const lat2 = toRad(coords[i + 1][1]);
522
+ const dLon = toRad(coords[i + 1][0] - coords[i][0]);
523
+ const y = Math.sin(dLon) * Math.cos(lat2);
524
+ const x =
525
+ Math.cos(lat1) * Math.sin(lat2) -
526
+ Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
527
+ let brng = Math.atan2(y, x);
528
+ brng = toDeg(brng);
529
+ const angle = (brng + 360) % 360;
530
+ arrowSvg = `<svg width="10" height="10" viewBox="0 0 22 22" style="display:block;transform:rotate(${angle}deg);"><polygon points="11,3 15,17 11,14 7,17" fill="#fff" stroke="#fff" stroke-width="1.5"/></svg>`;
531
+ }
532
+ iconHtml += `<div style="background:#2196F3;border-radius:50%;border:1px solid #fff;box-shadow:0 0 2px #000;padding:1px;width:8px;height:8px;display:flex;align-items:center;justify-content:center;">${arrowSvg}</div>`;
533
+ iconHtml += "</div>";
534
+ const marker = L.marker([coords[i][1], coords[i][0]], {
535
+ icon: L.divIcon({
536
+ className: "",
537
+ html: iconHtml,
538
+ iconSize: [14, 14],
539
+ iconAnchor: [7, 7],
540
+ }),
541
+ zIndexOffset: -100,
542
+ });
543
+ marker.addTo(featureGroup);
544
+ }
545
+ }
546
+ }
547
+ }
548
+ });
549
+ } else if (item.type === "Feature") {
550
+ if (
551
+ item.geometry.type === "Polygon" ||
552
+ item.geometry.type === "MultiPolygon" ||
553
+ item.geometry.type === "LineString" ||
554
+ item.geometry.type === "MultiLineString" ||
555
+ item.geometry.type === "Point"
556
+ ) {
557
+ // If it's a Point feature, collect it for clustering
558
+ if (item.geometry.type === "Point") {
559
+ const coords = item.geometry.coordinates;
560
+ pointsToCluster.push({
561
+ lat: coords[1],
562
+ lng: coords[0],
563
+ properties: item.properties || {},
564
+ feature: item,
565
+ });
566
+ } else {
567
+ // Non-point features: render normally
568
+ L.geoJson(item, {
569
+ pointToLayer: this.getPointToLayer.bind(this),
570
+ onEachFeature: function (feature, layer) {
571
+ featureGroup.addLayer(layer);
572
+ // Mostrar popup para Polygon y MultiPolygon al hacer clic
573
+ if (
574
+ feature.geometry.type === "Polygon" ||
575
+ feature.geometry.type === "MultiPolygon"
576
+ ) {
577
+ var tooltipContent =
578
+ self.getPolygonTooltipContent(feature);
579
+ layer.bindPopup(tooltipContent, {
580
+ maxHeight: 360,
581
+ maxWidth: 320,
582
+ className: "limit-popup",
583
+ });
584
+ }
585
+ },
586
+ });
587
+ }
588
+ // Agregar markers y flechas de dirección para LineString
589
+ if (
590
+ item.geometry.type === "LineString" &&
591
+ Array.isArray(item.geometry.coordinates)
592
+ ) {
593
+ const coords = item.geometry.coordinates;
594
+ const coordProps =
595
+ item.properties &&
596
+ Array.isArray(item.properties.coordProperties)
597
+ ? item.properties.coordProperties
598
+ : [];
599
+ // Only render per-vertex markers/arrows when coordProperties exist and are non-empty
600
+ if (Array.isArray(coordProps) && coordProps.length) {
601
+ for (let i = 0; i < coords.length; i++) {
602
+ // Unificar círculo y flecha en un solo divIcon
603
+ let iconHtml =
604
+ '<div style="display:flex;align-items:center;justify-content:center;width:14px;height:14px;">';
605
+ // Círculo azul con padding, envuelve el SVG
606
+ let arrowSvg = "";
607
+ if (
608
+ i < coords.length - 1 &&
609
+ coordProps[i] &&
610
+ coordProps[i + 1] &&
611
+ coordProps[i].fecha &&
612
+ coordProps[i + 1].fecha &&
613
+ new Date(coordProps[i + 1].fecha) >
614
+ new Date(coordProps[i].fecha)
615
+ ) {
616
+ // Calcular ángulo (bearing)
617
+ const toRad = (deg: number) => (deg * Math.PI) / 180;
618
+ const toDeg = (rad: number) => (rad * 180) / Math.PI;
619
+ const lat1 = toRad(coords[i][1]);
620
+ const lat2 = toRad(coords[i + 1][1]);
621
+ const dLon = toRad(coords[i + 1][0] - coords[i][0]);
622
+ const y = Math.sin(dLon) * Math.cos(lat2);
623
+ const x =
624
+ Math.cos(lat1) * Math.sin(lat2) -
625
+ Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon);
626
+ let brng = Math.atan2(y, x);
627
+ brng = toDeg(brng);
628
+ const angle = (brng + 360) % 360;
629
+ arrowSvg = `<svg width="10" height="10" viewBox="0 0 22 22" style="display:block;transform:rotate(${angle}deg);"><polygon points="11,3 15,17 11,14 7,17" fill="#fff" stroke="#fff" stroke-width="1.5"/></svg>`;
630
+ }
631
+ iconHtml += `<div style="background:#2196F3;border-radius:50%;border:1px solid #fff;box-shadow:0 0 2px #000;padding:1.5px;width:8px;height:8px;display:flex;align-items:center;justify-content:center;">${arrowSvg}</div>`;
632
+ iconHtml += "</div>";
633
+ const marker = L.marker([coords[i][1], coords[i][0]], {
634
+ icon: L.divIcon({
635
+ className: "",
636
+ html: iconHtml,
637
+ iconSize: [14, 14],
638
+ iconAnchor: [7, 7],
639
+ }),
640
+ });
641
+ marker.addTo(featureGroup);
642
+ }
643
+ }
644
+ }
645
+ }
646
+ }
647
+ });
648
+ // --- Render point clusters collected above ---
649
+ const renderPointClusters = () => {
650
+ clusterLayer.clearLayers();
651
+ if (!pointsToCluster.length) return;
652
+
653
+ // Helper to convert lat/lng to pixel point
654
+ const latLngToPixel = (lat: number, lng: number) =>
655
+ map.project([lat, lng], map.getZoom());
656
+
657
+ const pixelDistance = Math.max(30, 80 - map.getZoom() * 2); // dynamic threshold
658
+ const pts = pointsToCluster.slice();
659
+ const used = new Array(pts.length).fill(false);
660
+ const clusters: Array<any[]> = [];
661
+
662
+ for (let i = 0; i < pts.length; i++) {
663
+ if (used[i]) continue;
664
+ const base = pts[i];
665
+ const basePx = latLngToPixel(base.lat, base.lng);
666
+ const cluster = [base];
667
+ used[i] = true;
668
+ for (let j = i + 1; j < pts.length; j++) {
669
+ if (used[j]) continue;
670
+ const cmp = pts[j];
671
+ const cmpPx = latLngToPixel(cmp.lat, cmp.lng);
672
+ const dist = Math.sqrt(
673
+ Math.pow(basePx.x - cmpPx.x, 2) +
674
+ Math.pow(basePx.y - cmpPx.y, 2)
675
+ );
676
+ if (dist < pixelDistance) {
677
+ cluster.push(cmp);
678
+ used[j] = true;
679
+ }
680
+ }
681
+ clusters.push(cluster);
682
+ }
683
+
684
+ clusters.forEach((cluster) => {
685
+ // average center
686
+ const lat = cluster.reduce((s, p) => s + p.lat, 0) / cluster.length;
687
+ const lng = cluster.reduce((s, p) => s + p.lng, 0) / cluster.length;
688
+
689
+ // helper to create icon based on tipo property (match getPointToLayer)
690
+ const makeIconFor = (props: any) => {
691
+ const tipo = props?.tipo;
692
+ const w = 50;
693
+ const h = 50;
694
+ const ax = 16;
695
+ const ay = 41;
696
+ // popupAnchor relative to iconAnchor so popup arrow points to the visual center
697
+ const popupAnchor = [
698
+ Math.round(w / 2 - ax),
699
+ Math.round(h / 2 - ay),
700
+ ] as [number, number];
701
+ const firstIcon = L.icon({
702
+ iconUrl: origenIconUrl,
703
+ iconSize: [w, h],
704
+ iconAnchor: [ax, ay],
705
+ popupAnchor,
706
+ });
707
+ const lastIcon = L.icon({
708
+ iconUrl: destinoIconUrl,
709
+ iconSize: [w, h],
710
+ iconAnchor: [ax, ay],
711
+ popupAnchor,
712
+ });
713
+ const pointerIcon = L.icon({
714
+ iconUrl: llegadaIconUrl,
715
+ iconSize: [w, h],
716
+ iconAnchor: [ax, ay],
717
+ popupAnchor,
718
+ });
719
+ if (tipo === 0) return firstIcon;
720
+ if (tipo === 1) return lastIcon;
721
+ return pointerIcon;
722
+ };
723
+
724
+ if (cluster.length === 1) {
725
+ const p = cluster[0];
726
+ const icon = makeIconFor(p.properties || {});
727
+ const marker = L.marker([lat, lng], { icon, zIndexOffset: 1000 });
728
+ // bind popup from original feature
729
+ const tempFeature = p.feature || {};
730
+ const popupContent = vm.getPopupContent(tempFeature);
731
+ marker.bindPopup(popupContent, {
732
+ maxHeight: 360,
733
+ maxWidth: 320,
734
+ className: "limit-popup",
735
+ });
736
+ clusterLayer.addLayer(marker);
737
+ } else {
738
+ // Use icon of the first point as the visible marker
739
+ const primary = cluster[0];
740
+ const icon = makeIconFor(primary.properties || {});
741
+ // cluster marker container: marker with icon plus side badge
742
+ const markerHtml =
743
+ `<div style="position:relative;display:inline-block;">` +
744
+ `<img src="${
745
+ (icon as any).options.iconUrl
746
+ }" style="width:50px;height:50px;object-fit:cover;"/>` +
747
+ `<div style="position:absolute;right:-2px;top:-2px;background:#2196F3;color:#fff;font-size:11px;font-weight:bold;border-radius:50%;padding:0;min-width:18px;height:18px;display:flex;align-items:center;justify-content:center;z-index:3;border:1.5px solid #fff;">${cluster.length}</div>` +
748
+ `</div>`;
749
+
750
+ const marker = L.marker([lat, lng], {
751
+ icon: L.divIcon({
752
+ className: "",
753
+ html: markerHtml,
754
+ iconSize: [50, 50],
755
+ iconAnchor: [25, 25],
756
+ popupAnchor: [0, 0],
757
+ }),
758
+ zIndexOffset: 1000,
759
+ });
760
+
761
+ // build popup by concatenating each member's popup HTML separated by a divider
762
+ const parts: string[] = [];
763
+ cluster.forEach((p) => {
764
+ const tempFeature = p.feature || {};
765
+ const content = vm.getPopupContent(tempFeature) || "";
766
+ parts.push(`<div style="padding:6px 0;">${content}</div>`);
767
+ });
768
+ const popupHtml = parts.join(
769
+ '<hr style="border:none;border-top:1px solid #ddd;margin:6px 0;"/>'
770
+ );
771
+ marker.bindPopup(
772
+ `<div style="max-width:320px;">${popupHtml}</div>`,
773
+ { maxHeight: 360, maxWidth: 320, className: "limit-popup" }
774
+ );
775
+ clusterLayer.addLayer(marker);
776
+ }
777
+ });
778
+ };
779
+
780
+ // Initial render and re-render on zoom
781
+ renderPointClusters();
782
+ map.on("zoomend", () => {
783
+ renderPointClusters();
784
+ });
785
+
786
+ const bounds = featureGroup.getBounds();
787
+ map.fitBounds(bounds);
788
+ }
789
+ const drawControl = new L.Control.Draw({
790
+ position: "topleft",
791
+ draw: {
792
+ polygon: this.configurationMap?.createFigures.polygon,
793
+ circle: false, //this.configurationMap?.createFigures.circle,
794
+ rectangle: this.configurationMap?.createFigures.rectangle
795
+ ? {
796
+ shapeOptions: {
797
+ color: "blue",
798
+ },
799
+ }
800
+ : false,
801
+ marker: this.configurationMap?.createFigures.marker
802
+ ? {
803
+ icon: L.icon(iconDefaultMarket),
804
+ }
805
+ : false,
806
+ polyline: this.configurationMap?.createFigures.polyline
807
+ ? {
808
+ shapeOptions: {
809
+ color: "blue",
810
+ },
811
+ }
812
+ : false,
813
+ circlemarker: this.configurationMap?.createFigures.multipoint,
814
+ },
815
+ edit: contextEdit as any,
816
+ });
817
+ map.addControl(drawControl);
818
+
819
+ map.on("draw:created", (event: any) => {
820
+ const layer = event.layer;
821
+ featureGroup.addLayer(layer);
822
+ let geojson = layer.toGeoJSON();
823
+ // *** AGREGADO: Manejo especial para CircleMarker (convertir a MultiPoint) ***
824
+ if (event.layerType === "circlemarker") {
825
+ // Convertir CircleMarker a formato MultiPoint para consistencia
826
+ geojson = {
827
+ type: "Feature",
828
+ geometry: {
829
+ type: "MultiPoint",
830
+ coordinates: [geojson.geometry.coordinates],
831
+ },
832
+ properties: {
833
+ ...geojson.properties,
834
+ pointType: "multipoint",
835
+ style: {
836
+ color:
837
+ this.configurationMap?.createFigures?.multipoint?.color ||
838
+ "green",
839
+ fillColor:
840
+ this.configurationMap?.createFigures?.multipoint?.fillColor ||
841
+ "green",
842
+ fillOpacity:
843
+ this.configurationMap?.createFigures?.multipoint
844
+ ?.fillOpacity || 0.8,
845
+ radius:
846
+ this.configurationMap?.createFigures?.multipoint?.radius || 6,
847
+ weight:
848
+ this.configurationMap?.createFigures?.multipoint?.weight || 2,
849
+ },
850
+ },
851
+ };
852
+ }
853
+ if (this.getGeoJSON) this.getGeoJSON(geojson);
854
+ });
855
+
856
+ map.on("draw:edited", (event: any) => {
857
+ const layers = event.layers;
858
+ layers.eachLayer((layer: any) => {
859
+ const geojson = layer.toGeoJSON();
860
+ if (this.getGeoJSON) this.getGeoJSON(geojson);
861
+ });
862
+ });
863
+ },
864
+ getPointToLayer(feature: any, latlng: any) {
865
+ const geometryType = feature.geometry?.type;
866
+
867
+ // Point: icono según properties.tipo
868
+ if (geometryType === "Point") {
869
+ const tipo = feature.properties?.tipo;
870
+ const firstIcon = L.icon({
871
+ iconUrl: origenIconUrl,
872
+ iconSize: [50, 50],
873
+ iconAnchor: [16, 41],
874
+ });
875
+ const lastIcon = L.icon({
876
+ iconUrl: destinoIconUrl,
877
+ iconSize: [50, 50],
878
+ iconAnchor: [16, 41],
879
+ });
880
+ const pointerIcon = L.icon({
881
+ iconUrl: llegadaIconUrl,
882
+ iconSize: [50, 50],
883
+ iconAnchor: [16, 41],
884
+ });
885
+
886
+ let icon = pointerIcon;
887
+ if (tipo === 0) icon = firstIcon;
888
+ else if (tipo === 1) icon = lastIcon;
889
+
890
+ return L.marker(latlng, { icon });
891
+ }
892
+
893
+ // Otros tipos: icono por defecto
894
+ return L.marker(latlng);
895
+ },
896
+ async searchAddress(query: string) {
897
+ const address = await axios.get(
898
+ location.protocol + "//nominatim.openstreetmap.org/search?",
899
+ {
900
+ params: {
901
+ //query
902
+ q: query,
903
+ limit: 1,
904
+ format: "json",
905
+ },
906
+ }
907
+ );
908
+ if (address.data.length === 1) {
909
+ const lat = parseFloat(address.data[0].lat);
910
+ const lng = parseFloat(address.data[0].lon);
911
+ const coord = { lng, lat };
912
+ this.setCoordinates({ ...coord, moveMarker: true });
913
+ return address.data[0];
914
+ }
915
+ return address.data;
916
+ },
917
+ setCoordinates({
918
+ lat,
919
+ lng,
920
+ }: {
921
+ lat: number;
922
+ lng: number;
923
+ moveMarker?: boolean;
924
+ }): void {
925
+ if (this.mapRender) {
926
+ const newCenter = L.latLng(lat, lng);
927
+ this.mapRender.panTo(newCenter, { animate: true, duration: 0.5 });
928
+ }
929
+ },
930
+ getPopupContent(feature: any): string {
931
+ // Puedes personalizar el contenido del popup aquí
932
+ const props = feature.properties || {};
933
+ // Solo toma en cuenta descripcion y fechaHoraLlegada
934
+ const descripcion = props.descripcion || "";
935
+ const fechaHoraLlegada = props.fechaHoraLlegada || "";
936
+ // url de la foto
937
+ const fotoId = props.fotoId || "";
938
+
939
+ const formatIsoPreserve = (iso: string): string => {
940
+ const d = new Date(iso);
941
+ if (isNaN(d.getTime())) return iso;
942
+ const pad = (n: number) => n.toString().padStart(2, "0");
943
+ const day = pad(d.getUTCDate());
944
+ const month = pad(d.getUTCMonth() + 1);
945
+ const year = d.getUTCFullYear();
946
+ const hours = pad(d.getUTCHours());
947
+ const minutes = pad(d.getUTCMinutes());
948
+ const seconds = pad(d.getUTCSeconds());
949
+ return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}`;
950
+ };
951
+
952
+ // Formatear fecha a dd-MM-yyyy hh:mm:ss
953
+ let fechaFormateada = "";
954
+ if (fechaHoraLlegada) {
955
+ fechaFormateada = formatIsoPreserve(fechaHoraLlegada);
956
+ }
957
+
958
+ // Seleccionar el icono de referencia según el tipo (1 -> Destino, else -> LLegadaSalida)
959
+ const tipo = props.tipo;
960
+ const referenciaIcon =
961
+ tipo === 0
962
+ ? origenIconUrl
963
+ : tipo === 1
964
+ ? destinoIconUrl
965
+ : llegadaIconUrl;
966
+
967
+ // Construir HTML del popup con mejor UI para la imagen
968
+ // Contenedor externo controla el ancho; el popup tiene su propia clase para limitar altura
969
+ let html = `<div style="max-width:320px;">`;
970
+
971
+ if (fotoId) {
972
+ html += `\n<div style="display:flex;justify-content:center;margin-bottom:8px;">\n <img src="${fotoId}" style="height:200px;width:200px;max-width:320px;object-fit:cover;border-radius:10px;box-shadow:0 4px 10px rgba(0,0,0,0.12);"/>\n</div>`;
973
+ }
974
+
975
+ // Icono de referencia a la izquierda de la descripción
976
+ html += `<div style="display:flex;align-items:center;gap:8px;padding-top:6px;">`;
977
+ if (referenciaIcon) {
978
+ html += `<img src="${referenciaIcon}" style="width:30px;height:30px;flex:0 0 15px;"/>`;
979
+ }
980
+ if (fechaFormateada) {
981
+ html += `<div style="font-size:14px;"><b>${descripcion}:</b> ${fechaFormateada}</div>`;
982
+ } else {
983
+ html += `<div style="font-size:14px;">${descripcion}</div>`;
984
+ }
985
+ html += `</div>`;
986
+
987
+ html += `</div>`;
988
+ return html;
989
+ },
990
+ getPolygonTooltipContent(feature: any): string {
991
+ const props = feature.properties || {};
992
+ const descripcion = props.descripcion || props.name || "Sin descripción";
993
+
994
+ return `<div style="font-weight: bold; padding: 5px;">${descripcion}</div>`;
995
+ },
996
+ },
997
+ watch: {
998
+ coordinatesMap(newVal) {
999
+ this.renderCoordinates = Array.isArray(newVal)
1000
+ ? (newVal as number[])
1001
+ : [];
1002
+ if (
1003
+ this.mapRender &&
1004
+ this.markerRender &&
1005
+ this.renderCoordinates.length >= 2
1006
+ ) {
1007
+ const coords = this.renderCoordinates as number[];
1008
+ const newLatLng = L.latLng([coords[0], coords[1]]);
1009
+ this.mapRender.setView([coords[0], coords[1]], coords[2]);
1010
+ this.markerRender.setLatLng(newLatLng);
1011
+ }
1012
+ },
1013
+ dataPolygon(newVal) {
1014
+ this.renderGeojson = newVal;
1015
+ },
1016
+ },
1017
+ });
1018
+ </script>
1019
+
1020
+ <style>
1021
+ .leaflet-popup-content {
1022
+ width: 300px !important;
1023
+ margin: 0 !important;
1024
+ padding: 10px 15px !important;
1025
+ }
1026
+ .leaflet-popup.limit-popup .leaflet-popup-content-wrapper {
1027
+ /* Limit popup to viewport with some margin and force internal scroll.
1028
+ Remove extra right padding so the scrollbar sits at the popup edge. */
1029
+ overflow-y: auto !important;
1030
+ overflow-x: hidden !important;
1031
+ box-sizing: border-box;
1032
+ padding-right: 0 !important; /* let scrollbar be at the very edge */
1033
+ }
1034
+ .leaflet-popup.limit-popup .leaflet-popup-content {
1035
+ /* Reserve inner right padding so text doesn't hide behind the scrollbar */
1036
+ box-sizing: border-box;
1037
+ padding-right: 16px;
1038
+ }
1039
+ .leaflet-popup.limit-popup .leaflet-popup-close-button {
1040
+ /* Place close button above the content area (not creating layout padding),
1041
+ keep it visible above the scrollbar and give a compact circular style. */
1042
+ right: -8px !important;
1043
+ top: -8px !important;
1044
+ z-index: 10020 !important;
1045
+ background: #fff !important;
1046
+ border-radius: 14px !important;
1047
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12) !important;
1048
+ width: 28px !important;
1049
+ height: 28px !important;
1050
+ line-height: 28px !important;
1051
+ text-align: center !important;
1052
+ padding: 0 !important;
1053
+ }
1054
+ </style>