<div class="globeContainer">
  <svg id="globe" width="400" height="400"></svg>
  <div class="globeText">
    click and drag
  </div>
</div>

<style>
  .globeContainer {
    width: 100%;
    height: 100%;
    align-items: center;
    justify-content: center;
    background-color: #00111d;
    color: #ffffff;
  }

  :global(.footprint--LEO) {
    fill: rgba(255, 177, 64, 0.08);
    stroke: rgba(255, 177, 64, 0.5);
  }

  .globeText{
    text-align: center;
    font-size: 0.6em;
    color: #aaaaaa;
  }

  #globe {
    display: block;
  }
</style>

<script>
	import * as d3 from "d3";
	import * as sjs from 'satellite.js';
	import { onMount } from 'svelte';
	import tles from "../../static/tle.js"
	import world110m from "../../static/world-110m.js"
	import * as topojson from "topojson";
	
	export let width = 300;
	
	async function doThing() {
		let satelliteJs = sjs;
  var RADIANS = Math.PI / 180;
  var DEGREES = 180 / Math.PI;
  var R_EARTH = 6378.137; // equatorial radius (km)

  /* =============================================== */
  /* =============== CLOCK ========================= */
  /* =============================================== */

  /**
   * Factory function for keeping track of elapsed time and rates.
   */
  class Clock {
    constructor() {
      this._rate = 60; // 1ms elapsed : 60sec simulated
      this._date = d3.now();
      this._elapsed = 0;
    }
    async date(timeInMs) {
      if (!arguments.length) return this._date + this._elapsed * this._rate;
      this._date = timeInMs;
      return this;
    }
    async elapsed(ms) {
      if (!arguments.length) return this._date - d3.now(); // calculates elapsed
      this._elapsed = ms;
      return this;
    }
    async rate(secondsPerMsElapsed) {
      if (!arguments.length) return this._rate;
      this._rate = secondsPerMsElapsed;
      return this;
    }
  }

  /* ==================================================== */
  /* =============== CONVERSION ========================= */
  /* ==================================================== */

  async function satrecToFeature(satrec, date, props) {
    var properties = props || {};
    var positionAndVelocity = satelliteJs.propagate(satrec, date);
    var gmst = satelliteJs.gstime(date);
    var positionGd = satelliteJs.eciToGeodetic(
      positionAndVelocity.position,
      gmst
    );
    properties.height = positionGd.height;
    return {
      type: "Feature",
      properties: properties,
      geometry: {
        type: "Point",
        coordinates: [
          positionGd.longitude * DEGREES,
          positionGd.latitude * DEGREES,
        ],
      },
    };
  }
  /* ==================================================== */
  /* =============== TLE ================================ */
  /* ==================================================== */

  /**
   * Factory function for working with TLE.
   */
  class TLE {
    constructor() {
      this._properties;
      this._date;
    }
    async _lines(arry) {
      return arry.slice(0, 2);
    }
    async satrecs(tles) {
      return tles.map(function (d) {
        return satelliteJs.twoline2satrec.apply(null, this._lines(d));
      });
    }
    async features(tles) {
      var date = this._date || d3.now();

      return tles.map(function (d) {
        var satrec = satelliteJs.twoline2satrec.apply(null, this._lines(d));
        return satrecToFeature(satrec, date, this._properties(d));
      });
    }
    async lines(func) {
      if (!arguments.length) return this._lines;
      this._lines = func;
      return this;
    }
    async properties(func) {
      if (!arguments.length) return this._properties;
      this._properties = func;
      return this;
    }
    async date(ms) {
      if (!arguments.length) return this._date;
      this._date = ms;
      return this;
    }
  }

  /* ==================================================== */
  /* =============== PARSE ============================== */
  /* ==================================================== */

  /**
   * Parses text file string of tle into groups.
   * @return {string[][]} Like [['tle line 1', 'tle line 2'], ...]
   */
  async function parseTle(tleString) {
    // remove last newline so that we can properly split all the lines
    var lines = tleString.replace(/\r?\n$/g, "").split(/\r?\n/);

    return lines.reduce(function (acc, cur, index) {
      if (index % 2 === 0) acc.push([]);
      acc[acc.length - 1].push(cur);
      return acc;
    }, []);
  }

  /* ==================================================== */
  /* =============== SATELLITE ========================== */
  /* ==================================================== */

  /**
   * Satellite factory function that wraps satellitejs functionality
   * and can compute footprints based on TLE and date
   *
   * @param {string[][]} tle two-line element
   * @param {Date} date date to propagate with TLE
   */
  function Satellite(tle, date) {
    this._satrec = satelliteJs.twoline2satrec(tle[0], tle[1]);
    this._satNum = this._satrec.satnum; // NORAD Catalog Number

    this._altitude; // km
    this._position = {
      lat: null,
      lng: null,
    };
    this._halfAngle; // degrees
    this._date;
    this._gmst;

    this.setDate(date);
    this.update();
    this._orbitType = this.orbitTypeFromAlt(this._altitude); // LEO, MEO, or GEO
  }

  /**
   * Updates satellite position and altitude based on current TLE and date
   */
  Satellite.prototype.update = async function () {
    var positionAndVelocity = satelliteJs.propagate(this._satrec, this._date);
    var positionGd = satelliteJs.eciToGeodetic(
      positionAndVelocity.position,
      this._gmst
    );

    this._position = {
      lat: positionGd.latitude * DEGREES,
      lng: positionGd.longitude * DEGREES,
    };
    this._altitude = positionGd.height;
    return this;
  };

  /**
   * @returns {GeoJSON.Polygon} GeoJSON describing the satellite's current footprint on the Earth
   */
  Satellite.prototype.getFootprint = function () {
    var theta = this._halfAngle * RADIANS;

    let coreAngle = this._coreAngle(theta, this._altitude, R_EARTH) * DEGREES;

    return d3
      .geoCircle()
      .center([this._position.lng, this._position.lat])
      .radius(coreAngle)();
  };

  /**
   * A conical satellite with half angle casts a circle on the Earth. Find the angle
   * from the center of the earth to the radius of this circle
   * @param {number} theta: Satellite half angle in radians
   * @param {number} altitude Satellite altitude
   * @param {number} r Earth radius
   * @returns {number} core angle in radians
   */
  Satellite.prototype._coreAngle = function (theta, altitude, r) {
    // if FOV is larger than Earth, assume it goes to the tangential point
    if (Math.sin(theta) > r / (altitude + r)) {
      return Math.acos(r / (r + altitude));
    }
    return Math.abs(Math.asin(((r + altitude) * Math.sin(theta)) / r)) - theta;
  };

  Satellite.prototype.halfAngle = function (halfAngle) {
    if (!arguments.length) return this._halfAngle;
    this._halfAngle = halfAngle;
    return this;
  };

  Satellite.prototype.satNum = function (satNum) {
    if (!arguments.length) return this._satNum;
    this._satNum = satNum;
    return this;
  };

  Satellite.prototype.altitude = function (altitude) {
    if (!arguments.length) return this._altitude;
    this._altitude = altitude;
    return this;
  };

  Satellite.prototype.position = function (position) {
    if (!arguments.length) return this._position;
    this._position = position;
    return this;
  };

  Satellite.prototype.getOrbitType = function () {
    return this._orbitType;
  };

  /**
   * sets both the date and the Greenwich Mean Sidereal Time
   * @param {Date} date
   */
  Satellite.prototype.setDate = function (date) {
    this._date = date;
    this._gmst = satelliteJs.gstime(date);
    return this;
  };

  /**
   * Maps an altitude to a type of satellite
   * @param {number} altitude (in KM)
   * @returns {'LEO' | 'MEO' | 'GEO'}
   */
  Satellite.prototype.orbitTypeFromAlt = function (altitude) {
    this._altitude = altitude || this._altitude;
    return this._altitude < 1200
      ? "LEO"
      : this._altitude > 22000
      ? "GEO"
      : "MEO";
  };

  /* =============================================== */
  /* =================== GLOBE ===================== */
  /* =============================================== */

  // Approximate date the tle data was aquired from https://www.space-track.org/#recent
  var TLE_DATA_DATE = new Date(2015, 11, 3, 17 ,36).getTime();

  var activeClock;
  var sats;

  var svg = d3.select("#globe");

  var marginTop = 0;
  var width = svg.attr("width");
  var height = svg.attr("height") - marginTop;

  var projection = d3
    .geoOrthographic()
    .scale((height - 10) / 2)
    .translate([width / 2, height / 2 + marginTop])
    .rotate([45, -30]);

  var geoPath = d3.geoPath().projection(projection);

  svg
    .append("path")
    .datum({
      type: "Sphere",
    })
    .style("cursor", "grab")
    .attr("fill", "#2E86AB")
    .attr("d", geoPath);

  function initGlobe() {
			let worldData = world110m;
      svg
        .selectAll(".segment")
        .data([topojson.feature(worldData, worldData.objects.land)])
        .enter()
        .append("path")
        .style("cursor", "grab")
        .attr("class", "segment")
        .attr("d", geoPath)
        .style("stroke", "#88888800")
        .style("stroke-width", "0px")
        .style("fill", "#ffffff")
        .style("opacity", "1");
  }

  async function updateSats(date) {
    sats.forEach(async function (sat) {
      return sat.setDate(date).update();
    });
    return sats;
  }

  /**
   * Create satellite objects for each record in the TLEs and begin animation
   * @param {string[][]} parsedTles
   */
  async function initSats(parsedTles) {
    activeClock = new Clock();

    await activeClock.rate(100);
    await activeClock.date(TLE_DATA_DATE);

    sats = await Promise.all(
      parsedTles.map(async function (tle) {
        var sat = new Satellite(tle, new Date(2015, 11, 3, 17, 36));
        sat.halfAngle(61.73);
        return sat;
      })
    );

    window.requestAnimationFrame(animateSats);
    return sats;
  }

  async function draw() {
    // redrawGlobe();
    let allFootprints = svg.selectAll(".footprint");

    let allFootprintsData = allFootprints.data(sats, async function (sat) {
      return sat.satNum();
    });

    let allFootprintsDataJoin = allFootprintsData.join(function (enter) {
      return enter
        .append("path")
        .attr("class", function (sat) {
          return "footprint footprint--" + sat.getOrbitType();
        })
        .style("cursor", "grab");
    });

    allFootprintsDataJoin.attr("d", function (sat) {
      return geoPath(sat.getFootprint());
    });
  }

  async function redrawGlobe() {
    let allSelectedSegment = svg.selectAll(".segment");
    allSelectedSegment.attr("d", geoPath);
  }

  var m0;
  var o0;

  async function mousedown(e) {
    m0 = [e.pageX, e.pageY];
    o0 = projection.rotate();
    e.preventDefault();
  }

  async function mousemove(e) {
    if (m0) {
      var m1 = [e.pageX, e.pageY];
      const o1 = [o0[0] + (m1[0] - m0[0]) / 2.5, o0[1] + (m0[1] - m1[1]) / 2.5];
      projection.rotate(o1);
      redrawGlobe();
    }
  }

  async function mouseup(e) {
    if (m0) {
      mousemove(e);
      m0 = null;
    }
  }

  svg.on("mousedown", mousedown);
  d3.select(window).on("mousemove", mousemove).on("mouseup", mouseup);

  async function animateSats(elapsed) {
    var dateInMsI1 = await activeClock.elapsed(elapsed);
    var dateInMs = dateInMsI1.date();
    var date = new Date(await dateInMs);

    updateSats(date);
    draw();
    window.requestAnimationFrame(animateSats);
  }

  initGlobe();

  await initSats(await parseTle(tles))
  }
	
	
	
		onMount(async () => {
		doThing();
	});

</script>