Source: coz-popup.js

import { getLayers } from "./coz-helpers.js";
import { getQuery } from "./coz-helpers.js";
import { hasLayer } from "./coz-helpers.js";

// import {compile} from "handlebars";

// TODO ACCOUNT FOR GEOJSON TYPE
// TODO MAKE SEPARATE HIGHLIGHT AND CLICK FUNCTIONS

var highlightState = {
  click: {
    id: 0,
    layer: "",
    source: "",
    sourceLayer: "",
  },
  highlight: {
    id: 0,
    layer: "",
    source: "",
    sourceLayer: "",
  },
};

/**
 *
 * @param {*} map
 * @param {*} feature
 * @param {*} highlightOnly
 */
function highlightAddFeature(map, feature, highlightOnly) {
  // console.log(feature)
  var highlightMap = map;
  var highlightType = !highlightOnly ? "click" : "highlight";

  var highlightStateType = {
    click: {
      click: true,
    },
    highlight: {
      highlight: true,
    },
  };
  if (!feature.id && feature.id != 0) {
    console.warn("no feature id, highlight error");
    return;
  }

  highlightClearFeature(highlightMap, highlightType);

  var clone = [feature.layer].slice();
  var highlightLayer = clone[0];

  var height =
    highlightLayer.type === "fill-extrusion" ? highlightLayer.paint["fill-extrusion-height"] : 0;

  highlightLayer.id = "cozHighlight_" + highlightLayer.id.replace("cozHighlight_", "");

  highlightLayer.type =
    highlightLayer.type === "fill" || highlightLayer.type === "line"
      ? "line"
      : highlightLayer.type === "fill-extrusion"
      ? "fill"
      : "circle";

  highlightLayer.paint = !height
    ? highlightLayerPaint(highlightLayer.type)
    : highlightLayerPaint(highlightLayer.type, highlightLayer.paint["fill-extrusion-color"]);

  if (highlightLayer.type === "fill-extrusion") {
    highlightLayer.paint["fill-extrusion-height"] = height;
  }

  highlightLayer.layout = {
    visibility: "none",
  };

  if (!hasLayer(highlightMap, highlightLayer.id)) {
    console.log("adding highlight layer", highlightLayer.id);
    map.addLayer(highlightLayer);
  }

  highlightState[highlightType].id = feature.id;
  highlightState[highlightType].layer = highlightLayer.id;
  highlightState[highlightType].source = highlightLayer.source;
  highlightState[highlightType].sourceLayer = highlightLayer["source-layer"];

  map.setFeatureState(
    { source: highlightLayer.source, sourceLayer: highlightLayer["source-layer"], id: feature.id },
    highlightStateType[highlightType]
  );
  map.setLayoutProperty(highlightLayer.id, "visibility", "visible");
}

/**
 *
 * @param {*} e
 */
function highlightClearFeature(e, type, noFeatures) {
  var type = !type ? "click" : type;
  if (noFeatures) {
    //GET RID OF POPUP WINDOW ON CLICK HIGHLIGHT
    if (document.getElementById("map--info-window")) {
      document.getElementById("map--info-window").style.display = "none";
    }
  }

  var layers = getLayers(e);
  if (layers.indexOf(highlightState[type].layer) > -1) {
    if (type === "highlight") {
      e.setFeatureState(
        {
          source: highlightState[type].source,
          sourceLayer: highlightState[type].sourceLayer,
          id: highlightState[type].id,
        },
        { highlight: false }
      );
      var click = e.getFeatureState({
        source: highlightState[type].source,
        sourceLayer: highlightState[type].sourceLayer,
        id: highlightState[type].id,
      });
      if (!click.click) e.setLayoutProperty(highlightState[type].layer, "visibility", "none");
    } else {
      e.setFeatureState(
        {
          source: highlightState[type].source,
          sourceLayer: highlightState[type].sourceLayer,
          id: highlightState[type].id,
        },
        { click: false }
      );
      e.setLayoutProperty(highlightState[type].layer, "visibility", "none");

      //GET RID OF POPUP WINDOW ON CLICK HIGHLIGHT
      if (document.getElementById("map--info-window")) {
        document.getElementById("map--info-window").style.display = "none";
      }
    }
  }
}

