2018-07-19 11:01:31 -04:00
|
|
|
import 'ol/ol.css';
|
|
|
|
import {Map as olMap, View} from 'ol';
|
2018-07-21 15:22:32 -04:00
|
|
|
import {Control} from 'ol/control';
|
2018-07-23 17:09:06 -04:00
|
|
|
import {Group as LayerGroup, Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
|
2018-07-19 11:01:31 -04:00
|
|
|
import {OSM, Vector as VectorSource} from 'ol/source';
|
2018-08-10 11:42:08 -04:00
|
|
|
import {GPX} from 'ol/format';
|
2018-07-19 11:01:31 -04:00
|
|
|
import Feature from 'ol/Feature';
|
|
|
|
import {fromLonLat} from 'ol/proj';
|
2018-07-23 17:09:06 -04:00
|
|
|
import {Circle as CircleStyle, Icon, Fill, Stroke, Style, Text} from 'ol/style';
|
2018-07-19 11:01:31 -04:00
|
|
|
import Projection from 'ol/proj/Projection';
|
|
|
|
import {LineString, Point} from 'ol/geom';
|
|
|
|
|
|
|
|
import {readFileSync} from 'fs';
|
2018-07-19 11:49:55 -04:00
|
|
|
const packetLog = readFileSync(__dirname + '/../IS_packets.txt', 'utf-8');
|
2018-07-19 11:01:31 -04:00
|
|
|
|
|
|
|
import {APRSParser} from 'aprs-parser';
|
|
|
|
|
|
|
|
let tile_layer = new TileLayer({source: new OSM()});
|
|
|
|
|
2018-08-10 11:42:08 -04:00
|
|
|
import route_data from 'gpx/*.gpx';
|
|
|
|
|
|
|
|
let routes = new LayerGroup({
|
|
|
|
title: "Routes",
|
|
|
|
layers: Object.keys(route_data).map(name => new VectorLayer({
|
|
|
|
title: name.replace(/_/g, " "),
|
|
|
|
source: new VectorSource({
|
|
|
|
url: route_data[name],
|
|
|
|
format: new GPX()
|
|
|
|
}),
|
|
|
|
style: new Style({
|
|
|
|
stroke: new Stroke(
|
|
|
|
{color: 'hsl(200, 90%, 30%)', width: 5}
|
|
|
|
)})
|
|
|
|
}))
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2018-07-19 11:01:31 -04:00
|
|
|
let map = new olMap({
|
|
|
|
target: 'map',
|
|
|
|
layers: [
|
2018-08-10 11:42:08 -04:00
|
|
|
tile_layer,
|
|
|
|
routes
|
2018-07-19 11:01:31 -04:00
|
|
|
],
|
|
|
|
view: new View({
|
|
|
|
center: fromLonLat([-72.15, 43.90]),
|
|
|
|
zoom: 10
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
2018-07-31 15:56:50 -04:00
|
|
|
class ColorGenerator {
|
|
|
|
constructor(count) {
|
|
|
|
let mult = Math.floor(360 / count);
|
|
|
|
this.hues = Array.from(Array(count).keys()).map(x => x * mult);
|
|
|
|
|
|
|
|
// Shuffle (this is not a great shuffle, but I don't care)
|
|
|
|
this.hues.forEach((current, index, arr) => {
|
|
|
|
let randomIndex = Math.floor(Math.random() * index);
|
|
|
|
[arr[index], arr[randomIndex]] = [arr[randomIndex], arr[index]];
|
|
|
|
});
|
|
|
|
}
|
|
|
|
get() {
|
2018-07-19 11:01:31 -04:00
|
|
|
return this.hues.pop();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-08-04 19:10:22 -04:00
|
|
|
function transformGeometry(geometry) {
|
|
|
|
return geometry.transform(new Projection({code: "EPSG:4326"}),
|
|
|
|
tile_layer.getSource().getProjection());
|
2018-07-19 11:49:55 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
function plotPaths(packets) {
|
2018-08-04 19:10:22 -04:00
|
|
|
let path_layers = new LayerGroup({title: "Station Paths"});
|
|
|
|
map.addLayer(path_layers);
|
2018-07-19 13:18:41 -04:00
|
|
|
|
2018-08-04 19:10:22 -04:00
|
|
|
let paths = packets
|
|
|
|
.filter(packet => packet.date > new Date("2018-07-14") && packet.date < new Date("2018-07-15"))
|
|
|
|
// filter to just positional data
|
|
|
|
.filter(packet => 'data' in packet && 'latitude' in packet.data)
|
|
|
|
// join into Arrays of points
|
|
|
|
.reduce((acc, packet) => {
|
2018-08-09 20:47:37 -04:00
|
|
|
let callsign = packet.from.toString().trim();
|
|
|
|
if (!acc.has(callsign)) acc.set(callsign, []);
|
2018-08-10 17:25:55 -04:00
|
|
|
acc.get(callsign).push([packet.data.longitude, packet.data.latitude, packet]);
|
2018-08-04 19:10:22 -04:00
|
|
|
return acc;
|
|
|
|
}, new Map());
|
2018-07-19 11:01:31 -04:00
|
|
|
|
2018-08-04 19:10:22 -04:00
|
|
|
let colorGen = new ColorGenerator(paths.size);
|
|
|
|
|
|
|
|
// plot on map
|
|
|
|
paths.forEach((points, callsign) => {
|
2018-08-10 17:25:55 -04:00
|
|
|
let color = 'hsl(' + colorGen.get() + ', 75%, 50%)';
|
2018-08-04 19:10:22 -04:00
|
|
|
let path_layer = new VectorLayer({
|
2018-08-10 17:25:55 -04:00
|
|
|
title: "Path",
|
2018-08-04 19:10:22 -04:00
|
|
|
source: new VectorSource({features: [
|
2018-08-10 17:25:55 -04:00
|
|
|
new Feature({geometry: transformGeometry(new LineString(points))})
|
2018-08-04 19:10:22 -04:00
|
|
|
]}),
|
2018-08-10 17:25:55 -04:00
|
|
|
style: new Style({stroke: new Stroke({color: color, width: 2})})
|
|
|
|
});
|
|
|
|
|
|
|
|
let points_layer = new VectorLayer({
|
|
|
|
title: "Points",
|
|
|
|
source: new VectorSource({
|
|
|
|
features: points.map(point => new Feature({
|
|
|
|
geometry: transformGeometry(new Point(point)),
|
|
|
|
packet: point[3] // TODO: this seems a bit bad
|
|
|
|
}))
|
|
|
|
}),
|
2018-08-04 19:10:22 -04:00
|
|
|
style: new Style({
|
2018-08-10 17:25:55 -04:00
|
|
|
image: new CircleStyle({
|
|
|
|
radius: 4,
|
|
|
|
fill: new Fill({color: color})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let callsign_layer = new LayerGroup({
|
|
|
|
title: callsign,
|
|
|
|
layers: [path_layer, points_layer]
|
2018-07-19 11:49:55 -04:00
|
|
|
});
|
2018-08-10 17:25:55 -04:00
|
|
|
path_layers.getLayers().push(callsign_layer);
|
2018-08-04 19:10:22 -04:00
|
|
|
});
|
2018-07-19 11:49:55 -04:00
|
|
|
}
|
|
|
|
|
2018-07-21 12:07:23 -04:00
|
|
|
function plotPacketPaths(packets) {
|
|
|
|
function pathToString(path) {
|
|
|
|
return path
|
|
|
|
.filter(station => !station.call.match(/WIDE[12]|qA?|UV[123]|.*\*$|UNCAN/))
|
|
|
|
.map(station => station.toString().trim().replace(/\*$/, ""));
|
|
|
|
}
|
|
|
|
|
|
|
|
let digiCalls =
|
|
|
|
new Set(packets
|
|
|
|
.map(packet => pathToString(packet.via))
|
|
|
|
.reduce((acc, stations) => acc.concat(stations)));
|
|
|
|
|
|
|
|
let digiPos = packets
|
|
|
|
// filter to digis
|
|
|
|
.filter(packet => digiCalls.has(packet.from.toString().trim()))
|
|
|
|
// filter to just positional data
|
|
|
|
.filter(packet => 'data' in packet && 'latitude' in packet.data)
|
|
|
|
// convert to callsign -> position mapping
|
|
|
|
.reduce((stations, packet) =>
|
|
|
|
stations.set(packet.from.toString().trim(),
|
|
|
|
[packet.data.longitude, packet.data.latitude]),
|
|
|
|
new Map());
|
|
|
|
|
2018-07-31 15:56:50 -04:00
|
|
|
let colorGen = new ColorGenerator(digiPos.size);
|
|
|
|
let colorMap = Array.from(digiPos.keys()).reduce(
|
|
|
|
(acc, callsign, index) =>
|
|
|
|
acc.set(callsign, colorGen.hues[index]), new Map());
|
2018-07-21 12:07:23 -04:00
|
|
|
|
|
|
|
// plot digis
|
|
|
|
// TODO: icons
|
|
|
|
let digi_style = new Style({
|
|
|
|
image: new CircleStyle({
|
|
|
|
radius: 5,
|
|
|
|
fill: new Fill({color: 'grey'})
|
|
|
|
}),
|
|
|
|
text: new Text({
|
|
|
|
font: 'bold 11px "Open Sans", "Arial Unicode MS", "sans-serif"',
|
|
|
|
overflow: true,
|
|
|
|
fill: new Fill({color: 'black'})
|
|
|
|
})
|
|
|
|
});
|
|
|
|
let digi_layer = new VectorLayer({
|
2018-07-21 15:22:32 -04:00
|
|
|
title: "Digipeater Labels",
|
2018-07-21 12:07:23 -04:00
|
|
|
zIndex: 1, // TODO: probably not the best way to do this
|
2018-08-10 12:59:41 -04:00
|
|
|
source: new VectorSource({
|
|
|
|
features: Array.from(digiPos.keys()).map(callsign =>
|
|
|
|
new Feature({
|
|
|
|
geometry: transformGeometry(new Point(digiPos.get(callsign))),
|
|
|
|
callsign: callsign
|
|
|
|
})
|
|
|
|
)}),
|
2018-07-21 12:07:23 -04:00
|
|
|
style: feature => {
|
2018-07-31 16:08:06 -04:00
|
|
|
digi_style.setText(new Text({
|
|
|
|
text: feature.get('callsign'),
|
|
|
|
offsetY: 12
|
|
|
|
}));
|
2018-07-21 12:07:23 -04:00
|
|
|
return digi_style;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
map.addLayer(digi_layer);
|
|
|
|
|
2018-07-23 17:09:06 -04:00
|
|
|
let packet_path_layers = new LayerGroup({title: "Packet Paths"});
|
|
|
|
let layers_map = new Map();
|
|
|
|
map.addLayer(packet_path_layers);
|
2018-07-21 12:07:23 -04:00
|
|
|
packets
|
|
|
|
.filter(packet => packet.date > new Date("2018-07-14") && packet.date < new Date("2018-07-15"))
|
|
|
|
// filter by callsign
|
|
|
|
//.filter(packet => packet.from.toString() === "W1HS-9")
|
|
|
|
// filter to just positional data
|
|
|
|
.filter(packet => 'data' in packet && 'latitude' in packet.data)
|
|
|
|
.forEach(packet => {
|
|
|
|
pathToString(packet.via)
|
|
|
|
.forEach((station, index, stations) => {
|
|
|
|
if (digiPos.get(station) === undefined) {
|
|
|
|
console.log(station);
|
|
|
|
}
|
2018-07-23 17:09:06 -04:00
|
|
|
|
|
|
|
// first point in path is originating station
|
|
|
|
let previous = (index === 0) ?
|
|
|
|
[packet.data.longitude, packet.data.latitude] :
|
|
|
|
digiPos.get(stations[index - 1]) || [0, 0];
|
2018-07-21 12:07:23 -04:00
|
|
|
|
2018-08-04 19:11:03 -04:00
|
|
|
let pathFeature = new Feature(transformGeometry(
|
|
|
|
new LineString([previous, digiPos.get(station) || [0, 0]])));
|
2018-07-21 12:07:23 -04:00
|
|
|
|
|
|
|
// TODO: want to color per station that hears it, probably means
|
|
|
|
// making a lot more features
|
|
|
|
let color = colorMap.get(station);
|
2018-08-04 19:11:03 -04:00
|
|
|
pathFeature.setStyle(new Style({
|
|
|
|
stroke: new Stroke(
|
2018-07-21 12:07:23 -04:00
|
|
|
{color: 'hsl(' + color + ', 60%, 60%)', width: 2}
|
|
|
|
)}));
|
|
|
|
|
2018-07-23 17:09:06 -04:00
|
|
|
if (!layers_map.has(station)) {
|
|
|
|
layers_map.set(station, new VectorLayer({
|
|
|
|
title: station,
|
|
|
|
source: new VectorSource(),
|
2018-08-04 19:11:03 -04:00
|
|
|
renderMode: 'image'
|
2018-07-23 17:09:06 -04:00
|
|
|
}));
|
|
|
|
packet_path_layers.getLayers().push(layers_map.get(station));
|
|
|
|
}
|
|
|
|
layers_map.get(station).getSource().addFeature(pathFeature);
|
2018-07-21 12:07:23 -04:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-08-10 13:38:14 -04:00
|
|
|
function layer_toggle(layer) {
|
2018-07-23 17:09:06 -04:00
|
|
|
if (layer.toggle_element === undefined) {
|
2018-08-10 13:38:14 -04:00
|
|
|
layer.toggle_element = document.createElement('label');
|
2018-07-23 17:09:06 -04:00
|
|
|
|
|
|
|
let checkbox = layer.toggle_element.appendChild(
|
|
|
|
document.createElement('input'));
|
|
|
|
checkbox.type = "checkbox";
|
|
|
|
checkbox.checked = layer.getVisible();
|
|
|
|
checkbox.addEventListener('change', event => {
|
|
|
|
layer.setVisible(event.target.checked);
|
|
|
|
});
|
|
|
|
layer.toggle_element.appendChild(
|
|
|
|
document.createTextNode(layer.get('title')));
|
|
|
|
}
|
|
|
|
return layer.toggle_element;
|
|
|
|
}
|
|
|
|
|
2018-08-10 13:38:14 -04:00
|
|
|
function layer_toggles(layer, parentElement) {
|
|
|
|
if (layer instanceof LayerGroup) {
|
|
|
|
if (layer.group_toggle === undefined) {
|
|
|
|
let label = layer.group_toggle = parentElement.appendChild(
|
|
|
|
document.createElement('label'));
|
|
|
|
let input = label.appendChild(document.createElement('input'));
|
|
|
|
input.type = 'checkbox';
|
|
|
|
input.className = "expand";
|
|
|
|
label.appendChild(document.createElement('span'));
|
|
|
|
|
|
|
|
label.appendChild(layer_toggle(layer)); // whole LayerGroup
|
|
|
|
|
|
|
|
let child_toggle_element = label.appendChild(
|
|
|
|
document.createElement('input'));
|
|
|
|
child_toggle_element.type = 'checkbox';
|
|
|
|
child_toggle_element.checked = true;
|
|
|
|
child_toggle_element.addEventListener('change', event => {
|
|
|
|
layer.getLayers().forEach(subLayer => {
|
|
|
|
subLayer.setVisible(event.target.checked);
|
|
|
|
subLayer.toggle_element.querySelector("input").checked =
|
|
|
|
event.target.checked;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
let container = label.appendChild(document.createElement('div'));
|
|
|
|
container.className = 'collapsible-content';
|
|
|
|
layer.getLayers().forEach(
|
|
|
|
subLayer => layer_toggles(subLayer, container));
|
|
|
|
return label;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
parentElement.appendChild(layer_toggle(layer));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-10 11:45:22 -04:00
|
|
|
function render_layer_toggles(event, element) {
|
2018-07-21 15:22:32 -04:00
|
|
|
event.map.getLayers().getArray()
|
|
|
|
.filter(layer => layer.get('title') !== undefined)
|
2018-08-10 13:38:14 -04:00
|
|
|
.forEach(layer => layer_toggles(layer, element));
|
2018-07-21 15:22:32 -04:00
|
|
|
}
|
2018-07-23 17:09:06 -04:00
|
|
|
|
2018-08-10 11:45:22 -04:00
|
|
|
(function makeLayerToogleControl() {
|
|
|
|
let element = document.createElement('div');
|
|
|
|
element.className = 'layer-toggles ol-unselectable ol-control';
|
|
|
|
let inner = element.appendChild(document.createElement('div'));
|
|
|
|
|
|
|
|
let control = new Control(
|
|
|
|
{element: element,
|
|
|
|
render: event => render_layer_toggles(event, inner)});
|
|
|
|
map.addControl(control);
|
|
|
|
})();
|
2018-07-21 15:22:32 -04:00
|
|
|
|
|
|
|
|
2018-07-19 13:18:41 -04:00
|
|
|
function parsePackets(packetLog) {
|
|
|
|
let parser = new APRSParser();
|
|
|
|
return packetLog.trim().split("\n")
|
|
|
|
// parse to Date and APRS packet
|
|
|
|
.map(line => {
|
|
|
|
let packet = parser.parse(line.slice(29));
|
|
|
|
packet.date = new Date(line.slice(0,18));
|
|
|
|
return packet;
|
|
|
|
});
|
|
|
|
}
|
2018-07-19 11:01:31 -04:00
|
|
|
|
2018-07-19 13:18:41 -04:00
|
|
|
let packets = parsePackets(packetLog);
|
2018-07-19 11:49:55 -04:00
|
|
|
plotPaths(packets);
|
2018-07-21 12:07:23 -04:00
|
|
|
plotPacketPaths(packets);
|