<template>
        <div id="map">
        </div>
</template>

<style lang="sass">
  @import '../../node_modules/leaflet/dist/leaflet.css'
</style>

<style>
#map {
  height: 100%;
  background-color: #dae4f7;
}

.theme--dark #map {
  background-color: #111;
}

.cluster {
  border: 2px solid green;
  border-radius: 6px;
}
</style>

<script>
import L from 'leaflet'
import 'leaflet-realtime'
import 'leaflet.markercluster'
import 'leaflet-arrowheads'
import { mapActions, mapGetters, mapState } from 'vuex';
import ftstamp from '@/lib/FormatTimestamp'
import zoomFitIcon from '@/assets/zoom-fit-best.svg'
import { markdown, markdownInline } from '@/lib/markdown';

var map;

const tileMaps = {

  "No background": L.tileLayer("/nullmap"), // FIXME

  "Norwegian nautical charts": L.tileLayer('https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=sjokartraster&zoom={z}&x={x}&y={y}', {
    attribution: '<a href="http://www.kartverket.no/" target="_blank">Kartverket</a>'
  }),

  "GEBCO": L.tileLayer.wms("https://www.gebco.net/data_and_products/gebco_web_services/2019/mapserv?", {layers: "GEBCO_2019_Grid_2", attribution: "<a href='https://www.gebco.net/' target='_blank' title='General Bathymetric Chart of the Oceans'>GEBCO</a>"}),

  "OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
  }),

  "MODIS Satellite": L.tileLayer('https://map1.vis.earthdata.nasa.gov/wmts-webmerc/MODIS_Terra_CorrectedReflectance_TrueColor/default/{time}/{tilematrixset}{maxZoom}/{z}/{y}/{x}.{format}', {
    attribution: 'Imagery provided by services from the Global Imagery Browse Services (GIBS), operated by the NASA/GSFC/Earth Science Data and Information System (<a href="https://earthdata.nasa.gov">ESDIS</a>) with funding provided by NASA/HQ.',
    bounds: [[-85.0511287776, -179.999999975], [85.0511287776, 179.999999975]],
    minZoom: 1,
    maxZoom: 9,
    format: 'jpg',
    time: '',
    tilematrixset: 'GoogleMapsCompatible_Level'
  })

};