/**
 *
 * @param {*} type
 */
function highlightLayerPaint(featureType, color) {
  if (color) {
    color =
      "rgba(" + color.r * 1000 + "," + color.g * 1000 + "," + color.b * 1000 + "," + 0.5 + ")";
  }
  var paint = {
    line: {
      "line-color": "yellow",
      "line-width": [
        "case",
        ["==", ["feature-state", "click"], true],
        6,
        ["==", ["feature-state", "highlight"], true],
        2,
        0,
      ],
      "line-opacity": [
        "case",
        ["==", ["feature-state", "click"], true],
        1,
        ["==", ["feature-state", "highlight"], true],
        0.7,
        0,
      ],
    },
    circle: {
      "circle-radius": 14,
      "circle-color": "transparent",
      "circle-stroke-color": "yellow",
      "circle-stroke-width": 4,
      "circle-opacity": [
        "case",
        ["==", ["feature-state", "click"], true],
        1,
        ["==", ["feature-state", "highlight"], true],
        0.7,
        0,
      ],
      "circle-stroke-opacity": [
        "case",
        ["==", ["feature-state", "click"], true],
        1,
        ["==", ["feature-state", "highlight"], true],
        0.7,
        0,
      ],
    },
    fill: {
      "fill-color": [
        "case",
        ["==", ["feature-state", "click"], true],
        "yellow",
        ["==", ["feature-state", "highlight"], true],
        "yellow",
        "transparent",
      ],
      "fill-opacity": 0.7,
    },
    "fill-extrusion": {
      "fill-extrusion-color": [
        "case",
        ["==", ["feature-state", "click"], true],
        "yellow",
        ["==", ["feature-state", "highlight"], true],
        "yellow",
        color,
      ],
      "fill-extrusion-opacity": 0.6,
    },
  };

  return paint[featureType];
}

/**
 *
 * @memberof cozMap
 * @method getFeatures
 * @param {*} map
 * @param {*} e
 * @param {*} l
 */

