mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
style: format layer loading feedback helpers
This commit is contained in:
1 parent
a41f3ec3cd
commit
dec9dd288c
3 files changed
+245
-246
No files matched your search
@@ -5,14 +5,17 @@ export function installationFeedback(stats = {}, now = Date.now()) {
|
||||
timeout: 'Overpass timed out',
|
||||
query_failed: 'Overpass could not complete the query',
|
||||
};
|
||||
const reason = reasons[stats.failureReason] || 'Overpass temporarily unavailable';
|
||||
if (stats.loading) return stats.retrying ? 'Retrying mapped sites…' : 'Fetching mapped sites…';
|
||||
const reason =
|
||||
reasons[stats.failureReason] || 'Overpass temporarily unavailable';
|
||||
if (stats.loading)
|
||||
return stats.retrying ? 'Retrying mapped sites…' : 'Fetching mapped sites…';
|
||||
if (stats.retryAt > 0) {
|
||||
const seconds = Math.max(0, Math.ceil((stats.retryAt - now) / 1000));
|
||||
return `${reason} — ${seconds ? `retrying in ${seconds}s` : 'retry pending'}`;
|
||||
}
|
||||
if (stats.status === 'unavailable') return reason;
|
||||
if (stats.status === 'zoom-in') return 'Zoom in to search mapped installations';
|
||||
if (stats.status === 'zoom-in')
|
||||
return 'Zoom in to search mapped installations';
|
||||
if (stats.stale) return 'Showing cached mapped sites';
|
||||
if (stats.status === 'idle') return 'Mapped sites not loaded';
|
||||
return 'Mapped sites loaded';
|
||||
|
||||
+188
-86
@@ -16,21 +16,34 @@ function finiteCount(value) {
|
||||
/** Normalize one manager layer into a small loading-feedback record. */
|
||||
export function normalizeLayerLoading(layer = {}) {
|
||||
const stats = layer.stats || {};
|
||||
const lifecycleState = String(layer.lifecycleState || (layer.enabled ? 'enabled' : 'disabled'));
|
||||
const lifecycleState = String(
|
||||
layer.lifecycleState || (layer.enabled ? 'enabled' : 'disabled'),
|
||||
);
|
||||
const status = String(stats.status || '').toLowerCase();
|
||||
const disabling = lifecycleState === 'disabling';
|
||||
const loading = lifecycleState === 'enabling' || disabling || stats.loading === true || stats.refreshing === true;
|
||||
const loading =
|
||||
lifecycleState === 'enabling' ||
|
||||
disabling ||
|
||||
stats.loading === true ||
|
||||
stats.refreshing === true;
|
||||
const count = finiteCount(stats.count);
|
||||
const stoppingInstallations = layer.id === 'military-installations' && disabling;
|
||||
const stoppingInstallations =
|
||||
layer.id === 'military-installations' && disabling;
|
||||
// Guidance statuses ask the user to act (zoom in, run a search). They are
|
||||
// normal operation, never a batch failure — mirrors layerFeedState's carve-out
|
||||
// so a prompt stored alongside the status cannot turn the chip red.
|
||||
const guidance = GUIDANCE_STATUSES.includes(status);
|
||||
const error = stoppingInstallations ? null
|
||||
: (!guidance && stats.error) || stats.lastError || stats.managerRefreshError || null;
|
||||
const unavailable = !stoppingInstallations && (stats.unavailable === true
|
||||
|| stats.available === false
|
||||
|| ['unavailable', 'offline', 'down', 'error'].includes(status));
|
||||
const error = stoppingInstallations
|
||||
? null
|
||||
: (!guidance && stats.error) ||
|
||||
stats.lastError ||
|
||||
stats.managerRefreshError ||
|
||||
null;
|
||||
const unavailable =
|
||||
!stoppingInstallations &&
|
||||
(stats.unavailable === true ||
|
||||
stats.available === false ||
|
||||
['unavailable', 'offline', 'down', 'error'].includes(status));
|
||||
const keyRequired = stats.keyRequired === true || stats.missingKey === true;
|
||||
const degraded = stats.degraded === true || Boolean(error);
|
||||
const accepted = Boolean(stats.lastUpdate) || count > 0;
|
||||
@@ -39,7 +52,8 @@ export function normalizeLayerLoading(layer = {}) {
|
||||
label: String(layer.name || layer.id || 'Layer'),
|
||||
loading,
|
||||
disabling,
|
||||
refresh: loading && layer.enabled && (stats.refreshing === true || accepted),
|
||||
refresh:
|
||||
loading && layer.enabled && (stats.refreshing === true || accepted),
|
||||
lifecycleState,
|
||||
count,
|
||||
accepted,
|
||||
@@ -47,18 +61,27 @@ export function normalizeLayerLoading(layer = {}) {
|
||||
unavailable,
|
||||
keyRequired,
|
||||
degraded,
|
||||
installationRetry: layer.id === 'military-installations' && layer.enabled && !disabling
|
||||
? { retryAt: Number(stats.retryAt) || 0, retrying: stats.retrying === true,
|
||||
failureReason: stats.failureReason, loading, status: stats.status }
|
||||
: null,
|
||||
installationRetry:
|
||||
layer.id === 'military-installations' && layer.enabled && !disabling
|
||||
? {
|
||||
retryAt: Number(stats.retryAt) || 0,
|
||||
retrying: stats.retrying === true,
|
||||
failureReason: stats.failureReason,
|
||||
loading,
|
||||
status: stats.status,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function terminalFromParticipantStats(summary, participantIds) {
|
||||
if (!participantIds?.length) return null;
|
||||
const participants = new Set(participantIds);
|
||||
return summary.records.some((record) => participants.has(record.id)
|
||||
&& (record.error || record.unavailable || record.keyRequired))
|
||||
return summary.records.some(
|
||||
(record) =>
|
||||
participants.has(record.id) &&
|
||||
(record.error || record.unavailable || record.keyRequired),
|
||||
)
|
||||
? 'error'
|
||||
: null;
|
||||
}
|
||||
@@ -67,13 +90,17 @@ function terminalFromParticipantStats(summary, participantIds) {
|
||||
export function aggregateLayerLoading(layers = []) {
|
||||
const records = layers.map(normalizeLayerLoading);
|
||||
const active = records.filter((record) => record.loading);
|
||||
const disabling = active.length > 0 && active.every((record) => record.disabling);
|
||||
const disabling =
|
||||
active.length > 0 && active.every((record) => record.disabling);
|
||||
return {
|
||||
records,
|
||||
active,
|
||||
activeIds: active.map((record) => record.id),
|
||||
disabling,
|
||||
refresh: !disabling && active.length > 0 && active.every((record) => record.refresh),
|
||||
refresh:
|
||||
!disabling &&
|
||||
active.length > 0 &&
|
||||
active.every((record) => record.refresh),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -92,11 +119,11 @@ export function createLoadingFeedbackState() {
|
||||
}
|
||||
|
||||
/** Create a top-center status notice, optionally persistent until explicitly cleared. */
|
||||
export function createGlobalStatusNotice(message, nowMs = 0, {
|
||||
state = 'error',
|
||||
detail = '',
|
||||
persistent = false,
|
||||
} = {}) {
|
||||
export function createGlobalStatusNotice(
|
||||
message,
|
||||
nowMs = 0,
|
||||
{ state = 'error', detail = '', persistent = false } = {},
|
||||
) {
|
||||
const label = String(message || '').trim();
|
||||
if (!label) return null;
|
||||
return {
|
||||
@@ -117,9 +144,11 @@ export function presentGlobalStatusNotice(notice, nowMs = 0) {
|
||||
const now = Number.isFinite(nowMs) ? nowMs : 0;
|
||||
if (!notice?.label) return null;
|
||||
if (!notice.persistent && !Number.isFinite(notice.hideAt)) {
|
||||
notice.hideAt = now + (Number.isFinite(notice.dwellMs)
|
||||
? notice.dwellMs
|
||||
: LOADING_FAILURE_DWELL_MS);
|
||||
notice.hideAt =
|
||||
now +
|
||||
(Number.isFinite(notice.dwellMs)
|
||||
? notice.dwellMs
|
||||
: LOADING_FAILURE_DWELL_MS);
|
||||
}
|
||||
if (Number.isFinite(notice.hideAt) && now >= notice.hideAt) return null;
|
||||
return {
|
||||
@@ -130,10 +159,16 @@ export function presentGlobalStatusNotice(notice, nowMs = 0) {
|
||||
}
|
||||
|
||||
/** Whether deferred notice work still owns the current presentation epoch. */
|
||||
export function canPresentDeferredStatusNotice(expectedGeneration, currentGeneration, disposed = false) {
|
||||
return !disposed
|
||||
&& Number.isSafeInteger(expectedGeneration)
|
||||
&& expectedGeneration === currentGeneration;
|
||||
export function canPresentDeferredStatusNotice(
|
||||
expectedGeneration,
|
||||
currentGeneration,
|
||||
disposed = false,
|
||||
) {
|
||||
return (
|
||||
!disposed &&
|
||||
Number.isSafeInteger(expectedGeneration) &&
|
||||
expectedGeneration === currentGeneration
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,9 +176,19 @@ export function canPresentDeferredStatusNotice(expectedGeneration, currentGenera
|
||||
* hide a terminal manager failure. Failure dwell starts when the manager
|
||||
* reports it, so it must remain the highest-priority presentation while live.
|
||||
*/
|
||||
export function presentGlobalLoadingStatus(notice, loadingState, summary, nowMs = 0) {
|
||||
const loadingPresentation = presentLoadingFeedback(loadingState, summary, nowMs);
|
||||
if (['error', 'retry'].includes(loadingPresentation?.state)) return loadingPresentation;
|
||||
export function presentGlobalLoadingStatus(
|
||||
notice,
|
||||
loadingState,
|
||||
summary,
|
||||
nowMs = 0,
|
||||
) {
|
||||
const loadingPresentation = presentLoadingFeedback(
|
||||
loadingState,
|
||||
summary,
|
||||
nowMs,
|
||||
);
|
||||
if (['error', 'retry'].includes(loadingPresentation?.state))
|
||||
return loadingPresentation;
|
||||
return presentGlobalStatusNotice(notice, nowMs) || loadingPresentation;
|
||||
}
|
||||
|
||||
@@ -162,11 +207,11 @@ export function createTrafficSyncFeedbackState() {
|
||||
* Reduce one sampled Street Traffic status without extending completion on
|
||||
* every animation-loop poll. Coverage describes accepted data, not work.
|
||||
*/
|
||||
export function reduceTrafficSyncFeedback(previous, {
|
||||
enabled = false,
|
||||
stats = {},
|
||||
forceShow = false,
|
||||
} = {}, nowMs = 0) {
|
||||
export function reduceTrafficSyncFeedback(
|
||||
previous,
|
||||
{ enabled = false, stats = {}, forceShow = false } = {},
|
||||
nowMs = 0,
|
||||
) {
|
||||
const state = previous || createTrafficSyncFeedbackState();
|
||||
const now = Number.isFinite(nowMs) ? nowMs : 0;
|
||||
if (!enabled) return createTrafficSyncFeedbackState();
|
||||
@@ -174,10 +219,13 @@ export function reduceTrafficSyncFeedback(previous, {
|
||||
const hasProgress = Number.isFinite(stats.phaseProgressPct);
|
||||
const progressPct = hasProgress
|
||||
? Math.max(0, Math.min(100, Math.round(stats.phaseProgressPct)))
|
||||
: (stats.loading ? 1 : 100);
|
||||
const busy = stats.loading === true
|
||||
|| stats.worldJumping === true
|
||||
|| (hasProgress && (progressPct < 100 || (stats.prewarmQueueDepth ?? 0) > 0));
|
||||
: stats.loading
|
||||
? 1
|
||||
: 100;
|
||||
const busy =
|
||||
stats.loading === true ||
|
||||
stats.worldJumping === true ||
|
||||
(hasProgress && (progressPct < 100 || (stats.prewarmQueueDepth ?? 0) > 0));
|
||||
const label = String(stats.phaseLabel || stats.loadingLabel || '').trim();
|
||||
|
||||
if (busy) {
|
||||
@@ -193,13 +241,13 @@ export function reduceTrafficSyncFeedback(previous, {
|
||||
};
|
||||
}
|
||||
|
||||
const existingConfirmation = state.confirmationUntil > now
|
||||
? state.confirmationUntil
|
||||
: 0;
|
||||
const confirmationUntil = existingConfirmation || (state.busy || forceShow
|
||||
? now + TRAFFIC_SYNC_CONFIRM_MS
|
||||
: 0);
|
||||
const visible = confirmationUntil > now && progressPct >= 100 && Boolean(label);
|
||||
const existingConfirmation =
|
||||
state.confirmationUntil > now ? state.confirmationUntil : 0;
|
||||
const confirmationUntil =
|
||||
existingConfirmation ||
|
||||
(state.busy || forceShow ? now + TRAFFIC_SYNC_CONFIRM_MS : 0);
|
||||
const visible =
|
||||
confirmationUntil > now && progressPct >= 100 && Boolean(label);
|
||||
return {
|
||||
busy: false,
|
||||
visible,
|
||||
@@ -217,7 +265,8 @@ export function reduceTrafficSyncFeedback(previous, {
|
||||
|
||||
function terminalFromEvent(event) {
|
||||
const type = String(event?.type || '');
|
||||
if (type === 'visibility-failed' || type === 'refresh-failed' || event?.error) return 'error';
|
||||
if (type === 'visibility-failed' || type === 'refresh-failed' || event?.error)
|
||||
return 'error';
|
||||
if (type === 'visibility-cancelled' || event?.cancelled) return 'cancelled';
|
||||
if (type === 'visibility' || type === 'refresh') return 'complete';
|
||||
return null;
|
||||
@@ -238,7 +287,9 @@ export function reduceLoadingFeedback(previous, summary, nowMs, event = null) {
|
||||
const beginning = state.phase !== 'loading';
|
||||
const startedAt = beginning ? now : state.startedAt;
|
||||
const priorParticipants = beginning ? [] : state.activeIds;
|
||||
const activeIds = [...new Set([...priorParticipants, ...summary.activeIds])];
|
||||
const activeIds = [
|
||||
...new Set([...priorParticipants, ...summary.activeIds]),
|
||||
];
|
||||
const eventLayerId = String(event?.layerId || '');
|
||||
const eventParticipates = eventLayerId && activeIds.includes(eventLayerId);
|
||||
const batchOutcome = mergeTerminalOutcome(
|
||||
@@ -257,27 +308,41 @@ export function reduceLoadingFeedback(previous, summary, nowMs, event = null) {
|
||||
activeIds,
|
||||
batchOutcome,
|
||||
terminal: null,
|
||||
operation: summary.disabling ? 'disabling' : summary.refresh ? 'refresh' : 'loading',
|
||||
failedEventIds: [...new Set([
|
||||
...(beginning ? [] : state.failedEventIds || []),
|
||||
...(eventParticipates && terminalFromEvent(event) === 'error' ? [eventLayerId] : []),
|
||||
])],
|
||||
operation: summary.disabling
|
||||
? 'disabling'
|
||||
: summary.refresh
|
||||
? 'refresh'
|
||||
: 'loading',
|
||||
failedEventIds: [
|
||||
...new Set([
|
||||
...(beginning ? [] : state.failedEventIds || []),
|
||||
...(eventParticipates && terminalFromEvent(event) === 'error'
|
||||
? [eventLayerId]
|
||||
: []),
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (state.phase === 'loading') {
|
||||
const eventLayerId = String(event?.layerId || '');
|
||||
const eventParticipates = eventLayerId && state.activeIds.includes(eventLayerId);
|
||||
const terminal = mergeTerminalOutcome(
|
||||
const eventParticipates =
|
||||
eventLayerId && state.activeIds.includes(eventLayerId);
|
||||
const terminal =
|
||||
mergeTerminalOutcome(
|
||||
state.batchOutcome,
|
||||
terminalFromParticipantStats(summary, state.activeIds),
|
||||
),
|
||||
eventParticipates ? terminalFromEvent(event) : null,
|
||||
) || 'complete';
|
||||
mergeTerminalOutcome(
|
||||
state.batchOutcome,
|
||||
terminalFromParticipantStats(summary, state.activeIds),
|
||||
),
|
||||
eventParticipates ? terminalFromEvent(event) : null,
|
||||
) || 'complete';
|
||||
const wasVisible = state.visible || now >= state.showAt;
|
||||
if (!wasVisible && terminal === 'complete') return createLoadingFeedbackState();
|
||||
const dwell = terminal === 'error' ? LOADING_FAILURE_DWELL_MS : LOADING_TERMINAL_DWELL_MS;
|
||||
if (!wasVisible && terminal === 'complete')
|
||||
return createLoadingFeedbackState();
|
||||
const dwell =
|
||||
terminal === 'error'
|
||||
? LOADING_FAILURE_DWELL_MS
|
||||
: LOADING_TERMINAL_DWELL_MS;
|
||||
return {
|
||||
...state,
|
||||
phase: 'terminal',
|
||||
@@ -285,10 +350,14 @@ export function reduceLoadingFeedback(previous, summary, nowMs, event = null) {
|
||||
hideAt: now + dwell,
|
||||
batchOutcome: terminal,
|
||||
terminal,
|
||||
failedEventIds: [...new Set([
|
||||
...(state.failedEventIds || []),
|
||||
...(eventParticipates && terminalFromEvent(event) === 'error' ? [eventLayerId] : []),
|
||||
])],
|
||||
failedEventIds: [
|
||||
...new Set([
|
||||
...(state.failedEventIds || []),
|
||||
...(eventParticipates && terminalFromEvent(event) === 'error'
|
||||
? [eventLayerId]
|
||||
: []),
|
||||
]),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -298,11 +367,17 @@ export function reduceLoadingFeedback(previous, summary, nowMs, event = null) {
|
||||
|
||||
/** Build the user-facing status copy for the current loading state. */
|
||||
export function presentLoadingFeedback(state, summary, nowMs) {
|
||||
const site = summary.records.find(record => record.installationRetry?.retryAt > 0);
|
||||
const otherFailure = summary.records.some(record => record.id !== 'military-installations'
|
||||
&& (state?.activeIds || []).includes(record.id)
|
||||
&& (record.error || record.unavailable || record.keyRequired))
|
||||
|| (state?.failedEventIds || []).some(id => id !== 'military-installations');
|
||||
const site = summary.records.find(
|
||||
(record) => record.installationRetry?.retryAt > 0,
|
||||
);
|
||||
const otherFailure =
|
||||
summary.records.some(
|
||||
(record) =>
|
||||
record.id !== 'military-installations' &&
|
||||
(state?.activeIds || []).includes(record.id) &&
|
||||
(record.error || record.unavailable || record.keyRequired),
|
||||
) ||
|
||||
(state?.failedEventIds || []).some((id) => id !== 'military-installations');
|
||||
// Keep the actual retry visible between attempts, without hiding another
|
||||
// participant's failure or pretending that a scheduled retry is fetching.
|
||||
if (site && !summary.active.length && !otherFailure) {
|
||||
@@ -312,28 +387,55 @@ export function presentLoadingFeedback(state, summary, nowMs) {
|
||||
}
|
||||
if (!state?.visible) return null;
|
||||
if (state.phase === 'terminal') {
|
||||
const labels = { complete: 'LOAD COMPLETE', cancelled: 'LOAD CANCELLED', error: 'LOAD FAILED' };
|
||||
const label = state.operation === 'disabling' && state.terminal === 'complete'
|
||||
? 'LIVE DATA OFF'
|
||||
: state.terminal === 'complete' && state.activeIds?.length === 1 && state.activeIds[0] === 'military-installations'
|
||||
? 'MAPPED SITES LOADED' : labels[state.terminal] || 'LOAD COMPLETE';
|
||||
const labels = {
|
||||
complete: 'LOAD COMPLETE',
|
||||
cancelled: 'LOAD CANCELLED',
|
||||
error: 'LOAD FAILED',
|
||||
};
|
||||
const label =
|
||||
state.operation === 'disabling' && state.terminal === 'complete'
|
||||
? 'LIVE DATA OFF'
|
||||
: state.terminal === 'complete' &&
|
||||
state.activeIds?.length === 1 &&
|
||||
state.activeIds[0] === 'military-installations'
|
||||
? 'MAPPED SITES LOADED'
|
||||
: labels[state.terminal] || 'LOAD COMPLETE';
|
||||
return { state: state.terminal, label, detail: '' };
|
||||
}
|
||||
const active = summary.active;
|
||||
if (active.length === 1 && active[0].installationRetry && !summary.disabling) {
|
||||
return { state: 'loading', label: active[0].installationRetry.retrying
|
||||
? 'RETRYING MAPPED SITES' : 'FETCHING MAPPED SITES', detail: 'OpenStreetMap · Overpass' };
|
||||
if (
|
||||
active.length === 1 &&
|
||||
active[0].installationRetry &&
|
||||
!summary.disabling
|
||||
) {
|
||||
return {
|
||||
state: 'loading',
|
||||
label: active[0].installationRetry.retrying
|
||||
? 'RETRYING MAPPED SITES'
|
||||
: 'FETCHING MAPPED SITES',
|
||||
detail: 'OpenStreetMap · Overpass',
|
||||
};
|
||||
}
|
||||
const elapsed = Math.max(0, nowMs - state.startedAt);
|
||||
const label = summary.disabling
|
||||
? 'TURNING OFF LIVE DATA'
|
||||
: summary.refresh ? 'REFRESHING LIVE DATA' : 'LOADING LIVE DATA';
|
||||
const names = active.slice(0, 2).map((record) => record.label).join(' · ');
|
||||
: summary.refresh
|
||||
? 'REFRESHING LIVE DATA'
|
||||
: 'LOADING LIVE DATA';
|
||||
const names = active
|
||||
.slice(0, 2)
|
||||
.map((record) => record.label)
|
||||
.join(' · ');
|
||||
const suffix = active.length > 2 ? ` +${active.length - 2}` : '';
|
||||
return {
|
||||
state: elapsed >= LOADING_LONG_THRESHOLD_MS
|
||||
? 'long'
|
||||
: summary.disabling ? 'disabling' : summary.refresh ? 'refresh' : 'loading',
|
||||
state:
|
||||
elapsed >= LOADING_LONG_THRESHOLD_MS
|
||||
? 'long'
|
||||
: summary.disabling
|
||||
? 'disabling'
|
||||
: summary.refresh
|
||||
? 'refresh'
|
||||
: 'loading',
|
||||
label,
|
||||
detail: `${names}${suffix}`,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user