mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 19:34:01 +00:00
Share links carry the world time when made away from live
Field 28, worldTime, holds the world time of a link made away from live: zigzag seconds since 2026-01-01, like field 8, but it may lie ahead of the link's creation (a forecast moment). Opening such a link pauses the world clock at that time; a link without it leaves the clock alone, so a page opens live and Back or Forward does not stop a replay. Times no JS Date can hold are dropped.
This commit is contained in:
1 parent
0c4da3cdb2
commit
32ec6ed436
8 files changed
+96
-8
No files matched your search
@@ -119,6 +119,7 @@ probability of about 255 in 256 (many of them already fail as base64url).
|
|||||||
| 25 | `scopeFeatherPct` | uint | 100 | 0–200 |
|
| 25 | `scopeFeatherPct` | uint | 100 | 0–200 |
|
||||||
| 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus |
|
| 26 | `scopeTerminusPct` | uint | none | 94–100; absent is the adaptive terminus |
|
||||||
| 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs |
|
| 27 | `styleParams` | LEN | style | packed `(token, value × 100)` pairs |
|
||||||
|
| 28 | `worldTime` | epoch | none (live) | zigzag seconds since 2026-01-01T00:00:00Z |
|
||||||
|
|
||||||
Field 1 absent means `thing` when field 13 is present and `place` otherwise,
|
Field 1 absent means `thing` when field 13 is present and `place` otherwise,
|
||||||
so v1 never writes it; a later view kind writes its code.
|
so v1 never writes it; a later view kind writes its code.
|
||||||
@@ -132,6 +133,10 @@ style also extends the pinned style-defaults table.
|
|||||||
`hudVariant` code 1 (`operator`) is retired: the app no longer offers that
|
`hudVariant` code 1 (`operator`) is retired: the app no longer offers that
|
||||||
layout and restores it as `tactical`. The code stays reserved.
|
layout and restores it as `tactical`. The code stays reserved.
|
||||||
|
|
||||||
|
Field 28 is the world time the link was made at, written only when it was away
|
||||||
|
from live. A link without it opens live. Unlike field 8 it may lie ahead of the
|
||||||
|
link's creation (a forecast moment), and it opens paused at that time.
|
||||||
|
|
||||||
Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3
|
Flag bits (field 9): 0 `bloom`, 1 `sharpen`, 2 `hudVisible`, 3
|
||||||
`celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on).
|
`celestialRing` (each defaulting by style), 4 `scopeEnabled` (default on).
|
||||||
|
|
||||||
|
|||||||
@@ -1105,7 +1105,8 @@
|
|||||||
"src/ui/kit/UiBadge.vue",
|
"src/ui/kit/UiBadge.vue",
|
||||||
"src/ui/stores/globeActions.js",
|
"src/ui/stores/globeActions.js",
|
||||||
"src/ui/stores/recording.js",
|
"src/ui/stores/recording.js",
|
||||||
"src/ui/i18n.js"
|
"src/ui/i18n.js",
|
||||||
|
"src/time/worldClock.js"
|
||||||
],
|
],
|
||||||
"external": ["cesium", "vue", "pinia", "vue-i18n"]
|
"external": ["cesium", "vue", "pinia", "vue-i18n"]
|
||||||
},
|
},
|
||||||
@@ -3890,7 +3891,8 @@
|
|||||||
"src/i18n/locales/ru.json",
|
"src/i18n/locales/ru.json",
|
||||||
"src/i18n/locales/uk.json",
|
"src/i18n/locales/uk.json",
|
||||||
"src/i18n/locales/zh-CN.json",
|
"src/i18n/locales/zh-CN.json",
|
||||||
"src/i18n/locales/zh-Hant.json"
|
"src/i18n/locales/zh-Hant.json",
|
||||||
|
"src/time/worldClock.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
@@ -4580,7 +4582,8 @@
|
|||||||
"src/data/detectionPolicy.js",
|
"src/data/detectionPolicy.js",
|
||||||
"src/scopeMaskPolicy.js",
|
"src/scopeMaskPolicy.js",
|
||||||
"src/i18n/marker.js",
|
"src/i18n/marker.js",
|
||||||
"src/visualStylePolicy.js"
|
"src/visualStylePolicy.js",
|
||||||
|
"src/time/worldClock.js"
|
||||||
],
|
],
|
||||||
"external": []
|
"external": []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ const utc = (value) =>
|
|||||||
: t('Unavailable');
|
: t('Unavailable');
|
||||||
|
|
||||||
/** A per-application observation layer, using the existing layer lifecycle and
|
/** A per-application observation layer, using the existing layer lifecycle and
|
||||||
* row controls. History is transient: shared links always open latest imagery. */
|
* row controls. Its time is the world clock's, so a link made away from live
|
||||||
|
* opens its imagery at that time; one made live opens the latest. */
|
||||||
export function createWeatherLayer({
|
export function createWeatherLayer({
|
||||||
feed,
|
feed,
|
||||||
clock,
|
clock,
|
||||||
|
|||||||
+14
-2
@@ -31,6 +31,7 @@ import {
|
|||||||
} from '../data/detectionPolicy.js';
|
} from '../data/detectionPolicy.js';
|
||||||
import { clampScopeFeatherPct } from '../scopeMaskPolicy.js';
|
import { clampScopeFeatherPct } from '../scopeMaskPolicy.js';
|
||||||
import { VIEW_STYLES } from '../view/index.js';
|
import { VIEW_STYLES } from '../view/index.js';
|
||||||
|
import { isWorldTime } from '../time/worldClock.js';
|
||||||
import { RETIRED_STYLES } from '../visualStylePolicy.js';
|
import { RETIRED_STYLES } from '../visualStylePolicy.js';
|
||||||
import {
|
import {
|
||||||
GLOBAL_POST_DEFAULTS,
|
GLOBAL_POST_DEFAULTS,
|
||||||
@@ -373,6 +374,7 @@ export const SHARE_VIEW_FIELDS = freezeAll([
|
|||||||
{ number: 25, key: 'scopeFeatherPct', type: 'uint', default: 100 },
|
{ number: 25, key: 'scopeFeatherPct', type: 'uint', default: 100 },
|
||||||
{ number: 26, key: 'scopeTerminusPct', type: 'uint', default: null },
|
{ number: 26, key: 'scopeTerminusPct', type: 'uint', default: null },
|
||||||
{ number: 27, key: 'styleParams', type: 'bytes', default: BY_STYLE },
|
{ number: 27, key: 'styleParams', type: 'bytes', default: BY_STYLE },
|
||||||
|
{ number: 28, key: 'worldTime', type: 'epoch', default: null },
|
||||||
]);
|
]);
|
||||||
const FIELD_BY_KEY = new Map(SHARE_VIEW_FIELDS.map((f) => [f.key, f]));
|
const FIELD_BY_KEY = new Map(SHARE_VIEW_FIELDS.map((f) => [f.key, f]));
|
||||||
const fieldNumber = (key) => FIELD_BY_KEY.get(key).number;
|
const fieldNumber = (key) => FIELD_BY_KEY.get(key).number;
|
||||||
@@ -732,6 +734,9 @@ export function normalizeShareViewState(input = {}) {
|
|||||||
: null;
|
: null;
|
||||||
state.panelState = normalizePanelState(source.panelState);
|
state.panelState = normalizePanelState(source.panelState);
|
||||||
state.sharedAtMs = epochMsOr(source.sharedAtMs);
|
state.sharedAtMs = epochMsOr(source.sharedAtMs);
|
||||||
|
state.worldTime = isWorldTime(source.worldTime)
|
||||||
|
? Math.floor(source.worldTime / 1000) * 1000
|
||||||
|
: null;
|
||||||
let view = normalizeView(source.view);
|
let view = normalizeView(source.view);
|
||||||
if (state.layerState) view = liftTrackedEntity(state.layerState, view);
|
if (state.layerState) view = liftTrackedEntity(state.layerState, view);
|
||||||
if (view.kind === 'thing') {
|
if (view.kind === 'thing') {
|
||||||
@@ -948,8 +953,9 @@ export function encodeShareView(input) {
|
|||||||
writer.sintField(field.number, signedAngle(state[field.key]));
|
writer.sintField(field.number, signedAngle(state[field.key]));
|
||||||
break;
|
break;
|
||||||
case 'sharedAtMs':
|
case 'sharedAtMs':
|
||||||
if (state.sharedAtMs !== null)
|
case 'worldTime':
|
||||||
writer.sintField(field.number, epochSeconds(state.sharedAtMs));
|
if (state[field.key] !== null)
|
||||||
|
writer.sintField(field.number, epochSeconds(state[field.key]));
|
||||||
break;
|
break;
|
||||||
case 'flags': {
|
case 'flags': {
|
||||||
let bits = 0;
|
let bits = 0;
|
||||||
@@ -1347,6 +1353,12 @@ export function decodeShareView(code, { nowMs = Date.now() } = {}) {
|
|||||||
raw.sharedAtMs =
|
raw.sharedAtMs =
|
||||||
createdAtS >= 1 && Number.isSafeInteger(ms) && ms <= nowMs ? ms : null;
|
createdAtS >= 1 && Number.isSafeInteger(ms) && ms <= nowMs ? ms : null;
|
||||||
}
|
}
|
||||||
|
// The world time the link was made at, when it was away from live. It
|
||||||
|
// may lie ahead of the link's creation (a forecast) or of now.
|
||||||
|
if (get('worldTime')) {
|
||||||
|
const ms = (SHARE_VIEW_EPOCH_S + unz(get('worldTime').varint)) * 1000;
|
||||||
|
raw.worldTime = isWorldTime(ms) ? ms : null;
|
||||||
|
}
|
||||||
|
|
||||||
if (get('layerState'))
|
if (get('layerState'))
|
||||||
raw.layerState = decodeLayers(
|
raw.layerState = decodeLayers(
|
||||||
|
|||||||
@@ -361,6 +361,7 @@ test('every registered field round-trips at once', () => {
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
sharedAtMs: NOW_MS,
|
sharedAtMs: NOW_MS,
|
||||||
|
worldTime: NOW_MS - 6 * 3_600_000,
|
||||||
};
|
};
|
||||||
// A thing view derives its camera (roll 0), so the camera fields are
|
// A thing view derives its camera (roll 0), so the camera fields are
|
||||||
// checked on the same state as a place view.
|
// checked on the same state as a place view.
|
||||||
@@ -501,6 +502,7 @@ test('the field registry is well formed and permanent', () => {
|
|||||||
[25, 'scopeFeatherPct', 100],
|
[25, 'scopeFeatherPct', 100],
|
||||||
[26, 'scopeTerminusPct', null],
|
[26, 'scopeTerminusPct', null],
|
||||||
[27, 'styleParams', BY_STYLE],
|
[27, 'styleParams', BY_STYLE],
|
||||||
|
[28, 'worldTime', null],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
@@ -1338,3 +1340,22 @@ test('a layer option filed under a mirror layer is ignored', () => {
|
|||||||
{ scope: 'layer-option', code: `${military}.${layerTokenNumber('m')}` },
|
{ scope: 'layer-option', code: `${military}.${layerTokenNumber('m')}` },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the world time is written only away from live, and may lie ahead', () => {
|
||||||
|
const at = (worldTime) =>
|
||||||
|
decodeShareView(encodeShareView({ sharedAtMs: NOW_MS, worldTime }), {
|
||||||
|
nowMs: NOW_MS,
|
||||||
|
}).state.worldTime;
|
||||||
|
assert.equal(at(null), null);
|
||||||
|
assert.equal(at(NOW_MS - 3_600_000), NOW_MS - 3_600_000);
|
||||||
|
// A forecast moment ahead of the link's creation is kept.
|
||||||
|
assert.equal(at(NOW_MS + 86_400_000), NOW_MS + 86_400_000);
|
||||||
|
// Before the 2026 epoch too: the field is zigzag.
|
||||||
|
assert.equal(at(Date.UTC(2024, 0, 1)), Date.UTC(2024, 0, 1));
|
||||||
|
// Past what a Date can hold: dropped, so the link opens live.
|
||||||
|
assert.equal(at(8.8e15), null);
|
||||||
|
assert.ok(
|
||||||
|
encodeShareView({ sharedAtMs: NOW_MS, worldTime: NOW_MS - 1000 }).length >
|
||||||
|
encodeShareView({ sharedAtMs: NOW_MS }).length,
|
||||||
|
);
|
||||||
|
});
|
||||||
+11
-2
@@ -5,6 +5,7 @@ import {
|
|||||||
normalizeAllocationStrategy,
|
normalizeAllocationStrategy,
|
||||||
} from './data/detectionPolicy.js';
|
} from './data/detectionPolicy.js';
|
||||||
import { clampScopeTerminusPct } from './scopeMask.js';
|
import { clampScopeTerminusPct } from './scopeMask.js';
|
||||||
|
import { worldClock as pageWorldClock } from './time/worldClock.js';
|
||||||
import { clampScopeFeatherPct } from './scopeMaskPolicy.js';
|
import { clampScopeFeatherPct } from './scopeMaskPolicy.js';
|
||||||
import {
|
import {
|
||||||
decodeLayerStateParams,
|
decodeLayerStateParams,
|
||||||
@@ -68,9 +69,11 @@ export class ShareLinkManager {
|
|||||||
isNavigationCurrent,
|
isNavigationCurrent,
|
||||||
cancelOwnedNavigation,
|
cancelOwnedNavigation,
|
||||||
initialShareCode = null,
|
initialShareCode = null,
|
||||||
|
worldClock = pageWorldClock,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
this.viewer = viewer;
|
this.viewer = viewer;
|
||||||
|
this._worldClock = worldClock;
|
||||||
// A host may hand over the share code to open instead of the address.
|
// A host may hand over the share code to open instead of the address.
|
||||||
this._initialShareCode =
|
this._initialShareCode =
|
||||||
typeof initialShareCode === 'string' ? initialShareCode : null;
|
typeof initialShareCode === 'string' ? initialShareCode : null;
|
||||||
@@ -163,8 +166,9 @@ export class ShareLinkManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The restore state for a share code, in the long form's parsed shape plus
|
* The restore state for a share code, in the long form's parsed shape plus
|
||||||
* `shareView` (what the view follows, with its offset and last-known fix),
|
* `shareView` (what the view follows, with its offset and last-known fix)
|
||||||
* or null when the code is refused.
|
* and `worldTime` (null when the link was made live), or null when the
|
||||||
|
* code is refused.
|
||||||
*/
|
*/
|
||||||
_parseShareCode(code) {
|
_parseShareCode(code) {
|
||||||
const result = decodeShareView(code);
|
const result = decodeShareView(code);
|
||||||
@@ -174,6 +178,7 @@ export class ShareLinkManager {
|
|||||||
}
|
}
|
||||||
const state = toLegacyShareState(result.state);
|
const state = toLegacyShareState(result.state);
|
||||||
state.shareView = result.state.view;
|
state.shareView = result.state.view;
|
||||||
|
state.worldTime = result.state.worldTime;
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +597,10 @@ export class ShareLinkManager {
|
|||||||
// that state in the share format.
|
// that state in the share format.
|
||||||
const state = fromLegacyShareState(this._parseShareHash(params.toString()));
|
const state = fromLegacyShareState(this._parseShareHash(params.toString()));
|
||||||
state.sharedAtMs = Math.floor(nowMs / 1000) * 1000;
|
state.sharedAtMs = Math.floor(nowMs / 1000) * 1000;
|
||||||
|
// Away from live, the link opens at the same world time.
|
||||||
|
state.worldTime = this._worldClock.isLive()
|
||||||
|
? null
|
||||||
|
: Math.floor(this._worldClock.now() / 1000) * 1000;
|
||||||
let subject = null;
|
let subject = null;
|
||||||
try {
|
try {
|
||||||
subject = this._followSubjectProvider?.({ nowMs }) ?? null;
|
subject = this._followSubjectProvider?.({ nowMs }) ?? null;
|
||||||
|
|||||||
@@ -423,3 +423,28 @@ test('the layer coordinator replaces in-flight share layers with the navigated s
|
|||||||
);
|
);
|
||||||
coordinator.destroy();
|
coordinator.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a link made away from live carries its world time to the page that opens it', (t) => {
|
||||||
|
installTab(t, SHARE_A);
|
||||||
|
const worldTime = Date.UTC(2026, 9, 30, 6, 15, 42);
|
||||||
|
const sharer = new ShareLinkManager(fakeViewer(), {
|
||||||
|
worldClock: { isLive: () => false, now: () => worldTime + 400 },
|
||||||
|
});
|
||||||
|
const link = sharer.buildLink({ nowMs: Date.UTC(2026, 10, 1) });
|
||||||
|
sharer.destroy();
|
||||||
|
const code = new URL(link).pathname.split('/').at(-1);
|
||||||
|
const opener = new ShareLinkManager(fakeViewer(), { initialShareCode: code });
|
||||||
|
assert.equal(opener.parseInitialHash().worldTime, worldTime);
|
||||||
|
opener.destroy();
|
||||||
|
|
||||||
|
const live = new ShareLinkManager(fakeViewer(), {
|
||||||
|
worldClock: { isLive: () => true, now: () => worldTime },
|
||||||
|
});
|
||||||
|
const liveCode = new URL(live.buildLink()).pathname.split('/').at(-1);
|
||||||
|
live.destroy();
|
||||||
|
const liveOpener = new ShareLinkManager(fakeViewer(), {
|
||||||
|
initialShareCode: liveCode,
|
||||||
|
});
|
||||||
|
assert.equal(liveOpener.parseInitialHash().worldTime, null);
|
||||||
|
liveOpener.destroy();
|
||||||
|
});
|
||||||
@@ -4,6 +4,16 @@ import { canPresentDeferredStatusNotice } from '../loadingFeedback.js';
|
|||||||
import { t, msg, formatDate } from '../i18n/index.js';
|
import { t, msg, formatDate } from '../i18n/index.js';
|
||||||
import { SHARE_VIEW_ENTITY_KINDS } from '../share/follow.js';
|
import { SHARE_VIEW_ENTITY_KINDS } from '../share/follow.js';
|
||||||
import { UiLifetime } from './uiLifetime.js';
|
import { UiLifetime } from './uiLifetime.js';
|
||||||
|
import { isWorldTime, worldClock } from '../time/worldClock.js';
|
||||||
|
|
||||||
|
// A link made away from live opens at its world time, paused. One without a
|
||||||
|
// world time leaves the clock as it is: a page opens live, and Back or Forward
|
||||||
|
// through long-form hashes (which never carry one) does not stop a replay.
|
||||||
|
function applySharedWorldTime(state) {
|
||||||
|
if (!isWorldTime(state?.worldTime)) return;
|
||||||
|
worldClock.pause();
|
||||||
|
worldClock.seek(state.worldTime);
|
||||||
|
}
|
||||||
|
|
||||||
/** How long a shared vessel may take to appear in the vessel feed. */
|
/** How long a shared vessel may take to appear in the vessel feed. */
|
||||||
export const SHARED_VESSEL_WAIT_MS = 120_000;
|
export const SHARED_VESSEL_WAIT_MS = 120_000;
|
||||||
@@ -146,6 +156,7 @@ export class ShareRestoration {
|
|||||||
: Promise.resolve({ status: 'not-requested', share: null, layers: [] });
|
: Promise.resolve({ status: 'not-requested', share: null, layers: [] });
|
||||||
if (savedState) {
|
if (savedState) {
|
||||||
this._hasShareState = true;
|
this._hasShareState = true;
|
||||||
|
applySharedWorldTime(savedState);
|
||||||
// Reserve camera authority now; the delayed mesh-friendly flight may
|
// Reserve camera authority now; the delayed mesh-friendly flight may
|
||||||
// run only if no newer user, voice, or tracking navigation has won.
|
// run only if no newer user, voice, or tracking navigation has won.
|
||||||
this._initialShareNavigationGeneration =
|
this._initialShareNavigationGeneration =
|
||||||
@@ -301,6 +312,7 @@ export class ShareRestoration {
|
|||||||
layers: [],
|
layers: [],
|
||||||
});
|
});
|
||||||
this._initialShareState = state;
|
this._initialShareState = state;
|
||||||
|
applySharedWorldTime(state);
|
||||||
this._initialShareSelectionSuperseded = false;
|
this._initialShareSelectionSuperseded = false;
|
||||||
this._hasShareState = true;
|
this._hasShareState = true;
|
||||||
this._navigatedShareRestoreActive = true;
|
this._navigatedShareRestoreActive = true;
|
||||||
|
|||||||
Reference in new issue
Block a user