async function getFeatures(map, e, l) {
  var layers = map.getStyle().layers;
  if (layers.indexOf("gl-draw-polygon-stroke-inactive.cold") > -1) {
    if (
      map.getPaintProperty("gl-draw-polygon-stroke-inactive.cold", "line-color") &&
      map.getPaintProperty("gl-draw-polygon-stroke-inactive.cold", "line-color") != "red"
    ) {
      return [];
    }
  }

  //SHOULD MAYBE MOVE THIS OUT OF HERE TO A HELPER FILE

  var point;

  if (e.point && e.lngLat) {
    point = e.point;
  }
  if (!e.point && e.lngLat) {
    point = map.project(e.lngLat);
  }
  if (!e.point && !e.lngLat) {
    point = map.project(e.coordinates);
  }

  var bboxClickTargetSize = map.getZoom() * 1.5 < 18 ? 18 : map.getZoom() * 1.5;

  var bbox = [
    [point.x - bboxClickTargetSize / 2, point.y - bboxClickTargetSize / 2],
    [point.x + bboxClickTargetSize / 2, point.y + bboxClickTargetSize / 2],
  ];

  var bboxCoords = [map.unproject(bbox[0]), map.unproject(bbox[1])];

  var getFeatureOpts = getQuery(window.location.search.substring(1));
  if (getFeatureOpts && getFeatureOpts.debug === "true") {
    var poly = [
      [bboxCoords[0].lng, bboxCoords[0].lat],
      [bboxCoords[1].lng, bboxCoords[0].lat],
      [bboxCoords[1].lng, bboxCoords[1].lat],
      [bboxCoords[0].lng, bboxCoords[1].lat],
      [bboxCoords[0].lng, bboxCoords[0].lat],
    ];

    if (!map.getSource("clicked-bbox")) {
      map.addSource("clicked-bbox", {
        type: "geojson",
        data: {
          type: "Feature",
          geometry: {
            type: "Polygon",
            coordinates: [poly],
          },
        },
      });
      map.addLayer({
        id: "clicked-bbox",
        type: "fill",
        source: "clicked-bbox",
        layout: {},
        paint: {
          "fill-color": "firebrick",
          "fill-opacity": 0.5,
        },
      });
    } else {
      map.getSource("clicked-bbox").setData({
        type: "Feature",
        geometry: {
          type: "Polygon",
          coordinates: [poly],
        },
      });
    }
  }

  var queriedFeatures, bboxFeatures;

  if (l && l.length > 0) {
    queriedFeatures = map.queryRenderedFeatures(point, {
      layers: l,
    });
    bboxFeatures = map.queryRenderedFeatures(bbox, {
      layers: l,
    });
  } else {
    queriedFeatures = map.queryRenderedFeatures(point);
    bboxFeatures = map.queryRenderedFeatures(bbox);
  }

  const combinedFeatures = [...queriedFeatures, ...bboxFeatures];
  const features = cleanFeatures(combinedFeatures);
  if (features.length > 0) {
    for (let i = 0; i < features.length; i++) {
      const obj = features[i];
      try {
        const queryUrl = `https://cozmaps.org/features/functions/postgisftw.get_attachments/items.json?schema_name=public&table_name=${obj.sourceLayer?.replace(
          "public.",
          ""
        )}&parent_id=${obj.id}`;
        console.log({ queryUrl });
        const attachments = await fetch(queryUrl).then((res) => res.json());
        console.log({ attachments });
        if (attachments && attachments.length > 0) {
          attachments.forEach((a, i) => {
            obj.properties[`attachment_img_${i}`] = a?.url;
            //TODO add an image gallery to the popup if more than 1 image is found
            obj.properties[`attachment_img_name_${i}`] = a?.name;
          });
        }
      } catch (error) {
        console.error("Error fetching attachments:", error);
      }
    }
  }
  if (getFeatureOpts && getFeatureOpts.debug === "true") {
    console.log({ combinedFeatures });
    console.log({ features });
  }

  function cleanFeatures(objects) {
    const features = [];
    const props = [];
    for (let i = 0; i < objects.length; i++) {
      const obj = objects[i];
      obj.properties["__vt__id__index"] = obj.id;
      const p = JSON.stringify(obj.properties);
      let unique = true;
      props.forEach((e) => {
        if (p === e) {
          unique = false;
        }
      });
      if (unique) props.push(p);
      if (
        obj &&
        unique &&
        obj.source != "composite" &&
        obj.layer.metadata &&
        obj.layer.metadata.popup
      ) {
        features.push(obj);
      }
    }
    return features;
  }

  if (!features) {
    return;
  }

  //SORT FEATURE PROPERTIES BY FIELD NAME
  features.forEach(function (f) {
    var props = {};
    Object.keys(f.properties)
      .sort()
      .forEach(function (key) {
        props[key] = f.properties[key];
      });
    f.properties = props;
  });

  return features;
}