const layers = {
  "OpenSeaMap": L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
    attribution: 'Map data: &copy; <a href="http://www.openseamap.org">OpenSeaMap</a> contributors'
  }),

  "Preplots": L.geoJSON(null, {
    pointToLayer (point, latlng) {
      return L.circle(latlng, {
        radius: 1,
        color: "#3388ff",
        stroke: false,
        fillOpacity: 0.8
      });
    },
    style (feature) {
      return {
        opacity: 0.5
      }
    },
    onEachFeature (feature, layer) {
      const popup = feature.geometry.type == "Point"
        ? `Preplot<br/>Point <b>${feature.properties.line} / ${feature.properties.point}</b>`
        : `Preplot<br/>Line <b>${feature.properties.line}</b>`;
      layer.bindTooltip(popup, {sticky: true});
    },
  }),

  "Saillines": L.geoJSON(null, {
    pointToLayer (point, latlng) {
      return L.circle(latlng, {
        radius: 1,
        color: "#3388ff",
        stroke: false,
        fillOpacity: 0.8
      });
    },
    style (feature) {
      return {
        opacity: feature.properties.ntba ? 0.2 : 0.5,
        color: "cyan"
      }
    },
    onEachFeature (feature, layer) {
      const p = feature.properties;
      const popup = feature.geometry.type == "Point"
        ? `Preplot<br/>Point <b>${p.line} / ${p.point}</b>`
        : `Preplot${p.ntba? " (NTBA)":""}<br/>Line <b>${p.line}</b>${p.remarks ? markdown(p.remarks) : ""}`;
      layer.bindTooltip(popup, {sticky: true});
    },
  }),

  "Plan": L.geoJSON(null, {
    arrowheads: {
      size: "8px",
      frequency: "200px"
    },
    style (feature) {
      return {
        color: "brown",
        opacity: 0.7
      }
    },
    onEachFeature (feature, layer) {
      const p = feature.properties;
      if (feature.geometry) {

        const d = p.duration;
        const duration = `${d.days? d.days+" days ":""}${String(d.hours||0).padStart(2, "0")}:${String(d.minutes||0).padStart(2, "0")}`;

        const speed = (p.length / (new Date(p.ts1) - new Date(p.ts0))) * 3.6/1.852 * 1000;

        const remarks = p.remarks
          ? "<hr/>"+markdownInline(p.remarks)
          : "";

        const popup = `Planned sequence <b>${p.sequence}</b><br/>
            Line <b>${p.line}</b> – ${p.name}<br/>
            ${p.num_points} points<br/>
            ${Math.round(p.length)} m ${p.azimuth.toFixed(1)}°<br/>
            ${duration} @ ${speed.toFixed(1)} kt<br/>
            <table><tr><td><b>${p.fsp}</b></td><td>@ ${ftstamp(p.ts0)}</td></tr><tr><td><b>${p.lsp}</b></td><td>@ ${ftstamp(p.ts1)}</td></tr></table>${remarks}`;
        layer.bindPopup(popup);
        layer.bindTooltip(popup, {sticky: true});
      }
    }
  }),

  "Raw lines": L.geoJSON(null, {
    pointToLayer (point, latlng) {
      return L.circle(latlng, {
        radius: 3,
        color: "red",
        stroke: false,
        fillOpacity: 0.8
      });
    },
    style (feature) {
      return {
        color: "red"
      }
    },
    onEachFeature (feature, layer) {
      const p = feature.properties;
	  if (feature.geometry) {

        const ntbp = p.ntbp
          ? " <b>(NTBP)</b>"
          : "";

        const remarks = p.remarks
          ? "<hr/>"+markdown(p.remarks)
          : "";

		const popup = feature.geometry.type == "Point"
			? `Raw sequence ${feature.properties.sequence}${ntbp}<br/>Point <b>${feature.properties.line} / ${feature.properties.point}</b><br/>${feature.properties.objref}<br/>${feature.properties.tstamp}`
			: `Raw sequence ${p.sequence}${ntbp}<br/>
			Line <b>${p.line}</b><br/>
			${p.num_points} points${p.missing_shots ? " <i>("+p.missing_shots+" missing)</i>" : ""}<br/>
			${Math.round(p.length)} m ${p.azimuth.toFixed(1)}°<br/>
			${p.duration}<br/>
			<table><tr><td><b>${p.fsp}</b></td><td>@ ${ftstamp(p.ts0)}</td></tr><tr><td><b>${p.lsp}</b></td><td>@ ${ftstamp(p.ts1)}</td></tr></table>${remarks}`;
		layer.bindTooltip(popup, {sticky: true});

	  }
    }
  }),

  "Final lines": L.geoJSON(null, {
    pointToLayer (point, latlng) {
      return L.circle(latlng, {
        radius: 3,
        color: "orange",
        stroke: false,
        fillOpacity: 0.8
      });
    },
    style (feature) {
      return {
        color: "orange"
      }
    },
    onEachFeature (feature, layer) {
      const p = feature.properties;

        const remarks = p.remarks
          ? "<hr/>"+markdown(p.remarks)
          : "";

      const popup = feature.geometry.type == "Point"
        ? `Final sequence ${p.sequence}<br/>Point <b>${p.line} / ${p.point}</b><br/>${p.objref}<br/>${p.tstamp}`
        : `Final sequence ${p.sequence}<br/>
          Line <b>${p.line}</b><br/>
          ${p.num_points} points${p.missing_shots ? " <i>("+p.missing_shots+" missing)</i>" : ""}<br/>
          ${Math.round(p.length)} m ${p.azimuth.toFixed(1)}°<br/>
          ${p.duration}<br/>
          <table><tr><td><b>${p.fsp}</b></td><td>@ ${ftstamp(p.ts0)}</td></tr><tr><td><b>${p.lsp}</b></td><td>@ ${ftstamp(p.ts1)}</td></tr></table>${remarks}`;
//        : `Final sequence ${feature.properties.sequence}<br/>Line <b>${feature.properties.line}</b><br/>${feature.properties.num_points} points<br/>From ${feature.properties.ts0}<br/>until ${feature.properties.ts0}`;
      layer.bindTooltip(popup, {sticky: true});
    }
  }),

  "Events (QC)": L.geoJSON(null),

  "Events (Other)": L.geoJSON(null),

  "Real-time": L.realtime({
    url: '/api/navdata/gis/point?limit=1',
    type: 'json',
  }, {
    start: false,
    getFeatureId (feature) {
      return feature.properties.vesselName || feature.properties.vesselId;
    },
    pointToLayer (point, latlng) {
      return L.circleMarker(latlng, {
        radius: 10,
        color: "magenta",
        stroke: false,
        fillOpacity: 0.8
      });
    },
    style (feature) {
      return {
        color: "magenta",
        opacity: 0.5
      }
    },
    onEachFeature (feature, layer) {
      layer.bindPopup(function () {
        return makeRealTimePopup(feature);
      });
    }
  }),

  "Real-time (trail)": L.realtime({
    url: '/api/navdata/gis/line?limit=10000',
    type: 'json',
  }, {
    start: false,
    interval: 60 * 1000,
    style (feature) {
      return {
        color: "magenta",
        opacity: 0.5
      }
    }
  })
};

