diff --git a/web/mapComparison-body.jsp b/web/mapComparison-body.jsp index f7cca2fe..ba1bc046 100755 --- a/web/mapComparison-body.jsp +++ b/web/mapComparison-body.jsp @@ -96,7 +96,7 @@ //AntwebUtil.log("mapComparison-body.jsp taxon:" + thisChild); innerloop = 0; - while (innerloop < 3) { + while (innerloop < 1) { //AntwebUtil.log("mapComparison-body.jsp taxon:" + thisChild + " innerloop:" + innerloop); diff --git a/web/maps/googleMapInclude.jsp b/web/maps/googleMapInclude.jsp index 282fc4c5..4a49991d 100644 --- a/web/maps/googleMapInclude.jsp +++ b/web/maps/googleMapInclude.jsp @@ -1,7 +1,3 @@ - - - - <%@ page import="org.calacademy.antweb.Map" %> <%@ page import="org.calacademy.antweb.util.UserAgentTracker" %> @@ -9,154 +5,87 @@ boolean displayMap = true; if (HttpUtil.isBot(request)) { - out.println("Log In to see maps."); - displayMap = false; - } %> - -<% -/* - Here we generate the actual map. - -Includes /maps/googleMapPreInclude.jsp and /maps/googleMapInclude.jsp - dynamicMap-body.jsp - specimen-body.jsp - group-body.jsp: - collection-body.jsp: - taxonPage-body.jsp: - locality-body.jsp: - search/fieldGuide-body.jsp: - mapComparison-body.jsp - overview-body.jsp - -googleMapPreInclude.jsp - script /maps/drawGoogleMap.js - -googleMapInclude.jsp - calls.push("< %= googleMapFunction % >"); - -// V2? -includeMap.jsp - Not used? Yes it is. In the edit geolocale curator page and for localityOverviews that don't have a map but do have a centroid. - Probably cases that should be eliminated. - include /maps/googleMap.jsp - -googleMap.jsp - google.maps.event.addDomListener(window, 'load', makeMap) - -map.jsp - not used? Testing. http://localhost/antweb/maps/map.jsp - google.maps.event.addDomListener(window, 'load', initialize); - -map.jsp, googleMap.jsp and includeMap.jsp to be removed once the above is resolved (see incudeMap.jsp). -*/ + out.println("Log In to see maps."); + displayMap = false; + } %> - <% - //A.log("googleMapInclude.jsp map:" + map + " displayMap:" + displayMap); - - // We assume that map, object{"specimen", "taxon", "locality", "collection"}, mapType, and objectName are already set - if (map != null && displayMap) { - - String googleMapFunction = map.getGoogleMapFunction(); - - if ((googleMapFunction != null) && (googleMapFunction.length() > 0)) { - - //A.log("googleMapInclude.jsp object:" + object + " objectName:" + objectName + " mapType:" + mapType); - //AntwebUtil.logStackTrace(); - Map.addToDisplayMapCount(); - Map.addToDisplayMapCount(mapType); - - //String divName = map.getMapName(); - int n = googleMapFunction.indexOf("', '") + 4; - int o = googleMapFunction.indexOf("'", n); - String divName = googleMapFunction.substring(n, o); - String mapSize = "small"; - if (object.equals("dynamic")) { - mapSize = "large"; - googleMapFunction = googleMapFunction.replaceFirst("small","big"); - } - - if ("large".equals(mapSize)) { - if (map.getSubtitle() != null) out.println("

" + map.getSubtitle() + "

"); - } - - //A.log("googleMapInclude.jsp divName:" + divName + " googleMapFunction:" + googleMapFunction + " object:" + object); - // String heightAndWidth = (!object.equals("dynamic")) ? "height:232px;width:232px;" : "height:400px;width:926px;"; - String heightAndWidth = new String(); - if (object.equals("dynamic")) { - heightAndWidth = "height:650px;width:974px;"; - } else if (object.equals("thirds")) { // fieldGuide and mapComparison? - heightAndWidth = "height:262px;width:262px;"; - } else { - heightAndWidth = "height:232px;width:232px;"; - } + if (map != null && displayMap) { + String googleMapFunction = map.getGoogleMapFunction(); + + if (googleMapFunction != null && googleMapFunction.length() > 0) { + Map.addToDisplayMapCount(); + Map.addToDisplayMapCount(mapType); + + int n = googleMapFunction.indexOf("', '") + 4; + int o = googleMapFunction.indexOf("'", n); + String divName = googleMapFunction.substring(n, o); + + String mapSize = "small"; + if ("dynamic".equals(object)) { + mapSize = "large"; + googleMapFunction = googleMapFunction.replaceFirst("small", "big"); + } + + if ("large".equals(mapSize) && map.getSubtitle() != null) + out.println("

