PageSourceSearch

https://senat.bi/en/wp-content/plugins/open-user-map/src/js/frontend-block-map.js?ver=1.4.51

js senat.bi collected 2026-10-02 11:53:10 UTC 152,609 bytes, 4,665 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  function setupMarkerEvents() {
1300    map.on("popupopen", function (locationBubble) {
1301      const popup = locationBubble.popup;
1302      const marker = popup._source;
1303
1304      const applyBubbleHtml = (html) => {
1305        popup.setContent(html);
1306        if (typeof popup.update === "function") {
1307          popup.update();
1308        }
1309        finalizeMarkerBubbleUI(popup);
1310      };
1311
1312      if (!marker || !marker.options || !marker.options.post_id) {
1313        finalizeMarkerBubbleUI(popup);
1314        return;
1315      }
1316
1317      if (marker._oumBubbleHtmlCache) {
1318        applyBubbleHtml(marker._oumBubbleHtmlCache);
1319        return;
1320      }
1321
1322      marker._oumBubbleFetchId = (marker._oumBubbleFetchId || 0) + 1;
1323      const myFetchId = marker._oumBubbleFetchId;
1324      const postId = marker.options.post_id;
1325
1326      popup.setContent(oumBubbleLoadingHtml());
1327      if (typeof popup.update === "function") {
1328        popup.update();
1329      }
1330      finalizeMarkerBubbleUI(popup);
1331
1332      if (
1333        typeof jQuery === "undefined" ||
1334        !oum_ajax ||
1335        !oum_ajax.ajaxurl
1336      ) {
1337        applyBubbleHtml(oumBubbleErrorHtml());
1338        return;
1339      }
1340
1341      jQuery.ajax({
1342        type: "POST",
1343        url: oum_ajax.ajaxurl,
1344        dataType: "json",
1345        data: {
1346          action: oum_ajax.bubble_action || "oum_get_location_bubble",
1347          post_id: postId,
1348        },
1349        success: function (response) {
1350          if (myFetchId !== marker._oumBubbleFetchId) {
1351            return;
1352          }
1353          if (response && response.success && response.data && response.data.html) {
1354            marker._oumBubbleHtmlCache = response.data.html;
1355            applyBubbleHtml(response.data.html);
1356          } else {
1357            applyBubbleHtml(oumBubbleErrorHtml());
1358          }
1359        },
1360        error: function () {
1361          if (myFetchId !== marker._oumBubbleFetchId) {
1362            return;
1363          }
1364          applyBubbleHtml(oumBubbleErrorHtml());
1365        },
1366      });
1367    });
1368
1369    // Event: Close Location Bubble
1370    map.on("popupclose", function () {
1371      const el = document.querySelector(
1372        ".open-user-map #location-fullscreen-container"
1373      );
1374      if (el) {
1375        // Clear the content to stop any media playback (YouTube, Vimeo, etc.)
1376        const locationContentWrap = el.querySelector(".location-content-wrap");
1377        if (locationContentWrap) {
1378          locationContentWrap.innerHTML = '';
1379        }
1380        el.classList.remove("visible");
1381      }
1382      document.querySelector("body").classList.remove("oum-location-opened");
1383    });
1384  }
1385  
1386  /**
1387   * Check edit permission for a location and inject edit button if allowed
1388   * This is called when a popup opens to dynamically show edit buttons
1389   * based on real-time permissions (cache-safe solution)
1390   * 
1391   * @param {HTMLElement} container - The container element to search for placeholder
1392   */
1393  function checkEditPermissionAndInjectButton(container) {
1394    // Find the edit button placeholder in the opened popup
1395    const placeholder = container.querySelector('.edit-location-button-placeholder');
1396    
1397    if (!placeholder) {
1398      return; // No placeholder found, nothing to do
1399    }
1400    
1401    const postId = placeholder.getAttribute('data-post-id');
1402    
1403    if (!postId) {
1404      placeholder.remove();
1405      return;
1406    }
1407    
1408    if (typeof jQuery === 'undefined') {
1409      placeholder.remove();
1410      return;
1411    }
1412    
1413    if (!oum_ajax || !oum_ajax.ajaxurl) {
1414      placeholder.remove();
1415      return;
1416    }
1417    
1418    // Make AJAX call to check permissions
1419    jQuery.ajax({
1420      type: 'POST',
1421      url: oum_ajax.ajaxurl,
1422      dataType: 'json',
1423      data: {
1424        action: 'oum_check_edit_permission',
1425        post_id: postId
1426      },
1427      success: function(response) {
1428        if (response && response.success && response.data && response.data.can_edit) {
1429          // User can edit - inject the edit button
1430          const editButton = document.createElement('div');
1431          editButton.className = 'edit-location-button';
1432          editButton.setAttribute('data-post-id', postId);
1433          editButton.setAttribute('title', oum_custom_strings && oum_custom_strings.edit_location ? oum_custom_strings.edit_location : 'Edit location');
1434          
1435          // Replace placeholder with actual button
1436          placeholder.parentNode.replaceChild(editButton, placeholder);
1437        } else {
1438          // User cannot edit - remove placeholder (keep clean HTML)
1439          placeholder.remove();
1440        }
1441      },
1442      error: function(xhr, status, error) {
1443        // On error, just remove the placeholder
1444        placeholder.remove();
1445      }
1446    });
1447  }
1448
1449  function applyFilters() {
1450    if (!markersLayer) {
1451      return;
1452    }
1453
1454    markersLayer.clearLayers();
1455    if (vectorLayer) {
1456      vectorLayer.clearLayers();
1457    }
1458    const filteredMarkers = [];
1459
1460    allMarkers.forEach((marker) => {
1461      if (
1462        markerMatchesSearchtextFilter(marker) &&
1463        markerMatchesCategoriesFilter(marker) &&
1464        markerMatchesCustomfieldsFilter(marker)
1465      ) {
1466        filteredMarkers.push(marker);
1467      }
1468    });
1469
1470    visibleMarkersCount = filteredMarkers.length;
1471    addMarkersToLayer(filteredMarkers);
1472
1473    if (oum_enable_cluster && typeof markersLayer.refreshClusters === "function") {
1474      markersLayer.refreshClusters();
1475    }
1476  }
1477
1478  function addMarkersToLayer(markers) {
1479    if (!markersLayer || !markers.length) {
1480      return;
1481    }
1482
1483    const pointMarkers = [];
1484    const vectorMarkers = [];
1485
1486    markers.forEach((marker) => {
1487      if (marker instanceof L.Marker) {
1488        pointMarkers.push(marker);
1489      } else {
1490        vectorMarkers.push(marker);
1491      }
1492    });
1493
1494    vectorMarkers.forEach((marker) => {
1495      if (vectorLayer) {
1496        vectorLayer.addLayer(marker);
1497      }
1498    });
1499
1500    if (!pointMarkers.length) {
1501      return;
1502    }
1503
1504    // Batch addLayers() sometimes draws no marker when only one exists; one addLayer() avoids that bug.
1505    if (
1506      oum_enable_cluster &&
1507      pointMarkers.length === 1 &&
1508      typeof markersLayer.addLayer === "function"
1509    ) {
1510      markersLayer.addLayer(pointMarkers[0]);
1511      return;
1512    }
1513
1514    // Batch marker insertion when supported to improve large dataset performance.
1515    if (typeof markersLayer.addLayers === "function") {
1516      markersLayer.addLayers(pointMarkers);
1517      return;
1518    }
1519
1520    pointMarkers.forEach((marker) => {
1521      markersLayer.addLayer(marker);
1522    });
1523  }
1524
1525  function markerMatchesSearchtextFilter(marker) {
1526    if (!searchtextFilterValue) {
1527      return true;
1528    }
1529    return marker.options.content.toLowerCase().includes(searchtextFilterValue);
1530  }
1531
1532  function markerMatchesCategoriesFilter(marker) {
1533    const markerTypes = marker.options.types || [];
1534
1535    if (!markerTypes.length) {
1536      return true;
1537    }
1538
1539    // Before the user interacts with the category checkboxes, keep the previous behaviour (show all)
1540    if (categoriesFilterSelection === null) {
1541      return true;
1542    }
1543
1544    if (!categoriesFilterSelection.size) {
1545      return false;
1546    }
1547
1548    return markerTypes.some((type) => categoriesFilterSelection.has(type));
1549  }
1550
1551  function markerMatchesCustomfieldsFilter(marker) {
1552    if (!customfieldsFilterSelection || !Object.keys(customfieldsFilterSelection).length) {
1553      return true;
1554    }
1555
1556    const locationData = getLocationData(marker.options.post_id);
1557    if (!locationData) {
1558      return true;
1559    }
1560
1561    for (const [customFieldId, criterion] of Object.entries(customfieldsFilterSelection)) {
1562      // Special handling for opening_hours_open_now filter type
1563      if (criterion.type === 'opening_hours_open_now') {
1564        const field = locationData.custom_fields?.find(
1565          (customField) => String(customField.index) === String(customFieldId)
1566        );
1567        
1568        // Use pre-calculated open_now value from PHP
1569        if (field && typeof field.open_now === 'boolean') {
1570          if (field.open_now !== true) {
1571            return false;
1572          }
1573        } else {
1574          // Fallback: if open_now not available, exclude location
1575          return false;
1576        }
1577        continue;
1578      }
1579
1580      // Standard custom field filtering
1581      const fieldValue = getCustomFieldValue(locationData, customFieldId);
1582      if (!matchesCriterion(fieldValue, criterion)) {
1583        return false;
1584      }
1585    }
1586
1587    return true;
1588  }
1589
1590  function getLocationData(postId) {
1591    if (!postId) {
1592      return null;
1593    }
1594
1595    if (locationsById[String(postId)]) {
1596      return locationsById[String(postId)];
1597    }
1598
1599    if (typeof window !== "undefined" && Array.isArray(window.oum_all_locations)) {
1600      const fallback = window.oum_all_locations.find(
1601        (location) => String(location.post_id) === String(postId)
1602      );
1603      if (fallback) {
1604        locationsById[String(postId)] = fallback;
1605        return fallback;
1606      }
1607    }
1608
1609    return null;
1610  }
1611
1612  function getCustomFieldValue(locationData, customFieldId) {
1613    if (!locationData || !Array.isArray(locationData.custom_fields)) {
1614      return null;
1615    }
1616
1617    const field = locationData.custom_fields.find(
1618      (customField) => String(customField.index) === String(customFieldId)
1619    );
1620
1621    return field ? field.val : null;
1622  }
1623
1624  function normalizeFieldValues(fieldValue) {
1625    if (Array.isArray(fieldValue)) {
1626      return fieldValue.map((val) => val.toString()).filter((val) => val !== "");
1627    }
1628
1629    if (typeof fieldValue === "string") {
1630      return fieldValue
1631        .split("|")
1632        .map((val) => val.trim())
1633        .filter((val) => val !== "");
1634    }
1635
1636    if (fieldValue !== null && typeof fieldValue !== "undefined") {
1637      const value = fieldValue.toString();
1638      return value ? [value] : [];
1639    }
1640
1641    return [];
1642  }
1643
1644  function matchesCriterion(fieldValue, criterion) {
1645    if (
1646      fieldValue === null ||
1647      typeof fieldValue === "undefined" ||
1648      (typeof fieldValue === "string" && fieldValue.trim() === "") ||
1649      (Array.isArray(fieldValue) && fieldValue.length === 0)
1650    ) {
1651      return false;
1652    }
1653
1654    switch (criterion.type) {
1655      case "text":
1656        return fieldValue.toString().toLowerCase().includes(criterion.value);
1657      case "checkbox": {
1658        const values = normalizeFieldValues(fieldValue);
1659        const relation = criterion.relation || 'OR'; // Default to OR for backward compatibility
1660        
1661        if (relation === 'AND') {
1662          // AND: All selected checkbox values must be present in the field values
1663          return criterion.values.length > 0 && criterion.values.every((val) => values.includes(val));
1664        } else {
1665          // OR: At least one selected checkbox value must match (default behavior)
1666          return criterion.values.some((val) => values.includes(val));
1667        }
1668      }
1669      case "radio":
1670      case "select": {
1671        const values = normalizeFieldValues(fieldValue);
1672        return values.includes(criterion.value);
1673      }
1674      default:
1675        return true;
1676    }
1677  }
1678
1679  function updateSearchtextAndCategoriesFilters() {
1680    // Read searchtext filter
1681    const markerFilterInput = document.getElementById("oum_filter_markers");
1682    searchtextFilterValue = markerFilterInput ? markerFilterInput.value.toLowerCase() : "";
1683
1684    // Read categories filter
1685    const categoryInputs = document.querySelectorAll(categoryFilterSelector);
1686    if (categoryInputs.length === 0) {
1687      // No category UI on this map (hidden overlay / hide_filterbox) — do not filter by category
1688      categoriesFilterSelection = null;
1689    } else {
1690      categoriesFilterSelection = new Set(
1691        Array.from(categoryInputs)
1692          .filter((input) => input.checked)
1693          .map((input) => input.value)
1694      );
1695    }
1696
1697    applyFilters();
1698  }
1699
1700  function setupFilterListEvents() {
1701    const filterControls = document.querySelector(
1702      ".open-user-map .oum-filter-controls"
1703    );
1704    if (!filterControls) return;
1705
1706    const closeButton = filterControls.querySelector(".close-filter-list");
1707    const toggle = filterControls.querySelector(".oum-filter-toggle");
1708    const filterWrapper = filterControls.closest(".oum-map-filter-wrapper");
1709
1710    // Close button always collapses the filter controls
1711    if (closeButton) {
1712      closeButton.addEventListener("click", function(e) {
1713        e.stopPropagation();
1714        filterControls.classList.remove("active");
1715        // Ensure use-collapse class is present so the toggle button becomes visible
1716        filterControls.classList.add("use-collapse");
1717      });
1718    }
1719
1720    // Setup click toggle for the toggle button (works regardless of initial use-collapse state)
1721    if (toggle) {
1722      toggle.addEventListener("click", function(e) {
1723        e.stopPropagation();
1724        filterControls.classList.toggle("active");
1725      });
1726    }
1727
1728    // Close when clicking outside the filter wrapper (only if use-collapse is present)
1729    if (filterWrapper) {
1730      document.addEventListener("click", function(e) {
1731        if (filterControls.classList.contains("use-collapse") && !filterWrapper.contains(e.target)) {
1732          filterControls.classList.remove("active");
1733        }
1734      });
1735    }
1736  }
1737
1738  // Select all works for both the on-map overlay and the Advanced Filter section
1739  function setupToggleAll() {
1740    const categoryFilter = document.querySelector(
1741      ".open-user-map .oum-marker-categories-filter"
1742    );
1743    if (!categoryFilter) return;
1744
1745    const toggleAllCheckbox = categoryFilter.querySelector(
1746      ".oum-toggle-all-checkbox"
1747    );
1748    const categoryCheckboxes = categoryFilter.querySelectorAll(
1749      '[name="type"]'
1750    );
1751
1752    if (!toggleAllCheckbox || categoryCheckboxes.length === 0) return;
1753
1754    function updateToggleAllState() {
1755      const checkedCount = Array.from(categoryCheckboxes).filter(
1756        (cb) => cb.checked
1757      ).length;
1758      const totalCount = categoryCheckboxes.length;
1759
1760      if (checkedCount === 0) {
1761        toggleAllCheckbox.checked = false;
1762        toggleAllCheckbox.indeterminate = false;
1763      } else if (checkedCount === totalCount) {
1764        toggleAllCheckbox.checked = true;
1765        toggleAllCheckbox.indeterminate = false;
1766      } else {
1767        toggleAllCheckbox.checked = false;
1768        toggleAllCheckbox.indeterminate = true;
1769      }
1770    }
1771
1772    function toggleAllCategories() {
1773      const checkedCount = Array.from(categoryCheckboxes).filter(
1774        (cb) => cb.checked
1775      ).length;
1776      const totalCount = categoryCheckboxes.length;
1777      const shouldCheck = checkedCount < totalCount;
1778
1779      categoryCheckboxes.forEach((checkbox) => {
1780        checkbox.checked = shouldCheck;
1781      });
1782
1783      updateSearchtextAndCategoriesFilters();
1784    }
1785
1786    toggleAllCheckbox.addEventListener("change", toggleAllCategories);
1787
1788    categoryCheckboxes.forEach((checkbox) => {
1789      checkbox.addEventListener("change", updateToggleAllState);
1790    });
1791
1792    updateToggleAllState();
1793  }
1794
1795  function resetCategoriesFilter() {
1796    const categoryInputs = document.querySelectorAll(categoryFilterSelector);
1797    categoryInputs.forEach((input) => {
1798      input.checked = true;
1799    });
1800
1801    const toggleAllCheckbox = document.querySelector(
1802      ".open-user-map .oum-marker-categories-filter .oum-toggle-all-checkbox"
1803    );
1804    if (toggleAllCheckbox) {
1805      toggleAllCheckbox.checked = true;
1806      toggleAllCheckbox.indeterminate = false;
1807    }
1808
1809    updateSearchtextAndCategoriesFilters();
1810  }
1811
1812  function handleAutoOpenMarker(markerId) {
1813    const targetMarker = allMarkers.find((m) => m.options.post_id === markerId);
1814    if (targetMarker) {
1815      const openPopup = typeof oum_hide_location_popup === "undefined" || !oum_hide_location_popup;
1816      if (oum_enable_cluster) {
1817        if (targetMarker instanceof L.Marker && typeof markersLayer.zoomToShowLayer === "function") {
1818          setTimeout(() => {
1819            markersLayer.zoomToShowLayer(targetMarker, () => {
1820              if (openPopup) targetMarker.openPopup();
1821            });
1822          }, 500);
1823        } else if (targetMarker.getBounds) {
1824          map.fitBounds(targetMarker.getBounds(), { padding: [20, 20] });
1825          if (openPopup) targetMarker.openPopup();
1826        }
1827      } else {
1828        if (targetMarker.getLatLng) {
1829          map.setView(targetMarker.getLatLng(), targetMarker.options.zoom);
1830        } else if (targetMarker.getBounds) {
1831          map.fitBounds(targetMarker.getBounds(), { padding: [20, 20] });
1832        }
1833        if (openPopup) targetMarker.openPopup();
1834      }
1835    }
1836  }
1837
1838  // Public interface
1839  return {
1840    init: function (mapInstance) {
1841      map = mapInstance;
1842      initializeMarkersLayer();
1843      markersLayer.addTo(map);
1844      vectorLayer.addTo(map);
1845      setupMarkerEvents();
1846      setupFilterListEvents();
1847      setupToggleAll();
1848
1849      // Make layer globally available (for backward compatibility)
1850      window.oumMarkersLayer = markersLayer;
1851      window.oumAllMarkers = allMarkers;
1852
1853      // Dispatch markers initialized event
1854      document.dispatchEvent(new CustomEvent('oum:markers_initialized', {
1855        detail: {
1856          mapId: map._container.id,
1857          markersLayer: markersLayer
1858        }
1859      }));
1860
1861      return this;
1862    },
1863    addMarkers: function (locations) {
1864      locations.forEach((location) => {
1865        const marker = createLocationLayer(location);
1866        allMarkers.push(marker);
1867        locationsById[String(location.post_id)] = location;
1868      });
1869
1870      // Apply markers once so filters and clustering can batch-add visible markers.
1871      applyFilters();
1872
1873      // After adding all markers, check if we need to auto-open one
1874      const POPUP_MARKER_ID = OUMUtils.getParameterByName("markerid");
1875      if (POPUP_MARKER_ID) {
1876        handleAutoOpenMarker(POPUP_MARKER_ID);
1877      }
1878    },
1879    updateSearchtextAndCategoriesFilters: updateSearchtextAndCategoriesFilters,
1880    resetCategoriesFilter: resetCategoriesFilter,
1881    getMarkersLayer: function () {
1882      return markersLayer;
1883    },
1884    getAllMarkers: function () {
1885      return allMarkers;
1886    },
1887    getAllLocations: function () {
1888      return Object.values(locationsById);
1889    },
1890    setCustomfieldsFilter: function (criteria) {
1891      if (criteria && Object.keys(criteria).length > 0) {
1892        customfieldsFilterSelection = criteria;
1893      } else {
1894        customfieldsFilterSelection = null;
1895      }
1896      applyFilters();
1897    },
1898    getFilterState: function () {
1899      return {
1900        searchtext: searchtextFilterValue,
1901        categories:
1902          categoriesFilterSelection === null
1903            ? null
1904            : Array.from(categoriesFilterSelection),
1905        customfields: customfieldsFilterSelection
1906      };
1907    },
1908    getFilteredMarkersCount: function () {
1909      return visibleMarkersCount;
1910    },
1911    /** Clear lazy-loaded popup HTML so the next open refetches (e.g. after vote count changes). */
1912    invalidateBubbleHtmlCacheForPost: function (postId) {
1913      const id = String(postId);
1914      allMarkers.forEach((m) => {
1915        if (m && m.options && String(m.options.post_id) === id) {
1916          delete m._oumBubbleHtmlCache;
1917          m._oumBubbleFetchId = 0;
1918        }
1919      });
1920    },
1921  };
1922})();
1923
1924/**
1925 * Form Map Module - Handles all map-related functionality for the form
1926 */
1927const OUMFormMap = (function () {
1928  // Private variables
1929  let formMap = null;
1930  let locationMarker = null;
1931  let markerIsVisible = false;
1932  let isAdjusting = false;
1933  let isInitialized = false;
1934  let currentGeometryType = "point";
1935  let polylineDrawnItems = null;
1936  let polylineDrawControl = null;
1937  let polylineLayer = null;
1938
1939  // Private functions
1940  function initializeFormMap() {
1941    if (isInitialized) {
1942      return;
1943    }
1944
1945    formMap = L.map("mapGetLocation", {
1946      attributionControl: false,
1947      gestureHandling: true,
1948      minZoom: 1,
1949      zoomSnap: 0.01, // Allow fractional zoom levels (0.01 steps)
1950      zoomDelta: 1, // Zoom step size for controls
1951      fullscreenControl: oum_enable_fullscreen,
1952      fullscreenControlOptions: {
1953        position: "topleft",
1954      },
1955    });
1956
1957    // Make form map globally available (for backward compatibility)
1958    window.oumMap2 = formMap;
1959
1960    setupTileLayer();
1961    setupControls();
1962    setupMarker();
1963    setupMapEvents();
1964    setupPolylineDrawing();
1965    setupLocationTypeControls();
1966
1967    // Invalidate size to ensure accurate measurements before calculating bounds
1968    formMap.invalidateSize();
1969
1970    // Always apply bounds to prevent showing repeated maps
1971    const boundsToUse = oum_enable_fixed_map_bounds 
1972      ? sharedMapBounds 
1973      : OUMConfig.defaults.map.bounds;
1974
1975    // Set the bounds
1976    formMap.setMaxBounds(boundsToUse);
1977
1978    // Calculate minimum zoom level based on bounds
1979    // When fixed bounds are enabled, use sharedMapBounds for accurate calculation
1980    // This is used both for setting minZoom and in the moveend event handler
1981    let maxVisibleBounds;
1982    if (oum_enable_fixed_map_bounds && sharedMapBounds) {
1983      // Invalidate size first to ensure accurate measurements
1984      formMap.invalidateSize();
1985      maxVisibleBounds = formMap.getBoundsZoom(sharedMapBounds);
1986      // Set minimum zoom level to prevent users from zooming out too far
1987      formMap.setMinZoom(maxVisibleBounds);
1988    } else {
1989      maxVisibleBounds = formMap.getBoundsZoom(boundsToUse);
1990    }
1991
1992    // Add moveend event to enforce bounds
1993    formMap.on("moveend", function () {
1994      if (isAdjusting) return;
1995      isAdjusting = true;
1996
1997      const zoom = formMap.getZoom();
1998
1999      // Only enforce bounds if we're zoomed in beyond the minimum zoom
2000      if (zoom > maxVisibleBounds) {
2001        const currentBounds = formMap.getBounds();
2002        const currentCenter = formMap.getCenter();
2003
2004        let needsAdjustment = false;
2005        let newLat = currentCenter.lat;
2006        let newLng = currentCenter.lng;
2007
2008        // Calculate current viewport dimensions
2009        const viewportHeight = currentBounds.getNorth() - currentBounds.getSouth();
2010        const viewportWidth = currentBounds.getEast() - currentBounds.getWest();
2011
2012        // Check and adjust latitude (north/south)
2013        if (currentBounds.getNorth() > boundsToUse.getNorth()) {
2014          newLat = boundsToUse.getNorth() - viewportHeight / 2;
2015          needsAdjustment = true;
2016        } else if (currentBounds.getSouth() < boundsToUse.getSouth()) {
2017          newLat = boundsToUse.getSouth() + viewportHeight / 2;
2018          needsAdjustment = true;
2019        }
2020
2021        // Check and adjust longitude (east/west)
2022        if (currentBounds.getEast() > boundsToUse.getEast()) {
2023          newLng = boundsToUse.getEast() - viewportWidth / 2;
2024          needsAdjustment = true;
2025        } else if (currentBounds.getWest() < boundsToUse.getWest()) {
2026          newLng = boundsToUse.getWest() + viewportWidth / 2;
2027          needsAdjustment = true;
2028        }
2029
2030        if (needsAdjustment) {
2031          formMap.setView([newLat, newLng], zoom, { animate: false });
2032        }
2033      }
2034
2035      isAdjusting = false;
2036    });
2037
2038    // Keep the hidden zoom input in sync with the current form map zoom.
2039    formMap.on("zoomend", function () {
2040      syncLocationZoomField();
2041    });
2042
2043    // Update minZoom when fixed bounds are enabled
2044    // This prevents users from zooming out too far
2045    updateMinZoom();
2046
2047    isInitialized = true;
2048  }
2049
2050  function updateMinZoom() {
2051    if (!formMap) return;
2052    
2053    // Recalculate and set minZoom when fixed bounds are enabled
2054    // This prevents users from zooming out beyond the defined bounds
2055    if (oum_enable_fixed_map_bounds && sharedMapBounds) {
2056      formMap.invalidateSize();
2057      const maxVisibleBounds = formMap.getBoundsZoom(sharedMapBounds);
2058      formMap.setMinZoom(maxVisibleBounds);
2059    }
2060  }
2061
2062  function setupTileLayer() {
2063    // Default to OpenStreetMap if mapStyle is undefined
2064    const mapStyle = window.mapStyle || 'OpenFreeMap.bright';
2065
2066    window.OUMMapStyles.addBaseLayer(formMap, mapStyle, {
2067      mapboxKey: OUMConfig.getTileProviderKey(),
2068      cartoKey: OUMConfig.getCartoTileProviderKey(),
2069      setupCustomImageLayer: setupCustomImageLayer,
2070      customImageHideTiles: window.oum_custom_image_hide_tiles,
2071      customImageBackgroundColor: window.oum_custom_image_background_color,
2072    });
2073  }
2074
2075  function setupControls() {
2076    // Add searchbar: address
2077    const search = new GeoSearch.GeoSearchControl({
2078      style: "bar",
2079      showMarker: false,
2080      provider: OUMUtils.initGeosearchProvider(),
2081      searchLabel: oum_searchaddress_label,
2082      updateMap: false,
2083    });
2084    formMap.addControl(search);
2085
2086    // Add control: get current location
2087    if (oum_enable_currentlocation) {
2088      window.formMap_locate_process = L.control.locate({
2089        flyTo: true,
2090        showPopup: false,
2091      }).addTo(formMap);
2092    }
2093  }
2094
2095  function setupMarker() {
2096    // Marker Icon
2097    let markerIcon = L.icon({
2098      iconUrl: marker_icon_url,
2099      iconSize: [26, 41],
2100      iconAnchor: [13, 41],
2101      popupAnchor: [0, -25],
2102      shadowUrl: marker_shadow_url,
2103      shadowSize: [41, 41],
2104      shadowAnchor: [13, 41],
2105    });
2106
2107    locationMarker = L.marker([0, 0], {
2108      icon: markerIcon,
2109      draggable: true,
2110    });
2111
2112    // Make marker globally available (for backward compatibility)
2113    window.locationMarker = locationMarker;
2114    window.markerIsVisible = markerIsVisible;
2115
2116    // Event: drag marker
2117    locationMarker.on("dragend", function (e) {
2118      setLocationLatLng(e.target.getLatLng());
2119    });
2120  }
2121
2122  function setupMapEvents() {
2123    // Event: click on map to set marker OR location found
2124    formMap.on("click locationfound", function (e) {
2125      if (currentGeometryType !== "point") {
2126        return;
2127      }
2128
2129      let coords = e.latlng;
2130      locationMarker.setLatLng(coords);
2131
2132      if (!markerIsVisible) {
2133        locationMarker.addTo(formMap);
2134        markerIsVisible = true;
2135        window.markerIsVisible = true;
2136      }
2137
2138      setLocationLatLng(coords);
2139    });
2140
2141    // Event: geosearch success
2142    formMap.on("geosearch/showlocation", handleGeosearchSuccess);
2143  }
2144
2145  function handleGeosearchSuccess(e) {
2146    let coords = e.marker._latlng;
2147    let label = e.location.label;
2148    const searchBar = document.querySelector(`#mapGetLocation .leaflet-geosearch-bar form`);
2149
2150    // Check against fixed map bounds if enabled, otherwise allow any location
2151    let isInBounds = true; // Default to true when fixed bounds is disabled
2152    if (oum_enable_fixed_map_bounds === 'on' && sharedMapBounds) {
2153      // Check if location is within the fixed map bounds (not just current viewport)
2154      isInBounds = sharedMapBounds.contains(coords);
2155    }
2156
2157    // Only restrict movement if fixed map bounds is enabled and location is outside those bounds
2158    if (!isInBounds && oum_enable_fixed_map_bounds === 'on') {
2159      console.log("This search result is out of reach.");
2160      if (searchBar) {
2161        searchBar.style.boxShadow = "0 0 10px rgb(255, 111, 105)";
2162        setTimeout(function () {
2163          searchBar.style.boxShadow = "0 1px 5px rgba(255, 255, 255, 0.65)";
2164        }, 2000);
2165      }
2166    } else {
2167      if (currentGeometryType !== "point") {
2168        handleValidGeosearchResult(e.location);
2169        return;
2170      }
2171
2172      // Location is within fixed bounds (or fixed bounds is disabled) - handle normally
2173      // Set coordinates and address immediately (before map animation) for instant feedback
2174      setLocationLatLng(coords, label);
2175      
2176      // Set marker position
2177      locationMarker.setLatLng(coords);
2178
2179      if (!markerIsVisible) {
2180        locationMarker.addTo(formMap);
2181        markerIsVisible = true;
2182        window.markerIsVisible = true;
2183      }
2184
2185      // Fly to location (animated - happens after address is already set)
2186      handleValidGeosearchResult(e.location);
2187    }
2188  }
2189
2190  function handleValidGeosearchResult(location) {
2191    if (location.bounds !== null) {
2192      formMap.flyToBounds(location.bounds);
2193    } else if (location.raw.mapView) {
2194      formMap.flyToBounds([
2195        [location.raw.mapView.south, location.raw.mapView.west],
2196        [location.raw.mapView.north, location.raw.mapView.east],
2197      ]);
2198    } else {
2199      formMap.flyTo([location.y, location.x], 17);
2200    }
2201  }
2202
2203  function setLocationLatLng(markerLatLng, address, skipReverseGeocode = false) {
2204    document.getElementById("oum_location_lat").value = markerLatLng.lat;
2205    document.getElementById("oum_location_lng").value = markerLatLng.lng;
2206    syncLocationZoomField();
2207    
2208    // Only perform reverse geocoding if both subtitle field and autofill are enabled
2209    if (!skipReverseGeocode && shouldPerformReverseGeocoding()) {
2210      reverseGeocode(markerLatLng.lat, markerLatLng.lng, address);
2211    }
2212  }
2213
2214  function clearMarker() {
2215    if (locationMarker && formMap) {
2216      locationMarker.remove();
2217      markerIsVisible = false;
2218      window.markerIsVisible = false;
2219    }
2220  }
2221
2222  function setupLocationTypeControls() {
2223    const controls = document.querySelectorAll('input[name="oum_location_type_choice"]');
2224    controls.forEach((control) => {
2225      control.addEventListener("change", function () {
2226        if (this.checked) {
2227          setGeometryType(this.value);
2228        }
2229      });
2230    });
2231
2232    const typeField = document.getElementById("oum_geometry_type");
2233    setGeometryType(typeField && typeField.value ? typeField.value : "point", { autoStartDrawing: false });
2234  }
2235
2236  function setupPolylineDrawing() {
2237    if (!L.Control || !L.Control.Draw) {
2238      return;
2239    }
2240
2241    polylineDrawnItems = L.featureGroup().addTo(formMap);
2242    createPolylineDrawControl();
2243    setupPolylineDrawingEvents();
2244  }
2245
2246  function createPolylineDrawControl() {
2247    if (!formMap || !L.Control || !L.Control.Draw) {
2248      return;
2249    }
2250
2251    if (polylineDrawControl && polylineDrawControl._map) {
2252      formMap.removeControl(polylineDrawControl);
2253    }
2254
2255    polylineDrawControl = new L.Control.Draw({
2256      draw: {
2257        polyline: currentGeometryType === "polyline" ? {
2258          shapeOptions: getPolylineStyle(),
2259        } : false,
2260        polygon: currentGeometryType === "polygon" ? {
2261          shapeOptions: getPolygonStyle(),
2262        } : false,
2263        rectangle: false,
2264        circle: false,
2265        circlemarker: false,
2266        marker: false,
2267      },
2268      edit: {
2269        featureGroup: polylineDrawnItems,
2270        remove: true,
2271      },
2272    });
2273  }
2274
2275  function setupPolylineDrawingEvents() {
2276    formMap.on(L.Draw.Event.CREATED, function (event) {
2277      if (currentGeometryType === "point") {
2278        return;
2279      }
2280
2281      setPolylineLayer(event.layer);
2282    });
2283
2284    formMap.on(L.Draw.Event.EDITED, function () {
2285      syncPolylineGeometryFields();
2286    });
2287
2288    formMap.on(L.Draw.Event.DELETED, function () {
2289      polylineLayer = null;
2290      syncPolylineGeometryFields();
2291    });
2292  }
2293
2294  function getPolylineStyle() {
2295    return {
2296      color: "#e82c71",
2297      weight: 4,
2298      opacity: 0.9,
2299    };
2300  }
2301
2302  function getPolygonStyle() {
2303    return {
2304      ...getPolylineStyle(),
2305      fillColor: "#e82c71",
2306      fillOpacity: 0.25,
2307    };
2308  }
2309
2310  function setGeometryType(type, options = {}) {
2311    currentGeometryType = type === "polyline" || type === "polygon" ? type : "point";
2312    const autoStartDrawing = options.autoStartDrawing !== false;
2313
2314    cancelActiveDrawTool();
2315
2316    const addLocationEl = document.querySelector(".add-location");
2317    if (addLocationEl) {
2318      addLocationEl.setAttribute("data-oum-geometry-type", currentGeometryType);
2319    }
2320
2321    const typeField = document.getElementById("oum_geometry_type");
2322    if (typeField) {
2323      typeField.value = currentGeometryType;
2324    }
2325
2326    const choice = document.querySelector(`input[name="oum_location_type_choice"][value="${currentGeometryType}"]`);
2327    if (choice) {
2328      choice.checked = true;
2329    }
2330
2331    document.querySelectorAll("[data-oum-location-type-card]").forEach((card) => {
2332      card.classList.toggle("is-selected", card.getAttribute("data-oum-location-type-card") === currentGeometryType);
2333    });
2334
2335    updateLocationTypeCategoryUi();
2336
2337    if (currentGeometryType === "polyline") {
2338      clearMarker();
2339      createPolylineDrawControl();
2340      addPolylineControl();
2341      if (autoStartDrawing) {
2342        startActiveDrawTool();
2343      }
2344    } else if (currentGeometryType === "polygon") {
2345      clearMarker();
2346      createPolylineDrawControl();
2347      addPolylineControl();
2348      if (autoStartDrawing) {
2349        startActiveDrawTool();
2350      }
2351    } else {
2352      removePolylineControl();
2353      clearPolyline();
2354    }
2355
2356    syncPolylineGeometryFields();
2357  }
2358
2359  function updateLocationTypeCategoryUi() {
2360    const categoryLabelKey = currentGeometryType === "polygon"
2361      ? "polygonLabel"
2362      : (currentGeometryType === "polyline" ? "polylineLabel" : "pointLabel");
2363    const categoryTitles = document.querySelectorAll("[data-oum-category-title]");
2364
2365    categoryTitles.forEach((titleEl) => {
2366      const label = titleEl.dataset[categoryLabelKey] || titleEl.dataset.pointLabel || titleEl.textContent;
2367      titleEl.textContent = label;
2368    });
2369
2370    document.querySelectorAll(".oum-category-dropdown-display").forEach((display) => {
2371      display.setAttribute("data-oum-geometry-type", currentGeometryType);
2372    });
2373
2374    document.querySelectorAll(".oum-marker-category-option").forEach((option) => {
2375      const categoryType = option.getAttribute("data-category-type") || "point";
2376      const isMatchingType = categoryType === currentGeometryType;
2377      const input = option.querySelector('input[name="oum_marker_icon[]"]');
2378
2379      option.hidden = !isMatchingType;
2380
2381      if (input) {
2382        input.disabled = !isMatchingType;
2383        if (!isMatchingType) {
2384          input.checked = false;
2385        }
2386      }
2387    });
2388
2389    document.querySelectorAll('select#oum_marker_icon option[data-category-type]').forEach((option) => {
2390      const categoryType = option.getAttribute("data-category-type") || "point";
2391      const isMatchingType = categoryType === currentGeometryType;
2392
2393      option.hidden = !isMatchingType;
2394      option.disabled = !isMatchingType;
2395      if (!isMatchingType && option.selected) {
2396        option.selected = false;
2397      }
2398    });
2399
2400    document.querySelectorAll("select#oum_marker_icon").forEach((select) => {
2401      const selectedOption = select.options[select.selectedIndex];
2402      if (selectedOption && selectedOption.disabled) {
2403        select.value = "";
2404      }
2405      select.dispatchEvent(new Event("change"));
2406    });
2407  }
2408
2409  function startActiveDrawTool(attempt = 0) {
2410    if (!formMap) {
2411      return;
2412    }
2413
2414    window.setTimeout(() => {
2415      if (!formMap._loaded) {
2416        if (attempt < 10) {
2417          startActiveDrawTool(attempt + 1);
2418        }
2419        return;
2420      }
2421
2422      const mapContainer = formMap.getContainer();
2423      if (!mapContainer) {
2424        return;
2425      }
2426
2427      const selector = currentGeometryType === "polygon"
2428        ? ".leaflet-draw-draw-polygon"
2429        : ".leaflet-draw-draw-polyline";
2430      const drawButton = mapContainer.querySelector(selector);
2431
2432      if (
2433        drawButton &&
2434        !drawButton.classList.contains("leaflet-disabled") &&
2435        !drawButton.classList.contains("leaflet-draw-toolbar-button-enabled")
2436      ) {
2437        drawButton.click();
2438      }
2439    }, 0);
2440  }
2441
2442  function startDrawToolForCurrentType() {
2443    if (currentGeometryType === "polyline" || currentGeometryType === "polygon") {
2444      startActiveDrawTool();
2445    }
2446  }
2447
2448  function startEditLayersTool() {
2449    if (!formMap || !polylineLayer) {
2450      return;
2451    }
2452
2453    window.setTimeout(() => {
2454      const mapContainer = formMap.getContainer();
2455      if (!mapContainer) {
2456        return;
2457      }
2458
2459      const editButton = mapContainer.querySelector(".leaflet-draw-edit-edit");
2460      const editIsActive = editButton && editButton.classList.contains("leaflet-draw-toolbar-button-enabled");
2461      const editIsDisabled = editButton && editButton.classList.contains("leaflet-disabled");
2462
2463      if (editButton && !editIsActive && !editIsDisabled) {
2464        editButton.click();
2465      }
2466    }, 0);
2467  }
2468
2469  function cancelActiveDrawTool() {
2470    if (!formMap) {
2471      return;
2472    }
2473
2474    const mapContainer = formMap.getContainer();
2475    if (!mapContainer) {
2476      return;
2477    }
2478
2479    const cancelButton = mapContainer.querySelector(".leaflet-draw-actions a[title='Cancel drawing'], .leaflet-draw-actions a[title='Cancel']");
2480    if (cancelButton) {
2481      cancelButton.click();
2482    }
2483  }
2484
2485  function addPolylineControl() {
2486    if (polylineDrawControl && formMap && !polylineDrawControl._map) {
2487      formMap.addControl(polylineDrawControl);
2488    }
2489  }
2490
2491  function removePolylineControl() {
2492    if (polylineDrawControl && formMap && polylineDrawControl._map) {
2493      formMap.removeControl(polylineDrawControl);
2494    }
2495  }
2496
2497  function setPolylineLayer(layer) {
2498    if (!polylineDrawnItems || !layer) {
2499      return;
2500    }
2501
2502    clearPolyline();
2503    polylineLayer = layer;
2504    if (polylineLayer.setStyle) {
2505      polylineLayer.setStyle(currentGeometryType === "polygon" ? getPolygonStyle() : getPolylineStyle());
2506    }
2507    polylineDrawnItems.addLayer(polylineLayer);
2508    syncPolylineGeometryFields();
2509  }
2510
2511  function clearPolyline() {
2512    if (polylineDrawnItems) {
2513      polylineDrawnItems.clearLayers();
2514    }
2515    polylineLayer = null;
2516  }
2517
2518  function setPolylineGeometry(geometry) {
2519    if (!geometry || !Array.isArray(geometry.coordinates) || (geometry.type !== "LineString" && geometry.type !== "Polygon")) {
2520      clearPolyline();
2521      syncPolylineGeometryFields();
2522      return;
2523    }
2524
2525    const rawCoordinates = geometry.type === "Polygon" && Array.isArray(geometry.coordinates[0])
2526      ? geometry.coordinates[0]
2527      : geometry.coordinates;
2528    const latLngs = rawCoordinates
2529      .filter((coordinate) => Array.isArray(coordinate) && coordinate.length >= 2)
2530      .map((coordinate) => L.latLng(Number(coordinate[1]), Number(coordinate[0])));
2531
2532    if (latLngs.length < 2 || (geometry.type === "Polygon" && latLngs.length < 4)) {
2533      clearPolyline();
2534      syncPolylineGeometryFields();
2535      return;
2536    }
2537
2538    setGeometryType(geometry.type === "Polygon" ? "polygon" : "polyline", { autoStartDrawing: false });
2539    setPolylineLayer(geometry.type === "Polygon" ? L.polygon(latLngs, getPolygonStyle()) : L.polyline(latLngs, getPolylineStyle()));
2540    fitPolylineBounds();
2541  }
2542
2543  function fitPolylineBounds() {
2544    if (!formMap || !polylineLayer || !polylineLayer.getBounds) {
2545      return;
2546    }
2547
2548    const bounds = polylineLayer.getBounds();
2549    if (bounds && bounds.isValid()) {
2550      formMap.fitBounds(bounds, { padding: [20, 20] });
2551    }
2552  }
2553
2554  function syncPolylineGeometryFields() {
2555    const geometryField = document.getElementById("oum_location_geometry");
2556
2557    if (!geometryField) {
2558      return;
2559    }
2560
2561    if (currentGeometryType === "point" || !polylineLayer) {
2562      geometryField.value = "";
2563      return;
2564    }
2565
2566    const geometry = polylineLayer.toGeoJSON().geometry;
2567    geometryField.value = JSON.stringify(geometry);
2568
2569    const bounds = polylineLayer.getBounds();
2570    if (bounds && bounds.isValid()) {
2571      setLocationLatLng(bounds.getCenter(), "", true);
2572    }
2573  }
2574
2575  function syncLocationZoomField() {
2576    const zoomField = document.getElementById("oum_location_zoom");
2577    if (!zoomField || !formMap) {
2578      return;
2579    }
2580    zoomField.value = formMap.getZoom();
2581  }
2582
2583  /**
2584   * Check if reverse geocoding should be performed
2585   * Requires both subtitle field and autofill to be enabled
2586   * @returns {boolean}
2587   */
2588  function shouldPerformReverseGeocoding() {
2589    // Check if subtitle field is enabled (use window prefix for safety)
2590    const subtitleEnabled = typeof window.oum_enable_address !== 'undefined' && window.oum_enable_address === 'on';
2591    
2592    // Check if autofill is enabled (use window prefix for safety)
2593    const autofillEnabled = typeof window.oum_enable_address_autofill !== 'undefined' && window.oum_enable_address_autofill === 'on';
2594    
2595    return subtitleEnabled && autofillEnabled;
2596  }
2597
2598  /**
2599   * Format address from Nominatim API response
2600   * @param {Object} addressData - Address data from Nominatim API
2601   * @param {string} displayName - Fallback display name
2602   * @returns {string}
2603   */
2604  function formatAddressFromResponse(addressData, displayName) {
2605    const addressParts = [];
2606    
2607    // Build address from most specific to least specific
2608    if (addressData.house_number) addressParts.push(addressData.house_number);
2609    if (addressData.road) addressParts.push(addressData.road);
2610    if (addressData.suburb) addressParts.push(addressData.suburb);
2611    if (addressData.city || addressData.town || addressData.village) {
2612      addressParts.push(addressData.city || addressData.town || addressData.village);
2613    }
2614    if (addressData.postcode) addressParts.push(addressData.postcode);
2615    if (addressData.country) addressParts.push(addressData.country);
2616    
2617    return addressParts.length > 0 
2618      ? addressParts.join(', ') 
2619      : displayName || 'Address not found';
2620  }
2621
2622  /**
2623   * Reverse geocode coordinates to get address
2624   * Uses OpenStreetMap Nominatim API (free, no API key required)
2625   * @param {number} lat - Latitude
2626   * @param {number} lng - Longitude
2627   * @param {string} [providedAddress] - Optional address to use directly (e.g., from geosearch result)
2628   */
2629  function reverseGeocode(lat, lng, providedAddress) {
2630    // If address is already provided (e.g., from geosearch), use it directly
2631    if (providedAddress && providedAddress.trim()) {
2632      handleAddressResult(providedAddress.trim());
2633      return;
2634    }
2635
2636    // Validate coordinates
2637    if (!OUMUtils.validateCoordinates(lat, lng)) {
2638      console.warn('Open User Map: Invalid coordinates for reverse geocoding');
2639      return;
2640    }
2641
2642    // Use Nominatim reverse geocoding API
2643    const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`;
2644    
2645    fetch(url, {
2646      headers: {
2647        'User-Agent': 'OpenUserMap/1.0' // Required by Nominatim
2648      }
2649    })
2650      .then(response => {
2651        if (!response.ok) {
2652          throw new Error(`HTTP error! status: ${response.status}`);
2653        }
2654        return response.json();
2655      })
2656      .then(data => {
2657        if (data && data.address) {
2658          const address = formatAddressFromResponse(data.address, data.display_name);
2659          handleAddressResult(address);
2660        } else {
2661          console.log('Open User Map: No address found for coordinates:', lat, lng);
2662        }
2663      })
2664      .catch(error => {
2665        console.warn('Open User Map: Reverse geocoding error:', error);
2666      });
2667  }
2668
2669  /**
2670   * Handle the address result - log it and optionally fill subtitle field
2671   * @param {string} address - The formatted address
2672   */
2673  function handleAddressResult(address) {
2674    if (!address || typeof address !== 'string') {
2675      return;
2676    }
2677
2678    console.log('Open User Map: Address:', address);
2679    
2680    // Auto-fill subtitle field if enabled (always update when address changes)
2681    if (shouldPerformReverseGeocoding()) {
2682      const subtitleField = document.getElementById('oum_location_address');
2683      if (subtitleField) {
2684        subtitleField.value = address;
2685      }
2686    }
2687  }
2688
2689  // Helper function to setup custom image layer
2690  function setupCustomImageLayer() {
2691    // Check if we have an image URL and bounds
2692    if (typeof window.oum_custom_image_url !== 'undefined' && window.oum_custom_image_url && 
2693        typeof window.oum_custom_image_bounds !== 'undefined' && window.oum_custom_image_bounds) {
2694      
2695      // Check if the uploaded file is an SVG
2696      const isSVG = window.oum_custom_image_url.toLowerCase().includes('.svg');
2697      
2698      if (isSVG) {
2699        // Handle SVG file - fetch and render as DOM elements
2700        setupSVGFromFile();
2701      } else {
2702        // Handle regular image file
2703        setupImageOverlay();
2704      }
2705    } else {
2706    }
2707  }
2708
2709  // Helper function to setup SVG from uploaded file
2710  function setupSVGFromFile() {
2711    try {
2712      // Get bounds data (now properly handled as object)
2713      const bounds = window.oum_custom_image_bounds;
2714
2715      // Validate bounds
2716      if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' ||
2717          typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' ||
2718          bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') {
2719        console.warn('Open User Map: Invalid or empty bounds data, skipping SVG file layer');
2720        return;
2721      }
2722
2723
2724      // Fetch the SVG file and render it
2725      fetch(window.oum_custom_image_url)
2726        .then(response => response.text())
2727        .then(svgText => {
2728          // Create SVG element from the fetched content
2729          const svgElement = createSVGElement(svgText);
2730          if (!svgElement) {
2731            console.warn('Open User Map: Cannot create SVG layer from file - invalid SVG element');
2732            return;
2733          }
2734
2735          // Create a custom SVG layer
2736          const svgLayer = L.svgOverlay(svgElement, [
2737            [bounds.north, bounds.west], // Southwest corner
2738            [bounds.south, bounds.east]  // Northeast corner
2739          ], {
2740            opacity: 1.0,
2741            interactive: true
2742          });
2743
2744          svgLayer.addTo(formMap);
2745
2746
2747          // Store reference for potential removal
2748          window.oumCustomSVGLayer = svgLayer;
2749
2750          console.log('Open User Map: Custom SVG file layer added successfully');
2751        })
2752        .catch(error => {
2753          console.warn('Open User Map: Error fetching SVG file:', error);
2754        });
2755
2756    } catch (error) {
2757      console.warn('Open User Map: Error setting up custom SVG file layer:', error);
2758    }
2759  }
2760
2761  // Helper function to setup regular image overlay
2762  function setupImageOverlay() {
2763    try {
2764      // Get bounds data (now properly handled as object)
2765      const bounds = window.oum_custom_image_bounds;
2766
2767      // Validate bounds
2768      if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' ||
2769          typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' ||
2770          bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') {
2771        console.warn('Open User Map: Invalid or empty bounds data, skipping image layer');
2772        return;
2773      }
2774
2775
2776      // Create image overlay
2777      const imageOverlay = L.imageOverlay(window.oum_custom_image_url, [
2778        [bounds.north, bounds.west], // Southwest corner
2779        [bounds.south, bounds.east]  // Northeast corner
2780      ], {
2781        opacity: 1.0,
2782        interactive: true
2783      });
2784
2785      imageOverlay.addTo(formMap);
2786
2787
2788      // Store reference for potential removal
2789      window.oumCustomImageLayer = imageOverlay;
2790
2791      console.log('Open User Map: Custom image layer added successfully');
2792
2793    } catch (error) {
2794      console.warn('Open User Map: Error setting up custom image layer:', error);
2795    }
2796  }
2797
2798  // Helper function to create SVG element from text
2799  function createSVGElement(svgText) {
2800    // Create a temporary div to parse the SVG
2801    const tempDiv = document.createElement('div');
2802    tempDiv.innerHTML = svgText;
2803    const svgElement = tempDiv.querySelector('svg');
2804    
2805    if (!svgElement) {
2806      console.warn('Open User Map: No valid SVG element found in SVG text');
2807      return null;
2808    }
2809    
2810    // Preserve the original viewBox if it exists
2811    // If missing, try to create it from width/height attributes
2812    if (!svgElement.getAttribute('viewBox')) {
2813      const width = svgElement.getAttribute('width');
2814      const height = svgElement.getAttribute('height');
2815      if (width && height) {
2816        // Remove units if present (e.g., "1580px" -> "1580")
2817        const widthNum = parseFloat(width);
2818        const heightNum = parseFloat(height);
2819        if (!isNaN(widthNum) && !isNaN(heightNum)) {
2820          svgElement.setAttribute('viewBox', `0 0 ${widthNum} ${heightNum}`);
2821        } else {
2822          svgElement.setAttribute('viewBox', '0 0 1000 1200');
2823        }
2824      } else {
2825        svgElement.setAttribute('viewBox', '0 0 1000 1200');
2826      }
2827    }
2828    
2829    // Ensure the SVG has proper styling for overlay
2830    svgElement.style.width = '100%';
2831    svgElement.style.height = '100%';
2832    svgElement.style.display = 'block';
2833    
2834    // Ensure the SVG fills the entire bounds area to prevent cropping
2835    svgElement.setAttribute('preserveAspectRatio', 'none');
2836    
2837    console.log('Open User Map: SVG element created successfully:', svgElement);
2838    
2839    return svgElement;
2840  }
2841
2842  // Public interface
2843  return {
2844    init: function() {
2845      if (!document.getElementById("mapGetLocation")) {
2846        return;
2847      }
2848      initializeFormMap();
2849    },
2850    setLocation: function(lat, lng) {
2851      setGeometryType("point");
2852      if (!locationMarker) return;
2853      locationMarker.setLatLng([lat, lng]);
2854      if (!markerIsVisible) {
2855        locationMarker.addTo(formMap);
2856        markerIsVisible = true;
2857        window.markerIsVisible = true;
2858      }
2859      setLocationLatLng({lat, lng});
2860    },
2861    clearMarker: function() {
2862      clearMarker();
2863    },
2864    setGeometryType: setGeometryType,
2865    updateLocationTypeCategoryUi: updateLocationTypeCategoryUi,
2866    setPolylineGeometry: setPolylineGeometry,
2867    fitPolylineBounds: fitPolylineBounds,
2868    startDrawToolForCurrentType: startDrawToolForCurrentType,
2869    startEditLayersTool: startEditLayersTool,
2870    clearPolyline: clearPolyline,
2871    syncGeometryFields: syncPolylineGeometryFields,
2872    invalidateSize: function() {
2873      if (formMap) {
2874        formMap.invalidateSize();
2875      }
2876    },
2877    updateMinZoom: function() {
2878      updateMinZoom();
2879    },
2880    getMap: function() {
2881      return formMap;
2882    },
2883    setView: function(lat, lng, zoom) {
2884      if (formMap) {
2885        formMap.setView([lat, lng], zoom);
2886        syncLocationZoomField();
2887      }
2888    },
2889    setViewToMatchMainMap: function(mainMap) {
2890      if (!formMap || !mainMap) return;
2891      
2892      // Invalidate size to ensure accurate measurements
2893      formMap.invalidateSize();
2894      
2895      // Get the main map's bounds (the geographic area currently visible)
2896      const mainBounds = mainMap.getBounds();
2897      const mainCenter = mainMap.getCenter();
2898      const mainZoom = mainMap.getZoom();
2899      
2900      // Get form map container size
2901      const formMapContainer = formMap.getContainer();
2902      const formMapWidth = formMapContainer.offsetWidth || 520;
2903      const formMapHeight = formMapContainer.offsetHeight || 294;
2904      
2905      // Get main map container size
2906      const mainMapContainer = mainMap.getContainer();
2907      const mainMapWidth = mainMapContainer.offsetWidth || 520;
2908      const mainMapHeight = mainMapContainer.offsetHeight || 294;
2909      
2910      // Calculate aspect ratios
2911      const mainAspectRatio = mainMapWidth / mainMapHeight;
2912      const formAspectRatio = formMapWidth / formMapHeight;
2913      const aspectRatioDifference = Math.abs(mainAspectRatio - formAspectRatio);
2914      
2915      let targetZoom;
2916      
2917      // If aspect ratios match, use ratio-based zoom calculation
2918      if (aspectRatioDifference < 0.01) {
2919        // Calculate zoom adjustment based on size ratio
2920        const widthRatio = formMapWidth / mainMapWidth;
2921        const zoomAdjustment = Math.log2(widthRatio);
2922        targetZoom = mainZoom + zoomAdjustment;
2923      } else {
2924        // Different aspect ratios - use getBoundsZoom to fit the main map's bounds
2925        targetZoom = formMap.getBoundsZoom(mainBounds, false);
2926      }
2927      
2928      // Set view with calculated zoom to show the same geographic area
2929      formMap.setView([mainCenter.lat, mainCenter.lng], targetZoom, {
2930        animate: false
2931      });
2932    }
2933  };
2934})();
2935
2936/**
2937 * Form Controller Module - Handles all form-related functionality
2938 */
2939const OUMFormController = (function () {
2940  // Private variables
2941  let isEditMode = false;
2942  let currentLocationId = null;
2943  let selectedFiles = [];
2944
2945  // Private functions
2946  function showFormMessage(type, headline, message, buttonText = null, buttonCallback = null) {
2947    const form = document.getElementById('oum_add_location');
2948    const errorDiv = document.getElementById('oum_add_location_error');
2949    const thankyouDiv = document.getElementById('oum_add_location_thankyou');
2950    
2951    if (!form || !errorDiv || !thankyouDiv) {
2952      console.error('Required form elements not found');
2953      return;
2954    }
2955    
2956    // Hide form and error
2957    form.style.display = 'none';
2958    errorDiv.style.display = 'none';
2959    
2960    // Update thank you message
2961    const headlineEl = thankyouDiv.querySelector('h3');
2962    const messageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text');
2963    const buttonEl = thankyouDiv.querySelector('button');
2964
2965    if (!headlineEl || !messageEl || !buttonEl) {
2966      // Create elements if they don't exist
2967      if (!headlineEl) {
2968        const newHeadline = document.createElement('h3');
2969        thankyouDiv.appendChild(newHeadline);
2970      }
2971      if (!messageEl) {
2972        const newMessage = document.createElement('p');
2973        newMessage.className = 'oum-add-location-thankyou-text';
2974        thankyouDiv.appendChild(newMessage);
2975      }
2976      if (!buttonEl) {
2977        const newButton = document.createElement('button');
2978        thankyouDiv.appendChild(newButton);
2979      }
2980    }
2981
2982    // Get elements again (they should exist now)
2983    const finalHeadlineEl = thankyouDiv.querySelector('h3');
2984    const finalMessageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text');
2985    const finalButtonEl = thankyouDiv.querySelector('button');
2986    
2987    // Add specific class for delete confirmation
2988    thankyouDiv.className = type === 'confirm-delete' ? 'oum-delete-confirmation' : '';
2989    
2990    if (finalHeadlineEl) finalHeadlineEl.textContent = headline || '';
2991    if (finalMessageEl) finalMessageEl.textContent = message || '';
2992    
2993    // Handle button
2994    if (finalButtonEl) {
2995      if (buttonText && buttonCallback) {
2996        finalButtonEl.textContent = buttonText;
2997        finalButtonEl.onclick = buttonCallback;
2998        finalButtonEl.style.display = 'inline-block';
2999      } else {
3000        finalButtonEl.style.display = 'none';
3001      }
3002    }
3003    
3004    thankyouDiv.style.display = 'block';
3005  }
3006
3007  function setupDeleteButton() {
3008    const deleteBtn = document.getElementById('oum_delete_location_btn');
3009    if (deleteBtn) {
3010      deleteBtn.addEventListener('click', function(e) {
3011        e.preventDefault();
3012        
3013        // Show confirmation using the message system
3014        showFormMessage(
3015          'confirm-delete',
3016          oum_custom_strings.delete_location,
3017          oum_custom_strings.delete_location_message,
3018          oum_custom_strings.delete_location_button,
3019          function() {
3020            // Set delete flag
3021            document.getElementById('oum_delete_location').value = 'true';
3022            
3023            // Get the form
3024            const form = document.getElementById('oum_add_location');
3025            const formData = new FormData(form);
3026            formData.append('action', 'oum_add_location_from_frontend');
3027
3028            // Submit via AJAX
3029            jQuery.ajax({
3030              type: 'POST',
3031              url: oum_ajax.ajaxurl,
3032              cache: false,
3033              contentType: false,
3034              processData: false,
3035              data: formData,
3036              success: function(response) {
3037                if (response.success) {
3038                  showFormMessage(
3039                    'success',
3040                    oum_custom_strings.location_deleted,
3041                    oum_custom_strings.delete_success,
3042                    oum_custom_strings.close_and_refresh,
3043                    function() {
3044                      window.location.reload();
3045                    }
3046                  );
3047                } else {
3048                  oumShowError(response.data);
3049                }
3050              },
3051              error: function(XMLHttpRequest, textStatus, errorThrown) {
3052                console.log(errorThrown);
3053                oumShowError([{message: oum_custom_strings.delete_error}]);
3054              }
3055            });
3056          }
3057        );
3058      });
3059    }
3060  }
3061
3062  function setupFormEvents() {
3063    // Event: click on "+ Add Location" button
3064    const addLocationBtn = document.getElementById("open-add-location-overlay");
3065    if (addLocationBtn) {
3066      addLocationBtn.addEventListener("click", handleAddLocationClick);
3067    }
3068
3069    // Event: click on "Edit Location" button
3070    document.addEventListener('click', function(e) {
3071      if (e.target && e.target.classList.contains('edit-location-button')) {
3072        e.preventDefault();
3073        const locationId = e.target.getAttribute('data-post-id');
3074        const location = window.oum_all_locations.find(loc => loc.post_id === locationId);
3075        
3076        if (location) {
3077          resetForm();
3078          openForm(location);
3079        }
3080      }
3081    });
3082
3083    setupCloseEvents();
3084    setupNotificationEvents();
3085    setupMediaEvents();
3086  }
3087
3088  /**
3089   * Request a fresh form nonce so cached pages stay valid.
3090   *
3091   * We refresh when the overlay opens, ensuring the submission
3092   * uses a nonce generated in the current time window.
3093   */
3094  function refreshLocationNonce() {
3095    const formEl = document.getElementById('oum_add_location');
3096    if (!formEl || typeof oum_ajax === 'undefined') {
3097      return Promise.resolve();
3098    }
3099
3100    const nonceField = formEl.querySelector('input[name="oum_location_nonce"]');
3101    if (!nonceField || !oum_ajax.ajaxurl) {
3102      return Promise.resolve();
3103    }
3104
3105    const action = oum_ajax.refresh_nonce_action || 'oum_refresh_location_nonce';
3106
3107    return jQuery.ajax({
3108      type: 'POST',
3109      url: oum_ajax.ajaxurl,
3110      dataType: 'json',
3111      data: { action },
3112    }).done(function(response) {
3113      if (response && response.success && response.data && response.data.nonce) {
3114        nonceField.value = response.data.nonce;
3115      }
3116    }).fail(function() {
3117      // Keep backward compatibility: fall back to the existing nonce if refresh fails.
3118      console.warn('Open User Map: Unable to refresh nonce.');
3119    });
3120  }
3121
3122  function handleAddLocationClick() {
3123    resetForm();
3124    openForm();
3125  }
3126  
3127  function openForm(location = null) {
3128    // Refresh nonce on every open to avoid cached/expired tokens.
3129    refreshLocationNonce();
3130    document.querySelector(".add-location").classList.add("active");
3131    document.body.classList.add("oum-add-location-opened");
3132
3133    setTimeout(function () {
3134      // Initialize map if needed
3135      OUMFormMap.init();
3136      
3137      // Update minZoom after form opens to ensure it's set correctly when fixed bounds are enabled
3138      // Use a small delay to ensure the map container is properly sized
3139      setTimeout(function() {
3140        OUMFormMap.updateMinZoom();
3141      }, 100);
3142
3143      // Apply main map's aspect ratio to form map
3144      const applyAspectRatioAndView = () => {
3145        const mainMapEl = document.querySelector(`#${map_el}`);
3146        if (mainMapEl) {
3147          const mainMap = window.oumMap;
3148          if (mainMap) {
3149            const mainMapContainer = mainMap.getContainer();
3150            const mainMapWidth = mainMapContainer.offsetWidth;
3151            const mainMapHeight = mainMapContainer.offsetHeight;
3152            
3153            if (mainMapWidth && mainMapHeight) {
3154              // Calculate aspect ratio from main map
3155              const aspectRatio = mainMapWidth / mainMapHeight;
3156              
3157              // Apply aspect ratio to form map container
3158              const formMapWrap = document.querySelector('.add-location .location-overlay-content .map-wrap');
3159              if (formMapWrap) {
3160                formMapWrap.style.aspectRatio = aspectRatio.toString();
3161                formMapWrap.style.height = 'auto';
3162              }
3163            }
3164          }
3165        }
3166        
3167        if (location) {
3168          populateForm(location);
3169        } else {
3170          // Set view to match main map's geographic bounds
3171          const mainMapEl = document.querySelector(`#${map_el}`);
3172          if (mainMapEl) {
3173            const mainMap = window.oumMap;
3174            // Use the new method that calculates zoom based on form map size
3175            // This ensures the same geographic area is visible regardless of map size differences
3176            OUMFormMap.setViewToMatchMainMap(mainMap);
3177          }
3178        }
3179      };
3180      
3181      // Wait for container to resize after applying aspect ratio
3182      applyAspectRatioAndView();
3183      requestAnimationFrame(applyAspectRatioAndView);
3184
3185      // Add a separate timeout for invalidateSize to ensure accurate measurements
3186      setTimeout(() => {
3187        OUMFormMap.invalidateSize();
3188        // Re-apply the view after size invalidation to ensure correct zoom
3189        if (location && (location.geometry_type === "polyline" || location.geometry_type === "polygon") && location.geometry) {
3190          OUMFormMap.fitPolylineBounds();
3191        } else if (!location) {
3192          const mainMapEl = document.querySelector(`#${map_el}`);
3193          if (mainMapEl) {
3194            const mainMap = window.oumMap;
3195            OUMFormMap.setViewToMatchMainMap(mainMap);
3196          }
3197          OUMFormMap.startDrawToolForCurrentType();
3198        }
3199      }, 200);
3200    }, 150);
3201  }
3202
3203  function setupCloseEvents() {
3204    const closeBtn = document.getElementById("close-add-location-overlay");
3205    if (!closeBtn) return;
3206
3207    // Close button click
3208    closeBtn.addEventListener("click", closeForm);
3209
3210    // ESC key
3211    document.addEventListener("keydown", function(evt) {
3212      evt = evt || window.event;
3213      if (evt.key === "Escape" && document.getElementById("add-location-overlay").classList.contains("active")) {
3214        closeForm();
3215      }
3216    });
3217
3218    // Backdrop click
3219    document.getElementById("add-location-overlay").addEventListener("click", function(event) {
3220      if (event.target === this) {
3221        closeForm();
3222      }
3223    });
3224  }
3225
3226  function closeForm() {
3227    const addLocationOverlay = document.getElementById("add-location-overlay");
3228    if (addLocationOverlay) {
3229      addLocationOverlay.classList.remove("active");
3230    }
3231
3232    // Stop locate process
3233    if (window.formMap_locate_process) {
3234      window.formMap_locate_process.stop();
3235    }
3236
3237    // Allow body scrolling
3238    document.querySelector("body").classList.remove("oum-add-location-opened");
3239
3240    // Reset form and clear marker
3241    resetForm();
3242    OUMFormMap.clearMarker();
3243  }
3244
3245  function setupNotificationEvents() {
3246    const notificationCheckbox = document.getElementById("oum_location_notification");
3247    if (notificationCheckbox) {
3248      notificationCheckbox.addEventListener("change", function() {
3249        const authorFields = document.getElementById("oum_author");
3250        const nameField = document.getElementById("oum_location_author_name");
3251        const emailField = document.getElementById("oum_location_author_email");
3252
3253        if (this.checked) {
3254          authorFields.classList.add("active");
3255          nameField.required = true;
3256          emailField.required = true;
3257        } else {
3258          authorFields.classList.remove("active");
3259          nameField.required = false;
3260          emailField.required = false;
3261        }
3262      });
3263    }
3264  }
3265
3266  function setupMediaEvents() {
3267    // Image upload
3268    const imageInput = document.getElementById("oum_location_images");
3269    if (imageInput) {
3270      // Let OUMMedia handle the image upload
3271      OUMMedia.initializeImageUpload(imageInput);
3272    }
3273
3274    // Remove image button
3275    const removeImageBtn = document.getElementById("oum_remove_image");
3276    if (removeImageBtn) {
3277      removeImageBtn.addEventListener("click", function() {
3278        document.getElementById("oum_location_images_preview").innerHTML = "";
3279      });
3280    }
3281
3282    // Remove audio button
3283    const removeAudioBtn = document.getElementById("oum_remove_audio");
3284    if (removeAudioBtn) {
3285      removeAudioBtn.addEventListener("click", function() {
3286        const audioInput = document.getElementById("oum_location_audio");
3287        const previewContainer = audioInput.nextElementSibling;
3288        const previewDiv = previewContainer.querySelector('.audio-preview');
3289        
3290        // Clear the file input
3291        audioInput.value = "";
3292        
3293        // Clear the preview
3294        if (previewDiv) {
3295          previewDiv.innerHTML = '';
3296        }
3297        
3298        // Remove active state
3299        previewContainer.classList.remove("active");
3300        
3301        // Set remove flag
3302        document.getElementById("oum_remove_existing_audio").value = "1";
3303      });
3304    }
3305  }
3306
3307  function setupCategoryDropdownIcons() {
3308    // Handle dropdown with icons for marker categories
3309    const categorySelect = document.querySelector('select#oum_marker_icon');
3310    if (categorySelect) {
3311      // Create a custom dropdown wrapper
3312      const wrapper = document.createElement('div');
3313      wrapper.className = 'oum-category-dropdown-wrapper';
3314      wrapper.style.position = 'relative';
3315      
3316      // Insert wrapper before the select
3317      categorySelect.parentNode.insertBefore(wrapper, categorySelect);
3318      wrapper.appendChild(categorySelect);
3319      
3320      // Create custom dropdown display
3321      const display = document.createElement('div');
3322      display.className = 'oum-category-dropdown-display';
3323      
3324      // Create icon element
3325      const icon = document.createElement('img');
3326      icon.className = 'oum-category-dropdown-icon';
3327
3328      const linePreview = document.createElement('span');
3329      linePreview.className = 'oum-category-line-preview';
3330
3331      const areaPreview = document.createElement('span');
3332      areaPreview.className = 'oum-category-area-preview';
3333
3334      const routeIconTemplate = document.getElementById('oum-route-category-icon-template');
3335      const areaIconTemplate = document.getElementById('oum-area-category-icon-template');
3336      if (routeIconTemplate && routeIconTemplate.content) {
3337        linePreview.appendChild(routeIconTemplate.content.cloneNode(true));
3338      }
3339      if (areaIconTemplate && areaIconTemplate.content) {
3340        areaPreview.appendChild(areaIconTemplate.content.cloneNode(true));
3341      }
3342      
3343      // Create text element
3344      const text = document.createElement('span');
3345      text.className = 'oum-category-dropdown-text';
3346      
3347      display.appendChild(icon);
3348      display.appendChild(linePreview);
3349      display.appendChild(areaPreview);
3350      display.appendChild(text);
3351      
3352      // Insert display before select
3353      wrapper.insertBefore(display, categorySelect);
3354      
3355      // Make select invisible but still functional
3356      categorySelect.style.cssText = `
3357        position: absolute;
3358        top: 0;
3359        left: 0;
3360        width: 100%;
3361        height: 100%;
3362        opacity: 0;
3363        cursor: pointer;
3364        z-index: 1;
3365      `;
3366      
3367      // Update display when selection changes
3368      function updateDisplay() {
3369        const selectedOption = categorySelect.options[categorySelect.selectedIndex];
3370        if (selectedOption && selectedOption.value) {
3371          const iconUrl = selectedOption.getAttribute('data-icon');
3372          const categoryColor = selectedOption.getAttribute('data-color') || '#e82c71';
3373          const categoryType = selectedOption.getAttribute('data-category-type') || 'point';
3374
3375          if (categoryType === 'point' && iconUrl) {
3376            icon.src = iconUrl;
3377            icon.style.display = 'block';
3378          } else {
3379            icon.style.display = 'none';
3380          }
3381
3382          linePreview.style.color = categoryColor;
3383          linePreview.style.display = categoryType === 'polyline' ? 'inline-block' : 'none';
3384          areaPreview.style.color = categoryColor;
3385          areaPreview.style.display = categoryType === 'polygon' ? 'inline-block' : 'none';
3386          text.textContent = selectedOption.textContent;
3387        } else {
3388          icon.style.display = 'none';
3389          linePreview.style.color = '';
3390          linePreview.style.display = 'none';
3391          areaPreview.style.color = '';
3392          areaPreview.style.display = 'none';
3393          text.textContent = 'Select a category...';
3394        }
3395      }
3396      
3397      // Initial display update
3398      updateDisplay();
3399      if (typeof OUMFormMap !== "undefined" && OUMFormMap.updateLocationTypeCategoryUi) {
3400        OUMFormMap.updateLocationTypeCategoryUi();
3401        updateDisplay();
3402      }
3403      
3404      // Handle selection change
3405      categorySelect.addEventListener('change', updateDisplay);
3406
3407      
3408      // Make display focusable for accessibility
3409      display.setAttribute('tabindex', '0');
3410      
3411      // Handle keyboard navigation
3412      display.addEventListener('keydown', function(e) {
3413        if (e.key === 'Enter' || e.key === ' ') {
3414          e.preventDefault();
3415          categorySelect.focus();
3416          categorySelect.click();
3417        }
3418      });
3419    }
3420  }
3421
3422  function populateForm(location) {
3423    isEditMode = true;
3424    currentLocationId = location.post_id;
3425
3426    // Add edit-location class
3427    const addLocationEl = document.querySelector(".add-location");
3428    if (addLocationEl) {
3429        addLocationEl.classList.add("edit-location");
3430    }
3431
3432    // Set post_id
3433    const postIdField = document.getElementById("oum_post_id");
3434    if (postIdField) {
3435      postIdField.value = location.post_id;
3436    }
3437
3438    // Basic fields
3439    const titleField = document.getElementById("oum_location_title");
3440    const latField = document.getElementById("oum_location_lat");
3441    const lngField = document.getElementById("oum_location_lng");
3442    const addressField = document.getElementById("oum_location_address");
3443
3444    if (titleField) titleField.value = location.title || "";
3445    if (latField) latField.value = location.lat || "";
3446    if (lngField) lngField.value = location.lng || "";
3447    if (addressField) addressField.value = location.address || "";
3448
3449    const geometryType = location.geometry_type === "polyline" || location.geometry_type === "polygon" ? location.geometry_type : "point";
3450    OUMFormMap.setGeometryType(geometryType, { autoStartDrawing: false });
3451    
3452    // Marker types/categories
3453    if (location.types && Array.isArray(location.types)) {
3454        if (typeof oum_enable_multiple_marker_types !== 'undefined' && oum_enable_multiple_marker_types == 'true') {
3455            // Handle multiple marker types (checkboxes)
3456            const checkboxes = document.querySelectorAll('input[name="oum_marker_icon[]"]');
3457            if (checkboxes.length > 0) {
3458                checkboxes.forEach(checkbox => {
3459                    checkbox.checked = location.types.includes(checkbox.value);
3460                });
3461            }
3462        } else {
3463            // Handle single marker type (select)
3464            const markerSelect = document.querySelector('select#oum_marker_icon');
3465            if (markerSelect) {
3466                markerSelect.value = location.types[0] || '';
3467                
3468                // Update custom dropdown display if it exists
3469                const customDisplay = document.querySelector('.oum-category-dropdown-display');
3470                if (customDisplay) {
3471                    // Trigger the change event to update the display
3472                    markerSelect.dispatchEvent(new Event('change'));
3473                }
3474            }
3475        }
3476    }
3477
3478    // Description
3479    const descriptionField = document.getElementById("oum_location_text");
3480    if (descriptionField) {
3481        descriptionField.value = location.text || "";
3482    }
3483
3484    // Video field
3485    const videoField = document.getElementById("oum_location_video");
3486    if (videoField && location.video) {
3487        videoField.value = location.video;
3488    }
3489
3490    // Handle custom fields
3491    if (location.custom_fields && Array.isArray(location.custom_fields)) {
3492        location.custom_fields.forEach(field => {
3493            if (!field || typeof field.index === 'undefined') return;
3494
3495            if (field.fieldtype === 'checkbox') {
3496                const fieldValues = Array.isArray(field.val) ? field.val : [field.val];
3497                const checkboxes = document.querySelectorAll(
3498                    `input[type="checkbox"][name="oum_location_custom_fields[${field.index}][]"]`
3499                );
3500                
3501                if (checkboxes.length > 0) {
3502                    checkboxes.forEach(checkbox => {
3503                        if (checkbox) {
3504                            const checkboxValue = checkbox.value.trim();
3505                            const normalizedValues = fieldValues.map(val => (val || '').toString().trim());
3506                            checkbox.checked = normalizedValues.includes(checkboxValue);
3507                        }
3508                    });
3509                }
3510            } else if (field.fieldtype === 'radio') {
3511                const radioInputs = document.querySelectorAll(
3512                    `input[type="radio"][name="oum_location_custom_fields[${field.index}]"]`
3513                );
3514                
3515                if (radioInputs.length > 0) {
3516                    radioInputs.forEach(radio => {
3517                        if (radio) {
3518                            radio.checked = radio.value === field.val;
3519                        }
3520                    });
3521                }
3522            } else if (field.fieldtype === 'select') {
3523                // Handle single and multiple select
3524                let input = document.querySelector(`[name="oum_location_custom_fields[${field.index}][]"]`);
3525                const isMultiple = !!input;
3526                if (!input) {
3527                    input = document.querySelector(`[name="oum_location_custom_fields[${field.index}]"]`);
3528                }
3529                if (input && input.tagName === 'SELECT') {
3530                    const values = Array.isArray(field.val) ? field.val.map(v => (v || '').toString().trim()) : [(field.val || '').toString().trim()];
3531                    Array.from(input.options).forEach(opt => {
3532                        opt.selected = values.includes((opt.value || '').toString().trim());
3533                    });
3534                }
3535            } else if (field.fieldtype === 'opening_hours') {
3536                // Handle opening hours field - expect JSON string
3537                try {
3538                    const openingHoursData = JSON.parse(field.val);
3539                    if (openingHoursData && typeof openingHoursData === 'object') {
3540                        // Find the hours input
3541                        const hoursInput = document.querySelector(`input[name="oum_location_custom_fields[${field.index}][hours]"]`);
3542                        const fieldContainer = document.querySelector(`[data-custom-field-index="${field.index}"]`);
3543                        
3544                        // Check if 12-hour format is enabled for this field
3545                        const use12hour = fieldContainer && fieldContainer.dataset.use12hour === '1';
3546                        
3547                        if (hoursInput && typeof OUMOpeningHours !== 'undefined') {
3548                            // Convert JSON back to input format (with 12-hour format if enabled)
3549                            hoursInput.value = OUMOpeningHours.formatForInput(openingHoursData, use12hour);
3550                        }
3551                    }
3552                } catch (e) {
3553                    // Invalid JSON, try to set as raw value
3554                    const input = document.querySelector(`input[name="oum_location_custom_fields[${field.index}][hours]"]`);
3555                    if (input) {
3556                        input.value = field.val || '';
3557                    }
3558                }
3559            } else {
3560                const input = document.querySelector(`[name="oum_location_custom_fields[${field.index}]"]`);
3561                if (input) {
3562                    input.value = field.val || '';
3563                }
3564            }
3565        });
3566    }
3567
3568    // Handle images
3569    if (location.image) {
3570      const imageUrls = location.image.split('|').filter(url => url);
3571      const previewContainer = document.getElementById('oum_location_images_preview');
3572      
3573      // Remove required attribute from image upload field when editing with existing images
3574      const imageInput = document.getElementById('oum_location_images');
3575      if (imageInput && imageUrls.length > 0) {
3576        imageInput.removeAttribute('required');
3577      }
3578      
3579      if (previewContainer) {
3580        previewContainer.innerHTML = '';
3581        
3582        imageUrls.forEach(url => {
3583          if (!url) return;
3584
3585          const previewItem = OUMMedia.createExistingImagePreview(url);
3586          if (!previewItem) {
3587            return;
3588          }
3589
3590          previewItem.style.opacity = "0";
3591          previewItem.style.transform = "scale(0.9)";
3592          previewItem.style.transition = "all 0.3s ease";
3593          previewItem.style.opacity = "1";
3594          previewItem.style.transform = "scale(1)";
3595          previewContainer.appendChild(previewItem);
3596        });
3597      }
3598    }
3599
3600    // Handle audio
3601    if (location.audio) {
3602      OUMMedia.setExistingAudio(location.audio);
3603      document.getElementById("oum_remove_existing_audio").value = "0";
3604    }
3605
3606    // Set map view to location
3607    if (geometryType !== "point" && location.geometry) {
3608      OUMFormMap.setPolylineGeometry(location.geometry);
3609      OUMFormMap.startEditLayersTool();
3610    } else if (location.lat && location.lng) {
3611      const locationZoom = location.zoom ? Number(location.zoom) : 12;
3612      OUMFormMap.setView(location.lat, location.lng, locationZoom);
3613      OUMFormMap.setLocation(location.lat, location.lng);
3614    }
3615
3616    // Sync checkbox validation after form population
3617    OUMCheckboxValidation.syncAllGroups();
3618  }
3619
3620  function resetForm() {
3621    isEditMode = false;
3622    currentLocationId = null;
3623    selectedFiles = [];
3624    window.oumSelectedFiles = [];
3625    
3626    const addLocationEl = document.querySelector(".add-location");
3627    if (addLocationEl) {
3628      addLocationEl.classList.remove("edit-location");
3629    }
3630    
3631    // Reset form and message system
3632    const form = document.getElementById("oum_add_location");
3633    const errorDiv = document.getElementById("oum_add_location_error");
3634    const thankyouDiv = document.getElementById("oum_add_location_thankyou");
3635    
3636    // Reset form if it exists
3637    if (form) {
3638      form.reset();
3639      form.style.display = 'block';
3640    }
3641
3642    if (typeof OUMFormMap !== "undefined") {
3643      const typeField = document.getElementById("oum_geometry_type");
3644      OUMFormMap.setGeometryType(typeField && typeField.value ? typeField.value : "point", { autoStartDrawing: false });
3645      OUMFormMap.clearPolyline();
3646    }
3647
3648    // Stop locate process
3649    if (window.map_locate_process) {
3650      window.map_locate_process.stop();
3651    }
3652
3653    // Reset custom fields
3654    const customFields = document.querySelectorAll('[name^="oum_location_custom_fields"]');
3655    if (customFields) {
3656      customFields.forEach(field => {
3657        if (field.type === 'checkbox' || field.type === 'radio') {
3658          field.checked = false;
3659        } else if (field.type === 'select-one') {
3660          field.selectedIndex = 0;
3661        } else {
3662          field.value = '';
3663        }
3664      });
3665    }
3666
3667    // Reset post_id field
3668    const postIdField = document.getElementById("oum_post_id");
3669    if (postIdField) {
3670      postIdField.value = "";
3671    }
3672
3673    // Reset oum_delete_location field
3674    const deleteLocationField = document.getElementById("oum_delete_location");
3675    if (deleteLocationField) {
3676        deleteLocationField.value = "";
3677    }
3678
3679    // Reset error message if it exists
3680    if (errorDiv) {
3681      errorDiv.style.display = 'none';
3682    }
3683
3684    // Reset thank you message if it exists
3685    if (thankyouDiv) {
3686      thankyouDiv.style.display = 'none';
3687      thankyouDiv.classList.remove('oum-delete-confirmation');
3688    }
3689
3690    // Reset image preview
3691    const previewContainer = document.getElementById("oum_location_images_preview");
3692    if (previewContainer) {
3693      previewContainer.innerHTML = "";
3694    }
3695
3696    // Reset audio preview
3697    const audioInput = document.getElementById("oum_location_audio");
3698    if (audioInput) {
3699      const previewContainer = audioInput.nextElementSibling;
3700      const previewDiv = previewContainer.querySelector('.audio-preview');
3701      
3702      // Clear the file input
3703      audioInput.value = "";
3704      
3705      // Clear the preview
3706      if (previewDiv) {
3707        previewDiv.innerHTML = '';
3708      }
3709      
3710      // Remove active state
3711      previewContainer.classList.remove("active");
3712    }
3713
3714    // Reset hidden fields
3715    const removeExistingImage = document.getElementById("oum_remove_existing_image");
3716    if (removeExistingImage) {
3717      removeExistingImage.value = "0";
3718    }
3719
3720    const removeExistingAudio = document.getElementById("oum_remove_existing_audio");
3721    if (removeExistingAudio) {
3722      removeExistingAudio.value = "0";
3723    }
3724
3725    // Reset author section
3726    const authorSection = document.getElementById("oum_author");
3727    if (authorSection) {
3728      authorSection.classList.remove("active");
3729    }
3730
3731    // Reset name and email fields
3732    const nameField = document.getElementById("oum_location_author_name");
3733    const emailField = document.getElementById("oum_location_author_email");
3734    if (nameField && emailField) {
3735      nameField.required = false;
3736      emailField.required = false;
3737    }
3738  }
3739
3740  // Public interface
3741  return {
3742    init: function() {
3743      setupFormEvents();
3744      setupDeleteButton(); // Add delete button handler
3745      setupCategoryDropdownIcons();
3746    },
3747    showFormMessage: showFormMessage,
3748    openForm: openForm,
3749    closeForm: closeForm,
3750    resetForm: resetForm,
3751    populateForm: populateForm,
3752    isEditMode: function() {
3753      return isEditMode;
3754    },
3755    getCurrentLocationId: function() {
3756      return currentLocationId;
3757    }
3758  };
3759})();
3760
3761// OUMOpeningHours module is now in a separate file: frontend-opening-hours.js
3762// It's loaded as a dependency of this script
3763
3764/**
3765 * Media Module - Handles image upload and preview functionality
3766 */
3767const OUMMedia = (function () {
3768  // Private variables
3769  let selectedFiles = [];
3770  let startX, startY, originalPosition, placeholder;
3771  let isDragging = false;
3772  
3773  // Private functions
3774  function initializeImageUpload(imageInput) {
3775    if (!imageInput) return;
3776
3777    // Add click handler for upload icon label
3778    const uploadLabel = imageInput.parentElement.querySelector(
3779      'label[for="oum_location_images"]'
3780    );
3781    if (uploadLabel) {
3782      uploadLabel.addEventListener("click", function (e) {
3783        e.preventDefault();
3784        imageInput.click();
3785      });
3786    }
3787
3788    // Setup drag and drop handlers
3789    document.addEventListener("mousemove", handleDragMove);
3790    document.addEventListener("mouseup", handleDragEnd);
3791    if (imageInput.parentElement.querySelector(".image-preview-container")) {
3792      imageInput.parentElement.querySelector(".image-preview-container").addEventListener("dragover", handleDragOver);
3793    }
3794
3795    // Setup image input change handler
3796    imageInput.addEventListener("change", handleImageInputChange);
3797  }
3798
3799  function initializeAudioUpload() {
3800    const audioInput = document.getElementById('oum_location_audio');
3801    if (!audioInput) return;
3802
3803    // Add click handler for upload icon label
3804    const uploadLabel = audioInput.parentElement.querySelector(
3805      'label[for="oum_location_audio"]'
3806    );
3807    if (uploadLabel) {
3808      uploadLabel.addEventListener("click", function (e) {
3809        e.preventDefault();
3810        audioInput.click();
3811      });
3812    }
3813
3814    // Setup audio input change handler
3815    audioInput.addEventListener("change", handleAudioInputChange);
3816  }
3817
3818  function handleAudioInputChange(e) {
3819    const audioInput = e.target;
3820    const previewContainer = audioInput.nextElementSibling;
3821    const previewDiv = previewContainer.querySelector('.audio-preview');
3822    
3823    if (audioInput.files && audioInput.files[0]) {
3824      const file = audioInput.files[0];
3825      
3826      previewContainer.classList.add('active');
3827
3828      // Create audio preview element
3829      const audio = document.createElement('audio');
3830      audio.controls = true;
3831      audio.style.width = '100%';
3832      
3833      const source = document.createElement('source');
3834      source.src = URL.createObjectURL(file);
3835      source.type = file.type;
3836      
3837      audio.appendChild(source);
3838      
3839      // Replace existing audio preview if any
3840      previewDiv.innerHTML = '';
3841      previewDiv.appendChild(audio);
3842    }
3843  }
3844
3845  function setExistingAudio(audioUrl) {
3846    if (!audioUrl) return;
3847
3848    const audioInput = document.getElementById('oum_location_audio');
3849    if (!audioInput) return;
3850
3851    const previewContainer = audioInput.nextElementSibling;
3852    const previewDiv = previewContainer.querySelector('.audio-preview');
3853    
3854    previewContainer.classList.add('active');
3855
3856    // Create audio preview element
3857    const audio = document.createElement('audio');
3858    audio.controls = true;
3859    audio.style.width = '100%';
3860    
3861    const source = document.createElement('source');
3862    source.src = audioUrl;
3863    source.type = 'audio/' + audioUrl.split('.').pop();
3864    
3865    audio.appendChild(source);
3866    
3867    // Replace existing audio preview if any
3868    previewDiv.innerHTML = '';
3869    previewDiv.appendChild(audio);
3870  }
3871
3872  function handleImageInputChange(e) {
3873    const imageInput = e.target;
3874    const previewContainer = document.getElementById(
3875      "oum_location_images_preview"
3876    );
3877    const maxFiles = parseInt(imageInput.dataset.maxFiles) || 5;
3878    const maxFileSize = OUMConfig.defaults.media.maxImageSize; // in bytes
3879    
3880    // Convert FileList to Array and store in a variable
3881    const files = Array.prototype.slice.call(e.target.files);
3882    const existingCount = selectedFiles.length;
3883    const totalFiles = existingCount + files.length;
3884    
3885    if (totalFiles > maxFiles) {
3886      alert(
3887        OUMUtils.sprintf(
3888          oum_custom_strings.max_files_exceeded,
3889          maxFiles,
3890          maxFiles - existingCount
3891        )
3892      );
3893    }
3894    
3895    // Process only up to remaining slots
3896    const remainingSlots = maxFiles - existingCount;
3897    const filesToProcess = files.slice(0, remainingSlots);
3898    
3899    // Validate file sizes and collect valid files
3900    const validFiles = [];
3901    const invalidFiles = [];
3902    
3903    filesToProcess.forEach(file => {
3904      if (file.size > maxFileSize) {
3905        invalidFiles.push(file.name);
3906      } else {
3907        validFiles.push(file);
3908      }
3909    });
3910    
3911    // Show error message for invalid files
3912    if (invalidFiles.length > 0) {
3913      const maxSizeMB = Math.round(maxFileSize / (1024 * 1024));
3914      alert(
3915        OUMUtils.sprintf(
3916          oum_custom_strings.max_filesize_exceeded,
3917          maxSizeMB,
3918          invalidFiles.join('\n')
3919        )
3920      );
3921    }
3922    
3923    // Update selected files with only valid ones
3924    selectedFiles = [...selectedFiles, ...validFiles];
3925    
3926    // Create previews for valid files only
3927    createImagePreviews(validFiles, previewContainer);
3928
3929    // Make selectedFiles available globally for the form submission
3930    window.oumSelectedFiles = selectedFiles;
3931  }
3932
3933  function createImagePreviews(files, container) {
3934    files.forEach((file) => {
3935      const reader = new FileReader();
3936      
3937      reader.onload = function (e) {
3938        const previewItem = createPreviewItem(e.target.result, file.name);
3939
3940        // Add the item with a fade-in animation
3941        previewItem.style.opacity = "0";
3942        previewItem.style.transform = "scale(0.9)";
3943        container.appendChild(previewItem);
3944
3945        // Trigger animation after a brief delay
3946        setTimeout(() => {
3947          previewItem.style.transition = "all 0.3s ease";
3948          previewItem.style.opacity = "1";
3949          previewItem.style.transform = "scale(1)";
3950        }, 50);
3951      };
3952
3953      reader.readAsDataURL(file);
3954    });
3955  }
3956
3957  function createPreviewItem(imgSrc, fileName, isExisting) {
3958    const previewItem = document.createElement("div");
3959    previewItem.className = isExisting ? "image-preview-item existing-image" : "image-preview-item";
3960    if (fileName) {
3961      previewItem.dataset.fileName = fileName;
3962    }
3963    if (imgSrc) {
3964      previewItem.dataset.url = imgSrc;
3965    }
3966
3967    const img = document.createElement("img");
3968    img.setAttribute("alt", "Preview");
3969    img.src = imgSrc;
3970
3971    const remove = document.createElement("div");
3972    remove.className = "remove-image";
3973    remove.setAttribute("title", "Remove image");
3974    remove.textContent = "\u00d7";
3975
3976    const handle = document.createElement("div");
3977    handle.className = "drag-handle";
3978    handle.setAttribute("title", "Drag to reorder");
3979    handle.textContent = "\u22ee\u22ee";
3980
3981    previewItem.appendChild(img);
3982    previewItem.appendChild(remove);
3983    previewItem.appendChild(handle);
3984
3985    if (isExisting) {
3986      const hidden = document.createElement("input");
3987      hidden.type = "hidden";
3988      hidden.name = "existing_images[]";
3989      hidden.value = imgSrc;
3990      previewItem.appendChild(hidden);
3991    }
3992    
3993    // Add event listener for remove button
3994    const removeButton = previewItem.querySelector('.remove-image');
3995    if (removeButton) {
3996      removeButton.addEventListener('click', handleRemoveImage);
3997    }
3998    
3999    // Set up drag and drop for existing images
4000    setupDragAndDrop(previewItem);
4001
4002    return previewItem;
4003  }
4004
4005  function handleRemoveImage(e) {
4006    e.preventDefault();
4007    const previewItem = this.closest(".image-preview-item");
4008    
4009    // If it's an existing image, handle removal differently
4010    if (previewItem.classList.contains("existing-image")) {
4011      const imgUrl = previewItem.querySelector("[name='existing_images[]']").value;
4012      const removedImagesInput = document.getElementById("oum_remove_existing_image");
4013      const currentValue = removedImagesInput.value === "0" ? [] : removedImagesInput.value.split('|');
4014      currentValue.push(imgUrl);
4015      removedImagesInput.value = currentValue.join('|');
4016    } else {
4017      // Remove from selectedFiles array if it's a new image
4018      const fileName = previewItem.dataset.fileName;
4019      selectedFiles = selectedFiles.filter(file => file.name !== fileName);
4020      window.oumSelectedFiles = selectedFiles;
4021    }
4022
4023    // Animate and remove the preview item
4024    previewItem.style.transition = "all 0.3s ease";
4025    previewItem.style.transform = "scale(0.8)";
4026    previewItem.style.opacity = "0";
4027    
4028    setTimeout(() => {
4029      previewItem.remove();
4030    }, 300);
4031  }
4032
4033  function setupDragAndDrop(previewItem) {
4034    previewItem.setAttribute('draggable', 'true');
4035    
4036    previewItem.addEventListener('mousedown', function(e) {
4037      if (e.target.classList.contains('remove-image')) return;
4038
4039      isDragging = true;
4040      this.classList.add('dragging');
4041
4042      // Get element dimensions once at start
4043      const rect = this.getBoundingClientRect();
4044      this.style.width = rect.width + 'px';
4045      this.style.height = rect.height + 'px';
4046
4047      // Store initial grid container for safety check
4048      this.initialContainer = this.closest('.oum-image-preview-grid');
4049
4050      // Create placeholder immediately
4051      createPlaceholder(this);
4052      
4053      // Set up dragged element
4054      this.style.position = 'fixed';
4055      this.style.zIndex = '1000';
4056      this.style.opacity = '0.9';
4057      this.style.transform = 'scale(1.05) rotate(1deg)';
4058      this.style.pointerEvents = 'none';
4059      this.style.boxShadow = '0 5px 15px rgba(0,0,0,0.15)';
4060
4061      // Set initial position
4062      moveDraggedElement(this, e);
4063
4064      document.body.style.cursor = 'grabbing';
4065    });
4066    
4067    previewItem.addEventListener('touchstart', handleTouchStart);
4068  }
4069
4070  function createPlaceholder(element) {
4071    placeholder = document.createElement("div");
4072    placeholder.classList.add("image-preview-placeholder");
4073    placeholder.style.width = element.offsetWidth + "px";
4074    placeholder.style.height = element.offsetHeight + "px";
4075    placeholder.style.transition = "transform 0.2s ease";
4076    placeholder.style.border = "2px dashed #e82c71";
4077    placeholder.style.borderRadius = "4px";
4078    placeholder.style.backgroundColor = "rgba(224, 42, 175, 0.05)";
4079    element.parentNode.insertBefore(placeholder, element);
4080  }
4081
4082  function moveDraggedElement(draggable, e) {
4083    const rect = draggable.getBoundingClientRect();
4084    const centerOffsetX = rect.width / 2;
4085    const centerOffsetY = rect.height / 2;
4086    
4087    // Position element directly at cursor with center offset
4088    draggable.style.left = (e.clientX - centerOffsetX) + 'px';
4089    draggable.style.top = (e.clientY - centerOffsetY) + 'px';
4090  }
4091
4092  function updatePlaceholderPosition(e) {
4093    const previewContainer = document.getElementById("oum_location_images_preview");
4094    if (!previewContainer) return;
4095
4096    const draggable = document.querySelector('.dragging');
4097    if (!draggable) return;
4098
4099    const siblings = [...previewContainer.querySelectorAll(".image-preview-item:not(.dragging)")];
4100    
4101    // Find the closest sibling based on mouse position
4102    const closestSibling = siblings.reduce((closest, child) => {
4103      const rect = child.getBoundingClientRect();
4104      const centerX = rect.left + rect.width / 2;
4105      const offset = e.clientX - centerX;
4106      
4107      if (offset < 0 && (!closest.element || offset > closest.offset)) {
4108        return { offset: offset, element: child };
4109      }
4110      return closest;
4111    }, { offset: Number.NEGATIVE_INFINITY, element: null });
4112
4113    if (closestSibling.element) {
4114      previewContainer.insertBefore(placeholder, closestSibling.element);
4115    } else {
4116      previewContainer.appendChild(placeholder);
4117    }
4118  }
4119
4120  function handleDragMove(e) {
4121    if (!isDragging) return;
4122
4123    const draggable = document.querySelector(".dragging");
4124    if (!draggable) return;
4125
4126    // Update dragged element position
4127    moveDraggedElement(draggable, e);
4128    
4129    // Check if cursor is still within any grid container
4130    const gridContainer = document.getElementById("oum_location_images_preview");
4131    if (!gridContainer) return;
4132
4133    const gridRect = gridContainer.getBoundingClientRect();
4134    const isWithinGrid = e.clientX >= gridRect.left - 50 && 
4135                        e.clientX <= gridRect.right + 50 && 
4136                        e.clientY >= gridRect.top - 50 && 
4137                        e.clientY <= gridRect.bottom + 50;
4138
4139    // If cursor is outside grid boundaries, hide placeholder
4140    if (!isWithinGrid && placeholder) {
4141      placeholder.style.display = 'none';
4142    } else if (placeholder) {
4143      placeholder.style.display = 'block';
4144      updatePlaceholderPosition(e);
4145    }
4146  }
4147
4148  function handleDragEnd() {
4149    const draggable = document.querySelector(".dragging");
4150    if (!draggable) return;
4151
4152    // Reset cursor
4153    document.body.style.cursor = "";
4154
4155    // Check if we're still within the grid
4156    const gridContainer = document.getElementById("oum_location_images_preview");
4157    if (!gridContainer) {
4158      // If no grid found, return item to its initial position
4159      if (draggable.initialContainer) {
4160        draggable.initialContainer.appendChild(draggable);
4161      }
4162    } else {
4163      // Place draggable element at placeholder position if within grid
4164      if (placeholder && placeholder.style.display !== 'none') {
4165        draggable.style.transition = "none";
4166        placeholder.parentNode.insertBefore(draggable, placeholder);
4167      } else {
4168        // If placeholder is hidden (outside grid), append to end
4169        gridContainer.appendChild(draggable);
4170      }
4171    }
4172
4173    // Remove placeholder
4174    if (placeholder) {
4175      placeholder.remove();
4176    }
4177
4178    // Reset draggable element styles
4179    draggable.style.position = "";
4180    draggable.style.zIndex = "";
4181    draggable.style.top = "";
4182    draggable.style.left = "";
4183    draggable.style.width = "";
4184    draggable.style.height = "";
4185    draggable.style.transform = "";
4186    draggable.style.pointerEvents = "";
4187    draggable.style.boxShadow = "";
4188    draggable.classList.remove("dragging");
4189
4190    isDragging = false;
4191  }
4192
4193  function handleTouchStart(e) {
4194    const touch = e.touches[0];
4195    const mouseEvent = new MouseEvent("mousedown", {
4196      clientX: touch.clientX,
4197      clientY: touch.clientY
4198    });
4199    this.dispatchEvent(mouseEvent);
4200  }
4201
4202  function handleDragOver(e) {
4203    e.preventDefault();
4204    e.stopPropagation();
4205
4206    const draggable = document.querySelector(".dragging");
4207    if (!draggable) return;
4208
4209    const afterElement = getDragAfterElement(this, e.clientX);
4210    if (afterElement == null) {
4211      this.appendChild(placeholder);
4212    } else {
4213      this.insertBefore(placeholder, afterElement);
4214    }
4215  }
4216
4217  // Public interface
4218  return {
4219    init: function () {
4220      initializeAudioUpload();
4221    },
4222    initializeImageUpload: function(imageInput) {
4223      initializeImageUpload(imageInput);
4224    },
4225    getSelectedFiles: function () {
4226      return selectedFiles;
4227    },
4228    setupDragAndDrop: setupDragAndDrop,
4229    handleRemoveImage: handleRemoveImage,
4230    createExistingImagePreview: function(url) {
4231      if (!url) {
4232        return null;
4233      }
4234      return createPreviewItem(url, '', true);
4235    },
4236    setExistingAudio: setExistingAudio
4237  };
4238})();
4239
4240/**
4241 * Checkbox Validation Module - Handles required checkbox group validation
4242 */
4243const OUMCheckboxValidation = (function () {
4244  // Private functions
4245  function syncRequired(group) {
4246    const anyChecked = group.some(cb => cb.checked);
4247    const first = group[0];
4248    first.required = !anyChecked;
4249  }
4250
4251  function getCheckboxGroups() {
4252    const checkboxGroups = document.querySelectorAll('.oum-checkbox-group.is-required input[type="checkbox"]');
4253    
4254    if (!checkboxGroups.length) return {};
4255
4256    // Group checkboxes by name
4257    const groups = {};
4258    checkboxGroups.forEach(checkbox => {
4259      const name = checkbox.name;
4260      if (!groups[name]) groups[name] = [];
4261      groups[name].push(checkbox);
4262    });
4263
4264    return groups;
4265  }
4266
4267  function processCheckboxGroup(group, setupEvents = true) {
4268    const first = group[0];
4269    
4270    // Only process if first checkbox is required
4271    if (!first.required) return;
4272
4273    // Add event listeners if requested
4274    if (setupEvents) {
4275      group.forEach(cb => cb.addEventListener('change', () => syncRequired(group)));
4276    }
4277    
4278    // Sync the state
4279    syncRequired(group);
4280  }
4281
4282  function initCheckboxGroups() {
4283    const groups = getCheckboxGroups();
4284    
4285    // Process each group with event listeners
4286    Object.keys(groups).forEach(groupName => {
4287      processCheckboxGroup(groups[groupName], true);
4288    });
4289  }
4290
4291  // Public interface
4292  return {
4293    init: function() {
4294      initCheckboxGroups();
4295    },
4296    syncAllGroups: function() {
4297      const groups = getCheckboxGroups();
4298      
4299      // Sync each group without setting up new event listeners
4300      Object.keys(groups).forEach(groupName => {
4301        processCheckboxGroup(groups[groupName], false);
4302      });
4303    }
4304  };
4305})();
4306
4307/**
4308 * Advanced Filter Interface Module - Handles advanced filtering functionality
4309 */
4310const OUMAdvancedFilter = (function () {
4311  // Private variables
4312  let isInitialized = false;
4313  let filterSidebar = null;
4314
4315  // Private functions
4316  function init() {
4317    if (isInitialized) return;
4318
4319    filterSidebar = document.querySelector('.oum-advanced-filter-sidebar');
4320    if (!filterSidebar) {
4321      return;
4322    }
4323    
4324    setupFilterEvents();
4325    setupFloatingPanelToggle();
4326    isInitialized = true;
4327  }
4328
4329  // Setup click toggle for button/panel
4330  function setupFloatingPanelToggle() {
4331    const floatingPanel = document.querySelector('.oum-advanced-filter-button, .oum-advanced-filter-panel');
4332    if (!floatingPanel || !floatingPanel.classList.contains('use-collapse')) {
4333      return;
4334    }
4335
4336    const toggle = floatingPanel.querySelector('.oum-advanced-filter-toggle');
4337    const closeButton = floatingPanel.querySelector('.close-advanced-filter');
4338    const filterWrapper = floatingPanel.closest('.oum-map-filter-wrapper');
4339
4340    // Toggle active class on click
4341    if (toggle) {
4342      toggle.addEventListener('click', function(e) {
4343        e.stopPropagation();
4344        floatingPanel.classList.toggle('active');
4345      });
4346    }
4347
4348    // Close button always collapses the floating panel
4349    if (closeButton) {
4350      closeButton.addEventListener('click', function(e) {
4351        e.stopPropagation();
4352        floatingPanel.classList.remove('active');
4353      });
4354    }
4355
4356    // Close when clicking outside the filter wrapper
4357    if (filterWrapper) {
4358      document.addEventListener('click', function(e) {
4359        if (!filterWrapper.contains(e.target)) {
4360          floatingPanel.classList.remove('active');
4361        }
4362      });
4363    }
4364  }
4365
4366  function setupFilterEvents() {
4367    // Get all filter inputs
4368    const filterInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input');
4369    const filterCheckboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox');
4370    const filterRadios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio');
4371    const filterSelects = filterSidebar.querySelectorAll('.oum-advanced-filter-select');
4372
4373    // Add event listeners to all filter inputs
4374    filterInputs.forEach(input => {
4375      input.addEventListener('input', handleFilterChange);
4376    });
4377
4378    filterCheckboxes.forEach(checkbox => {
4379      checkbox.addEventListener('change', handleFilterChange);
4380    });
4381
4382    filterRadios.forEach(radio => {
4383      radio.addEventListener('change', handleFilterChange);
4384    });
4385
4386    filterSelects.forEach(select => {
4387      select.addEventListener('change', handleFilterChange);
4388    });
4389
4390    // Add reset button functionality
4391    const resetButton = filterSidebar.querySelector('#oum-advanced-filter-reset');
4392    if (resetButton) {
4393      resetButton.addEventListener('click', resetAllFilters);
4394    }
4395  }
4396
4397  function handleFilterChange() {
4398    if (typeof OUMMarkers === 'undefined' || !OUMMarkers.setCustomfieldsFilter) {
4399      console.warn('OUMMarkers not available for advanced filtering');
4400      return;
4401    }
4402
4403    const filterCriteria = getFilterCriteria();
4404    OUMMarkers.setCustomfieldsFilter(filterCriteria);
4405  }
4406
4407  function getFilterCriteria() {
4408    const criteria = {};
4409    if (!filterSidebar) return criteria;
4410
4411    // Get text/email/url inputs
4412    const textInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input');
4413    textInputs.forEach(input => {
4414      const customFieldId = input.dataset.customFieldId;
4415      if (customFieldId && input.value.trim()) {
4416        criteria[customFieldId] = {
4417          type: 'text',
4418          value: input.value.trim().toLowerCase()
4419        };
4420      }
4421    });
4422
4423    // Get checkbox selections
4424    const checkboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox');
4425    checkboxes.forEach(checkbox => {
4426      const customFieldId = checkbox.dataset.customFieldId;
4427      const filterType = checkbox.dataset.filterType;
4428      
4429      if (customFieldId) {
4430        // Check if this is an opening_hours open_now filter
4431        if (filterType === 'opening_hours_open_now') {
4432          if (checkbox.checked) {
4433            criteria[customFieldId] = {
4434              type: 'opening_hours_open_now',
4435              value: 'open'
4436            };
4437          }
4438        } else {
4439          // Standard checkbox handling
4440          if (!criteria[customFieldId]) {
4441            // Get checkbox relation from the fieldset (default to OR for backward compatibility)
4442            const fieldset = checkbox.closest('.oum-checkbox-group');
4443            const relation = fieldset ? (fieldset.dataset.checkboxRelation || 'OR') : 'OR';
4444            criteria[customFieldId] = { type: 'checkbox', values: [], relation: relation };
4445          }
4446          if (checkbox.checked) {
4447            criteria[customFieldId].values.push(checkbox.value);
4448          }
4449        }
4450      }
4451    });
4452
4453    // Get radio selections
4454    const radios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio');
4455    radios.forEach(radio => {
4456      const customFieldId = radio.dataset.customFieldId;
4457      if (customFieldId && radio.checked) {
4458        criteria[customFieldId] = {
4459          type: 'radio',
4460          value: radio.value
4461        };
4462      }
4463    });
4464
4465    // Get select selections
4466    const selects = filterSidebar.querySelectorAll('.oum-advanced-filter-select');
4467    selects.forEach(select => {
4468      const customFieldId = select.dataset.customFieldId;
4469      if (customFieldId && select.value) {
4470        criteria[customFieldId] = {
4471          type: 'select',
4472          value: select.value
4473        };
4474      }
4475    });
4476
4477    // Remove empty criteria (no checkboxes selected, empty text inputs, etc.)
4478    Object.keys(criteria).forEach(key => {
4479      const criterion = criteria[key];
4480      if (criterion.type === 'checkbox' && criterion.values.length === 0) {
4481        delete criteria[key];
4482      }
4483    });
4484
4485    return criteria;
4486  }
4487
4488  function resetAllFilters() {
4489    // Clear all text inputs
4490    const filterInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input');
4491    filterInputs.forEach(input => {
4492      input.value = '';
4493    });
4494
4495    // Uncheck all checkboxes
4496    const filterCheckboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox');
4497    filterCheckboxes.forEach(checkbox => {
4498      checkbox.checked = false;
4499    });
4500
4501    // Uncheck all radio buttons
4502    const filterRadios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio');
4503    filterRadios.forEach(radio => {
4504      radio.checked = false;
4505    });
4506
4507    // Reset all select dropdowns to first option
4508    const filterSelects = filterSidebar.querySelectorAll('.oum-advanced-filter-select');
4509    filterSelects.forEach(select => {
4510      select.selectedIndex = 0;
4511    });
4512
4513    if (typeof OUMMarkers !== 'undefined' && OUMMarkers.setCustomfieldsFilter) {
4514      OUMMarkers.setCustomfieldsFilter(null);
4515    }
4516
4517    // Restore Marker Categories to the default "all checked" state
4518    if (typeof OUMMarkers !== 'undefined' && OUMMarkers.resetCategoriesFilter) {
4519      OUMMarkers.resetCategoriesFilter();
4520    }
4521
4522    resetSearchbar();
4523  }
4524
4525  function resetSearchbar() {
4526    // Live Filter Markers
4527    const liveFilterInput = document.getElementById("oum_filter_markers");
4528    if (liveFilterInput) {
4529      liveFilterInput.value = "";
4530      if (typeof OUMMarkers !== "undefined" && OUMMarkers.updateSearchtextAndCategoriesFilters) {
4531        OUMMarkers.updateSearchtextAndCategoriesFilters();
4532      }
4533    }
4534
4535    // Marker search (leaflet-search)
4536    const markerSearchCancel = document.querySelector("#oum_search_marker .search-cancel");
4537    if (markerSearchCancel) {
4538      markerSearchCancel.click();
4539    } else {
4540      const markerSearchInput = document.querySelector("#oum_search_marker .search-input");
4541      if (markerSearchInput) {
4542        markerSearchInput.value = "";
4543        markerSearchInput.dispatchEvent(new Event("input", { bubbles: true }));
4544        markerSearchInput.dispatchEvent(new Event("keyup", { bubbles: true }));
4545      }
4546    }
4547
4548    // Address search (GeoSearch)
4549    const addressReset = document.querySelector("#oum_search_address button.reset, .oum-searchbar-filter .leaflet-geosearch-bar button.reset");
4550    if (addressReset) {
4551      addressReset.click();
4552    } else {
4553      const addressInput = document.querySelector("#oum_search_address input, .oum-searchbar-filter .leaflet-geosearch-bar input");
4554      if (addressInput) {
4555        addressInput.value = "";
4556      }
4557    }
4558  }
4559
4560  // Public interface
4561  return {
4562    init: init
4563  };
4564})();
4565
4566// Main initialization function
4567function oumInitializeMap() {
4568  // Only proceed if we have a map element
4569  if (!document.getElementById(map_el)) {
4570    console.warn('‼️ Open User Map: Map container missing. Initialization aborted. Disable page caching if applicable, or contact [email protected] for help.');
4571    return;
4572  }
4573  
4574  // Initialize opening hours UI
4575  OUMOpeningHours.init();
4576
4577  // Restore the extended L object
4578  window.L = window.OUMLeaflet.L;
4579
4580  // Initialize map and get instance
4581  const mapInstance = OUMMap.init(map_el);
4582
4583  // Initialize markers
4584  const markersModule = OUMMarkers.init(mapInstance);
4585
4586  // Add markers from the global oum_all_locations
4587  if (
4588    typeof oum_all_locations !== "undefined" &&
4589    Array.isArray(oum_all_locations)
4590  ) {
4591    markersModule.addMarkers(oum_all_locations);
4592  }
4593
4594  // Initialize location form
4595  OUMFormMap.init(mapInstance);
4596
4597  // Initialize form controller
4598  OUMFormController.init();
4599
4600  // Initialize media handling
4601  OUMMedia.init();
4602
4603  // Initialize checkbox validation
4604  OUMCheckboxValidation.init();
4605
4606  // Initialize Advanced Filter Interface
4607  OUMAdvancedFilter.init();
4608
4609  // Setup filter events
4610  const markerFilterInput = document.getElementById("oum_filter_markers");
4611  if (markerFilterInput) {
4612    markerFilterInput.addEventListener("input", OUMMarkers.updateSearchtextAndCategoriesFilters);
4613  }
4614
4615  const categoryInputs = document.querySelectorAll(
4616    '.open-user-map .oum-marker-categories-filter [name="type"]'
4617  );
4618  if (categoryInputs.length > 0) {
4619    categoryInputs.forEach((input) => {
4620      input.addEventListener("change", OUMMarkers.updateSearchtextAndCategoriesFilters);
4621    });
4622  }
4623
4624  // Execute custom JS from settings
4625  if (typeof custom_js !== "undefined" && custom_js.snippet) {
4626    try {
4627      // Wrap custom JS execution in a try-catch with proper element existence checks
4628      const wrappedJS = `
4629        try {
4630          if (typeof document !== 'undefined') {
4631            // Defer map2-related code execution
4632            if (${custom_js.snippet.includes("oumMap2")}) {
4633              // Create a MutationObserver to watch for the form map initialization
4634              const observer = new MutationObserver((mutations) => {
4635                if (window.oumMap2) {
4636                  // Execute the custom JS only when oumMap2 is available
4637                  try {
4638                    ${custom_js.snippet}
4639                  } catch (e) {
4640                    console.warn('Custom JS execution error (deferred):', e);
4641                  }
4642                  observer.disconnect();
4643                }
4644              });
4645
4646              // Start observing the document for the form map to be added
4647              observer.observe(document.body, {
4648                childList: true,
4649                subtree: true
4650              });
4651            } else {
4652              // Execute non-map2 related code immediately
4653              ${custom_js.snippet}
4654            }
4655          }
4656        } catch (e) {
4657          console.warn('Custom JS execution error:', e);
4658        }
4659      `;
4660      Function(wrappedJS)();
4661    } catch (error) {
4662      console.warn("Error executing custom JS:", error);
4663    }
4664  }
4665}

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.