layers["Real-time"].on('update', function (e) {
  Object.keys(e.features).forEach( (id) => {
    const feature = e.features[id];
    this.getLayer(id).bindPopup(makeRealTimePopup(feature));
  });
}, this);

layers["Real-time"].on('add', function (e) {
  this.start();
});

layers["Real-time"].on('remove', function (e) {
  this.stop();
});

layers["Real-time (trail)"].on('add', function (e) {
  this.start();
});

layers["Real-time (trail)"].on('remove', function (e) {
  this.stop();
});

function makeRealTimePopup(feature) {
  const p = feature.properties;
  const online = p._online
    ? `
      <table>
        <tr><td><b>Line name:</b></td><td>${p.lineName}</td></tr>
        <tr><td><b>Sequence:</b></td><td>${p._sequence}</td></tr>
        <tr><td><b>Line:</b></td><td>${p._line}</td></tr>
        <tr><td><b>Shot:</b></td><td>${p._point}</td></tr>
        <tr><td><b>Crossline:</b></td><td>${p.crossline || "???"} m</td></tr>
        <tr><td><b>Inline:</b></td><td>${p.inline || "???"} m</td></tr>
        <tr><td><b>Source fired:</b></td><td>${p.src_number|| "???"}</td></tr>
        <tr><td><b>Manifold press.:</b></td><td>${p.manifold|| "???"} psi</td></tr>
      </table>
    `
    : "";
  const wgs84 = `${feature.geometry.coordinates[1].toFixed(6)}, ${feature.geometry.coordinates[0].toFixed(6)}`
  const popup = `
    Position as of ${p.tstamp}<br/><hr/>
    ${online}
    <table>
      <tr><td><b>Speed:</b></td><td>${p.speed ? (p.speed*3.6/1.852).toFixed(1) : "???"} kt</td></tr>
      <tr><td><b>CMG:</b></td><td>${p.cmg || "???"}°</td></tr>
      <tr><td><b>Water depth:</b></td><td>${p.waterDepth || "???"} m</td></tr>
      <tr><td><b>WGS84:</b></td><td>${wgs84}</td></tr>
      <tr><td><b>Local grid:</b></td><td>${p.easting.toFixed(1)}, ${p.northing.toFixed(1)}</td></tr>
    </table>
  `
  return popup;
}


