kopia lustrzana https://github.com/projecthorus/chasemapper
332 wiersze
13 KiB
HTML
332 wiersze
13 KiB
HTML
<!DOCTYPE HTML>
|
|
<html>
|
|
<head>
|
|
<title>Project Horus Chase Mapper</title>
|
|
|
|
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/leaflet.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/leaflet-sidebar.min.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/leaflet-control-topcenter.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/Leaflet.PolylineMeasure.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/leaflet-routing-machine.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/easy-button.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/tabulator_simple.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/jquery-ui.css') }}" rel="stylesheet">
|
|
<link href="{{ url_for('static', filename='css/chasemapper.css') }}" rel="stylesheet">
|
|
|
|
|
|
<!-- I should probably feel bad for using so many libraries, but apparently this is the way thing are done :-/ -->
|
|
<script src="{{ url_for('static', filename='js/jquery-3.3.1.min.js')}}"></script>
|
|
<script src="{{ url_for('static', filename='js/jquery-ui.min.js')}}"></script>
|
|
<script src="{{ url_for('static', filename='js/socket.io.min.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/leaflet.js') }}"></script>
|
|
<!-- Leaflet plugins... -->
|
|
<script src="{{ url_for('static', filename='js/leaflet.rotatedMarker.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/leaflet-control-topcenter.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/leaflet-sidebar.min.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/Leaflet.Control.Custom.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/Leaflet.PolylineMeasure.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/easy-button.js') }}"></script>
|
|
|
|
|
|
|
|
<!-- Custom scripts -->
|
|
|
|
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
|
|
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
|
|
<script type="text/javascript" charset="utf-8">
|
|
|
|
|
|
// Chase car position.
|
|
// properties will contain:
|
|
// latest_data: [lat,lon, alt] (latest car position)
|
|
// heading: Car heading (to point icon appropriately.)
|
|
// marker: Leaflet marker
|
|
var chase_car_position = {latest_data: [0.0,0.0,0.0], heading:0, marker: 'NONE', path: 'NONE'};
|
|
|
|
var current_bearing = 0;
|
|
|
|
var bearing_length = 20000;
|
|
|
|
// Leaflet map instance.
|
|
var map;
|
|
|
|
|
|
|
|
// Socket.IO Settings
|
|
var namespace = '/chasemapper';
|
|
// Socket.IO instance.
|
|
var socket;
|
|
|
|
$(document).ready(function() {
|
|
|
|
// Connect to the Socket.IO server.
|
|
// The connection URL has the following format:
|
|
// http[s]://<domain>:<port>[/<namespace>]
|
|
socket = io.connect(location.protocol + '//' + document.domain + ':' + location.port + namespace);
|
|
|
|
// Grab the System config on startup.
|
|
// Refer to config.py for the contents of the configuration blob.
|
|
$.ajax({
|
|
url: "/get_config",
|
|
dataType: 'json',
|
|
async: false, // Yes, this is deprecated...
|
|
success: function(data) {
|
|
serverSettingsUpdate(data);
|
|
}
|
|
});
|
|
|
|
//
|
|
// LEAFLET MAP SETUP
|
|
//
|
|
// Setup a basic Leaflet map
|
|
map = L.map('map').setView([chase_config.default_lat, chase_config.default_lon], 12);
|
|
|
|
// Add OSM Map Layer
|
|
var osm_map = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: ''
|
|
}).addTo(map);
|
|
|
|
// Add OSM Topo Map Layer
|
|
var osm_topo_map = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
|
attribution: ''
|
|
});
|
|
|
|
// Add ESRI Satellite Map layers.
|
|
var esrimapLink =
|
|
'<a href="http://www.esri.com/">Esri</a>';
|
|
var esriwholink =
|
|
'i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community';
|
|
var esri_sat_map = L.tileLayer(
|
|
'http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
|
{
|
|
attribution: '',
|
|
maxZoom: 18,
|
|
});
|
|
// Dark Matter map layer, one of the maps suitable for 'dark mode'
|
|
var dark_matter_map = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png', {
|
|
attribution: ''
|
|
});
|
|
|
|
|
|
|
|
var map_layers = {'OSM':osm_map, 'OpenTopo':osm_topo_map, 'ESRI Satellite':esri_sat_map, 'Dark Matter':dark_matter_map};
|
|
|
|
// Add ThunderForest layers, if we have a key provided.
|
|
if (chase_config.thunderforest_api_key !== 'none'){
|
|
// Thunderforest Outdoors layer.
|
|
var thunderforest_outdoors = L.tileLayer('https://tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey='+chase_config.thunderforest_api_key,
|
|
{
|
|
attribution: ''
|
|
});
|
|
map_layers['Outdoors (Terrain)'] = thunderforest_outdoors;
|
|
|
|
}
|
|
|
|
if (chase_config.stadia_api_key !== 'none'){
|
|
// Stadia Alidade Smooth Dark Layer.
|
|
var alidade_smooth_dark = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png?apikey='+chase_config.stadia_api_key,
|
|
{
|
|
attribution: ''
|
|
});
|
|
map_layers['Alidade Smooth Dark'] = alidade_smooth_dark;
|
|
|
|
}
|
|
|
|
// Add Offline map layers, if we have any.
|
|
for (var i = 0, len = chase_config.offline_tile_layers.length; i < len; i++) {
|
|
var _layer_name = chase_config.offline_tile_layers[i];
|
|
map_layers['Offline - ' + _layer_name] = L.tileLayer(location.protocol + '//' + document.domain + ':' + location.port + '/tiles/'+_layer_name+'/{z}/{x}/{y}.png');
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add layer selection control (top right).
|
|
map.addControl(new L.Control.Layers(map_layers));
|
|
|
|
|
|
// Add custom controls, which show various sets of data.
|
|
|
|
|
|
L.easyButton('fa-car', function(btn, map){
|
|
map.panTo(chase_car_position.latest_data);
|
|
}, 'Follow Chase Car', 'followCarButton', {
|
|
position: 'topright'
|
|
}
|
|
).addTo(map);
|
|
|
|
map.zoomControl.setPosition('topright');
|
|
|
|
// Chase Car Speed Display
|
|
L.control.custom({
|
|
position: 'bottomcenter',
|
|
content : "<div class='dataAgeHeader' id='bearing_header'></div><div id='bearing_data' class='bearingData'></div>",
|
|
classes : 'btn-group-vertical btn-group-sm',
|
|
id: 'bearing_display',
|
|
style :
|
|
{
|
|
margin: '5px',
|
|
padding: '0px 0 0 0',
|
|
cursor: 'pointer',
|
|
}
|
|
})
|
|
.addTo(map);
|
|
|
|
|
|
chase_car_position.marker = L.marker(chase_car_position.latest_data,{title:"Chase Car", icon: carIcon, rotationOrigin: "center center"})
|
|
.addTo(map);
|
|
|
|
var bearing_path = L.polyline([chase_car_position.latest_data],{title:"Bearing", color:'red', weight:2.0});
|
|
bearing_path.addTo(map);
|
|
|
|
|
|
function calculateDestination(latlng, heading, distance) {
|
|
heading = (heading + 360) % 360;
|
|
var rad = Math.PI / 180,
|
|
radInv = 180 / Math.PI,
|
|
R = 6378137, // approximation of Earth's radius
|
|
lon1 = latlng.lng * rad,
|
|
lat1 = latlng.lat * rad,
|
|
rheading = heading * rad,
|
|
sinLat1 = Math.sin(lat1),
|
|
cosLat1 = Math.cos(lat1),
|
|
cosDistR = Math.cos(distance / R),
|
|
sinDistR = Math.sin(distance / R),
|
|
lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 *
|
|
sinDistR * Math.cos(rheading)),
|
|
lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR *
|
|
cosLat1, cosDistR - sinLat1 * Math.sin(lat2));
|
|
lon2 = lon2 * radInv;
|
|
lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2;
|
|
return L.latLng([lat2 * radInv, lon2]);
|
|
}
|
|
|
|
function updateBearing(){
|
|
var _end = calculateDestination(L.latLng(chase_car_position.latest_data), current_bearing, bearing_length);
|
|
|
|
bearing_path.setLatLngs([L.latLng(chase_car_position.latest_data), _end]);
|
|
|
|
$("#bearing_data").text(current_bearing.toFixed(0));
|
|
}
|
|
|
|
function modulus(x, m){
|
|
return (x % m + m) % m;
|
|
}
|
|
|
|
// Controls to change bearings
|
|
L.easyButton('<', function(btn, map){
|
|
current_bearing = modulus((current_bearing-1),360.0);
|
|
updateBearing();
|
|
}, 'Bearing CCW 1 degrees', 'bearingCCW1Deg', {
|
|
position: 'bottomleft'
|
|
}).addTo(map);
|
|
L.easyButton('<<', function(btn, map){
|
|
current_bearing = modulus((current_bearing-5),360.0);
|
|
updateBearing();
|
|
}, 'Bearing CCW 5 degrees', 'bearingCCW5Deg', {
|
|
position: 'bottomleft'
|
|
}
|
|
).addTo(map);
|
|
L.easyButton('<<<', function(btn, map){
|
|
current_bearing = modulus((current_bearing-10),360.0);
|
|
updateBearing();
|
|
}, 'Bearing CCW 10 degrees', 'bearingCCW10Deg', {
|
|
position: 'bottomleft'
|
|
}
|
|
).addTo(map);
|
|
|
|
|
|
L.easyButton('>', function(btn, map){
|
|
current_bearing = modulus((current_bearing+1),360.0);
|
|
updateBearing();
|
|
}, 'Bearing CW 10 degrees', 'bearingCW1Deg', {
|
|
position: 'bottomright'
|
|
}
|
|
).addTo(map);
|
|
|
|
L.easyButton('>>', function(btn, map){
|
|
current_bearing = modulus((current_bearing+5),360.0);
|
|
updateBearing();
|
|
}, 'Bearing CW 5 degrees', 'bearingCW5Deg', {
|
|
position: 'bottomright'
|
|
}
|
|
).addTo(map);
|
|
|
|
L.easyButton('>>>', function(btn, map){
|
|
current_bearing = modulus((current_bearing+10),360.0);
|
|
updateBearing();
|
|
}, 'Bearing CW 10 degrees', 'bearingCW10Deg', {
|
|
position: 'bottomright'
|
|
}
|
|
).addTo(map);
|
|
|
|
// Tell the server we are connected and ready for data.
|
|
socket.on('connect', function() {
|
|
socket.emit('client_connected', {data: 'I\'m connected!'});
|
|
// This will cause the server to emit a few messages telling us to fetch data.
|
|
});
|
|
|
|
|
|
$("#bearing_data").click(function(){
|
|
// Push a bearing to the backend for display.
|
|
_bearing_info = {
|
|
'type': 'BEARING',
|
|
'bearing_type': 'absolute',
|
|
'source': 'EasyBearing',
|
|
'latitude': chase_car_position.latest_data[0],
|
|
'longitude': chase_car_position.latest_data[1],
|
|
'bearing': current_bearing
|
|
};
|
|
|
|
socket.emit('add_manual_bearing', _bearing_info);
|
|
|
|
});
|
|
|
|
|
|
function handleTelemetry(data){
|
|
// Telemetry Event messages contain a dictionary of position data.
|
|
// It should have the fields:
|
|
// callsign: string
|
|
// position: [lat, lon, alt]
|
|
// vel_v: float
|
|
// time_to_landing: String
|
|
// If callsign = 'CAR', the lat/lon/alt will be considered to be a car telemetry position.
|
|
|
|
// Handle chase car position updates.
|
|
if (data.callsign == 'CAR'){
|
|
// Update car position.
|
|
chase_car_position.latest_data = data.position;
|
|
chase_car_position.heading = data.heading; // degrees true
|
|
chase_car_position.speed = data.speed; // m/s
|
|
|
|
chase_car_position.marker.setLatLng(chase_car_position.latest_data).update();
|
|
updateBearing();
|
|
}
|
|
}
|
|
|
|
|
|
// Telemetry event handler.
|
|
// We will get one of these mesages with every new balloon position
|
|
socket.on('telemetry_event', function(data) {
|
|
handleTelemetry(data);
|
|
});
|
|
|
|
|
|
window.setInterval(function(){
|
|
// Dunno
|
|
},1000);
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="map" class="map"></div>
|
|
</body>
|
|
</html>
|