import * as mapsources from "../../external/maps/mapsources.js";
import tileprov from "../../external/maps/tileprov.js";
import * as divisors from "../visualize/divisors.js";
import * as visualize from "../visualize/visualize.js";
import geospatial from "../../external/gridded-data/geospatial/geospatial.js";
/**
* Module for mapping data.
* @class map
*/
//Controllers, map and layers
//Most variables are left as internal variables for control when the hydrolang instance is live.
var osmap,
layercontroller,
drawings,
isDrawToolAdded = false,
drawControl;
// Google Maps specific variables
var mapId, mapType;
//Global variables for library usages.
window.baselayers = {};
window.overlayers = {};
window.usedColors = new Set()
/**
* Calls the map type according to the user input. The renderMap function is required
* for map visualization.
* @function loader
* @memberof map
* @param {Object} params - Contains: maptype (google or osm[leaflet])
* @param {Object} args: Contains: key (required by google)
* @returns {Element} Libraries appended to the header of webpage.
* @example
* hydro.map.loader({params: {maptype: 'osm'}, args: {key: 'somekey'}})
*/
async function loader({ params = { maptype: "leaflet" }, args = {}, data = {} } = {}) {
//For google maps API.
if (params.maptype == "google") {
const gApiKey = args.key,
//call the class constructor.
gmapApi = new mapsources.googlemapsapi(gApiKey);
await gmapApi.load();
}
//For leaflet API.
if (params.maptype == "leaflet") {
//call the class constructor.
const mapper = new mapsources.leafletosmapi();
await mapper.load();
}
}
function removeMap() {
if (osmap) {
// Remove the map and all its layers
osmap.remove();
// Clean up global variables
osmap = null;
layercontroller = null;
window.baselayers = {};
window.overlayers = {};
window.usedColors = new Set();
}
}
function reinitializeController(mapType) {
if (!osmap) return;
// Remove existing controller if it exists
if (layercontroller) {
if (mapType === "google") {
const controlDiv = document.getElementById("gmap-control-div");
if (controlDiv) controlDiv.remove();
} else if (mapType === "leaflet") {
layercontroller.remove();
}
layercontroller = null;
}
// Reinitialize the controller
if (mapType === "google") {
// Google Maps controller initialization
// ... existing Google Maps controller code ...
return
} else if (mapType === "leaflet") {
layercontroller = new L.control.layers().addTo(osmap);
// Readd existing layers
Object.entries(window.baselayers).forEach(([name, layer]) => {
layercontroller.addBaseLayer(layer, name);
});
Object.entries(window.overlayers).forEach(([name, layer]) => {
layercontroller.addOverlay(layer, name);
});
}
}
/**
* Layer function for appending tiles, geodata, markers, kml, georaster, or drawing tools to a map.
* @function Layers
* @memberof map
* @param {Object} args - Contains: type (tile, geodata, markers, kml, georaster, draw, removelayers), name (name of layer)
* @param {Object} data - Contains: data as a JS array or georaster object.
* @returns {Element} Layer appended to a map div that has already been created. The layer is added into the global
* layer object.
* @example
* // Add a standard OpenStreetMap tile layer
* hydro.map.Layers({
* args: { type: 'tile', name: 'OpenStreetMap' }
* });
*
* @example
* // Add a GeoJSON layer with custom styling
* hydro.map.Layers({
* args: {
* type: 'geojson',
* name: 'Rivers',
* styleFunction: (feature) => ({ color: 'blue', weight: 2 })
* },
* data: riverGeoJSON
* });
*
* @example
* // Add markers to the map
* hydro.map.Layers({
* args: {
* type: 'marker',
* name: 'Station Locations',
* markertype: 'marker',
* coord: [42.3601, -71.0589],
* popUpContent: 'Boston Station'
* }
* });
*
* @example
* // Add a WMS layer (e.g., weather radar)
* hydro.map.Layers({
* args: {
* type: 'wms',
* name: 'Radar',
* layers: 'nexrad-n0r-900913',
* format: 'image/png',
* transparent: true
* },
* data: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi'
* });
*
* @example
* // Add a GeoRaster layer (DEM/Elevation)
* hydro.map.Layers({
* args: {
* type: 'georaster',
* name: 'Elevation',
* styleOptions: { colorScheme: 'terrain', opacity: 0.7 }
* },
* data: georasterObj
* });
*/
async function Layers({ params, args, data } = {}) {
var layertype, mapconfig = {};
data = data || args.data;
// Determine map type from params or global variable
if (params && params.maptype) {
mapconfig.maptype = params.maptype;
} else if (typeof mapType !== 'undefined') {
mapconfig.maptype = mapType;
} else {
mapconfig.maptype = 'leaflet'; // Default fallback
}
console.log('Layers: maptype determined as:', mapconfig.maptype);
//Creating configuration object for rendering layers.
//If a property is not found, is simply set to null.
//Setting it up as default behavior.
var layertype = {
type: args.type,
markertype: args.markertype,
geotype: args.geo,
data: data || args.data,
name: args.name,
coord: data || args.coord,
popUpContent: args.popUp || null,
styleFunction: args.styleFunction || null,
popUpFunction: args.popUpFunction || null,
onClickFunction: args.onClickFunction || null
};
console.log('Layers: layertype constructed:', layertype);
// Handle direct GeoTIFF URL loading
if (layertype.type === 'georaster' && typeof layertype.data === 'string') {
console.log('Layers: Detected GeoTIFF URL, fetching and parsing with geospatial module...');
try {
// Ensure geospatial libraries are loaded
await geospatial.load({ includeGeoTIFF: true });
// Load GeoTIFF using geospatial module
const result = await geospatial.loadGeoTIFF(layertype.data);
// Convert to georaster format expected by layer functions
const georaster = await convertGeoTIFFToGeoraster(result.data);
layertype.data = georaster;
data = georaster; // Update local data variable as well
console.log('Layers: GeoTIFF parsed successfully via geospatial module');
} catch (error) {
console.error('Layers: Failed to load GeoTIFF from URL:', error);
throw error;
}
}
try {
//in case the map required is google.
if (mapconfig.maptype === "google") {
var layer,
type = layertype.type,
layername = layertype.name;
// Create layer controller if undefined
if (typeof layercontroller === "undefined") {
// Creating layers div for Google Maps
!divisors.isdivAdded({ params: { id: "gmap-control-div" } })
? divisors.createDiv({
params: {
id: "gmap-control-div",
style: `
#gmap-control-div {
position: relative;
width: 200px;
font-family: Arial, sans-serif;
}
#select-box {
display: flex;
justify-content: space-between;
align-items: center;
padding: 11px 23px;
border: 1px solid #ccc;
cursor: pointer;
text-align: left;
color: rgb(0, 0, 0);
font-family: Roboto, Arial, sans-serif;
font-size: 18px;
border-radius: 2px;
box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 4px -1px;
background: none padding-box rgb(255, 255, 255);
font-weight: bold;
margin: 10px 10px 0px 0px;
}
#options-container {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
border: 1px solid #ccc;
border-top: none;
max-height: 200px;
overflow-y: auto;
background-color: white;
margin: 0px 10px 0px 0px;
}
#options-container label {
display: block;
padding: 8px 10px;
}
#options-container label:hover {
background-color: #f0f0f0;
}
`
},
})
: null;
let controlDiv = document.getElementById("gmap-control-div");
layercontroller = controlDiv;
// Create select list for Layers
controlDiv.innerHTML = `
<div id="select-box">
<span class="selected-options">Layers</span>
<span class="arrow">▾</span>
</div>
<div id="options-container">
</div>
`;
// Add Layer button to top right of the map
osmap.controls[google.maps.ControlPosition.TOP_RIGHT].push(controlDiv);
const selectBox = document.querySelector('#select-box');
const optionsContainer = document.querySelector('#options-container');
// Toggle the controller list if the Layer button is clicked
selectBox.addEventListener('click', function (event) {
optionsContainer.style.display = optionsContainer.style.display === 'block' ? 'none' : 'block';
});
// Toggle the layer if checkbox is toggles on or off
optionsContainer.addEventListener('change', function (event) {
if (event.target.type === 'checkbox') {
console.log(event.target.value)
if (event.target.checked) {
baselayers[event.target.value].setMap(osmap);
} else {
baselayers[event.target.value].setMap(null);
}
}
});
}
//Caller for the marker data renderer.
if (type === "marker") {
// Load AdvancedMarkerElement for Markers
if (!window.AdvancedMarkerElement) {
var { AdvancedMarkerElement } = await google.maps.importLibrary(
"marker",
);
window.AdvancedMarkerElement = AdvancedMarkerElement
};
layer = addMarker({ params: mapconfig, args: layertype });
layer.setMap(osmap);
baselayers[layername] = layer;
addLayerToGMapController(layer, layername)
//Caller for the geoJSON data renderer.
} else if (type === "geojson") {
layer = geoJSON({ params: mapconfig, args: layertype, data: layertype.data });
osmap.addLayer(layer)
layer.setMap(osmap);
baselayers[layername] = layer;
addLayerToGMapController(layer, layername)
// Caller for georaster data renderer
} else if (type === "georaster") {
layer = addGeoRasterLayer({ params: mapconfig, args: layertype });
layer.setMap(osmap);
baselayers[layername] = layer;
addLayerToGMapController(layer, layername)
// Caller for removing layers using layer name
} else if (type === "removelayers") {
if (baselayers.hasOwnProperty(layername)) {
baselayers[layername].setMap(null);
delete baselayers[layername];
removeLayerFromGMapController(layer, layername)
} else {
console.log("there is no layer with that name!");
}
}
return layer; // Return the layer for Google Maps
}
//in case the map required is osm.
if (mapconfig.maptype === "leaflet") {
var layer,
type = layertype.type,
layername = layertype.name;
// Synchronous handling for tiles to ensure map renders immediately
if (type === "tile") {
let tileUrl, tileOptions;
if (layertype.url) {
// Custom URL provided
tileUrl = layertype.url;
tileOptions = layertype.options || {};
} else if (tileprov && tileprov[layername]) {
// Use registered provider
tileUrl = tileprov[layername].url;
tileOptions = tileprov[layername].options;
} else {
console.error('Layers: Invalid tile provider or layer name:', layername);
return;
}
layer = new L.TileLayer(
tileUrl,
tileOptions
);
Object.assign(baselayers, { [layername]: layer });
osmap.addLayer(layer);
// Initialize controller if needed
if (typeof layercontroller === "undefined") {
reinitializeController('leaflet');
} else {
layercontroller.addBaseLayer(layer, layername);
}
return layer;
} else if (type === "wms") {
// WMS Layer Support
layer = L.tileLayer.wms(layertype.data, {
layers: layertype.layers,
format: layertype.format || 'image/png',
transparent: layertype.transparent !== undefined ? layertype.transparent : true,
version: layertype.version || '1.1.1',
attribution: layertype.attribution || ''
});
Object.assign(overlayers, { [layername]: layer });
osmap.addLayer(layer);
if (typeof layercontroller === "undefined") {
reinitializeController('leaflet');
} else {
layercontroller.addOverlay(layer, layername);
}
return layer;
}
// Async handling for other layer types
return new Promise((resolve, reject) => {
const onReady = async function () {
try {
if (type === "geojson") {
//Caller for the geoJSON data renderer.
layer = await geoJSON({
params: mapconfig,
args: layertype,
data: data
});
Object.assign(overlayers, { [layername]: layer });
osmap.addLayer(layer)
layercontroller.addOverlay(layer, layername);
} else if (type === "marker") {
//Caller for the marker renderer.
layer = addMarker({ params: mapconfig, args: layertype });
Object.assign(overlayers, { [layername]: layer });
osmap.addLayer(layer)
layercontroller.addOverlay(layer, layername);
} else if (type === "kml") {
//Caller for the KML data renderer.
layer = kml({ params: mapconfig, data: data });
Object.assign(overlayers, { [layername]: layer });
layercontroller.addOverlay(layer, layername);
} else if (type === "georaster") {
//Caller for the georaster data renderer.
layer = await addGeoRasterLayer({ params: mapconfig, args: layertype });
Object.assign(overlayers, { [layername]: layer });
osmap.addLayer(layer);
try {
if (layercontroller && typeof layercontroller.addOverlay === 'function') {
layercontroller.addOverlay(layer, layername);
}
} catch (e) { console.log('Controller error:', e); }
} else if (type === "draw") {
if (!isDrawToolAdded) {
drawings = new L.FeatureGroup();
draw({ params: mapconfig, args: layertype });
isDrawToolAdded = true
}
osmap.addLayer(drawings);
layer = drawings;
} else if (type === "removelayers") {
if (baselayers.hasOwnProperty(layername)) {
osmap.removeLayer(baselayers[layername]);
layercontroller.removeLayer(baselayers[layername]);
delete baselayers[layername];
} else if (overlayers.hasOwnProperty(layername)) {
osmap.removeLayer(overlayers[layername]);
layercontroller.removeLayer(overlayers[layername]);
delete overlayers[layername];
} else if (layername === "map") {
osmap.remove();
} else if (layername === "draw") {
drawControl.remove();
}
}
resolve(layer);
} catch (error) {
console.error('Layers: Error processing layer:', error);
reject(error);
}
};
if (osmap && osmap._loaded) {
onReady();
} else {
osmap.whenReady(onReady);
}
});
}
} catch (error) {
console.error(`There was an error when generating the map`, error)
}
}
// Add layer to Google Map Controller
function addLayerToGMapController(layer, layername) {
let optionsContainer = document.getElementById("options-container");
optionsContainer.innerHTML += `
<label><input type="checkbox" value="${layername}" checked="true">${layername}</label>
`
}
// Remove layer to Google Map Controller
function removeLayerFromGMapController(layer, layername) {
let optionsContainer = document.getElementById("options-container");
for (const child of optionsContainer.children) {
if (child.firstChild.value === layername) {
child.remove()
}
}
}
/**
* Rendering function according to the map selected by the user.
* Currently loads everything with the Leaflet render and OSM tile. The funciton loads the library to the header.
* It automatically is zoomed to 15.
* @function renderMap
* @memberof map
* @param {Object} param - Contains: null object, not necessary to be passed.
* @param {Object} args - Contains: maptype (osm, google maps), lat, lon
* @returns {Element} Map object appended to the web page.
* zoom.
* @example
* hydro.map.renderMap({params: {}, args: {{maptype: "leaflet", lat: "40", lon: "-100"}})
*/
async function renderMap({ params = {}, args = {}, data } = {}) {
await Promise.resolve(loader({ params, args }));
//Reading layer types and map configurations from the user's parameters inputs.
var layertype,
{ maptype = "leaflet", lat = 41.6572, lon = -91.5414 } = params;
let mapconfig = {
maptype,
lat,
lon,
zoom: 10
};
//Allocating a container object where the map should be set.
var container;
!divisors.isdivAdded({ params: { id: "map" } }) ?
divisors.createDiv({
params: {
id: "map",
style: `
#map {
height: 400px;
width: 800px;
margin-left: auto;
margin-right: auto;
position: ${maptype === 'leaflet' ? 'fixed' : 'absolute'};
min-width: 200px;
min-height: 200px;
}
.content {
max-width: 900px;
margin: auto
}
`
}
}) : null;
container = document.getElementById("map");
// Creating an icon in the bottom right corner of the map that allows the map to be resized
// !divisors.isdivAdded({params:{id: "resize-icon"}}) ?
// divisors.createDiv({
// params: {
// id: "resize-icon",
// style: `
// #resize-icon {
// position: absolute;
// top: 380px;
// left: 780px;
// width: 20px;
// height: 20px;
// background-color: gray;
// border-radius: 50%;
// opacity: 50%;
// color: transparent;
// }
// #resize-icon:hover {
// background-color: transparent;
// font-size: 24px;
// color: black;
// opacity: 100%;
// }
// #resize-icon::before {
// content: '\\2921';
// }
// `
// }
// }) : null;
// const resizeIcon = document.getElementById('resize-icon');
// Setup map resizing icon
// setupMapResizing(container, resizeIcon);
mapId = 'MAP_ID'
//From here onwards, the the library caller renders either Google Maps or Leaflet Maps.
if (mapconfig.maptype === "google") {
const options = {
mapTypeId: "terrain",
zoom: mapconfig.zoom,
center: {
lat: mapconfig.lat,
lng: mapconfig.lon,
},
mapId: mapId
};
mapType = 'google';
//append a new map to the map variable.
osmap = new google.maps.Map(container, options);
} else if (mapconfig.maptype === "leaflet") {
let { type = "tile", name = "OpenStreetMap" } = args
layertype = {
...args,
type,
name
};
mapType = "leaflet"
osmap = new L.map(container.id);
//assign the tile type to the data object for rendering.
const tiletype = layertype.name;
//Rendering the tile type the user has requested from the available tile types.
if (tiletype === "tile" && !tileprov.hasOwnProperty(tiletype)) {
console.log("No tile found!");
return;
}
//import the tile options from the given data.
osmap.setView([mapconfig.lat, mapconfig.lon], mapconfig.zoom);
Layers({ params: mapconfig, args: layertype });
//Allow for popups to be prompted when touching the screen.
var popup = new L.popup();
var onMapClick = (e) => {
popup
.setLatLng(e.latlng)
.setContent(`You clicked the map at ${e.latlng.toString()}`)
.openOn(osmap);
};
osmap.on("click", onMapClick);
}
}
// Helper function to implement drag and drop map resizing functionality
function setupMapResizing(map, resizeIcon) {
// State variables for tracking drag operation
let isDragging = false;
let startX, startY;
// Add event listeners for drag operations
resizeIcon.addEventListener('mousedown', startDrag);
// Function to initiate dragging
function startDrag(e) {
isDragging = true;
// Calculate the offset of the mouse position from the icon's top-left corner
startX = e.clientX - resizeIcon.offsetLeft;
startY = e.clientY - resizeIcon.offsetTop;
// Add listeners only when dragging starts
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', endDrag);
}
// Function to handle dragging
function drag(e) {
if (!isDragging) return;
e.preventDefault();
// Calculate new position of the resize icon
let left = e.clientX - startX;
let top = e.clientY - startY;
// Update the position of the resize icon
resizeIcon.style.left = `${left}px`;
resizeIcon.style.top = `${top}px`;
}
// Function to end dragging and resize the map
function endDrag() {
if (!isDragging) return;
isDragging = false;
// Calculate new dimensions, ensuring they're at least 200px
const newWidth = Math.max(resizeIcon.offsetLeft, 200);
const newHeight = Math.max(resizeIcon.offsetTop, 200);
// Resize the map and reposition the resize icon
resizeMap(map, newWidth, newHeight);
repositionResizeIcon(resizeIcon, newWidth, newHeight);
// Remove listeners when dragging ends
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', endDrag);
}
// Helper function to resize the map
function resizeMap(map, width, height) {
map.style.width = `${width}px`;
map.style.height = `${height}px`;
}
// Helper function to reposition the resize icon to the bottom-right corner
function repositionResizeIcon(icon, mapWidth, mapHeight) {
icon.style.left = `${mapWidth - icon.offsetWidth}px`;
icon.style.top = `${mapHeight - icon.offsetHeight}px`;
}
}
/**
*
* @param {*} param0
*/
async function recenter({ params, args, data } = {}) {
let latLon = L.latLng(args.lat, args.lon);
var bounds = latLon.toBounds(12000); // 500 = metres
osmap.panTo(latLon).fitBounds(bounds);
}
/***************************/
/*** Supporting functions **/
/***************************/
/**
*geoJSON type renderer for Leaflet and Google Maps. It attaches point, lines, and polygon layers to
* a rendered map. Must be called through the Layers function passing the argument
* @function geoJSON
* @memberof map
* @param {Object} params - Contains: maptype (google, osm)
* @param {Function} args.markerOptionsCallback - Callback function to set the geojsonMarkerOptions based on geoJson properties
* @param {Function} args.markerPopupCallback - Callback function to set the bindPopup based on geoJson properties
* @param {Function} args.onClickFunction - Callback function for an onClick event for the geoJson properties
* @param {Object} data - Data as geoJSON format compliant to OGM standards. See: https://docs.ogc.org/is/17-003r2/17-003r2.html
* @returns {Element} geoJSON layer added into a rendered map.
* @example
* hydro.map.geoJSON({params: {maptype: 'someMapType'}, data: {somegeoJSON}})
*/
async function geoJSON({ params, args, data } = {}) {
let geoType;
let { styleFunction, popUpFunction, onClickFunction } = args;
console.log('data', data);
if (data.type === "FeatureCollection") {
//Get the type of feature to be drawn
geoType = data.features[0].geometry.type;
} else if (data.type === "Feature") {
geoType = data.geometry.type;
}
if (params.maptype === "google") {
const layer = new google.maps.Data();
const infoWindow = new google.maps.InfoWindow();
let contentString;
const geoPoint = {
scale: 5,
fillColor: "red",
color: "#0dc1d3",
weight: 1,
opacity: 1,
fillOpacity: 0.6,
strokeWeight: 0.7
};
const geoPolygon = {
fillColor: "#2ce4f3",
color: "#0dc1d3",
weight: 1,
opacity: 1,
fillOpacity: 0.6,
};
if (geoType === "Point") {
layer.addGeoJson(data);
layer.setStyle(function (feature) {
return {
map: osmap,
center: feature.getGeometry().get(),
icon: {
path: google.maps.SymbolPath.CIRCLE,
...styleFunction ? styleFunction(feature) : geoPoint,
}
}
});
layer.addListener("click", function (event) {
if (popUpFunction) {
contentString = popUpFunction(event.feature)
} else {
contentString = `<div>Coordinates: ${event.latLng}</div>`;
}
infoWindow.setContent(contentString);
infoWindow.setPosition(event.feature.getGeometry().get());
infoWindow.open(osmap);
if (onClickFunction) {
onClickFunction(event)
}
});
//layer.setMap(osmap);
return layer
} else if (geoType === "Polygon" || geoType === "MultiPolygon") {
layer.addGeoJson(data);
layer.setStyle(function (feature) {
return {
map: osmap,
...styleFunction ? styleFunction(feature) : geoPolygon,
}
});
layer.addListener("click", function (event) {
if (popUpFunction) {
contentString = popUpFunction(event.feature)
}
console.log(event.feature.getGeometry())
infoWindow.setContent(contentString);
infoWindow.setPosition(event.latLng);
infoWindow.open(osmap);
if (onClickFunction) {
onClickFunction(event.feature.Gg)
}
});
//layer.setMap(osmap);
return layer;
}
//return layer;
} else if (params.maptype === "leaflet") {
// Bind Popup values and onClick function values
const onEachFeature = (feature, layer) => {
if (popUpFunction) {
layer.bindPopup(popUpFunction(feature))
} else if (feature.properties && feature.properties.Name && feature.properties.Lat && feature.properties.Lon) {
layer.bindPopup(`${feature.properties.Name} (${feature.properties.Lat}, ${feature.properties.Lon})`);
}
if (onClickFunction) {
layer.on(
"click", onClickFunction
)
};
};
const geoPoint = {
radius: 10,
fillColor: "#2ce4f3",
color: "#0dc1d3",
weight: 1,
opacity: 1,
fillOpacity: 0.7,
};
const geoPolygon = {
weight: 2,
color: "#432",
};
if (geoType === "Point") {
// return L.geoJSON(data, {
// pointToLayer: function (feature, latlng) {
// // Apply style function if it exists, otherwise use default geoPoint
// const style = styleFunction !== null ? styleFunction(feature) : geoPoint;
// return L.circleMarker(latlng, style);
// },
// onEachFeature: onEachFeature,
// style: function (feature) {
// // Only needed for non-point features
// if (styleFunction !== null) return styleFunction(feature);
// return geoPolygon; // Default style for polygons
// }
// });
return L.geoJSON(data
// {
// pointToLayer: function (feature, latlng) {
// // Apply style function if it exists, otherwise use default geoPoint
// const style = styleFunction !== null ? styleFunction(feature) : geoPoint;
// return L.circleMarker(latlng, style);
// },
// onEachFeature: onEachFeature,
// style: function (feature) {
// // Only needed for non-point features
// if (styleFunction !== null) return styleFunction(feature);
// return geoPolygon; // Default style for polygons
// }
// }
)
} else if (geoType === "LineString" || geoType === "MultiLineString") {
return L.geoJSON(data, {
style: function (feature) {
if (styleFunction !== null) return styleFunction(feature);
return { color: "#0000FF", weight: 3 }; // Default line style
},
onEachFeature: onEachFeature,
});
} else if (geoType === "Polygon" || geoType === "MultiPolygon") {
return L.geoJSON(data, {
style: function (feature) {
if (styleFunction !== null) return styleFunction(feature);
return geoPolygon;
},
onEachFeature: onEachFeature,
});
}
}
}
/**
* Creates layer of kml data passed through an object to anexisting map.
* Notice that the map most be already created for the method to be used.
* @function kml
* @memberof map
* @param {Object} params - Contains: maptype (google, osm)
* @param {Object} data - Contains: KML data in XML format.
* @returns {Element} Appends KML layer to rendered map.
* @example
* hydro.map.kml({params: {maptype: 'someMapType'}, data: {someKMLdata}})
*/
function kml({ params, args, data } = {}) {
if (params.maptype == "google") {
var kmlLayer = new google.maps.KmlLayer(data, {
suppressInfoWindows: true,
preserveViewport: false,
map: osmap,
});
kmlLayer.addListener("click", function (event) {
var content = event.featureData.infoWindowHtml,
testimonial = document.getElementById("capture");
testimonial.innerHTML = content;
});
} else if (params.maptype == "leaflet") {
const parser = new DOMParser(),
kml = parser.parseFromString(data, "text/xml"),
track = new L.KML(kml);
return track;
}
}
/**
* Adds a new marker to the map, given coordinates, map type and marker type.
* @function addMarker
* @memberof map
* @param {Object} params - Contains: maptype (google, osm)
* @param {Object} args - Contains: markertype (rectangle, circle, circleMarker, polyline, polygon, marker), coord (JS array with coordinates)
* @returns {Element} Layer object rendered on the map
* @example
* hydro.map.addMarker({params: {maptype: 'someMap'}, args: {markertype: 'someMarker', coord: [markerLat, markerLon]}})
*/
function addMarker({ params, args, data } = {}) {
var type = args.markertype,
coord = args.coord, layer, ltlngCoordinate,
title = args.name;
console.log('addMarker called with:', { maptype: params.maptype, markertype: type, coord: coord });
if (params.maptype === "google") {
switch (type) {
case "marker":
layer = new AdvancedMarkerElement({
position: { lat: coord[0], lng: coord[1] },
title: title
});
// Bind popup to marker layer
var infowindow = new google.maps.InfoWindow();
makeInfoWindowEvent(
osmap,
infowindow,
args.popUpContent || `Coordinates: lat: ${coord[0]}, lon: ${coord[1]}`,
layer
);
break;
case "rectangle":
layer = new google.maps.Rectangle({
...markerStyles({ params: { map: "google", fig: "rectangle" } }),
bounds: {
north: coord[0] + 0.01,
south: coord[0] - 0.01,
east: coord[1] + 0.01,
west: coord[1] - 0.01,
},
});
break;
case "circle":
layer = new google.maps.Circle({
...markerStyles({ params: { map: "google", fig: "circle" } }),
center: { lat: coord[0], lng: coord[1] },
});
break;
case "polyline":
ltlngCoordinate = coord.map(([x, y]) => { return { lat: x, lng: y } })
layer = new google.maps.Polyline({
path: ltlngCoordinate,
geodesic: true,
...markerStyles({ params: { map: "google", fig: "polyline" } }),
});
case "polygon":
ltlngCoordinate = coord.map(([x, y]) => { return { lat: x, lng: y } })
layer = new google.maps.Polygon({
path: ltlngCoordinate,
geodesic: true,
...markerStyles({ params: { map: "google", fig: "polygon" } })
});
}
}
if (params.maptype === "leaflet") {
//the markerstyle function renders different types of preset styles. If other style types are needed
//change the code accordingly.
switch (type) {
case "rectangle":
layer = new L.rectangle(
coord,
markerStyles({ params: { map: "leaflet", fig: "rectangle" } })
);
break;
case "circle":
layer = new L.circle(
coord,
markerStyles({ params: { map: "leaflet", fig: "circle" } })
);
break;
case "circlemarker":
layer = new L.circleMarker(
coord,
markerStyles({ params: { map: "leaflet", fig: "circlemarker" } })
);
break;
case "polyline":
layer = new L.polyline(
coord,
markerStyles({ params: { map: "leaflet", fig: "polyline" } })
);
break;
case "polygon":
layer = new L.polygon(
coord,
markerStyles({ params: { map: "leaflet", fig: "polygon" } })
);
break;
case "marker":
layer = new L.marker(
coord,
markerStyles({ params: { map: "leaflet", fig: "marker" } })
).bindPopup(args.popUpContent || `Coordinates: lat: ${coord[0]}, lon: ${coord[1]}`);
break;
default:
alert("no markers with that name");
}
}
return layer;
}
// Helper function for google maps based visualization to bind popup value on layers
function makeInfoWindowEvent(map, infowindow, contentString, marker) {
google.maps.event.addListener(marker, 'click', function () {
infowindow.setContent(contentString);
infowindow.open(map, marker);
});
}
/**
* Adds a georaster layer to the map for elevation/terrain visualization
* @function addGeoRasterLayer
* @memberof map
* @param {Object} params - Contains: maptype (google, leaflet)
* @param {Object} args - Contains: data (georaster object), name (layer name), style options
* @returns {Object} Georaster layer object
* @example
* hydro.map.addGeoRasterLayer({params: {maptype: 'leaflet'}, args: {data: georasterObj, name: 'DEM'}})
*/
async function addGeoRasterLayer({ params = {}, args, data } = {}) {
let { name: layerName, styleOptions = {} } = args;
let georaster = data;
// Default to leaflet if not specified
const mapType = params.maptype || 'leaflet';
console.log('addGeoRasterLayer called with:', {
maptype: mapType,
hasGeoraster: !!georaster,
georasterKeys: georaster ? Object.keys(georaster) : null
});
if (!georaster) {
throw new Error("Georaster data is required for georaster layer");
}
// Handle ArrayBuffer input (e.g. from generic file fetch)
if (georaster instanceof ArrayBuffer || (georaster.buffer instanceof ArrayBuffer)) {
console.log('Detected ArrayBuffer input, parsing as GeoTIFF...');
try {
// Ensure geospatial libraries are loaded
await geospatial.load({ includeGeoTIFF: true });
const buffer = georaster instanceof ArrayBuffer ? georaster : georaster.buffer;
const result = await geospatial.loadGeoTIFFFromBuffer(buffer);
// Convert to georaster format
georaster = await convertGeoTIFFToGeoraster(result.data);
console.log('Successfully parsed ArrayBuffer to GeoRaster object');
} catch (e) {
console.error('Failed to parse ArrayBuffer as GeoTIFF:', e);
throw new Error('Failed to parse input data as valid GeoTIFF/GeoRaster');
}
}
// Ensure osmap is available
if (!osmap) {
console.warn('Map instance (osmap) not found. Layer created but not added to map.');
}
if (mapType === "google") {
// For Google Maps, we'll create a custom overlay using canvas
console.log('Creating Google Maps georaster layer');
const layer = createGoogleMapsGeoRasterLayer(georaster, styleOptions);
if (osmap) {
layer.setMap(osmap);
}
return layer;
} else if (mapType === "leaflet") {
// Handle case where georaster is wrapped (e.g. from hydro.data.retrieve)
if (georaster && georaster.georaster) {
georaster = georaster.georaster;
}
// For Leaflet, we'll use GeoRasterLayer if available, otherwise create a custom implementation
const layer = await createLeafletGeoRasterLayer(georaster, styleOptions);
// Automatically add to map as requested
if (osmap) {
layer.addTo(osmap);
// Add to the library's internal layer controller if it exists
if (layercontroller && typeof layercontroller.addOverlay === 'function') {
// Use provided name or default unique name
const explicitName = layerName || `Raster Layer ${Math.floor(Math.random() * 1000)}`;
layercontroller.addOverlay(layer, explicitName);
// Also update global overlayers registry consistent with other modules
if (typeof window !== 'undefined' && window.overlayers) {
window.overlayers[explicitName] = layer;
}
}
}
return layer;
} else {
throw new Error(`Unsupported map type for georaster: ${mapType}`);
}
}
/**
* Creates a georaster layer for Google Maps using canvas overlay
* @param {Object} georaster - Georaster object with elevation data
* @param {Object} styleOptions - Styling options for the layer
* @returns {Object} Google Maps overlay object
* @private
*/
function createGoogleMapsGeoRasterLayer(georaster, styleOptions = {}) {
const {
opacity = 0.7,
colorScheme = 'terrain' // 'terrain', 'grayscale', 'viridis', etc.
} = styleOptions;
// Create a custom overlay class for Google Maps
class GeoRasterOverlay extends google.maps.OverlayView {
constructor(georaster, options = {}) {
super();
this.georaster = georaster;
this.opacity = options.opacity || 0.7;
this.colorScheme = options.colorScheme || 'terrain';
this.canvas = null;
}
onAdd() {
this.canvas = document.createElement('canvas');
this.canvas.style.position = 'absolute';
this.canvas.style.opacity = this.opacity;
const panes = this.getPanes();
panes.overlayLayer.appendChild(this.canvas);
this.draw();
}
draw() {
if (!this.canvas) return;
const overlayProjection = this.getProjection();
if (!overlayProjection) return;
const bounds = new google.maps.LatLngBounds(
new google.maps.LatLng(this.georaster.ymin, this.georaster.xmin),
new google.maps.LatLng(this.georaster.ymax, this.georaster.xmax)
);
const sw = overlayProjection.fromLatLngToDivPixel(bounds.getSouthWest());
const ne = overlayProjection.fromLatLngToDivPixel(bounds.getNorthEast());
this.canvas.style.left = sw.x + 'px';
this.canvas.style.top = ne.y + 'px';
this.canvas.style.width = (ne.x - sw.x) + 'px';
this.canvas.style.height = (sw.y - ne.y) + 'px';
this.canvas.width = ne.x - sw.x;
this.canvas.height = sw.y - ne.y;
// Render elevation data to canvas
this.renderElevationData();
}
renderElevationData() {
const ctx = this.canvas.getContext('2d');
const imageData = ctx.createImageData(this.canvas.width, this.canvas.height);
const data = imageData.data;
const { values, mins, maxs, noDataValue } = this.georaster;
const elevationData = values[0];
const minValue = mins[0];
const maxValue = maxs[0];
for (let y = 0; y < this.canvas.height; y++) {
for (let x = 0; x < this.canvas.width; x++) {
const pixelIndex = (y * this.canvas.width + x) * 4;
// Scale coordinates to georaster dimensions
const geoX = Math.floor((x / this.canvas.width) * this.georaster.width);
const geoY = Math.floor((y / this.canvas.height) * this.georaster.height);
const elevation = elevationData[geoY]?.[geoX];
if (elevation === noDataValue || !Number.isFinite(elevation)) {
// Transparent for no-data
data[pixelIndex] = 0;
data[pixelIndex + 1] = 0;
data[pixelIndex + 2] = 0;
data[pixelIndex + 3] = 0;
} else {
// Color based on elevation
const normalizedElevation = (elevation - minValue) / (maxValue - minValue);
const color = this.getColorForElevation(normalizedElevation);
data[pixelIndex] = color.r;
data[pixelIndex + 1] = color.g;
data[pixelIndex + 2] = color.b;
data[pixelIndex + 3] = 255; // Alpha
}
}
}
ctx.putImageData(imageData, 0, 0);
}
getColorForElevation(normalizedValue) {
// Use the shared helper function to ensure consistency with Leaflet
return getLeafletColorForElevation(normalizedValue, this.colorScheme);
}
onRemove() {
if (this.canvas && this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas);
this.canvas = null;
}
}
}
return new GeoRasterOverlay(georaster, { opacity, colorScheme });
}
/**
* Creates a georaster layer for Leaflet
* @param {Object} georaster - Georaster object with elevation data
* @param {Object} styleOptions - Styling options for the layer
* @returns {Object} Leaflet layer object
* @private
*/
async function createLeafletGeoRasterLayer(georaster, styleOptions = {}) {
const {
opacity = 0.7,
colorScheme = 'terrain'
} = styleOptions;
// For custom manually created objects (geoprocessor fallback), skip plugin and use simple overlay
if (georaster._isGeorasterObject) {
console.log('Detecting custom georaster object, using simple overlay fallback...');
return createSimpleGeoRasterOverlay(georaster, { opacity, colorScheme });
}
// Try to use GeoRasterLayer if available (from georaster-layer-for-leaflet)
if (typeof L.GeoRasterLayer !== 'undefined') {
return new L.GeoRasterLayer({
georaster: georaster,
opacity: opacity,
resolution: 256,
pixelValuesToColorFn: function (pixelValues) {
const elevation = pixelValues[0];
const { mins, maxs, noDataValue } = georaster;
if (elevation === noDataValue || !Number.isFinite(elevation)) {
return null; // Transparent
}
const normalizedValue = (elevation - mins[0]) / (maxs[0] - mins[0]);
return getLeafletColorForElevation(normalizedValue, colorScheme);
}
});
}
// Try to load GeoRasterLayer plugin dynamically
try {
console.log('Attempting to load GeoRasterLayer plugin...');
await loadGeoRasterLayerPlugin();
if (typeof L.GeoRasterLayer !== 'undefined') {
console.log('GeoRasterLayer plugin loaded successfully');
return new L.GeoRasterLayer({
georaster: georaster,
opacity: opacity,
resolution: 256,
pixelValuesToColorFn: function (pixelValues) {
const elevation = pixelValues[0];
const { mins, maxs, noDataValue } = georaster;
if (elevation === noDataValue || !Number.isFinite(elevation)) {
return null; // Transparent
}
const normalizedValue = (elevation - mins[0]) / (maxs[0] - mins[0]);
return getLeafletColorForElevation(normalizedValue, colorScheme);
}
});
}
} catch (error) {
console.log('GeoRasterLayer plugin not available, using canvas fallback:', error.message);
}
// Fallback 1: Try simple image overlay approach
try {
console.log('Using simple GeoRaster overlay fallback...');
return createSimpleGeoRasterOverlay(georaster, { opacity, colorScheme });
} catch (error) {
console.log('Simple overlay failed, using canvas fallback:', error.message);
}
// Fallback 2: Create custom canvas-based layer
return createLeafletCanvasGeoRasterLayer(georaster, { opacity, colorScheme });
}
/**
* Creates a simple image overlay from georaster data
* @param {Object} georaster - Georaster object with elevation data
* @param {Object} options - Layer options
* @returns {Object} Leaflet image overlay
* @private
*/
function createSimpleGeoRasterOverlay(georaster, options = {}) {
const { opacity = 0.7, colorScheme = 'terrain' } = options;
if (!georaster.width || !georaster.height) {
console.error('Invalid georaster dimensions:', georaster);
throw new Error('Georaster dimensions are missing or invalid');
}
// Create a canvas to render the georaster data
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas size to match georaster dimensions
canvas.width = georaster.width;
canvas.height = georaster.height;
// Render elevation data to canvas
const imageData = ctx.createImageData(georaster.width, georaster.height);
const data = imageData.data;
// Access elevation data - handle different possible structures
let elevationData;
if (georaster.values && georaster.values[0]) {
elevationData = georaster.values[0];
} else if (georaster.values && Array.isArray(georaster.values)) {
elevationData = georaster.values;
} else {
throw new Error('Georaster values not found or in unexpected format');
}
const { mins, maxs, noDataValue } = georaster;
for (let y = 0; y < georaster.height; y++) {
for (let x = 0; x < georaster.width; x++) {
const pixelIndex = (y * georaster.width + x) * 4;
const elevation = elevationData[y][x];
if (elevation === noDataValue || !Number.isFinite(elevation)) {
// Transparent for no-data
data[pixelIndex] = 0;
data[pixelIndex + 1] = 0;
data[pixelIndex + 2] = 0;
data[pixelIndex + 3] = 0;
} else {
// Color based on elevation
const normalizedValue = (elevation - mins[0]) / (maxs[0] - mins[0]);
const color = getLeafletColorForElevation(normalizedValue, colorScheme);
data[pixelIndex] = color.r;
data[pixelIndex + 1] = color.g;
data[pixelIndex + 2] = color.b;
data[pixelIndex + 3] = Math.floor(opacity * 255);
}
}
}
ctx.putImageData(imageData, 0, 0);
// Convert canvas to data URL
const imageUrl = canvas.toDataURL();
// Create bounds for the image overlay (Leaflet format: [[lat, lng], [lat, lng]])
// Ensure proper ordering: southwest to northeast
const bounds = [
[Math.min(georaster.ymin, georaster.ymax), Math.min(georaster.xmin, georaster.xmax)],
[Math.max(georaster.ymin, georaster.ymax), Math.max(georaster.xmin, georaster.xmax)]
];
// Validate bounds
if (!bounds || bounds.length !== 2 ||
!Array.isArray(bounds[0]) || !Array.isArray(bounds[1]) ||
bounds[0].length !== 2 || bounds[1].length !== 2) {
throw new Error('Invalid georaster bounds');
}
// Check for valid coordinate values
const [lat1, lng1] = bounds[0];
const [lat2, lng2] = bounds[1];
if (isNaN(lat1) || isNaN(lng1) || isNaN(lat2) || isNaN(lng2)) {
throw new Error('Georaster bounds contain invalid coordinate values');
}
// Validate and clamp bounds if slightly out of range (due to float precision)
const EPSILON = 0.01; // Allow some wiggle room for floating point noise
if (lat1 < -90 - EPSILON || lat1 > 90 + EPSILON || lat2 < -90 - EPSILON || lat2 > 90 + EPSILON ||
lng1 < -180 - EPSILON || lng1 > 180 + EPSILON || lng2 < -180 - EPSILON || lng2 > 180 + EPSILON) {
throw new Error(`Georaster bounds contain out-of-range coordinate values: [[${lat1}, ${lng1}], [${lat2}, ${lng2}]]`);
}
// Clamp to exact limits for Leaflet to avoid any internal Leaflet errors
bounds[0][0] = Math.max(-90, Math.min(90, bounds[0][0]));
bounds[0][1] = Math.max(-180, Math.min(180, bounds[0][1]));
bounds[1][0] = Math.max(-90, Math.min(90, bounds[1][0]));
bounds[1][1] = Math.max(-180, Math.min(180, bounds[1][1]));
// Create Leaflet image overlay
const imageOverlay = L.imageOverlay(imageUrl, bounds, {
opacity: opacity,
interactive: false
});
return imageOverlay;
}
/**
* Creates a canvas-based georaster layer for Leaflet (fallback)
* @param {Object} georaster - Georaster object with elevation data
* @param {Object} options - Layer options
* @returns {Object} Leaflet canvas layer
* @private
*/
function createLeafletCanvasGeoRasterLayer(georaster, options = {}) {
const { opacity = 0.7, colorScheme = 'terrain' } = options;
// Create the layer class
const GeoRasterCanvasLayer = L.GridLayer.extend({
options: {
opacity: opacity,
colorScheme: colorScheme
},
initialize: function (georaster, options) {
this.georaster = georaster;
L.GridLayer.prototype.initialize.call(this, options);
},
});
// Return instantiated layer
return new GeoRasterCanvasLayer(georaster, options);
}
/**
* Gets color for elevation value in Leaflet format
* @param {number} normalizedValue - Normalized elevation (0-1)
* @param {string} colorScheme - Color scheme name
* @returns {Object} RGB color object
*/
/**
* Gets color for elevation value in Leaflet format
* @param {number} normalizedValue - Normalized elevation (0-1)
* @param {string|Array} colorScheme - Color scheme name or array of hex colors
* @returns {Object} RGB color object
* @private
*/
function getLeafletColorForElevation(normalizedValue, colorScheme) {
// 1. Custom Array Scheme (e.g., ['#ffffff', '#0000ff'])
if (Array.isArray(colorScheme) && colorScheme.length >= 2) {
// Find segment
const step = 1 / (colorScheme.length - 1);
const segmentIndex = Math.min(Math.floor(normalizedValue / step), colorScheme.length - 2);
const segmentFactor = (normalizedValue - (segmentIndex * step)) / step;
const c1 = parseColor(colorScheme[segmentIndex]);
const c2 = parseColor(colorScheme[segmentIndex + 1]);
return interpolateColor(c1, c2, segmentFactor);
}
// 2. Named Schemes
if (colorScheme === 'terrain') {
if (normalizedValue < 0.2) return { r: 0, g: 100, b: 0 }; // Dark green
if (normalizedValue < 0.4) return { r: 34, g: 139, b: 34 }; // Green
if (normalizedValue < 0.6) return { r: 255, g: 255, b: 0 }; // Yellow
if (normalizedValue < 0.8) return { r: 255, g: 165, b: 0 }; // Orange
return { r: 139, g: 69, b: 19 }; // Brown
}
if (colorScheme === 'blues') {
// Simple white-to-blue gradient
return interpolateColor({ r: 255, g: 255, b: 255 }, { r: 0, g: 0, b: 255 }, normalizedValue);
}
// 3. Grayscale Fallback
const gray = Math.floor(normalizedValue * 255);
return { r: gray, g: gray, b: gray };
}
// Stats Helpers
function parseColor(input) {
if (typeof input === 'string') {
if (input === 'transparent') return { r: 0, g: 0, b: 0, a: 0 };
if (input.startsWith('#')) return hexToRgb(input);
// Add basic named colors if needed or rely on canvas context for parsing (complex/async)
// For now support basic hex/rgb
}
return { r: 0, g: 0, b: 0 }; // Default black
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 };
}
function interpolateColor(c1, c2, factor) {
return {
r: Math.round(c1.r + factor * (c2.r - c1.r)),
g: Math.round(c1.g + factor * (c2.g - c1.g)),
b: Math.round(c1.b + factor * (c2.b - c1.b))
};
}
/**
* Attempts to load the GeoRasterLayer plugin for Leaflet
* @returns {Promise<void>}
* @private
*/
async function loadGeoRasterLayerPlugin() {
if (typeof L.GeoRasterLayer !== 'undefined') {
return; // Already loaded
}
try {
// Try to load the georaster-layer-for-leaflet plugin
const script = document.createElement('script');
script.src = 'https://unpkg.com/georaster-layer-for-leaflet@3.5.0/dist/georaster-layer-for-leaflet.min.js';
script.type = 'text/javascript';
return new Promise((resolve, reject) => {
script.onload = () => {
console.log('GeoRasterLayer plugin loaded');
resolve();
};
script.onerror = () => {
reject(new Error('Failed to load GeoRasterLayer plugin'));
};
document.head.appendChild(script);
});
} catch (error) {
throw new Error(`GeoRasterLayer plugin loading failed: ${error.message}`);
}
}
/**
* Attempts to load the georaster parsing library
* @returns {Promise<void>}
*/
/**
* Converts a GeoTIFF object (from geotiff.js) to the georaster format expected by layer functions
* @param {Object} tiff - GeoTIFF object
* @returns {Promise<Object>} Georaster-like object
*/
async function convertGeoTIFFToGeoraster(tiff) {
const image = await tiff.getImage();
const width = image.getWidth();
const height = image.getHeight();
const rasters = await image.readRasters();
// Calculate basic stats for visualization
const mins = [], maxs = [], ranges = [];
const numBands = rasters.length;
// Process each band
const values = [];
for (let i = 0; i < numBands; i++) {
const band = rasters[i];
let min = Infinity, max = -Infinity;
// Convert to 2D array structure expected by fallback renderer
const bandValues = [];
for (let y = 0; y < height; y++) {
const row = [];
for (let x = 0; x < width; x++) {
const val = band[y * width + x];
row.push(val);
if (val > max) max = val;
if (val < min) min = val;
}
bandValues.push(row);
}
values.push(bandValues);
mins.push(min);
maxs.push(max);
ranges.push(max - min);
}
// Get raw bounds
const origin = image.getOrigin();
const resolution = image.getResolution();
let xmin = origin[0];
let ymax = origin[1];
const pixelWidth = resolution[0];
const pixelHeight = Math.abs(resolution[1]);
let xmax = xmin + width * pixelWidth;
let ymin = ymax - height * pixelHeight;
// Extract Projection Information
const geoKeys = image.getGeoKeys();
let projection = 4326;
if (geoKeys) {
projection = geoKeys.ProjectedCSTypeGeoKey || geoKeys.GeographicTypeGeoKey || 4326;
}
console.log(`Parsed GeoTIFF: Projection=${projection}, Raw Bounds=[${xmin}, ${ymin}, ${xmax}, ${ymax}]`);
// Reproject to WGS84 if needed (Leaflet requires Lat/Lng)
// Simple check: if coordinates are huge, it's likely meters (Projected)
if ((Math.abs(xmin) > 180 || Math.abs(ymax) > 90) && projection !== 4326 && geospatial.isLoaded()) {
console.log('Detected projected coordinates. Attempting reprojection to WGS84...');
try {
const fromProj = `EPSG:${projection}`;
const toProj = 'EPSG:4326';
// Reproject corners
const sw = geospatial.transformCoordinates([xmin, ymin], fromProj, toProj);
const ne = geospatial.transformCoordinates([xmax, ymax], fromProj, toProj);
// Update bounds
xmin = sw[0];
ymin = sw[1];
xmax = ne[0];
ymax = ne[1];
console.log(`Reprojected Bounds to WGS84: [${xmin}, ${ymin}, ${xmax}, ${ymax}]`);
} catch (projError) {
console.warn('Failed to reproject GeoTIFF bounds:', projError);
// Fallback: don't fail immediately, let the layer try or user see the error
}
}
return {
width,
height,
values, // array of bands, each band is 2D array
pixelWidth: pixelWidth, // Note: these might be invalid after reprojection but width/height stay same
pixelHeight: pixelHeight,
xmin,
xmax,
ymin,
ymax,
noDataValue: null, // extracted from tiff if possible
numberOfRasters: numBands,
projection: projection,
_isGeorasterObject: true, // Marker for our internal check
mins,
maxs,
ranges
};
}
/**
* Adds a popup to a specific layer with support for various content types including charts.
* @function addPopup
* @memberof map
* @param {Object} params - Configuration parameters
* @param {string} params.type - Type of popup content: 'chart', 'html', 'text'
* @param {string} [params.name] - Optional title for the popup
* @param {Object} args - Arguments
* @param {Object} args.layer - The layer object to attach the popup to
* @param {Object} [args.chartOptions] - Configuration options for the chart (passed to visualize.draw)
* @param {Object|Array} data - Data to be visualized in the popup
*/
function addPopup({ params, args, data } = {}) {
const { type = 'text', name } = params;
const { layer, chartOptions = {} } = args;
if (!layer) {
throw new Error('addPopup: Layer is required');
}
// Check if layer supports Leaflet popup API
if (typeof layer.bindPopup !== 'function') {
throw new Error('addPopup: Layer does not support bindPopup (Google Maps not currently supported)');
}
if (type === 'chart') {
// Create a unique ID for the chart container
const chartId = `popup-chart-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
// Create the content HTML
let contentHtml = '<div class="popup-content" style="min-width: 300px; min-height: 200px;">';
if (name) {
contentHtml += `<h3>${name}</h3>`;
}
contentHtml += `<div id="${chartId}" style="width: 100%; height: 200px;">Loading chart...</div>`;
contentHtml += '</div>';
// Bind the popup
layer.bindPopup(contentHtml);
// Add event listener for when the popup opens
layer.on('popupopen', () => {
// Wait a brief moment for the DOM to be ready
setTimeout(() => {
const container = document.getElementById(chartId);
if (container) {
// Clear loading text
container.innerHTML = '';
// Render the chart using visualize module
visualize.draw({
params: {
type: 'chart',
id: chartId,
...chartOptions
},
args: chartOptions,
data: data
});
}
}, 100);
});
} else if (type === 'html' || type === 'text') {
let content = data;
if (name) {
content = `<h3>${name}</h3><div>${data}</div>`;
}
layer.bindPopup(content);
}
}
/**
* Adds a custom legend to the map based on the map type and position specified.
* @param {Object} param0 - Object containing the map type and position for the legend.
* @param {string} param0.position - The position for the legend (top, top left, left, bottom left, bottom, bottom right, right, top right).
* @private
*/
async function addCustomLegend({ params, args, data } = {}) {
const { position } = params;
const { div } = args;
// If no div is provided, return an error message
if (!div) {
return "Pass in a div for overlay";
}
let type = mapType;
console.log('mapType', type);
// Handle the case when the map type is 'google'
if (type === "google") {
switch (position) {
case 'top':
osmap.controls[google.maps.ControlPosition.TOP_CENTER].push(div);
break;
case 'top left':
osmap.controls[google.maps.ControlPosition.TOP_LEFT].push(div);
break;
case 'left':
osmap.controls[google.maps.ControlPosition.LEFT_CENTER].push(div);
break;
case 'bottom left':
osmap.controls[google.maps.ControlPosition.BOTTOM_LEFT].push(div);
break;
case 'bottom':
osmap.controls[google.maps.ControlPosition.BOTTOM_CENTER].push(div);
break;
case 'bottom right':
osmap.controls[google.maps.ControlPosition.BOTTOM_RIGHT].push(div);
break;
case 'right':
osmap.controls[google.maps.ControlPosition.RIGHT_CENTER].push(div);
break;
case 'top right':
osmap.controls[google.maps.ControlPosition.TOP_RIGHT].push(div);
break;
default:
console.log("Possible values for position are 'top', 'left', 'bottom', 'right', 'top left', 'top right', 'bottom left', 'bottom right'");
break;
}
}
// Handle the case when the map type is 'leaflet'
else if (type === "leaflet") {
let { position } = params;
position = position.replace(" ", "")
if (position === 'topleft' || position === 'topright' || position === 'bottomleft' || position === 'bottomright') {
const legend = L.control({ position: position });
legend.onAdd = function (map) {
// Add the legend to the map
let legendDiv = L.DomUtil.create('div', 'legend-container')
console.log(div)
legendDiv.innerHTML = args.div.innerHTML;
return legendDiv
};
legend.addTo(osmap);
} else {
console.log("Possible values for position are 'top left', 'top right', 'bottom left' or 'bottom right'");
}
}
// If no map type is specified, log an error message
else {
console.log("Error: map not found");
}
}
/**
* Creaes different styles for depending on the marker that has been selected for drawing.
* @function markerStyles
* @memberof map
* @param {Object} params - Contains: maptype (google, osm), fig (rectangle, circle, circleMarker, polyline, polygon, marker)
* @returns {CSSRule} New styles that are used for drawing a marker.
* @example
* hydro.map.markerStyles({params: {map: 'someMap', fig: 'someFig'}})
*/
function markerStyles({ params, args, data } = {}) {
var map = params.map,
fig = params.fig,
layer;
//Implementation for google markers still ongoing.
if (map === "google") {
switch (fig) {
case "rectangle":
return {
strokeColor: "#FF0000",
strokeOpacity: 0.8,
strokeWeight: 2,
fillOpacity: 0.5,
fillColor: "#800080",
strokeColor: "#800080",
};
case "circle":
return {
strokeOpacity: 0.8,
strokeWeight: 2,
fillOpacity: 0.5,
fillColor: "#3CB043",
strokeColor: "#3CB043",
radius: 1000,
};
case "polyline":
return {
strokeColor: "#FF0000",
strokeOpacity: 1.0,
strokeWeight: 2,
};
case "polygon":
return {
strokeColor: "#FF0000",
strokeOpacity: 1.0,
strokeWeight: 2,
fillColor: "#FF0000",
fillOpacity: 0.35,
};
}
}
//Full implementation of the OpenStreetMap ready for usage.
if (map === "leaflet") {
switch (fig) {
case "rectangle":
layer = {
weight: 2,
color: "#e1e1100",
};
break;
case "circle":
layer = {
radius: 200,
fillColor: "#2ce4f3",
color: "#0dc1d3",
weight: 1,
opacity: 1,
fillOpacity: 0.6,
};
break;
case "circlemarker":
layer = {
radius: 5,
fillColor: "#2ce4f3",
color: "#0dc1d3",
weight: 1,
opacity: 1,
fillOpacity: 0.6,
};
break;
case "polyline":
layer = {
weight: 1,
color: "#432",
opacity: 1,
};
break;
case "polygon":
layer = {
weight: 2,
color: "#e1e1100",
opacity: 1,
};
break;
case "marker":
layer = {
markerIcon: null,
zIndexOffset: 2000,
};
break;
default:
break;
}
}
return layer;
}
/**
* Adds a drawing tool functionality to an exisiting map to create layers inside.
* @function draw
* @memberof map
* @param {Object} params - Contains: maptype(google, osm)
* @returns {Element} Toolkit layer added to map.
* @example
* // Enable standard drawing tools on the map
* hydro.map.draw({
* params: { maptype: 'leaflet' }
* });
*
* @example
* // The draw tool allows users to create:
* // - Markers
* // - Polylines
* // - Polygons
* // - Rectangles
* // - Circles
* // Created shapes are added to a FeatureGroup and can be interacted with.
*/
function draw({ params, args, data } = {}) {
//Implementation of Google Maps API still ongoing.
if (params.maptype == "google") {
}
//Full implementation of OpenStreetMaps ready for usage.
else if (params.maptype == "leaflet") {
var options = {
position: "topleft",
scale: true,
draw: {
polyline: {
metric: true,
shapeOptions: {
color: "#3388ff",
weight: 4,
opacity: 0.7
},
},
polygon: {
allowIntersection: false,
metric: true,
drawError: {
color: "#ff0000",
message: "<strong> You cant do that!",
},
shapeOptions: {
color: "#3388ff",
weight: 4,
opacity: 0.7,
fillColor: "#3388ff",
fillOpacity: 0.2
},
},
rectangle: {
allowIntersection: false,
metric: true,
drawError: {
color: "#ff0000",
message: "<strong> You cant do that!",
},
shapeOptions: {
color: "#3388ff",
weight: 4,
opacity: 0.7,
fillColor: "#3388ff",
fillOpacity: 0.2
},
},
circle: {
metric: true,
feet: true,
shapeOptions: {
color: "#3388ff",
weight: 4,
opacity: 0.7,
fillColor: "#3388ff",
fillOpacity: 0.2
},
},
marker: {
markerIcon: null,
zIndexOffset: 2000,
},
},
edit: {
featureGroup: drawings,
remove: true,
},
};
//Defining a drawing control for the Leaflet library.
drawControl = new L.Control.Draw(options);
osmap.addControl(drawControl);
//Event triggers added to clicking inside the maps through different types of markers and styles..
osmap.on("draw:created", function (e) {
var type = e.layerType,
layer = e.layer,
latLngs = layer.getLatLngs ? layer.getLatLngs() : layer.getLatLng();
// Attach custom onClickFunction if provided
if (args && args.onClickFunction) {
layer.on("click", function (event) {
args.onClickFunction(e);
});
} else {
// Default behavior if no custom function provided
if (type === "marker") {
layer.on("click", function () {
layer.bindPopup(`Marker coordinates: ${latLngs}.`).openPopup();
});
} else if (type === "rectangle") {
layer.on("click", function () {
layer.bindPopup(
`Rectangle corners coordinates: ${latLngs}.`
).openPopup();
});
} else if (type === "circle") {
layer.on("click", function () {
layer.bindPopup(
`Circle coordinates: ${layer.getLatLng()} with radius: ${layer.getRadius()}.`
).openPopup();
});
} else if (type === "polygon") {
layer.on("click", function () {
layer.bindPopup(
`Polygon corners coordinates: ${layer.getLatLngs()} with area.`
).openPopup();
});
}
}
drawings.addLayer(layer);
});
}
}
/**
* Returns a hex color for rendering
* @function generateColors
* @memberof map
* @returns {String} - Random color to be rendered.
*/
function generateColors() {
const letters = "0123456789ABCDEF";
let color = "#";
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
/**********************************/
/*** End of Supporting functions **/
/**********************************/
/**
* Returns the current map instance (Leaflet or Google Maps)
* @returns {Object} Map instance
* @private
*/
function getMap() {
return osmap;
}
//export { loader, Layers, renderMap, recenter, addCustomLegend, addGeoRasterLayer, removeMap, reinitializeController, addPopup, getMap };
export default {
loader, Layers, renderMap, recenter, addCustomLegend, addGeoRasterLayer, removeMap, reinitializeController, addPopup, getMap
};