/**
* 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...