function popup(map, features, n, highlightOnly) {
  map.once("contextmenu", function () {
    highlightClearFeature(popupMap, highlightType);
    if (document.getElementById("map--info-window-close")) {
      document.getElementById("map--info-window-close").click();
    }
  });

  var popupMap = map;
  var highlightType = !highlightOnly ? "click" : "highlight";

  if (!features || features.length === 0) {
    highlightClearFeature(popupMap, highlightType, true);
    return;
  }

  var featuresTemp = features.filter((f) => {
    return f != null;
  });

  var popupFeatures = featuresTemp;

  if (!popupFeatures.length === 0 && !highlightOnly) {
    highlightClearFeature(popupMap, "click");
    return;
  }

  if (highlightOnly && popupFeatures.length > 0) {
    highlightAddFeature(map, features[0], true);
    return;
  }

  var x = !n ? 0 : n;

  highlightAddFeature(popupMap, popupFeatures[x]);

  var popupDiv = document.createElement("div");

  // GET TITLE OR USE FEATURE PROPERTIES

  var popupHeading = "Feature Properties";

  for (var v in popupFeatures[x].properties) {
    if (
      (v.toUpperCase() === "NAME" || v.toUpperCase() === "TITLE") &&
      popupFeatures[x].properties[v] &&
      popupFeatures[x].properties[v] != null &&
      popupFeatures[x].properties[v] != "null"
    ) {
      popupHeading = popupFeatures[x].properties[v]; //.substring(0,20).trim();
      // console.log(popupFeatures[x].properties[v] )
      //if ((popupFeatures[x].properties[v]).length > 20) popupHeading += "..."
    }
  }

  if (
    popupHeading === "Feature Properties" &&
    popupFeatures[x].layer.metadata &&
    popupFeatures[x].layer.metadata.name
  ) {
    popupHeading = popupFeatures[x].layer.metadata.name; //.substring(0,20).trim()
  }

  var popupDivTitle = popupShowMoreFeatures(popupMap, popupFeatures, popupHeading, x);

  popupDiv.appendChild(popupDivTitle);

  var popupHtml = popupBuildHtml(map, popupFeatures[x]);

  popupDiv.appendChild(popupHtml);

  /**
   * SHOULD MOVE THIS TO CREATING THE DOM ELEMENT IN THE JS FILE INSTEAD OF IN THE HTML WOULD NEED TO HAVE A STYLESHEET TO GO ALONG WITH IT
   */

  var popupWindow = document.getElementById("map--info-window-content");

  if (popupWindow.children.length > 0) popupWindow.removeChild(popupWindow.childNodes[0]);

  popupWindow.appendChild(popupDiv);

  document.getElementById("map--info-window").style.display = "block";

  if (window.innerWidth > 768) {
    // var width = document.querySelector("#map--info-window-content").children[0].children[1].offsetWidth
    // console.log(document.querySelector("#map--info-window-content").children[0].children[1], width)
    document.getElementById("map--info-window").style.width = "300px";
  } else {
    document.getElementById("map--info-window").style.width = "100%";
  }
}

function popupShowMoreFeatures(map, features, heading, x) {
  var newMap = map;
  var div = document.createElement("div");
  div.style.textAlign = "center";
  div.style.height = "auto";
  div.style.padding = "0 0 10px 0";
  div.style.borderBottom = "solid thin lightgray";

  var prev = document.createElement("button");
  prev.classList = "btn btn-link btn-sm";
  prev.innerHTML = "&#9664";

  prev.style.marginLeft = "10px";
  prev.style.float = "left";
  prev.onclick = function () {
    popup(newMap, features, x - 1);
  };

  var next = document.createElement("button");
  next.classList = "btn btn-link btn-sm";
  next.style.marginRight = "10px";
  next.innerHTML = "&#9654";
  next.style.float = "right";
  next.style.position = "absolute";
  next.style.top = "10px";
  next.style.right = "20px";
  next.onclick = function () {
    popup(newMap, features, x + 1);
  };

  if (x == features.length - 1) {
    next.style.opacity = "0";
    next.style.visibility = "hidden";
    next.style.cursor = "default";
  }

  if (x == 0) {
    prev.style.opacity = "0";
    prev.style.visibility = "hidden";
    prev.style.cursor = "default";
  }

  div.appendChild(prev);

  var span = document.createElement("div");
  span.textContent = heading;
  span.style.fontSize = "18px";
  span.style.lineHeight = "28px";
  span.style.fontWeight = "600";
  span.style.margin = "0 48px";
  div.appendChild(span);

  div.appendChild(next);

  return div;
}

