/** * Leaflet Map Application * Refactored for better organization, maintainability, and modern JavaScript practices */ // ============================================================================= // CONFIGURATION & CONSTANTS // ============================================================================= const CONFIG = { map: { preferCanvas: true, defaultCenter: [38, -80], defaultZoom: 10, }, icons: { defaultSize: [38, 38], defaultAnchor: [19, 19], }, bounds: { wv: { corner1: [37.1411, -82.8003], corner2: [40.6888, -77.6728], }, }, urls: { settingsFile: (mapId) => `/z/doc?command=view&allfile=true&file={tempdirs}/60daytemp/${mapId}_settings.json`, menu: '/z/mapdraw?command=leaflet&step=menu&skin=ajax', query: '/z/mapdraw?command=leaflet&step=query&skin=ajax', lassoHome: '/z/mapdraw?command=leaflet&step=lassoedhome&skin=ajax', }, layers: { esri: { taxMaps: 'https://services.wvgis.wvu.edu/arcgis/rest/services/Planning_Cadastre/WV_Parcels/MapServer', floodPublic: 'https://services.wvgis.wvu.edu/arcgis/rest/services/Hazards/floodTool_publicView/MapServer', forestParks: 'https://services.wvgis.wvu.edu/arcgis/rest/services/Boundaries/wv_protected_lands/MapServer', trails: 'https://services.wvgis.wvu.edu/arcgis/rest/services/Applications/trails_trailService/MapServer/', politicalBoundary: 'https://services.wvgis.wvu.edu/arcgis/rest/services/Boundaries/wv_political_boundary/MapServer', cellService: 'https://atlas.wvgs.wvnet.edu/arcgis/rest/services/WestVirginiaBroadbandOnlineV10/WvTechnologyGroupD/MapServer', internet: 'https://atlas.wvgs.wvnet.edu/arcgis/rest/services/WestVirginiaBroadbandOnlineV10/WestVirginiaBroadbandMap/MapServer/', contour1ft: 'https://services.wvgis.wvu.edu/arcgis/rest/services/Elevation/wv_contour_1ft/MapServer', addresses: 'https://services.wvgis.wvu.edu/arcgis/rest/services/Planning_Cadastre/WV_Parcels/MapServer/5', topo: 'https://services.arcgisonline.com/ArcGIS/rest/services/USA_Topo_Maps/MapServer', topoAlt: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer', hillshade: 'https://tagis.dep.wv.gov/arcgis/rest/services/webMercator/WVhillshade_wm/MapServer', leafless: 'https://services.wvgis.wvu.edu/arcgis/rest/services/Imagery_BaseMaps_EarthCover/wv_imagery_WVGISTC_leaf_off_mosaic/MapServer', }, }, }; // ============================================================================= // APPLICATION STATE // ============================================================================= const AppState = { control: '', layers: { base: {}, overlays: {}, settings: { overlays: [] }, }, settings: {}, icons: {}, lasso: { control: null, enabled: false, }, activeLayers: [], currentBaseLayer: 'Default', viewSet: false, masterGroupAdded: false, masterGroup: null, }; // Global map variable for backward compatibility with external scripts var map = null; // ============================================================================= // MAP INITIALIZATION // ============================================================================= const MapApp = { /** * Initialize the map application */ init() { this.createMap(); this.setupEventListeners(); this.initializeComponents(); }, /** * Create the Leaflet map instance */ createMap() { map = L.map('map', CONFIG.map) .setView(CONFIG.map.defaultCenter, CONFIG.map.defaultZoom); }, /** * Setup map event listeners */ setupEventListeners() { map.on('baselayerchange', (e) => { AppState.currentBaseLayer = e.name; }); map.on('overlayremove', (e) => { const index = AppState.activeLayers.indexOf(e.name); if (index > -1) { AppState.activeLayers.splice(index, 1); } }); map.on('overlayadd', (e) => { AppState.activeLayers.push(e.name); }); }, /** * Initialize all map components */ initializeComponents() { if (typeof maprealm !== 'undefined' && maprealm === 'agent') { GeocodeSearch.init(); } Icons.init(); ExternalOverlays.init(); Settings.read(); Events.initMove(); Events.initClick(); Menu.init(); Lasso.init(); MapFilters.init(); }, }; // ============================================================================= // ICONS // ============================================================================= const Icons = { init() { this.createIcon('redoaksign', '/objects/maps/googlemapsign.png'); this.createIcon('wvumailboxIcon', '/objects/mailbox_icon.png'); }, createIcon(name, url, size = CONFIG.icons.defaultSize, anchor = CONFIG.icons.defaultAnchor) { AppState.icons[name] = L.icon({ iconUrl: url, iconSize: size, iconAnchor: anchor, }); }, getIcon(name) { return AppState.icons[name] || new L.Icon.Default(); }, }; // ============================================================================= // GEOCODE SEARCH // ============================================================================= const GeocodeSearch = { init() { const bounds = L.latLngBounds(CONFIG.bounds.wv.corner1, CONFIG.bounds.wv.corner2); this.initArcGISSearch(bounds); if (typeof maprealm !== 'undefined' && maprealm === 'agent') { this.initRedOakSearch(bounds); } }, initArcGISSearch(bounds) { const arcgisOnline = L.esri.Geocoding.arcgisOnlineProvider(); const searchControl = L.esri.Geocoding.geosearch({ position: 'topright', providers: [arcgisOnline], searchBounds: bounds, }).addTo(map); const results = L.layerGroup().addTo(map); searchControl.on('results', (data) => { results.clearLayers(); data.results.forEach((result) => { results.addLayer(L.marker(result.latlng)); }); }); }, initRedOakSearch(bounds) { const redOakGeo = L.esri.Geocoding.arcgisOnlineProvider({ url: 'https://www.property4u.com/z/mapdraw?command=geocode&step=redoakgeo&type=p&qin=', searchFields: ['CountyName'], label: 'RED OAK RESULTS', maxResults: '15', }); const searchControl2 = L.esri.Geocoding.geosearch({ position: 'topright', placeholder: 'Search Red Oak', providers: [redOakGeo], searchBounds: bounds, }).addTo(map); const results2 = L.layerGroup().addTo(map); searchControl2.on('results', (data) => { results2.clearLayers(); data.results.forEach((result) => { results2.addLayer(L.marker(result.latlng)); }); }); }, }; // ============================================================================= // EVENT HANDLERS // ============================================================================= const Events = { initMove() { if (typeof maprealm !== 'undefined' && maprealm === 'agent') { map.on('move', () => { if (typeof gmapready !== 'undefined' && gmapready && typeof gmapsetbounds === 'function') { const { lat, lng } = map.getCenter(); const zoom = map.getZoom(); gmapsetbounds(lat, lng, zoom); } }); } }, initClick() { if (typeof maprealm !== 'undefined' && maprealm === 'agent') { map.on('click', this.handleClick); } }, handleClick(e) { const theoverlays = {}; AppState.activeLayers.forEach((layer, i) => { theoverlays[i] = layer; }); const { latlng } = e; const popup = L.popup() .setLatLng(latlng) .setContent('loading') .openOn(map); let url = `${CONFIG.urls.query}&latlan=${latlng.toString()}`; if (AppState.settings.querykey) { url += `&key=${AppState.settings.querykey}`; } const bounds = map.getBounds(); url += `&overlays=${JSON.stringify(theoverlays)}`; url += `&maprealm=${maprealm}`; url += `&bounds=${bounds.getWest()},${bounds.getSouth()},${bounds.getEast()},${bounds.getNorth()}`; $.get(url).done((data) => { popup.setContent(data); popup.update(); }); }, }; // ============================================================================= // MENU // ============================================================================= const Menu = { init() { let menuUrl = CONFIG.urls.menu; if (typeof mapid !== 'undefined' && mapid) { menuUrl += `&mapid=${mapid}`; } if (typeof maprealm !== 'undefined' && maprealm) { menuUrl += `&maprealm=${maprealm}`; } $.get(menuUrl).done((data) => { L.control.slideMenu(data, { position: 'bottomright', menuposition: 'bottomright' }).addTo(map); }); // Locate control L.control.locate({ setView: 'untilPanOrZoom', keepCurrentZoomLevel: true, }).addTo(map); // Fullscreen control map.addControl(new L.Control.Fullscreen()); }, }; // ============================================================================= // SETTINGS // ============================================================================= const Settings = { read() { if (typeof readsettings === 'undefined' || readsettings !== 'true') { LayerControl.add(); return; } const settingsUrl = CONFIG.urls.settingsFile(mapid); $.getJSON(settingsUrl, (settingsData) => { AppState.settings = settingsData; AppState.layers.settings.overlays = []; this.initMarkers(); this.initGeoJsonLayers(); LayerControl.add(); Watermark.init(); }).fail((err) => { console.error('Failed to load settings:', err); LayerControl.add(); }); }, initMarkers() { const { settings } = AppState; if (!settings.markers) return; for (const layerName in settings.markers) { const markerGroup = settings.nocluster ? new L.featureGroup() : L.markerClusterGroup(); for (const id in settings.markers[layerName]) { const markerData = settings.markers[layerName][id]; const markerOptions = this.buildMarkerOptions(markerData); const marker = L.marker( [markerData.lat, markerData.lon], markerOptions ); // Store original icon for lasso reset functionality if (markerOptions.icon) { marker.originalIcon = markerOptions.icon; } // Store price and acres for filtering marker.price = markerData.price ?? 0; marker.acres = markerData.acres ?? 0; // Debug: log first few markers to verify data if (typeof window._markerDebugCount === 'undefined') window._markerDebugCount = 0; if (window._markerDebugCount < 3) { console.log('Marker data:', id, 'price:', marker.price, 'acres:', marker.acres); window._markerDebugCount++; } if (markerData.draggable) { marker.on('dragend', () => { const latlonInput = document.getElementById('latlonlocation'); if (latlonInput) { latlonInput.value = `${marker.getLatLng().lat},${marker.getLatLng().lng}`; } }); } if (markerData.bindpopup) { marker.bindPopup(markerData.bindpopup); } marker.userid = id; marker.username = layerName; marker.addTo(markerGroup); } AppState.layers.settings.overlays[layerName] = markerGroup; if (!settings.layers?.geojson) { // Add padding at top for filter controls, and some on sides map.fitBounds(markerGroup.getBounds(), { paddingTopLeft: [20, 100], paddingBottomRight: [20, 20] }); } } }, buildMarkerOptions(markerData) { const options = {}; if (markerData.icon) { options.icon = L.icon({ iconUrl: markerData.icon, iconSize: markerData.iconSize || CONFIG.icons.defaultSize, iconAnchor: markerData.iconAnchor || CONFIG.icons.defaultAnchor, }); } if (markerData.numberlabel) { options.icon = new L.AwesomeNumberMarkers({ number: markerData.numberlabel, markerColor: 'blue', }); } if (markerData.draggable) { options.draggable = markerData.draggable; } return options; }, initGeoJsonLayers() { const { settings } = AppState; if (!settings.layers?.geojson) return; settings.layers.geojson.forEach((layerConfig, index) => { const geojsonLayer = new L.GeoJSON.AJAX(layerConfig.url, { onEachFeature: GeoJsonUtils.onEachFeature, }); if (index === 0) { // Primary layer - fit bounds and bring to front geojsonLayer.on('data:loaded', function() { map.fitBounds(this.getBounds(), { paddingTopLeft: [20, 100], paddingBottomRight: [20, 20] }); if (settings.zoom) { map.setZoom(settings.zoom); } geojsonLayer.addTo(map).bringToFront(); }); } else { // Secondary layers - bring to back geojsonLayer.on('data:loaded', function() { geojsonLayer.addTo(map).bringToBack(); }); } AppState.layers.settings.overlays[layerConfig.id] = geojsonLayer; }); }, }; // ============================================================================= // GEOJSON UTILITIES // ============================================================================= const GeoJsonUtils = { onEachFeature(feature, layer) { if (feature.properties?.popup) { layer.bindPopup(feature.properties.popup); } if (feature.style) { layer.setStyle(feature.style); } }, }; // ============================================================================= // LAYER CONTROL // ============================================================================= const LayerControl = { add() { const { settings, layers } = AppState; // Set base layer if (settings.basemap && layers.base[settings.basemap]) { map.addLayer(layers.base[settings.basemap]); } else if (layers.base['Default']) { layers.base['Default'].addTo(map); } // Build overlays object const overlays = { ...layers.overlays }; for (const name in layers.settings.overlays) { overlays[name] = layers.settings.overlays[name]; } const baseLayers = { ...layers.base }; // Load initial overlays if (settings.initoverlays) { for (const name in settings.initoverlays) { if (layers.overlays[name]) { layers.overlays[name].addTo(map); AppState.activeLayers.push(name); } } } // Load specified overlays or all settings layers if (settings.theoverlays) { for (const index in settings.theoverlays) { const layerName = settings.theoverlays[index]; if (overlays[layerName]) { overlays[layerName].addTo(map); } } } else { // Display all settings layers AppState.masterGroup = new L.featureGroup(); for (const name in layers.settings.overlays) { layers.settings.overlays[name].addTo(map); layers.settings.overlays[name].addTo(AppState.masterGroup); AppState.masterGroupAdded = true; overlays[name] = layers.settings.overlays[name]; } } // Set view if no geojson layers if (!settings.layers?.geojson) { this.setView(); } // Add layer control L.control.layers(baseLayers, overlays, { collapsed: true }).addTo(map); // Add scale L.control.scale().addTo(map); }, setView() { const { settings, masterGroup, masterGroupAdded } = AppState; console.log('Setting map view'); if (settings.view) { console.log('Using view setting'); map.setView( [settings.view.latitude, settings.view.longitude], settings.view.zoom ); } else if (settings.masterlayer && AppState.layers.settings.overlays[settings.masterlayer]) { console.log('Using master layer'); map.fitBounds(AppState.layers.settings.overlays[settings.masterlayer].getBounds(), { paddingTopLeft: [20, 100], paddingBottomRight: [20, 20] }); } else if (masterGroupAdded && masterGroup) { console.log('Using master group'); map.fitBounds(masterGroup.getBounds(), { paddingTopLeft: [20, 100], paddingBottomRight: [20, 20] }); masterGroup.bringToFront(); } else { console.log('No bounds method found'); } if (settings.zoom) { console.log('Applying zoom setting'); map.setZoom(settings.zoom); } }, }; // ============================================================================= // WATERMARK // ============================================================================= const Watermark = { init() { L.Control.Watermark = L.Control.extend({ onAdd() { const img = L.DomUtil.create('img'); img.src = AppState.settings.watermark || '/objects/maps/mapwatermarkredoak.png'; img.style.width = '50px'; return img; }, onRemove() {}, }); L.control.watermark = (opts) => new L.Control.Watermark(opts); L.control.watermark({ position: 'bottomleft' }).addTo(map); }, }; // ============================================================================= // EXTERNAL OVERLAYS // ============================================================================= const ExternalOverlays = { init() { this.initBaseLayers(); this.initOverlays(); }, initBaseLayers() { const base = AppState.layers.base; // Google layers const googleTypes = ['roadmap', 'satellite', 'hybrid']; const googleNames = ['RoadMap', 'Satelite', 'Hybrid']; googleTypes.forEach((type, i) => { base[googleNames[i]] = L.gridLayer.googleMutant({ maxZoom: 24, type, }); }); // Default is hybrid base['Default'] = L.gridLayer.googleMutant({ maxZoom: 24, type: 'hybrid', }); // ESRI Topo layers base['Topo'] = L.esri.tiledMapLayer({ maxZoom: 24, maxNativeZoom: 16, url: CONFIG.layers.esri.topo, }); base['TopoAlt'] = L.esri.tiledMapLayer({ maxZoom: 24, maxNativeZoom: 16, url: CONFIG.layers.esri.topoAlt, }); base['ShadedHillside'] = L.esri.tiledMapLayer({ maxZoom: 24, url: CONFIG.layers.esri.hillshade, }); // Agent-only layers if (typeof maprealm !== 'undefined' && maprealm === 'agent') { base['Leafless (slow)'] = L.esri.dynamicMapLayer({ url: CONFIG.layers.esri.leafless, }); } }, initOverlays() { const overlays = AppState.layers.overlays; // Flood layers overlays['FloodPUB'] = this.createDynamicLayer(CONFIG.layers.esri.floodPublic, [1], 0.4); // Property layers overlays['TaxMaps'] = this.createDynamicLayer(CONFIG.layers.esri.taxMaps, [0, 1], 1, 'svg'); // Boundary layers overlays['ForestandParks'] = this.createDynamicLayer(CONFIG.layers.esri.forestParks, [0, 3, 7], 0.5, 'svg'); overlays['Trails'] = this.createDynamicLayer(CONFIG.layers.esri.trails, [0, 1, 2, 3, 4, 5, 6, 7], 0.8); overlays['City Bounds'] = this.createDynamicLayer(CONFIG.layers.esri.politicalBoundary, [1], 0.4); overlays['Counties'] = this.createDynamicLayer(CONFIG.layers.esri.politicalBoundary, [0], 0.6); // Communication overlays['CELL SERVICE'] = L.esri.tiledMapLayer({ url: CONFIG.layers.esri.cellService, opacity: 0.6, }); overlays['INTERNET'] = this.createDynamicLayer(CONFIG.layers.esri.internet, [0, 1, 2, 3, 6], 0.8); // Terrain overlays['1ftTopo'] = L.esri.dynamicMapLayer({ url: CONFIG.layers.esri.contour1ft, useCors: true, }); // Addresses overlays['Addresses'] = this.createAddressLayer(); }, createDynamicLayer(url, layers = null, opacity = 1, format = 'png', useCors = true) { const options = { url, f: 'image', format, opacity, }; if (layers) { options.layers = layers; } if (!useCors) { options.useCors = false; } return L.esri.dynamicMapLayer(options); }, createAddressLayer() { return L.esri.featureLayer({ url: CONFIG.layers.esri.addresses, minZoom: 16, pointToLayer(geojson, latlng) { const props = geojson.properties; const address = `${props.FULLADDR}
${props.MUNICIPALITY}, ${props.State} ${props.Zip}`; const googleLink = `https://www.google.com/search?q=${encodeURIComponent( `${props.FULLADDR} ${props.MUNICIPALITY}, ${props.State} ${props.Zip}` )}`; const popup = `${address}
Google`; return L.marker(latlng, { icon: Icons.getIcon('wvumailboxIcon'), }).bindPopup(popup); }, }); }, }; // ============================================================================= // MARKER ROTATION EXTENSION // ============================================================================= const MarkerRotation = { init() { const proto_initIcon = L.Marker.prototype._initIcon; const proto_setPos = L.Marker.prototype._setPos; const oldIE = L.DomUtil.TRANSFORM === 'msTransform'; L.Marker.addInitHook(function() { const iconOptions = this.options.icon?.options; const iconAnchor = iconOptions?.iconAnchor; const anchorString = iconAnchor ? `${iconAnchor[0]}px ${iconAnchor[1]}px` : 'center bottom'; this.options.rotationOrigin = this.options.rotationOrigin || anchorString; this.options.rotationAngle = this.options.rotationAngle || 0; this.on('drag', (e) => e.target._applyRotation()); }); L.Marker.include({ _initIcon: proto_initIcon, _setPos(pos) { proto_setPos.call(this, pos); this._applyRotation(); }, _applyRotation() { if (this.options.rotationAngle) { this._icon.style[`${L.DomUtil.TRANSFORM}Origin`] = this.options.rotationOrigin; if (oldIE) { this._icon.style[L.DomUtil.TRANSFORM] = `rotate(${this.options.rotationAngle}deg)`; } else { this._icon.style[L.DomUtil.TRANSFORM] += ` rotateZ(${this.options.rotationAngle}deg)`; } } }, setRotationAngle(angle) { this.options.rotationAngle = angle; this.update(); return this; }, setRotationOrigin(origin) { this.options.rotationOrigin = origin; this.update(); return this; }, }); }, }; // ============================================================================= // EXPORT MAP // ============================================================================= const ExportMap = { export(event) { event.preventDefault(); const currentMap = { mapid: typeof mapid !== 'undefined' ? mapid : null, leaflet: { basemap: AppState.currentBaseLayer, theoverlays: {}, view: { latitude: map.getCenter().lat, longitude: map.getCenter().lng, zoom: map.getZoom(), }, }, height: $('#map').height(), width: $('#map').width(), }; AppState.activeLayers.forEach((layer, i) => { currentMap.leaflet.theoverlays[i] = layer; }); $('#currentmapfield').val(JSON.stringify(currentMap)); $('#exportform').submit(); }, }; // ============================================================================= // UTILITY FUNCTIONS // ============================================================================= const Utils = { setMapSize(size) { const [width, height] = size.split('x'); const mapDiv = $(`#${typeof mapdivid !== 'undefined' ? mapdivid : 'map'}`); mapDiv.height(height); mapDiv.width(width); map.invalidateSize(); }, }; // ============================================================================= // MAP FILTERS (Price/Acres) // ============================================================================= const MapFilters = { priceRanges: [ { label: 'Any Price', min: null, max: null }, { label: 'Under $50k', min: 0, max: 50000 }, { label: '$50k - $100k', min: 50000, max: 100000 }, { label: '$100k - $250k', min: 100000, max: 250000 }, { label: '$250k - $500k', min: 250000, max: 500000 }, { label: '$500k+', min: 500000, max: null }, ], acresRanges: [ { label: 'Any Acres', min: null, max: null }, { label: 'Under 5', min: 0, max: 5 }, { label: '5 - 25', min: 5, max: 25 }, { label: '25 - 100', min: 25, max: 100 }, { label: '100+', min: 100, max: null }, ], currentFilters: { price: 0, acres: 0, }, init() { // Only initialize for homepage lasso mode setTimeout(() => { if (AppState.settings.lassomode === 'homemodal') { this.addFilterControls(); } }, 1100); }, addFilterControls() { const mapContainer = map.getContainer(); const lassoOverlay = mapContainer.querySelector('.lasso-overlay'); if (!lassoOverlay) { console.log('Lasso overlay not found, skipping filter controls'); return; } // Create filter container const filterContainer = document.createElement('div'); filterContainer.className = 'map-filter-container'; filterContainer.style.cssText = 'display:flex;gap:8px;align-items:center;flex-wrap:wrap;'; // Create price dropdown const priceSelect = this.createDropdown('price', this.priceRanges, 'Filter by price'); filterContainer.appendChild(priceSelect); // Create acres dropdown const acresSelect = this.createDropdown('acres', this.acresRanges, 'Filter by acreage'); filterContainer.appendChild(acresSelect); // Add to lasso overlay lassoOverlay.appendChild(filterContainer); console.log('Map filter controls added'); }, createDropdown(type, options, title) { const select = document.createElement('select'); select.className = 'map-filter-select'; select.id = `filter-${type}`; select.title = title; options.forEach((opt, index) => { const option = document.createElement('option'); option.value = index; option.textContent = opt.label; select.appendChild(option); }); select.addEventListener('change', (e) => { this.currentFilters[type] = parseInt(e.target.value, 10); this.applyFilters(); }); return select; }, applyFilters() { const priceFilter = this.priceRanges[this.currentFilters.price]; const acresFilter = this.acresRanges[this.currentFilters.acres]; let visibleCount = 0; let hiddenCount = 0; // Helper function to process a marker const processMarker = (layer) => { if (layer instanceof L.Marker && !(layer instanceof L.MarkerCluster)) { // Get marker data - stored on the marker itself const price = layer.price ?? 0; const acres = layer.acres ?? 0; let visible = true; // Check price filter if (priceFilter.min !== null || priceFilter.max !== null) { if (priceFilter.min !== null && price < priceFilter.min) visible = false; if (priceFilter.max !== null && price >= priceFilter.max) visible = false; } // Check acres filter if (visible && (acresFilter.min !== null || acresFilter.max !== null)) { if (acresFilter.min !== null && acres < acresFilter.min) visible = false; if (acresFilter.max !== null && acres >= acresFilter.max) visible = false; } if (visible) { this.showMarker(layer); visibleCount++; } else { this.hideMarker(layer); hiddenCount++; } } }; // Iterate through marker groups in AppState.layers.settings.overlays for (const layerName in AppState.layers.settings.overlays) { const group = AppState.layers.settings.overlays[layerName]; if (group && typeof group.eachLayer === 'function') { group.eachLayer(processMarker); } } // Also check direct map layers (fallback) map.eachLayer(processMarker); console.log(`Filters applied: ${visibleCount} visible, ${hiddenCount} hidden`); }, showMarker(marker) { if (marker._icon) { marker._icon.style.display = ''; marker._icon.classList.remove('map-filter-hidden'); } if (marker._shadow) { marker._shadow.style.display = ''; } marker._filtered = false; }, hideMarker(marker) { if (marker._icon) { marker._icon.style.display = 'none'; marker._icon.classList.add('map-filter-hidden'); } if (marker._shadow) { marker._shadow.style.display = 'none'; } marker._filtered = true; }, isMarkerVisible(marker) { return !marker._filtered; }, }; // ============================================================================= // LASSO SELECTION (Homepage Modal) // ============================================================================= const Lasso = { init() { // Only initialize if L.lasso exists (plugin loaded) if (typeof L.lasso === 'undefined') { console.log('Lasso plugin not loaded'); return; } // Check if lassomode is set in settings (delayed check after settings load) setTimeout(() => { if (AppState.settings.lassomode === 'homemodal') { this.setupHomeLasso(); } }, 1000); }, setupHomeLasso() { console.log('Setting up homepage lasso'); // Create lasso control AppState.lasso.control = L.lasso(map); // Get UI elements const toggleBtn = document.getElementById('toggleLasso'); const containRadio = document.getElementById('contain'); const intersectRadio = document.getElementById('intersect'); const enabledDisplay = document.getElementById('lassoEnabled'); const resultDisplay = document.getElementById('lassoResult'); // Lasso events map.on('lasso.finished', (event) => { this.handleLassoFinished(event.layers); }); map.on('lasso.enabled', () => { if (enabledDisplay) enabledDisplay.innerHTML = 'Enabled'; this.resetSelectedState(); }); map.on('lasso.disabled', () => { if (enabledDisplay) enabledDisplay.innerHTML = 'Disabled'; }); // Reset on mousedown map.on('mousedown', () => { this.resetSelectedState(); }); // Toggle button if (toggleBtn) { toggleBtn.addEventListener('click', () => { if (AppState.lasso.control.enabled()) { AppState.lasso.control.disable(); } else { AppState.lasso.control.enable(); } }); } // Contain/Intersect options if (containRadio) { containRadio.addEventListener('change', () => { AppState.lasso.control.setOptions({ intersect: intersectRadio?.checked }); }); } if (intersectRadio) { intersectRadio.addEventListener('change', () => { AppState.lasso.control.setOptions({ intersect: intersectRadio.checked }); }); } // Add lasso button to map controls this.addLassoButton(); }, addLassoButton() { // Create a prominent overlay button at top center of map const mapContainer = map.getContainer(); const overlay = document.createElement('div'); overlay.className = 'lasso-overlay'; overlay.style.cssText = 'position:absolute;top:10px;left:50%;transform:translateX(-50%);z-index:1000;display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center;'; const button = document.createElement('button'); button.className = 'lasso-overlay-btn'; button.innerHTML = '✏️ Select Properties'; button.title = 'Click to enable lasso tool, then draw on map to select multiple properties'; button.style.cssText = 'padding:10px 20px;font-size:16px;font-weight:600;background:linear-gradient(135deg,#0d6efd,#0b5ed7);color:#fff;border:none;border-radius:25px;cursor:pointer;box-shadow:0 4px 15px rgba(13,110,253,0.4);transition:all 0.3s ease;display:flex;align-items:center;gap:8px;'; button.onmouseenter = function() { if (!AppState.lasso.control.enabled()) { this.style.transform = 'scale(1.05)'; this.style.boxShadow = '0 6px 20px rgba(13,110,253,0.5)'; } }; button.onmouseleave = function() { if (!AppState.lasso.control.enabled()) { this.style.transform = 'scale(1)'; this.style.boxShadow = '0 4px 15px rgba(13,110,253,0.4)'; } }; button.onclick = function(e) { e.preventDefault(); e.stopPropagation(); if (AppState.lasso.control.enabled()) { AppState.lasso.control.disable(); button.innerHTML = '✏️ Select Properties'; button.style.background = 'linear-gradient(135deg,#0d6efd,#0b5ed7)'; button.style.boxShadow = '0 4px 15px rgba(13,110,253,0.4)'; } else { AppState.lasso.control.enable(); button.innerHTML = '✏️ Drawing Active - Draw on Map'; button.style.background = 'linear-gradient(135deg,#198754,#157347)'; button.style.boxShadow = '0 4px 15px rgba(25,135,84,0.5)'; } }; // Update button when lasso is disabled (e.g., after selection) map.on('lasso.disabled', function() { button.innerHTML = '✏️ Select Properties'; button.style.background = 'linear-gradient(135deg,#0d6efd,#0b5ed7)'; button.style.boxShadow = '0 4px 15px rgba(13,110,253,0.4)'; }); overlay.appendChild(button); mapContainer.appendChild(overlay); }, resetSelectedState() { map.eachLayer((layer) => { if (layer instanceof L.Marker && !(layer instanceof L.MarkerCluster)) { if (layer.originalIcon) { layer.setIcon(layer.originalIcon); } if (layer._icon) { layer._icon.classList.remove('lasso-selected'); } } else if (layer instanceof L.Path) { if (layer.originalStyle) { layer.setStyle(layer.originalStyle); delete layer.originalStyle; } } }); }, handleLassoFinished(layers) { this.resetSelectedState(); if (layers.length === 0) { return; } // Collect selected property IDs (skip filtered/hidden markers) const selectedIds = []; layers.forEach((layer) => { if (layer instanceof L.Marker && !(layer instanceof L.MarkerCluster)) { // Skip markers that are hidden by filters if (layer._filtered) { return; } // Highlight selected marker if (!layer.originalIcon && layer.options.icon) { layer.originalIcon = layer.options.icon; } if (layer._icon) { layer._icon.classList.add('lasso-selected'); } // Collect ID (userid is the property record ID) if (layer.userid) { selectedIds.push(layer.userid); } } }); if (selectedIds.length === 0) { return; } // Show modal with results this.showModal(selectedIds); }, showModal(propertyIds) { // If we're in an iframe, send message to parent window instead if (window.parent !== window) { window.parent.postMessage({ type: 'lassoSelection', propertyIds: propertyIds, count: propertyIds.length }, '*'); return; } const modal = document.getElementById('lassoModal'); const modalBody = document.getElementById('lassoModalBody'); if (!modal || !modalBody) { console.error('Lasso modal not found'); return; } // Show loading state modalBody.innerHTML = `

Loading ${propertyIds.length} properties...

`; // Show the modal using our custom function if (typeof openLassoModal === 'function') { openLassoModal(); } else { modal.style.display = 'block'; } // Fetch property details const formData = new FormData(); formData.append('propertyids', JSON.stringify(propertyIds)); fetch(CONFIG.urls.lassoHome, { method: 'POST', body: formData }) .then(response => response.text()) .then(html => { modalBody.innerHTML = html; }) .catch(err => { modalBody.innerHTML = `
Error loading properties: ${err.message}
`; }); }, }; // ============================================================================= // GLOBAL FUNCTION EXPORTS (for backward compatibility) // ============================================================================= // Export functions to global scope for external use window.exportmap = ExportMap.export.bind(ExportMap); window.leafletapp_setsize = Utils.setMapSize; window.leafletapp_onclickfun = Events.handleClick; // ============================================================================= // INITIALIZATION // ============================================================================= // Initialize marker rotation extension MarkerRotation.init(); // Initialize the map application MapApp.init();