1
0
mirror of https://github.com/f4exb/sdrangel.git synced 2026-01-07 00:39:25 -05:00
Jon Beniston 9c7aa8b333 Map Updates
Allow OpenSkyNetwork DB, OpenAIP and OurAirports DB stuctures to be
shared by different plugins, to speed up loading.
Perform map anti-aliasing on the whole map, rather than just info boxes,
to improve rendering speed when there are many items. Add map
multisampling as a preference.
Add plotting of airspaces, airports, navaids on Map feature.
Add support for polylines and polygons to be plotted on Map feature.
Add support for images to 2D Map feature.
Add distance and name filters to Map feature.
Filter map items when zoomed out or if off screen, to improve rendering
performance.
Add UK DAB, FM and AM transmitters to Map feature.
Use labelless maps for 2D transmit maps in Map feature (same as in ADS-B
demod).
2023-02-14 14:46:08 +00:00

514 lines
23 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="/Cesium/Cesium.js"></script>
<style>
@import url(/Cesium/Widgets/widgets.css);
html,
body,
#cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
</head>
<body style="margin:0;padding:0">
<div id="cesiumContainer"></div>
<script>
// See: https://community.cesium.com/t/how-to-run-an-animation-for-an-entity-model/16932
function getActiveAnimations(viewer, entity) {
var primitives = viewer.scene.primitives;
var length = primitives.length;
for(var i = 0; i < length; i++) {
var primitive = primitives.get(i);
if (primitive.id === entity && primitive instanceof Cesium.Model && primitive.ready) {
return primitive.activeAnimations;
}
}
return undefined;
}
function playAnimation(viewer, command, retries) {
var entity = czmlStream.entities.getById(command.id);
if (entity !== undefined) {
var animations = getActiveAnimations(viewer, entity);
if (animations !== undefined) {
try {
let options = {
name: command.animation,
startOffset: command.startOffset,
reverse: command.reverse,
loop: command.loop ? Cesium.ModelAnimationLoop.REPEAT : Cesium.ModelAnimationLoop.NONE,
multiplier: command.multiplier,
};
options.startTime = Cesium.JulianDate.fromIso8601(command.startDateTime);
// https://github.com/CesiumGS/cesium/issues/10048
// Animations aren't moved to last frame if startTime in the past
// so just play now, in order to ensure gears are down, etc
if (Cesium.JulianDate.compare(options.startTime, viewer.clock.currentTime) < 0) {
options.startTime = viewer.clock.currentTime;
}
if (command.duration != 0) {
options.stopTime = Cesium.JulianDate.addSeconds(options.startTime, command.duration, new Cesium.JulianDate());
}
animations.add(options);
} catch (e) {
// Note we get TypeError instead of DeveloperError, if running minified version of Cesium
if ((e instanceof Cesium.DeveloperError) || (e instanceof TypeError)) {
// ADS-B plugin doesn't know which animations each aircraft has
// so we should expect a lot of these, as it tries to start slat animations
// on aircraft that do not have them
console.log(`Exception playing ${command.animation} for ${command.id}\n${e}`);
} else {
throw e;
}
}
} else {
// Give Entity time to create primitive
// No ready promise in entity API - https://github.com/CesiumGS/cesium/issues/4727
if (retries > 0) {
setTimeout(function() {
//console.log(`Retrying animation for entity ${command.id}`);
playAnimation(viewer, command, retries-1);
}, 1000);
} else {
console.log(`Gave up trying to play animation for entity ${command.id}`);
}
}
} else {
// It seems in some cases, entities aren't created immediately, so wait and retry
if (retries > 0) {
setTimeout(function() {
//console.log(`Retrying entity ${command.id}`);
playAnimation(viewer, command, retries-1);
}, 1000);
} else {
console.log(`Gave up trying to find entity ${command.id}`);
}
}
}
// There's no way to stop a looped animation that doesn't have a stopTime,
// only remove it
// So we need to remove it, then re-add it with a new stopTime, so that it
// plays again if the timeline is changed
function stopAnimation(viewer, command) {
var entity = czmlStream.entities.getById(command.id);
if (entity !== undefined) {
var animations = getActiveAnimations(viewer, entity);
if (animations !== undefined) {
var length = animations.length;
var anim = undefined;
// Find animation with lastet startTime
for (var i = 0; i < length; i++) {
var a = animations.get(i);
if (a.name == command.animation) {
if ((anim === undefined) || (Cesium.JulianDate.compare(a.startTime, anim.startTime) >= 0)) {
anim = a;
}
}
}
if (anim !== undefined) {
animations.remove(anim);
// Re add with new stopTime
animations.add({
name: anim.name,
startOffset: anim.startOffset,
reverse: anim.reverse,
loop: anim.loop,
multiplier: anim.multiplier,
startTime: anim.startTime,
stopTime: Cesium.JulianDate.fromIso8601(command.startDateTime)
});
}
}
}
}
function icrf(scene, time) {
if (scene.mode !== Cesium.SceneMode.SCENE3D) {
return;
}
var icrfToFixed = Cesium.Transforms.computeIcrfToFixedMatrix(time);
if (Cesium.defined(icrfToFixed)) {
var camera = viewer.camera;
var offset = Cesium.Cartesian3.clone(camera.position);
var transform = Cesium.Matrix4.fromRotationTranslation(icrfToFixed);
camera.lookAtTransform(transform, offset);
}
}
// Polygons (such as for airspaces) should be prioritized behind other entities
function pickEntityPrioritized(e)
{
var picked = viewer.scene.drillPick(e.position);
if (Cesium.defined(picked)) {
var firstPolygon = null;
for (let i = 0; i < picked.length; i++) {
var id = Cesium.defaultValue(picked[i].id, picked[i].primitive.id);
if (id instanceof Cesium.Entity) {
if (!Cesium.defined(id.polygon)) {
return id;
} else if (firstPolygon == null) {
firstPolygon = id;
}
}
}
return firstPolygon;
}
}
function pickEntity(e) {
viewer.selectedEntity = pickEntityPrioritized(e);
}
Cesium.Ion.defaultAccessToken = '$CESIUM_ION_API_KEY$';
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain(),
animation: true,
shouldAnimate: true,
timeline: true,
geocoder: false,
fullscreenButton: true,
navigationHelpButton: false,
navigationInstructionsInitiallyVisible: false,
terrainProviderViewModels: [] // User should adjust terrain via dialog, so depthTestAgainstTerrain doesn't get set
});
viewer.scene.globe.depthTestAgainstTerrain = false; // So labels/points aren't clipped by terrain
viewer.screenSpaceEventHandler.setInputAction(pickEntity, Cesium.ScreenSpaceEventType.LEFT_CLICK);
var buildings = undefined;
const images = new Map();
var mufGeoJSONStream = null;
var foF2GeoJSONStream = null;
// Generate HTML for MUF contour info box from properties in GeoJSON
function describeMUF(properties, nameProperty) {
let html = "";
if (properties.hasOwnProperty("level-value")) {
const value = properties["level-value"];
if (Cesium.defined(value)) {
html = `<p>MUF: ${value} MHz<p>MUF (Maximum Usable Frequency) is the highest frequency that will reflect from the ionosphere on a 3000km path`;
}
}
return html;
}
// Generate HTML for foF2 contour info box from properties in GeoJSON
function describefoF2(properties, nameProperty) {
let html = "";
if (properties.hasOwnProperty("level-value")) {
const value = properties["level-value"];
if (Cesium.defined(value)) {
html = `<p>foF2: ${value} MHz<p>foF2 (F2 region critical frequency) is the highest frequency that will be reflected vertically from the F2 ionosphere region`;
}
}
return html;
}
// Use CZML to stream data from Map plugin to Cesium
var czmlStream = new Cesium.CzmlDataSource();
viewer.dataSources.add(czmlStream);
function cameraLight(scene, time) {
viewer.scene.light.direction = Cesium.Cartesian3.clone(scene.camera.directionWC, viewer.scene.light.direction);
}
// Use WebSockets for handling commands from MapPlugin
// (CZML doesn't support camera control, for example)
// and sending events back to it
let socket = new WebSocket("ws://127.0.0.1:$WS_PORT$");
socket.onmessage = function(event) {
try {
const command = JSON.parse(event.data);
if (command.command == "trackId") {
// Track an entity with the given ID
viewer.trackedEntity = czmlStream.entities.getById(command.id);
} else if (command.command == "setHomeView") {
// Set the viewing rectangle used when the home button is pressed
Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees(
command.longitude - command.angle,
command.latitude - command.angle,
command.longitude + command.angle,
command.latitude + command.angle
);
Cesium.Camera.DEFAULT_VIEW_FACTOR = 0.0;
viewer.camera.flyHome(0);
} else if (command.command == "setView") {
// Set the camera view
viewer.scene.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(command.longitude, command.latitude, command.altitude),
orientation: {
heading: 0,
},
});
} else if (command.command == "playAnimation") {
// Play model animation
if (command.stop) {
//console.log(`stopping animation ${command.animation} for ${command.id}`);
stopAnimation(viewer, command);
} else {
//console.log(`playing animation ${command.animation} for ${command.id}`);
playAnimation(viewer, command, 30);
}
} else if (command.command == "setDateTime") {
// Set current date and time of viewer
var dateTime = Cesium.JulianDate.fromIso8601(command.dateTime);
viewer.clock.currentTime = dateTime;
} else if (command.command == "getDateTime") {
// Get current date and time of viewer
reportClock();
} else if (command.command == "setTerrain") {
// Support using Ellipsoid terrain for performance and also
// because paths can't be clammped to ground, so AIS paths
// currently appear underground if terrain is used
if (command.provider == "Ellipsoid") {
if (!(viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider)) {
viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();
}
} else if (command.provider == "Cesium World Terrain") {
viewer.terrainProvider = Cesium.createWorldTerrain();
} else if (command.provider == "CesiumTerrainProvider") {
viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
url: command.url
});
} else if (command.provider == "ArcGISTiledElevationTerrainProvider") {
viewer.terrainProvider = new Cesium.ArcGISTiledElevationTerrainProvider({
url: command.url
});
} else {
console.log(`Unknown terrain ${command.terrain}`);
}
viewer.scene.globe.depthTestAgainstTerrain = false; // So labels/points aren't clipped by terrain
} else if (command.command == "setBuildings") {
if (command.buildings == "None") {
if (buildings !== undefined) {
viewer.scene.primitives.remove(buildings);
buildings = undefined;
}
} else {
if (buildings === undefined) {
buildings = viewer.scene.primitives.add(Cesium.createOsmBuildings());
}
}
} else if (command.command == "setSunLight") {
// Enable illumination of the globe from the direction of the Sun or camera
viewer.scene.globe.enableLighting = command.useSunLight;
viewer.scene.globe.nightFadeOutDistance = 0.0;
if (!command.useSunLight) {
viewer.scene.light = new Cesium.DirectionalLight({
direction : new Cesium.Cartesian3(1, 0, 0)
});
viewer.scene.preRender.addEventListener(cameraLight);
} else {
viewer.scene.light = new Cesium.SunLight();
viewer.scene.preRender.removeEventListener(cameraLight);
}
} else if (command.command == "setCameraReferenceFrame") {
if (command.eci) {
viewer.scene.postUpdate.addEventListener(icrf);
} else {
viewer.scene.postUpdate.removeEventListener(icrf);
}
} else if (command.command == "setAntiAliasing") {
if (command.antiAliasing == "FXAA") {
viewer.scene.postProcessStages.fxaa.enabled = true;
} else {
viewer.scene.postProcessStages.fxaa.enabled = false;
}
} else if (command.command == "showMUF") {
if (mufGeoJSONStream != null) {
viewer.dataSources.remove(mufGeoJSONStream, true);
mufGeoJSONStream = null;
}
if (command.show == true) {
viewer.dataSources.add(
Cesium.GeoJsonDataSource.load(
"muf.geojson",
{describe: describeMUF}
)
).then(function(dataSource) {mufGeoJSONStream = dataSource; });
}
} else if (command.command == "showfoF2") {
if (foF2GeoJSONStream != null) {
viewer.dataSources.remove(foF2GeoJSONStream, true);
foF2GeoJSONStream = null;
}
if (command.show == true) {
viewer.dataSources.add(
Cesium.GeoJsonDataSource.load(
"fof2.geojson",
{describe: describefoF2}
)
).then(function(dataSource) {foF2GeoJSONStream = dataSource; });
}
} else if (command.command == "updateImage") {
// Textures on entities can flash white when changed: https://github.com/CesiumGS/cesium/issues/1640
// so we use a primitive instead of an entity
// Can't modify geometry of primitives, so need to create a new primitive each time
// Material needs to be set as translucent in order to allow camera to zoom through it
var oldImage = images.get(command.name);
var image = viewer.scene.primitives.add(new Cesium.Primitive({
geometryInstances : new Cesium.GeometryInstance({
geometry : new Cesium.RectangleGeometry({
rectangle : Cesium.Rectangle.fromDegrees(command.west, command.south, command.east, command.north),
vertexFormat : Cesium.EllipsoidSurfaceAppearance.VERTEX_FORMAT,
height: command.altitude
})
}),
appearance : new Cesium.EllipsoidSurfaceAppearance({
aboveGround : false,
material: new Cesium.Material({
fabric: {
type: 'Image',
uniforms: {
image: command.data,
}
},
translucent: true
})
})
}));
images.set(command.name, image);
if (oldImage !== undefined) {
image.readyPromise.then(function(prim) {
viewer.scene.primitives.remove(oldImage);
});
}
} else if (command.command == "removeImage") {
var image = images.get(command.name);
if (image !== undefined) {
viewer.scene.primitives.remove(image);
} else {
console.log(`Can't find image ${command.name} to remove it`);
}
} else if (command.command == "removeAllImages") {
for (let [k,image] of images) {
viewer.scene.primitives.remove(image);
}
} else if (command.command == "removeAllCZMLEntities") {
czmlStream.entities.removeAll();
} else if (command.command == "czml") {
// Implement CLIP_TO_GROUND, to work around https://github.com/CesiumGS/cesium/issues/4049
if (command.hasOwnProperty('altitudeReference') && command.hasOwnProperty('position') && command.position.hasOwnProperty('cartographicDegrees')) {
var size = command.position.cartographicDegrees.length;
if ((size == 3) || (size == 4)) {
var position;
var height;
if (size == 3) {
position = Cesium.Cartographic.fromDegrees(command.position.cartographicDegrees[0], command.position.cartographicDegrees[1]);
height = command.position.cartographicDegrees[2];
} else if (size == 4) {
position = Cesium.Cartographic.fromDegrees(command.position.cartographicDegrees[1], command.position.cartographicDegrees[2]);
height = command.position.cartographicDegrees[3];
}
if (viewer.terrainProvider instanceof Cesium.EllipsoidTerrainProvider) {
// sampleTerrainMostDetailed will reject Ellipsoid.
if (height < 0) {
if (size == 3) {
command.position.cartographicDegrees[2] = 0;
} else if (size == 4) {
command.position.cartographicDegrees[3] = 0;
}
}
czmlStream.process(command);
} else {
var promise = Cesium.sampleTerrainMostDetailed(viewer.terrainProvider, [position]);
Cesium.when(promise, function(updatedPositions) {
if (height < updatedPositions[0].height) {
if (size == 3) {
command.position.cartographicDegrees[2] = updatedPositions[0].height;
} else if (size == 4) {
command.position.cartographicDegrees[3] = updatedPositions[0].height;
}
}
czmlStream.process(command);
}, function() {
console.log(`Terrain doesn't support sampleTerrainMostDetailed`);
czmlStream.process(command);
});
};
} else {
console.log(`Can't currently use altitudeReference when more than one position`);
czmlStream.process(command);
}
} else {
czmlStream.process(command);
}
} else {
console.log(`Unknown command ${command.command}`);
}
} catch(e) {
console.log(`Erroring processing received message:\n${e}\n${event.data}`);
}
};
viewer.selectedEntityChanged.addEventListener(function(selectedEntity) {
if (Cesium.defined(selectedEntity) && Cesium.defined(selectedEntity.id)) {
socket.send(JSON.stringify({event: "selected", id: selectedEntity.id}));
} else {
socket.send(JSON.stringify({event: "selected"}));
}
});
viewer.trackedEntityChanged.addEventListener(function(trackedEntity) {
if (Cesium.defined(trackedEntity) && Cesium.defined(trackedEntity.id)) {
socket.send(JSON.stringify({event: "tracking", id: trackedEntity.id}));
} else {
socket.send(JSON.stringify({event: "tracking"}));
}
});
// Report clock changes for use by other plugins
var systemTime = new Cesium.JulianDate();
function reportClock() {
if (socket.readyState === 1) {
Cesium.JulianDate.now(systemTime);
socket.send(JSON.stringify({
event: "clock",
canAnimate: viewer.clock.canAnimate,
shouldAnimate: viewer.clock.shouldAnimate,
currentTime: Cesium.JulianDate.toIso8601(viewer.clock.currentTime),
multiplier: viewer.clock.multiplier,
systemTime: Cesium.JulianDate.toIso8601(systemTime)
}));
}
};
Cesium.knockout.getObservable(viewer.clockViewModel, 'shouldAnimate').subscribe(function(isAnimating) {
reportClock();
});
Cesium.knockout.getObservable(viewer.clockViewModel, 'multiplier').subscribe(function(multiplier) {
reportClock();
});
// This is called every frame
//Cesium.knockout.getObservable(viewer.clockViewModel, 'currentTime').subscribe(function(currentTime) {
//reportClock();
//});
viewer.timeline.addEventListener('settime', reportClock, false);
socket.onopen = () => {
reportClock();
};
</script>
</div>
</body>
</html>