2135 lines
67 KiB
JavaScript
2135 lines
67 KiB
JavaScript
/*
|
|
Copyright (c) 2023-forever Douglas Malnati. All rights reserved.
|
|
|
|
See the /faq/tos page for details.
|
|
|
|
(If this generated header is stamped on a file which is a 3rd party file or under a different license or copyright, then ignore this copyright statement and use that file's terms.)
|
|
*/
|
|
|
|
import * as utl from '/js/Utl.js';
|
|
|
|
import { Animation } from './Animation.js'
|
|
import { AsyncResourceLoader } from './AsyncResourceLoader.js';
|
|
import { Base } from './Base.js';
|
|
import { CSSDynamic } from './CSSDynamic.js';
|
|
import { DialogBox } from './DomWidgets.js';
|
|
import { Timeline } from '/js/Timeline.js';
|
|
import { WSPREncoded } from '/js/WSPREncoded.js';
|
|
|
|
let t = new Timeline();
|
|
t.SetCcGlobal(true);
|
|
t.Event(`SpotMap::AsyncModuleResourceLoad Start`);
|
|
let p1 = AsyncResourceLoader.AsyncLoadScript(`https://cdn.jsdelivr.net/npm/ol@v7.3.0/dist/ol.js`);
|
|
let p2 = AsyncResourceLoader.AsyncLoadStylesheet(`https://cdn.jsdelivr.net/npm/ol@v7.3.0/ol.css`);
|
|
await Promise.all([p1, p2]);
|
|
t.Event(`SpotMap::AsyncModuleResourceLoad End`);
|
|
|
|
|
|
export class Spot
|
|
{
|
|
constructor(spotData)
|
|
{
|
|
this.spotData = spotData;
|
|
|
|
this.loc = ol.proj.fromLonLat([this.GetLng(), this.GetLat()]);
|
|
}
|
|
|
|
GetLoc()
|
|
{
|
|
return this.loc;
|
|
}
|
|
|
|
GetLat()
|
|
{
|
|
return this.spotData.lat;
|
|
}
|
|
|
|
GetLng()
|
|
{
|
|
return this.spotData.lng;
|
|
}
|
|
|
|
GetGrid()
|
|
{
|
|
return this.spotData.grid;
|
|
}
|
|
|
|
GetGrid4()
|
|
{
|
|
return this.GetGrid().substr(0, 4);
|
|
}
|
|
|
|
GetAccuracy()
|
|
{
|
|
return this.spotData.accuracy;
|
|
}
|
|
|
|
GetDTLocal()
|
|
{
|
|
return this.spotData.dtLocal;
|
|
}
|
|
|
|
GetSeenDataList()
|
|
{
|
|
return this.spotData.seenDataList;
|
|
}
|
|
}
|
|
|
|
|
|
// https://openlayers.org/en/latest/examples/custom-controls.html
|
|
function GetStoredBoolean(storageKey, defaultValue, legacyKeyList = [])
|
|
{
|
|
for (const key of [storageKey, ...legacyKeyList])
|
|
{
|
|
const value = localStorage.getItem(key);
|
|
if (value != null)
|
|
{
|
|
return value == "true";
|
|
}
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
class MapButtonControl extends ol.control.Control {
|
|
constructor({ label, rightPx, title = "" })
|
|
{
|
|
const button = document.createElement('button');
|
|
button.innerHTML = label;
|
|
button.title = title;
|
|
button.style.fontWeight = "bold";
|
|
button.style.cursor = "pointer";
|
|
|
|
const element = document.createElement('div');
|
|
element.className = 'ol-unselectable ol-control';
|
|
element.style.top = "7px";
|
|
element.style.right = `${rightPx}px`;
|
|
element.appendChild(button);
|
|
|
|
super({
|
|
element: element,
|
|
});
|
|
|
|
this.button = button;
|
|
}
|
|
|
|
SetActive(tf)
|
|
{
|
|
if (tf)
|
|
{
|
|
this.button.style.backgroundColor = "rgb(215, 237, 255)";
|
|
this.button.style.borderColor = "rgb(120, 160, 210)";
|
|
this.button.style.color = "rgb(25, 55, 95)";
|
|
this.button.style.boxShadow = "inset 0 1px 4px rgba(0, 0, 0, 0.22)";
|
|
this.button.style.transform = "translateY(1px)";
|
|
}
|
|
else
|
|
{
|
|
this.button.style.backgroundColor = "rgb(248, 248, 248)";
|
|
this.button.style.borderColor = "rgb(180, 180, 180)";
|
|
this.button.style.color = "rgb(70, 70, 70)";
|
|
this.button.style.boxShadow = "0 1px 1px rgba(255, 255, 255, 0.8) inset";
|
|
this.button.style.transform = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
class MapToggleControl extends MapButtonControl {
|
|
constructor({ label, rightPx, title, storageKey, defaultValue, legacyKeyList = [], onToggle })
|
|
{
|
|
super({ label, rightPx, title });
|
|
|
|
this.storageKey = storageKey;
|
|
this.onToggle = onToggle;
|
|
this.enabled = GetStoredBoolean(storageKey, defaultValue, legacyKeyList);
|
|
|
|
this.SetActive(this.enabled);
|
|
|
|
this.button.addEventListener('click', () => {
|
|
this.SetEnabled(!this.enabled);
|
|
});
|
|
}
|
|
|
|
SetEnabled(enabled)
|
|
{
|
|
this.enabled = enabled;
|
|
this.SetActive(this.enabled);
|
|
localStorage.setItem(this.storageKey, this.enabled);
|
|
this.onToggle(this.enabled);
|
|
}
|
|
|
|
IsEnabled()
|
|
{
|
|
return this.enabled;
|
|
}
|
|
}
|
|
|
|
class MapHelpControl extends MapButtonControl {
|
|
constructor(spotMap)
|
|
{
|
|
super({
|
|
label: '?',
|
|
rightPx: 30,
|
|
title: 'Map control help',
|
|
});
|
|
|
|
this.button.addEventListener('click', () => {
|
|
spotMap.ShowControlHelpDialog();
|
|
});
|
|
}
|
|
}
|
|
|
|
export class SpotMap
|
|
extends Base
|
|
{
|
|
static STORAGE_KEY_SHOW_LINES = "spotMapShowLines";
|
|
static STORAGE_KEY_SHOW_RX = "spotMapShowRx";
|
|
static STORAGE_KEY_AUTO_MOVE = "spotMapAutoMove";
|
|
static STORAGE_KEY_HOVER_EMPHASIS = "spotMapHoverEmphasis";
|
|
|
|
constructor(cfg)
|
|
{
|
|
super();
|
|
|
|
this.cfg = cfg;
|
|
this.container = this.cfg.container;
|
|
|
|
this.t.Event("SpotMap::Constructor");
|
|
|
|
// Initial state of map
|
|
this.initialCenterLocation = ol.proj.fromLonLat([-40, 40]);
|
|
this.initialZoom = 1;
|
|
|
|
this.dataSetPreviously = false;
|
|
|
|
this.dt__data = new Map();
|
|
this.spotDt__idx = new Map();
|
|
this.spot__rxFeatureList = new WeakMap();
|
|
this.spotFeatureList = [];
|
|
this.lineFeatureList = [];
|
|
this.lineFeatureListByEndIdx = [];
|
|
this.hoverStartIdxBySpotIdx = [];
|
|
this.currentHoverWindow = null;
|
|
this.currentHoverSpotDt = null;
|
|
this.spotOpacityList = [];
|
|
this.lineOpacityByEndIdx = [];
|
|
this.spotStyleCache = new Map();
|
|
this.lineStyleCache = new Map();
|
|
this.rxFeatureListKeyLast = null;
|
|
this.pendingPointerMove = null;
|
|
this.pointerMoveRafId = null;
|
|
this.hoverSpotDt = null;
|
|
this.rxStyleSeen = new ol.style.Style({
|
|
image: new ol.style.Circle({
|
|
radius: 3,
|
|
fill: new ol.style.Fill({
|
|
color: 'rgba(255, 0, 255, 1)',
|
|
}),
|
|
stroke: new ol.style.Stroke({
|
|
color: 'rgba(255, 0, 255, 1)',
|
|
width: 0.1,
|
|
}),
|
|
}),
|
|
});
|
|
|
|
this.spotListLast = [];
|
|
this.showLines = GetStoredBoolean(SpotMap.STORAGE_KEY_SHOW_LINES, true);
|
|
this.showRxEnabled = GetStoredBoolean(SpotMap.STORAGE_KEY_SHOW_RX, true, ["showRxState"]);
|
|
this.autoMove = GetStoredBoolean(SpotMap.STORAGE_KEY_AUTO_MOVE, true);
|
|
this.hoverEmphasisEnabled = GetStoredBoolean(SpotMap.STORAGE_KEY_HOVER_EMPHASIS, true);
|
|
|
|
this.mapControl = new MapToggleControl({
|
|
label: 'L',
|
|
rightPx: 53,
|
|
title: 'Toggle lines between spots',
|
|
storageKey: SpotMap.STORAGE_KEY_SHOW_LINES,
|
|
defaultValue: true,
|
|
onToggle: enabled => this.OnLineToggle(enabled),
|
|
});
|
|
this.mapControlRx = new MapToggleControl({
|
|
label: 'R',
|
|
rightPx: 76,
|
|
title: 'Toggle receiver markers',
|
|
storageKey: SpotMap.STORAGE_KEY_SHOW_RX,
|
|
defaultValue: true,
|
|
legacyKeyList: ["showRxState"],
|
|
onToggle: enabled => this.OnRxToggle(enabled),
|
|
});
|
|
this.mapControlMove = new MapToggleControl({
|
|
label: 'M',
|
|
rightPx: 99,
|
|
title: 'Toggle map movement to new spots',
|
|
storageKey: SpotMap.STORAGE_KEY_AUTO_MOVE,
|
|
defaultValue: true,
|
|
onToggle: enabled => this.OnMoveToggle(enabled),
|
|
});
|
|
this.mapControlHover = new MapToggleControl({
|
|
label: 'H',
|
|
rightPx: 122,
|
|
title: 'Toggle hover emphasis',
|
|
storageKey: SpotMap.STORAGE_KEY_HOVER_EMPHASIS,
|
|
defaultValue: true,
|
|
onToggle: enabled => this.OnHoverEmphasisToggle(enabled),
|
|
});
|
|
this.mapControlHelp = new MapHelpControl(this);
|
|
this.showRxState = this.showRxEnabled ? "default" : "disabled";
|
|
|
|
this.MakeUI();
|
|
this.controlHelpDialog = this.MakeControlHelpDialog();
|
|
document.body.appendChild(this.controlHelpDialog.GetUI());
|
|
this.Load();
|
|
this.SetupEscapeHandler();
|
|
}
|
|
|
|
SetDebug(tf)
|
|
{
|
|
this.t.SetCcGlobal(tf);
|
|
}
|
|
|
|
MakeUI()
|
|
{
|
|
let cd = new CSSDynamic();
|
|
|
|
this.ui = document.createElement('div');
|
|
this.ui.style.margin = "0px";
|
|
this.ui.style.padding = "0px";
|
|
this.ui.style.width = "100%";
|
|
this.ui.style.height = "100%"
|
|
this.ui.style.position = "relative";
|
|
this.ui.style.opacity = "0.0"; // initially
|
|
|
|
this.container.appendChild(this.ui);
|
|
|
|
this.hoverInfoUi = document.createElement('div');
|
|
this.hoverInfoUi.style.position = "absolute";
|
|
this.hoverInfoUi.style.top = "34px";
|
|
this.hoverInfoUi.style.right = "30px";
|
|
this.hoverInfoUi.style.fontSize = "11px";
|
|
this.hoverInfoUi.style.color = "rgba(40, 40, 40, 0.9)";
|
|
this.hoverInfoUi.style.pointerEvents = "none";
|
|
this.hoverInfoUi.style.backgroundColor = "rgba(255, 255, 255, 0.0)";
|
|
this.hoverInfoUi.style.zIndex = "2";
|
|
this.hoverInfoUi.style.display = "none";
|
|
this.hoverInfoUi.style.whiteSpace = "pre";
|
|
this.ui.appendChild(this.hoverInfoUi);
|
|
|
|
// create and style the entire popup
|
|
this.popup = document.createElement('div');
|
|
this.popup.classList.add('ol-popup');
|
|
cd.SetCssClassProperties(`ol-popup`, {
|
|
position: "absolute",
|
|
backgroundColor: "white",
|
|
boxShadow: "0 1px 4px rgba(0,0,0,0.2)",
|
|
padding: "15px",
|
|
borderRadius: "10px",
|
|
border: "1px solid #cccccc",
|
|
bottom: "12px",
|
|
left: "-50px",
|
|
minWidth: "250px",
|
|
zIndex: "-1",
|
|
});
|
|
|
|
for (let ccName of ["ol-popup::after", "ol-popup::before"])
|
|
{
|
|
cd.SetCssClassProperties(ccName, {
|
|
top: "100%",
|
|
border: "solid transparent",
|
|
content: " ",
|
|
height: 0,
|
|
width: 0,
|
|
position: "absolute",
|
|
pointerEvents: "none",
|
|
});
|
|
}
|
|
cd.SetCssClassDynamicProperties("ol-popup", "after", " ", `
|
|
border-top-color: white;
|
|
border-width: 10px;
|
|
left: 48px;
|
|
margin-left: -10px;
|
|
`);
|
|
|
|
cd.SetCssClassDynamicProperties("ol-popup", "before", " ", `
|
|
border-top-color: #cccccc;
|
|
border-width: 11px;
|
|
left: 48px;
|
|
margin-left: -11px;
|
|
`);
|
|
|
|
|
|
// create and style the X button
|
|
this.popupCloser = document.createElement('div');
|
|
this.popupCloser.appendChild(document.createTextNode("✖"));
|
|
this.popupCloser.style.cursor = "default";
|
|
this.popupCloser.classList.add('ol-popup-closer');
|
|
cd.SetCssClassProperties(`ol-popup-closer`, {
|
|
textDecoration: "none",
|
|
position: "absolute",
|
|
top: "2px",
|
|
right: "5px",
|
|
});
|
|
|
|
// create container for content
|
|
this.popupContent = document.createElement('div');
|
|
|
|
// assemble
|
|
this.popup.appendChild(this.popupCloser);
|
|
this.popup.appendChild(this.popupContent);
|
|
}
|
|
|
|
Load()
|
|
{
|
|
this.MakeMapBase();
|
|
this.MakeMapLayers();
|
|
this.MakeMapOverlay();
|
|
this.SetupEventHandlers();
|
|
}
|
|
|
|
SetupEscapeHandler()
|
|
{
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key == 'Escape')
|
|
{
|
|
this.ClosePopup();
|
|
}
|
|
});
|
|
}
|
|
|
|
MakeControlHelpDialog()
|
|
{
|
|
let dlg = new DialogBox();
|
|
dlg.SetTitleBar("Map Controls Help");
|
|
dlg.GetUI().style.top = "100px";
|
|
dlg.GetUI().style.left = "100px";
|
|
|
|
let body = dlg.GetContentContainer();
|
|
body.style.padding = "12px";
|
|
body.style.minWidth = "420px";
|
|
body.style.maxWidth = "560px";
|
|
body.style.backgroundColor = "rgb(245, 245, 245)";
|
|
body.style.fontSize = "14px";
|
|
body.style.lineHeight = "1.45";
|
|
|
|
let intro = document.createElement("div");
|
|
intro.innerHTML = `Use the upper-right map controls to change how spots are displayed.`;
|
|
intro.style.marginBottom = "8px";
|
|
intro.style.fontWeight = "600";
|
|
body.appendChild(intro);
|
|
|
|
let list = document.createElement("ul");
|
|
list.style.margin = "0px";
|
|
list.style.paddingLeft = "20px";
|
|
|
|
for (const html of [
|
|
`<b>H</b> toggles highlighting the last lap to the hovered point.`,
|
|
`<b>M</b> toggles the map panning to new spot locations on update or not.`,
|
|
`<b>R</b> toggles showing receivers (purple dots).`,
|
|
`<b>L</b> toggles interconnecting the spots with lines.`,
|
|
`<b>?</b> opens this help dialog.`,
|
|
])
|
|
{
|
|
let li = document.createElement("li");
|
|
li.innerHTML = html;
|
|
list.appendChild(li);
|
|
}
|
|
|
|
body.appendChild(list);
|
|
|
|
let note = document.createElement("div");
|
|
note.innerHTML = `These settings are saved and restored automatically.`;
|
|
note.style.marginTop = "8px";
|
|
body.appendChild(note);
|
|
|
|
return dlg;
|
|
}
|
|
|
|
ShowControlHelpDialog()
|
|
{
|
|
this.controlHelpDialog.Show();
|
|
}
|
|
|
|
MakeMapBase()
|
|
{
|
|
// for base raster, we use Open Street Maps
|
|
const source = new ol.source.OSM();
|
|
|
|
// let's set up a little mini-map in the lower-left corner
|
|
const overviewMapControl = new ol.control.OverviewMap({
|
|
layers: [
|
|
new ol.layer.Tile({
|
|
source: source,
|
|
}),
|
|
],
|
|
});
|
|
|
|
// set up controls for mini-map
|
|
let controls = new ol.Collection();
|
|
|
|
// set up layers
|
|
const engOsmLayer = new ol.layer.Tile({
|
|
source: new ol.source.XYZ({
|
|
url: './tiles/{z}/{x}/{y}.png',
|
|
attributions: [
|
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors.'
|
|
]
|
|
}),
|
|
minZoom: 0,
|
|
maxZoom: 7
|
|
});
|
|
|
|
const osmLayer = new ol.layer.Tile({
|
|
source: source,
|
|
minZoom: 7
|
|
});
|
|
|
|
// set up attribution
|
|
const attributionControl = new ol.control.Attribution({
|
|
collapsible: false
|
|
});
|
|
|
|
// Load map instance
|
|
this.map = new ol.Map({
|
|
controls: controls.extend([
|
|
overviewMapControl,
|
|
new ol.control.FullScreen(),
|
|
this.mapControl,
|
|
this.mapControlRx,
|
|
this.mapControlMove,
|
|
this.mapControlHover,
|
|
this.mapControlHelp,
|
|
attributionControl,
|
|
]),
|
|
// target: this.container,
|
|
target: this.ui,
|
|
layers: [
|
|
engOsmLayer,
|
|
osmLayer,
|
|
],
|
|
view: new ol.View({
|
|
center: this.initialCenterLocation,
|
|
zoom: this.initialZoom,
|
|
}),
|
|
});
|
|
|
|
// make sure the mini-map is closed by default
|
|
overviewMapControl.setCollapsed(true);
|
|
}
|
|
|
|
HeatMapGetWeight(feature)
|
|
{
|
|
const grid4 = feature.get("grid4");
|
|
|
|
console.log(`${grid4}`)
|
|
|
|
// look up pre-cached data about relative grid reception
|
|
let data = this.grid4__data.get(grid4);
|
|
|
|
// calculate weight
|
|
let retVal = 0.0;
|
|
retVal = data.maxHeard / this.maxHeardGlobal;
|
|
|
|
retVal = data.maxHeard / 50.0;
|
|
if (retVal > 1) { retVal = 1; }
|
|
|
|
console.log(`returning ${grid4}: ${data.maxHeard} / ${this.maxHeardGlobal} = ${retVal}`)
|
|
return retVal;
|
|
}
|
|
|
|
MakeMapLayers()
|
|
{
|
|
// create heat map
|
|
// https://gis.stackexchange.com/questions/418820/creating-heatmap-from-vector-tiles-using-openlayers
|
|
//
|
|
// let heatmapSource = new ol.source.Vector();
|
|
// this.hmLayer = new ol.layer.Heatmap({
|
|
// source: heatmapSource,
|
|
// weight: feature => {
|
|
// return this.HeatMapGetWeight(feature);
|
|
// },
|
|
// radius: 10,
|
|
// blur: 30,
|
|
// });
|
|
// this.map.addLayer(this.hmLayer);
|
|
|
|
// create a layer to put rx station markers on
|
|
this.rxLayer = new ol.layer.Vector({
|
|
source: new ol.source.Vector({
|
|
features: [],
|
|
}),
|
|
});
|
|
this.map.addLayer(this.rxLayer);
|
|
|
|
// create a layer to put spot markers on
|
|
this.spotLayer = new ol.layer.Vector({
|
|
source: new ol.source.Vector({
|
|
features: [],
|
|
}),
|
|
});
|
|
this.map.addLayer(this.spotLayer);
|
|
}
|
|
|
|
MakeMapOverlay()
|
|
{
|
|
this.overlay = new ol.Overlay({
|
|
// element: document.getElementById('popup'),
|
|
element: this.popup,
|
|
autoPan: {
|
|
animation: {
|
|
duration: 250,
|
|
},
|
|
},
|
|
});
|
|
|
|
this.map.addOverlay(this.overlay);
|
|
|
|
// let closer = document.getElementById('popup-closer');
|
|
// closer.onclick = () => {
|
|
this.popupCloser.onclick = () => {
|
|
this.ClosePopup();
|
|
this.popupCloser.blur();
|
|
return false;
|
|
};
|
|
}
|
|
|
|
ClosePopup()
|
|
{
|
|
if (this.overlay)
|
|
{
|
|
if (this.showRxState != "disabled")
|
|
{
|
|
this.showRxState = "default";
|
|
this.HandleSeen(this.spotListLast);
|
|
}
|
|
|
|
this.overlay.setPosition(undefined);
|
|
}
|
|
}
|
|
|
|
OnLineToggle(enabled)
|
|
{
|
|
this.showLines = enabled;
|
|
|
|
// re-display
|
|
this.SetSpotList(this.spotListLast, { preserveView: true });
|
|
}
|
|
|
|
OnRxToggle(enabled)
|
|
{
|
|
this.showRxEnabled = enabled;
|
|
this.SetShowRxState(this.showRxEnabled ? "default" : "disabled");
|
|
}
|
|
|
|
OnMoveToggle(enabled)
|
|
{
|
|
this.autoMove = enabled;
|
|
}
|
|
|
|
OnHoverEmphasisToggle(enabled)
|
|
{
|
|
this.hoverEmphasisEnabled = enabled;
|
|
this.currentHoverWindow = null;
|
|
|
|
if (this.hoverEmphasisEnabled)
|
|
{
|
|
this.PrecomputeHoverEmphasisData();
|
|
}
|
|
else
|
|
{
|
|
this.hoverStartIdxBySpotIdx = [];
|
|
}
|
|
|
|
this.ApplyHoverEmphasis();
|
|
}
|
|
|
|
GetSpotStyle(accuracy, opacity = 1.0)
|
|
{
|
|
const opacityUse = Math.max(0.07, Math.min(1.0, opacity));
|
|
const key = `${accuracy}|${opacityUse.toFixed(3)}`;
|
|
if (this.spotStyleCache.has(key))
|
|
{
|
|
return this.spotStyleCache.get(key);
|
|
}
|
|
|
|
const radius =
|
|
accuracy == "veryHigh" ? 3 :
|
|
accuracy == "high" ? 5 :
|
|
5;
|
|
|
|
const strokeRgb =
|
|
accuracy == "veryHigh" ? [55, 143, 205] :
|
|
accuracy == "high" ? [55, 143, 205] :
|
|
[205, 143, 55];
|
|
|
|
const style = new ol.style.Style({
|
|
image: new ol.style.Circle({
|
|
radius: radius,
|
|
fill: new ol.style.Fill({
|
|
color: `rgba(255, 255, 255, ${(0.4 * opacityUse).toFixed(3)})`,
|
|
}),
|
|
stroke: new ol.style.Stroke({
|
|
color: `rgba(${strokeRgb[0]}, ${strokeRgb[1]}, ${strokeRgb[2]}, ${opacityUse.toFixed(3)})`,
|
|
width: 1.1,
|
|
}),
|
|
}),
|
|
});
|
|
|
|
this.spotStyleCache.set(key, style);
|
|
|
|
return style;
|
|
}
|
|
|
|
GetLineStyle(opacity = 1.0)
|
|
{
|
|
const opacityUse = Math.max(0.07, Math.min(1.0, opacity));
|
|
const key = opacityUse.toFixed(3);
|
|
if (this.lineStyleCache.has(key))
|
|
{
|
|
return this.lineStyleCache.get(key);
|
|
}
|
|
|
|
const style = new ol.style.Style({
|
|
stroke: new ol.style.Stroke({
|
|
color: `rgba(0, 128, 0, ${opacityUse.toFixed(3)})`,
|
|
width: 1,
|
|
}),
|
|
});
|
|
|
|
this.lineStyleCache.set(key, style);
|
|
|
|
return style;
|
|
}
|
|
|
|
GetHoverEmphasisWindow()
|
|
{
|
|
if (this.hoverEmphasisEnabled == false || this.hoverSpotDt == null || this.spotFeatureList.length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
let hoverIdx = this.spotDt__idx.get(this.hoverSpotDt);
|
|
if (hoverIdx == undefined)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
startIdx: this.hoverStartIdxBySpotIdx[hoverIdx] ?? 0,
|
|
hoverIdx: hoverIdx,
|
|
};
|
|
}
|
|
|
|
GetHoverEmphasisOpacity(spotIdx, window)
|
|
{
|
|
const DIMMED_OPACITY = 0.07;
|
|
|
|
if (window == null)
|
|
{
|
|
return 1.0;
|
|
}
|
|
|
|
if (spotIdx > window.hoverIdx || spotIdx < window.startIdx)
|
|
{
|
|
return DIMMED_OPACITY;
|
|
}
|
|
|
|
return 1.0;
|
|
}
|
|
|
|
UpdateHoverInfoUi(window)
|
|
{
|
|
if (this.hoverSpotDt == null)
|
|
{
|
|
this.hoverInfoUi.style.display = "none";
|
|
this.hoverInfoUi.textContent = "";
|
|
return;
|
|
}
|
|
|
|
const hoverIdx = this.spotDt__idx.get(this.hoverSpotDt);
|
|
const hoverSpot = hoverIdx == undefined ? null : this.spotFeatureList[hoverIdx]?.get("spot");
|
|
if (!hoverSpot)
|
|
{
|
|
this.hoverInfoUi.style.display = "none";
|
|
this.hoverInfoUi.textContent = "";
|
|
return;
|
|
}
|
|
|
|
if (this.hoverEmphasisEnabled && window != null)
|
|
{
|
|
const spotStart = this.spotFeatureList[window.startIdx]?.get("spot");
|
|
const spotEnd = this.spotFeatureList[window.hoverIdx]?.get("spot");
|
|
if (!spotStart || !spotEnd)
|
|
{
|
|
this.hoverInfoUi.style.display = "none";
|
|
this.hoverInfoUi.textContent = "";
|
|
return;
|
|
}
|
|
|
|
const dtStartUtc = utl.ConvertLocalToUtc(spotStart.GetDTLocal());
|
|
const dtEndUtc = utl.ConvertLocalToUtc(spotEnd.GetDTLocal());
|
|
const dtStartLocal = spotStart.GetDTLocal();
|
|
const dtEndLocal = spotEnd.GetDTLocal();
|
|
|
|
this.hoverInfoUi.textContent =
|
|
`UTC ${dtStartUtc.slice(0, 16)} - ${dtEndUtc.slice(0, 16)}\n` +
|
|
`LCL ${dtStartLocal.slice(0, 16)} - ${dtEndLocal.slice(0, 16)}`;
|
|
}
|
|
else
|
|
{
|
|
const dtUtc = utl.ConvertLocalToUtc(hoverSpot.GetDTLocal());
|
|
const dtLocal = hoverSpot.GetDTLocal();
|
|
|
|
this.hoverInfoUi.textContent =
|
|
`UTC ${dtUtc.slice(0, 16)}\n` +
|
|
`LCL ${dtLocal.slice(0, 16)}`;
|
|
}
|
|
|
|
this.hoverInfoUi.style.display = "";
|
|
}
|
|
|
|
PrecomputeHoverEmphasisData()
|
|
{
|
|
const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000;
|
|
|
|
this.hoverStartIdxBySpotIdx = [];
|
|
|
|
if (this.hoverEmphasisEnabled == false || this.spotFeatureList.length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let spotMsList = [];
|
|
let unwrappedLngList = [];
|
|
let lngLast = null;
|
|
for (const feature of this.spotFeatureList)
|
|
{
|
|
spotMsList.push(utl.ParseTimeToMs(feature.get("spot").GetDTLocal()));
|
|
|
|
let lng = feature.get("spot").GetLng();
|
|
if (lngLast != null)
|
|
{
|
|
while ((lng - lngLast) > 180) { lng -= 360; }
|
|
while ((lng - lngLast) < -180) { lng += 360; }
|
|
}
|
|
|
|
unwrappedLngList.push(lng);
|
|
lngLast = lng;
|
|
}
|
|
|
|
let SegmentCrossesEquivalentLongitude = (lngA, lngB, targetLng) => {
|
|
let lngMin = Math.min(lngA, lngB);
|
|
let lngMax = Math.max(lngA, lngB);
|
|
|
|
let kMin = Math.ceil((lngMin - targetLng) / 360);
|
|
let kMax = Math.floor((lngMax - targetLng) / 360);
|
|
|
|
return kMin <= kMax;
|
|
};
|
|
|
|
let minLookbackIdx = 0;
|
|
for (let hoverIdx = 0; hoverIdx < unwrappedLngList.length; ++hoverIdx)
|
|
{
|
|
let hoverMs = spotMsList[hoverIdx];
|
|
let minAllowedMs = hoverMs - TEN_DAYS_MS;
|
|
while (minLookbackIdx < hoverIdx && spotMsList[minLookbackIdx] < minAllowedMs)
|
|
{
|
|
++minLookbackIdx;
|
|
}
|
|
|
|
let targetLng = unwrappedLngList[hoverIdx];
|
|
let startIdx = minLookbackIdx;
|
|
|
|
for (let i = minLookbackIdx; i >= 1; --i)
|
|
{
|
|
let lngA = unwrappedLngList[i - 1];
|
|
let lngB = unwrappedLngList[i];
|
|
|
|
if (SegmentCrossesEquivalentLongitude(lngA, lngB, targetLng))
|
|
{
|
|
startIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.hoverStartIdxBySpotIdx[hoverIdx] = startIdx;
|
|
}
|
|
}
|
|
|
|
ApplyHoverEmphasis()
|
|
{
|
|
if (this.spotFeatureList.length == 0)
|
|
{
|
|
this.UpdateHoverInfoUi(null);
|
|
return;
|
|
}
|
|
|
|
const window = this.GetHoverEmphasisWindow();
|
|
const windowSame =
|
|
this.currentHoverWindow?.startIdx == window?.startIdx &&
|
|
this.currentHoverWindow?.hoverIdx == window?.hoverIdx;
|
|
const hoverSpotSame = this.currentHoverSpotDt == this.hoverSpotDt;
|
|
if (windowSame)
|
|
{
|
|
if (hoverSpotSame)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.UpdateHoverInfoUi(window);
|
|
|
|
for (let i = 0; i < this.spotFeatureList.length; ++i)
|
|
{
|
|
let opacity = this.GetHoverEmphasisOpacity(i, window);
|
|
if (this.spotOpacityList[i] != opacity)
|
|
{
|
|
const feature = this.spotFeatureList[i];
|
|
const spot = feature.get("spot");
|
|
feature.setStyle(this.GetSpotStyle(spot.GetAccuracy(), opacity));
|
|
this.spotOpacityList[i] = opacity;
|
|
}
|
|
}
|
|
|
|
for (let endIdx = 1; endIdx < this.lineFeatureListByEndIdx.length; ++endIdx)
|
|
{
|
|
const opacity = Math.min(
|
|
this.GetHoverEmphasisOpacity(endIdx - 1, window),
|
|
this.GetHoverEmphasisOpacity(endIdx, window),
|
|
);
|
|
if (this.lineOpacityByEndIdx[endIdx] == opacity)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (const feature of this.lineFeatureListByEndIdx[endIdx] ?? [])
|
|
{
|
|
feature.setStyle(this.GetLineStyle(opacity));
|
|
}
|
|
this.lineOpacityByEndIdx[endIdx] = opacity;
|
|
}
|
|
|
|
this.currentHoverWindow = window ? { ...window } : null;
|
|
this.currentHoverSpotDt = this.hoverSpotDt;
|
|
}
|
|
|
|
GetLatestFeatureByType(featureList, type)
|
|
{
|
|
let featureLatest = null;
|
|
let dtLatest = null;
|
|
for (const feature of featureList)
|
|
{
|
|
if (feature.get("type") == type)
|
|
{
|
|
let dt = feature.get("spot").GetDTLocal();
|
|
if (dtLatest == null || dt > dtLatest)
|
|
{
|
|
dtLatest = dt;
|
|
featureLatest = feature;
|
|
}
|
|
}
|
|
}
|
|
|
|
return featureLatest;
|
|
}
|
|
|
|
GetLatestSpotFeatureAtPixel(pixel)
|
|
{
|
|
let featureLatest = this.map.forEachFeatureAtPixel(pixel, (feature, layer) => {
|
|
if (layer === this.spotLayer && feature.get("type") == "spot")
|
|
{
|
|
return feature;
|
|
}
|
|
|
|
return undefined;
|
|
}, {
|
|
hitTolerance: 4,
|
|
layerFilter: layer => layer === this.spotLayer,
|
|
});
|
|
|
|
return featureLatest;
|
|
}
|
|
|
|
OnPointerMove(pixel, coordinate, e)
|
|
{
|
|
// if map moving (by a fling), a mouseover event causes a noticable
|
|
// hang in motion. prevent that by only handling this event if we
|
|
// are not in motion.
|
|
if (this.moveState == "moving") { return; }
|
|
if (this.showRxState == "frozen") { return; }
|
|
|
|
// figure out what you're hovering over.
|
|
// prioritize mousing over spots.
|
|
//
|
|
// update - holy shit this takes like 100ms when the dev console
|
|
// is open, but seemingly not when it isn't
|
|
let spotFeature = this.GetLatestSpotFeatureAtPixel(pixel);
|
|
|
|
// accumulate firing of the same spot, and also distinguish between
|
|
// hovering over something vs nothing
|
|
|
|
if (this.spotFeatureLast == spotFeature)
|
|
{
|
|
// either still the same something, or still nothing, but either
|
|
// way we don't care and ignore it
|
|
spotFeature = null;
|
|
}
|
|
else
|
|
{
|
|
// there was a change
|
|
if (spotFeature)
|
|
{
|
|
// was nothing, now something
|
|
const spot = spotFeature.get("spot");
|
|
|
|
this.hoverSpotDt = spot.GetDTLocal();
|
|
this.ApplyHoverEmphasis();
|
|
|
|
if (this.showRxState != "disabled")
|
|
{
|
|
this.showRxState = "hover";
|
|
this.HandleSeen([spot]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// was something, now nothing
|
|
this.hoverSpotDt = null;
|
|
this.ApplyHoverEmphasis();
|
|
|
|
if (this.showRxState != "disabled")
|
|
{
|
|
this.showRxState = "default";
|
|
this.HandleSeen(this.spotListLast);
|
|
}
|
|
}
|
|
|
|
// remember for next time
|
|
this.spotFeatureLast = spotFeature;
|
|
}
|
|
}
|
|
|
|
OnClick(pixel, coordinate, e)
|
|
{
|
|
let feature = this.GetLatestSpotFeatureAtPixel(pixel);
|
|
|
|
if (feature)
|
|
{
|
|
let spotLast = null;
|
|
if (feature)
|
|
{
|
|
spotLast = feature.get("spot");
|
|
}
|
|
|
|
if (spotLast)
|
|
{
|
|
// if the external click generator passes along the
|
|
// specific spot to use, use it instead
|
|
if (e.spot)
|
|
{
|
|
spotLast = e.spot;
|
|
}
|
|
|
|
// set rx location updates frozen since we know we're
|
|
// doing a popup here. the rx locations of this spot
|
|
// should already be being shown given the mouse
|
|
// clicked it, but let's be explicit anyway
|
|
if (this.showRxState != "disabled")
|
|
{
|
|
// temporarily lift a potential freeze
|
|
// (from prior popup click) to show the rx for this
|
|
// specific spot
|
|
this.showRxState = "hover";
|
|
this.HandleSeen([spotLast]);
|
|
|
|
// now freeze
|
|
this.showRxState = "frozen";
|
|
}
|
|
|
|
// fill out popup
|
|
let td = spotLast.spotData.td;
|
|
|
|
// let content = document.getElementById('popup-content');
|
|
let content = this.popupContent;
|
|
// content.innerHTML = `<p>You clicked ${td.Get(0, "DateTimeLocal")}</p>`;
|
|
content.innerHTML = ``;
|
|
let table = utl.MakeTableTransposed(td.GetDataTable());
|
|
content.appendChild(table);
|
|
|
|
// add additional links
|
|
let lat = spotLast.GetLat();
|
|
let lng = spotLast.GetLng();
|
|
// get altitude but strip comma from it first
|
|
let altM = td.Get(0, "AltM");
|
|
if (altM)
|
|
{
|
|
altM = altM.toString();
|
|
altM = parseInt(altM.replace(/\,/g,''), 10);
|
|
}
|
|
else
|
|
{
|
|
altM = 0;
|
|
}
|
|
|
|
// make jump link active
|
|
let domJl = document.createElement("span");
|
|
domJl.innerHTML = "jump to data";
|
|
domJl.style.cursor = "pointer";
|
|
domJl.style.color = "blue";
|
|
domJl.style.textDecoration = "underline";
|
|
domJl.style.userSelect = "none";
|
|
|
|
domJl.onclick = () => {
|
|
window.parent.postMessage({
|
|
type: "JUMP_TO_DATA",
|
|
ts: spotLast.GetDTLocal(),
|
|
}, "*");
|
|
};
|
|
|
|
// fill out more popup
|
|
content.appendChild(domJl);
|
|
content.appendChild(document.createElement("br"));
|
|
content.appendChild(document.createElement("br"));
|
|
content.appendChild(document.createTextNode("Links:"));
|
|
content.appendChild(document.createElement("br"));
|
|
|
|
// create a table of links to show
|
|
let dataTableLinks = [
|
|
["windy.com", "suncalc.org", "hysplit"]
|
|
];
|
|
let dataRow = [];
|
|
|
|
// fill out windy links
|
|
let windyLinksList = [];
|
|
windyLinksList.push(utl.MakeLink(this.MakeUrlWindyWind(lat, lng, altM), "wind"));
|
|
windyLinksList.push(utl.MakeLink(this.MakeUrlWindyCloudtop(lat, lng), "cloudtop"));
|
|
windyLinksList.push(utl.MakeLink(this.MakeUrlWindyRain(lat, lng), "rain"));
|
|
|
|
let windyLinksStr = windyLinksList.join(", ");
|
|
dataRow.push(windyLinksStr);
|
|
|
|
// fill out suncalc links
|
|
let suncalcLinksList = [];
|
|
suncalcLinksList.push(utl.MakeLink(this.MakeUrlSuncalc(lat, lng), "suncalc"));
|
|
|
|
let suncalcLinksStr = suncalcLinksList.join(", ");
|
|
dataRow.push(suncalcLinksStr);
|
|
|
|
// fill out hysplit links
|
|
let hysplitLinksList = [];
|
|
hysplitLinksList.push(utl.MakeLink(this.MakeUrlHysplitTrajectory(), "traj"));
|
|
hysplitLinksList.push(utl.MakeLink(this.MakeUrlHysplitTrajectoryBalloon(), "for balloons"));
|
|
|
|
let hysplitLinksStr = hysplitLinksList.join(", ");
|
|
dataRow.push(hysplitLinksStr);
|
|
|
|
// push data into data table
|
|
dataTableLinks.push(dataRow);
|
|
|
|
// construct html table and insert
|
|
let linksTable = utl.MakeTableTransposed(dataTableLinks);
|
|
content.appendChild(linksTable);
|
|
|
|
// position
|
|
this.overlay.setPosition(coordinate);
|
|
}
|
|
}
|
|
}
|
|
|
|
// https://openlayers.org/en/latest/apidoc/module-ol_MapBrowserEvent-MapBrowserEvent.html
|
|
SetupEventHandlers()
|
|
{
|
|
this.map.on('click', e => {
|
|
this.OnClick(e.pixel, e.coordinate, e)
|
|
});
|
|
|
|
this.map.on('pointermove', e => {
|
|
this.pendingPointerMove = e;
|
|
|
|
if (this.pointerMoveRafId == null)
|
|
{
|
|
this.pointerMoveRafId = window.requestAnimationFrame(() => {
|
|
let evt = this.pendingPointerMove;
|
|
this.pendingPointerMove = null;
|
|
this.pointerMoveRafId = null;
|
|
|
|
if (evt)
|
|
{
|
|
this.OnPointerMove(evt.pixel, evt.coordinate, evt);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
this.moveState = "stopped";
|
|
this.map.on('movestart', e => {
|
|
// console.log("move start")
|
|
this.moveState = "moving";
|
|
});
|
|
this.map.on('moveend', e => {
|
|
// console.log("move end")
|
|
this.moveState = "stopped";
|
|
});
|
|
|
|
// this.map.on('precompose', e => { console.log("precompose") });
|
|
// this.map.on('postcompose', e => { console.log("postcompose") });
|
|
// this.map.on('prerender', e => { console.log("prerender") });
|
|
// this.map.on('postrender', e => { console.log("postrender") });
|
|
// this.map.on('rendercomplete', e => {
|
|
// console.log("rendercomplete");
|
|
// });
|
|
}
|
|
|
|
MakeUrlHysplitTrajectoryBalloon()
|
|
{
|
|
return `https://www.ready.noaa.gov/hypub-bin/trajsrc.pl?trjtype=4`;
|
|
}
|
|
|
|
MakeUrlHysplitTrajectory()
|
|
{
|
|
// save a click from https://www.ready.noaa.gov/HYSPLIT_traj.php
|
|
// then https://www.ready.noaa.gov/hypub-bin/trajtype.pl
|
|
|
|
return `https://www.ready.noaa.gov/hypub-bin/trajsrc.pl`;
|
|
}
|
|
|
|
MakeUrlSuncalc(lat, lng)
|
|
{
|
|
// seems providing a date and a time will set the page to something other than
|
|
// "now," but it expects the date and time to be in the local timezone, which I
|
|
// have no way of getting (easily). Does not appear to support UTC.
|
|
|
|
let mapZoom = 5;
|
|
return `https://suncalc.org/#/${lat},${lng},${mapZoom}/null/null/null/null`;
|
|
}
|
|
|
|
MakeUrlWindyRain(lat, lng)
|
|
{
|
|
return `https://windy.com/?rain,${lat},${lng},5,d:picker`;
|
|
}
|
|
|
|
MakeUrlWindyCloudtop(lat, lng)
|
|
{
|
|
return `https://windy.com/?cloudtop,${lat},${lng},5,d:picker`;
|
|
}
|
|
|
|
MakeUrlWindyWind(lat, lng, altM)
|
|
{
|
|
let altLabelList = [
|
|
[0, "surface"],
|
|
[100, "100m"],
|
|
[600, "950h"],
|
|
[750, "925h"],
|
|
[900, "900h"],
|
|
[1500, "850h"],
|
|
[2000, "800h"],
|
|
[3000, "700h"],
|
|
[4200, "600h"],
|
|
[5500, "500h"],
|
|
[7000, "400h"],
|
|
[9000, "300h"],
|
|
[10000, "250h"],
|
|
[11700, "200h"],
|
|
[13500, "150h"],
|
|
];
|
|
|
|
if (altM < 0) { altM = 0; }
|
|
|
|
// determine the correct elevation for the map
|
|
let labelUse = null;
|
|
for (let [alt, label] of altLabelList)
|
|
{
|
|
// console.log(`Checking ${altM} against ${alt}, ${label}`);
|
|
|
|
if (altM >= alt)
|
|
{
|
|
labelUse = label;
|
|
|
|
// console.log(`using ${labelUse} for now`);
|
|
}
|
|
}
|
|
// console.log(`using ${labelUse} final`);
|
|
|
|
// force at least a single decimal place or the page doesn't drop a pin correctly
|
|
let latStr = lat.toFixed(9);
|
|
let lngStr = lng.toFixed(9);
|
|
|
|
return `https://windy.com/?wind,${labelUse},${latStr},${lngStr},5,d:picker`;
|
|
}
|
|
|
|
SetShowRxState(state)
|
|
{
|
|
this.showRxState = state;
|
|
this.rxFeatureListKeyLast = null;
|
|
|
|
this.HandleSeen(this.spotListLast);
|
|
}
|
|
|
|
HandleSeen(spotList)
|
|
{
|
|
if (this.showRxState == "frozen") { return ; }
|
|
|
|
let source = this.rxLayer.getSource();
|
|
if (this.showRxState == "disabled")
|
|
{
|
|
source.clear(true);
|
|
this.rxFeatureListKeyLast = null;
|
|
return;
|
|
}
|
|
|
|
// decide which rx to show depending on state
|
|
let spotListUse = [];
|
|
if (this.showRxState == "default")
|
|
{
|
|
if (spotList.length)
|
|
{
|
|
spotListUse = [spotList.at(-1)];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
spotListUse = spotList;
|
|
}
|
|
|
|
let featureListKey = this.showRxState + "|" + spotListUse.map(spot => spot.GetDTLocal()).join("|");
|
|
if (featureListKey == this.rxFeatureListKeyLast)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let featureList = [];
|
|
for (const spot of spotListUse)
|
|
{
|
|
featureList.push(... this.GetRxFeatureListForSpot(spot));
|
|
}
|
|
|
|
source.clear(true);
|
|
if (featureList.length)
|
|
{
|
|
source.addFeatures(featureList);
|
|
}
|
|
|
|
this.rxFeatureListKeyLast = featureListKey;
|
|
}
|
|
|
|
GetRxFeatureListForSpot(spot)
|
|
{
|
|
if (this.spot__rxFeatureList.has(spot))
|
|
{
|
|
return this.spot__rxFeatureList.get(spot);
|
|
}
|
|
|
|
let featureList = [];
|
|
for (const seenData of spot.GetSeenDataList())
|
|
{
|
|
let pointSeen = new ol.geom.Point(ol.proj.fromLonLat([seenData.lng, seenData.lat]));
|
|
|
|
let featureSeen = new ol.Feature({
|
|
geometry: pointSeen,
|
|
});
|
|
|
|
featureSeen.setStyle(this.rxStyleSeen);
|
|
featureSeen.set("type", "rx");
|
|
featureSeen.set("spot", spot);
|
|
|
|
featureList.push(featureSeen);
|
|
}
|
|
|
|
this.spot__rxFeatureList.set(spot, featureList);
|
|
|
|
return featureList;
|
|
}
|
|
|
|
// function to pre-process spots such that a heat map can be constructed.
|
|
// goal is to:
|
|
// - break all spots down into grid4 locations
|
|
// - sum up all the confirmed spots in each grid 4
|
|
// - or take max?
|
|
// - determine the max grid
|
|
// - use this as the "top" value by which all others are scaled
|
|
// - this data is used to supply the heat map weight (0-1) with a relative
|
|
// order
|
|
// - heat should avoid giving a metric to every spot in a grid, it'll sum up
|
|
// to be too high
|
|
// - instead use the "middle"?
|
|
HeatMapHandleData(spotList)
|
|
{
|
|
this.grid4__data = new Map();
|
|
|
|
// group all spots by grid4
|
|
for (const spot of spotList)
|
|
{
|
|
let grid4 = spot.GetGrid4();
|
|
|
|
if (this.grid4__data.has(grid4) == false)
|
|
{
|
|
this.grid4__data.set(grid4, {
|
|
spotList: [],
|
|
maxHeard: 0,
|
|
});
|
|
}
|
|
|
|
let data = this.grid4__data.get(grid4);
|
|
|
|
data.spotList.push(spot);
|
|
}
|
|
|
|
// find the max per-grid and global grid max
|
|
this.maxHeardGlobal = 0;
|
|
this.grid4__data.forEach((data, grid4, map) => {
|
|
console.log(`grid4 ${grid4}`)
|
|
|
|
for (const spot of data.spotList)
|
|
{
|
|
let heard = spot.GetSeenDataList().length;
|
|
|
|
console.log(` dt ${spot.GetDTLocal()} heard ${heard}`);
|
|
|
|
if (heard > data.maxHeard)
|
|
{
|
|
console.log(` that's a new grid max`)
|
|
data.maxHeard = heard;
|
|
}
|
|
|
|
if (heard > this.maxHeardGlobal)
|
|
{
|
|
console.log(` and a new global max`)
|
|
this.maxHeardGlobal = heard;
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`global max: ${this.maxHeardGlobal}`)
|
|
}
|
|
|
|
SetSpotList(spotList, options = {})
|
|
{
|
|
const preserveView = options.preserveView ?? false;
|
|
|
|
this.t.Reset();
|
|
this.t.Event("SpotMap::SetSpotList Start");
|
|
|
|
// this.HeatMapHandleData(spotList);
|
|
|
|
// draw first so spots overlap
|
|
this.HandleSeen(spotList);
|
|
|
|
// clear old spot features
|
|
if (this.dataSetPreviously == true)
|
|
{
|
|
let FnCount = (thing) => {
|
|
let count = 0;
|
|
|
|
thing.forEachFeature(t => {
|
|
++count;
|
|
});
|
|
|
|
return count;
|
|
};
|
|
|
|
// console.log(`clearing ${FnCount(this.spotLayer.getSource())} features`)
|
|
this.spotLayer.getSource().clear(true);
|
|
// this.hmLayer.getSource().clear(true);
|
|
}
|
|
|
|
this.spotFeatureList = [];
|
|
this.lineFeatureList = [];
|
|
this.lineFeatureListByEndIdx = [];
|
|
this.spotOpacityList = [];
|
|
this.lineOpacityByEndIdx = [];
|
|
this.hoverStartIdxBySpotIdx = [];
|
|
this.currentHoverWindow = null;
|
|
this.currentHoverSpotDt = null;
|
|
this.spotDt__idx.clear();
|
|
this.UpdateHoverInfoUi(null);
|
|
|
|
// add points
|
|
for (let idx = 0; idx < spotList.length; ++idx)
|
|
{
|
|
const spot = spotList[idx];
|
|
let point = new ol.geom.Point(spot.GetLoc());
|
|
|
|
let feature = new ol.Feature({
|
|
geometry: point,
|
|
});
|
|
|
|
feature.setStyle(this.GetSpotStyle(spot.GetAccuracy(), 1.0));
|
|
|
|
feature.set("type", "spot");
|
|
feature.set("spot", spot);
|
|
feature.set("spotIndex", idx);
|
|
|
|
this.spotLayer.getSource().addFeature(feature);
|
|
this.spotFeatureList.push(feature);
|
|
this.spotOpacityList[idx] = 1.0;
|
|
this.spotDt__idx.set(spot.GetDTLocal(), idx);
|
|
}
|
|
|
|
if (this.hoverEmphasisEnabled)
|
|
{
|
|
this.PrecomputeHoverEmphasisData();
|
|
}
|
|
|
|
// // heat map driven off of grid4
|
|
// for (const grid4 of this.grid4__data.keys())
|
|
// {
|
|
// let [lat, lng] = WSPREncoded.DecodeMaidenheadToDeg(grid4);
|
|
|
|
// let point = new ol.geom.Point(ol.proj.fromLonLat([lng, lat]));
|
|
|
|
// let feature = new ol.Feature({
|
|
// geometry: point,
|
|
// });
|
|
|
|
// feature.set("type", "grid4");
|
|
// feature.set("grid4", grid4);
|
|
|
|
// // heat map shows from which locations you're heard the best
|
|
// this.hmLayer.getSource().addFeature(feature);
|
|
// }
|
|
|
|
// cache data about spots
|
|
for (const spot of spotList)
|
|
{
|
|
this.dt__data.set(spot.GetDTLocal(), {
|
|
spot: spot,
|
|
});
|
|
}
|
|
|
|
// add lines
|
|
if (spotList.length > 1 && this.showLines)
|
|
{
|
|
// get latLngList from spots
|
|
let latLngList = [];
|
|
for (const spot of spotList)
|
|
{
|
|
latLngList.push([spot.GetLat(), spot.GetLng()]);
|
|
}
|
|
|
|
// do special processing to draw lines, which avoids the 180/-180 boundary issue
|
|
let lineSegmentList = this.MakeLineSegmentList(latLngList);
|
|
|
|
// plot it
|
|
for (const { lineString, spotIndexA, spotIndexB } of lineSegmentList)
|
|
{
|
|
// turn into a line
|
|
let feature = new ol.Feature({
|
|
geometry: lineString,
|
|
});
|
|
|
|
feature.set("spotIndexA", spotIndexA);
|
|
feature.set("spotIndexB", spotIndexB);
|
|
feature.setStyle(this.GetLineStyle(1.0));
|
|
|
|
this.spotLayer.getSource().addFeature(feature);
|
|
this.lineFeatureList.push(feature);
|
|
if (this.lineFeatureListByEndIdx[spotIndexB] == undefined)
|
|
{
|
|
this.lineFeatureListByEndIdx[spotIndexB] = [];
|
|
}
|
|
this.lineFeatureListByEndIdx[spotIndexB].push(feature);
|
|
this.lineOpacityByEndIdx[spotIndexB] = 1.0;
|
|
}
|
|
}
|
|
|
|
this.ApplyHoverEmphasis();
|
|
|
|
// keep the map load from being so sudden
|
|
Animation.FadeOpacityUp(this.ui);
|
|
|
|
if (spotList.length == 0)
|
|
{
|
|
// do nothing, prior spots cleared, we're just a blank map now
|
|
}
|
|
else if (this.dataSetPreviously == true)
|
|
{
|
|
if (!preserveView)
|
|
{
|
|
// leave center and zoom as it was previously
|
|
let spotLatest = spotList.at(-1);
|
|
|
|
if (this.autoMove)
|
|
{
|
|
// smoothly pan to the new location
|
|
let view = this.map.getView();
|
|
view.animate({
|
|
center: spotLatest.GetLoc(),
|
|
duration: 500,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// center map on latest
|
|
let spotLatest = spotList.at(-1);
|
|
this.map.getView().setCenter(spotLatest.GetLoc());
|
|
this.map.getView().setZoom(4);
|
|
}
|
|
|
|
this.dataSetPreviously = true;
|
|
|
|
this.spotListLast = spotList;
|
|
|
|
// Always ring the latest spot with a red circle.
|
|
this.UnHighlightLatest();
|
|
this.HighlightLatest();
|
|
|
|
this.t.Event("SpotMap::SetSpotList End");
|
|
}
|
|
|
|
FocusOn(ts)
|
|
{
|
|
// hopefully find the spot based on time right away
|
|
let data = this.dt__data.get(ts);
|
|
let spot = null;
|
|
|
|
// console.log(`FocusOn ${ts}`)
|
|
|
|
if (data)
|
|
{
|
|
// console.log(`found immediately`)
|
|
spot = data.spot;
|
|
}
|
|
else
|
|
{
|
|
// console.log(`hunting for it`)
|
|
// we don't have that time, find the spot that is closest in time
|
|
let tsDiffMin = null;
|
|
for (let [keyTs, valueData] of this.dt__data)
|
|
{
|
|
let spotTmp = valueData.spot;
|
|
let tsDiff = Math.abs(utl.MsDiff(keyTs, ts));
|
|
|
|
// console.log(`${keyTs} - ${ts} = ${tsDiff}`)
|
|
|
|
if (tsDiffMin == null || tsDiff < tsDiffMin)
|
|
{
|
|
tsDiffMin = tsDiff;
|
|
|
|
// console.log(`new spot`)
|
|
|
|
spot = spotTmp;
|
|
}
|
|
}
|
|
|
|
// overwrite the time now that we have a specific spot to focus on
|
|
if (tsDiffMin)
|
|
{
|
|
ts = spot.GetDTLocal();
|
|
}
|
|
}
|
|
|
|
// work out where on the screen this spot is
|
|
let pixel = this.map.getPixelFromCoordinate(spot.GetLoc());
|
|
|
|
// if it is out of the screen, things don't seem to work correctly,
|
|
// so zoom out so much that everything is on the screen
|
|
let [pixX, pixY] = pixel;
|
|
let [mapWidth, mapHeight] = this.map.getSize();
|
|
if (pixX < 0 || pixX > mapWidth || pixY < 0 || pixY > mapHeight)
|
|
{
|
|
// console.log(`have to move the screen`)
|
|
this.map.getView().setCenter(spot.GetLoc());
|
|
this.map.getView().setZoom(1);
|
|
}
|
|
|
|
// async complete the rest after the map has a chance to do stuff for
|
|
// potentially zooming out
|
|
setTimeout(() => {
|
|
let pixel = this.map.getPixelFromCoordinate(spot.GetLoc());
|
|
|
|
// now that we can see the feature, we use a pixel to point, but now
|
|
// need to figure out which specific feature is the one we're
|
|
// looking for, since many can be "at" the same pixel
|
|
let f = null;
|
|
let tsDiffMin = null;
|
|
this.map.forEachFeatureAtPixel(pixel, (feature, layer) => {
|
|
let fSpot = feature.get("spot");
|
|
if (fSpot)
|
|
{
|
|
let tsDiff = Math.abs(utl.MsDiff(ts, fSpot.GetDTLocal()));
|
|
|
|
// console.log(`${ts} - ${fSpot.GetDTLocal()} = ${tsDiff}`)
|
|
|
|
if (tsDiffMin == null || tsDiff < tsDiffMin)
|
|
{
|
|
tsDiffMin = tsDiff;
|
|
|
|
// console.log(`new feature`)
|
|
f = feature;
|
|
}
|
|
}
|
|
});
|
|
|
|
// console.log(`done looking at features`)
|
|
|
|
let coordinate = null;
|
|
if (f)
|
|
{
|
|
let g = f.getGeometry();
|
|
let c = g.getCoordinates();
|
|
coordinate = c;
|
|
}
|
|
|
|
this.map.dispatchEvent({
|
|
type: 'click',
|
|
pixel: pixel,
|
|
pixel_: pixel,
|
|
dragging: false,
|
|
coordinate: coordinate,
|
|
coordinate_: coordinate,
|
|
originalEvent: {},
|
|
dragging: false,
|
|
map: this.map,
|
|
spot: spot,
|
|
});
|
|
}, 50);
|
|
}
|
|
|
|
HighlightLatest()
|
|
{
|
|
this.focusFeature = null;
|
|
|
|
if (this.spotListLast.length != 0)
|
|
{
|
|
let spot = this.spotListLast[this.spotListLast.length - 1];
|
|
|
|
let style = new ol.style.Style({
|
|
image: new ol.style.Circle({
|
|
radius: 8,
|
|
fill: new ol.style.Fill({
|
|
color: 'rgba(255, 255, 255, 0.1)',
|
|
}),
|
|
stroke: new ol.style.Stroke({
|
|
color: 'rgba(255, 0, 0, 1)',
|
|
width: 2.0,
|
|
}),
|
|
}),
|
|
});
|
|
|
|
let point = new ol.geom.Point(spot.GetLoc());
|
|
let feature = new ol.Feature({
|
|
geometry: point,
|
|
});
|
|
feature.setStyle(style);
|
|
feature.set("type", "spot");
|
|
feature.set("spot", spot);
|
|
this.spotLayer.getSource().addFeature(feature);
|
|
|
|
this.focusFeature = feature;
|
|
}
|
|
}
|
|
|
|
UnHighlightLatest()
|
|
{
|
|
if (this.focusFeature)
|
|
{
|
|
this.spotLayer.getSource().removeFeature(this.focusFeature);
|
|
|
|
this.focusFeature = null;
|
|
}
|
|
}
|
|
|
|
MakeLineSegmentList(latLngList)
|
|
{
|
|
let lineSegmentList = [];
|
|
|
|
function CloseToWrap(lng)
|
|
{
|
|
return (180 - Math.abs(lng)) < 30;
|
|
}
|
|
|
|
function ToEzLat(lat)
|
|
{
|
|
return (lat < 0) ? (90 + -lat) : lat;
|
|
}
|
|
|
|
function FromEzLat(latEz)
|
|
{
|
|
return (latEz > 90) ? (-latEz + 90) : latEz;
|
|
}
|
|
|
|
function CalcCrossingLat(latA, lngA, latB, lngB)
|
|
{
|
|
let latAEz = ToEzLat(latA);
|
|
let latBEz = ToEzLat(latB);
|
|
let latCrossEz = latAEz;
|
|
|
|
if (lngA > 0 && lngB < 0)
|
|
{
|
|
let lngToMark = 180 - lngA;
|
|
let markToLng = lngB - -180;
|
|
let dx = lngToMark + markToLng;
|
|
let dy = Math.abs(latBEz - latAEz);
|
|
|
|
if (dx == 0)
|
|
{
|
|
latCrossEz = latAEz;
|
|
}
|
|
else if (latAEz < latBEz)
|
|
{
|
|
let dc = Math.sqrt((dx ** 2) + (dy ** 2));
|
|
let a = lngToMark;
|
|
let c = lngToMark / dx * dc;
|
|
let b = Math.sqrt(Math.max(0, (c ** 2) - (a ** 2)));
|
|
latCrossEz = latAEz + b;
|
|
}
|
|
else if (latAEz > latBEz)
|
|
{
|
|
let dc = Math.sqrt((dx ** 2) + (dy ** 2));
|
|
let a = lngToMark;
|
|
let c = lngToMark / dx * dc;
|
|
let b = Math.sqrt(Math.max(0, (c ** 2) - (a ** 2)));
|
|
latCrossEz = latAEz - b;
|
|
}
|
|
}
|
|
else if (lngA < 0 && lngB > 0)
|
|
{
|
|
let lngToMark = 180 - lngB;
|
|
let markToLng = lngA - -180;
|
|
let dx = lngToMark + markToLng;
|
|
let dy = Math.abs(latBEz - latAEz);
|
|
|
|
if (dx == 0)
|
|
{
|
|
latCrossEz = latAEz;
|
|
}
|
|
else if (latAEz < latBEz)
|
|
{
|
|
let dc = Math.sqrt((dx ** 2) + (dy ** 2));
|
|
let a = lngToMark;
|
|
let c = lngToMark / dx * dc;
|
|
let b = Math.sqrt(Math.max(0, (c ** 2) - (a ** 2)));
|
|
latCrossEz = latAEz + b;
|
|
}
|
|
else if (latAEz > latBEz)
|
|
{
|
|
let dc = Math.sqrt((dx ** 2) + (dy ** 2));
|
|
let a = lngToMark;
|
|
let c = lngToMark / dx * dc;
|
|
let b = Math.sqrt(Math.max(0, (c ** 2) - (a ** 2)));
|
|
latCrossEz = latAEz - b;
|
|
}
|
|
}
|
|
|
|
return FromEzLat(latCrossEz);
|
|
}
|
|
|
|
for (let i = 1; i < latLngList.length; ++i)
|
|
{
|
|
let [latA, lngA] = latLngList[i - 1];
|
|
let [latB, lngB] = latLngList[i];
|
|
|
|
let crossesPosToNeg = lngA > 0 && lngB < 0 && (CloseToWrap(lngA) || CloseToWrap(lngB));
|
|
let crossesNegToPos = lngA < 0 && lngB > 0 && (CloseToWrap(lngA) || CloseToWrap(lngB));
|
|
|
|
if (crossesPosToNeg || crossesNegToPos)
|
|
{
|
|
let latCross = CalcCrossingLat(latA, lngA, latB, lngB);
|
|
|
|
let breakA = crossesPosToNeg ? [180, latCross] : [-180, latCross];
|
|
let breakB = crossesPosToNeg ? [-180, latCross] : [180, latCross];
|
|
|
|
lineSegmentList.push({
|
|
lineString: new ol.geom.LineString([
|
|
ol.proj.fromLonLat([lngA, latA]),
|
|
ol.proj.fromLonLat(breakA),
|
|
]),
|
|
spotIndexA: i - 1,
|
|
spotIndexB: i,
|
|
});
|
|
lineSegmentList.push({
|
|
lineString: new ol.geom.LineString([
|
|
ol.proj.fromLonLat(breakB),
|
|
ol.proj.fromLonLat([lngB, latB]),
|
|
]),
|
|
spotIndexA: i - 1,
|
|
spotIndexB: i,
|
|
});
|
|
}
|
|
else
|
|
{
|
|
lineSegmentList.push({
|
|
lineString: new ol.geom.LineString([
|
|
ol.proj.fromLonLat([lngA, latA]),
|
|
ol.proj.fromLonLat([lngB, latB]),
|
|
]),
|
|
spotIndexA: i - 1,
|
|
spotIndexB: i,
|
|
});
|
|
}
|
|
}
|
|
|
|
return lineSegmentList;
|
|
}
|
|
|
|
// lng( 179.5846), lat(40.7089) => lng(19991266.226313718), lat(4969498.835332252)
|
|
// lng(-176.8324), lat(41.7089) => lng(-19684892.723752473), lat(5117473.325588154)
|
|
MakeLineStringList(latLngList)
|
|
{
|
|
let locListList = [[]];
|
|
|
|
function CloseToWrap(lng)
|
|
{
|
|
// if you're within x degrees of the wraparound, let's assume
|
|
// this is the case we're dealing with (not the wrap over europe)
|
|
return (180 - Math.abs(lng)) < 30;
|
|
}
|
|
|
|
let latLast;
|
|
let lngLast;
|
|
for (let i = 0; i < latLngList.length; ++i)
|
|
{
|
|
let [lat, lng] = latLngList[i];
|
|
|
|
// only check subsequent points to see if they cross the 180/-180 longitude
|
|
if (i)
|
|
{
|
|
if (lngLast > 0 && lng < 0 && (CloseToWrap(lngLast) || CloseToWrap(lng)))
|
|
{
|
|
// oops, it happened going from +180 to -180
|
|
|
|
// let's convert latitude to easier to math numbers
|
|
// latitude is 90 at the poles, converges to zero at the
|
|
// equator.
|
|
// the south is depicted as having a negative latitude.
|
|
// so let's call it 0 (north pole) to 180 (south pole)
|
|
|
|
let latEz = (lat < 0) ? (90 + -lat) : lat;
|
|
let latLastEz = (latLast < 0) ? (90 + -latLast) : latLast;
|
|
|
|
// want to determine crossover point
|
|
let latCrossoverEz;
|
|
|
|
// let's look at whether travel toward north or south
|
|
if (latLastEz < latEz)
|
|
{
|
|
// example: 20m, chan 65, VE3OCL, 2023-04-25 to 2023-04-26
|
|
|
|
// moving north, interpolate
|
|
// let's model a giant triangle from last pos to this pos
|
|
|
|
// measure horizontal distance
|
|
let lngToMark = 180 - lngLast;
|
|
let markToLng = lng - -180;
|
|
let dx = lngToMark + markToLng;
|
|
|
|
// measure vertical distance
|
|
let dy = latEz - latLastEz;
|
|
|
|
// calculate big triangle hypotenuse
|
|
let dc = Math.sqrt((dx ** 2) + (dy ** 2));
|
|
|
|
// now we can calculate the portion of the big triangle
|
|
// that the meridian slices off, which itself is a triangle
|
|
// on the left side.
|
|
|
|
// horizontal distance is lngToMark
|
|
let a = lngToMark;
|
|
|
|
// the small hypotenuse is the same percent of its length
|
|
// as the length to the mark is of the entire distance
|
|
let c = lngToMark / dx * dc;
|
|
|
|
// now reverse the Pythagorean theorem
|
|
let b = Math.sqrt(Math.sqrt(c) - Math.sqrt(a));
|
|
|
|
// ok that's our crossover point
|
|
latCrossoverEz = latLastEz + b;
|
|
}
|
|
else if (latLastEz == latEz)
|
|
{
|
|
// you know the lat
|
|
latCrossoverEz = latEz;
|
|
}
|
|
else if (latLastEz > latEz)
|
|
{
|
|
// example: 20m, chan 99, VE3KCL, 2023-04-30 to 2023-04-31
|
|
|
|
// moving south, interpolate
|
|
// let's model a giant triangle from last pos to this pos
|
|
|
|
// measure horizontal distance
|
|
let lngToMark = 180 - lngLast;
|
|
let markToLng = lng - -180;
|
|
let dx = lngToMark + markToLng;
|
|
|
|
// measure vertical distance
|
|
let dy = latLastEz - latEz;
|
|
|
|
// calculate big triangle hypotenuse
|
|
let dc = Math.sqrt((dx ** 2) + (dy ** 2));
|
|
|
|
// now we can calculate the portion of the big triangle
|
|
// that the meridian slices off, which itself is a triangle
|
|
// on the left side.
|
|
|
|
// horizontal distance is lngToMark
|
|
let a = lngToMark;
|
|
|
|
// the small hypotenuse is the same percent of its length
|
|
// as the length to the mark is of the entire distance
|
|
let c = lngToMark / dx * dc;
|
|
|
|
// now reverse the Pythagorean theorem
|
|
let b = Math.sqrt(Math.sqrt(c) - Math.sqrt(a));
|
|
|
|
// ok that's our crossover point
|
|
latCrossoverEz = latLastEz - b;
|
|
}
|
|
|
|
// convert ez back to real latitude
|
|
let latCrossover = (latCrossoverEz > 90) ? (-latCrossoverEz + 90) : latCrossoverEz;
|
|
|
|
// now break this point into two, one which gets from the prior
|
|
// point to the break, and then from the break to this point.
|
|
// put in the right (opposite) order for conversion
|
|
let one = [180, latCrossover];
|
|
let two = [-180, latCrossover];
|
|
let three = [lng, lat];
|
|
|
|
locListList.at(-1).push(ol.proj.fromLonLat(one));
|
|
locListList.push([]);
|
|
locListList.at(-1).push(ol.proj.fromLonLat(two));
|
|
locListList.at(-1).push(ol.proj.fromLonLat(three));
|
|
}
|
|
else if (lngLast < 0 && lng > 0 && (CloseToWrap(lngLast) || CloseToWrap(lng)))
|
|
{
|
|
// oops, it happened going from -180 to +180
|
|
|
|
// let's convert latitude to easier to math numbers
|
|
// latitude is 90 at the poles, converges to zero at the
|
|
// equator.
|
|
// the south is depicted as having a negative latitude.
|
|
// so let's call it 0 (north pole) to 180 (south pole)
|
|
|
|
let latEz = (lat < 0) ? (90 + -lat) : lat;
|
|
let latLastEz = (latLast < 0) ? (90 + -latLast) : latLast;
|
|
|
|
// want to determine crossover point
|
|
let latCrossoverEz;
|
|
|
|
// let's look at whether travel toward north or south
|
|
if (latLastEz < latEz)
|
|
{
|
|
// example: 20m, chan 99, VE3CKL, 2023-03-12 to 2023-03-12
|
|
|
|
// moving north, interpolate
|
|
// let's model a giant triangle from last pos to this pos
|
|
|
|
// measure horizontal distance
|
|
let lngToMark = 180 - lng;
|
|
let markToLng = lngLast - -180;
|
|
let dx = lngToMark + markToLng;
|
|
|
|
// measure vertical distance
|
|
let dy = latEz - latLastEz;
|
|
|
|
// calculate big triangle hypotenuse
|
|
let dc = Math.sqrt((dx ** 2) + (dy ** 2));
|
|
|
|
// now we can calculate the portion of the big triangle
|
|
// that the meridian slices off, which itself is a triangle
|
|
// on the left side.
|
|
|
|
// horizontal distance is lngToMark
|
|
let a = lngToMark;
|
|
|
|
// the small hypotenuse is the same percent of its length
|
|
// as the length to the mark is of the entire distance
|
|
let c = lngToMark / dx * dc;
|
|
|
|
// now reverse the Pythagorean theorem
|
|
let b = Math.sqrt(Math.sqrt(c) - Math.sqrt(a));
|
|
|
|
// ok that's our crossover point
|
|
latCrossoverEz = latLastEz + b;
|
|
}
|
|
else if (latLastEz == latEz)
|
|
{
|
|
// you know the lat
|
|
latCrossoverEz = latEz;
|
|
}
|
|
else if (latLastEz > latEz)
|
|
{
|
|
// example: ??
|
|
|
|
// moving south, interpolate
|
|
// let's model a giant triangle from last pos to this pos
|
|
|
|
// measure horizontal distance
|
|
let lngToMark = 180 - lng;
|
|
let markToLng = lngLast - -180;
|
|
let dx = lngToMark + markToLng;
|
|
|
|
// measure vertical distance
|
|
let dy = latLastEz - latEz;
|
|
|
|
// calculate big triangle hypotenuse
|
|
let dc = Math.sqrt((dx ** 2) + (dy ** 2));
|
|
|
|
// now we can calculate the portion of the big triangle
|
|
// that the meridian slices off, which itself is a triangle
|
|
// on the left side.
|
|
|
|
// horizontal distance is lngToMark
|
|
let a = lngToMark;
|
|
|
|
// the small hypotenuse is the same percent of its length
|
|
// as the length to the mark is of the entire distance
|
|
let c = lngToMark / dx * dc;
|
|
|
|
// now reverse the Pythagorean theorem
|
|
let b = Math.sqrt(Math.sqrt(c) - Math.sqrt(a));
|
|
|
|
// ok that's our crossover point
|
|
latCrossoverEz = latLastEz - b;
|
|
}
|
|
|
|
// convert ez back to real latitude
|
|
let latCrossover = (latCrossoverEz > 90) ? (-latCrossoverEz + 90) : latCrossoverEz;
|
|
|
|
// now break this point into two, one which gets from the prior
|
|
// point to the break, and then from the break to this point.
|
|
// put in the right (opposite) order for conversion
|
|
let one = [-180, latCrossover];
|
|
let two = [180, latCrossover];
|
|
let three = [lng, lat];
|
|
|
|
locListList.at(-1).push(ol.proj.fromLonLat(one));
|
|
locListList.push([]);
|
|
locListList.at(-1).push(ol.proj.fromLonLat(two));
|
|
locListList.at(-1).push(ol.proj.fromLonLat(three));
|
|
}
|
|
else
|
|
{
|
|
locListList.at(-1).push(ol.proj.fromLonLat([lng, lat]));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
locListList.at(-1).push(ol.proj.fromLonLat([lng, lat]));
|
|
}
|
|
|
|
latLast = lat;
|
|
lngLast = lng;
|
|
}
|
|
|
|
// convert locListList to LineStringList
|
|
let lineStringList = [];
|
|
for (let locList of locListList)
|
|
{
|
|
lineStringList.push(new ol.geom.LineString(locList));
|
|
}
|
|
|
|
return lineStringList
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|