/**
* 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',
},
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: {},
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();
},
};
// =============================================================================
// 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
);
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) {
map.fitBounds(markerGroup.getBounds());
}
}
},
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());
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());
} else if (masterGroupAdded && masterGroup) {
console.log('Using master group');
map.fitBounds(masterGroup.getBounds());
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();
},
};
// =============================================================================
// 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();