export default {
  name: "Map",

  data () {
    return {
      //map: null,
      requestsCount: 0,
      layerRefreshConfig: [
        {
          layer: layers.Preplots,
          url: (query = "") => {
            return map.getZoom() < 18
              ? `/project/${this.$route.params.project}/gis/preplot/line`
              : `/project/${this.$route.params.project}/gis/preplot/point?${query.toString()}`;
          }
        },
        {
          layer: layers["Saillines"],
          url: (query = "") => {
            const q = new URLSearchParams(query);
            q.set("class", "V");
            return map.getZoom() < 18
              ? `/project/${this.$route.params.project}/gis/preplot/line?${q.toString()}`
              : `/project/${this.$route.params.project}/gis/preplot/point?${q.toString()}`;
          }
        },
        {
          layer: layers.Plan,
          url: (query = "") => {
            return `/project/${this.$route.params.project}/plan`;
          }
        },
        {
          layer: layers["Raw lines"],
          url: (query = "") => {
            return map.getZoom() < 17
              ? `/project/${this.$route.params.project}/gis/raw/line`
              : `/project/${this.$route.params.project}/gis/raw/point?${query.toString()}`;
          }
        },
        {
          layer: layers["Final lines"],
          url: (query = "") => {
            return map.getZoom() < 17
              ? `/project/${this.$route.params.project}/gis/final/line`
              : `/project/${this.$route.params.project}/gis/final/point?${query.toString()}`;
          }
        }
      ],
      labels: {},
      hashMarker: null,
      references: {}
    };
  },

  computed: {
    ...mapGetters(['user', 'loading', 'serverEvent', 'lineName', 'serverEvent']),
    ...mapState({projectSchema: state => state.project.projectSchema})
  },

  watch: {
    loading (isLoading) {
      const el = document.getElementById("loadingControl");
      if (el) {
        if (isLoading) {
          el.classList.remove("d-none");
        } else {
          el.classList.add("d-none");
        }
      }
    },

    user (newVal, oldVal) {
      if (newVal && (!oldVal || newVal.name != oldVal.name)) {
        this.initView();
      }
    },

    serverEvent (event) {
      if (event.channel == "realtime" && event.payload && event.payload.new) {
        const rtLayer = layers["Real-time"];
        if (rtLayer.isRunning()) {
          const geojson = {
            type: "Feature",
            geometry: event.payload.new.geometry,
            properties: event.payload.new.meta
          };
          rtLayer.update(geojson);
        }
      } else if (event.channel == "event" && event.payload.schema == this.projectSchema) {
        //console.log("EVENT", event);
      }
    },

    $route (to, from) {
      if (to.name == "map") {
        this.setHashMarker();
      }
    }
  },

  methods: {

    async fitProjectBounds () {
      const res = await this.api([`/project/${this.$route.params.project}/gis`]);
      const bbox = new L.GeoJSON(res);
      map.fitBounds(bbox.getBounds());
    },

    getEvents (ffn = i => true) {
      return async (success, error) => {
        const url = `/project/${this.$route.params.project}/event`;
        const data = await this.api([url, {headers: {"Accept": "application/geo+json"}}]);
        if (data) {

          const colour = (feature) => {
            if (feature.properties.meta?.qc_id) {
              return feature.properties.labels.includes("QCAccepted")
                ? "lightgray"
                : "green";
            } else if (feature.properties.type == "midnight shot") { // FIXME
              // The above will no longer work. See #223.
              return "cyan";
            } else if (feature.properties.labels?.length) {
              return this.labels?.[feature.properties.labels[0]]?.view?.colour ?? "orange";
            }
            return "brown";
          }

          const features = data.filter(ffn).map(feature => {
            feature.properties.colour = colour(feature);
            return feature;
          });
          success(features);
        } else {
          error("Failed to get events");
        }
      }
    },

    async refreshLayers (layerset) {

      const bounds = map.getBounds().pad(0.3);
      const bboxScale = map.getZoom()/5;
      const bbox = [
        bounds._southWest.lng,
        bounds._southWest.lat,
        bounds._northEast.lng,
        bounds._northEast.lat
      ].map(i => i.toFixed(bboxScale)).join(",");
      const limit = 10000; // Empirical value

      const query = new URLSearchParams({bbox, limit});

      for (const l of this.layerRefreshConfig.filter(i => !layerset || layerset.includes(i.layer))) {
        if (map.hasLayer(l.layer)) {

          const url = l.url(query);
          // Skip unnecessary requests
          if (url == l.layer.lastRequestURL) continue;

          if (l.layer.abort && l.layer.abort instanceof AbortController) {
            l.layer.abort.abort();
          }

          l.layer.abort = new AbortController();
          const signal = l.layer.abort.signal;
          const init = {
            signal,
            headers: {
              Accept: "application/geo+json"
            }
          };

          // Firing all refresh events asynchronously, which is OK provided
          // we don't have hundreds of layers to be refreshed.
          this.api([url, init])
          .then( (layer) => {
            if (!layer) {
              return;
            }

            if (typeof l.transform == 'function') {
              layer = l.transform(layer);
            }

            l.layer.clearLayers();
            //if (layer instanceof L.Layer || (layer.features && layer.features.length < limit) || ("length" in layer && layer.length < limit)) {
            if (layer instanceof L.Layer || ((layer.features?.length ?? layer?.length) < limit)) {

              if (l.layer.addData) {
                l.layer.addData(layer);
              } else if (l.layer.addLayer) {
                l.layer.addLayer(layer);
              }

              l.layer.lastRequestURL = url;
            } else if (!layer.features) {
              console.log(`Layer ${url} is empty`);
            } else {
              console.warn(`Too much data from ${url} (${layer.features?.length ?? layer.length} ≥ ${limit} features)`);

              this.showSnack([`Layer ${l.layer.options.userLayerName ? "‘"+l.layer.options.userLayerName+"’ " : ""}is too large: ${layer.features?.length ?? layer.length} features; maximum is ${limit}`, "error"]);
            }
          })
          .finally( () => {
            delete l.layer.abort;
          });
        }
      }
    },

    updateURL (includeLayers = true) {
      const { lat, lng } = map.getCenter();
      const zoom = map.getZoom();
      const o = [];
      const l = [];
      let value;
      if (includeLayers) {
        for (const overlay of Object.keys(tileMaps)) {
          if (map.hasLayer(tileMaps[overlay])) {
            o.push(overlay);
          }
        }
        for (const layer of Object.keys(layers)) {
          if (map.hasLayer(layers[layer])) {
            l.push(layer);
          }
        }
        value = `${zoom}/${lat}/${lng}:${o.join(";")}:${l.join(";")}`;
      } else {
        value = `${zoom}/${lat}/${lng}`;
      }

      if (value) {
        localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/view`, value);
      }
    },

    decodeURL () {
      const value = localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/view`);

      if (!value) {
        return {};
      }

      const parts = value.split(":");
      const activeOverlays = parts.length > 1 && parts[1].split(";");
      const activeLayers = parts.length > 2 && parts[2].split(";");
      let position = parts && parts[0].split("/").map(i => Number(i));
      if (position.length != 3) {
        position = false;
      }

      return {position, activeOverlays, activeLayers};
    },

    initView () {
      if (!map) {
        return;
      }

      map.off('overlayadd', this.updateURL);
      map.off('overlayremove', this.updateURL);
      map.off('layeradd', this.updateURL);
      map.off('layerremove', this.updateURL);

      const init = this.decodeURL();

      if (init.activeOverlays) {
        Object.keys(tileMaps).forEach(k => {
          const l = tileMaps[k];
          if (init.activeOverlays.includes(k)) {
            if (!map.hasLayer(l)) {
              l.addTo(map);
            }
          } else {
            map.removeLayer(l);
          }
        });
      } else {
        tileMaps["No background"].addTo(map);
      }

      if (init.activeLayers) {
        Object.keys(layers).forEach(k => {
          const l = layers[k];
          if (init.activeLayers.includes(k)) {
            if (!map.hasLayer(l)) {
              l.addTo(map);
            }
          } else {
            map.removeLayer(l);
          }
        });
      } else {
        layers.OpenSeaMap.addTo(map);
        layers.Preplots.addTo(map);
      }

      if (init.position) {
        map.setView(init.position.slice(1), init.position[0]);
      }

      map.on('overlayadd', this.updateURL);
      map.on('overlayremove', this.updateURL);
      map.on('layeradd', this.updateURL);
      map.on('layerremove', this.updateURL);

    },

    setHashMarker () {

      const crosshairsMarkerIcon = L.divIcon({
        iconSize:   [20, 20],
        iconAnchor: [10, 10],
        className: 'svgmarker',
        html: `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path style="fill:inherit;fill-opacity:1;stroke:none"
    d="M 7 3 L 7 4.03125 A 4.5 4.5 0 0 0 3.0332031 8 L 2 8 L 2 9 L 3.03125 9 A 4.5 4.5 0 0 0 7 12.966797 L 7 14 L 8 14 L 8 12.96875 A 4.5 4.5 0 0 0 11.966797 9 L 13 9 L 13 8 L 11.96875 8 A 4.5 4.5 0 0 0 8 4.0332031 L 8 3 L 7 3 z M 7 5.0390625 L 7 8 L 4.0410156 8 A 3.5 3.5 0 0 1 7 5.0390625 z M 8 5.0410156 A 3.5 3.5 0 0 1 10.960938 8 L 8 8 L 8 5.0410156 z M 4.0390625 9 L 7 9 L 7 11.958984 A 3.5 3.5 0 0 1 4.0390625 9 z M 8 9 L 10.958984 9 A 3.5 3.5 0 0 1 8 11.960938 L 8 9 z "
    />
</svg>
      `
      });

      const updateMarker = (latlng) => {
        if (this.hashMarker) {
          if (latlng) {
            this.hashMarker.setLatLng(latlng);
          } else {
            map.removeLayer(this.hashMarker);
            this.hashMarker = null;
          }
        } else if (latlng) {
          this.hashMarker = L.marker(latlng, {icon: crosshairsMarkerIcon, interactive: false});
          this.hashMarker.addTo(map).getElement().style.fill = "fuchsia";
        }
      }

      const parts = document.location.hash.substring(1).split(":")[0].split("/").map(p => decodeURIComponent(p));
      if (parts.length == 3) {
        setTimeout(() => map.setView(parts.slice(1).reverse(), parts[0]), 500);
        updateMarker(parts.slice(1).reverse());
      } else if (parts.length == 2) {
        parts.reverse();
        setTimeout(() => map.panTo(parts), 500);
        updateMarker(parts);
      } else {
        updateMarker();
      }
    },

    async getLabelDefinitions () {
      const url = `/project/${this.$route.params.project}/label`;

      this.labels = await this.api([url]) || [];
    },


    removeUserLayers () {
      map.eachLayer( layer => {
        if (layer.options.userLayer === true) {
          console.log("Removing", layer);
          layer.eachLayer( sublayer => {
            const idx = this.layerRefreshConfig.findIndex(i => i.layer == layer);
            if (idx != -1) {
              this.layerRefreshConfig.splice(idx, 1);
            }
          });
          map.removeLayer(layer);
          this.references.layerControl.removeLayer(layer);
        }
      });
    },

    async addUserLayers (userLayers) {

      const options = {
        userLayer: true,
        style (feature) {
          const style = {
            stroke: undefined,
            color: "grey",
            weight: 2,
            opacity: 0.5,
            lineCap: undefined,
            lineJoin: undefined,
            dashArray: undefined,
            dashOffset: undefined,
            fill: undefined,
            fillColor: "lightgrey",
            fillOpacity: 0.5,
            fillRule: undefined
          };

          for (let key in style) {
            switch (key) {
              case "color":
                style[key] = feature.properties?.colour ?? feature.properties?.color ?? style[key];
                break;
              case "fillColor":
                style[key] = feature.properties?.fillColour ?? feature.properties?.fillColor ?? style[key];
                break;
              default:
                style[key] = feature.properties?.[key] ?? style[key];
            }

            if (typeof style[key] === "undefined") {
              delete style[key];
            }
          }

          return style;
        }
      };

      const userLayerGroups = {};
      userLayers.forEach(layer => {
        if (!(layer.name in userLayerGroups)) {
          userLayerGroups[layer.name] = [];
        }
        userLayerGroups[layer.name].push(layer);
      });

      for (let userLayerName in userLayerGroups) {
        const userLayerGroup = userLayerGroups[userLayerName];

        const layer = L.featureGroup(null, {userLayer: true, userLayerGroup: true, userLayerName});
        userLayerGroup.forEach(l => {
          const sublayer = L.geoJSON(null, {...options, userLayerName});
          layer.addLayer(sublayer);
          sublayer.on('add', ({target}) => {
            this.refreshLayers([target])
          });

          if (l.tooltip) {
            sublayer.bindTooltip((layer) => {
              return layer?.feature?.properties?.[l.tooltip] ?? userLayerName;
            });
          }

          if (l.popup) {
            if (l.popup === true) {
              sublayer.bindPopup((layer) => {
                const p = layer?.feature?.properties;
                let t = "";
                if (p) {
                  t += "<table>";
                  for (let [k, v] of Object.entries(p)) {
                    t += `<tr><td><b>${k}: </b></td><td>${v}</td></tr>`;
                  }
                  t += "</table>";
                  return t;
                }
                return userLayerName;
              });
            } else {
              sublayer.binPopup((layer) => {
                return layer?.feature?.properties?.[l.popup] ?? userLayerName;
              });
            }
          }

          const refreshConfig = {
            layer: sublayer,
            url: (query = "") => {
              return `/files/${l.path}`;
            }
          };

          this.layerRefreshConfig.push(refreshConfig);
        });

        layer.on('add', ({target}) => {
          this.refreshLayers(target.getLayers())
        });
        this.references.layerControl.addOverlay(layer, `<span title="User layer" style="text-decoration: dotted underline;">${userLayerName}</span>`);
      }
    },

    async fetchUserLayers () {
      const url = `/project/${this.$route.params.project}/gis/layer`;
      const userLayers = await this.api([url]) || [];

      this.removeUserLayers();
      this.addUserLayers(userLayers);
    },

    ...mapActions(["api", "showSnack"])

  },

  mounted () {
    map = L.map('map', {maxZoom: 22});

    const eventsOptions = () => {
      return {
        start: false,
        container: L.markerClusterGroup({maxClusterRadius: 1, className: "cluster"}),
        getFeatureId (feature) {
          return feature.properties.id;
        },
        pointToLayer (point, latlng) {
          return L.circleMarker(latlng, {
            radius: 6,
            color: point.properties.colour || "gray",
            stroke: false,
            fillOpacity: 0.6
          });
        },
        onEachFeature (feature, layer) {
          const p = feature.properties;
          const popup = (p.sequence
            ? `Event @ ${p.tstamp}<br/>Sequence ${p.sequence}<br/>Point <b>${p.line} / ${p.point}</b><br/><hr/>${markdownInline(p.remarks)}`
            : `Event @ ${p.tstamp}<br/><hr/>${markdownInline(p.remarks)}`)
            + (p.labels.length? `<br/>[<i>${p.labels.join(", ")}</i>]` : "");
          layer.bindTooltip(popup, {sticky: true});
        }
      }
    };

    this.getLabelDefinitions(); // No await

    layers["Events (QC)"] = L.realtime(this.getEvents(i => i.properties.meta?.qc_id), eventsOptions());
    layers["Events (Other)"] = L.realtime(this.getEvents(i => !i.properties.meta?.qc_id), eventsOptions());

    layers["Events (Other)"].on('update', function (e) {
      //console.log("Events (Other) update event", e);
    });

    layers["Events (QC)"].on('add', function (e) {
      //console.log("Events (QC) add event", e);
      e.target._src(data => e.target.update(data), err => console.error)
    });

    layers["Events (QC)"].on('remove', function (e) {
      //console.log("Events (QC) remove event", e);
    });

    layers["Events (Other)"].on('add', function (e) {
      //console.log("Events (Other) add event", e);
      e.target._src(data => e.target.update(data), err => console.error)
    });

    layers["Events (Other)"].on('remove', function (e) {
      //console.log("Events (Other) remove event", e);
    });


    const init = this.decodeURL();

    if (init.activeOverlays) {
      init.activeOverlays.forEach(o => tileMaps[o].addTo(map));
    } else {
      tileMaps["No background"].addTo(map);
    }

    if (init.activeLayers) {
      init.activeLayers.forEach(l => layers[l]?.addTo(map));
    } else {
      layers.OpenSeaMap.addTo(map);
      layers.Preplots.addTo(map);
    }

    const layerControl = L.control.layers(tileMaps, layers).addTo(map);
    const scaleControl = L.control.scale().addTo(map);

    this.references.layerControl = layerControl;
    this.references.scaleControl = scaleControl;

    if (init.position) {
      map.setView(init.position.slice(1), init.position[0]);
    } else {
      map.setView([0, 0], 3);
    }

    let moveStart = map.getBounds().pad(0.3);
    let zoomStart = map.getZoom();

    map.on('movestart', () => {
      moveStart = map.getBounds().pad(0.3);
      zoomStart = map.getZoom();
    });

    map.on('moveend', () => {
      if (!moveStart.contains(map.getBounds()) || map.getZoom() != zoomStart) {
        this.refreshLayers();
      }

      this.updateURL();
    });

    map.on('overlayadd', this.updateURL);
    map.on('overlayremove', this.updateURL);
    map.on('layeradd', this.updateURL);
    map.on('layerremove', this.updateURL);


    this.layerRefreshConfig.forEach( l => {
      l.layer.on('add', ({target}) => this.refreshLayers([target]));
    });

    this.fetchUserLayers();

    if (init.position) {
      this.refreshLayers();
    } else {
      setTimeout(() => {
        if(!this.decodeURL().position) {
          this.fitProjectBounds();
        }
      }, 1000);
    }

// /usr/share/icons/breeze/actions/16/zoom-fit-best.svg
    const fitProjectBounds = this.fitProjectBounds;
    const FitToBoundsControl = L.Control.extend({
        onAdd (map) {
            const widget = L.DomUtil.create('div');
            widget.className = "leaflet-touch leaflet-bar leaflet-control";
            //widget.appendChild(document.getElementById("zoom-fit-best-icon"));
            widget.innerHTML = `<a href="#"><img src="${zoomFitIcon}""></a>`;
            widget.setAttribute("title", "Fit to bounds");
            widget.addEventListener("click", fitProjectBounds);
            return widget;
        },

        onRemove (map) {
            this.removeEventListener("click", fitProjectBounds);
        }
    });

    function fitToBoundsControl (opts) {
        return new FitToBoundsControl(opts);
    }

    fitToBoundsControl({position: "topleft"}).addTo(map);

    const LoadingControl = L.Control.extend({
      onAdd (map) {
        const widget = L.DomUtil.create('div');
        widget.className = "leaflet-touch leaflet-bar leaflet-control d-none";
        //widget.appendChild(document.getElementById("zoom-fit-best-icon"));
        widget.innerHTML = `Loading…`;
        widget.setAttribute("id", "loadingControl");
        return widget;
      },

      onRemove (map) {
      }
    });

    (new LoadingControl({position: "bottomright"})).addTo(map);

    // Decode a position if one given in the hash
    this.setHashMarker();
  }

}

</script>
