<template>
    <div id="map-container">
      <v-overlay :value="error">
        <v-alert type="error">{{ error }}</v-alert>
      </v-overlay>
      <div id="map">
      </div>
      <div> <!-- This div holds the map overlays -->
        <div class="map-overlay top left expand-on-hover">
          <div class="map-overlay-icon">
            <v-icon>mdi-menu</v-icon>
          </div>
          <div class="map-overlay-inner">
            <h3>Background</h3>
            <form>
              <input id="lyr-osm" type="checkbox" value="osm" v-model="layerSelection"/>
              <label for="lyr-osm">OpenStreetMap</label>

              <input id="lyr-sea" type="checkbox" value="sea" v-model="layerSelection"/>
              <label for="lyr-sea">OpenSeaMap</label>

              <input id="lyr-nau" type="checkbox" value="nau" v-model="layerSelection" disabled/>
              <label for="lyr-nau" title="Scan of Norway's nautical charts – CURRENTLY UNAVAILABLE" disabled>Nautical charts (NO)</label>
            </form>

            <hr class="my-2"/>

            <h3>Layers</h3>

            <h4>Baseline</h4>

            <div class="lines-points">

              <template v-if="baseline">
                <span :title="baseline.name">
                  <v-icon small right :color="stringToRGB(baseline.pid)">mdi-solid</v-icon>
                  {{baseline.pid}}
                </span>
                <label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" :value="`${baseline.pid}-seqfp`" v-model="layerSelection"/></label>
                <label title="Show lines"><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" :value="`${baseline.pid}-seqfl`" v-model="layerSelection"/></label>
                <label><!-- No heatmap available --></label>
              </template>

            </div>

            <h4>Monitor data</h4>

            <div class="lines-points">

              <template v-if="monitor">
                <span :title="monitor.name">
                  <v-icon small right :color="stringToRGB(monitor.pid)">mdi-solid</v-icon>
                  {{monitor.pid}}
                </span>
                <label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" :value="`${monitor.pid}-seqfp`" v-model="layerSelection"/></label>
                <label title="Show lines"><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" :value="`${monitor.pid}-seqfl`" v-model="layerSelection"/></label>
                <label><!-- No heatmap available --></label>
              </template>
              <template v-else-if="monitors && monitors.length">

                <template v-if="monitors.length > 2">
                  <span title="Show / hide all monitor data">
                    <v-icon small right color="#00000000">mdi-solid</v-icon>
                    <i>All data</i>
                  </span>
                  <label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" :checked="allMonitorPoints" @click="toggleAllMonitorPoints"/></label>
                  <label title="Show lines"><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" :checked="allMonitorLines" @click="toggleAllMonitorLines"/></label>
                  <label><!-- No heatmap available --></label>
                </template>

                <template v-for="monitor of monitors">
                  <span :title="monitor.name">
                    <v-icon small right :color="stringToRGB(monitor.pid)">mdi-solid</v-icon>
                    {{monitor.pid}}
                  </span>
                  <label title="Show points"><v-icon small left class="mx-0">mdi-vector-point</v-icon> <input type="checkbox" :value="`${monitor.pid}-seqfp`" v-model="layerSelection"/></label>
                  <label title="Show lines"><v-icon small left class="mx-0">mdi-vector-line</v-icon> <input type="checkbox" :value="`${monitor.pid}-seqfl`" v-model="layerSelection"/></label>
                  <label><!-- No heatmap available --></label>
                </template>
              </template>

            </div>

            <div class="lines-points">

              <template v-if="crosshairsPositions.length">
              <span>Markers</span>
              <label title="Show crosshair markers"><v-icon small left class="mx-0">mdi-crosshairs</v-icon> <input type="checkbox" value="crosshairs" v-model="layerSelection"/></label>
              <label title="Show marker labels" v-if="crosshairsLabels.length"><v-icon small left class="mx-0">mdi-format-text-variant</v-icon> <input type="checkbox" value="labels" v-model="layerSelection"/></label>
              <label v-else><!-- No labels provided --></label>
              <label><!-- No heatmap available --></label>
              </template>

            </div>

          </div>
        </div>

        <div class="map-overlay top right">
          <div>
            <v-icon
              class="my-1 mt-4"
              title="Fit view"
              @click="zoomReset"
              >mdi-magnify-scan</v-icon>
          </div>
          <div>
            <v-icon
              class="my-1"
              title="Zoom in"
              @click="zoomIn"
              >mdi-magnify-plus-outline</v-icon>
          </div>
          <div>
            <v-icon
              class="my-1"
              title="Zoom out"
              @click="zoomOut"
              >mdi-magnify-minus-outline</v-icon>
          </div>
          <div>
            <v-icon
              class="my-1"
              :title="isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'"
              @click="toggleFullscreen"
            >{{ isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen' }}</v-icon>
          </div>
        </div>

        <div class="map-overlay bottom right">
          <!-- TODO Implement search -->
          <div>
            <!--
            <input v-show="searchVisible"
              type="text"
              class="mr-2"
              placeholder="Search for a point or event"
              v-model="searchText"
            />
            <v-icon
              class="my-1"
              title="Find element"
              @click="searchVisible = !searchVisible"
              >mdi-crosshairs</v-icon>
            -->
          </div>
          <!-- TODO Implement filtering -->
          <div>
            <!--
            <input v-show="filterVisible"
              type="text"
              class="mr-2"
              placeholder="Filter by sequence or line"
              v-model="filterText"
            />
            <v-icon
              class="my-1"
              title="Filter items"
              @click="filterVisible = !filterVisible"
              >mdi-filter-outline</v-icon>
            -->
          </div>
          <div>
            <v-progress-circular v-if="loading || loadingProgress !== null"
              size="42"
              color="primary"
              title="Loading data…"
              :indeterminate="loadingProgress === null"
              :value="loadingProgress"
            >{{ Math.round(loadingProgress) }}%</v-progress-circular>
            <div v-else>
              <!--
              <v-icon
                class="my-1"
                title="Manual refresh"
                @click="refresh"
                >mdi-cloud-refresh-variant-outline</v-icon>
              -->
            </div>
          </div>
        </div>

        <div class="map-overlay bottom left">
          <div>
            <v-btn color="primary" title="Switch to data view" @click="$emit('input', false)">Back to data</v-btn>
          </div>
        </div>

      </div>
    </div>
</template>

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

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

.theme--dark .map-overlay-inner, .theme--dark .map-overlay-icon {
  background-color: #222;
}

.map-overlay {
  font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
  position: absolute;
  z-index: 1;
}

.top {
  top: 8px;
}

.bottom {
  bottom: 8px;
}

.left {
  left: 8px;
  max-width: 20%;
}

.right {
  right: 8px;
  max-width: 20%;
  text-align: right;
}

.map-overlay-icon {
  background-color: white;
  border: 1px solid black;
  border-radius: 3px;
}

.map-overlay-inner {
  padding: 10px;
  background-color: white;
  border: 1px solid black;
  border-radius: 3px;
}

.map-overlay-inner form {
  display: grid;
  grid-template-columns: 36px 1fr;
}

.map-overlay-inner .lines-points {
  display: grid;
  grid-template-columns: minmax(auto, 1fr) 36px 36px 36px;
  grid-column-gap: 4px;
}

.map-overlay-inner .lines-points :nth-child(4n+1) {
  margin-right: 6px;
}

.map-overlay input[type=text] {
  background-color: white;
  border: 1px solid black;
  border-radius: 4px;
}

.expand-on-hover:hover .map-overlay-icon {
  display: none;
}

.expand-on-hover:not(:hover) .map-overlay-inner {
  display: none;
}

.expand-on-hover:hover .map-overlay-inner {
  display: block;
}

/* BEGIN
 * This is meant to be a possible solution for keeping
 * the layers menu open while selecting from a v-select
 * in the context of displaying QC values. There may be
 * better solutions.
 */

.expand-on-hover:focus-within .map-overlay-icon {
  display: none;
}

.expand-on-hover:focus-within .map-overlay-inner {
  display: block;
}
/*
 * END
 */

</style>

<script>

import MapLayersMixin from '@/views/MapLayersMixin';
import { mapActions, mapGetters, mapState } from 'vuex';
import zoomFitIcon from '@/assets/zoom-fit-best.svg'

import { Deck, FlyToInterpolator } from '@deck.gl/core';
import { IconLayer, TextLayer, GeoJsonLayer, PathLayer } from '@deck.gl/layers';
import { DougalBinaryBundle, DougalBinaryChunkSequential, DougalBinaryChunkInterleaved } from '@dougal/binary';
import { DougalSequenceLayer } from '@/lib/deck.gl';
import DougalBinaryLoader from '@/lib/deck.gl/DougalBinaryLoader';
import { getLayerById, getAllPickingInfo } from '@/lib/deck.gl/getLayerPickingInfo';


let deck;

/* Decoded SVG:
 * <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path style="fill:#ff0000;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 hashMarkerIconURL = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KPHBhdGggc3R5bGU9ImZpbGw6I2ZmMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgZD0iTSA3IDMgTCA3IDQuMDMxMjUgQSA0LjUgNC41IDAgMCAwIDMuMDMzMjAzMSA4IEwgMiA4IEwgMiA5IEwgMy4wMzEyNSA5IEEgNC41IDQuNSAwIDAgMCA3IDEyLjk2Njc5NyBMIDcgMTQgTCA4IDE0IEwgOCAxMi45Njg3NSBBIDQuNSA0LjUgMCAwIDAgMTEuOTY2Nzk3IDkgTCAxMyA5IEwgMTMgOCBMIDExLjk2ODc1IDggQSA0LjUgNC41IDAgMCAwIDggNC4wMzMyMDMxIEwgOCAzIEwgNyAzIHogTSA3IDUuMDM5MDYyNSBMIDcgOCBMIDQuMDQxMDE1NiA4IEEgMy41IDMuNSAwIDAgMSA3IDUuMDM5MDYyNSB6IE0gOCA1LjA0MTAxNTYgQSAzLjUgMy41IDAgMCAxIDEwLjk2MDkzOCA4IEwgOCA4IEwgOCA1LjA0MTAxNTYgeiBNIDQuMDM5MDYyNSA5IEwgNyA5IEwgNyAxMS45NTg5ODQgQSAzLjUgMy41IDAgMCAxIDQuMDM5MDYyNSA5IHogTSA4IDkgTCAxMC45NTg5ODQgOSBBIDMuNSAzLjUgMCAwIDEgOCAxMS45NjA5MzggTCA4IDkgeiAiIC8+Cjwvc3ZnPgo=";


function stringToRGB(input, o = 255) {
  const str = String(input);
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash |= 0; // Convert to 32-bit integer
  }
  const r = (hash >> 16) & 255;
  const g = (hash >> 8) & 255;
  const b = hash & 255;
  return [r, g, b, (o%256)];
}

export default {
  name: "DougalGroupMap",

  mixins: [
    MapLayersMixin
  ],

  components: {

  },

  props: {
    baseline: { type: Object, required: true },
    monitor: { type: Object, required: false },
    comparison: { type: Object, required: false },
    monitors: { type: Array, required: false },
    comparisons: { type: Array, required: false },
  },

  data () {
    return {
      layerSelection: [],
      layersAvailable: {},
      layersPendingLoad: [],
      sequenceDataTStamp: null,
      loadingProgress: null,
      viewState: {},
      viewStateDefaults: {
        //maxZoom: 18,
        maxPitch: 89
      },

      heatmapValue: "total_error",
      isFullscreen: false,
      crosshairsPositions: [],
      crosshairsLabels: [],

      searchText: "",
      searchVisible: false,
      filterText: "",
      filterVisible: false,

      error: null,
    };
  },

  computed: {

    lineTStamp () {
      return this.$store.state.line.timestamp;
    },

    sequenceTStamp () {
      return this.$store.state.sequence.timestamp;
    },

    sequenceData () {
      return this.sequenceDataElements?.map( el => el.data );
    },

    allMonitorPoints () {
      return this.monitors?.every( ({pid}) => this.layerSelection.includes(`${pid}-seqfp`) );
    },

    allMonitorLines () {
      return this.monitors?.every( ({pid}) => this.layerSelection.includes(`${pid}-seqfl`) );
    },

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

  watch: {

    baseline () {
      this.populateDataLayersAvailable();
    },

    monitor () {
      this.populateDataLayersAvailable();
    },

    monitors () {
      this.populateDataLayersAvailable();
    },

    layerSelection (newVal, oldVal) {
      this.populateDataLayersAvailable();
      this.render();
    },

    layersPendingLoad (nv, ov) {
      const total = this.layerSelection.length;
      const pending = this.layerSelection.map( l => this.layersPendingLoad.includes(l) ? 1 : 0 ).filter( i => i ).length;
      console.log("total = ", total, "pending = ", pending);
      if (total && pending) {
        this.loadingProgress = Math.min(100, Math.max(0, 100 - (pending / total * 100)))
      } else {
        this.loadingProgress = null;
      }
      console.log("loading progress = ", this.loadingProgress);
    },

    loadingProgress (newValue, oldValue) {
      if (oldValue && newValue === null) {
        console.log("Zooming to extents");
        this.zoomReset();
      }
    },

    viewState: {
      handler () {
        this.setViewState();
      },
      deep: true
    },

  },

  methods: {
    initView () {
    },

    async render () {
      if (deck) {
        const layers = [];
        for (const name in this.layersAvailable) {
          //console.log("Visible", name, this.layerSelection.includes(name));
          const fn = this.layersAvailable[name];

          if (typeof fn != 'function') {
            throw new Error(`Layer ${name}: expected a function, got ${typeof fn}`);
          }

          layers.push(await fn({
            visible: this.layerSelection.includes(name)
          }));
        }

        //console.log("setProps", this.sequenceDataTStamp, deck.layerManager && deck.layerManager.getLayers().length);

        //deck.setProps({layers, initialViewState: this.viewState});
        deck.setProps({layers});
      }
    },

    zoomReset() {
      if (deck) {
        const bounds = this.getBBox(deck);
        if (bounds) {
          const viewport = deck.getViewports()[0];
          const newViewState = {
            ...this.viewStateDefaults,
            ...viewport.fitBounds(bounds)
          };
          newViewState.transitionDuration = 500;
          newViewState.transitionInterpolator = new FlyToInterpolator();
          deck.setProps({ initialViewState: newViewState });
        } else {
          console.warn('Unable to calculate bounding box');
        }
      } else {
        console.warn('Deck instance not available');
      }
    },

    zoomIn () {
      if (deck) {
        const viewState = deck.getViewports()[0];
        const initialViewState = {...this.viewStateDefaults, ...viewState};
        initialViewState.zoom += 1;
        initialViewState.transitionDuration = 300;
        deck.setProps({initialViewState});
      }
    },

    zoomOut () {
      if (deck) {
        const viewState = deck.getViewports()[0];
        const initialViewState = {...this.viewStateDefaults, ...viewState};
        initialViewState.zoom -= 1;
        initialViewState.transitionDuration = 300;
        deck.setProps({initialViewState});
      }
    },

    toggleFullscreen() {
      const mapElement = document.getElementById('map-container');
      if (!this.isFullscreen) {
        if (mapElement.requestFullscreen) {
          mapElement.requestFullscreen();
        } else if (mapElement.webkitRequestFullscreen) { // Safari
          mapElement.webkitRequestFullscreen();
        } else if (mapElement.msRequestFullscreen) { // IE11
          mapElement.msRequestFullscreen();
        }
        this.isFullscreen = true;
      } else {
        if (document.exitFullscreen) {
          document.exitFullscreen();
        } else if (document.webkitExitFullscreen) {
          document.webkitExitFullscreen();
        } else if (document.msExitFullscreen) {
          document.msExitFullscreen();
        }
        this.isFullscreen = false;
      }
    },

    getLayerById (layerId) {
      return getLayerById(deck, layerId);
    },

    async refresh () {
      if (window.caches) {
        const cache = await caches.open("dougal");
        for (const key of await cache.keys()) {
          // Match only resolved requests
          if (key instanceof Request) {
            if (key.url?.match(`/project/${this.$route.params.project}`)) {
              console.log("removing", key.method, key.url);
              cache.delete(key);
            }
          }
        }
      }
      await this.$root.sleep(300);
      this.sequenceDataElements = [];
      this.getSequenceData();
    },

    getBBox(deck, margin = 0.1) {
      if (!deck || !deck.layerManager) {
        console.warn('Invalid deck instance or layer manager');
        return null;
      }

      let λ0 = +Infinity, φ0 = +Infinity, λ1 = -Infinity, φ1 = -Infinity;

      const nonTileLayerTypes = ['ScatterplotLayer', 'GeoJsonLayer', 'LineLayer', 'PathLayer', 'IconLayer', 'DougalSequenceLayer'];

      deck.layerManager.layers.forEach(layer => {
        // Skip tile-based layers
        if (['TileLayer', 'MVTLayer'].includes(layer.constructor.layerName)) {
          return;
        }

        // Only process visible layers
        if (!layer.props.visible || !nonTileLayerTypes.includes(layer.constructor.layerName)) {
          return;
        }

        let data = layer.props.data;

        // If the layer has a getBounds() method, use that
        if (typeof layer.getBounds === 'function') {
          const bounds = layer.getBounds();
          if (bounds?.length) {
            bounds.forEach( ([lon, lat]) => {
              if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
                λ0 = Math.min(λ0, lon);
                φ0 = Math.min(φ0, lat);
                λ1 = Math.max(λ1, lon);
                φ1 = Math.max(φ1, lat);

                return; // Continue with the next layer
              }
            })
          }
          // If for whatever reason getBounds() has failed, we try other methods
        }

        // Handle DougalSequenceLayer
        if (layer.constructor.layerName === 'DougalSequenceLayer') {
          const positions = layer?.props?.data?.attributes?.getPosition?.value;
          if (positions && positions.length) {
            for (let i = 0; i < positions.length; i += 2) {
              const lon = positions[i];
              const lat = positions[i + 1];
              if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
                λ0 = Math.min(λ0, lon);
                φ0 = Math.min(φ0, lat);
                λ1 = Math.max(λ1, lon);
                φ1 = Math.max(φ1, lat);
              }
            }
          }
        } else if (layer.constructor.layerName === 'ScatterplotLayer') {
          // Direct point data (e.g., navp)
          if (Array.isArray(data)) {
            data.forEach(d => {
              const [lon, lat] = layer.props.getPosition(d);
              if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
                λ0 = Math.min(λ0, lon);
                φ0 = Math.min(φ0, lat);
                λ1 = Math.max(λ1, lon);
                φ1 = Math.max(φ1, lat);
              }
            });
          } else if (data && data.attributes && data.attributes.getPosition) {
            // Binary data
            const positions = data.attributes.getPosition.value;
            for (let i = 0; i < data.length * 2; i += 2) {
              const lon = positions[i];
              const lat = positions[i + 1];
              if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
                λ0 = Math.min(λ0, lon);
                φ0 = Math.min(φ0, lat);
                λ1 = Math.max(λ1, lon);
                φ1 = Math.max(φ1, lat);
              }
            }
          }
        } else if (layer.constructor.layerName === 'GeoJsonLayer') {
          // Extract points from GeoJSON features
          let featureCollections = Array.isArray(data) ? data : [data];
          featureCollections.forEach(item => {
            let features = item;
            if (item?.type === 'FeatureCollection') {
              features = item.features || [];
            }
            if (Array.isArray(features)) {
              features.forEach(feature => {
                if (feature.geometry?.type === 'Point') {
                  const [lon, lat] = feature.geometry.coordinates;
                  if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
                    λ0 = Math.min(λ0, lon);
                    φ0 = Math.min(φ0, lat);
                    λ1 = Math.max(λ1, lon);
                    φ1 = Math.max(φ1, lat);
                  }
                } else if (feature.geometry?.type === 'LineString') {
                  feature.geometry.coordinates.forEach(([lon, lat]) => {
                    if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
                      λ0 = Math.min(λ0, lon);
                      φ0 = Math.min(φ0, lat);
                      λ1 = Math.max(λ1, lon);
                      φ1 = Math.max(φ1, lat);
                    }
                  });
                } else if (feature.geometry?.type === 'Polygon') {
                  feature.geometry.coordinates[0].forEach(([lon, lat]) => {
                    if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
                      λ0 = Math.min(λ0, lon);
                      φ0 = Math.min(φ0, lat);
                      λ1 = Math.max(λ1, lon);
                      φ1 = Math.max(φ1, lat);
                    }
                  });
                }
              });
            }
          });
        } else if (layer.constructor.layerName === 'LineLayer') {
          // Extract source and target positions
          if (Array.isArray(data)) {
            data.forEach(d => {
              const source = layer.props.getSourcePosition(d);
              const target = layer.props.getTargetPosition(d);
              if (source && source[0] != null && source[1] != null && isFinite(source[0]) && isFinite(source[1])) {
                λ0 = Math.min(λ0, source[0]);
                φ0 = Math.min(φ0, source[1]);
                λ1 = Math.max(λ1, source[0]);
                φ1 = Math.max(φ1, source[1]);
              }
              if (target && target[0] != null && target[1] != null && isFinite(target[0]) && isFinite(target[1])) {
                λ0 = Math.min(λ0, target[0]);
                φ0 = Math.min(φ0, target[1]);
                λ1 = Math.max(λ1, target[0]);
                φ1 = Math.max(φ1, target[1]);
              }
            });
          }
        } else if (layer.constructor.layerName === 'IconLayer') {
          // Single point (e.g., crosshairs)
          if (Array.isArray(data)) {
            data.forEach(d => {
              const [lon, lat] = layer.props.getPosition(d);
              if (lon != null && lat != null && isFinite(lon) && isFinite(lat)) {
                λ0 = Math.min(λ0, lon);
                φ0 = Math.min(φ0, lat);
                λ1 = Math.max(λ1, lon);
                φ1 = Math.max(φ1, lat);
              }
            });
          }
        }
      });

      // Check if any valid points were found
      if (!isFinite(λ0) || !isFinite(φ0) || !isFinite(λ1) || !isFinite(φ1)) {
        console.warn('No valid coordinates found for non-tile layers');
        return null;
      }

      // Apply margin
      const δλ = λ1 - λ0;
      const δφ = φ1 - φ0;
      const mλ = δλ * margin;
      const mφ = δφ * margin;

      return [[λ0 - mλ, φ0 - mφ], [λ1 + mλ, φ1 + mφ]];
    },

    // Returns the current second, as an integer.
    // Used for triggering Deck.gl URL refreshes
    currentSecond () {
      return Math.floor(Date.now()/1000);
    },



    async initLayers (gl) {
      deck.onViewStateChange = this.updateURL;
    },

    setViewState () {
      const viewport = deck.getViewports()[0];
      if (deck && viewport && this.viewState) {
        const initialViewState = {
          ...this.initialViewState,
          ...viewport,
          ...this.viewState
        };
        deck.setProps({initialViewState});
      }
    },

    checkWebGLSupport() {
      const canvas = document.createElement('canvas');
      const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
      if (!gl) {
        //this.error = 'WebGL is not supported in this browser or device.';
        //this.loading = false;
        console.error('WebGL not supported');
        return false;
      }
      const isWebGL2 = gl instanceof WebGL2RenderingContext;
      console.log('WebGL Support:', isWebGL2 ? 'WebGL2' : 'WebGL1');
      return true;
    },

    loadOptions (options = {}) {
      return {
        loadOptions: {
          fetch: {
            method: 'GET',
            headers: {
              'Authorization': `Bearer ${this.$store.getters.jwt}`,
            }
          },
          ...options
        },
      };

    },

    preplotLinesLayer (options = {}) {
      return new GeoJsonLayer({
        id: 'ppll',
        data: `/api/project/${this.baseline.pid}/gis/preplot/line?v=${Date.now()}`,
        ...this.loadOptions(),
        lineWidthMinPixels: 1,
        getLineColor: (d) => d.properties.ntba ? [240, 248, 255, 200] : [85, 170, 255, 200],
        getLineWidth: 1,
        getPointRadius: 2,
        radiusUnits: "pixels",
        pointRadiusMinPixels: 2,
        pickable: true,
        ...options
      })
    },


    preplotPointsLayer (options = {}) {
      return new DougalSequenceLayer({
        id: 'pplp',
        data: `/api/project/${this.baseline.pid}/line/source?v=${Date.now()}`, // API endpoint returning binary data
        loaders: [DougalBinaryLoader],
        ...this.loadOptions({
          fetch: {
            method: 'GET',
            headers: {
              Authorization: `Bearer ${this.$store.getters.jwt}`,
              Accept: 'application/vnd.aaltronav.dougal+octet-stream'
            }
          }
        }),
        getRadius: 2,
        getFillColor: (d, {data, index}) => data.attributes.value2.value[index] ? [240, 248, 255, 200] : [85, 170, 255, 200],
        //getFillColor: [0, 120, 220, 200],
        pickable: true,
        ...options
      });
    },

    getMonitorLayer (monitor, type = "points") {

      const colour = stringToRGB(monitor.pid, 150);

      if (type == "lines") {

        return (options = {}) => {
          return new PathLayer({
            id: `${monitor.pid}-seqfl`,
            data: `/api/project/${monitor.pid}/sequence?type=4`, // API endpoint returning binary data
            ...this.loadOptions({
              fetch: {
                method: 'GET',
                headers: {
                  Authorization: `Bearer ${this.$store.getters.jwt}`,
                  Accept: 'application/vnd.aaltronav.dougal+octet-stream'
                }
              }
            }),
            loaders: [DougalBinaryLoader],
            dataTransform: (data) => {
              //console.log("Incoming data:", data);

              // Validate input
              if (!data || !data.attributes) {
                console.error("Invalid loaded data: missing attributes");
                return [];
              }

              const lines = data.attributes.value0?.value; // Uint16Array (line numbers)
              const sequences = data.attributes.value2?.value; // Uint16Array (sequence numbers)
              const positions = data.attributes.getPosition?.value; // Float32Array (lon, lat pairs)

              // Check for missing attributes
              if (!lines || !sequences || !positions) {
                console.error("Missing required attributes:", {
                  lines: !!lines,
                  sequences: !!sequences,
                  positions: !!positions,
                });
                return [];
              }

              // Validate array lengths
              const numPoints = lines.length;
              if (numPoints !== sequences.length || numPoints !== positions.length / 2) {
                console.error("Mismatched attribute lengths:", {
                  lines: numPoints,
                  sequences: sequences.length,
                  positions: positions.length / 2,
                });
                return [];
              }

              // Group points by sequence and line, preserving point order
              const tuples = {};
              for (let n = 0; n < numPoints; n++) {
                const line = lines[n];
                const sequence = sequences[n];
                const λ = positions[n * 2]; // Longitude
                const φ = positions[n * 2 + 1]; // Latitude
                const point = data.attributes.value1?.value?.[n] || n; // Fallback to index if point (j) unavailable

                // Validate coordinates
                if (typeof λ !== 'number' || typeof φ !== 'number' || isNaN(λ) || isNaN(φ)) {
                  console.warn(`Skipping invalid position at index ${n}: [${λ}, ${φ}]`);
                  continue;
                }

                const key = `${sequence}-${line}`; // Unique key for sequence and line
                if (!tuples[key]) {
                  tuples[key] = { sequence, line, path: [], points: [] };
                }
                tuples[key].path.push({ point, coords: [λ, φ] });
              }

              // Sort paths by point and filter single-point paths
              const result = [];
              for (const key in tuples) {
                const item = tuples[key];
                // Sort by point to ensure correct path order
                item.path.sort((a, b) => a.point - b.point);
                item.path = item.path.map(p => p.coords); // Extract sorted coordinates

                if (item.path.length >= 2) {
                  result.push({
                    sequence: item.sequence,
                    line: item.line,
                    path: item.path,
                    pid: monitor.pid,
                    name: monitor.name
                  });
                } else {
                  console.log(`Discarding single-point path: sequence=${item.sequence}, line=${item.line}, points=${item.path.length}`);
                }
              }

              //console.log("Outgoing data:", result);
              return result;
            },
            onDataLoad: (value, {layer}) => {
              console.log("Loading finished", layer.id);
              const index = this.layersPendingLoad.indexOf(layer.id);
              if (index != -1)
                this.layersPendingLoad.splice(index, 1);
            },
            getPath: (d) => d.path,
            getWidth: 2,
            widthUnits: "pixels",
            widthMinPixels: 1,
            getColor: colour,
            //getFillColor: [0, 120, 220, 200],
            pickable: true,
            ...options
          });
        }


      } else if (type == "points") {

        return (options = {}) => {
          return new DougalSequenceLayer({
            id: `${monitor.pid}-seqfp`,
            data: `/api/project/${monitor.pid}/sequence?type=4`, // API endpoint returning binary data
            loaders: [DougalBinaryLoader],
            ...this.loadOptions({
              fetch: {
                method: 'GET',
                headers: {
                  Authorization: `Bearer ${this.$store.getters.jwt}`,
                  Accept: 'application/vnd.aaltronav.dougal+octet-stream'
                }
              }
            }),
            dataTransform: (data) => {
              data.pid = monitor.pid;
              data.name = monitor.name;
              return data;
            },
            onDataLoad: (value, {layer}) => {
              console.log("Loading finished", layer.id);
              const index = this.layersPendingLoad.indexOf(layer.id);
              if (index != -1)
                this.layersPendingLoad.splice(index, 1);
            },
            getRadius: 3,
            getFillColor: colour,
            //getFillColor: [0, 120, 220, 200],
            pickable: true,
            ...options
          });
        }

      }

    },

    populateDataLayersAvailable () {
      Object.keys(this.layersAvailable)
        .filter( key => [ "seqfl", "seqfp" ].some( id => key.includes(id ) ))
        .forEach( key => delete this.layersAvailable[key] );

      if (this.baseline) {
        const pid = this.baseline.pid;
        this.layersAvailable[`${pid}-seqfp`] = this.getMonitorLayer(this.baseline, "points");
        this.layersAvailable[`${pid}-seqfl`] = this.getMonitorLayer(this.baseline, "lines");
      }

      if (this.monitor) {
        const pid = this.monitor.pid;
        this.layersAvailable[`${pid}-seqfp`] = this.getMonitorLayer(this.monitor, "points");
        this.layersAvailable[`${pid}-seqfl`] = this.getMonitorLayer(this.monitor, "lines");
      } else if (this.monitors) {
        for (const monitor of this.monitors) {
          const pid = monitor.pid;
          this.layersAvailable[`${pid}-seqfp`] = this.getMonitorLayer(monitor, "points");
          this.layersAvailable[`${pid}-seqfl`] = this.getMonitorLayer(monitor, "lines");
        }
      }
    },

    getTooltip (args) {
      const style = { "max-width": "50ex"};
      let html = "";

      if (args?.layer?.constructor?.tooltip) {
        return args.layer.constructor.tooltip(args);
      } else if (args?.layer?.id.includes("seqrp")) {
        // Baseline points (NOTE: not used!)

        const line = args.layer.props.data.attributes.value0.value[args.index];
        const point = args.layer.props.data.attributes.value1.value[args.index];
        const sequence = args.layer.props.data.attributes.value2.value[args.index];

        html += `Baseline ${args.layer.props.data.pid} – ${args.layer.props.data.name}<br/>\n`;
        html += `L${line} S${sequence} P${point}`;

        return {html, style};

      } else if (args?.layer?.id.includes("seqrl")) {
        // Baseline lines (NOTE: not used!)

        if (!args.object) {
          console.warn("No 'object' in", args);
          return;
        }

        const { pid, name, sequence, line } = args?.object;

        html += `Baseline ${pid} – ${name}<br/>\n`;
        html += `L${line} S${sequence}`;

        return {html, style};

      } else if (args?.layer?.id.includes("seqfp")) {
        // Monitor points

        const pid = args.layer.props.data.pid;
        const name = args.layer.props.data.name;
        const line = args.layer.props.data.attributes.value0.value[args.index];
        const point = args.layer.props.data.attributes.value1.value[args.index];
        const sequence = args.layer.props.data.attributes.value2.value[args.index];

        const kind = pid === this.baseline.pid
          ? "Baseline"
          : "Monitor";

        html += `${kind} ${pid} – ${name}<br/>\n`;
        html += `L${line} S${sequence} P${point}`;

        return {html, style};

      } else if (args?.layer?.id.includes("seqfl")) {
        // Monitor lines

        if (!args.object) {
          console.warn("No 'object' in", args);
          return;
        }

        const { pid, name, sequence, line } = args.object;
        const kind = pid === this.baseline.pid
          ? "Baseline"
          : "Monitor";

        html += `${kind} ${pid} – ${name}<br/>\n`;
        html += `L${line} S${sequence}`;

        return {html, style};
      }
    },

    toggleAllMonitorPoints () {
      if (this.allMonitorPoints) {
        this.monitors?.forEach( ({pid}) => {
          delete this.layerSelection[this.layerSelection.indexOf(`${pid}-seqfp`)];
          this.layerSelection = [...this.layerSelection];
        })
      } else {
        this.monitors?.forEach( ({pid}) => {
          this.layerSelection.push(`${pid}-seqfp`);
        })
      }
    },

    toggleAllMonitorLines () {
      if (this.allMonitorPoints) {
        this.monitors?.forEach( ({pid}) => {
          delete this.layerSelection[this.layerSelection.indexOf(`${pid}-seqfl`)];
          this.layerSelection = [...this.layerSelection];
        })
      } else {
        this.monitors?.forEach( ({pid}) => {
          this.layerSelection.push(`${pid}-seqfl`);
        })
      }
    },

    stringToRGB (input, opacity) {
      return "#"+stringToRGB(input, opacity).map( i => i.toString(16) ).join("");
    },

    ...mapActions(["api"])

  },

  async mounted () {

    if (!this.checkWebGLSupport()) {
      for (const countdown = 5; countdown > 0; countdown--) {
        if (countdown) {
          this.error = `This browser does not support WebGL. Switching to legacy map view in ${countdown} seconds.`;
          await new Promise( (resolve) => setTimeout(() => resolve, 1000) );
        }
      }
      this.error = null;
      console.log("TODO: Should switch to legacy map view");
    }

    this.layersAvailable.osm = this.osmLayer;

    this.layersAvailable.sea = this.openSeaMapLayer;

    //this.layersAvailable.ppll = this.preplotLinesLayer;

    //this.layersAvailable.pplp = this.preplotPointsLayer;

    this.layersAvailable.crosshairs = (options = {}) => {
      return new IconLayer({
        id: 'crosshairs',
        data: this.crosshairsPositions,
        getPosition: d => d,
        getSize: 32,
        getColor: [ 255, 0, 0 ],
        getIcon: (d) => {
          return {
            url: hashMarkerIconURL,
            width: 24,
            height: 24
          };
        },
        ...options
      });
    };

    this.layersAvailable.labels = (options = {}) => {
      return new TextLayer({
        id: 'labels',
        data: this.crosshairsPositions,
        getPosition: d => d,
        getSize: 18,
        getColor: [ 255, 0, 0 ],
        getText: (d, {index}) => this.crosshairsLabels[index],
        getPixelOffset: [ 0, -24 ],
        getTextAnchor: "start",
        getAlignmentBase: "bottom",
        ...options
      });
    };

    deck = new Deck({
      parent: document.getElementById("map"),
      mapStyle: 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json',
      initialViewState: {
        ...this.viewStateDefaults,
        latitude: 61.5,
        longitude: 2.5,
        zoom: 7,
        pitch: 0,
      },
      controller: {inertia: true},
      layers: [],
      getTooltip: this.getTooltip,
      pickingRadius: 24,
      onWebGLInitialized: this.initLayers
    });
    //console.log("deck = ", deck);

    // All layers are initially shown

    if (this.baseline) {
      this.layerSelection.push(`${this.baseline.pid}-seqfl`);
      this.layerSelection.push(`${this.baseline.pid}-seqfp`);

      this.layersPendingLoad.push(`${this.baseline.pid}-seqfl`);
      this.layersPendingLoad.push(`${this.baseline.pid}-seqfp`);
    }

    if (this.monitors) {
        this.monitors.forEach( monitor => {
          this.layerSelection.push(`${monitor.pid}-seqfl`);
          this.layerSelection.push(`${monitor.pid}-seqfp`);

          this.layersPendingLoad.push(`${monitor.pid}-seqfl`);
          this.layersPendingLoad.push(`${monitor.pid}-seqfp`);
        })
    }

    // Get fullscreen state

    document.addEventListener('fullscreenchange', () => {
      this.isFullscreen = !!document.fullscreenElement;
    });
    document.addEventListener('webkitfullscreenchange', () => {
      this.isFullscreen = !!document.fullscreenElement;
    });
    document.addEventListener('msfullscreenchange', () => {
      this.isFullscreen = !!document.fullscreenElement;
    });

  },

}

</script>