function popupBuildHtml(map, f) {
  var popupDiv = document.createElement("div");

  var popupTemplate = !f.layer.metadata
    ? true
    : f.layer.metadata.popup && f.layer.metadata.popup === false
    ? false
    : f.layer.metadata.popup === true
    ? []
    : f.layer.metadata.popup;

  if (popupTemplate || popupTemplate === undefined) {
    var table = document.createElement("table");
    table.classList = "table table-striped";
    // table.style.width = "auto";
    table.style.minWidth = "280px";

    var tbody = document.createElement("tbody");

    var keys = Object.keys(f.properties);

    const aliasFields = {
      // "AADT": "Average Annual Daily Traffic",
      // "PARCELNUM": "Parcel ID",
      // "Land_Use": "Land Use",
      // "MASTER_TNM": "Tax Name",
      // "FIRST_OWNE": "Owner",
      // "ERU": "ERU for Account",
      // "Sump_Depth": "Sump Depth",
      // "Owner_1": "Owner",
      // "Legal_Description": "Legal Description",
      // "ELEV": "Elevation",
      // "SQFT": "Impervious Surface",
      // "FIELDID": "Field ID",
      // "url_pdf": "URL for Inpsection Report",
      // "IDUP": "Upstream Asset ID",
      // "IDDN": "Downstream Asset ID",
      // "INSPECTLOG": "Inspection Log",
      // "audlink": "Auditor Link",
      CreationDate: "Creation Date",
      EditDate: "Edit Date",
      url_1: "Image 1",
      url_2: "Image 2",
      url_3: "Image 3",
      url_4: "Image 4",
      esrignss_positionsourcetype: "Position Source Type",
    };

    const excludeFields = [
      "ACCOUNT_NUM",
      "CENTROID_X",
      "CENTROID_Y",
      "Creator",
      "DATEUPDATE",
      "END_X",
      "END_Y",
      "ERU_CHARGE",
      "Editor",
      "Enabled",
      "FID",
      "FIRST_NOTE",
      "GEO_ID",
      "GlobalID",
      "IKey",
      "INSIDE_X",
      "INSIDE_Y",
      "Land_Use_Name",
      "Log_Date",
      "MID_X",
      "MID_Y",
      "OBJECTID",
      "OID",
      "OLDFIELDID",
      "SHAPE_Area",
      "SHAPE_Leng",
      "SHAPE_Length",
      "START_X",
      "START_Y",
      "Shape_Area",
      "Shape_Leng",
      "Shape_Leng",
      "Shape_Length",
      "Shape_Length",
      "TEMP",
      "acres_1",
      "ca",
      "color",
      "dt_created",
      "dt_edited",
      "dt_updated",
      "id",
      "id",
      "l_bearing",
      "obs_pk",
      "pano",
      "point_y",
      "point_x",
      "shape_leng",
      "skey",
      "userkey",
      "vtlid",
      "xcoord",
      "ycoord",
      "extrudeheight",
      "parcel_number",
      "uuid",
      "plan_local",
      "__vt__id__index",
    ];

    excludeFields.map(function (f, i) {
      excludeFields[i] = f.toUpperCase();
    });

    //GET POPUP VALUES FROM LAYER METADATA IF THEY EXIST
    var layers = map.getStyle().layers;

    var layer = layers.filter((l) => {
      return l.id === f.layer.id.replace("cozHighlight_", "");
    });

    var popupValues = !layer[0].metadata
      ? null
      : !layer[0].metadata.popup
      ? null
      : layer[0].metadata.popup;

    if (popupValues && popupValues != true) {
      // console.log(popupValues.length)
      keys = keys.filter((k) => {
        return popupValues.indexOf(k) > -1;
      });
    }

    var hasImage = false;

    table.innerHTML += `<!-- ${f.layer.id} -->`;

    keys.forEach(function (k, i) {
      const key = aliasFields[k] ? aliasFields[k] : k.replace(/esrignss/g, "gps_");
      var val = f.properties[k];
      if (excludeFields.indexOf(k.toUpperCase()) < 0) {
        if (val && val != null && val != "" && val != " " && val != undefined && val != "null") {
          var property = isMapillary(val, f)
            ? formatMapillary(val)
            : isEpochDate(val)
            ? formatDate(val)
            : isLink(val)
            ? formatLink(val)
            : isParcel(val)
            ? formatParcel(val)
            : isNumber(val)
            ? formatNumber(val)
            : val;
          tbody.innerHTML += `<tr><td>${formatTitle(key)}</td><td>${property}</td></tr>`;
        }
        if (!hasImage) hasImage = isImage(k, val);
      }
    });
    if (hasImage != false) {
      var popupImageLink = document.createElement("a");
      popupImageLink.href = hasImage;
      popupImageLink.target = "_blank";

      var popupImg = document.createElement("img");
      popupImg.src = hasImage;
      popupImg.style.width = "100%";

      popupImageLink.appendChild(popupImg);
      popupDiv.appendChild(popupImageLink);
    }

    table.appendChild(tbody);
    popupDiv.appendChild(table);
  }

  return popupDiv;
}