" + map.getSubtitle() + "

"); + + String heightAndWidth; + if ("dynamic".equals(object)) { + heightAndWidth = "height:650px;width:974px;"; + } else if ("thirds".equals(object)) { + heightAndWidth = "height:262px;width:262px;"; + } else { + heightAndWidth = "height:232px;width:232px;"; + } %> -
- -<% - // We now prevent this on the server by disallowing more than 1000 points. - //if (googleMapFunction.length() > 70000) { - // AntwebUtil.log("info", "taxonPage-body.jsp: googleMapFunction length is " + googleMapFunction.length() + " for " + HttpUtil.getRequestInfo(request)); - //} - - if ((!object.equals("thirds")) && (!object.equals("dynamic"))) { - String overviewParam = ""; - - - //A.log("googleMapInclude.jsp overview:" + overview); - // This was thought to be unnecessary. It is here: http://localhost/antweb/description.do?genus=tetramorium&species=simillimum&rank=species&countryName=Brazil - if (overview != null) { - overviewParam = overview.getParams(); //"&" + - } - - //A.log("googleMapInclude.jsp object:" + object + " objectName:" + objectName + " overviewParam:" + overviewParam); - String objectParam = ""; - //when is this necessary? - if ("taxonName".equals(object)) { - objectParam = object + "=" + objectName + "&"; - } - if ("specimen".equals(object)) { - objectParam = object + "=" + objectName + "&"; - } - if ("locality".equals(object)) { - objectParam = object + "=" + objectName + "&"; - overviewParam = ""; - } - if ("collection".equals(object)) { - objectParam = object + "=" + objectName + "&"; - overviewParam = ""; - } - -%> -
Enlarge Map
-<% - } // We have a googleMapFunction + +
+
+ +<% if (!"thirds".equals(object) && !"dynamic".equals(object)) { + String overviewParam = ""; + if (overview != null) overviewParam = overview.getParams(); + + String objectParam = ""; + if ("taxonName".equals(object) || "specimen".equals(object)) { + objectParam = object + "=" + objectName + "&"; + } else if ("locality".equals(object) || "collection".equals(object)) { + objectParam = object + "=" + objectName + "&"; + overviewParam = ""; + } %> - - +
+ + Enlarge Map + +
+<% } %> + + - <% - //A.log("googleMapInclude.jsp googleMapFunction:" + googleMapFunction); - if ("large".equals(mapSize)) { - if ((AntwebProps.isDevMode() || LoginMgr.isAdmin(request)) && map.getInfo() != null) out.println("Map Info: " + map.getInfo()); - } -%> - -<% - } else { - if (AntwebProps.isDevMode()) { - String aMessage = "No googleMapFunction"; - A.log("googleMapInclude.jsp " + aMessage); - out.println(aMessage); - } + } else { + if (AntwebProps.isDevMode()) { + String msg = "No map data found for " + object + " (" + objectName + ")"; + out.println("
" + msg + "
"); + } + } } - } // map != null %> - \ No newline at end of file + diff --git a/web/maps/googleMapPreInclude.jsp b/web/maps/googleMapPreInclude.jsp index 1b3607cb..147a1409 100644 --- a/web/maps/googleMapPreInclude.jsp +++ b/web/maps/googleMapPreInclude.jsp @@ -1,20 +1,18 @@ -<%-- /web/maps/googleMapPreInclude.jsp (Leaflet) --%> -<% - // Point this to YOUR existing microservice - final String MAPS_BASE = "http://localhost:8081"; -%> +<%-- /web/maps/googleMapPreInclude.jsp (Local Leaflet Integration) --%> <% if (!HttpUtil.isBot(request)) { %> - + + + + - - + + + - - <% } %> diff --git a/web/maps/sdk/drawMap.js b/web/maps/sdk/drawMap.js index 5797ca5b..d695ae6e 100644 --- a/web/maps/sdk/drawMap.js +++ b/web/maps/sdk/drawMap.js @@ -1,25 +1,39 @@ (function (global) { - // ---- SETTINGS ---- - const USE_CLUSTER = false; // set to true if you ever want the numeric balloons back const DOT_STYLE = { - radius: 5, - color: "#ffffff", // white border stroke - weight: 2, // thicker border - fillColor: "#ff3b3b", // inner red + radius: 6, + color: "#ffffff", + weight: 2, + fillColor: "#ff3b3b", fillOpacity: 1 }; + const style = document.createElement("style"); + style.textContent = ` + .leaflet-container { position: relative !important; z-index: 0 !important; } + .leaflet-popup-content a { + color: #0074d9; + text-decoration: none; + font-weight: 600; + } + .leaflet-popup-content a:hover { text-decoration: underline; } + .leaflet-popup-content img { + max-width: 160px; + margin-top: 4px; + border-radius: 6px; + display: block; + } + `; + document.head.appendChild(style); - let map = null; - let layer = null; // LayerGroup (no cluster) OR MarkerClusterGroup - let dots = []; // keep references to markers for fitBounds() + const mapRegistry = {}; - function ensureMap(divName, lat, lon, zoom) { - if (map) return map; + function initMap(divName, lat, lon, zoom) { + if (mapRegistry[divName]) return mapRegistry[divName]; let el = document.getElementById(divName) || document.getElementById("map") || document.getElementById("map-canvas"); + if (!el) { el = document.createElement("div"); el.id = divName || "map"; @@ -28,91 +42,133 @@ document.body.appendChild(el); } - const cfg = global.__MAP_SVC_CFG__ || {}; - const tileUrl = cfg.TILE_URL || "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; - const tileAttr = cfg.TILE_ATTR || "© OpenStreetMap contributors"; - - // preferCanvas makes thousands of circleMarkers smooth - map = L.map(el, { preferCanvas: true }) - .setView([parseFloat(lat) || 0, parseFloat(lon) || 0], zoom || 2); - - L.tileLayer(tileUrl, { maxZoom: 19, attribution: tileAttr }).addTo(map); - - layer = USE_CLUSTER - ? L.markerClusterGroup({ chunkedLoading: true }) - : L.layerGroup(); - - map.addLayer(layer); - return map; + const map = L.map(el, { + preferCanvas: true, + zoomControl: false, + attributionControl: false + }).setView([parseFloat(lat) || 0, parseFloat(lon) || 0], zoom || 2); + + // --- Base layers --- + const mapLayer = L.tileLayer( + "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", + { maxZoom: 19, attribution: "© OpenStreetMap contributors" } + ); + const terrainLayer = L.tileLayer( + "https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{z}/{y}/{x}", + { maxZoom: 13, attribution: "Terrain © Esri" } + ); + const satelliteLayer = L.tileLayer( + "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + { maxZoom: 19, attribution: "Imagery © Esri, Maxar, Earthstar Geographics" } + ); + const labelOverlay = L.tileLayer( + "https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}", + { maxZoom: 19, attribution: "Labels © Esri" } + ); + const hybridLayer = L.layerGroup([satelliteLayer, labelOverlay]); + + mapLayer.addTo(map); + L.control.layers( + { "Map": mapLayer, "Terrain": terrainLayer, "Satellite": hybridLayer }, + null, + { position: "topright" } + ).addTo(map); + + L.control.zoom({ position: "bottomright" }).addTo(map); + L.control.attribution({ prefix: "", position: "bottomleft" }).addTo(map); + if (L.control.fullscreen) L.control.fullscreen({ position: "topright" }).addTo(map); + + // --- No clustering: show all dots directly --- + const layer = L.layerGroup().addTo(map); + + mapRegistry[divName] = { map, layer, dots: [] }; + + // Fix sizing for hidden grid cells + setTimeout(() => map.invalidateSize(), 400); + return mapRegistry[divName]; } - function addDot(lat, lon, html) { - if (lat == null || lon == null) return null; + function addDot(bucket, lat, lon, html) { + if (!bucket) return null; const y = parseFloat(lat), x = parseFloat(lon); - if (Number.isNaN(y) || Number.isNaN(x)) return null; - - // circleMarker = small red dot (no default pin, no numbers) - const m = L.circleMarker([y, x], DOT_STYLE); - if (html) m.bindPopup(html); - layer.addLayer(m); - dots.push(m); - return m; + if (isNaN(y) || isNaN(x)) return null; + + const marker = L.circleMarker([y, x], DOT_STYLE); + if (html) marker.bindPopup(html, { autoClose: true, closeButton: true }); + marker.on("click", () => marker.openPopup()); + bucket.layer.addLayer(marker); + bucket.dots.push(marker); + return marker; } - function fitToDots(padding) { - if (!dots.length) return; - const group = L.featureGroup(dots); - map.fitBounds(group.getBounds(), { padding: padding || [30, 30] }); + function fitToDots(bucket) { + if (!bucket || !bucket.dots.length) return; + const group = L.featureGroup(bucket.dots); + bucket.map.fitBounds(group.getBounds(), { padding: [30, 30] }); } - // ---- Public API (keeps your current function names) ---- + // --- Basic dot map --- global.drawMap = function (divName, latArray, lonArray) { - const lat0 = latArray?.length ? parseFloat(latArray[0]) : 0; - const lon0 = lonArray?.length ? parseFloat(lonArray[0]) : 0; - ensureMap(divName, lat0, lon0, 5); - - const n = Math.min(latArray?.length || 0, lonArray?.length || 0); - for (let i = 0; i < n; i++) addDot(latArray[i], lonArray[i]); - fitToDots(); + const bucket = initMap(divName, latArray?.[0] || 0, lonArray?.[0] || 0, 4); + for (let i = 0; i < latArray.length; i++) { + addDot(bucket, latArray[i], lonArray[i]); + } + fitToDots(bucket); }; + // --- Localities --- global.drawMapLocalities = function (divName, latArray, lonArray, nameArray, codeArray) { - const lat0 = latArray?.length ? parseFloat(latArray[0]) : 0; - const lon0 = lonArray?.length ? parseFloat(lonArray[0]) : 0; - ensureMap(divName, lat0, lon0, 5); - - const n = Math.min(latArray?.length || 0, lonArray?.length || 0); - for (let i = 0; i < n; i++) { - const name = (nameArray && nameArray[i]) || ""; - const code = (codeArray && codeArray[i]) || ""; - const html = `
Locality: ${name}
Code: ${code}
`; - addDot(latArray[i], lonArray[i], html); + const bucket = initMap(divName, latArray?.[0] || 0, lonArray?.[0] || 0, 5); + for (let i = 0; i < latArray.length; i++) { + const name = nameArray?.[i] || ""; + const code = codeArray?.[i] || ""; + const url = `/locality.do?code=${encodeURIComponent(code)}`; + const html = ` +
+ ${name || code}
+ ${code} +
`; + addDot(bucket, latArray[i], lonArray[i], html); } - fitToDots(); + fitToDots(bucket); }; + // --- Specimens --- global.drawMapSpecimens = function (divName, latArray, lonArray, nameArray, codeArray, imageArray) { - const lat0 = latArray?.length ? parseFloat(latArray[0]) : 0; - const lon0 = lonArray?.length ? parseFloat(lonArray[0]) : 0; - ensureMap(divName, lat0, lon0, 5); - - const n = Math.min(latArray?.length || 0, lonArray?.length || 0); - for (let i = 0; i < n; i++) { - const name = (nameArray && nameArray[i]) || ""; - const code = (codeArray && codeArray[i]) || ""; - const img = (imageArray && imageArray[i]) || ""; - const imgTag = img ? `
` : ""; - const html = `
Specimen: ${name}
Code: ${code}${imgTag}
`; - addDot(latArray[i], lonArray[i], html); + const bucket = initMap(divName, latArray?.[0] || 0, lonArray?.[0] || 0, 5); + for (let i = 0; i < latArray.length; i++) { + const name = nameArray?.[i] || ""; + const code = codeArray?.[i] || ""; + const img = imageArray?.[i] || ""; + const url = `/specimen.do?name=${encodeURIComponent(code)}`; + const imgTag = img ? `
${name}` : ""; + const html = ` +
+ ${name}
+ ${code}${imgTag} +
`; + addDot(bucket, latArray[i], lonArray[i], html); } - fitToDots(); + fitToDots(bucket); }; + // --- Single Point --- global.drawMapSinglePoint = function (divName, lat, lon, name, code, img) { - ensureMap(divName, parseFloat(lat) || 0, parseFloat(lon) || 0, 8); - const imgTag = img ? `
` : ""; - const html = `
${name || ""}
${code || ""}${imgTag}
`; - addDot(lat, lon, html); - fitToDots(); + const bucket = initMap(divName, lat, lon, 8); + const url = `/specimen.do?name=${encodeURIComponent(code)}`; + const imgTag = img ? `
${name}` : ""; + const html = ` +
+ ${name}
+ ${code}${imgTag} +
`; + addDot(bucket, lat, lon, html); }; + + // --- Resize all maps on load --- + document.addEventListener("DOMContentLoaded", () => { + setTimeout(() => { + Object.values(mapRegistry).forEach(b => b.map.invalidateSize()); + }, 800); + }); })(window);