PageSourceSearch

https://abandoned.place/wp-content/plugins/open-user-map-pro/src/js/frontend-block-map.js?ver=1.4.52

js abandoned.place collected 2026-10-02 01:08:37 UTC 153,311 bytes, 4,682 lines download raw bytes

1/**
2 * @typedef {Object} Location
3 * @property {string} title - Location title
4 * @property {number} lat - Latitude
5 * @property {number} lng - Longitude
6 * @property {string} content - Location content/description
7 * @property {string} icon - URL to marker icon
8 * @property {string} post_id - WordPress post ID
9 * @property {string[]} types - Array of location types/categories
10 */
11
12/**
13 * @typedef {Object} MapBounds
14 * @property {number} lat - Center latitude
15 * @property {number} lng - Center longitude
16 * @property {number} zoom - Zoom level
17 */
18
19// Add shared bounds variable at the top level
20let sharedMapBounds = null;
21
22/**
23 * Utility Module - Contains helper functions used across other modules
24 */
25const OUMUtils = (function () {
26  function getParameterByName(name) {
27    name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
28    const regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
29        results = regex.exec(location.search);
30    return results === null
31      ? ""
32      : decodeURIComponent(results[1].replace(/\+/g, " "));
33  }
34
35  function latLngToBounds(lat, lng, zoom, width, height) {
36    // Convert all inputs to numbers first, handling string inputs
37    lat =
38      typeof lat === "string"
39        ? parseFloat(lat.replace(/['"]+/g, ""))
40        : parseFloat(lat);
41    lng =
42      typeof lng === "string"
43        ? parseFloat(lng.replace(/['"]+/g, ""))
44        : parseFloat(lng);
45    zoom =
46      typeof zoom === "string"
47        ? parseFloat(zoom.replace(/['"]+/g, ""))
48        : parseFloat(zoom);
49    width =
50      typeof width === "string"
51        ? parseFloat(width.replace(/['"]+/g, ""))
52        : parseFloat(width);
53    height =
54      typeof height === "string"
55        ? parseFloat(height.replace(/['"]+/g, ""))
56        : parseFloat(height);
57
58    // Validate coordinates
59    if (!validateCoordinates(lat, lng)) {
60      console.warn("Invalid coordinates for latLngToBounds, using defaults");
61      return [
62        [OUMConfig.defaults.map.lat, OUMConfig.defaults.map.lng],
63        [OUMConfig.defaults.map.lat, OUMConfig.defaults.map.lng],
64      ];
65    }
66
67    // Validate dimensions
68    if (isNaN(width) || width <= 0 || isNaN(height) || height <= 0) {
69      console.warn("Invalid dimensions for latLngToBounds");
70      width = 520; // Default width
71      height = 294; // Default height
72    }
73
74    // Validate and adjust zoom
75    if (isNaN(zoom)) {
76      zoom = OUMConfig.defaults.map.zoom;
77    } else {
78      // Ensure zoom is between 1 and 20
79      zoom = Math.max(1, Math.min(20, zoom));
80    }
81
82    // Use Leaflet's projection system to properly account for Mercator distortion
83    // This ensures equal visual margins regardless of latitude
84    const crs = L.CRS.EPSG3857; // Default Mercator projection used by Leaflet
85    
86    // Project the center position to pixel space at the specified zoom level
87    const centerLatLng = L.latLng(lat, lng);
88    const centerPoint = crs.latLngToPoint(centerLatLng, zoom);
89    
90    // Calculate pixel bounds: center ± half width/height
91    const halfWidth = width / 2;
92    const halfHeight = height / 2;
93    
94    // Calculate the four corners of the viewport in pixel space
95    const topLeftPoint = L.point(centerPoint.x - halfWidth, centerPoint.y - halfHeight);
96    const topRightPoint = L.point(centerPoint.x + halfWidth, centerPoint.y - halfHeight);
97    const bottomLeftPoint = L.point(centerPoint.x - halfWidth, centerPoint.y + halfHeight);
98    const bottomRightPoint = L.point(centerPoint.x + halfWidth, centerPoint.y + halfHeight);
99    
100    // Unproject the corners back to lat/lng (this properly accounts for Mercator distortion)
101    const topLeftLatLng = crs.pointToLatLng(topLeftPoint, zoom);
102    const topRightLatLng = crs.pointToLatLng(topRightPoint, zoom);
103    const bottomLeftLatLng = crs.pointToLatLng(bottomLeftPoint, zoom);
104    const bottomRightLatLng = crs.pointToLatLng(bottomRightPoint, zoom);
105    
106    // Find the min/max lat and lng from all four corners
107    // This ensures we capture the full bounds correctly
108    const minLat = Math.min(topLeftLatLng.lat, topRightLatLng.lat, bottomLeftLatLng.lat, bottomRightLatLng.lat);
109    const maxLat = Math.max(topLeftLatLng.lat, topRightLatLng.lat, bottomLeftLatLng.lat, bottomRightLatLng.lat);
110    const minLng = Math.min(topLeftLatLng.lng, topRightLatLng.lng, bottomLeftLatLng.lng, bottomRightLatLng.lng);
111    const maxLng = Math.max(topLeftLatLng.lng, topRightLatLng.lng, bottomLeftLatLng.lng, bottomRightLatLng.lng);
112
113    return [
114      [minLat, minLng],  // Southwest corner
115      [maxLat, maxLng],  // Northeast corner
116    ];
117  }
118
119  function customAutoSuggestText(text, val) {
120    return (
121      '<div><img src="' +
122      val.layer.options.icon.options.iconUrl +
123      '" />' +
124      val.layer.options.title +
125      "</div>"
126    );
127  }
128
129  function initGeosearchProvider() {
130    let provider;
131    switch (oum_geosearch_provider) {
132      case "osm":
133        provider = new GeoSearch.OpenStreetMapProvider();
134        break;
135      case "geoapify":
136        provider = new GeoSearch.GeoapifyProvider({
137          params: {
138            apiKey: oum_geosearch_provider_geoapify_key,
139          },
140        });
141        break;
142      case "here":
143        provider = new GeoSearch.HereProvider({
144          params: {
145            apiKey: oum_geosearch_provider_here_key,
146          },
147        });
148        break;
149      case "mapbox":
150        provider = new GeoSearch.MapBoxProvider({
151          params: {
152            access_token: oum_geosearch_provider_mapbox_key,
153          },
154        });
155        break;
156      default:
157        provider = new GeoSearch.OpenStreetMapProvider();
158        break;
159    }
160    return provider;
161  }
162
163  /**
164   * Validates and sanitizes coordinates
165   * @param {number|string} lat
166   * @param {number|string} lng
167   * @returns {boolean}
168   */
169  function validateCoordinates(lat, lng) {
170    const parsedLat = parseFloat(lat);
171    const parsedLng = parseFloat(lng);
172    return (
173      !isNaN(parsedLat) &&
174      !isNaN(parsedLng) &&
175      parsedLat >= -90 &&
176      parsedLat <= 90 &&
177      parsedLng >= -180 &&
178      parsedLng <= 180
179    );
180  }
181
182  /**
183   * Safely parses a JSON string
184   * @param {string} str
185   * @param {*} fallback
186   * @returns {*}
187   */
188  function safeJSONParse(str, fallback = null) {
189    try {
190      return JSON.parse(str);
191    } catch (e) {
192      return fallback;
193    }
194  }
195
196  /**
197   * Debounces a function
198   * @param {Function} func
199   * @param {number} wait
200   * @returns {Function}
201   */
202  function debounce(func, wait = 250) {
203    let timeout;
204    return function executedFunction(...args) {
205      const later = () => {
206        clearTimeout(timeout);
207        func(...args);
208      };
209      clearTimeout(timeout);
210      timeout = setTimeout(later, wait);
211    };
212  }
213
214  /**
215   * Simple sprintf implementation for string formatting
216   * @param {string} str - String with placeholders (%1$d, %2$s, etc)
217   * @param {...any} args - Values to replace placeholders
218   * @returns {string}
219   */
220  function sprintf(str, ...args) {
221    return str
222      .replace(/\\n/g, "\n")
223      .replace(/%(\d+)\$[sd]/g, (match, num) =>
224        typeof args[num - 1] !== "undefined" ? args[num - 1] : match
225      );
226  }
227
228  // Public interface
229  return {
230    getParameterByName,
231    latLngToBounds,
232    customAutoSuggestText,
233    initGeosearchProvider,
234    validateCoordinates,
235    safeJSONParse,
236    debounce,
237    sprintf
238  };
239})();
240
241/**
242 * Error Handler Module - Centralizes error management
243 */
244const OUMErrorHandler = (function () {
245  function showError(message, type = "error") {
246    console.error(`OUM Error: ${message}`);
247
248    // Show error in UI if error container exists
249    const errorContainer = document.getElementById("oum_add_location_error");
250    if (errorContainer) {
251      errorContainer.innerHTML = `${message}<br>`;
252      errorContainer.style.display = "block";
253    }
254  }
255
256  function handleAjaxError(error) {
257    showError(`Ajax request failed: ${error.message}`);
258  }
259
260  function validateCoordinates(lat, lng) {
261    const parsedLat = parseFloat(lat);
262    const parsedLng = parseFloat(lng);
263
264    if (isNaN(parsedLat) || isNaN(parsedLng)) {
265      showError("Invalid coordinates provided");
266      return false;
267    }
268
269    if (
270      parsedLat < -90 ||
271      parsedLat > 90 ||
272      parsedLng < -180 ||
273      parsedLng > 180
274    ) {
275      showError("Coordinates out of valid range");
276      return false;
277    }
278
279    return true;
280  }
281
282  return {
283    showError,
284    handleAjaxError,
285    validateCoordinates,
286  };
287})();
288
289/**
290 * Configuration Module - Centralizes all configuration settings
291 */
292const OUMConfig = (function () {
293  // Private variables
294  const defaults = {
295    map: {
296      lat: 26,
297      lng: 10,
298      zoom: 1,
299      bounds: L.latLngBounds(
300        L.latLng(-85, -200), // Southwest corner (adjusted to prevent grey areas)
301        L.latLng(85, 200)    // Northeast corner (adjusted to prevent grey areas)
302      ),
303    },
304    media: {
305      maxFiles: 5,
306      validImageExtensions: ["jpeg", "jpg", "png", "webp"],
307      maxImageSize: (window.oum_max_image_filesize || 10) * 1048576, // Convert MB to bytes
308    },
309    search: {
310      zoomLevel: window.oum_searchmarkers_zoom || 8,
311      addressLabel: window.oum_searchaddress_label || "Search for address",
312      markersLabel: window.oum_searchmarkers_label || "Find marker",
313    },
314  };
315
316  function getMapStyle() {
317    return window.mapStyle || "OpenFreeMap.bright";
318  }
319
320  function getTileProviderKey() {
321    return window.oum_tile_provider_mapbox_key || "";
322  }
323
324  function getCartoTileProviderKey() {
325    return window.oum_tile_provider_carto_key || "";
326  }
327
328  function getGeosearchProvider() {
329    let provider;
330    switch (oum_geosearch_provider) {
331      case "geoapify":
332        provider = new GeoSearch.GeoapifyProvider({
333          params: {
334            apiKey: oum_geosearch_provider_geoapify_key,
335          },
336        });
337        break;
338      case "here":
339        provider = new GeoSearch.HereProvider({
340          params: {
341            apiKey: oum_geosearch_provider_here_key,
342          },
343        });
344        break;
345      case "mapbox":
346        provider = new GeoSearch.MapBoxProvider({
347          params: {
348            access_token: oum_geosearch_provider_mapbox_key,
349          },
350        });
351        break;
352      default:
353        provider = new GeoSearch.OpenStreetMapProvider();
354    }
355    return provider;
356  }
357
358  return {
359    defaults,
360    getMapStyle,
361    getTileProviderKey,
362    getCartoTileProviderKey,
363    getGeosearchProvider,
364  };
365})();
366
367/**
368 * Map Core Module - Handles the main map initialization and configuration
369 */
370const OUMMap = (function () {
371  // Private variables
372  let map = null;
373  let world_bounds = null;
374  let startPosition = {
375    lat:
376      typeof start_lat !== "undefined"
377        ? Number(start_lat)
378        : OUMConfig.defaults.map.lat,
379    lng:
380      typeof start_lng !== "undefined"
381        ? Number(start_lng)
382        : OUMConfig.defaults.map.lng,
383    zoom:
384      typeof start_zoom !== "undefined"
385        ? Number(start_zoom)
386        : OUMConfig.defaults.map.zoom,
387  };
388
389  // Private functions
390  function initializeStartPosition() {
391    // Validate coordinates
392    if (!OUMUtils.validateCoordinates(startPosition.lat, startPosition.lng)) {
393      console.warn("Invalid coordinates, using defaults");
394      startPosition.lat = OUMConfig.defaults.map.lat;
395      startPosition.lng = OUMConfig.defaults.map.lng;
396    }
397
398    // Validate zoom level (between 1 and 20)
399    if (
400      isNaN(startPosition.zoom) ||
401      startPosition.zoom < 1 ||
402      startPosition.zoom > 20
403    ) {
404      console.warn("Invalid zoom level, using default");
405      startPosition.zoom = OUMConfig.defaults.map.zoom;
406    }
407  }
408
409  // Leaflet.markercluster expects discrete zoom steps to keep cluster membership stable.
410  function isClusterZoomLocked() {
411    return !!oum_enable_cluster;
412  }
413
414  // Normalize programmatic zoom values so clustered maps never start on fractional zooms.
415  function normalizeMainMapZoom(zoom) {
416    const parsedZoom = Number(zoom);
417    if (isNaN(parsedZoom)) {
418      return zoom;
419    }
420
421    return isClusterZoomLocked() ? Math.round(parsedZoom) : parsedZoom;
422  }
423
424  // Route main-map setView calls through the same zoom normalization logic.
425  function setMainMapView(center, zoom, options = {}) {
426    map.setView(center, normalizeMainMapZoom(zoom), options);
427  }
428
429  // Keep animated moves aligned with the integer zoom used by clustered maps.
430  function flyMainMapTo(center, zoom, options = {}) {
431    map.flyTo(center, normalizeMainMapZoom(zoom), options);
432  }
433
434  // Clamp flyToBounds to an integer max zoom so cluster refreshes stay deterministic.
435  function flyMainMapToBounds(bounds, options = {}) {
436    const flyOptions = { ...options };
437
438    if (isClusterZoomLocked()) {
439      flyOptions.maxZoom = normalizeMainMapZoom(map.getBoundsZoom(bounds, false));
440    }
441
442    map.flyToBounds(bounds, flyOptions);
443  }
444
445  function setupMapBounds() {
446    // Set bounds if fixed map bounds is enabled
447    if (oum_enable_fixed_map_bounds) {
448      // Calculate bounds using the updated latLngToBounds method
449      // This uses Leaflet's projection system to properly account for Mercator distortion
450      // Settings map dimensions (used when setting initial view in admin)
451      const boundsArray = OUMUtils.latLngToBounds(
452        startPosition.lat,
453        startPosition.lng,
454        startPosition.zoom,
455        520, // Width of settings map
456        294 // Height of settings map
457      );
458
459      // Convert the bounds array to a Leaflet LatLngBounds object
460      world_bounds = L.latLngBounds(
461        L.latLng(boundsArray[0][0], boundsArray[0][1]),
462        L.latLng(boundsArray[1][0], boundsArray[1][1])
463      );
464
465      // Store bounds globally for form map to use
466      sharedMapBounds = world_bounds;
467    } else {
468      // Use default world bounds when fixed map bounds is disabled
469      world_bounds = OUMConfig.defaults.map.bounds;
470      sharedMapBounds = world_bounds;
471    }
472
473    // Set the minimum zoom level to prevent zooming out too far
474    const maxVisibleBounds = map.getBoundsZoom(world_bounds);
475    map.setMinZoom(maxVisibleBounds);
476
477    // Set max bounds without padding
478    map.setMaxBounds(world_bounds);
479
480    let isAdjusting = false;
481
482    // Handle map movement without recursion
483    map.on("moveend", function () {
484      if (isAdjusting) return;
485      isAdjusting = true;
486
487      const zoom = map.getZoom();
488
489      // Only enforce bounds if we're zoomed in beyond the minimum zoom
490      if (zoom > maxVisibleBounds) {
491        const currentBounds = map.getBounds();
492        const currentCenter = map.getCenter();
493
494        let needsAdjustment = false;
495        let newLat = currentCenter.lat;
496        let newLng = currentCenter.lng;
497
498        // Calculate current viewport dimensions
499        const viewportHeight =
500          currentBounds.getNorth() - currentBounds.getSouth();
501        const viewportWidth =
502          currentBounds.getEast() - currentBounds.getWest();
503
504        // Check and adjust latitude (north/south)
505        if (currentBounds.getNorth() > world_bounds.getNorth()) {
506          newLat = world_bounds.getNorth() - viewportHeight / 2;
507          needsAdjustment = true;
508        } else if (currentBounds.getSouth() < world_bounds.getSouth()) {
509          newLat = world_bounds.getSouth() + viewportHeight / 2;
510          needsAdjustment = true;
511        }
512
513        // Check and adjust longitude (east/west)
514        if (currentBounds.getEast() > world_bounds.getEast()) {
515          newLng = world_bounds.getEast() - viewportWidth / 2;
516          needsAdjustment = true;
517        } else if (currentBounds.getWest() < world_bounds.getWest()) {
518          newLng = world_bounds.getWest() + viewportWidth / 2;
519          needsAdjustment = true;
520        }
521
522        if (needsAdjustment) {
523          setMainMapView([newLat, newLng], zoom, { animate: false });
524        }
525      }
526
527      isAdjusting = false;
528    });
529  }
530
531  function setupTileLayer(mapStyle) {
532    return window.OUMMapStyles.addBaseLayer(map, mapStyle, {
533      mapboxKey: OUMConfig.getTileProviderKey(),
534      cartoKey: OUMConfig.getCartoTileProviderKey(),
535      setupCustomImageLayer: setupCustomImageLayer,
536      customImageHideTiles: window.oum_custom_image_hide_tiles,
537      customImageBackgroundColor: window.oum_custom_image_background_color,
538    });
539  }
540
541  function setupCustomImageLayer() {
542    // Check if we have an image URL and bounds
543    if (typeof window.oum_custom_image_url !== 'undefined' && window.oum_custom_image_url && 
544        typeof window.oum_custom_image_bounds !== 'undefined' && window.oum_custom_image_bounds) {
545      
546      // Check if the uploaded file is an SVG
547      const isSVG = window.oum_custom_image_url.toLowerCase().includes('.svg');
548      
549      if (isSVG) {
550        // Handle SVG file - fetch and render as DOM elements
551        setupSVGFromFile();
552      } else {
553        // Handle regular image file
554        setupImageOverlay();
555      }
556    } else {
557    }
558  }
559
560  // Helper function to setup SVG from uploaded file
561  function setupSVGFromFile() {
562    try {
563      // Get bounds data (now properly handled as object)
564      const bounds = window.oum_custom_image_bounds;
565
566      // Validate bounds
567      if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' ||
568          typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' ||
569          bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') {
570        console.warn('Open User Map: Invalid or empty bounds data, skipping SVG file layer');
571        return;
572      }
573
574
575      // Fetch the SVG file and render it
576      fetch(window.oum_custom_image_url)
577        .then(response => response.text())
578        .then(svgText => {
579          // Create SVG element from the fetched content
580          const svgElement = createSVGElement(svgText);
581          if (!svgElement) {
582            console.warn('Open User Map: Cannot create SVG layer from file - invalid SVG element');
583            return;
584          }
585
586          // Create a custom SVG layer
587          const svgLayer = L.svgOverlay(svgElement, [
588            [bounds.south, bounds.west], // Southwest corner
589            [bounds.north, bounds.east]  // Northeast corner
590          ], {
591            opacity: 1.0,
592            interactive: true
593          });
594
595          svgLayer.addTo(map);
596
597
598          // Store reference for potential removal
599          window.oumCustomSVGLayer = svgLayer;
600
601          console.log('Open User Map: Custom SVG file layer added successfully');
602        })
603        .catch(error => {
604          console.warn('Open User Map: Error fetching SVG file:', error);
605        });
606
607    } catch (error) {
608      console.warn('Open User Map: Error setting up custom SVG file layer:', error);
609    }
610  }
611
612  // Helper function to setup regular image overlay
613  function setupImageOverlay() {
614    try {
615      // Get bounds data (now properly handled as object)
616      const bounds = window.oum_custom_image_bounds;
617
618      // Validate bounds
619      if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' ||
620          typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' ||
621          bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') {
622        console.warn('Open User Map: Invalid or empty bounds data, skipping image layer');
623        return;
624      }
625
626
627      // Create image overlay
628      const imageOverlay = L.imageOverlay(window.oum_custom_image_url, [
629        [bounds.south, bounds.west], // Southwest corner
630        [bounds.north, bounds.east]  // Northeast corner
631      ], {
632        opacity: 1.0,
633        interactive: true
634      });
635
636      imageOverlay.addTo(map);
637
638
639      // Store reference for potential removal
640      window.oumCustomImageLayer = imageOverlay;
641
642      console.log('Open User Map: Custom image layer added successfully');
643
644    } catch (error) {
645      console.warn('Open User Map: Error setting up custom image layer:', error);
646    }
647  }
648
649  // Helper function to create SVG element from text
650  function createSVGElement(svgText) {
651    // Create a temporary div to parse the SVG
652    const tempDiv = document.createElement('div');
653    tempDiv.innerHTML = svgText;
654    const svgElement = tempDiv.querySelector('svg');
655    
656    if (!svgElement) {
657      console.warn('Open User Map: No valid SVG element found in SVG text');
658      return null;
659    }
660    
661    // Preserve the original viewBox if it exists
662    // If missing, try to create it from width/height attributes
663    if (!svgElement.getAttribute('viewBox')) {
664      const width = svgElement.getAttribute('width');
665      const height = svgElement.getAttribute('height');
666      if (width && height) {
667        // Remove units if present (e.g., "1580px" -> "1580")
668        const widthNum = parseFloat(width);
669        const heightNum = parseFloat(height);
670        if (!isNaN(widthNum) && !isNaN(heightNum)) {
671          svgElement.setAttribute('viewBox', `0 0 ${widthNum} ${heightNum}`);
672        } else {
673          svgElement.setAttribute('viewBox', '0 0 1000 1200');
674        }
675      } else {
676        svgElement.setAttribute('viewBox', '0 0 1000 1200');
677      }
678    }
679    
680    // Ensure the SVG has proper styling for overlay
681    svgElement.style.width = '100%';
682    svgElement.style.height = '100%';
683    svgElement.style.display = 'block';
684    
685    // Ensure the SVG fills the entire bounds area to prevent cropping
686    svgElement.setAttribute('preserveAspectRatio', 'none');
687    
688    console.log('Open User Map: SVG element created successfully:', svgElement);
689    
690    return svgElement;
691  }
692
693  function setupControls() {
694    // Search markers control as button
695    if (oum_enable_searchmarkers_button) {
696      L.control
697        .search({
698          textPlaceholder: oum_searchmarkers_label,
699          layer: window.oumMarkersLayer,
700          propertyName: "content",
701          initial: false,
702          buildTip: OUMUtils.customAutoSuggestText,
703          autoType: false,
704          firstTipSubmit: true,
705          autoCollapse: true,
706          zoom: oum_searchmarkers_zoom,
707        })
708        .addTo(map);
709    }
710
711    // Search markers control as searchbar
712    if (oum_enable_searchbar && oum_searchbar_type === "markers") {
713      const markerSearchBar = L.control.search({
714        textPlaceholder: oum_searchmarkers_label,
715        layer: window.oumMarkersLayer,
716        propertyName: "content",
717        initial: false,
718        buildTip: OUMUtils.customAutoSuggestText,
719        autoType: false,
720        firstTipSubmit: true,
721        autoCollapse: false,
722        collapsed: false,
723        zoom: oum_searchmarkers_zoom,
724        container: 'oum_search_marker'
725      });
726      markerSearchBar.addTo(map);
727      // Do not leave the internal search "content" string in the input after a result is chosen
728      markerSearchBar.on("search:locationfound", function () {
729        this._input.value = "";
730        if (this._cancel) {
731          this._cancel.style.display = "none";
732        }
733      });
734    }
735
736    // Add searchbar for address search
737    if (oum_enable_searchbar && oum_searchbar_type === "address") {
738      const searchBar = new GeoSearch.GeoSearchControl({
739        style: "bar",
740        showMarker: false,
741        provider: OUMUtils.initGeosearchProvider(),
742        searchLabel: oum_searchaddress_label,
743        updateMap: false,
744      });
745      map.addControl(searchBar);
746      relocateAddressSearchBar();
747    }
748
749    // Add button for address search
750    if (oum_enable_searchaddress_button) {
751      const searchButton = new GeoSearch.GeoSearchControl({
752        style: "button",
753        showMarker: false,
754        provider: OUMUtils.initGeosearchProvider(),
755        searchLabel: oum_searchaddress_label,
756        updateMap: false,
757      });
758      map.addControl(searchButton);
759    }
760
761    // Current location control
762    if (oum_enable_currentlocation) {
763      window.map_locate_process = L.control
764        .locate({
765          flyTo: true,
766          showPopup: false,
767        })
768        .addTo(map);
769    }
770  }
771
772  // Move the Address searchbar into Advanced Filter when that section is present.
773  function relocateAddressSearchBar() {
774    const target = document.getElementById("oum_search_address");
775    if (!target || !map) {
776      return;
777    }
778
779    const mapContainer = map.getContainer();
780    const bars = mapContainer.querySelectorAll(".leaflet-control-geosearch.leaflet-geosearch-bar");
781    let bar = null;
782    bars.forEach(function (el) {
783      if (!el.closest(".leaflet-top") && !el.closest(".leaflet-bottom")) {
784        bar = el;
785      }
786    });
787    if (!bar && bars.length) {
788      bar = bars[0];
789    }
790    if (bar) {
791      target.appendChild(bar);
792    }
793  }
794
795  function setupMapEvents() {
796    // Event: pan or zoom Map
797    map.on("moveend", function (ev) {
798      startPosition.lat = map.getCenter().lat;
799      startPosition.lng = map.getCenter().lng;
800      startPosition.zoom = map.getZoom();
801    });
802
803    // Event: Enter/Exit Fullscreen
804    const addLocationPopup = document.querySelector("#add-location-overlay");
805    const originalContainer = addLocationPopup?.parentElement;
806    const fullscreenContainer = document.querySelector(
807      ".open-user-map .map-wrap"
808    );
809
810    if (addLocationPopup) {
811      map.on("enterFullscreen", function () {
812        fullscreenContainer.appendChild(addLocationPopup);
813      });
814
815      map.on("exitFullscreen", function () {
816        originalContainer.appendChild(addLocationPopup);
817      });
818    }
819
820    // Event: geosearch success
821    map.on("geosearch/showlocation", function(e) {
822      let coords = e.marker._latlng;
823      
824      // Check against fixed map bounds if enabled, otherwise allow any location
825      let isInBounds = true; // Default to true when fixed bounds is disabled
826      if (oum_enable_fixed_map_bounds === 'on' && sharedMapBounds) {
827        // Check if location is within the fixed map bounds (not just current viewport)
828        isInBounds = sharedMapBounds.contains(coords);
829      }
830      
831      if (!isInBounds && oum_enable_fixed_map_bounds === 'on') {
832        console.log("This search result is out of reach.");
833        const searchBar = document.querySelector(`#${map_el} .leaflet-geosearch-bar form`)
834          || document.querySelector("#oum_search_address form")
835          || document.querySelector(".oum-searchbar-filter .leaflet-geosearch-bar form");
836        if (searchBar) {
837          searchBar.style.boxShadow = "0 0 10px rgb(255, 111, 105)";
838          setTimeout(function () {
839            searchBar.style.boxShadow = "0 1px 5px rgba(255, 255, 255, 0.65)";
840          }, 2000);
841        }
842      } else {
843        // Handle valid search result
844        if (e.location.bounds) {
845          flyMainMapToBounds(e.location.bounds);
846        } else if (e.location.raw && e.location.raw.mapView) {
847          flyMainMapToBounds([
848            [e.location.raw.mapView.south, e.location.raw.mapView.west],
849            [e.location.raw.mapView.north, e.location.raw.mapView.east]
850          ]);
851        } else {
852          flyMainMapTo([e.location.y, e.location.x], 17);
853        }
854      }
855    });
856  }
857
858  function setupRegionEvents() {
859    document
860      .querySelectorAll(".open-user-map .change_region")
861      .forEach(function (btn) {
862        btn.onclick = function (event) {
863          let el = event.currentTarget;
864          let region_lat = el.getAttribute("data-lat");
865          let region_lng = el.getAttribute("data-lng");
866          let region_zoom = el.getAttribute("data-zoom");
867
868          // Get frontend map container size
869          const mapContainer = map.getContainer();
870          const frontendMapWidth = mapContainer.offsetWidth || 520;
871          const frontendMapHeight = mapContainer.offsetHeight || 294;
872          const settingsMapWidth = 520;
873          const settingsMapHeight = 294;
874          
875          // Calculate bounds based on settings map dimensions
876          let region_bounds = OUMUtils.latLngToBounds(
877            parseFloat(region_lat),
878            parseFloat(region_lng),
879            parseFloat(region_zoom),
880            settingsMapWidth,
881            settingsMapHeight
882          );
883          
884          // Convert to Leaflet bounds
885          const regionBounds = L.latLngBounds(
886            L.latLng(region_bounds[0][0], region_bounds[0][1]),
887            L.latLng(region_bounds[1][0], region_bounds[1][1])
888          );
889          
890          // Calculate aspect ratios
891          const settingsAspectRatio = settingsMapWidth / settingsMapHeight;
892          const frontendAspectRatio = frontendMapWidth / frontendMapHeight;
893          const aspectRatioDifference = Math.abs(settingsAspectRatio - frontendAspectRatio);
894          
895          let region_bounds_zoom;
896          
897          // If aspect ratios match, use ratio-based calculation
898          if (aspectRatioDifference < 0.01) {
899            const widthRatio = frontendMapWidth / settingsMapWidth;
900            const zoomAdjustment = Math.log2(widthRatio);
901            region_bounds_zoom = parseFloat(region_zoom) + zoomAdjustment;
902          } else {
903            // Different aspect ratios - use getBoundsZoom
904            region_bounds_zoom = map.getBoundsZoom(regionBounds, false);
905          }
906
907          // Center Map
908          flyMainMapTo([region_lat, region_lng], region_bounds_zoom);
909
910          // Update active state
911          document
912            .querySelectorAll(".open-user-map .change_region")
913            .forEach(function (el) {
914              el.classList.remove("active");
915            });
916          el.classList.add("active");
917        };
918
919        // Event: Change Region on ?region=Europe
920        const REGION_ID = OUMUtils.getParameterByName("region");
921        if (btn.textContent == REGION_ID) {
922          btn.click();
923        }
924      });
925  }
926
927  // Public interface
928  return {
929    init: function (mapEl) {
930      try {
931        initializeStartPosition();
932
933        // Initialize map
934        map = L.map(mapEl, {
935          gestureHandling: oum_enable_scrollwheel_zoom_map ? false : true,
936          minZoom: 1, // Set default minimum zoom
937          zoomSnap: isClusterZoomLocked() ? 1 : 0.01, // MarkerCluster needs integer zoom steps.
938          zoomDelta: 1, // Zoom step size for controls
939          attributionControl: true,
940          fullscreenControl: oum_enable_fullscreen,
941          fullscreenControlOptions: {
942            position: "topleft",
943            fullscreenElement: document.querySelector(
944              ".open-user-map .map-wrap"
945            ),
946          },
947        });
948
949        map.attributionControl.setPrefix(false);
950
951        // First set up the tile layer
952        setupTileLayer(OUMConfig.getMapStyle());
953
954        // Invalidate size to ensure accurate measurements
955        map.invalidateSize();
956        
957        // Get the actual frontend map container size
958        const mapContainer = map.getContainer();
959        const frontendMapWidth = mapContainer.offsetWidth || 520;
960        const frontendMapHeight = mapContainer.offsetHeight || 294;
961        
962        // Settings map dimensions (fixed)
963        const settingsMapWidth = 520;
964        const settingsMapHeight = 294;
965        
966        // Calculate aspect ratios to determine if they match
967        const settingsAspectRatio = settingsMapWidth / settingsMapHeight;
968        const frontendAspectRatio = frontendMapWidth / frontendMapHeight;
969        const aspectRatioDifference = Math.abs(settingsAspectRatio - frontendAspectRatio);
970        
971        // Calculate initial bounds based on settings map dimensions (520)
972        // This gives us the geographic area that should be visible
973        const settingsBoundsArray = OUMUtils.latLngToBounds(
974          startPosition.lat,
975          startPosition.lng,
976          startPosition.zoom,
977          settingsMapWidth,
978          settingsMapHeight
979        );
980
981        // Convert to Leaflet bounds
982        const settingsBounds = L.latLngBounds(
983          L.latLng(settingsBoundsArray[0][0], settingsBoundsArray[0][1]),
984          L.latLng(settingsBoundsArray[1][0], settingsBoundsArray[1][1])
985        );
986
987        let targetZoom;
988        
989        // If aspect ratios are very similar (within 1%), use ratio-based zoom calculation
990        // This preserves the exact same geographic area when aspect ratios match
991        if (aspectRatioDifference < 0.01) {
992          // Calculate zoom adjustment based on size ratio
993          // If frontend map is larger, we need higher zoom to show same area
994          // Formula: zoom adjustment = log2(frontendWidth / settingsWidth)
995          const widthRatio = frontendMapWidth / settingsMapWidth;
996          const zoomAdjustment = Math.log2(widthRatio);
997          targetZoom = startPosition.zoom + zoomAdjustment;
998        } else {
999          // Aspect ratios differ - use getBoundsZoom to account for different aspect ratios
1000          // This will fit the bounds but may show slightly different area due to aspect ratio
1001          targetZoom = map.getBoundsZoom(settingsBounds, false);
1002        }
1003        targetZoom = normalizeMainMapZoom(targetZoom);
1004        
1005        // Use setView with the calculated zoom level to show the same geographic area
1006        // This ensures the same geographic bounds are visible regardless of frontend map size
1007        setMainMapView([startPosition.lat, startPosition.lng], targetZoom, {
1008          animate: false
1009        });
1010
1011        // Set up other components
1012        setupMapBounds();
1013        setupMapEvents();
1014        setupRegionEvents();
1015
1016        window.oumMap = map;
1017
1018        // Listen for markers initialized event
1019        document.addEventListener('oum:markers_initialized', function(e) {
1020          if (e.detail.mapId === mapEl) {
1021            // Set up controls after markers are ready
1022            setupControls();
1023          }
1024        }, { once: true });  // Use once: true to ensure it only runs once
1025
1026        // Dispatch map initialized event when everything is ready
1027        document.dispatchEvent(new CustomEvent('oum:map_initialized', {
1028          detail: {
1029            mapId: mapEl,
1030            map: oumMap
1031          }
1032        }));
1033
1034        return map;
1035      } catch (error) {
1036        OUMErrorHandler.showError("Error initializing map: " + error.message);
1037        throw error;
1038      }
1039    },
1040    getMap: function () {
1041      return map;
1042    },
1043    getStartPosition: function () {
1044      return startPosition;
1045    },
1046  };
1047})();
1048
1049/**
1050 * Markers Module - Handles all marker-related functionality
1051 */
1052const OUMMarkers = (function () {
1053  // Private variables
1054  let markersLayer = null;
1055  let vectorLayer = null;
1056  let allMarkers = [];
1057  let map = null;
1058  let locationsById = {};
1059  let searchtextFilterValue = "";
1060  let categoriesFilterSelection = null;
1061  let customfieldsFilterSelection = null;
1062  let visibleMarkersCount = 0;
1063  const categoryFilterSelector = '.open-user-map .oum-marker-categories-filter [name="type"]';
1064
1065  // Private functions
1066  function initializeMarkersLayer() {
1067    markersLayer = !oum_enable_cluster
1068      ? L.layerGroup({ chunkedLoading: true })
1069      : L.markerClusterGroup({
1070          showCoverageOnHover: false,
1071          removeOutsideVisibleBounds: true,
1072          maxClusterRadius: 50,
1073          chunkedLoading: true,
1074        });
1075    vectorLayer = L.layerGroup();
1076  }
1077
1078  function oumBubbleLoadingHtml() {
1079    const label =
1080      typeof oum_custom_strings !== "undefined" && oum_custom_strings.bubble_loading
1081        ? String(oum_custom_strings.bubble_loading)
1082            .replace(/\\/g, "\\\\")
1083            .replace(/"/g, "&quot;")
1084        : "Loading";
1085    return (
1086      '<div class="oum-bubble-loading" role="status" aria-live="polite" aria-busy="true" aria-label="' +
1087      label +
1088      '"><span class="oum-bubble-loading-spinner" aria-hidden="true"></span></div>'
1089    );
1090  }
1091
1092  function oumBubbleErrorHtml() {
1093    const msg =
1094      typeof oum_custom_strings !== "undefined" && oum_custom_strings.bubble_load_error
1095        ? oum_custom_strings.bubble_load_error
1096        : "Unable to load location details.";
1097    return '<div class="oum-bubble-error" role="alert">' + msg + "</div>";
1098  }
1099
1100  /**
1101   * After popup HTML is set: fullscreen mirror, edit button, votes/stars, opening hours.
1102   */
1103  function finalizeMarkerBubbleUI(popup) {
1104    const sourceLayer = popup && popup._source ? popup._source : null;
1105    const el = document.querySelector(".open-user-map #location-fullscreen-container");
1106    const locationContentWrap = el ? el.querySelector(".location-content-wrap") : null;
1107    if (locationContentWrap) {
1108      locationContentWrap.innerHTML = popup.getContent();
1109      injectVectorMeasurement(locationContentWrap, sourceLayer);
1110    }
1111    if (el) {
1112      el.classList.add("visible");
1113      document.querySelector("body").classList.add("oum-location-opened");
1114    }
1115
1116    if (locationContentWrap) {
1117      checkEditPermissionAndInjectButton(locationContentWrap);
1118    }
1119
1120    const popupContent = popup.getElement();
1121    if (popupContent) {
1122      injectVectorMeasurement(popupContent, sourceLayer);
1123      checkEditPermissionAndInjectButton(popupContent);
1124    }
1125
1126    if (window.OUMVoteHandler && window.OUMVoteHandler.updateVoteButtonStates) {
1127      window.OUMVoteHandler.updateVoteButtonStates();
1128    }
1129
1130    if (window.OUMVoteHandler && window.OUMVoteHandler.initializeVoteCountsFromData) {
1131      const voteButtons = el ? el.querySelectorAll(".oum_vote_button") : [];
1132      if (voteButtons.length > 0) {
1133        window.OUMVoteHandler.initializeVoteCountsFromData();
1134      }
1135    }
1136
1137    if (window.OUMVoteHandler && window.OUMVoteHandler.initializeStarRatings) {
1138      const starRatings = el ? el.querySelectorAll(".oum_star_rating") : [];
1139      if (starRatings.length > 0) {
1140        window.OUMVoteHandler.initializeStarRatings();
1141      }
1142    }
1143
1144    if (locationContentWrap) {
1145      OUMOpeningHours.setupToggle(locationContentWrap);
1146    }
1147    if (popupContent) {
1148      OUMOpeningHours.setupToggle(popupContent);
1149    }
1150  }
1151
1152  function injectVectorMeasurement(container, layer) {
1153    if (!container || !layer || !layer.options || !layer.options.oumMeasurementLabel) {
1154      return;
1155    }
1156
1157    const title = container.querySelector(".oum_location_title");
1158    if (!title || title.querySelector(".oum_location_measurement")) {
1159      return;
1160    }
1161
1162    const measurement = document.createElement("span");
1163    measurement.className = "oum_location_measurement";
1164    measurement.textContent = layer.options.oumMeasurementLabel;
1165    title.appendChild(measurement);
1166  }
1167
1168  function getPolylineDistanceMeters(latLngs) {
1169    return latLngs.reduce((distance, latLng, index) => {
1170      if (index === 0) {
1171        return distance;
1172      }
1173
1174      return distance + latLngs[index - 1].distanceTo(latLng);
1175    }, 0);
1176  }
1177
1178  function getPolygonAreaSquareMeters(latLngs) {
1179    if (L.GeometryUtil && L.GeometryUtil.geodesicArea) {
1180      return Math.abs(L.GeometryUtil.geodesicArea(latLngs));
1181    }
1182
1183    return 0;
1184  }
1185
1186  function formatMeters(value) {
1187    if (!isFinite(value) || value <= 0) {
1188      return "";
1189    }
1190
1191    return value >= 1000
1192      ? `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)} km`
1193      : `${Math.round(value)} m`;
1194  }
1195
1196  function formatSquareMeters(value) {
1197    if (!isFinite(value) || value <= 0) {
1198      return "";
1199    }
1200
1201    return `${Math.round(value).toLocaleString()} m²`;
1202  }
1203
1204  function createMarker(location) {
1205    // location.content is plain search text from PHP; strip tags keeps backward compatibility if HTML slips in.
1206    const searchBody = (location.content || "")
1207      .toString()
1208      .replace(/(<([^>]+)>)/gi, " ")
1209      .replace(/\s\s+/g, " ");
1210    const contentText = (location.title + " | " + searchBody).toLowerCase();
1211
1212    let marker = L.marker([location.lat, location.lng], {
1213      title: location.title,
1214      post_id: location.post_id,
1215      content: contentText,
1216      zoom: location.zoom,
1217      icon: L.icon({
1218        iconUrl: location.icon,
1219        iconSize: [26, 41],
1220        iconAnchor: [13, 41],
1221        popupAnchor: [0, -25],
1222        shadowUrl: marker_shadow_url,
1223        shadowSize: [41, 41],
1224        shadowAnchor: [13, 41],
1225      }),
1226      types: location.types || [],
1227    });
1228
1229    // Lazy-load full bubble HTML on popup open; placeholder until AJAX returns.
1230    if (typeof oum_hide_location_popup === "undefined" || !oum_hide_location_popup) {
1231      const popup = L.responsivePopup().setContent(oumBubbleLoadingHtml());
1232      marker.bindPopup(popup);
1233    }
1234
1235    return marker;
1236  }
1237
1238  function createPolylineLocation(location) {
1239    const searchBody = (location.content || "")
1240      .toString()
1241      .replace(/(<([^>]+)>)/gi, " ")
1242      .replace(/\s\s+/g, " ");
1243    const contentText = (location.title + " | " + searchBody).toLowerCase();
1244    const geometry = location.geometry || {};
1245    const coordinates = geometry.type === "Polygon" && Array.isArray(geometry.coordinates) && Array.isArray(geometry.coordinates[0])
1246      ? geometry.coordinates[0]
1247      : (Array.isArray(geometry.coordinates) ? geometry.coordinates : []);
1248    const latLngs = coordinates
1249      .filter((coordinate) => Array.isArray(coordinate) && coordinate.length >= 2)
1250      .map((coordinate) => L.latLng(Number(coordinate[1]), Number(coordinate[0])))
1251      .filter((latLng) => isFinite(latLng.lat) && isFinite(latLng.lng));
1252
1253    if (latLngs.length < 2) {
1254      return createMarker(location);
1255    }
1256
1257    const first = latLngs[0];
1258    const last = latLngs[latLngs.length - 1];
1259    const isPolygon = geometry.type === "Polygon";
1260    const isClosed = isPolygon || (latLngs.length >= 4 && first.equals(last));
1261    const color = location.category_color || "#e82c71";
1262    const measurementLabel = location.show_measurement
1263      ? (isClosed ? formatSquareMeters(getPolygonAreaSquareMeters(latLngs)) : formatMeters(getPolylineDistanceMeters(latLngs)))
1264      : "";
1265    const layerOptions = {
1266      title: location.title,
1267      post_id: location.post_id,
1268      content: contentText,
1269      zoom: location.zoom,
1270      types: location.types || [],
1271      oumMeasurementLabel: measurementLabel,
1272      color: color,
1273      weight: 4,
1274      opacity: 0.9,
1275    };
1276
1277    const layer = isClosed
1278      ? L.polygon(latLngs, {
1279          ...layerOptions,
1280          fillColor: color,
1281          fillOpacity: 0.25,
1282        })
1283      : L.polyline(latLngs, layerOptions);
1284
1285    if (typeof oum_hide_location_popup === "undefined" || !oum_hide_location_popup) {
1286      const popup = L.responsivePopup().setContent(oumBubbleLoadingHtml());
1287      layer.bindPopup(popup);
1288    }
1289
1290    return layer;
1291  }
1292
1293  function createLocationLayer(location) {
1294    return (location.geometry_type === "polyline" || location.geometry_type === "polygon") && location.geometry
1295      ? createPolylineLocation(location)
1296      : createMarker(location);
1297  }
1298
1299  /**
1300   * GestureHandling shows "use two fingers to move the map" on one-finger touchmove.
1301   * Leaflet already stops touchstart on popups, but nested popup content still
1302   * bubbles touchmove to the map container. Stop that so scrolling the bubble
1303   * does not trigger the overlay.
1304   */
1305  function preventGestureHandlingInsidePopup(popup) {
1306    const popupEl = popup && typeof popup.getElement === "function" ? popup.getElement() : null;
1307    if (!popupEl || popupEl.dataset.oumIgnoreGestures === "true") {
1308      return;
1309    }
1310
1311    popupEl.dataset.oumIgnoreGestures = "true";
1312    L.DomEvent.on(popupEl, "touchmove", L.DomEvent.stopPropagation);
1313  }
1314
1315  function setupMarkerEvents() {
1316    map.on("popupopen", function (locationBubble) {
1317      const popup = locationBubble.popup;
1318      preventGestureHandlingInsidePopup(popup);
1319      const marker = popup._source;
1320
1321      const applyBubbleHtml = (html) => {
1322        popup.setContent(html);
1323        if (typeof popup.update === "function") {
1324          popup.update();
1325        }
1326        finalizeMarkerBubbleUI(popup);
1327      };
1328
1329      if (!marker || !marker.options || !marker.options.post_id) {
1330        finalizeMarkerBubbleUI(popup);
1331        return;
1332      }
1333
1334      if (marker._oumBubbleHtmlCache) {
1335        applyBubbleHtml(marker._oumBubbleHtmlCache);
1336        return;
1337      }
1338
1339      marker._oumBubbleFetchId = (marker._oumBubbleFetchId || 0) + 1;
1340      const myFetchId = marker._oumBubbleFetchId;
1341      const postId = marker.options.post_id;
1342
1343      popup.setContent(oumBubbleLoadingHtml());
1344      if (typeof popup.update === "function") {
1345        popup.update();
1346      }
1347      finalizeMarkerBubbleUI(popup);
1348
1349      if (
1350        typeof jQuery === "undefined" ||
1351        !oum_ajax ||
1352        !oum_ajax.ajaxurl
1353      ) {
1354        applyBubbleHtml(oumBubbleErrorHtml());
1355        return;
1356      }
1357
1358      jQuery.ajax({
1359        type: "POST",
1360        url: oum_ajax.ajaxurl,
1361        dataType: "json",
1362        data: {
1363          action: oum_ajax.bubble_action || "oum_get_location_bubble",
1364          post_id: postId,
1365        },
1366        success: function (response) {
1367          if (myFetchId !== marker._oumBubbleFetchId) {
1368            return;
1369          }
1370          if (response && response.success && response.data && response.data.html) {
1371            marker._oumBubbleHtmlCache = response.data.html;
1372            applyBubbleHtml(response.data.html);
1373          } else {
1374            applyBubbleHtml(oumBubbleErrorHtml());
1375          }
1376        },
1377        error: function () {
1378          if (myFetchId !== marker._oumBubbleFetchId) {
1379            return;
1380          }
1381          applyBubbleHtml(oumBubbleErrorHtml());
1382        },
1383      });
1384    });
1385
1386    // Event: Close Location Bubble
1387    map.on("popupclose", function () {
1388      const el = document.querySelector(
1389        ".open-user-map #location-fullscreen-container"
1390      );
1391      if (el) {
1392        // Clear the content to stop any media playback (YouTube, Vimeo, etc.)
1393        const locationContentWrap = el.querySelector(".location-content-wrap");
1394        if (locationContentWrap) {
1395          locationContentWrap.innerHTML = '';
1396        }
1397        el.classList.remove("visible");
1398      }
1399      document.querySelector("body").classList.remove("oum-location-opened");
1400    });
1401  }
1402  
1403  /**
1404   * Check edit permission for a location and inject edit button if allowed
1405   * This is called when a popup opens to dynamically show edit buttons
1406   * based on real-time permissions (cache-safe solution)
1407   * 
1408   * @param {HTMLElement} container - The container element to search for placeholder
1409   */
1410  function checkEditPermissionAndInjectButton(container) {
1411    // Find the edit button placeholder in the opened popup
1412    const placeholder = container.querySelector('.edit-location-button-placeholder');
1413    
1414    if (!placeholder) {
1415      return; // No placeholder found, nothing to do
1416    }
1417    
1418    const postId = placeholder.getAttribute('data-post-id');
1419    
1420    if (!postId) {
1421      placeholder.remove();
1422      return;
1423    }
1424    
1425    if (typeof jQuery === 'undefined') {
1426      placeholder.remove();
1427      return;
1428    }
1429    
1430    if (!oum_ajax || !oum_ajax.ajaxurl) {
1431      placeholder.remove();
1432      return;
1433    }
1434    
1435    // Make AJAX call to check permissions
1436    jQuery.ajax({
1437      type: 'POST',
1438      url: oum_ajax.ajaxurl,
1439      dataType: 'json',
1440      data: {
1441        action: 'oum_check_edit_permission',
1442        post_id: postId
1443      },
1444      success: function(response) {
1445        if (response && response.success && response.data && response.data.can_edit) {
1446          // User can edit - inject the edit button
1447          const editButton = document.createElement('div');
1448          editButton.className = 'edit-location-button';
1449          editButton.setAttribute('data-post-id', postId);
1450          editButton.setAttribute('title', oum_custom_strings && oum_custom_strings.edit_location ? oum_custom_strings.edit_location : 'Edit location');
1451          
1452          // Replace placeholder with actual button
1453          placeholder.parentNode.replaceChild(editButton, placeholder);
1454        } else {
1455          // User cannot edit - remove placeholder (keep clean HTML)
1456          placeholder.remove();
1457        }
1458      },
1459      error: function(xhr, status, error) {
1460        // On error, just remove the placeholder
1461        placeholder.remove();
1462      }
1463    });
1464  }
1465
1466  function applyFilters() {
1467    if (!markersLayer) {
1468      return;
1469    }
1470
1471    markersLayer.clearLayers();
1472    if (vectorLayer) {
1473      vectorLayer.clearLayers();
1474    }
1475    const filteredMarkers = [];
1476
1477    allMarkers.forEach((marker) => {
1478      if (
1479        markerMatchesSearchtextFilter(marker) &&
1480        markerMatchesCategoriesFilter(marker) &&
1481        markerMatchesCustomfieldsFilter(marker)
1482      ) {
1483        filteredMarkers.push(marker);
1484      }
1485    });
1486
1487    visibleMarkersCount = filteredMarkers.length;
1488    addMarkersToLayer(filteredMarkers);
1489
1490    if (oum_enable_cluster && typeof markersLayer.refreshClusters === "function") {
1491      markersLayer.refreshClusters();
1492    }
1493  }
1494
1495  function addMarkersToLayer(markers) {
1496    if (!markersLayer || !markers.length) {
1497      return;
1498    }
1499
1500    const pointMarkers = [];
1501    const vectorMarkers = [];
1502
1503    markers.forEach((marker) => {
1504      if (marker instanceof L.Marker) {
1505        pointMarkers.push(marker);
1506      } else {
1507        vectorMarkers.push(marker);
1508      }
1509    });
1510
1511    vectorMarkers.forEach((marker) => {
1512      if (vectorLayer) {
1513        vectorLayer.addLayer(marker);
1514      }
1515    });
1516
1517    if (!pointMarkers.length) {
1518      return;
1519    }
1520
1521    // Batch addLayers() sometimes draws no marker when only one exists; one addLayer() avoids that bug.
1522    if (
1523      oum_enable_cluster &&
1524      pointMarkers.length === 1 &&
1525      typeof markersLayer.addLayer === "function"
1526    ) {
1527      markersLayer.addLayer(pointMarkers[0]);
1528      return;
1529    }
1530
1531    // Batch marker insertion when supported to improve large dataset performance.
1532    if (typeof markersLayer.addLayers === "function") {
1533      markersLayer.addLayers(pointMarkers);
1534      return;
1535    }
1536
1537    pointMarkers.forEach((marker) => {
1538      markersLayer.addLayer(marker);
1539    });
1540  }
1541
1542  function markerMatchesSearchtextFilter(marker) {
1543    if (!searchtextFilterValue) {
1544      return true;
1545    }
1546    return marker.options.content.toLowerCase().includes(searchtextFilterValue);
1547  }
1548
1549  function markerMatchesCategoriesFilter(marker) {
1550    const markerTypes = marker.options.types || [];
1551
1552    if (!markerTypes.length) {
1553      return true;
1554    }
1555
1556    // Before the user interacts with the category checkboxes, keep the previous behaviour (show all)
1557    if (categoriesFilterSelection === null) {
1558      return true;
1559    }
1560
1561    if (!categoriesFilterSelection.size) {
1562      return false;
1563    }
1564
1565    return markerTypes.some((type) => categoriesFilterSelection.has(type));
1566  }
1567
1568  function markerMatchesCustomfieldsFilter(marker) {
1569    if (!customfieldsFilterSelection || !Object.keys(customfieldsFilterSelection).length) {
1570      return true;
1571    }
1572
1573    const locationData = getLocationData(marker.options.post_id);
1574    if (!locationData) {
1575      return true;
1576    }
1577
1578    for (const [customFieldId, criterion] of Object.entries(customfieldsFilterSelection)) {
1579      // Special handling for opening_hours_open_now filter type
1580      if (criterion.type === 'opening_hours_open_now') {
1581        const field = locationData.custom_fields?.find(
1582          (customField) => String(customField.index) === String(customFieldId)
1583        );
1584        
1585        // Use pre-calculated open_now value from PHP
1586        if (field && typeof field.open_now === 'boolean') {
1587          if (field.open_now !== true) {
1588            return false;
1589          }
1590        } else {
1591          // Fallback: if open_now not available, exclude location
1592          return false;
1593        }
1594        continue;
1595      }
1596
1597      // Standard custom field filtering
1598      const fieldValue = getCustomFieldValue(locationData, customFieldId);
1599      if (!matchesCriterion(fieldValue, criterion)) {
1600        return false;
1601      }
1602    }
1603
1604    return true;
1605  }
1606
1607  function getLocationData(postId) {
1608    if (!postId) {
1609      return null;
1610    }
1611
1612    if (locationsById[String(postId)]) {
1613      return locationsById[String(postId)];
1614    }
1615
1616    if (typeof window !== "undefined" && Array.isArray(window.oum_all_locations)) {
1617      const fallback = window.oum_all_locations.find(
1618        (location) => String(location.post_id) === String(postId)
1619      );
1620      if (fallback) {
1621        locationsById[String(postId)] = fallback;
1622        return fallback;
1623      }
1624    }
1625
1626    return null;
1627  }
1628
1629  function getCustomFieldValue(locationData, customFieldId) {
1630    if (!locationData || !Array.isArray(locationData.custom_fields)) {
1631      return null;
1632    }
1633
1634    const field = locationData.custom_fields.find(
1635      (customField) => String(customField.index) === String(customFieldId)
1636    );
1637
1638    return field ? field.val : null;
1639  }
1640
1641  function normalizeFieldValues(fieldValue) {
1642    if (Array.isArray(fieldValue)) {
1643      return fieldValue.map((val) => val.toString()).filter((val) => val !== "");
1644    }
1645
1646    if (typeof fieldValue === "string") {
1647      return fieldValue
1648        .split("|")
1649        .map((val) => val.trim())
1650        .filter((val) => val !== "");
1651    }
1652
1653    if (fieldValue !== null && typeof fieldValue !== "undefined") {
1654      const value = fieldValue.toString();
1655      return value ? [value] : [];
1656    }
1657
1658    return [];
1659  }
1660
1661  function matchesCriterion(fieldValue, criterion) {
1662    if (
1663      fieldValue === null ||
1664      typeof fieldValue === "undefined" ||
1665      (typeof fieldValue === "string" && fieldValue.trim() === "") ||
1666      (Array.isArray(fieldValue) && fieldValue.length === 0)
1667    ) {
1668      return false;
1669    }
1670
1671    switch (criterion.type) {
1672      case "text":
1673        return fieldValue.toString().toLowerCase().includes(criterion.value);
1674      case "checkbox": {
1675        const values = normalizeFieldValues(fieldValue);
1676        const relation = criterion.relation || 'OR'; // Default to OR for backward compatibility
1677        
1678        if (relation === 'AND') {
1679          // AND: All selected checkbox values must be present in the field values
1680          return criterion.values.length > 0 && criterion.values.every((val) => values.includes(val));
1681        } else {
1682          // OR: At least one selected checkbox value must match (default behavior)
1683          return criterion.values.some((val) => values.includes(val));
1684        }
1685      }
1686      case "radio":
1687      case "select": {
1688        const values = normalizeFieldValues(fieldValue);
1689        return values.includes(criterion.value);
1690      }
1691      default:
1692        return true;
1693    }
1694  }
1695
1696  function updateSearchtextAndCategoriesFilters() {
1697    // Read searchtext filter
1698    const markerFilterInput = document.getElementById("oum_filter_markers");
1699    searchtextFilterValue = markerFilterInput ? markerFilterInput.value.toLowerCase() : "";
1700
1701    // Read categories filter
1702    const categoryInputs = document.querySelectorAll(categoryFilterSelector);
1703    if (categoryInputs.length === 0) {
1704      // No category UI on this map (hidden overlay / hide_filterbox) — do not filter by category
1705      categoriesFilterSelection = null;
1706    } else {
1707      categoriesFilterSelection = new Set(
1708        Array.from(categoryInputs)
1709          .filter((input) => input.checked)
1710          .map((input) => input.value)
1711      );
1712    }
1713
1714    applyFilters();
1715  }
1716
1717  function setupFilterListEvents() {
1718    const filterControls = document.querySelector(
1719      ".open-user-map .oum-filter-controls"
1720    );
1721    if (!filterControls) return;
1722
1723    const closeButton = filterControls.querySelector(".close-filter-list");
1724    const toggle = filterControls.querySelector(".oum-filter-toggle");
1725    const filterWrapper = filterControls.closest(".oum-map-filter-wrapper");
1726
1727    // Close button always collapses the filter controls
1728    if (closeButton) {
1729      closeButton.addEventListener("click", function(e) {
1730        e.stopPropagation();
1731        filterControls.classList.remove("active");
1732        // Ensure use-collapse class is present so the toggle button becomes visible
1733        filterControls.classList.add("use-collapse");
1734      });
1735    }
1736
1737    // Setup click toggle for the toggle button (works regardless of initial use-collapse state)
1738    if (toggle) {
1739      toggle.addEventListener("click", function(e) {
1740        e.stopPropagation();
1741        filterControls.classList.toggle("active");
1742      });
1743    }
1744
1745    // Close when clicking outside the filter wrapper (only if use-collapse is present)
1746    if (filterWrapper) {
1747      document.addEventListener("click", function(e) {
1748        if (filterControls.classList.contains("use-collapse") && !filterWrapper.contains(e.target)) {
1749          filterControls.classList.remove("active");
1750        }
1751      });
1752    }
1753  }
1754
1755  // Select all works for both the on-map overlay and the Advanced Filter section
1756  function setupToggleAll() {
1757    const categoryFilter = document.querySelector(
1758      ".open-user-map .oum-marker-categories-filter"
1759    );
1760    if (!categoryFilter) return;
1761
1762    const toggleAllCheckbox = categoryFilter.querySelector(
1763      ".oum-toggle-all-checkbox"
1764    );
1765    const categoryCheckboxes = categoryFilter.querySelectorAll(
1766      '[name="type"]'
1767    );
1768
1769    if (!toggleAllCheckbox || categoryCheckboxes.length === 0) return;
1770
1771    function updateToggleAllState() {
1772      const checkedCount = Array.from(categoryCheckboxes).filter(
1773        (cb) => cb.checked
1774      ).length;
1775      const totalCount = categoryCheckboxes.length;
1776
1777      if (checkedCount === 0) {
1778        toggleAllCheckbox.checked = false;
1779        toggleAllCheckbox.indeterminate = false;
1780      } else if (checkedCount === totalCount) {
1781        toggleAllCheckbox.checked = true;
1782        toggleAllCheckbox.indeterminate = false;
1783      } else {
1784        toggleAllCheckbox.checked = false;
1785        toggleAllCheckbox.indeterminate = true;
1786      }
1787    }
1788
1789    function toggleAllCategories() {
1790      const checkedCount = Array.from(categoryCheckboxes).filter(
1791        (cb) => cb.checked
1792      ).length;
1793      const totalCount = categoryCheckboxes.length;
1794      const shouldCheck = checkedCount < totalCount;
1795
1796      categoryCheckboxes.forEach((checkbox) => {
1797        checkbox.checked = shouldCheck;
1798      });
1799
1800      updateSearchtextAndCategoriesFilters();
1801    }
1802
1803    toggleAllCheckbox.addEventListener("change", toggleAllCategories);
1804
1805    categoryCheckboxes.forEach((checkbox) => {
1806      checkbox.addEventListener("change", updateToggleAllState);
1807    });
1808
1809    updateToggleAllState();
1810  }
1811
1812  function resetCategoriesFilter() {
1813    const categoryInputs = document.querySelectorAll(categoryFilterSelector);
1814    categoryInputs.forEach((input) => {
1815      input.checked = true;
1816    });
1817
1818    const toggleAllCheckbox = document.querySelector(
1819      ".open-user-map .oum-marker-categories-filter .oum-toggle-all-checkbox"
1820    );
1821    if (toggleAllCheckbox) {
1822      toggleAllCheckbox.checked = true;
1823      toggleAllCheckbox.indeterminate = false;
1824    }
1825
1826    updateSearchtextAndCategoriesFilters();
1827  }
1828
1829  function handleAutoOpenMarker(markerId) {
1830    const targetMarker = allMarkers.find((m) => m.options.post_id === markerId);
1831    if (targetMarker) {
1832      const openPopup = typeof oum_hide_location_popup === "undefined" || !oum_hide_location_popup;
1833      if (oum_enable_cluster) {
1834        if (targetMarker instanceof L.Marker && typeof markersLayer.zoomToShowLayer === "function") {
1835          setTimeout(() => {
1836            markersLayer.zoomToShowLayer(targetMarker, () => {
1837              if (openPopup) targetMarker.openPopup();
1838            });
1839          }, 500);
1840        } else if (targetMarker.getBounds) {
1841          map.fitBounds(targetMarker.getBounds(), { padding: [20, 20] });
1842          if (openPopup) targetMarker.openPopup();
1843        }
1844      } else {
1845        if (targetMarker.getLatLng) {
1846          map.setView(targetMarker.getLatLng(), targetMarker.options.zoom);
1847        } else if (targetMarker.getBounds) {
1848          map.fitBounds(targetMarker.getBounds(), { padding: [20, 20] });
1849        }
1850        if (openPopup) targetMarker.openPopup();
1851      }
1852    }
1853  }
1854
1855  // Public interface
1856  return {
1857    init: function (mapInstance) {
1858      map = mapInstance;
1859      initializeMarkersLayer();
1860      markersLayer.addTo(map);
1861      vectorLayer.addTo(map);
1862      setupMarkerEvents();
1863      setupFilterListEvents();
1864      setupToggleAll();
1865
1866      // Make layer globally available (for backward compatibility)
1867      window.oumMarkersLayer = markersLayer;
1868      window.oumAllMarkers = allMarkers;
1869
1870      // Dispatch markers initialized event
1871      document.dispatchEvent(new CustomEvent('oum:markers_initialized', {
1872        detail: {
1873          mapId: map._container.id,
1874          markersLayer: markersLayer
1875        }
1876      }));
1877
1878      return this;
1879    },
1880    addMarkers: function (locations) {
1881      locations.forEach((location) => {
1882        const marker = createLocationLayer(location);
1883        allMarkers.push(marker);
1884        locationsById[String(location.post_id)] = location;
1885      });
1886
1887      // Apply markers once so filters and clustering can batch-add visible markers.
1888      applyFilters();
1889
1890      // After adding all markers, check if we need to auto-open one
1891      const POPUP_MARKER_ID = OUMUtils.getParameterByName("markerid");
1892      if (POPUP_MARKER_ID) {
1893        handleAutoOpenMarker(POPUP_MARKER_ID);
1894      }
1895    },
1896    updateSearchtextAndCategoriesFilters: updateSearchtextAndCategoriesFilters,
1897    resetCategoriesFilter: resetCategoriesFilter,
1898    getMarkersLayer: function () {
1899      return markersLayer;
1900    },
1901    getAllMarkers: function () {
1902      return allMarkers;
1903    },
1904    getAllLocations: function () {
1905      return Object.values(locationsById);
1906    },
1907    setCustomfieldsFilter: function (criteria) {
1908      if (criteria && Object.keys(criteria).length > 0) {
1909        customfieldsFilterSelection = criteria;
1910      } else {
1911        customfieldsFilterSelection = null;
1912      }
1913      applyFilters();
1914    },
1915    getFilterState: function () {
1916      return {
1917        searchtext: searchtextFilterValue,
1918        categories:
1919          categoriesFilterSelection === null
1920            ? null
1921            : Array.from(categoriesFilterSelection),
1922        customfields: customfieldsFilterSelection
1923      };
1924    },
1925    getFilteredMarkersCount: function () {
1926      return visibleMarkersCount;
1927    },
1928    /** Clear lazy-loaded popup HTML so the next open refetches (e.g. after vote count changes). */
1929    invalidateBubbleHtmlCacheForPost: function (postId) {
1930      const id = String(postId);
1931      allMarkers.forEach((m) => {
1932        if (m && m.options && String(m.options.post_id) === id) {
1933          delete m._oumBubbleHtmlCache;
1934          m._oumBubbleFetchId = 0;
1935        }
1936      });
1937    },
1938  };
1939})();
1940
1941/**
1942 * Form Map Module - Handles all map-related functionality for the form
1943 */
1944const OUMFormMap = (function () {
1945  // Private variables
1946  let formMap = null;
1947  let locationMarker = null;
1948  let markerIsVisible = false;
1949  let isAdjusting = false;
1950  let isInitialized = false;
1951  let currentGeometryType = "point";
1952  let polylineDrawnItems = null;
1953  let polylineDrawControl = null;
1954  let polylineLayer = null;
1955
1956  // Private functions
1957  function initializeFormMap() {
1958    if (isInitialized) {
1959      return;
1960    }
1961
1962    formMap = L.map("mapGetLocation", {
1963      attributionControl: false,
1964      gestureHandling: true,
1965      minZoom: 1,
1966      zoomSnap: 0.01, // Allow fractional zoom levels (0.01 steps)
1967      zoomDelta: 1, // Zoom step size for controls
1968      fullscreenControl: oum_enable_fullscreen,
1969      fullscreenControlOptions: {
1970        position: "topleft",
1971      },
1972    });
1973
1974    // Make form map globally available (for backward compatibility)
1975    window.oumMap2 = formMap;
1976
1977    setupTileLayer();
1978    setupControls();
1979    setupMarker();
1980    setupMapEvents();
1981    setupPolylineDrawing();
1982    setupLocationTypeControls();
1983
1984    // Invalidate size to ensure accurate measurements before calculating bounds
1985    formMap.invalidateSize();
1986
1987    // Always apply bounds to prevent showing repeated maps
1988    const boundsToUse = oum_enable_fixed_map_bounds 
1989      ? sharedMapBounds 
1990      : OUMConfig.defaults.map.bounds;
1991
1992    // Set the bounds
1993    formMap.setMaxBounds(boundsToUse);
1994
1995    // Calculate minimum zoom level based on bounds
1996    // When fixed bounds are enabled, use sharedMapBounds for accurate calculation
1997    // This is used both for setting minZoom and in the moveend event handler
1998    let maxVisibleBounds;
1999    if (oum_enable_fixed_map_bounds && sharedMapBounds) {
2000      // Invalidate size first to ensure accurate measurements
2001      formMap.invalidateSize();
2002      maxVisibleBounds = formMap.getBoundsZoom(sharedMapBounds);
2003      // Set minimum zoom level to prevent users from zooming out too far
2004      formMap.setMinZoom(maxVisibleBounds);
2005    } else {
2006      maxVisibleBounds = formMap.getBoundsZoom(boundsToUse);
2007    }
2008
2009    // Add moveend event to enforce bounds
2010    formMap.on("moveend", function () {
2011      if (isAdjusting) return;
2012      isAdjusting = true;
2013
2014      const zoom = formMap.getZoom();
2015
2016      // Only enforce bounds if we're zoomed in beyond the minimum zoom
2017      if (zoom > maxVisibleBounds) {
2018        const currentBounds = formMap.getBounds();
2019        const currentCenter = formMap.getCenter();
2020
2021        let needsAdjustment = false;
2022        let newLat = currentCenter.lat;
2023        let newLng = currentCenter.lng;
2024
2025        // Calculate current viewport dimensions
2026        const viewportHeight = currentBounds.getNorth() - currentBounds.getSouth();
2027        const viewportWidth = currentBounds.getEast() - currentBounds.getWest();
2028
2029        // Check and adjust latitude (north/south)
2030        if (currentBounds.getNorth() > boundsToUse.getNorth()) {
2031          newLat = boundsToUse.getNorth() - viewportHeight / 2;
2032          needsAdjustment = true;
2033        } else if (currentBounds.getSouth() < boundsToUse.getSouth()) {
2034          newLat = boundsToUse.getSouth() + viewportHeight / 2;
2035          needsAdjustment = true;
2036        }
2037
2038        // Check and adjust longitude (east/west)
2039        if (currentBounds.getEast() > boundsToUse.getEast()) {
2040          newLng = boundsToUse.getEast() - viewportWidth / 2;
2041          needsAdjustment = true;
2042        } else if (currentBounds.getWest() < boundsToUse.getWest()) {
2043          newLng = boundsToUse.getWest() + viewportWidth / 2;
2044          needsAdjustment = true;
2045        }
2046
2047        if (needsAdjustment) {
2048          formMap.setView([newLat, newLng], zoom, { animate: false });
2049        }
2050      }
2051
2052      isAdjusting = false;
2053    });
2054
2055    // Keep the hidden zoom input in sync with the current form map zoom.
2056    formMap.on("zoomend", function () {
2057      syncLocationZoomField();
2058    });
2059
2060    // Update minZoom when fixed bounds are enabled
2061    // This prevents users from zooming out too far
2062    updateMinZoom();
2063
2064    isInitialized = true;
2065  }
2066
2067  function updateMinZoom() {
2068    if (!formMap) return;
2069    
2070    // Recalculate and set minZoom when fixed bounds are enabled
2071    // This prevents users from zooming out beyond the defined bounds
2072    if (oum_enable_fixed_map_bounds && sharedMapBounds) {
2073      formMap.invalidateSize();
2074      const maxVisibleBounds = formMap.getBoundsZoom(sharedMapBounds);
2075      formMap.setMinZoom(maxVisibleBounds);
2076    }
2077  }
2078
2079  function setupTileLayer() {
2080    // Default to OpenStreetMap if mapStyle is undefined
2081    const mapStyle = window.mapStyle || 'OpenFreeMap.bright';
2082
2083    window.OUMMapStyles.addBaseLayer(formMap, mapStyle, {
2084      mapboxKey: OUMConfig.getTileProviderKey(),
2085      cartoKey: OUMConfig.getCartoTileProviderKey(),
2086      setupCustomImageLayer: setupCustomImageLayer,
2087      customImageHideTiles: window.oum_custom_image_hide_tiles,
2088      customImageBackgroundColor: window.oum_custom_image_background_color,
2089    });
2090  }
2091
2092  function setupControls() {
2093    // Add searchbar: address
2094    const search = new GeoSearch.GeoSearchControl({
2095      style: "bar",
2096      showMarker: false,
2097      provider: OUMUtils.initGeosearchProvider(),
2098      searchLabel: oum_searchaddress_label,
2099      updateMap: false,
2100    });
2101    formMap.addControl(search);
2102
2103    // Add control: get current location
2104    if (oum_enable_currentlocation) {
2105      window.formMap_locate_process = L.control.locate({
2106        flyTo: true,
2107        showPopup: false,
2108      }).addTo(formMap);
2109    }
2110  }
2111
2112  function setupMarker() {
2113    // Marker Icon
2114    let markerIcon = L.icon({
2115      iconUrl: marker_icon_url,
2116      iconSize: [26, 41],
2117      iconAnchor: [13, 41],
2118      popupAnchor: [0, -25],
2119      shadowUrl: marker_shadow_url,
2120      shadowSize: [41, 41],
2121      shadowAnchor: [13, 41],
2122    });
2123
2124    locationMarker = L.marker([0, 0], {
2125      icon: markerIcon,
2126      draggable: true,
2127    });
2128
2129    // Make marker globally available (for backward compatibility)
2130    window.locationMarker = locationMarker;
2131    window.markerIsVisible = markerIsVisible;
2132
2133    // Event: drag marker
2134    locationMarker.on("dragend", function (e) {
2135      setLocationLatLng(e.target.getLatLng());
2136    });
2137  }
2138
2139  function setupMapEvents() {
2140    // Event: click on map to set marker OR location found
2141    formMap.on("click locationfound", function (e) {
2142      if (currentGeometryType !== "point") {
2143        return;
2144      }
2145
2146      let coords = e.latlng;
2147      locationMarker.setLatLng(coords);
2148
2149      if (!markerIsVisible) {
2150        locationMarker.addTo(formMap);
2151        markerIsVisible = true;
2152        window.markerIsVisible = true;
2153      }
2154
2155      setLocationLatLng(coords);
2156    });
2157
2158    // Event: geosearch success
2159    formMap.on("geosearch/showlocation", handleGeosearchSuccess);
2160  }
2161
2162  function handleGeosearchSuccess(e) {
2163    let coords = e.marker._latlng;
2164    let label = e.location.label;
2165    const searchBar = document.querySelector(`#mapGetLocation .leaflet-geosearch-bar form`);
2166
2167    // Check against fixed map bounds if enabled, otherwise allow any location
2168    let isInBounds = true; // Default to true when fixed bounds is disabled
2169    if (oum_enable_fixed_map_bounds === 'on' && sharedMapBounds) {
2170      // Check if location is within the fixed map bounds (not just current viewport)
2171      isInBounds = sharedMapBounds.contains(coords);
2172    }
2173
2174    // Only restrict movement if fixed map bounds is enabled and location is outside those bounds
2175    if (!isInBounds && oum_enable_fixed_map_bounds === 'on') {
2176      console.log("This search result is out of reach.");
2177      if (searchBar) {
2178        searchBar.style.boxShadow = "0 0 10px rgb(255, 111, 105)";
2179        setTimeout(function () {
2180          searchBar.style.boxShadow = "0 1px 5px rgba(255, 255, 255, 0.65)";
2181        }, 2000);
2182      }
2183    } else {
2184      if (currentGeometryType !== "point") {
2185        handleValidGeosearchResult(e.location);
2186        return;
2187      }
2188
2189      // Location is within fixed bounds (or fixed bounds is disabled) - handle normally
2190      // Set coordinates and address immediately (before map animation) for instant feedback
2191      setLocationLatLng(coords, label);
2192      
2193      // Set marker position
2194      locationMarker.setLatLng(coords);
2195
2196      if (!markerIsVisible) {
2197        locationMarker.addTo(formMap);
2198        markerIsVisible = true;
2199        window.markerIsVisible = true;
2200      }
2201
2202      // Fly to location (animated - happens after address is already set)
2203      handleValidGeosearchResult(e.location);
2204    }
2205  }
2206
2207  function handleValidGeosearchResult(location) {
2208    if (location.bounds !== null) {
2209      formMap.flyToBounds(location.bounds);
2210    } else if (location.raw.mapView) {
2211      formMap.flyToBounds([
2212        [location.raw.mapView.south, location.raw.mapView.west],
2213        [location.raw.mapView.north, location.raw.mapView.east],
2214      ]);
2215    } else {
2216      formMap.flyTo([location.y, location.x], 17);
2217    }
2218  }
2219
2220  function setLocationLatLng(markerLatLng, address, skipReverseGeocode = false) {
2221    document.getElementById("oum_location_lat").value = markerLatLng.lat;
2222    document.getElementById("oum_location_lng").value = markerLatLng.lng;
2223    syncLocationZoomField();
2224    
2225    // Only perform reverse geocoding if both subtitle field and autofill are enabled
2226    if (!skipReverseGeocode && shouldPerformReverseGeocoding()) {
2227      reverseGeocode(markerLatLng.lat, markerLatLng.lng, address);
2228    }
2229  }
2230
2231  function clearMarker() {
2232    if (locationMarker && formMap) {
2233      locationMarker.remove();
2234      markerIsVisible = false;
2235      window.markerIsVisible = false;
2236    }
2237  }
2238
2239  function setupLocationTypeControls() {
2240    const controls = document.querySelectorAll('input[name="oum_location_type_choice"]');
2241    controls.forEach((control) => {
2242      control.addEventListener("change", function () {
2243        if (this.checked) {
2244          setGeometryType(this.value);
2245        }
2246      });
2247    });
2248
2249    const typeField = document.getElementById("oum_geometry_type");
2250    setGeometryType(typeField && typeField.value ? typeField.value : "point", { autoStartDrawing: false });
2251  }
2252
2253  function setupPolylineDrawing() {
2254    if (!L.Control || !L.Control.Draw) {
2255      return;
2256    }
2257
2258    polylineDrawnItems = L.featureGroup().addTo(formMap);
2259    createPolylineDrawControl();
2260    setupPolylineDrawingEvents();
2261  }
2262
2263  function createPolylineDrawControl() {
2264    if (!formMap || !L.Control || !L.Control.Draw) {
2265      return;
2266    }
2267
2268    if (polylineDrawControl && polylineDrawControl._map) {
2269      formMap.removeControl(polylineDrawControl);
2270    }
2271
2272    polylineDrawControl = new L.Control.Draw({
2273      draw: {
2274        polyline: currentGeometryType === "polyline" ? {
2275          shapeOptions: getPolylineStyle(),
2276        } : false,
2277        polygon: currentGeometryType === "polygon" ? {
2278          shapeOptions: getPolygonStyle(),
2279        } : false,
2280        rectangle: false,
2281        circle: false,
2282        circlemarker: false,
2283        marker: false,
2284      },
2285      edit: {
2286        featureGroup: polylineDrawnItems,
2287        remove: true,
2288      },
2289    });
2290  }
2291
2292  function setupPolylineDrawingEvents() {
2293    formMap.on(L.Draw.Event.CREATED, function (event) {
2294      if (currentGeometryType === "point") {
2295        return;
2296      }
2297
2298      setPolylineLayer(event.layer);
2299    });
2300
2301    formMap.on(L.Draw.Event.EDITED, function () {
2302      syncPolylineGeometryFields();
2303    });
2304
2305    formMap.on(L.Draw.Event.DELETED, function () {
2306      polylineLayer = null;
2307      syncPolylineGeometryFields();
2308    });
2309  }
2310
2311  function getPolylineStyle() {
2312    return {
2313      color: "#e82c71",
2314      weight: 4,
2315      opacity: 0.9,
2316    };
2317  }
2318
2319  function getPolygonStyle() {
2320    return {
2321      ...getPolylineStyle(),
2322      fillColor: "#e82c71",
2323      fillOpacity: 0.25,
2324    };
2325  }
2326
2327  function setGeometryType(type, options = {}) {
2328    currentGeometryType = type === "polyline" || type === "polygon" ? type : "point";
2329    const autoStartDrawing = options.autoStartDrawing !== false;
2330
2331    cancelActiveDrawTool();
2332
2333    const addLocationEl = document.querySelector(".add-location");
2334    if (addLocationEl) {
2335      addLocationEl.setAttribute("data-oum-geometry-type", currentGeometryType);
2336    }
2337
2338    const typeField = document.getElementById("oum_geometry_type");
2339    if (typeField) {
2340      typeField.value = currentGeometryType;
2341    }
2342
2343    const choice = document.querySelector(`input[name="oum_location_type_choice"][value="${currentGeometryType}"]`);
2344    if (choice) {
2345      choice.checked = true;
2346    }
2347
2348    document.querySelectorAll("[data-oum-location-type-card]").forEach((card) => {
2349      card.classList.toggle("is-selected", card.getAttribute("data-oum-location-type-card") === currentGeometryType);
2350    });
2351
2352    updateLocationTypeCategoryUi();
2353
2354    if (currentGeometryType === "polyline") {
2355      clearMarker();
2356      createPolylineDrawControl();
2357      addPolylineControl();
2358      if (autoStartDrawing) {
2359        startActiveDrawTool();
2360      }
2361    } else if (currentGeometryType === "polygon") {
2362      clearMarker();
2363      createPolylineDrawControl();
2364      addPolylineControl();
2365      if (autoStartDrawing) {
2366        startActiveDrawTool();
2367      }
2368    } else {
2369      removePolylineControl();
2370      clearPolyline();
2371    }
2372
2373    syncPolylineGeometryFields();
2374  }
2375
2376  function updateLocationTypeCategoryUi() {
2377    const categoryLabelKey = currentGeometryType === "polygon"
2378      ? "polygonLabel"
2379      : (currentGeometryType === "polyline" ? "polylineLabel" : "pointLabel");
2380    const categoryTitles = document.querySelectorAll("[data-oum-category-title]");
2381
2382    categoryTitles.forEach((titleEl) => {
2383      const label = titleEl.dataset[categoryLabelKey] || titleEl.dataset.pointLabel || titleEl.textContent;
2384      titleEl.textContent = label;
2385    });
2386
2387    document.querySelectorAll(".oum-category-dropdown-display").forEach((display) => {
2388      display.setAttribute("data-oum-geometry-type", currentGeometryType);
2389    });
2390
2391    document.querySelectorAll(".oum-marker-category-option").forEach((option) => {
2392      const categoryType = option.getAttribute("data-category-type") || "point";
2393      const isMatchingType = categoryType === currentGeometryType;
2394      const input = option.querySelector('input[name="oum_marker_icon[]"]');
2395
2396      option.hidden = !isMatchingType;
2397
2398      if (input) {
2399        input.disabled = !isMatchingType;
2400        if (!isMatchingType) {
2401          input.checked = false;
2402        }
2403      }
2404    });
2405
2406    document.querySelectorAll('select#oum_marker_icon option[data-category-type]').forEach((option) => {
2407      const categoryType = option.getAttribute("data-category-type") || "point";
2408      const isMatchingType = categoryType === currentGeometryType;
2409
2410      option.hidden = !isMatchingType;
2411      option.disabled = !isMatchingType;
2412      if (!isMatchingType && option.selected) {
2413        option.selected = false;
2414      }
2415    });
2416
2417    document.querySelectorAll("select#oum_marker_icon").forEach((select) => {
2418      const selectedOption = select.options[select.selectedIndex];
2419      if (selectedOption && selectedOption.disabled) {
2420        select.value = "";
2421      }
2422      select.dispatchEvent(new Event("change"));
2423    });
2424  }
2425
2426  function startActiveDrawTool(attempt = 0) {
2427    if (!formMap) {
2428      return;
2429    }
2430
2431    window.setTimeout(() => {
2432      if (!formMap._loaded) {
2433        if (attempt < 10) {
2434          startActiveDrawTool(attempt + 1);
2435        }
2436        return;
2437      }
2438
2439      const mapContainer = formMap.getContainer();
2440      if (!mapContainer) {
2441        return;
2442      }
2443
2444      const selector = currentGeometryType === "polygon"
2445        ? ".leaflet-draw-draw-polygon"
2446        : ".leaflet-draw-draw-polyline";
2447      const drawButton = mapContainer.querySelector(selector);
2448
2449      if (
2450        drawButton &&
2451        !drawButton.classList.contains("leaflet-disabled") &&
2452        !drawButton.classList.contains("leaflet-draw-toolbar-button-enabled")
2453      ) {
2454        drawButton.click();
2455      }
2456    }, 0);
2457  }
2458
2459  function startDrawToolForCurrentType() {
2460    if (currentGeometryType === "polyline" || currentGeometryType === "polygon") {
2461      startActiveDrawTool();
2462    }
2463  }
2464
2465  function startEditLayersTool() {
2466    if (!formMap || !polylineLayer) {
2467      return;
2468    }
2469
2470    window.setTimeout(() => {
2471      const mapContainer = formMap.getContainer();
2472      if (!mapContainer) {
2473        return;
2474      }
2475
2476      const editButton = mapContainer.querySelector(".leaflet-draw-edit-edit");
2477      const editIsActive = editButton && editButton.classList.contains("leaflet-draw-toolbar-button-enabled");
2478      const editIsDisabled = editButton && editButton.classList.contains("leaflet-disabled");
2479
2480      if (editButton && !editIsActive && !editIsDisabled) {
2481        editButton.click();
2482      }
2483    }, 0);
2484  }
2485
2486  function cancelActiveDrawTool() {
2487    if (!formMap) {
2488      return;
2489    }
2490
2491    const mapContainer = formMap.getContainer();
2492    if (!mapContainer) {
2493      return;
2494    }
2495
2496    const cancelButton = mapContainer.querySelector(".leaflet-draw-actions a[title='Cancel drawing'], .leaflet-draw-actions a[title='Cancel']");
2497    if (cancelButton) {
2498      cancelButton.click();
2499    }
2500  }
2501
2502  function addPolylineControl() {
2503    if (polylineDrawControl && formMap && !polylineDrawControl._map) {
2504      formMap.addControl(polylineDrawControl);
2505    }
2506  }
2507
2508  function removePolylineControl() {
2509    if (polylineDrawControl && formMap && polylineDrawControl._map) {
2510      formMap.removeControl(polylineDrawControl);
2511    }
2512  }
2513
2514  function setPolylineLayer(layer) {
2515    if (!polylineDrawnItems || !layer) {
2516      return;
2517    }
2518
2519    clearPolyline();
2520    polylineLayer = layer;
2521    if (polylineLayer.setStyle) {
2522      polylineLayer.setStyle(currentGeometryType === "polygon" ? getPolygonStyle() : getPolylineStyle());
2523    }
2524    polylineDrawnItems.addLayer(polylineLayer);
2525    syncPolylineGeometryFields();
2526  }
2527
2528  function clearPolyline() {
2529    if (polylineDrawnItems) {
2530      polylineDrawnItems.clearLayers();
2531    }
2532    polylineLayer = null;
2533  }
2534
2535  function setPolylineGeometry(geometry) {
2536    if (!geometry || !Array.isArray(geometry.coordinates) || (geometry.type !== "LineString" && geometry.type !== "Polygon")) {
2537      clearPolyline();
2538      syncPolylineGeometryFields();
2539      return;
2540    }
2541
2542    const rawCoordinates = geometry.type === "Polygon" && Array.isArray(geometry.coordinates[0])
2543      ? geometry.coordinates[0]
2544      : geometry.coordinates;
2545    const latLngs = rawCoordinates
2546      .filter((coordinate) => Array.isArray(coordinate) && coordinate.length >= 2)
2547      .map((coordinate) => L.latLng(Number(coordinate[1]), Number(coordinate[0])));
2548
2549    if (latLngs.length < 2 || (geometry.type === "Polygon" && latLngs.length < 4)) {
2550      clearPolyline();
2551      syncPolylineGeometryFields();
2552      return;
2553    }
2554
2555    setGeometryType(geometry.type === "Polygon" ? "polygon" : "polyline", { autoStartDrawing: false });
2556    setPolylineLayer(geometry.type === "Polygon" ? L.polygon(latLngs, getPolygonStyle()) : L.polyline(latLngs, getPolylineStyle()));
2557    fitPolylineBounds();
2558  }
2559
2560  function fitPolylineBounds() {
2561    if (!formMap || !polylineLayer || !polylineLayer.getBounds) {
2562      return;
2563    }
2564
2565    const bounds = polylineLayer.getBounds();
2566    if (bounds && bounds.isValid()) {
2567      formMap.fitBounds(bounds, { padding: [20, 20] });
2568    }
2569  }
2570
2571  function syncPolylineGeometryFields() {
2572    const geometryField = document.getElementById("oum_location_geometry");
2573
2574    if (!geometryField) {
2575      return;
2576    }
2577
2578    if (currentGeometryType === "point" || !polylineLayer) {
2579      geometryField.value = "";
2580      return;
2581    }
2582
2583    const geometry = polylineLayer.toGeoJSON().geometry;
2584    geometryField.value = JSON.stringify(geometry);
2585
2586    const bounds = polylineLayer.getBounds();
2587    if (bounds && bounds.isValid()) {
2588      setLocationLatLng(bounds.getCenter(), "", true);
2589    }
2590  }
2591
2592  function syncLocationZoomField() {
2593    const zoomField = document.getElementById("oum_location_zoom");
2594    if (!zoomField || !formMap) {
2595      return;
2596    }
2597    zoomField.value = formMap.getZoom();
2598  }
2599
2600  /**
2601   * Check if reverse geocoding should be performed
2602   * Requires both subtitle field and autofill to be enabled
2603   * @returns {boolean}
2604   */
2605  function shouldPerformReverseGeocoding() {
2606    // Check if subtitle field is enabled (use window prefix for safety)
2607    const subtitleEnabled = typeof window.oum_enable_address !== 'undefined' && window.oum_enable_address === 'on';
2608    
2609    // Check if autofill is enabled (use window prefix for safety)
2610    const autofillEnabled = typeof window.oum_enable_address_autofill !== 'undefined' && window.oum_enable_address_autofill === 'on';
2611    
2612    return subtitleEnabled && autofillEnabled;
2613  }
2614
2615  /**
2616   * Format address from Nominatim API response
2617   * @param {Object} addressData - Address data from Nominatim API
2618   * @param {string} displayName - Fallback display name
2619   * @returns {string}
2620   */
2621  function formatAddressFromResponse(addressData, displayName) {
2622    const addressParts = [];
2623    
2624    // Build address from most specific to least specific
2625    if (addressData.house_number) addressParts.push(addressData.house_number);
2626    if (addressData.road) addressParts.push(addressData.road);
2627    if (addressData.suburb) addressParts.push(addressData.suburb);
2628    if (addressData.city || addressData.town || addressData.village) {
2629      addressParts.push(addressData.city || addressData.town || addressData.village);
2630    }
2631    if (addressData.postcode) addressParts.push(addressData.postcode);
2632    if (addressData.country) addressParts.push(addressData.country);
2633    
2634    return addressParts.length > 0 
2635      ? addressParts.join(', ') 
2636      : displayName || 'Address not found';
2637  }
2638
2639  /**
2640   * Reverse geocode coordinates to get address
2641   * Uses OpenStreetMap Nominatim API (free, no API key required)
2642   * @param {number} lat - Latitude
2643   * @param {number} lng - Longitude
2644   * @param {string} [providedAddress] - Optional address to use directly (e.g., from geosearch result)
2645   */
2646  function reverseGeocode(lat, lng, providedAddress) {
2647    // If address is already provided (e.g., from geosearch), use it directly
2648    if (providedAddress && providedAddress.trim()) {
2649      handleAddressResult(providedAddress.trim());
2650      return;
2651    }
2652
2653    // Validate coordinates
2654    if (!OUMUtils.validateCoordinates(lat, lng)) {
2655      console.warn('Open User Map: Invalid coordinates for reverse geocoding');
2656      return;
2657    }
2658
2659    // Use Nominatim reverse geocoding API
2660    const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`;
2661    
2662    fetch(url, {
2663      headers: {
2664        'User-Agent': 'OpenUserMap/1.0' // Required by Nominatim
2665      }
2666    })
2667      .then(response => {
2668        if (!response.ok) {
2669          throw new Error(`HTTP error! status: ${response.status}`);
2670        }
2671        return response.json();
2672      })
2673      .then(data => {
2674        if (data && data.address) {
2675          const address = formatAddressFromResponse(data.address, data.display_name);
2676          handleAddressResult(address);
2677        } else {
2678          console.log('Open User Map: No address found for coordinates:', lat, lng);
2679        }
2680      })
2681      .catch(error => {
2682        console.warn('Open User Map: Reverse geocoding error:', error);
2683      });
2684  }
2685
2686  /**
2687   * Handle the address result - log it and optionally fill subtitle field
2688   * @param {string} address - The formatted address
2689   */
2690  function handleAddressResult(address) {
2691    if (!address || typeof address !== 'string') {
2692      return;
2693    }
2694
2695    console.log('Open User Map: Address:', address);
2696    
2697    // Auto-fill subtitle field if enabled (always update when address changes)
2698    if (shouldPerformReverseGeocoding()) {
2699      const subtitleField = document.getElementById('oum_location_address');
2700      if (subtitleField) {
2701        subtitleField.value = address;
2702      }
2703    }
2704  }
2705
2706  // Helper function to setup custom image layer
2707  function setupCustomImageLayer() {
2708    // Check if we have an image URL and bounds
2709    if (typeof window.oum_custom_image_url !== 'undefined' && window.oum_custom_image_url && 
2710        typeof window.oum_custom_image_bounds !== 'undefined' && window.oum_custom_image_bounds) {
2711      
2712      // Check if the uploaded file is an SVG
2713      const isSVG = window.oum_custom_image_url.toLowerCase().includes('.svg');
2714      
2715      if (isSVG) {
2716        // Handle SVG file - fetch and render as DOM elements
2717        setupSVGFromFile();
2718      } else {
2719        // Handle regular image file
2720        setupImageOverlay();
2721      }
2722    } else {
2723    }
2724  }
2725
2726  // Helper function to setup SVG from uploaded file
2727  function setupSVGFromFile() {
2728    try {
2729      // Get bounds data (now properly handled as object)
2730      const bounds = window.oum_custom_image_bounds;
2731
2732      // Validate bounds
2733      if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' ||
2734          typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' ||
2735          bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') {
2736        console.warn('Open User Map: Invalid or empty bounds data, skipping SVG file layer');
2737        return;
2738      }
2739
2740
2741      // Fetch the SVG file and render it
2742      fetch(window.oum_custom_image_url)
2743        .then(response => response.text())
2744        .then(svgText => {
2745          // Create SVG element from the fetched content
2746          const svgElement = createSVGElement(svgText);
2747          if (!svgElement) {
2748            console.warn('Open User Map: Cannot create SVG layer from file - invalid SVG element');
2749            return;
2750          }
2751
2752          // Create a custom SVG layer
2753          const svgLayer = L.svgOverlay(svgElement, [
2754            [bounds.north, bounds.west], // Southwest corner
2755            [bounds.south, bounds.east]  // Northeast corner
2756          ], {
2757            opacity: 1.0,
2758            interactive: true
2759          });
2760
2761          svgLayer.addTo(formMap);
2762
2763
2764          // Store reference for potential removal
2765          window.oumCustomSVGLayer = svgLayer;
2766
2767          console.log('Open User Map: Custom SVG file layer added successfully');
2768        })
2769        .catch(error => {
2770          console.warn('Open User Map: Error fetching SVG file:', error);
2771        });
2772
2773    } catch (error) {
2774      console.warn('Open User Map: Error setting up custom SVG file layer:', error);
2775    }
2776  }
2777
2778  // Helper function to setup regular image overlay
2779  function setupImageOverlay() {
2780    try {
2781      // Get bounds data (now properly handled as object)
2782      const bounds = window.oum_custom_image_bounds;
2783
2784      // Validate bounds
2785      if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' ||
2786          typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' ||
2787          bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') {
2788        console.warn('Open User Map: Invalid or empty bounds data, skipping image layer');
2789        return;
2790      }
2791
2792
2793      // Create image overlay
2794      const imageOverlay = L.imageOverlay(window.oum_custom_image_url, [
2795        [bounds.north, bounds.west], // Southwest corner
2796        [bounds.south, bounds.east]  // Northeast corner
2797      ], {
2798        opacity: 1.0,
2799        interactive: true
2800      });
2801
2802      imageOverlay.addTo(formMap);
2803
2804
2805      // Store reference for potential removal
2806      window.oumCustomImageLayer = imageOverlay;
2807
2808      console.log('Open User Map: Custom image layer added successfully');
2809
2810    } catch (error) {
2811      console.warn('Open User Map: Error setting up custom image layer:', error);
2812    }
2813  }
2814
2815  // Helper function to create SVG element from text
2816  function createSVGElement(svgText) {
2817    // Create a temporary div to parse the SVG
2818    const tempDiv = document.createElement('div');
2819    tempDiv.innerHTML = svgText;
2820    const svgElement = tempDiv.querySelector('svg');
2821    
2822    if (!svgElement) {
2823      console.warn('Open User Map: No valid SVG element found in SVG text');
2824      return null;
2825    }
2826    
2827    // Preserve the original viewBox if it exists
2828    // If missing, try to create it from width/height attributes
2829    if (!svgElement.getAttribute('viewBox')) {
2830      const width = svgElement.getAttribute('width');
2831      const height = svgElement.getAttribute('height');
2832      if (width && height) {
2833        // Remove units if present (e.g., "1580px" -> "1580")
2834        const widthNum = parseFloat(width);
2835        const heightNum = parseFloat(height);
2836        if (!isNaN(widthNum) && !isNaN(heightNum)) {
2837          svgElement.setAttribute('viewBox', `0 0 ${widthNum} ${heightNum}`);
2838        } else {
2839          svgElement.setAttribute('viewBox', '0 0 1000 1200');
2840        }
2841      } else {
2842        svgElement.setAttribute('viewBox', '0 0 1000 1200');
2843      }
2844    }
2845    
2846    // Ensure the SVG has proper styling for overlay
2847    svgElement.style.width = '100%';
2848    svgElement.style.height = '100%';
2849    svgElement.style.display = 'block';
2850    
2851    // Ensure the SVG fills the entire bounds area to prevent cropping
2852    svgElement.setAttribute('preserveAspectRatio', 'none');
2853    
2854    console.log('Open User Map: SVG element created successfully:', svgElement);
2855    
2856    return svgElement;
2857  }
2858
2859  // Public interface
2860  return {
2861    init: function() {
2862      if (!document.getElementById("mapGetLocation")) {
2863        return;
2864      }
2865      initializeFormMap();
2866    },
2867    setLocation: function(lat, lng) {
2868      setGeometryType("point");
2869      if (!locationMarker) return;
2870      locationMarker.setLatLng([lat, lng]);
2871      if (!markerIsVisible) {
2872        locationMarker.addTo(formMap);
2873        markerIsVisible = true;
2874        window.markerIsVisible = true;
2875      }
2876      setLocationLatLng({lat, lng});
2877    },
2878    clearMarker: function() {
2879      clearMarker();
2880    },
2881    setGeometryType: setGeometryType,
2882    updateLocationTypeCategoryUi: updateLocationTypeCategoryUi,
2883    setPolylineGeometry: setPolylineGeometry,
2884    fitPolylineBounds: fitPolylineBounds,
2885    startDrawToolForCurrentType: startDrawToolForCurrentType,
2886    startEditLayersTool: startEditLayersTool,
2887    clearPolyline: clearPolyline,
2888    syncGeometryFields: syncPolylineGeometryFields,
2889    invalidateSize: function() {
2890      if (formMap) {
2891        formMap.invalidateSize();
2892      }
2893    },
2894    updateMinZoom: function() {
2895      updateMinZoom();
2896    },
2897    getMap: function() {
2898      return formMap;
2899    },
2900    setView: function(lat, lng, zoom) {
2901      if (formMap) {
2902        formMap.setView([lat, lng], zoom);
2903        syncLocationZoomField();
2904      }
2905    },
2906    setViewToMatchMainMap: function(mainMap) {
2907      if (!formMap || !mainMap) return;
2908      
2909      // Invalidate size to ensure accurate measurements
2910      formMap.invalidateSize();
2911      
2912      // Get the main map's bounds (the geographic area currently visible)
2913      const mainBounds = mainMap.getBounds();
2914      const mainCenter = mainMap.getCenter();
2915      const mainZoom = mainMap.getZoom();
2916      
2917      // Get form map container size
2918      const formMapContainer = formMap.getContainer();
2919      const formMapWidth = formMapContainer.offsetWidth || 520;
2920      const formMapHeight = formMapContainer.offsetHeight || 294;
2921      
2922      // Get main map container size
2923      const mainMapContainer = mainMap.getContainer();
2924      const mainMapWidth = mainMapContainer.offsetWidth || 520;
2925      const mainMapHeight = mainMapContainer.offsetHeight || 294;
2926      
2927      // Calculate aspect ratios
2928      const mainAspectRatio = mainMapWidth / mainMapHeight;
2929      const formAspectRatio = formMapWidth / formMapHeight;
2930      const aspectRatioDifference = Math.abs(mainAspectRatio - formAspectRatio);
2931      
2932      let targetZoom;
2933      
2934      // If aspect ratios match, use ratio-based zoom calculation
2935      if (aspectRatioDifference < 0.01) {
2936        // Calculate zoom adjustment based on size ratio
2937        const widthRatio = formMapWidth / mainMapWidth;
2938        const zoomAdjustment = Math.log2(widthRatio);
2939        targetZoom = mainZoom + zoomAdjustment;
2940      } else {
2941        // Different aspect ratios - use getBoundsZoom to fit the main map's bounds
2942        targetZoom = formMap.getBoundsZoom(mainBounds, false);
2943      }
2944      
2945      // Set view with calculated zoom to show the same geographic area
2946      formMap.setView([mainCenter.lat, mainCenter.lng], targetZoom, {
2947        animate: false
2948      });
2949    }
2950  };
2951})();
2952
2953/**
2954 * Form Controller Module - Handles all form-related functionality
2955 */
2956const OUMFormController = (function () {
2957  // Private variables
2958  let isEditMode = false;
2959  let currentLocationId = null;
2960  let selectedFiles = [];
2961
2962  // Private functions
2963  function showFormMessage(type, headline, message, buttonText = null, buttonCallback = null) {
2964    const form = document.getElementById('oum_add_location');
2965    const errorDiv = document.getElementById('oum_add_location_error');
2966    const thankyouDiv = document.getElementById('oum_add_location_thankyou');
2967    
2968    if (!form || !errorDiv || !thankyouDiv) {
2969      console.error('Required form elements not found');
2970      return;
2971    }
2972    
2973    // Hide form and error
2974    form.style.display = 'none';
2975    errorDiv.style.display = 'none';
2976    
2977    // Update thank you message
2978    const headlineEl = thankyouDiv.querySelector('h3');
2979    const messageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text');
2980    const buttonEl = thankyouDiv.querySelector('button');
2981
2982    if (!headlineEl || !messageEl || !buttonEl) {
2983      // Create elements if they don't exist
2984      if (!headlineEl) {
2985        const newHeadline = document.createElement('h3');
2986        thankyouDiv.appendChild(newHeadline);
2987      }
2988      if (!messageEl) {
2989        const newMessage = document.createElement('p');
2990        newMessage.className = 'oum-add-location-thankyou-text';
2991        thankyouDiv.appendChild(newMessage);
2992      }
2993      if (!buttonEl) {
2994        const newButton = document.createElement('button');
2995        thankyouDiv.appendChild(newButton);
2996      }
2997    }
2998
2999    // Get elements again (they should exist now)
3000    const finalHeadlineEl = thankyouDiv.querySelector('h3');
3001    const finalMessageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text');
3002    const finalButtonEl = thankyouDiv.querySelector('button');
3003    
3004    // Add specific class for delete confirmation
3005    thankyouDiv.className = type === 'confirm-delete' ? 'oum-delete-confirmation' : '';
3006    
3007    if (finalHeadlineEl) finalHeadlineEl.textContent = headline || '';
3008    if (finalMessageEl) finalMessageEl.textContent = message || '';
3009    
3010    // Handle button
3011    if (finalButtonEl) {
3012      if (buttonText && buttonCallback) {
3013        finalButtonEl.textContent = buttonText;
3014        finalButtonEl.onclick = buttonCallback;
3015        finalButtonEl.style.display = 'inline-block';
3016      } else {
3017        finalButtonEl.style.display = 'none';
3018      }
3019    }
3020    
3021    thankyouDiv.style.display = 'block';
3022  }
3023
3024  function setupDeleteButton() {
3025    const deleteBtn = document.getElementById('oum_delete_location_btn');
3026    if (deleteBtn) {
3027      deleteBtn.addEventListener('click', function(e) {
3028        e.preventDefault();
3029        
3030        // Show confirmation using the message system
3031        showFormMessage(
3032          'confirm-delete',
3033          oum_custom_strings.delete_location,
3034          oum_custom_strings.delete_location_message,
3035          oum_custom_strings.delete_location_button,
3036          function() {
3037            // Set delete flag
3038            document.getElementById('oum_delete_location').value = 'true';
3039            
3040            // Get the form
3041            const form = document.getElementById('oum_add_location');
3042            const formData = new FormData(form);
3043            formData.append('action', 'oum_add_location_from_frontend');
3044
3045            // Submit via AJAX
3046            jQuery.ajax({
3047              type: 'POST',
3048              url: oum_ajax.ajaxurl,
3049              cache: false,
3050              contentType: false,
3051              processData: false,
3052              data: formData,
3053              success: function(response) {
3054                if (response.success) {
3055                  showFormMessage(
3056                    'success',
3057                    oum_custom_strings.location_deleted,
3058                    oum_custom_strings.delete_success,
3059                    oum_custom_strings.close_and_refresh,
3060                    function() {
3061                      window.location.reload();
3062                    }
3063                  );
3064                } else {
3065                  oumShowError(response.data);
3066                }
3067              },
3068              error: function(XMLHttpRequest, textStatus, errorThrown) {
3069                console.log(errorThrown);
3070                oumShowError([{message: oum_custom_strings.delete_error}]);
3071              }
3072            });
3073          }
3074        );
3075      });
3076    }
3077  }
3078
3079  function setupFormEvents() {
3080    // Event: click on "+ Add Location" button
3081    const addLocationBtn = document.getElementById("open-add-location-overlay");
3082    if (addLocationBtn) {
3083      addLocationBtn.addEventListener("click", handleAddLocationClick);
3084    }
3085
3086    // Event: click on "Edit Location" button
3087    document.addEventListener('click', function(e) {
3088      if (e.target && e.target.classList.contains('edit-location-button')) {
3089        e.preventDefault();
3090        const locationId = e.target.getAttribute('data-post-id');
3091        const location = window.oum_all_locations.find(loc => loc.post_id === locationId);
3092        
3093        if (location) {
3094          resetForm();
3095          openForm(location);
3096        }
3097      }
3098    });
3099
3100    setupCloseEvents();
3101    setupNotificationEvents();
3102    setupMediaEvents();
3103  }
3104
3105  /**
3106   * Request a fresh form nonce so cached pages stay valid.
3107   *
3108   * We refresh when the overlay opens, ensuring the submission
3109   * uses a nonce generated in the current time window.
3110   */
3111  function refreshLocationNonce() {
3112    const formEl = document.getElementById('oum_add_location');
3113    if (!formEl || typeof oum_ajax === 'undefined') {
3114      return Promise.resolve();
3115    }
3116
3117    const nonceField = formEl.querySelector('input[name="oum_location_nonce"]');
3118    if (!nonceField || !oum_ajax.ajaxurl) {
3119      return Promise.resolve();
3120    }
3121
3122    const action = oum_ajax.refresh_nonce_action || 'oum_refresh_location_nonce';
3123
3124    return jQuery.ajax({
3125      type: 'POST',
3126      url: oum_ajax.ajaxurl,
3127      dataType: 'json',
3128      data: { action },
3129    }).done(function(response) {
3130      if (response && response.success && response.data && response.data.nonce) {
3131        nonceField.value = response.data.nonce;
3132      }
3133    }).fail(function() {
3134      // Keep backward compatibility: fall back to the existing nonce if refresh fails.
3135      console.warn('Open User Map: Unable to refresh nonce.');
3136    });
3137  }
3138
3139  function handleAddLocationClick() {
3140    resetForm();
3141    openForm();
3142  }
3143  
3144  function openForm(location = null) {
3145    // Refresh nonce on every open to avoid cached/expired tokens.
3146    refreshLocationNonce();
3147    document.querySelector(".add-location").classList.add("active");
3148    document.body.classList.add("oum-add-location-opened");
3149
3150    setTimeout(function () {
3151      // Initialize map if needed
3152      OUMFormMap.init();
3153      
3154      // Update minZoom after form opens to ensure it's set correctly when fixed bounds are enabled
3155      // Use a small delay to ensure the map container is properly sized
3156      setTimeout(function() {
3157        OUMFormMap.updateMinZoom();
3158      }, 100);
3159
3160      // Apply main map's aspect ratio to form map
3161      const applyAspectRatioAndView = () => {
3162        const mainMapEl = document.querySelector(`#${map_el}`);
3163        if (mainMapEl) {
3164          const mainMap = window.oumMap;
3165          if (mainMap) {
3166            const mainMapContainer = mainMap.getContainer();
3167            const mainMapWidth = mainMapContainer.offsetWidth;
3168            const mainMapHeight = mainMapContainer.offsetHeight;
3169            
3170            if (mainMapWidth && mainMapHeight) {
3171              // Calculate aspect ratio from main map
3172              const aspectRatio = mainMapWidth / mainMapHeight;
3173              
3174              // Apply aspect ratio to form map container
3175              const formMapWrap = document.querySelector('.add-location .location-overlay-content .map-wrap');
3176              if (formMapWrap) {
3177                formMapWrap.style.aspectRatio = aspectRatio.toString();
3178                formMapWrap.style.height = 'auto';
3179              }
3180            }
3181          }
3182        }
3183        
3184        if (location) {
3185          populateForm(location);
3186        } else {
3187          // Set view to match main map's geographic bounds
3188          const mainMapEl = document.querySelector(`#${map_el}`);
3189          if (mainMapEl) {
3190            const mainMap = window.oumMap;
3191            // Use the new method that calculates zoom based on form map size
3192            // This ensures the same geographic area is visible regardless of map size differences
3193            OUMFormMap.setViewToMatchMainMap(mainMap);
3194          }
3195        }
3196      };
3197      
3198      // Wait for container to resize after applying aspect ratio
3199      applyAspectRatioAndView();
3200      requestAnimationFrame(applyAspectRatioAndView);
3201
3202      // Add a separate timeout for invalidateSize to ensure accurate measurements
3203      setTimeout(() => {
3204        OUMFormMap.invalidateSize();
3205        // Re-apply the view after size invalidation to ensure correct zoom
3206        if (location && (location.geometry_type === "polyline" || location.geometry_type === "polygon") && location.geometry) {
3207          OUMFormMap.fitPolylineBounds();
3208        } else if (!location) {
3209          const mainMapEl = document.querySelector(`#${map_el}`);
3210          if (mainMapEl) {
3211            const mainMap = window.oumMap;
3212            OUMFormMap.setViewToMatchMainMap(mainMap);
3213          }
3214          OUMFormMap.startDrawToolForCurrentType();
3215        }
3216      }, 200);
3217    }, 150);
3218  }
3219
3220  function setupCloseEvents() {
3221    const closeBtn = document.getElementById("close-add-location-overlay");
3222    if (!closeBtn) return;
3223
3224    // Close button click
3225    closeBtn.addEventListener("click", closeForm);
3226
3227    // ESC key
3228    document.addEventListener("keydown", function(evt) {
3229      evt = evt || window.event;
3230      if (evt.key === "Escape" && document.getElementById("add-location-overlay").classList.contains("active")) {
3231        closeForm();
3232      }
3233    });
3234
3235    // Backdrop click
3236    document.getElementById("add-location-overlay").addEventListener("click", function(event) {
3237      if (event.target === this) {
3238        closeForm();
3239      }
3240    });
3241  }
3242
3243  function closeForm() {
3244    const addLocationOverlay = document.getElementById("add-location-overlay");
3245    if (addLocationOverlay) {
3246      addLocationOverlay.classList.remove("active");
3247    }
3248
3249    // Stop locate process
3250    if (window.formMap_locate_process) {
3251      window.formMap_locate_process.stop();
3252    }
3253
3254    // Allow body scrolling
3255    document.querySelector("body").classList.remove("oum-add-location-opened");
3256
3257    // Reset form and clear marker
3258    resetForm();
3259    OUMFormMap.clearMarker();
3260  }
3261
3262  function setupNotificationEvents() {
3263    const notificationCheckbox = document.getElementById("oum_location_notification");
3264    if (notificationCheckbox) {
3265      notificationCheckbox.addEventListener("change", function() {
3266        const authorFields = document.getElementById("oum_author");
3267        const nameField = document.getElementById("oum_location_author_name");
3268        const emailField = document.getElementById("oum_location_author_email");
3269
3270        if (this.checked) {
3271          authorFields.classList.add("active");
3272          nameField.required = true;
3273          emailField.required = true;
3274        } else {
3275          authorFields.classList.remove("active");
3276          nameField.required = false;
3277          emailField.required = false;
3278        }
3279      });
3280    }
3281  }
3282
3283  function setupMediaEvents() {
3284    // Image upload
3285    const imageInput = document.getElementById("oum_location_images");
3286    if (imageInput) {
3287      // Let OUMMedia handle the image upload
3288      OUMMedia.initializeImageUpload(imageInput);
3289    }
3290
3291    // Remove image button
3292    const removeImageBtn = document.getElementById("oum_remove_image");
3293    if (removeImageBtn) {
3294      removeImageBtn.addEventListener("click", function() {
3295        document.getElementById("oum_location_images_preview").innerHTML = "";
3296      });
3297    }
3298
3299    // Remove audio button
3300    const removeAudioBtn = document.getElementById("oum_remove_audio");
3301    if (removeAudioBtn) {
3302      removeAudioBtn.addEventListener("click", function() {
3303        const audioInput = document.getElementById("oum_location_audio");
3304        const previewContainer = audioInput.nextElementSibling;
3305        const previewDiv = previewContainer.querySelector('.audio-preview');
3306        
3307        // Clear the file input
3308        audioInput.value = "";
3309        
3310        // Clear the preview
3311        if (previewDiv) {
3312          previewDiv.innerHTML = '';
3313        }
3314        
3315        // Remove active state
3316        previewContainer.classList.remove("active");
3317        
3318        // Set remove flag
3319        document.getElementById("oum_remove_existing_audio").value = "1";
3320      });
3321    }
3322  }
3323
3324  function setupCategoryDropdownIcons() {
3325    // Handle dropdown with icons for marker categories
3326    const categorySelect = document.querySelector('select#oum_marker_icon');
3327    if (categorySelect) {
3328      // Create a custom dropdown wrapper
3329      const wrapper = document.createElement('div');
3330      wrapper.className = 'oum-category-dropdown-wrapper';
3331      wrapper.style.position = 'relative';
3332      
3333      // Insert wrapper before the select
3334      categorySelect.parentNode.insertBefore(wrapper, categorySelect);
3335      wrapper.appendChild(categorySelect);
3336      
3337      // Create custom dropdown display
3338      const display = document.createElement('div');
3339      display.className = 'oum-category-dropdown-display';
3340      
3341      // Create icon element
3342      const icon = document.createElement('img');
3343      icon.className = 'oum-category-dropdown-icon';
3344
3345      const linePreview = document.createElement('span');
3346      linePreview.className = 'oum-category-line-preview';
3347
3348      const areaPreview = document.createElement('span');
3349      areaPreview.className = 'oum-category-area-preview';
3350
3351      const routeIconTemplate = document.getElementById('oum-route-category-icon-template');
3352      const areaIconTemplate = document.getElementById('oum-area-category-icon-template');
3353      if (routeIconTemplate && routeIconTemplate.content) {
3354        linePreview.appendChild(routeIconTemplate.content.cloneNode(true));
3355      }
3356      if (areaIconTemplate && areaIconTemplate.content) {
3357        areaPreview.appendChild(areaIconTemplate.content.cloneNode(true));
3358      }
3359      
3360      // Create text element
3361      const text = document.createElement('span');
3362      text.className = 'oum-category-dropdown-text';
3363      
3364      display.appendChild(icon);
3365      display.appendChild(linePreview);
3366      display.appendChild(areaPreview);
3367      display.appendChild(text);
3368      
3369      // Insert display before select
3370      wrapper.insertBefore(display, categorySelect);
3371      
3372      // Make select invisible but still functional
3373      categorySelect.style.cssText = `
3374        position: absolute;
3375        top: 0;
3376        left: 0;
3377        width: 100%;
3378        height: 100%;
3379        opacity: 0;
3380        cursor: pointer;
3381        z-index: 1;
3382      `;
3383      
3384      // Update display when selection changes
3385      function updateDisplay() {
3386        const selectedOption = categorySelect.options[categorySelect.selectedIndex];
3387        if (selectedOption && selectedOption.value) {
3388          const iconUrl = selectedOption.getAttribute('data-icon');
3389          const categoryColor = selectedOption.getAttribute('data-color') || '#e82c71';
3390          const categoryType = selectedOption.getAttribute('data-category-type') || 'point';
3391
3392          if (categoryType === 'point' && iconUrl) {
3393            icon.src = iconUrl;
3394            icon.style.display = 'block';
3395          } else {
3396            icon.style.display = 'none';
3397          }
3398
3399          linePreview.style.color = categoryColor;
3400          linePreview.style.display = categoryType === 'polyline' ? 'inline-block' : 'none';
3401          areaPreview.style.color = categoryColor;
3402          areaPreview.style.display = categoryType === 'polygon' ? 'inline-block' : 'none';
3403          text.textContent = selectedOption.textContent;
3404        } else {
3405          icon.style.display = 'none';
3406          linePreview.style.color = '';
3407          linePreview.style.display = 'none';
3408          areaPreview.style.color = '';
3409          areaPreview.style.display = 'none';
3410          text.textContent = 'Select a category...';
3411        }
3412      }
3413      
3414      // Initial display update
3415      updateDisplay();
3416      if (typeof OUMFormMap !== "undefined" && OUMFormMap.updateLocationTypeCategoryUi) {
3417        OUMFormMap.updateLocationTypeCategoryUi();
3418        updateDisplay();
3419      }
3420      
3421      // Handle selection change
3422      categorySelect.addEventListener('change', updateDisplay);
3423
3424      
3425      // Make display focusable for accessibility
3426      display.setAttribute('tabindex', '0');
3427      
3428      // Handle keyboard navigation
3429      display.addEventListener('keydown', function(e) {
3430        if (e.key === 'Enter' || e.key === ' ') {
3431          e.preventDefault();
3432          categorySelect.focus();
3433          categorySelect.click();
3434        }
3435      });
3436    }
3437  }
3438
3439  function populateForm(location) {
3440    isEditMode = true;
3441    currentLocationId = location.post_id;
3442
3443    // Add edit-location class
3444    const addLocationEl = document.querySelector(".add-location");
3445    if (addLocationEl) {
3446        addLocationEl.classList.add("edit-location");
3447    }
3448
3449    // Set post_id
3450    const postIdField = document.getElementById("oum_post_id");
3451    if (postIdField) {
3452      postIdField.value = location.post_id;
3453    }
3454
3455    // Basic fields
3456    const titleField = document.getElementById("oum_location_title");
3457    const latField = document.getElementById("oum_location_lat");
3458    const lngField = document.getElementById("oum_location_lng");
3459    const addressField = document.getElementById("oum_location_address");
3460
3461    if (titleField) titleField.value = location.title || "";
3462    if (latField) latField.value = location.lat || "";
3463    if (lngField) lngField.value = location.lng || "";
3464    if (addressField) addressField.value = location.address || "";
3465
3466    const geometryType = location.geometry_type === "polyline" || location.geometry_type === "polygon" ? location.geometry_type : "point";
3467    OUMFormMap.setGeometryType(geometryType, { autoStartDrawing: false });
3468    
3469    // Marker types/categories
3470    if (location.types && Array.isArray(location.types)) {
3471        if (typeof oum_enable_multiple_marker_types !== 'undefined' && oum_enable_multiple_marker_types == 'true') {
3472            // Handle multiple marker types (checkboxes)
3473            const checkboxes = document.querySelectorAll('input[name="oum_marker_icon[]"]');
3474            if (checkboxes.length > 0) {
3475                checkboxes.forEach(checkbox => {
3476                    checkbox.checked = location.types.includes(checkbox.value);
3477                });
3478            }
3479        } else {
3480            // Handle single marker type (select)
3481            const markerSelect = document.querySelector('select#oum_marker_icon');
3482            if (markerSelect) {
3483                markerSelect.value = location.types[0] || '';
3484                
3485                // Update custom dropdown display if it exists
3486                const customDisplay = document.querySelector('.oum-category-dropdown-display');
3487                if (customDisplay) {
3488                    // Trigger the change event to update the display
3489                    markerSelect.dispatchEvent(new Event('change'));
3490                }
3491            }
3492        }
3493    }
3494
3495    // Description
3496    const descriptionField = document.getElementById("oum_location_text");
3497    if (descriptionField) {
3498        descriptionField.value = location.text || "";
3499    }
3500
3501    // Video field
3502    const videoField = document.getElementById("oum_location_video");
3503    if (videoField && location.video) {
3504        videoField.value = location.video;
3505    }
3506
3507    // Handle custom fields
3508    if (location.custom_fields && Array.isArray(location.custom_fields)) {
3509        location.custom_fields.forEach(field => {
3510            if (!field || typeof field.index === 'undefined') return;
3511
3512            if (field.fieldtype === 'checkbox') {
3513                const fieldValues = Array.isArray(field.val) ? field.val : [field.val];
3514                const checkboxes = document.querySelectorAll(
3515                    `input[type="checkbox"][name="oum_location_custom_fields[${field.index}][]"]`
3516                );
3517                
3518                if (checkboxes.length > 0) {
3519                    checkboxes.forEach(checkbox => {
3520                        if (checkbox) {
3521                            const checkboxValue = checkbox.value.trim();
3522                            const normalizedValues = fieldValues.map(val => (val || '').toString().trim());
3523                            checkbox.checked = normalizedValues.includes(checkboxValue);
3524                        }
3525                    });
3526                }
3527            } else if (field.fieldtype === 'radio') {
3528                const radioInputs = document.querySelectorAll(
3529                    `input[type="radio"][name="oum_location_custom_fields[${field.index}]"]`
3530                );
3531                
3532                if (radioInputs.length > 0) {
3533                    radioInputs.forEach(radio => {
3534                        if (radio) {
3535                            radio.checked = radio.value === field.val;
3536                        }
3537                    });
3538                }
3539            } else if (field.fieldtype === 'select') {
3540                // Handle single and multiple select
3541                let input = document.querySelector(`[name="oum_location_custom_fields[${field.index}][]"]`);
3542                const isMultiple = !!input;
3543                if (!input) {
3544                    input = document.querySelector(`[name="oum_location_custom_fields[${field.index}]"]`);
3545                }
3546                if (input && input.tagName === 'SELECT') {
3547                    const values = Array.isArray(field.val) ? field.val.map(v => (v || '').toString().trim()) : [(field.val || '').toString().trim()];
3548                    Array.from(input.options).forEach(opt => {
3549                        opt.selected = values.includes((opt.value || '').toString().trim());
3550                    });
3551                }
3552            } else if (field.fieldtype === 'opening_hours') {
3553                // Handle opening hours field - expect JSON string
3554                try {
3555                    const openingHoursData = JSON.parse(field.val);
3556                    if (openingHoursData && typeof openingHoursData === 'object') {
3557                        // Find the hours input
3558                        const hoursInput = document.querySelector(`input[name="oum_location_custom_fields[${field.index}][hours]"]`);
3559                        const fieldContainer = document.querySelector(`[data-custom-field-index="${field.index}"]`);
3560                        
3561                        // Check if 12-hour format is enabled for this field
3562                        const use12hour = fieldContainer && fieldContainer.dataset.use12hour === '1';
3563                        
3564                        if (hoursInput && typeof OUMOpeningHours !== 'undefined') {
3565                            // Convert JSON back to input format (with 12-hour format if enabled)
3566                            hoursInput.value = OUMOpeningHours.formatForInput(openingHoursData, use12hour);
3567                        }
3568                    }
3569                } catch (e) {
3570                    // Invalid JSON, try to set as raw value
3571                    const input = document.querySelector(`input[name="oum_location_custom_fields[${field.index}][hours]"]`);
3572                    if (input) {
3573                        input.value = field.val || '';
3574                    }
3575                }
3576            } else {
3577                const input = document.querySelector(`[name="oum_location_custom_fields[${field.index}]"]`);
3578                if (input) {
3579                    input.value = field.val || '';
3580                }
3581            }
3582        });
3583    }
3584
3585    // Handle images
3586    if (location.image) {
3587      const imageUrls = location.image.split('|').filter(url => url);
3588      const previewContainer = document.getElementById('oum_location_images_preview');
3589      
3590      // Remove required attribute from image upload field when editing with existing images
3591      const imageInput = document.getElementById('oum_location_images');
3592      if (imageInput && imageUrls.length > 0) {
3593        imageInput.removeAttribute('required');
3594      }
3595      
3596      if (previewContainer) {
3597        previewContainer.innerHTML = '';
3598        
3599        imageUrls.forEach(url => {
3600          if (!url) return;
3601
3602          const previewItem = OUMMedia.createExistingImagePreview(url);
3603          if (!previewItem) {
3604            return;
3605          }
3606
3607          previewItem.style.opacity = "0";
3608          previewItem.style.transform = "scale(0.9)";
3609          previewItem.style.transition = "all 0.3s ease";
3610          previewItem.style.opacity = "1";
3611          previewItem.style.transform = "scale(1)";
3612          previewContainer.appendChild(previewItem);
3613        });
3614      }
3615    }
3616
3617    // Handle audio
3618    if (location.audio) {
3619      OUMMedia.setExistingAudio(location.audio);
3620      document.getElementById("oum_remove_existing_audio").value = "0";
3621    }
3622
3623    // Set map view to location
3624    if (geometryType !== "point" && location.geometry) {
3625      OUMFormMap.setPolylineGeometry(location.geometry);
3626      OUMFormMap.startEditLayersTool();
3627    } else if (location.lat && location.lng) {
3628      const locationZoom = location.zoom ? Number(location.zoom) : 12;
3629      OUMFormMap.setView(location.lat, location.lng, locationZoom);
3630      OUMFormMap.setLocation(location.lat, location.lng);
3631    }
3632
3633    // Sync checkbox validation after form population
3634    OUMCheckboxValidation.syncAllGroups();
3635  }
3636
3637  function resetForm() {
3638    isEditMode = false;
3639    currentLocationId = null;
3640    selectedFiles = [];
3641    window.oumSelectedFiles = [];
3642    
3643    const addLocationEl = document.querySelector(".add-location");
3644    if (addLocationEl) {
3645      addLocationEl.classList.remove("edit-location");
3646    }
3647    
3648    // Reset form and message system
3649    const form = document.getElementById("oum_add_location");
3650    const errorDiv = document.getElementById("oum_add_location_error");
3651    const thankyouDiv = document.getElementById("oum_add_location_thankyou");
3652    
3653    // Reset form if it exists
3654    if (form) {
3655      form.reset();
3656      form.style.display = 'block';
3657    }
3658
3659    if (typeof OUMFormMap !== "undefined") {
3660      const typeField = document.getElementById("oum_geometry_type");
3661      OUMFormMap.setGeometryType(typeField && typeField.value ? typeField.value : "point", { autoStartDrawing: false });
3662      OUMFormMap.clearPolyline();
3663    }
3664
3665    // Stop locate process
3666    if (window.map_locate_process) {
3667      window.map_locate_process.stop();
3668    }
3669
3670    // Reset custom fields
3671    const customFields = document.querySelectorAll('[name^="oum_location_custom_fields"]');
3672    if (customFields) {
3673      customFields.forEach(field => {
3674        if (field.type === 'checkbox' || field.type === 'radio') {
3675          field.checked = false;
3676        } else if (field.type === 'select-one') {
3677          field.selectedIndex = 0;
3678        } else {
3679          field.value = '';
3680        }
3681      });
3682    }
3683
3684    // Reset post_id field
3685    const postIdField = document.getElementById("oum_post_id");
3686    if (postIdField) {
3687      postIdField.value = "";
3688    }
3689
3690    // Reset oum_delete_location field
3691    const deleteLocationField = document.getElementById("oum_delete_location");
3692    if (deleteLocationField) {
3693        deleteLocationField.value = "";
3694    }
3695
3696    // Reset error message if it exists
3697    if (errorDiv) {
3698      errorDiv.style.display = 'none';
3699    }
3700
3701    // Reset thank you message if it exists
3702    if (thankyouDiv) {
3703      thankyouDiv.style.display = 'none';
3704      thankyouDiv.classList.remove('oum-delete-confirmation');
3705    }
3706
3707    // Reset image preview
3708    const previewContainer = document.getElementById("oum_location_images_preview");
3709    if (previewContainer) {
3710      previewContainer.innerHTML = "";
3711    }
3712
3713    // Reset audio preview
3714    const audioInput = document.getElementById("oum_location_audio");
3715    if (audioInput) {
3716      const previewContainer = audioInput.nextElementSibling;
3717      const previewDiv = previewContainer.querySelector('.audio-preview');
3718      
3719      // Clear the file input
3720      audioInput.value = "";
3721      
3722      // Clear the preview
3723      if (previewDiv) {
3724        previewDiv.innerHTML = '';
3725      }
3726      
3727      // Remove active state
3728      previewContainer.classList.remove("active");
3729    }
3730
3731    // Reset hidden fields
3732    const removeExistingImage = document.getElementById("oum_remove_existing_image");
3733    if (removeExistingImage) {
3734      removeExistingImage.value = "0";
3735    }
3736
3737    const removeExistingAudio = document.getElementById("oum_remove_existing_audio");
3738    if (removeExistingAudio) {
3739      removeExistingAudio.value = "0";
3740    }
3741
3742    // Reset author section
3743    const authorSection = document.getElementById("oum_author");
3744    if (authorSection) {
3745      authorSection.classList.remove("active");
3746    }
3747
3748    // Reset name and email fields
3749    const nameField = document.getElementById("oum_location_author_name");
3750    const emailField = document.getElementById("oum_location_author_email");
3751    if (nameField && emailField) {
3752      nameField.required = false;
3753      emailField.required = false;
3754    }
3755  }
3756
3757  // Public interface
3758  return {
3759    init: function() {
3760      setupFormEvents();
3761      setupDeleteButton(); // Add delete button handler
3762      setupCategoryDropdownIcons();
3763    },
3764    showFormMessage: showFormMessage,
3765    openForm: openForm,
3766    closeForm: closeForm,
3767    resetForm: resetForm,
3768    populateForm: populateForm,
3769    isEditMode: function() {
3770      return isEditMode;
3771    },
3772    getCurrentLocationId: function() {
3773      return currentLocationId;
3774    }
3775  };
3776})();
3777
3778// OUMOpeningHours module is now in a separate file: frontend-opening-hours.js
3779// It's loaded as a dependency of this script
3780
3781/**
3782 * Media Module - Handles image upload and preview functionality
3783 */
3784const OUMMedia = (function () {
3785  // Private variables
3786  let selectedFiles = [];
3787  let startX, startY, originalPosition, placeholder;
3788  let isDragging = false;
3789  
3790  // Private functions
3791  function initializeImageUpload(imageInput) {
3792    if (!imageInput) return;
3793
3794    // Add click handler for upload icon label
3795    const uploadLabel = imageInput.parentElement.querySelector(
3796      'label[for="oum_location_images"]'
3797    );
3798    if (uploadLabel) {
3799      uploadLabel.addEventListener("click", function (e) {
3800        e.preventDefault();
3801        imageInput.click();
3802      });
3803    }
3804
3805    // Setup drag and drop handlers
3806    document.addEventListener("mousemove", handleDragMove);
3807    document.addEventListener("mouseup", handleDragEnd);
3808    if (imageInput.parentElement.querySelector(".image-preview-container")) {
3809      imageInput.parentElement.querySelector(".image-preview-container").addEventListener("dragover", handleDragOver);
3810    }
3811
3812    // Setup image input change handler
3813    imageInput.addEventListener("change", handleImageInputChange);
3814  }
3815
3816  function initializeAudioUpload() {
3817    const audioInput = document.getElementById('oum_location_audio');
3818    if (!audioInput) return;
3819
3820    // Add click handler for upload icon label
3821    const uploadLabel = audioInput.parentElement.querySelector(
3822      'label[for="oum_location_audio"]'
3823    );
3824    if (uploadLabel) {
3825      uploadLabel.addEventListener("click", function (e) {
3826        e.preventDefault();
3827        audioInput.click();
3828      });
3829    }
3830
3831    // Setup audio input change handler
3832    audioInput.addEventListener("change", handleAudioInputChange);
3833  }
3834
3835  function handleAudioInputChange(e) {
3836    const audioInput = e.target;
3837    const previewContainer = audioInput.nextElementSibling;
3838    const previewDiv = previewContainer.querySelector('.audio-preview');
3839    
3840    if (audioInput.files && audioInput.files[0]) {
3841      const file = audioInput.files[0];
3842      
3843      previewContainer.classList.add('active');
3844
3845      // Create audio preview element
3846      const audio = document.createElement('audio');
3847      audio.controls = true;
3848      audio.style.width = '100%';
3849      
3850      const source = document.createElement('source');
3851      source.src = URL.createObjectURL(file);
3852      source.type = file.type;
3853      
3854      audio.appendChild(source);
3855      
3856      // Replace existing audio preview if any
3857      previewDiv.innerHTML = '';
3858      previewDiv.appendChild(audio);
3859    }
3860  }
3861
3862  function setExistingAudio(audioUrl) {
3863    if (!audioUrl) return;
3864
3865    const audioInput = document.getElementById('oum_location_audio');
3866    if (!audioInput) return;
3867
3868    const previewContainer = audioInput.nextElementSibling;
3869    const previewDiv = previewContainer.querySelector('.audio-preview');
3870    
3871    previewContainer.classList.add('active');
3872
3873    // Create audio preview element
3874    const audio = document.createElement('audio');
3875    audio.controls = true;
3876    audio.style.width = '100%';
3877    
3878    const source = document.createElement('source');
3879    source.src = audioUrl;
3880    source.type = 'audio/' + audioUrl.split('.').pop();
3881    
3882    audio.appendChild(source);
3883    
3884    // Replace existing audio preview if any
3885    previewDiv.innerHTML = '';
3886    previewDiv.appendChild(audio);
3887  }
3888
3889  function handleImageInputChange(e) {
3890    const imageInput = e.target;
3891    const previewContainer = document.getElementById(
3892      "oum_location_images_preview"
3893    );
3894    const maxFiles = parseInt(imageInput.dataset.maxFiles) || 5;
3895    const maxFileSize = OUMConfig.defaults.media.maxImageSize; // in bytes
3896    
3897    // Convert FileList to Array and store in a variable
3898    const files = Array.prototype.slice.call(e.target.files);
3899    const existingCount = selectedFiles.length;
3900    const totalFiles = existingCount + files.length;
3901    
3902    if (totalFiles > maxFiles) {
3903      alert(
3904        OUMUtils.sprintf(
3905          oum_custom_strings.max_files_exceeded,
3906          maxFiles,
3907          maxFiles - existingCount
3908        )
3909      );
3910    }
3911    
3912    // Process only up to remaining slots
3913    const remainingSlots = maxFiles - existingCount;
3914    const filesToProcess = files.slice(0, remainingSlots);
3915    
3916    // Validate file sizes and collect valid files
3917    const validFiles = [];
3918    const invalidFiles = [];
3919    
3920    filesToProcess.forEach(file => {
3921      if (file.size > maxFileSize) {
3922        invalidFiles.push(file.name);
3923      } else {
3924        validFiles.push(file);
3925      }
3926    });
3927    
3928    // Show error message for invalid files
3929    if (invalidFiles.length > 0) {
3930      const maxSizeMB = Math.round(maxFileSize / (1024 * 1024));
3931      alert(
3932        OUMUtils.sprintf(
3933          oum_custom_strings.max_filesize_exceeded,
3934          maxSizeMB,
3935          invalidFiles.join('\n')
3936        )
3937      );
3938    }
3939    
3940    // Update selected files with only valid ones
3941    selectedFiles = [...selectedFiles, ...validFiles];
3942    
3943    // Create previews for valid files only
3944    createImagePreviews(validFiles, previewContainer);
3945
3946    // Make selectedFiles available globally for the form submission
3947    window.oumSelectedFiles = selectedFiles;
3948  }
3949
3950  function createImagePreviews(files, container) {
3951    files.forEach((file) => {
3952      const reader = new FileReader();
3953      
3954      reader.onload = function (e) {
3955        const previewItem = createPreviewItem(e.target.result, file.name);
3956
3957        // Add the item with a fade-in animation
3958        previewItem.style.opacity = "0";
3959        previewItem.style.transform = "scale(0.9)";
3960        container.appendChild(previewItem);
3961
3962        // Trigger animation after a brief delay
3963        setTimeout(() => {
3964          previewItem.style.transition = "all 0.3s ease";
3965          previewItem.style.opacity = "1";
3966          previewItem.style.transform = "scale(1)";
3967        }, 50);
3968      };
3969
3970      reader.readAsDataURL(file);
3971    });
3972  }
3973
3974  function createPreviewItem(imgSrc, fileName, isExisting) {
3975    const previewItem = document.createElement("div");
3976    previewItem.className = isExisting ? "image-preview-item existing-image" : "image-preview-item";
3977    if (fileName) {
3978      previewItem.dataset.fileName = fileName;
3979    }
3980    if (imgSrc) {
3981      previewItem.dataset.url = imgSrc;
3982    }
3983
3984    const img = document.createElement("img");
3985    img.setAttribute("alt", "Preview");
3986    img.src = imgSrc;
3987
3988    const remove = document.createElement("div");
3989    remove.className = "remove-image";
3990    remove.setAttribute("title", "Remove image");
3991    remove.textContent = "\u00d7";
3992
3993    const handle = document.createElement("div");
3994    handle.className = "drag-handle";
3995    handle.setAttribute("title", "Drag to reorder");
3996    handle.textContent = "\u22ee\u22ee";
3997
3998    previewItem.appendChild(img);
3999    previewItem.appendChild(remove);
4000    previewItem.appendChild(handle);
4001
4002    if (isExisting) {
4003      const hidden = document.createElement("input");
4004      hidden.type = "hidden";
4005      hidden.name = "existing_images[]";
4006      hidden.value = imgSrc;
4007      previewItem.appendChild(hidden);
4008    }
4009    
4010    // Add event listener for remove button
4011    const removeButton = previewItem.querySelector('.remove-image');
4012    if (removeButton) {
4013      removeButton.addEventListener('click', handleRemoveImage);
4014    }
4015    
4016    // Set up drag and drop for existing images
4017    setupDragAndDrop(previewItem);
4018
4019    return previewItem;
4020  }
4021
4022  function handleRemoveImage(e) {
4023    e.preventDefault();
4024    const previewItem = this.closest(".image-preview-item");
4025    
4026    // If it's an existing image, handle removal differently
4027    if (previewItem.classList.contains("existing-image")) {
4028      const imgUrl = previewItem.querySelector("[name='existing_images[]']").value;
4029      const removedImagesInput = document.getElementById("oum_remove_existing_image");
4030      const currentValue = removedImagesInput.value === "0" ? [] : removedImagesInput.value.split('|');
4031      currentValue.push(imgUrl);
4032      removedImagesInput.value = currentValue.join('|');
4033    } else {
4034      // Remove from selectedFiles array if it's a new image
4035      const fileName = previewItem.dataset.fileName;
4036      selectedFiles = selectedFiles.filter(file => file.name !== fileName);
4037      window.oumSelectedFiles = selectedFiles;
4038    }
4039
4040    // Animate and remove the preview item
4041    previewItem.style.transition = "all 0.3s ease";
4042    previewItem.style.transform = "scale(0.8)";
4043    previewItem.style.opacity = "0";
4044    
4045    setTimeout(() => {
4046      previewItem.remove();
4047    }, 300);
4048  }
4049
4050  function setupDragAndDrop(previewItem) {
4051    previewItem.setAttribute('draggable', 'true');
4052    
4053    previewItem.addEventListener('mousedown', function(e) {
4054      if (e.target.classList.contains('remove-image')) return;
4055
4056      isDragging = true;
4057      this.classList.add('dragging');
4058
4059      // Get element dimensions once at start
4060      const rect = this.getBoundingClientRect();
4061      this.style.width = rect.width + 'px';
4062      this.style.height = rect.height + 'px';
4063
4064      // Store initial grid container for safety check
4065      this.initialContainer = this.closest('.oum-image-preview-grid');
4066
4067      // Create placeholder immediately
4068      createPlaceholder(this);
4069      
4070      // Set up dragged element
4071      this.style.position = 'fixed';
4072      this.style.zIndex = '1000';
4073      this.style.opacity = '0.9';
4074      this.style.transform = 'scale(1.05) rotate(1deg)';
4075      this.style.pointerEvents = 'none';
4076      this.style.boxShadow = '0 5px 15px rgba(0,0,0,0.15)';
4077
4078      // Set initial position
4079      moveDraggedElement(this, e);
4080
4081      document.body.style.cursor = 'grabbing';
4082    });
4083    
4084    previewItem.addEventListener('touchstart', handleTouchStart);
4085  }
4086
4087  function createPlaceholder(element) {
4088    placeholder = document.createElement("div");
4089    placeholder.classList.add("image-preview-placeholder");
4090    placeholder.style.width = element.offsetWidth + "px";
4091    placeholder.style.height = element.offsetHeight + "px";
4092    placeholder.style.transition = "transform 0.2s ease";
4093    placeholder.style.border = "2px dashed #e82c71";
4094    placeholder.style.borderRadius = "4px";
4095    placeholder.style.backgroundColor = "rgba(224, 42, 175, 0.05)";
4096    element.parentNode.insertBefore(placeholder, element);
4097  }
4098
4099  function moveDraggedElement(draggable, e) {
4100    const rect = draggable.getBoundingClientRect();
4101    const centerOffsetX = rect.width / 2;
4102    const centerOffsetY = rect.height / 2;
4103    
4104    // Position element directly at cursor with center offset
4105    draggable.style.left = (e.clientX - centerOffsetX) + 'px';
4106    draggable.style.top = (e.clientY - centerOffsetY) + 'px';
4107  }
4108
4109  function updatePlaceholderPosition(e) {
4110    const previewContainer = document.getElementById("oum_location_images_preview");
4111    if (!previewContainer) return;
4112
4113    const draggable = document.querySelector('.dragging');
4114    if (!draggable) return;
4115
4116    const siblings = [...previewContainer.querySelectorAll(".image-preview-item:not(.dragging)")];
4117    
4118    // Find the closest sibling based on mouse position
4119    const closestSibling = siblings.reduce((closest, child) => {
4120      const rect = child.getBoundingClientRect();
4121      const centerX = rect.left + rect.width / 2;
4122      const offset = e.clientX - centerX;
4123      
4124      if (offset < 0 && (!closest.element || offset > closest.offset)) {
4125        return { offset: offset, element: child };
4126      }
4127      return closest;
4128    }, { offset: Number.NEGATIVE_INFINITY, element: null });
4129
4130    if (closestSibling.element) {
4131      previewContainer.insertBefore(placeholder, closestSibling.element);
4132    } else {
4133      previewContainer.appendChild(placeholder);
4134    }
4135  }
4136
4137  function handleDragMove(e) {
4138    if (!isDragging) return;
4139
4140    const draggable = document.querySelector(".dragging");
4141    if (!draggable) return;
4142
4143    // Update dragged element position
4144    moveDraggedElement(draggable, e);
4145    
4146    // Check if cursor is still within any grid container
4147    const gridContainer = document.getElementById("oum_location_images_preview");
4148    if (!gridContainer) return;
4149
4150    const gridRect = gridContainer.getBoundingClientRect();
4151    const isWithinGrid = e.clientX >= gridRect.left - 50 && 
4152                        e.clientX <= gridRect.right + 50 && 
4153                        e.clientY >= gridRect.top - 50 && 
4154                        e.clientY <= gridRect.bottom + 50;
4155
4156    // If cursor is outside grid boundaries, hide placeholder
4157    if (!isWithinGrid && placeholder) {
4158      placeholder.style.display = 'none';
4159    } else if (placeholder) {
4160      placeholder.style.display = 'block';
4161      updatePlaceholderPosition(e);
4162    }
4163  }
4164
4165  function handleDragEnd() {
4166    const draggable = document.querySelector(".dragging");
4167    if (!draggable) return;
4168
4169    // Reset cursor
4170    document.body.style.cursor = "";
4171
4172    // Check if we're still within the grid
4173    const gridContainer = document.getElementById("oum_location_images_preview");
4174    if (!gridContainer) {
4175      // If no grid found, return item to its initial position
4176      if (draggable.initialContainer) {
4177        draggable.initialContainer.appendChild(draggable);
4178      }
4179    } else {
4180      // Place draggable element at placeholder position if within grid
4181      if (placeholder && placeholder.style.display !== 'none') {
4182        draggable.style.transition = "none";
4183        placeholder.parentNode.insertBefore(draggable, placeholder);
4184      } else {
4185        // If placeholder is hidden (outside grid), append to end
4186        gridContainer.appendChild(draggable);
4187      }
4188    }
4189
4190    // Remove placeholder
4191    if (placeholder) {
4192      placeholder.remove();
4193    }
4194
4195    // Reset draggable element styles
4196    draggable.style.position = "";
4197    draggable.style.zIndex = "";
4198    draggable.style.top = "";
4199    draggable.style.left = "";
4200    draggable.style.width = "";
4201    draggable.style.height = "";
4202    draggable.style.transform = "";
4203    draggable.style.pointerEvents = "";
4204    draggable.style.boxShadow = "";
4205    draggable.classList.remove("dragging");
4206
4207    isDragging = false;
4208  }
4209
4210  function handleTouchStart(e) {
4211    const touch = e.touches[0];
4212    const mouseEvent = new MouseEvent("mousedown", {
4213      clientX: touch.clientX,
4214      clientY: touch.clientY
4215    });
4216    this.dispatchEvent(mouseEvent);
4217  }
4218
4219  function handleDragOver(e) {
4220    e.preventDefault();
4221    e.stopPropagation();
4222
4223    const draggable = document.querySelector(".dragging");
4224    if (!draggable) return;
4225
4226    const afterElement = getDragAfterElement(this, e.clientX);
4227    if (afterElement == null) {
4228      this.appendChild(placeholder);
4229    } else {
4230      this.insertBefore(placeholder, afterElement);
4231    }
4232  }
4233
4234  // Public interface
4235  return {
4236    init: function () {
4237      initializeAudioUpload();
4238    },
4239    initializeImageUpload: function(imageInput) {
4240      initializeImageUpload(imageInput);
4241    },
4242    getSelectedFiles: function () {
4243      return selectedFiles;
4244    },
4245    setupDragAndDrop: setupDragAndDrop,
4246    handleRemoveImage: handleRemoveImage,
4247    createExistingImagePreview: function(url) {
4248      if (!url) {
4249        return null;
4250      }
4251      return createPreviewItem(url, '', true);
4252    },
4253    setExistingAudio: setExistingAudio
4254  };
4255})();
4256
4257/**
4258 * Checkbox Validation Module - Handles required checkbox group validation
4259 */
4260const OUMCheckboxValidation = (function () {
4261  // Private functions
4262  function syncRequired(group) {
4263    const anyChecked = group.some(cb => cb.checked);
4264    const first = group[0];
4265    first.required = !anyChecked;
4266  }
4267
4268  function getCheckboxGroups() {
4269    const checkboxGroups = document.querySelectorAll('.oum-checkbox-group.is-required input[type="checkbox"]');
4270    
4271    if (!checkboxGroups.length) return {};
4272
4273    // Group checkboxes by name
4274    const groups = {};
4275    checkboxGroups.forEach(checkbox => {
4276      const name = checkbox.name;
4277      if (!groups[name]) groups[name] = [];
4278      groups[name].push(checkbox);
4279    });
4280
4281    return groups;
4282  }
4283
4284  function processCheckboxGroup(group, setupEvents = true) {
4285    const first = group[0];
4286    
4287    // Only process if first checkbox is required
4288    if (!first.required) return;
4289
4290    // Add event listeners if requested
4291    if (setupEvents) {
4292      group.forEach(cb => cb.addEventListener('change', () => syncRequired(group)));
4293    }
4294    
4295    // Sync the state
4296    syncRequired(group);
4297  }
4298
4299  function initCheckboxGroups() {
4300    const groups = getCheckboxGroups();
4301    
4302    // Process each group with event listeners
4303    Object.keys(groups).forEach(groupName => {
4304      processCheckboxGroup(groups[groupName], true);
4305    });
4306  }
4307
4308  // Public interface
4309  return {
4310    init: function() {
4311      initCheckboxGroups();
4312    },
4313    syncAllGroups: function() {
4314      const groups = getCheckboxGroups();
4315      
4316      // Sync each group without setting up new event listeners
4317      Object.keys(groups).forEach(groupName => {
4318        processCheckboxGroup(groups[groupName], false);
4319      });
4320    }
4321  };
4322})();
4323
4324/**
4325 * Advanced Filter Interface Module - Handles advanced filtering functionality
4326 */
4327const OUMAdvancedFilter = (function () {
4328  // Private variables
4329  let isInitialized = false;
4330  let filterSidebar = null;
4331
4332  // Private functions
4333  function init() {
4334    if (isInitialized) return;
4335
4336    filterSidebar = document.querySelector('.oum-advanced-filter-sidebar');
4337    if (!filterSidebar) {
4338      return;
4339    }
4340    
4341    setupFilterEvents();
4342    setupFloatingPanelToggle();
4343    isInitialized = true;
4344  }
4345
4346  // Setup click toggle for button/panel
4347  function setupFloatingPanelToggle() {
4348    const floatingPanel = document.querySelector('.oum-advanced-filter-button, .oum-advanced-filter-panel');
4349    if (!floatingPanel || !floatingPanel.classList.contains('use-collapse')) {
4350      return;
4351    }
4352
4353    const toggle = floatingPanel.querySelector('.oum-advanced-filter-toggle');
4354    const closeButton = floatingPanel.querySelector('.close-advanced-filter');
4355    const filterWrapper = floatingPanel.closest('.oum-map-filter-wrapper');
4356
4357    // Toggle active class on click
4358    if (toggle) {
4359      toggle.addEventListener('click', function(e) {
4360        e.stopPropagation();
4361        floatingPanel.classList.toggle('active');
4362      });
4363    }
4364
4365    // Close button always collapses the floating panel
4366    if (closeButton) {
4367      closeButton.addEventListener('click', function(e) {
4368        e.stopPropagation();
4369        floatingPanel.classList.remove('active');
4370      });
4371    }
4372
4373    // Close when clicking outside the filter wrapper
4374    if (filterWrapper) {
4375      document.addEventListener('click', function(e) {
4376        if (!filterWrapper.contains(e.target)) {
4377          floatingPanel.classList.remove('active');
4378        }
4379      });
4380    }
4381  }
4382
4383  function setupFilterEvents() {
4384    // Get all filter inputs
4385    const filterInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input');
4386    const filterCheckboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox');
4387    const filterRadios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio');
4388    const filterSelects = filterSidebar.querySelectorAll('.oum-advanced-filter-select');
4389
4390    // Add event listeners to all filter inputs
4391    filterInputs.forEach(input => {
4392      input.addEventListener('input', handleFilterChange);
4393    });
4394
4395    filterCheckboxes.forEach(checkbox => {
4396      checkbox.addEventListener('change', handleFilterChange);
4397    });
4398
4399    filterRadios.forEach(radio => {
4400      radio.addEventListener('change', handleFilterChange);
4401    });
4402
4403    filterSelects.forEach(select => {
4404      select.addEventListener('change', handleFilterChange);
4405    });
4406
4407    // Add reset button functionality
4408    const resetButton = filterSidebar.querySelector('#oum-advanced-filter-reset');
4409    if (resetButton) {
4410      resetButton.addEventListener('click', resetAllFilters);
4411    }
4412  }
4413
4414  function handleFilterChange() {
4415    if (typeof OUMMarkers === 'undefined' || !OUMMarkers.setCustomfieldsFilter) {
4416      console.warn('OUMMarkers not available for advanced filtering');
4417      return;
4418    }
4419
4420    const filterCriteria = getFilterCriteria();
4421    OUMMarkers.setCustomfieldsFilter(filterCriteria);
4422  }
4423
4424  function getFilterCriteria() {
4425    const criteria = {};
4426    if (!filterSidebar) return criteria;
4427
4428    // Get text/email/url inputs
4429    const textInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input');
4430    textInputs.forEach(input => {
4431      const customFieldId = input.dataset.customFieldId;
4432      if (customFieldId && input.value.trim()) {
4433        criteria[customFieldId] = {
4434          type: 'text',
4435          value: input.value.trim().toLowerCase()
4436        };
4437      }
4438    });
4439
4440    // Get checkbox selections
4441    const checkboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox');
4442    checkboxes.forEach(checkbox => {
4443      const customFieldId = checkbox.dataset.customFieldId;
4444      const filterType = checkbox.dataset.filterType;
4445      
4446      if (customFieldId) {
4447        // Check if this is an opening_hours open_now filter
4448        if (filterType === 'opening_hours_open_now') {
4449          if (checkbox.checked) {
4450            criteria[customFieldId] = {
4451              type: 'opening_hours_open_now',
4452              value: 'open'
4453            };
4454          }
4455        } else {
4456          // Standard checkbox handling
4457          if (!criteria[customFieldId]) {
4458            // Get checkbox relation from the fieldset (default to OR for backward compatibility)
4459            const fieldset = checkbox.closest('.oum-checkbox-group');
4460            const relation = fieldset ? (fieldset.dataset.checkboxRelation || 'OR') : 'OR';
4461            criteria[customFieldId] = { type: 'checkbox', values: [], relation: relation };
4462          }
4463          if (checkbox.checked) {
4464            criteria[customFieldId].values.push(checkbox.value);
4465          }
4466        }
4467      }
4468    });
4469
4470    // Get radio selections
4471    const radios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio');
4472    radios.forEach(radio => {
4473      const customFieldId = radio.dataset.customFieldId;
4474      if (customFieldId && radio.checked) {
4475        criteria[customFieldId] = {
4476          type: 'radio',
4477          value: radio.value
4478        };
4479      }
4480    });
4481
4482    // Get select selections
4483    const selects = filterSidebar.querySelectorAll('.oum-advanced-filter-select');
4484    selects.forEach(select => {
4485      const customFieldId = select.dataset.customFieldId;
4486      if (customFieldId && select.value) {
4487        criteria[customFieldId] = {
4488          type: 'select',
4489          value: select.value
4490        };
4491      }
4492    });
4493
4494    // Remove empty criteria (no checkboxes selected, empty text inputs, etc.)
4495    Object.keys(criteria).forEach(key => {
4496      const criterion = criteria[key];
4497      if (criterion.type === 'checkbox' && criterion.values.length === 0) {
4498        delete criteria[key];
4499      }
4500    });
4501
4502    return criteria;
4503  }
4504
4505  function resetAllFilters() {
4506    // Clear all text inputs
4507    const filterInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input');
4508    filterInputs.forEach(input => {
4509      input.value = '';
4510    });
4511
4512    // Uncheck all checkboxes
4513    const filterCheckboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox');
4514    filterCheckboxes.forEach(checkbox => {
4515      checkbox.checked = false;
4516    });
4517
4518    // Uncheck all radio buttons
4519    const filterRadios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio');
4520    filterRadios.forEach(radio => {
4521      radio.checked = false;
4522    });
4523
4524    // Reset all select dropdowns to first option
4525    const filterSelects = filterSidebar.querySelectorAll('.oum-advanced-filter-select');
4526    filterSelects.forEach(select => {
4527      select.selectedIndex = 0;
4528    });
4529
4530    if (typeof OUMMarkers !== 'undefined' && OUMMarkers.setCustomfieldsFilter) {
4531      OUMMarkers.setCustomfieldsFilter(null);
4532    }
4533
4534    // Restore Marker Categories to the default "all checked" state
4535    if (typeof OUMMarkers !== 'undefined' && OUMMarkers.resetCategoriesFilter) {
4536      OUMMarkers.resetCategoriesFilter();
4537    }
4538
4539    resetSearchbar();
4540  }
4541
4542  function resetSearchbar() {
4543    // Live Filter Markers
4544    const liveFilterInput = document.getElementById("oum_filter_markers");
4545    if (liveFilterInput) {
4546      liveFilterInput.value = "";
4547      if (typeof OUMMarkers !== "undefined" && OUMMarkers.updateSearchtextAndCategoriesFilters) {
4548        OUMMarkers.updateSearchtextAndCategoriesFilters();
4549      }
4550    }
4551
4552    // Marker search (leaflet-search)
4553    const markerSearchCancel = document.querySelector("#oum_search_marker .search-cancel");
4554    if (markerSearchCancel) {
4555      markerSearchCancel.click();
4556    } else {
4557      const markerSearchInput = document.querySelector("#oum_search_marker .search-input");
4558      if (markerSearchInput) {
4559        markerSearchInput.value = "";
4560        markerSearchInput.dispatchEvent(new Event("input", { bubbles: true }));
4561        markerSearchInput.dispatchEvent(new Event("keyup", { bubbles: true }));
4562      }
4563    }
4564
4565    // Address search (GeoSearch)
4566    const addressReset = document.querySelector("#oum_search_address button.reset, .oum-searchbar-filter .leaflet-geosearch-bar button.reset");
4567    if (addressReset) {
4568      addressReset.click();
4569    } else {
4570      const addressInput = document.querySelector("#oum_search_address input, .oum-searchbar-filter .leaflet-geosearch-bar input");
4571      if (addressInput) {
4572        addressInput.value = "";
4573      }
4574    }
4575  }
4576
4577  // Public interface
4578  return {
4579    init: init
4580  };
4581})();
4582
4583// Main initialization function
4584function oumInitializeMap() {
4585  // Only proceed if we have a map element
4586  if (!document.getElementById(map_el)) {
4587    console.warn('‼️ Open User Map: Map container missing. Initialization aborted. Disable page caching if applicable, or contact [email protected] for help.');
4588    return;
4589  }
4590  
4591  // Initialize opening hours UI
4592  OUMOpeningHours.init();
4593
4594  // Restore the extended L object
4595  window.L = window.OUMLeaflet.L;
4596
4597  // Initialize map and get instance
4598  const mapInstance = OUMMap.init(map_el);
4599
4600  // Initialize markers
4601  const markersModule = OUMMarkers.init(mapInstance);
4602
4603  // Add markers from the global oum_all_locations
4604  if (
4605    typeof oum_all_locations !== "undefined" &&
4606    Array.isArray(oum_all_locations)
4607  ) {
4608    markersModule.addMarkers(oum_all_locations);
4609  }
4610
4611  // Initialize location form
4612  OUMFormMap.init(mapInstance);
4613
4614  // Initialize form controller
4615  OUMFormController.init();
4616
4617  // Initialize media handling
4618  OUMMedia.init();
4619
4620  // Initialize checkbox validation
4621  OUMCheckboxValidation.init();
4622
4623  // Initialize Advanced Filter Interface
4624  OUMAdvancedFilter.init();
4625
4626  // Setup filter events
4627  const markerFilterInput = document.getElementById("oum_filter_markers");
4628  if (markerFilterInput) {
4629    markerFilterInput.addEventListener("input", OUMMarkers.updateSearchtextAndCategoriesFilters);
4630  }
4631
4632  const categoryInputs = document.querySelectorAll(
4633    '.open-user-map .oum-marker-categories-filter [name="type"]'
4634  );
4635  if (categoryInputs.length > 0) {
4636    categoryInputs.forEach((input) => {
4637      input.addEventListener("change", OUMMarkers.updateSearchtextAndCategoriesFilters);
4638    });
4639  }
4640
4641  // Execute custom JS from settings
4642  if (typeof custom_js !== "undefined" && custom_js.snippet) {
4643    try {
4644      // Wrap custom JS execution in a try-catch with proper element existence checks
4645      const wrappedJS = `
4646        try {
4647          if (typeof document !== 'undefined') {
4648            // Defer map2-related code execution
4649            if (${custom_js.snippet.includes("oumMap2")}) {
4650              // Create a MutationObserver to watch for the form map initialization
4651              const observer = new MutationObserver((mutations) => {
4652                if (window.oumMap2) {
4653                  // Execute the custom JS only when oumMap2 is available
4654                  try {
4655                    ${custom_js.snippet}
4656                  } catch (e) {
4657                    console.warn('Custom JS execution error (deferred):', e);
4658                  }
4659                  observer.disconnect();
4660                }
4661              });
4662
4663              // Start observing the document for the form map to be added
4664              observer.observe(document.body, {
4665                childList: true,
4666                subtree: true
4667              });
4668            } else {
4669              // Execute non-map2 related code immediately
4670              ${custom_js.snippet}
4671            }
4672          }
4673        } catch (e) {
4674          console.warn('Custom JS execution error:', e);
4675        }
4676      `;
4677      Function(wrappedJS)();
4678    } catch (error) {
4679      console.warn("Error executing custom JS:", error);
4680    }
4681  }
4682}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.