export { getFeatures, popup, highlightAddFeature, highlightClearFeature };

/****
 * HELPERS
 */

function isImage(key, value) {
  if (!value) return false;
  if (key === "alt_link") return false;
  if (value.toString().indexOf("https") != 0) return false;
  if (value.split(",")) {
    value = value.split(",")[0];
  }
  let img = value.split(".");
  let images = ["JPG", "JPEG", "PNG"];

  console.log(images.indexOf(img[img.length - 1].toUpperCase()));
  if (images.indexOf(img[img.length - 1].toUpperCase()) < 0) {
    return false;
  } else {
    return value;
  }
}

function formatTitle(str) {
  return str.replace(/_/g, " ");
}

function isMapillary(value, feature) {
  return value === "key" && feature.source === "mapillary";
}

function formatMapillary(value) {
  return `<img src='https://d1cuyjsrcm0gby.cloudfront.net/${value}/thumb-320.jpg'>`;
}

function isEpochDate(value) {
  var simpleDateRegex = new RegExp(/^\d{13}$/);
  return simpleDateRegex.test(value);
}

function formatDate(value) {
  var valueDate = new Date(value);
  return valueDate.toLocaleDateString();
}

function isLink(value) {
  var urlRegExCheck = new RegExp(
    /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi
  );
  return urlRegExCheck.test(value);
}

function formatLink(value) {
  if (value.split(",").length > 1) {
    value = value.split(",")[0];
  }
  return `<a href="${value}" target='_blank'><i class='fas fa-external-link-alt'></i> View Resource</a></td></tr>`;
}

function isParcel(value) {
  var parcelRegex = new RegExp(/^\d{2}[-]\d{2}[-]\d{2}[-]\d{2}[-]\d{3}$/);
  return value == "PARCELNUM" || parcelRegex.test(value);
}

function formatParcel(value) {
  return `<a href='https://muskingumoh-auditor-classic.ddti.net/Data.aspx?ParcelID=${value}' target='_blank' class='text-center'><i class='fas fa-external-link-alt'></i> ${value}</a>`;
}

function isNumber(value) {
  var isNumber = /^\d*\.?\d*$/;
  var isYear = /^[12][0-9]{3}$/;
  return isNumber.test(value) && !isYear.test(value);
}

function formatNumber(value) {
  return Number(value).toLocaleString();
}

/**
 * genId
 * Generate an ID of x length, NOT IN USE
 * @param {Number} length
 */
function genId(length) {
  var result = "";
  var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  var charactersLength = characters.length;
  for (var i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
}