mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 15:22:21 +00:00
feat(shipwrecks): period picker like air accidents, sharper icons
- Replace the four era chips with the start and end year pickers the air-accidents layer uses (the same RowRange control, ported as is); the era colours stay as a legend under the row. - Rasterize the boat marker at 144 px so large markers stay sharp. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
dcd0f2cccd
commit
112ffd4386
25 files changed
+439
-71
No files matched your search
@@ -3439,5 +3439,11 @@
|
||||
"Fatalities: {count}": "الوفيات: {count}",
|
||||
"Survivors: {count}": "الناجون: {count}",
|
||||
"Click the card to open the article ↗": "انقر على البطاقة لفتح المقالة ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "يأتي حطام السفن التراثي الأوروبي من EMODnet Human Activities، ويأتي الحطام البارز في العالم من ويكي بيانات. يبيّن اللون متى غرقت السفينة، ويكبر حجم العلامة مع عدد الوفيات. انقر على علامة لعرض بطاقتها."
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "يأتي حطام السفن التراثي الأوروبي من EMODnet Human Activities، ويأتي الحطام البارز في العالم من ويكي بيانات. يبيّن اللون متى غرقت السفينة، ويكبر حجم العلامة مع عدد الوفيات. انقر على علامة لعرض بطاقتها.",
|
||||
"All years": "كل السنوات",
|
||||
"Period": "الفترة",
|
||||
"From": "من",
|
||||
"To": "إلى",
|
||||
"First year of the shipwreck period": "السنة الأولى من فترة الغرق",
|
||||
"Last year of the shipwreck period": "السنة الأخيرة من فترة الغرق"
|
||||
}
|
||||
@@ -3135,5 +3135,11 @@
|
||||
"Fatalities: {count}": "Todesopfer: {count}",
|
||||
"Survivors: {count}": "Überlebende: {count}",
|
||||
"Click the card to open the article ↗": "Karte anklicken, um den Artikel zu öffnen ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Europäische Kulturerbe-Wracks stammen von EMODnet Human Activities, bekannte Wracks weltweit von Wikidata. Die Farbe zeigt, wann das Schiff sank; die Markierung wächst mit der Zahl der Todesopfer. Klicke auf eine Markierung für ihre Karte."
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Europäische Kulturerbe-Wracks stammen von EMODnet Human Activities, bekannte Wracks weltweit von Wikidata. Die Farbe zeigt, wann das Schiff sank; die Markierung wächst mit der Zahl der Todesopfer. Klicke auf eine Markierung für ihre Karte.",
|
||||
"All years": "Alle Jahre",
|
||||
"Period": "Zeitraum",
|
||||
"From": "Von",
|
||||
"To": "Bis",
|
||||
"First year of the shipwreck period": "Erstes Jahr des Schiffbruch-Zeitraums",
|
||||
"Last year of the shipwreck period": "Letztes Jahr des Schiffbruch-Zeitraums"
|
||||
}
|
||||
@@ -3211,5 +3211,11 @@
|
||||
"Fatalities: {count}": "Fallecidos: {count}",
|
||||
"Survivors: {count}": "Supervivientes: {count}",
|
||||
"Click the card to open the article ↗": "Haz clic en la ficha para abrir el artículo ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Los pecios históricos europeos proceden de EMODnet Human Activities; los naufragios notables de todo el mundo, de Wikidata. El color indica cuándo se hundió el barco; el marcador crece con el número de muertos. Haz clic en un marcador para ver su ficha."
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Los pecios históricos europeos proceden de EMODnet Human Activities; los naufragios notables de todo el mundo, de Wikidata. El color indica cuándo se hundió el barco; el marcador crece con el número de muertos. Haz clic en un marcador para ver su ficha.",
|
||||
"All years": "Todos los años",
|
||||
"Period": "Periodo",
|
||||
"From": "Desde",
|
||||
"To": "Hasta",
|
||||
"First year of the shipwreck period": "Primer año del periodo de naufragios",
|
||||
"Last year of the shipwreck period": "Último año del periodo de naufragios"
|
||||
}
|
||||
@@ -3211,5 +3211,11 @@
|
||||
"Fatalities: {count}": "Morts : {count}",
|
||||
"Survivors: {count}": "Survivants : {count}",
|
||||
"Click the card to open the article ↗": "Cliquez sur la fiche pour ouvrir l'article ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Les épaves patrimoniales européennes viennent d'EMODnet Human Activities ; les épaves notables du monde entier viennent de Wikidata. La couleur indique quand le navire a coulé ; le repère grossit avec le nombre de morts. Cliquez sur un repère pour voir sa fiche."
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Les épaves patrimoniales européennes viennent d'EMODnet Human Activities ; les épaves notables du monde entier viennent de Wikidata. La couleur indique quand le navire a coulé ; le repère grossit avec le nombre de morts. Cliquez sur un repère pour voir sa fiche.",
|
||||
"All years": "Toutes les années",
|
||||
"Period": "Période",
|
||||
"From": "Du",
|
||||
"To": "Au",
|
||||
"First year of the shipwreck period": "Première année de la période des naufrages",
|
||||
"Last year of the shipwreck period": "Dernière année de la période des naufrages"
|
||||
}
|
||||
@@ -3211,5 +3211,11 @@
|
||||
"Fatalities: {count}": "Vittime: {count}",
|
||||
"Survivors: {count}": "Sopravvissuti: {count}",
|
||||
"Click the card to open the article ↗": "Fai clic sulla scheda per aprire l'articolo ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "I relitti storici europei provengono da EMODnet Human Activities; i relitti noti di tutto il mondo da Wikidata. Il colore indica quando la nave è affondata; l'indicatore cresce con il numero di vittime. Fai clic su un indicatore per la sua scheda."
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "I relitti storici europei provengono da EMODnet Human Activities; i relitti noti di tutto il mondo da Wikidata. Il colore indica quando la nave è affondata; l'indicatore cresce con il numero di vittime. Fai clic su un indicatore per la sua scheda.",
|
||||
"All years": "Tutti gli anni",
|
||||
"Period": "Periodo",
|
||||
"From": "Dal",
|
||||
"To": "Al",
|
||||
"First year of the shipwreck period": "Primo anno del periodo dei naufragi",
|
||||
"Last year of the shipwreck period": "Ultimo anno del periodo dei naufragi"
|
||||
}
|
||||
@@ -3059,5 +3059,11 @@
|
||||
"Fatalities: {count}": "死者: {count}",
|
||||
"Survivors: {count}": "生存者: {count}",
|
||||
"Click the card to open the article ↗": "カードをクリックして記事を開く ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "欧州の歴史的な難破船はEMODnet Human Activities、世界の著名な難破船はWikidataによります。色は沈没した時期を示し、マーカーは死者数に応じて大きくなります。マーカーをクリックするとカードが開きます。"
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "欧州の歴史的な難破船はEMODnet Human Activities、世界の著名な難破船はWikidataによります。色は沈没した時期を示し、マーカーは死者数に応じて大きくなります。マーカーをクリックするとカードが開きます。",
|
||||
"All years": "全期間",
|
||||
"Period": "期間",
|
||||
"From": "開始",
|
||||
"To": "終了",
|
||||
"First year of the shipwreck period": "難破の期間の開始年",
|
||||
"Last year of the shipwreck period": "難破の期間の終了年"
|
||||
}
|
||||
@@ -3211,5 +3211,11 @@
|
||||
"Fatalities: {count}": "Mortos: {count}",
|
||||
"Survivors: {count}": "Sobreviventes: {count}",
|
||||
"Click the card to open the article ↗": "Clique na ficha para abrir o artigo ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Os naufrágios históricos europeus vêm do EMODnet Human Activities; os naufrágios notáveis do mundo vêm do Wikidata. A cor mostra quando o navio afundou; o marcador cresce com o número de mortos. Clique num marcador para ver a ficha."
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Os naufrágios históricos europeus vêm do EMODnet Human Activities; os naufrágios notáveis do mundo vêm do Wikidata. A cor mostra quando o navio afundou; o marcador cresce com o número de mortos. Clique num marcador para ver a ficha.",
|
||||
"All years": "Todos os anos",
|
||||
"Period": "Período",
|
||||
"From": "Início",
|
||||
"To": "Fim",
|
||||
"First year of the shipwreck period": "Primeiro ano do período dos naufrágios",
|
||||
"Last year of the shipwreck period": "Último ano do período dos naufrágios"
|
||||
}
|
||||
@@ -3287,5 +3287,11 @@
|
||||
"Fatalities: {count}": "Погибших: {count}",
|
||||
"Survivors: {count}": "Выживших: {count}",
|
||||
"Click the card to open the article ↗": "Нажмите на карточку, чтобы открыть статью ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Исторические затонувшие суда Европы — из EMODnet Human Activities, известные кораблекрушения по всему миру — из Wikidata. Цвет показывает, когда судно затонуло; маркер растёт с числом погибших. Нажмите на маркер, чтобы открыть карточку."
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Исторические затонувшие суда Европы — из EMODnet Human Activities, известные кораблекрушения по всему миру — из Wikidata. Цвет показывает, когда судно затонуло; маркер растёт с числом погибших. Нажмите на маркер, чтобы открыть карточку.",
|
||||
"All years": "Все годы",
|
||||
"Period": "Период",
|
||||
"From": "С",
|
||||
"To": "По",
|
||||
"First year of the shipwreck period": "Первый год периода кораблекрушений",
|
||||
"Last year of the shipwreck period": "Последний год периода кораблекрушений"
|
||||
}
|
||||
@@ -3287,5 +3287,11 @@
|
||||
"Fatalities: {count}": "Загиблих: {count}",
|
||||
"Survivors: {count}": "Тих, хто вижив: {count}",
|
||||
"Click the card to open the article ↗": "Натисніть картку, щоб відкрити статтю ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Історичні затонулі судна Європи — з EMODnet Human Activities, відомі корабельні аварії світу — з Wikidata. Колір показує, коли судно затонуло; маркер збільшується з кількістю загиблих. Натисніть маркер, щоб відкрити картку."
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "Історичні затонулі судна Європи — з EMODnet Human Activities, відомі корабельні аварії світу — з Wikidata. Колір показує, коли судно затонуло; маркер збільшується з кількістю загиблих. Натисніть маркер, щоб відкрити картку.",
|
||||
"All years": "Усі роки",
|
||||
"Period": "Період",
|
||||
"From": "З",
|
||||
"To": "По",
|
||||
"First year of the shipwreck period": "Перший рік періоду корабельних аварій",
|
||||
"Last year of the shipwreck period": "Останній рік періоду корабельних аварій"
|
||||
}
|
||||
@@ -3059,5 +3059,11 @@
|
||||
"Fatalities: {count}": "遇难:{count}",
|
||||
"Survivors: {count}": "幸存者:{count}",
|
||||
"Click the card to open the article ↗": "点击卡片打开文章 ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "欧洲历史沉船来自 EMODnet Human Activities;全球知名沉船来自 Wikidata。颜色表示船只沉没的时期,标记大小随遇难人数增加。点击标记查看卡片。"
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "欧洲历史沉船来自 EMODnet Human Activities;全球知名沉船来自 Wikidata。颜色表示船只沉没的时期,标记大小随遇难人数增加。点击标记查看卡片。",
|
||||
"All years": "所有年份",
|
||||
"Period": "时段",
|
||||
"From": "起",
|
||||
"To": "止",
|
||||
"First year of the shipwreck period": "沉船时段的起始年份",
|
||||
"Last year of the shipwreck period": "沉船时段的结束年份"
|
||||
}
|
||||
@@ -3059,5 +3059,11 @@
|
||||
"Fatalities: {count}": "罹難:{count}",
|
||||
"Survivors: {count}": "倖存者:{count}",
|
||||
"Click the card to open the article ↗": "點擊卡片開啟文章 ↗",
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "歐洲歷史沉船來自 EMODnet Human Activities;全球知名沉船來自 Wikidata。顏色表示船隻沉沒的時期,標記大小隨罹難人數增加。點擊標記查看卡片。"
|
||||
"European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.": "歐洲歷史沉船來自 EMODnet Human Activities;全球知名沉船來自 Wikidata。顏色表示船隻沉沒的時期,標記大小隨罹難人數增加。點擊標記查看卡片。",
|
||||
"All years": "所有年份",
|
||||
"Period": "時段",
|
||||
"From": "起",
|
||||
"To": "迄",
|
||||
"First year of the shipwreck period": "沉船時段的起始年份",
|
||||
"Last year of the shipwreck period": "沉船時段的結束年份"
|
||||
}
|
||||
@@ -19,15 +19,3 @@ export function eraOf(record) {
|
||||
if (year <= 1945) return '1900-1945';
|
||||
return 'after-1945';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a requested set of eras; unknown names are ignored and an empty
|
||||
* or missing request keeps the current set.
|
||||
* @param {unknown} value Array of era ids.
|
||||
* @param {Set<string>} current
|
||||
* @returns {Set<string>}
|
||||
*/
|
||||
export function normalizeEras(value, current) {
|
||||
if (!Array.isArray(value)) return new Set(current);
|
||||
return new Set(value.filter((era) => SHIPWRECK_ERAS.includes(era)));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { SHIPWRECK_ERAS, eraOf, normalizeEras } from './eras.js';
|
||||
import { eraOf } from './eras.js';
|
||||
|
||||
test('each wreck falls in one era', () => {
|
||||
assert.equal(eraOf({ year: 1588 }), 'before-1900');
|
||||
@@ -11,13 +11,3 @@ test('each wreck falls in one era', () => {
|
||||
assert.equal(eraOf({ year: null }), 'undated');
|
||||
assert.equal(eraOf(null), 'undated');
|
||||
});
|
||||
|
||||
test('era requests keep known eras only', () => {
|
||||
const all = new Set(SHIPWRECK_ERAS);
|
||||
assert.deepEqual(
|
||||
[...normalizeEras(['after-1945', 'bogus'], all)],
|
||||
['after-1945'],
|
||||
);
|
||||
assert.deepEqual([...normalizeEras('after-1945', all)], SHIPWRECK_ERAS);
|
||||
assert.deepEqual([...normalizeEras([], all)], []);
|
||||
});
|
||||
@@ -2,10 +2,17 @@ import * as Cesium from 'cesium';
|
||||
import { formatNumber, msg, t, tn } from '../../i18n/index.js';
|
||||
import { createHorizonCull } from '../streetLevel/view.js';
|
||||
import { ERA_COLORS, ERA_LABELS, describeShipwreck } from './cards.js';
|
||||
import { SHIPWRECK_ERAS, eraOf, normalizeEras } from './eras.js';
|
||||
import { SHIPWRECK_ERAS, eraOf } from './eras.js';
|
||||
import {
|
||||
inPeriod,
|
||||
normalizePeriod,
|
||||
periodRange,
|
||||
yearExtent,
|
||||
} from './period.js';
|
||||
export * from './records.js';
|
||||
export * from './cards.js';
|
||||
export * from './eras.js';
|
||||
export * from './period.js';
|
||||
export { createShipwrecksSource } from './source.js';
|
||||
|
||||
export const SHIPWRECKS_LAYER_ID = 'shipwrecks';
|
||||
@@ -28,13 +35,14 @@ export function markerPixelSize(casualties) {
|
||||
|
||||
/**
|
||||
* A round marker in the era's colour with a sinking hull and mast in the
|
||||
* middle, as an SVG data URL.
|
||||
* middle, as an SVG data URL. It is rasterized at 144 px, four times the
|
||||
* largest marker, so big markers stay sharp on high-density screens.
|
||||
* @param {string} color CSS colour.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function wreckMarkerImage(color) {
|
||||
const svg =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 36 36">` +
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 36 36">` +
|
||||
`<circle cx="18" cy="18" r="16" fill="${color}" stroke="#ffffff" stroke-width="2"/>` +
|
||||
`<path d="M8 21h20l-3.5 5h-13z" fill="#0b1420"/>` +
|
||||
`<path d="M17 8v12M17 9l6 7h-6" fill="none" stroke="#0b1420" stroke-width="2.2" stroke-linejoin="round"/>` +
|
||||
@@ -69,7 +77,9 @@ export function createShipwrecksLayer({
|
||||
let _selectedId = null;
|
||||
let _selectedCardId = null;
|
||||
let _selectedLink = null;
|
||||
let _eras = new Set(SHIPWRECK_ERAS);
|
||||
let _period = { from: null, to: null };
|
||||
let _extent = null;
|
||||
let _range = null;
|
||||
const _images = Object.fromEntries(
|
||||
SHIPWRECK_ERAS.map((era) => [era, wreckMarkerImage(ERA_COLORS[era])]),
|
||||
);
|
||||
@@ -86,16 +96,16 @@ export function createShipwrecksLayer({
|
||||
onChange: render,
|
||||
});
|
||||
|
||||
/** Shown: its era chip is on. */
|
||||
/** Shown: its year is inside the period (undated ones only for all years). */
|
||||
function visible(record) {
|
||||
return Boolean(record) && _eras.has(eraOf(record));
|
||||
return Boolean(record) && inPeriod(record, _range);
|
||||
}
|
||||
|
||||
function applyPin(pin) {
|
||||
pin.entity.show = pin.inFront && visible(pin.record);
|
||||
}
|
||||
|
||||
/** Re-apply the era chips to every marker and the open card. */
|
||||
/** Re-apply the period to every marker and the open card. */
|
||||
function applyFilter() {
|
||||
for (const pin of _pins) applyPin(pin);
|
||||
if (_selectedId && !visible(_records.get(_selectedId))) clearSelection();
|
||||
@@ -105,6 +115,8 @@ export function createShipwrecksLayer({
|
||||
|
||||
function rebuild(records) {
|
||||
_records = new Map(records.map((record) => [record.id, record]));
|
||||
_extent = yearExtent(records);
|
||||
_range = periodRange(_period, _extent);
|
||||
_pins = [];
|
||||
if (!_dataSource) return;
|
||||
_dataSource.entities.suspendEvents();
|
||||
@@ -257,18 +269,46 @@ export function createShipwrecksLayer({
|
||||
picking?.unregisterPickOwner?.(SHIPWRECKS_LAYER_ID);
|
||||
}
|
||||
|
||||
function toggleEra(era) {
|
||||
if (_eras.has(era)) _eras.delete(era);
|
||||
else _eras.add(era);
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
function countOf(era) {
|
||||
let count = 0;
|
||||
for (const record of _records.values()) if (eraOf(record) === era) count++;
|
||||
for (const record of _records.values())
|
||||
if (eraOf(record) === era && inPeriod(record, _range)) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
/** The start and end year pickers, side by side, as on Air accidents. */
|
||||
function periodControls() {
|
||||
if (!_extent || _extent.min === _extent.max) return {};
|
||||
const range = periodRange(_period, _extent);
|
||||
const valueText = range.all
|
||||
? t('All years')
|
||||
: range.from === range.to
|
||||
? String(range.from)
|
||||
: `${range.from}–${range.to}`;
|
||||
const options = [];
|
||||
for (let year = _extent.max; year >= _extent.min; year--)
|
||||
options.push({ value: year, label: String(year) });
|
||||
return {
|
||||
range: {
|
||||
id: 'period',
|
||||
label: t('Period'),
|
||||
valueText,
|
||||
from: {
|
||||
label: t('From'),
|
||||
ariaLabel: t('First year of the shipwreck period'),
|
||||
value: range.from,
|
||||
},
|
||||
to: {
|
||||
label: t('To'),
|
||||
ariaLabel: t('Last year of the shipwreck period'),
|
||||
value: range.to,
|
||||
},
|
||||
options,
|
||||
params: (end, value) => ({ [end]: Number(value) }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const layer = {
|
||||
id: SHIPWRECKS_LAYER_ID,
|
||||
name: 'Shipwrecks',
|
||||
@@ -347,17 +387,16 @@ export function createShipwrecksLayer({
|
||||
_rowListener = null;
|
||||
},
|
||||
|
||||
/** The era filter: `eras` lists the era chips that are on. */
|
||||
/** The period filter: `from` and `to` are years (null = oldest / latest in the data). */
|
||||
getParams() {
|
||||
return { eras: SHIPWRECK_ERAS.filter((era) => _eras.has(era)) };
|
||||
return { ..._period };
|
||||
},
|
||||
|
||||
setParams(params = {}) {
|
||||
if (!('eras' in params)) return true;
|
||||
const next = normalizeEras(params.eras, _eras);
|
||||
if (next.size === _eras.size && [...next].every((era) => _eras.has(era)))
|
||||
return true;
|
||||
_eras = next;
|
||||
const next = normalizePeriod(params, _period);
|
||||
if (next.from === _period.from && next.to === _period.to) return true;
|
||||
_period = next;
|
||||
_range = periodRange(_period, _extent);
|
||||
applyFilter();
|
||||
return true;
|
||||
},
|
||||
@@ -368,17 +407,16 @@ export function createShipwrecksLayer({
|
||||
|
||||
getRowControls() {
|
||||
return {
|
||||
chips: SHIPWRECK_ERAS.map((era) => ({
|
||||
id: era,
|
||||
label: t(ERA_LABELS[era]),
|
||||
active: _eras.has(era),
|
||||
chips: [],
|
||||
...periodControls(),
|
||||
// The colours are a legend: when the ship sank.
|
||||
legend: SHIPWRECK_ERAS.map((era) => ({
|
||||
text: t(ERA_LABELS[era]),
|
||||
color: ERA_COLORS[era],
|
||||
title: tn(countOf(era), '{count} wreck', '{count} wrecks', {
|
||||
blurb: tn(countOf(era), '{count} wreck', '{count} wrecks', {
|
||||
count: formatNumber(countOf(era)),
|
||||
}),
|
||||
onClick: () => toggleEra(era),
|
||||
})),
|
||||
legend: [],
|
||||
info: t('EMODnet (European waters) · Wikidata (worldwide)'),
|
||||
infoTitle: t(
|
||||
'European heritage wrecks come from EMODnet Human Activities; notable wrecks worldwide come from Wikidata. Colour shows when the ship sank; marker size grows with deaths. Click a marker for its card.',
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* The sinking year, or null when it is unknown (ancient wrecks dated only by
|
||||
* period, "C1st AD", have none).
|
||||
*/
|
||||
export function recordYear(record) {
|
||||
return Number.isInteger(record?.year) ? record.year : null;
|
||||
}
|
||||
|
||||
/** First and last year present in the records (null when none is dated). */
|
||||
export function yearExtent(records) {
|
||||
let min = Infinity;
|
||||
let max = -Infinity;
|
||||
for (const record of records) {
|
||||
const year = recordYear(record);
|
||||
if (year === null) continue;
|
||||
if (year < min) min = year;
|
||||
if (year > max) max = year;
|
||||
}
|
||||
return Number.isFinite(min) ? { min, max } : null;
|
||||
}
|
||||
|
||||
function yearParam(value) {
|
||||
if (value === null) return null;
|
||||
const year =
|
||||
typeof value === 'string' && /^-?\d+$/.test(value) ? Number(value) : value;
|
||||
return Number.isInteger(year) && Math.abs(year) < 10000 ? year : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a (partial) period request into the current one. `from: null` means
|
||||
* the first year in the data and `to: null` the latest; an invalid year keeps
|
||||
* the current one.
|
||||
* @param {{from?: number|string|null, to?: number|string|null}} params
|
||||
* @param {{from: number|null, to: number|null}} current
|
||||
* @returns {{from: number|null, to: number|null}}
|
||||
*/
|
||||
export function normalizePeriod(
|
||||
params = {},
|
||||
current = { from: null, to: null },
|
||||
) {
|
||||
const next = { ...current };
|
||||
for (const end of ['from', 'to']) {
|
||||
if (!(end in params)) continue;
|
||||
const year = yearParam(params[end]);
|
||||
if (year !== undefined) next[end] = year;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* The inclusive year range a period covers over the data's extent. Years are
|
||||
* clamped to the data, and a start after the end is read as the reverse.
|
||||
* @returns {{from: number, to: number, all: boolean}|null}
|
||||
*/
|
||||
export function periodRange(period, extent) {
|
||||
if (!extent) return null;
|
||||
const clamp = (year, fallback) =>
|
||||
Math.min(extent.max, Math.max(extent.min, year ?? fallback));
|
||||
const start = clamp(period.from, extent.min);
|
||||
const end = clamp(period.to, extent.max);
|
||||
const from = Math.min(start, end);
|
||||
const to = Math.max(start, end);
|
||||
return { from, to, all: from === extent.min && to === extent.max };
|
||||
}
|
||||
|
||||
/** Whether a record falls in the range; undated records only when all years show. */
|
||||
export function inPeriod(record, range) {
|
||||
if (!range || range.all) return true;
|
||||
const year = recordYear(record);
|
||||
return year !== null && year >= range.from && year <= range.to;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
inPeriod,
|
||||
normalizePeriod,
|
||||
periodRange,
|
||||
recordYear,
|
||||
yearExtent,
|
||||
} from './period.js';
|
||||
|
||||
const RECORDS = [
|
||||
{ year: 1588 },
|
||||
{ year: 1912 },
|
||||
{ year: 2012 },
|
||||
{ year: null, ancient: true },
|
||||
];
|
||||
|
||||
test('years come from the record; the extent ignores undated wrecks', () => {
|
||||
assert.equal(recordYear(RECORDS[1]), 1912);
|
||||
assert.equal(recordYear(RECORDS[3]), null);
|
||||
assert.deepEqual(yearExtent(RECORDS), { min: 1588, max: 2012 });
|
||||
assert.equal(yearExtent([{ year: null }]), null);
|
||||
});
|
||||
|
||||
test('all years show every wreck; a narrower period hides undated ones', () => {
|
||||
const extent = yearExtent(RECORDS);
|
||||
const all = periodRange({ from: null, to: null }, extent);
|
||||
assert.deepEqual(all, { from: 1588, to: 2012, all: true });
|
||||
assert.ok(RECORDS.every((record) => inPeriod(record, all)));
|
||||
const wars = periodRange(normalizePeriod({ from: '1914', to: 1945 }), extent);
|
||||
assert.deepEqual(
|
||||
RECORDS.map((record) => inPeriod(record, wars)),
|
||||
[false, false, false, false],
|
||||
);
|
||||
const reversed = periodRange({ from: 1950, to: 1900 }, extent);
|
||||
assert.deepEqual(reversed, { from: 1900, to: 1950, all: false });
|
||||
assert.equal(inPeriod(RECORDS[1], reversed), true);
|
||||
});
|
||||
+21
-2
@@ -445,6 +445,8 @@ export class LayerPanel {
|
||||
this._selectListItem(layerId, itemId, generation),
|
||||
onSlider: (layerId, sliderId, value) =>
|
||||
this._moveSlider(layerId, sliderId, value, generation),
|
||||
onRange: (layerId, rangeId, end, value) =>
|
||||
this._pickRangeEnd(layerId, rangeId, end, value, generation),
|
||||
}),
|
||||
id === null ? this._toggleContainer : this._groupContainers.get(id),
|
||||
);
|
||||
@@ -474,7 +476,7 @@ export class LayerPanel {
|
||||
}
|
||||
|
||||
/**
|
||||
* A layer's row chips, one-of-many switch, slider, color legend, info line
|
||||
* A layer's row chips, one-of-many switch, slider, start/end pickers, color legend, info line
|
||||
* and ordered list. The block
|
||||
* is hidden while the layer is off (or while a dependency owner has
|
||||
* surrendered it), so a quiet row stays quiet. Chips and list items are
|
||||
@@ -501,6 +503,7 @@ export class LayerPanel {
|
||||
const infoText = controls?.info ? String(controls.info) : '';
|
||||
const segmented = controls?.segmented || null;
|
||||
const slider = controls?.slider || null;
|
||||
const range = controls?.range || null;
|
||||
// The info line, once shown, stays in place and hides when empty.
|
||||
if (infoText) row.info = true;
|
||||
return {
|
||||
@@ -509,10 +512,12 @@ export class LayerPanel {
|
||||
legend.length === 0 &&
|
||||
!infoText &&
|
||||
!segmented &&
|
||||
!slider,
|
||||
!slider &&
|
||||
!range,
|
||||
chips,
|
||||
segmented,
|
||||
slider,
|
||||
range,
|
||||
legend,
|
||||
info: row.info
|
||||
? { text: infoText, title: controls?.infoTitle || '' }
|
||||
@@ -577,6 +582,20 @@ export class LayerPanel {
|
||||
this.setLayerParams(id, slider.params(value), { origin: 'user' });
|
||||
}
|
||||
|
||||
_pickRangeEnd(id, rangeId, end, value, generation = this._generation) {
|
||||
if (this._destroyed || this._generation !== generation) return;
|
||||
const range = this._rowControlsFor(id)?.range;
|
||||
if (
|
||||
!range ||
|
||||
range.id !== rangeId ||
|
||||
(end !== 'from' && end !== 'to') ||
|
||||
typeof range.params !== 'function' ||
|
||||
!this.isEnabled(id)
|
||||
)
|
||||
return;
|
||||
this.setLayerParams(id, range.params(end, value), { origin: 'user' });
|
||||
}
|
||||
|
||||
_selectListItem(id, itemId, generation = this._generation) {
|
||||
if (this._destroyed || this._generation !== generation) return;
|
||||
const item = this._rowControlsFor(id)?.list?.items?.find(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import RailChips from '../rail/RailChips.vue';
|
||||
import RailRowList from '../rail/RailRowList.vue';
|
||||
import RowRange from './RowRange.vue';
|
||||
import RowSlider from './RowSlider.vue';
|
||||
import RowSwitch from './RowSwitch.vue';
|
||||
import RowText from './RowText.vue';
|
||||
@@ -15,6 +16,7 @@ defineProps({
|
||||
onChip: { type: Function, default: () => {} },
|
||||
onListItem: { type: Function, default: () => {} },
|
||||
onSlider: { type: Function, default: () => {} },
|
||||
onRange: { type: Function, default: () => {} },
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -68,6 +70,13 @@ defineProps({
|
||||
:on-input="
|
||||
(value) => onSlider(item.id, item.controls.slider.id, value)
|
||||
"
|
||||
/><RowRange
|
||||
v-if="item.controls.range"
|
||||
:range="item.controls.range"
|
||||
:on-select="
|
||||
(end, value) =>
|
||||
onRange(item.id, item.controls.range.id, end, value)
|
||||
"
|
||||
/><span
|
||||
v-for="(entry, index) in item.controls.legend"
|
||||
:key="index"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
// A row's start and end pickers on one line (e.g. the Air Accidents period).
|
||||
// Both ends share one option list; `onSelect` reports which end moved
|
||||
// ('from' or 'to') and the chosen option value.
|
||||
defineProps({
|
||||
range: { type: Object, required: true },
|
||||
onSelect: { type: Function, default: () => {} },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="data-toggle-range" :data-range-id="range.id"
|
||||
><div class="data-toggle-slider-head"
|
||||
><span class="data-toggle-slider-label">{{ range.label }}</span
|
||||
><span class="data-toggle-slider-value gev-slider-value">{{
|
||||
range.valueText
|
||||
}}</span></div
|
||||
><div class="data-toggle-range-ends"
|
||||
><label
|
||||
v-for="end in ['from', 'to']"
|
||||
:key="end"
|
||||
class="data-toggle-range-end"
|
||||
><span class="data-toggle-range-caption">{{ range[end].label }}</span
|
||||
><select
|
||||
class="data-toggle-range-select"
|
||||
:data-range-end="end"
|
||||
:aria-label="range[end].ariaLabel || undefined"
|
||||
:value="String(range[end].value)"
|
||||
@change="onSelect(end, $event.target.value)"
|
||||
><option
|
||||
v-for="option in range.options"
|
||||
:key="option.value"
|
||||
:value="String(option.value)"
|
||||
>{{ option.label }}</option
|
||||
></select
|
||||
></label
|
||||
></div
|
||||
></div
|
||||
>
|
||||
</template>
|
||||
@@ -159,3 +159,62 @@ test('a row switch and slider render their markup and report choices', () => {
|
||||
assert.equal(segments[1].classList.contains('active'), true);
|
||||
render(null, container);
|
||||
});
|
||||
|
||||
test('a row range renders start and end pickers and reports which end moved', () => {
|
||||
document.body.replaceChildren();
|
||||
const container = document.body.appendChild(document.createElement('div'));
|
||||
const picks = [];
|
||||
const item = {
|
||||
...row(false),
|
||||
key: 'air-accidents',
|
||||
id: 'air-accidents',
|
||||
controls: {
|
||||
hidden: false,
|
||||
chips: [],
|
||||
segmented: null,
|
||||
slider: null,
|
||||
range: {
|
||||
id: 'period',
|
||||
label: 'Period',
|
||||
valueText: '2010–2024',
|
||||
from: { label: 'From', ariaLabel: 'First year', value: 2010 },
|
||||
to: { label: 'To', ariaLabel: 'Last year', value: 2024 },
|
||||
options: [2024, 2010, 2007].map((year) => ({
|
||||
value: year,
|
||||
label: String(year),
|
||||
})),
|
||||
},
|
||||
legend: [],
|
||||
info: null,
|
||||
list: null,
|
||||
},
|
||||
};
|
||||
render(
|
||||
h(LayerRows, {
|
||||
items: [item],
|
||||
onRange: (id, rangeId, end, value) =>
|
||||
picks.push([id, rangeId, end, value]),
|
||||
}),
|
||||
container,
|
||||
);
|
||||
const block = container.querySelector('.data-toggle-range');
|
||||
assert.equal(block.dataset.rangeId, 'period');
|
||||
assert.equal(
|
||||
block.querySelector('.data-toggle-slider-value').textContent,
|
||||
'2010–2024',
|
||||
);
|
||||
const [from, to] = block.querySelectorAll('select.data-toggle-range-select');
|
||||
assert.equal(from.getAttribute('aria-label'), 'First year');
|
||||
assert.equal(from.value, '2010');
|
||||
assert.equal(to.value, '2024');
|
||||
assert.deepEqual(
|
||||
[...block.querySelectorAll('.data-toggle-range-caption')].map(
|
||||
(caption) => caption.textContent,
|
||||
),
|
||||
['From', 'To'],
|
||||
);
|
||||
to.value = '2007';
|
||||
to.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
assert.deepEqual(picks, [['air-accidents', 'period', 'to', '2007']]);
|
||||
render(null, container);
|
||||
});
|
||||
@@ -1067,6 +1067,43 @@ html:not([lang='en']) .data-row-list-text {
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
|
||||
/* Optional start and end pickers on one line (Air Accidents period). The
|
||||
shared field style paints each select. */
|
||||
.data-toggle-range {
|
||||
flex-basis: 100%;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.data-toggle-range-ends {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.data-toggle-range-end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.data-toggle-range-caption {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 8px;
|
||||
letter-spacing: 1.2px;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.data-toggle-range-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 24px;
|
||||
font: 10px/1 var(--font-mono);
|
||||
}
|
||||
|
||||
/* Optional per-layer quantitative slider (Active Fires minimum FRP). The
|
||||
track is painted by the layer (`--slider-track`, a stepped color ramp), so
|
||||
the slider doubles as the marker legend; ticks sit under their thresholds. */
|
||||
|
||||
Reference in new issue
Block a user