mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Merge pull request #1518 from manjunath22466/manjunath/hal-769-range-ring-visibility
fix(awareness): make the 250 km range ring readable on any ground
This commit is contained in:
6 files changed
+309
-15
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
- Draw the Global Context 250 km range ring as a 4 px line with a dark halo,
|
||||||
|
so it stays visible over desert, snow, ocean and forest and under the FLIR,
|
||||||
|
NVG and CRT styles, where the old 1 px outline disappeared (manjunath22466,
|
||||||
|
#1518).
|
||||||
@@ -4997,6 +4997,7 @@ silently demoting every later lookup for the session.
|
|||||||
- Enabled Data Layer controls report normalized feed health on the button (`LOADING`, `DEGRADED`, `STALE`, `FALLBACK`, or `UNAVAILABLE`) while the metadata line retains the source and reason. A partial CelesTrak group failure keeps the usable catalog and reports `DEGRADED`; a total outage keeps last-good catalog data visible but reports `UNAVAILABLE`.
|
- Enabled Data Layer controls report normalized feed health on the button (`LOADING`, `DEGRADED`, `STALE`, `FALLBACK`, or `UNAVAILABLE`) while the metadata line retains the source and reason. A partial CelesTrak group failure keeps the usable catalog and reports `DEGRADED`; a total outage keeps last-good catalog data visible but reports `UNAVAILABLE`.
|
||||||
- On activation it focuses the nearest currently observed aircraft across the civilian and military feeds, with military winning an exact distance tie; if none are available, it focuses an observed AIS vessel. The aircraft search is deliberately uncapped and one refresh-tick retry handles initially empty feeds. This is an attention-priority navigation shortcut, not a high-risk, affiliation, or threat classification.
|
- On activation it focuses the nearest currently observed aircraft across the civilian and military feeds, with military winning an exact distance tie; if none are available, it focuses an observed AIS vessel. The aircraft search is deliberately uncapped and one refresh-tick retry handles initially empty feeds. This is an attention-priority navigation shortcut, not a high-risk, affiliation, or threat classification.
|
||||||
- A selected aircraft, AIS vessel, or mapped installation gets a 250 km **context window** with nearby cohort counts, nearest examples, source labels, and stale/unavailable reasons. It emits `NEARBY` or `UNKNOWN`; no detection, engagement, affiliation, or sensor-activity conclusion is calculated.
|
- A selected aircraft, AIS vessel, or mapped installation gets a 250 km **context window** with nearby cohort counts, nearest examples, source labels, and stale/unavailable reasons. It emits `NEARBY` or `UNKNOWN`; no detection, engagement, affiliation, or sensor-activity conclusion is calculated.
|
||||||
|
- The window's edge is drawn on the globe as a 250 km range ring: a 4 px polyline with a 2 px Context-blue core inside a 1 px dark halo on each side, 2.5 km above the ellipsoid (`AWARENESS_RANGE_RING_STYLE` in `src/layers/awareness/policy.js`). The halo keeps it legible over bright desert and snow, dark ocean and forest, and under the FLIR, NVG and CRT styles, where the earlier 1 px ellipse outline disappeared. It is a polyline because WebGL draws ellipse outlines at 1 px whatever width is asked for. The ring follows a moving subject once it has moved 250 m.
|
||||||
- For a selected live aircraft or vessel, the context window refreshes from the existing tracker/feed position every 750 ms, so nearest distances and cohort counts follow the subject without introducing a duplicate poll loop.
|
- For a selected live aircraft or vessel, the context window refreshes from the existing tracker/feed position every 750 ms, so nearest distances and cohort counts follow the subject without introducing a duplicate poll loop.
|
||||||
- Aircraft cohort membership uses every locally loaded, selectable contact inside the 250 km window, including a plane hidden only by horizon culling or because its 3D model owns the visual. Counts and navigation therefore do not change with the current camera angle or billboard/model handoff.
|
- Aircraft cohort membership uses every locally loaded, selectable contact inside the 250 km window, including a plane hidden only by horizon culling or because its 3D model owns the visual. Counts and navigation therefore do not change with the current camera angle or billboard/model handoff.
|
||||||
- Nearby examples in the context panel are focus controls: they use their owning layer's existing selection/tracking path, then frame that contact. Static-installation distances use ellipsoidal surface distance so the count matches the ground-projected context disk.
|
- Nearby examples in the context panel are focus controls: they use their owning layer's existing selection/tracking path, then frame that contact. Static-installation distances use ellipsoidal surface distance so the count matches the ground-projected context disk.
|
||||||
|
|||||||
@@ -1223,5 +1223,6 @@
|
|||||||
"src/voice/areaActions.test.mjs",
|
"src/voice/areaActions.test.mjs",
|
||||||
"src/voice/imageryActions.test.mjs",
|
"src/voice/imageryActions.test.mjs",
|
||||||
"src/voice/osmActions.test.mjs",
|
"src/voice/osmActions.test.mjs",
|
||||||
"src/voice/realtimeProtocol.test.mjs"
|
"src/voice/realtimeProtocol.test.mjs",
|
||||||
|
"src/layers/awareness/rangeRing.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -44,6 +44,20 @@ export const AWARENESS_MAX_NAVIGATION_EXAMPLES = 10000;
|
|||||||
|
|
||||||
export const CONTEXT_RIM_HEIGHT_M = 2500;
|
export const CONTEXT_RIM_HEIGHT_M = 2500;
|
||||||
|
|
||||||
|
/** @constant The 250 km range ring around the Global Context subject.
|
||||||
|
* The old 1 px ellipse outline vanished over desert, snow and under
|
||||||
|
* the FLIR, NVG and CRT styles. A bright core inside a dark halo keeps
|
||||||
|
* luminance contrast on light and dark ground alike, and luminance is what
|
||||||
|
* the monochrome styles keep. The core is the Context panel's blue; the halo
|
||||||
|
* is the map-card ink. Widths are screen pixels; `outlineWidth` is per side. */
|
||||||
|
export const AWARENESS_RANGE_RING_STYLE = Object.freeze({
|
||||||
|
color: 'rgba(98, 181, 255, 0.95)',
|
||||||
|
outlineColor: 'rgba(4, 12, 16, 0.8)',
|
||||||
|
width: 4,
|
||||||
|
outlineWidth: 1,
|
||||||
|
segments: 128,
|
||||||
|
});
|
||||||
|
|
||||||
export const VESSEL_FOCUS_RADIUS_M = 3000;
|
export const VESSEL_FOCUS_RADIUS_M = 3000;
|
||||||
|
|
||||||
export const SOURCE_LABEL = {
|
export const SOURCE_LABEL = {
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
// src/layers/awareness/rangeRing.test.mjs — the 250 km Global Context range
|
||||||
|
// ring is a haloed polyline that stays legible on light and dark ground,
|
||||||
|
// sits where the old ellipse outline did, and follows the subject.
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import { AWARENESS_RADIUS_M } from '../../data/militaryAwarenessEngine.js';
|
||||||
|
import { AWARENESS_RANGE_RING_STYLE, CONTEXT_RIM_HEIGHT_M } from './policy.js';
|
||||||
|
import { awarenessRangeRingPositions, createRendering } from './rendering.js';
|
||||||
|
import { createState } from './state.js';
|
||||||
|
|
||||||
|
/** WCAG relative luminance of a Cesium colour, ignoring alpha. */
|
||||||
|
function luminance(color) {
|
||||||
|
const channel = (c) =>
|
||||||
|
c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||||||
|
return (
|
||||||
|
0.2126 * channel(color.red) +
|
||||||
|
0.7152 * channel(color.green) +
|
||||||
|
0.0722 * channel(color.blue)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The rendering part over a bare entity collection, counting render asks. */
|
||||||
|
function ringRendering() {
|
||||||
|
const state = createState({ services: {} });
|
||||||
|
const entities = new Cesium.EntityCollection();
|
||||||
|
state.viewer = { entities };
|
||||||
|
let renders = 0;
|
||||||
|
const rendering = createRendering({
|
||||||
|
state,
|
||||||
|
services: {
|
||||||
|
geometry: {},
|
||||||
|
render: {
|
||||||
|
governorRequestRender() {
|
||||||
|
renders += 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parts: {},
|
||||||
|
});
|
||||||
|
return { entities, rendering, renders: () => renders };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Surface distance from `center` to each ring position. */
|
||||||
|
function surfaceDistances(center, positions) {
|
||||||
|
const from = Cesium.Cartographic.fromCartesian(center);
|
||||||
|
return positions.map((position) => {
|
||||||
|
const to = Cesium.Cartographic.fromCartesian(position);
|
||||||
|
return new Cesium.EllipsoidGeodesic(from, to).surfaceDistance;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [place, lat, lon] of [
|
||||||
|
['equator', 0, 0],
|
||||||
|
['Sahara', 27, 3],
|
||||||
|
['Greenland', 72, -40],
|
||||||
|
['antimeridian', 10, 179.9],
|
||||||
|
]) {
|
||||||
|
test(`ring positions stay 250 km from the subject at ${place}`, () => {
|
||||||
|
const center = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
|
||||||
|
const positions = awarenessRangeRingPositions(
|
||||||
|
center,
|
||||||
|
AWARENESS_RADIUS_M,
|
||||||
|
CONTEXT_RIM_HEIGHT_M,
|
||||||
|
);
|
||||||
|
assert.equal(positions.length, AWARENESS_RANGE_RING_STYLE.segments + 1);
|
||||||
|
assert.ok(
|
||||||
|
Cesium.Cartesian3.equalsEpsilon(positions[0], positions.at(-1), 0, 1e-6),
|
||||||
|
'the ring closes on itself',
|
||||||
|
);
|
||||||
|
// Within 100 m of 250 km on the WGS84 ellipsoid.
|
||||||
|
for (const distance of surfaceDistances(center, positions))
|
||||||
|
assert.ok(Math.abs(distance - AWARENESS_RADIUS_M) < 100, `${distance} m`);
|
||||||
|
for (const position of positions)
|
||||||
|
assert.ok(
|
||||||
|
Math.abs(
|
||||||
|
Cesium.Cartographic.fromCartesian(position).height -
|
||||||
|
CONTEXT_RIM_HEIGHT_M,
|
||||||
|
) < 1e-3,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [place, lat, lon] of [
|
||||||
|
['the North Pole', 90, 0],
|
||||||
|
['the South Pole', -90, 0],
|
||||||
|
['1e-7° from the North Pole', 90 - 1e-7, 45],
|
||||||
|
['1e-8° from the South Pole', -90 + 1e-8, -120],
|
||||||
|
]) {
|
||||||
|
test(`the ring stays a full circle at ${place}`, () => {
|
||||||
|
const center = Cesium.Cartesian3.fromDegrees(lon, lat, 0);
|
||||||
|
const segments = AWARENESS_RANGE_RING_STYLE.segments;
|
||||||
|
const positions = awarenessRangeRingPositions(
|
||||||
|
center,
|
||||||
|
AWARENESS_RADIUS_M,
|
||||||
|
CONTEXT_RIM_HEIGHT_M,
|
||||||
|
);
|
||||||
|
assert.equal(positions.length, segments + 1);
|
||||||
|
assert.ok(
|
||||||
|
Cesium.Cartesian3.equalsEpsilon(positions[0], positions.at(-1), 0, 1e-6),
|
||||||
|
'the ring closes on itself',
|
||||||
|
);
|
||||||
|
const distinct = new Set(
|
||||||
|
positions
|
||||||
|
.slice(0, segments)
|
||||||
|
.map(({ x, y, z }) => [x, y, z].map((v) => v.toFixed(1)).join()),
|
||||||
|
);
|
||||||
|
assert.equal(distinct.size, segments, 'every vertex is its own point');
|
||||||
|
// Ordinary chords are 2π × 250 km / 128 ≈ 12.3 km long.
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const chord = Cesium.Cartesian3.distance(positions[i], positions[i + 1]);
|
||||||
|
assert.ok(chord > 11_500 && chord < 13_000, `chord ${i}: ${chord} m`);
|
||||||
|
}
|
||||||
|
for (const distance of surfaceDistances(center, positions))
|
||||||
|
assert.ok(Math.abs(distance - AWARENESS_RADIUS_M) < 100, `${distance} m`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('the ring core and halo contrast on light and dark ground', () => {
|
||||||
|
const core = Cesium.Color.fromCssColorString(
|
||||||
|
AWARENESS_RANGE_RING_STYLE.color,
|
||||||
|
);
|
||||||
|
const halo = Cesium.Color.fromCssColorString(
|
||||||
|
AWARENESS_RANGE_RING_STYLE.outlineColor,
|
||||||
|
);
|
||||||
|
// Core against halo is the edge that survives bright desert and snow, and
|
||||||
|
// the monochrome styles that map everything to luminance.
|
||||||
|
const ratio = (luminance(core) + 0.05) / (luminance(halo) + 0.05);
|
||||||
|
assert.ok(ratio >= 4.5, `core:halo contrast ${ratio.toFixed(2)}`);
|
||||||
|
assert.ok(core.alpha >= 0.9 && halo.alpha >= 0.7);
|
||||||
|
// A visible halo on each side of a core at least 2 px wide.
|
||||||
|
const { width, outlineWidth } = AWARENESS_RANGE_RING_STYLE;
|
||||||
|
assert.ok(outlineWidth >= 1);
|
||||||
|
assert.ok(width - 2 * outlineWidth >= 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renderVisual draws one haloed polyline ring, not an ellipse outline', () => {
|
||||||
|
const { entities, rendering } = ringRendering();
|
||||||
|
const subject = {
|
||||||
|
layerId: 'flights',
|
||||||
|
id: 'abc123',
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(3, 27, 9000),
|
||||||
|
};
|
||||||
|
rendering.renderVisual(subject);
|
||||||
|
assert.equal(entities.values.length, 1);
|
||||||
|
const [entity] = entities.values;
|
||||||
|
assert.equal(entity.ellipse, undefined);
|
||||||
|
const now = Cesium.JulianDate.now();
|
||||||
|
assert.equal(entity.polyline.width.getValue(now), 4);
|
||||||
|
assert.equal(entity.polyline.arcType.getValue(now), Cesium.ArcType.NONE);
|
||||||
|
const material = entity.polyline.material;
|
||||||
|
assert.ok(material instanceof Cesium.PolylineOutlineMaterialProperty);
|
||||||
|
assert.equal(
|
||||||
|
material.color.getValue(now).toCssColorString(),
|
||||||
|
Cesium.Color.fromCssColorString(
|
||||||
|
AWARENESS_RANGE_RING_STYLE.color,
|
||||||
|
).toCssColorString(),
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
material.outlineWidth.getValue(now),
|
||||||
|
AWARENESS_RANGE_RING_STYLE.outlineWidth,
|
||||||
|
);
|
||||||
|
const ground = Cesium.Cartesian3.fromDegrees(3, 27, 0);
|
||||||
|
for (const distance of surfaceDistances(
|
||||||
|
ground,
|
||||||
|
entity.polyline.positions.getValue(now),
|
||||||
|
))
|
||||||
|
assert.ok(Math.abs(distance - AWARENESS_RADIUS_M) < 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the ring follows the subject past 250 m and ignores smaller drift', () => {
|
||||||
|
const { entities, rendering, renders } = ringRendering();
|
||||||
|
const subject = (lon) => ({
|
||||||
|
layerId: 'flights',
|
||||||
|
id: 'abc123',
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(lon, 27, 9000),
|
||||||
|
});
|
||||||
|
rendering.renderVisual(subject(3));
|
||||||
|
const [entity] = entities.values;
|
||||||
|
const now = Cesium.JulianDate.now();
|
||||||
|
const first = entity.polyline.positions.getValue(now);
|
||||||
|
const rendersAfterAdd = renders();
|
||||||
|
|
||||||
|
// ~100 m east: under the threshold, nothing is rebuilt or rendered.
|
||||||
|
rendering.renderVisual(subject(3.001));
|
||||||
|
assert.equal(entity.polyline.positions.getValue(now), first);
|
||||||
|
assert.equal(renders(), rendersAfterAdd);
|
||||||
|
|
||||||
|
// ~1 km east: the same entity moves to the new centre.
|
||||||
|
rendering.renderVisual(subject(3.01));
|
||||||
|
assert.equal(entities.values.length, 1);
|
||||||
|
assert.equal(entities.values[0], entity);
|
||||||
|
const moved = entity.polyline.positions.getValue(now);
|
||||||
|
assert.notEqual(moved, first);
|
||||||
|
for (const distance of surfaceDistances(
|
||||||
|
Cesium.Cartesian3.fromDegrees(3.01, 27, 0),
|
||||||
|
moved,
|
||||||
|
))
|
||||||
|
assert.ok(Math.abs(distance - AWARENESS_RADIUS_M) < 100);
|
||||||
|
assert.equal(renders(), rendersAfterAdd + 1);
|
||||||
|
|
||||||
|
rendering.clearVisual();
|
||||||
|
assert.equal(entities.values.length, 0);
|
||||||
|
});
|
||||||
@@ -10,9 +10,66 @@ import {
|
|||||||
AWARENESS_RADIUS_M,
|
AWARENESS_RADIUS_M,
|
||||||
} from '../../data/militaryAwarenessEngine.js';
|
} from '../../data/militaryAwarenessEngine.js';
|
||||||
import { bearingBetweenCoordinates } from '../../cockpitMath.js';
|
import { bearingBetweenCoordinates } from '../../cockpitMath.js';
|
||||||
import { AWARENESS_PAGE_SIZE, CONTEXT_RIM_HEIGHT_M } from './policy.js';
|
import {
|
||||||
|
AWARENESS_PAGE_SIZE,
|
||||||
|
AWARENESS_RANGE_RING_STYLE,
|
||||||
|
CONTEXT_RIM_HEIGHT_M,
|
||||||
|
} from './policy.js';
|
||||||
import { formatAwarenessDirection } from './presentation.js';
|
import { formatAwarenessDirection } from './presentation.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A closed ring of positions `radiusM` along the surface from `groundCenter`,
|
||||||
|
* raised to `heightM`. Each bearing steps over a sphere with the ellipsoid's
|
||||||
|
* radius of curvature in that direction (Euler's formula), which keeps the
|
||||||
|
* ring within ~50 m of the true geodesic radius at 250 km.
|
||||||
|
* @param {Cesium.Cartesian3} groundCenter Ring centre on the ellipsoid.
|
||||||
|
* @param {number} radiusM Surface radius in metres.
|
||||||
|
* @param {number} heightM Height of the ring above the ellipsoid.
|
||||||
|
* @param {number} [segments] Chords around the ring.
|
||||||
|
* @returns {Cesium.Cartesian3[]} `segments + 1` positions; the last repeats the first.
|
||||||
|
*/
|
||||||
|
export function awarenessRangeRingPositions(
|
||||||
|
groundCenter,
|
||||||
|
radiusM,
|
||||||
|
heightM,
|
||||||
|
segments = AWARENESS_RANGE_RING_STYLE.segments,
|
||||||
|
) {
|
||||||
|
const { latitude, longitude } =
|
||||||
|
Cesium.Cartographic.fromCartesian(groundCenter);
|
||||||
|
const { x: a, z: b } = Cesium.Ellipsoid.WGS84.radii;
|
||||||
|
const e2 = 1 - (b * b) / (a * a);
|
||||||
|
const sinLat = Math.sin(latitude);
|
||||||
|
const cosLat = Math.cos(latitude);
|
||||||
|
const sinLon = Math.sin(longitude);
|
||||||
|
const cosLon = Math.cos(longitude);
|
||||||
|
const w = 1 - e2 * sinLat * sinLat;
|
||||||
|
const meridional = (a * (1 - e2)) / (w * Math.sqrt(w));
|
||||||
|
const primeVertical = a / Math.sqrt(w);
|
||||||
|
// Step along the great circle in 3D from the centre's up, north and east
|
||||||
|
// unit vectors. Unlike the latitude/longitude destination formula, this
|
||||||
|
// stays well conditioned at and next to the poles.
|
||||||
|
const up = [cosLat * cosLon, cosLat * sinLon, sinLat];
|
||||||
|
const north = [-sinLat * cosLon, -sinLat * sinLon, cosLat];
|
||||||
|
const east = [-sinLon, cosLon, 0];
|
||||||
|
const positions = [];
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
const bearing = ((i % segments) / segments) * Cesium.Math.TWO_PI;
|
||||||
|
const cosB = Math.cos(bearing);
|
||||||
|
const sinB = Math.sin(bearing);
|
||||||
|
const angular =
|
||||||
|
radiusM * ((cosB * cosB) / meridional + (sinB * sinB) / primeVertical);
|
||||||
|
const cosA = Math.cos(angular);
|
||||||
|
const sinA = Math.sin(angular);
|
||||||
|
const [x, y, z] = up.map(
|
||||||
|
(u, k) => u * cosA + (north[k] * cosB + east[k] * sinB) * sinA,
|
||||||
|
);
|
||||||
|
const lat = Math.atan2(z, Math.hypot(x, y));
|
||||||
|
const lon = Math.atan2(y, x);
|
||||||
|
positions.push(Cesium.Cartesian3.fromRadians(lon, lat, heightM));
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
export function createRendering({
|
export function createRendering({
|
||||||
state: layerState,
|
state: layerState,
|
||||||
services,
|
services,
|
||||||
@@ -292,15 +349,19 @@ export function createRendering({
|
|||||||
);
|
);
|
||||||
const key = `${subject.layerId}:${subject.id}`;
|
const key = `${subject.layerId}:${subject.id}`;
|
||||||
if (layerState.visual?.key === key) {
|
if (layerState.visual?.key === key) {
|
||||||
// Large ellipse geometry is more reliable as a ConstantPositionProperty.
|
// Move the ring only after a meaningful displacement so live tracking
|
||||||
// Move it only after a meaningful displacement so live tracking does not
|
// does not rebuild its geometry for sub-pixel motion every 750 ms.
|
||||||
// churn ten large geometries for sub-pixel motion every 750 ms.
|
|
||||||
if (
|
if (
|
||||||
Cesium.Cartesian3.distance(layerState.visual.center, groundCenter) >=
|
Cesium.Cartesian3.distance(layerState.visual.center, groundCenter) >=
|
||||||
250
|
250
|
||||||
) {
|
) {
|
||||||
|
const positions = awarenessRangeRingPositions(
|
||||||
|
groundCenter,
|
||||||
|
AWARENESS_RADIUS_M,
|
||||||
|
CONTEXT_RIM_HEIGHT_M,
|
||||||
|
);
|
||||||
for (const entity of layerState.visual.entities)
|
for (const entity of layerState.visual.entities)
|
||||||
entity.position.setValue(groundCenter);
|
entity.polyline.positions.setValue(positions);
|
||||||
Cesium.Cartesian3.clone(groundCenter, layerState.visual.center);
|
Cesium.Cartesian3.clone(groundCenter, layerState.visual.center);
|
||||||
// Content actually changed — buy exactly one frame instead of holding.
|
// Content actually changed — buy exactly one frame instead of holding.
|
||||||
governorRequestRender('awareness-visual');
|
governorRequestRender('awareness-visual');
|
||||||
@@ -311,17 +372,26 @@ export function createRendering({
|
|||||||
|
|
||||||
const entities = [];
|
const entities = [];
|
||||||
|
|
||||||
|
// A polyline, not an ellipse outline: WebGL draws ellipse outlines at
|
||||||
|
// 1 px whatever `outlineWidth` says, and the ring needs a halo.
|
||||||
|
const ring = AWARENESS_RANGE_RING_STYLE;
|
||||||
entities.push(
|
entities.push(
|
||||||
layerState.viewer.entities.add({
|
layerState.viewer.entities.add({
|
||||||
position: groundCenter,
|
polyline: {
|
||||||
ellipse: {
|
positions: new Cesium.ConstantProperty(
|
||||||
semiMajorAxis: AWARENESS_RADIUS_M,
|
awarenessRangeRingPositions(
|
||||||
semiMinorAxis: AWARENESS_RADIUS_M,
|
groundCenter,
|
||||||
fill: false,
|
AWARENESS_RADIUS_M,
|
||||||
outline: true,
|
CONTEXT_RIM_HEIGHT_M,
|
||||||
outlineColor:
|
),
|
||||||
Cesium.Color.fromCssColorString('#62b5ff').withAlpha(0.72),
|
),
|
||||||
height: CONTEXT_RIM_HEIGHT_M,
|
width: ring.width,
|
||||||
|
arcType: Cesium.ArcType.NONE,
|
||||||
|
material: new Cesium.PolylineOutlineMaterialProperty({
|
||||||
|
color: Cesium.Color.fromCssColorString(ring.color),
|
||||||
|
outlineColor: Cesium.Color.fromCssColorString(ring.outlineColor),
|
||||||
|
outlineWidth: ring.outlineWidth,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user