modules/map/map.js

import * as mapsources from "../../external/maps/mapsources.js";
import tileprov from "../../external/maps/tileprov.js";
import * as divisors from "../visualize/divisors.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();
  }
}

/**
 * Layer function for appending tiles, geodata, markers, kml or drawing tools to a map.
 * @function Layers
 * @memberof map
 * @param {Object} args - Contains: type (tile, geodata, markers, kml, draw, removelayers), name (name of layer)
 * @param {Object} data - Contains: data as a JS array.
 * @returns {Element} Layer appended to a map div that has already been created. The layer is added into the global
 * layer object.
 * @example
 * hydro.map.Layers({args: {type: 'tile', name: 'someName'}, data: [data1, data2...]})
 * hydro.map.Layers({args: {type: 'geodata', name: 'someName'}, data: {somegeoJSON}})
 * hydro.map.Layers({args: {type: 'marker', name: 'someName'}, data: [markerLat, marketLon]})
 */

async function Layers({ params, args, data } = {}) {
  var layertype, mapconfig = {};
  //The mapconfig is set to be as OSM.
  if(mapType === "google") {
    mapconfig.maptype= "google" 
  } else if(mapType === "leaflet"){
    mapconfig.maptype= "leaflet" }
  
  //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,
    name: args.name,
    coord: data,
    popUpContent:  args.popUp|| null,
    styleFunction: args.styleFunction || null,
    popUpFunction: args.popUpFunction || null,
    onClickFunction: args.onClickFunction || null
  };


  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">&#9662;</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 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!");
        }
      }
    }

    //in case the map required is osm.
    else if (mapconfig.maptype === "leaflet") {
      var layer,
      type = layertype.type,
      layername = layertype.name;
    
    await osmap.whenReady(async function () {
      if (typeof layercontroller === "undefined") {
        //Defining the controller for the layers.
        layercontroller = new L.control.layers().addTo(osmap);
      }

      if (type === "tile") {
        //Defining a new layertype
        layer = new L.TileLayer(
          tileprov[layername].url,
          tileprov[layername].options
        );
        Object.assign(baselayers, {
          [layername]: layer,
        });
        osmap.addLayer(layer)
        layercontroller.addBaseLayer(layer, layername);
      }


      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);
        //osmap.fitBounds(layer.getBounds());
      } 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);
        //osmap.fitBounds(layer.getBounds());
      } else if (type === "draw") {
        if (!isDrawToolAdded) {
        //Caller for drawing tool renderer.
        drawings = new L.FeatureGroup();
        draw({ params: mapconfig });
        isDrawToolAdded = true
      } else {
        console.log("Draw tool is already added in the map.")
      }
        osmap.addLayer(drawings);
      } else if (type === "removelayers") {
        //If using HydroLang-ML, there is no need to use this functions since the layers that are not to be included in a map
        //Are simply not added into the request as a layer.
        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();
        } else {
          console.log("there is no layer with that name!");
        }
      }
    });
  }
  } 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 = {
      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) {
          return L.circleMarker(latlng, function (feature) {
            if(styleFunction!== null) return styleFunction(feature);
            return geoPoint;
          });
        },
        onEachFeature: onEachFeature,
        
        style:  function (feature) {
          if(styleFunction!== null) return styleFunction(feature);
          return geoPoint;
        },
      });
    } 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;

  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 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).
 */
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
 * hydro.map.draw({params:{maptype: 'someMap'}})
 */
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: "#bada55",
          },
        },
        polygon: {
          allowIntersection: false,
          metric: true,
          drawError: {
            color: "#e1e1100",
            message: "<strong> You cant do that!",
          },
          shapeOptions: {
            color: "#432",
          },
        },
        rectangle: {
          allowIntersection: false,
          metric: true,
          drawError: {
            color: "#e1e1100",
            message: "<strong> You cant do that!",
          },
          shapeOptions: {
            color: "#432",
          },
        },
        circle: {
          metric: true,
          feet: true,
          shapeOptions: {
            color: "#432",
          },
        },
        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();
      if (type === "marker") {
        layer.on("click", function () {
          layer.bindPopup(`Marker coordinates: ${latLngs}.`);
        });
      } else if (type === "rectangle") {
        layer.on("click", function () {
          layer.bindPopup(
            `Rectangle corners coordinates: ${latLngs}.`
          );
        });
      } else if (type === "circle") {
        layer.on("click", function () {
          layer.bindPopup(
            `Circle coordinates: ${layer.getLatLng()} with radius: ${layer.getRadius()}.`
          );
        });
      } else if (type === "polygon") {
        layer.on("click", function () {
          layer.bindPopup(
            `Polygon corners coordinates: ${layer.getLatLngs()} with area.`
          );
        });
      }
      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 **/
/**********************************/

export { loader, Layers, renderMap, recenter, addCustomLegend };