fix(news): an application's picture cards stay at every height

Zoomed out, a hotspot's reports give way to its beacon, and the cards an application draws through pictureCards went with them; they came back only once the camera had stopped after zooming in. The cards now come from every report the close-up view would mark, at any height; the layer's own markers and beacons are unchanged.
This commit is contained in:
Sameh Khamis committed 2026-11-11 19:43:06 -08:00
1 parent 202cb8c3dc
commit a105e819e0
3 files changed
+44 -3

No files matched your search

+1 -1
View File
@@ -6972,7 +6972,7 @@ request viewbox and continues searching without that geographic preference.
### News (November 2026) ### News (November 2026)
- **What it shows.** Events ▸ **News**: the last 24 hours only, or the fewer hours a server names. Zoomed out, one beacon per hotspot with its count for the window and its trend, and a breathing beacon per developing situation (still under reduced motion). Below 2,500 km (7,500 km with a hotspot chosen), the hotspot outlines with disputed areas dotted and named, and every placed report as a ground-clamped marker: fill is severity (0 grey no physical event, 1 yellow minor, 2 orange significant, 3 red major), the icon is the category, the rim is attestation (solid confirmed, dashed claim, thin single), size grows with sources and the marker fades over the day. Colour never marks a side. Up to ten picture cards (physical events with two or more sources, major or pressing) stand over their markers without overlapping. An app built on Core may draw the picture cards itself: `createApplicationCatalog({ newsOptions: { pictureCards } })` calls `pictureCards(cards, { selectedId })` on each redraw with one card per event on the map that has a picture, single source or not, most severe then newest first (`{ id, title, lat, lon, publishedAt, severity, color, image, video, credit }`, at the marker), and the selected report's id or null (its own card still stands at its marker); with no cards when the layer turns off; the layer then paints none of its own, and a click on one is `setParams({ reportId, focus: true })`. A selected report's card reads "Source: host ↗" and opens the article. - **What it shows.** Events ▸ **News**: the last 24 hours only, or the fewer hours a server names. Zoomed out, one beacon per hotspot with its count for the window and its trend, and a breathing beacon per developing situation (still under reduced motion). Below 2,500 km (7,500 km with a hotspot chosen), the hotspot outlines with disputed areas dotted and named, and every placed report as a ground-clamped marker: fill is severity (0 grey no physical event, 1 yellow minor, 2 orange significant, 3 red major), the icon is the category, the rim is attestation (solid confirmed, dashed claim, thin single), size grows with sources and the marker fades over the day. Colour never marks a side. Up to ten picture cards (physical events with two or more sources, major or pressing) stand over their markers without overlapping. An app built on Core may draw the picture cards itself: `createApplicationCatalog({ newsOptions: { pictureCards } })` calls `pictureCards(cards, { selectedId })` on each redraw with one card per event on the map that has a picture, single source or not, at every height (zoomed out, where a hotspot stands as one beacon, its reports' pictures still come), most severe then newest first (`{ id, title, lat, lon, publishedAt, severity, color, image, video, credit }`, at the marker), and the selected report's id or null (its own card still stands at its marker); with no cards when the layer turns off; the layer then paints none of its own, and a click on one is `setParams({ reportId, focus: true })`. A selected report's card reads "Source: host ↗" and opens the article.
- **Time.** The layer reads its time from the world clock (`worldClock.timeFor('news')`, passed as `now` in `src/app/constructCatalog.js`): report ages, the 24-hour window, "today", fading and the pressing and card rules all count from it. Core does not follow the clock for the layer, so this is the wall clock and nothing changes. A host whose news source can answer a past time may follow it for `news`; the layer then shows that time's news as it was then, with ages counted from the timeline. - **Time.** The layer reads its time from the world clock (`worldClock.timeFor('news')`, passed as `now` in `src/app/constructCatalog.js`): report ages, the 24-hour window, "today", fading and the pressing and card rules all count from it. Core does not follow the clock for the layer, so this is the wall clock and nothing changes. A host whose news source can answer a past time may follow it for `news`; the layer then shows that time's news as it was then, with ages counted from the timeline.
- **Panel.** The right-rail NEWS panel (`src/ui/newsPanel.js`, `src/ui/news/`) has a time window at the top (the server's 24 h, or 12, 6, 3 or 1 h; the hotspot counts are read for the chosen window) and two sections. **Conflicts**: hotspot chips with counts, a severity floor (significant and above by default), a category filter, the chosen event, and a continuous feed, newest first. **Local**, while the camera is below 3,000 km: every report in the area at the view centre, newest first, named at the top ("News from …") and outlined on the globe, which then shows those reports instead of the hotspots' news; at sea, reports within 500 km. A disputed area of the bundled list (11, see `src/data/local_data/natural_earth/README.md`), or a territory Natural Earth gives no country code (Somaliland, Northern Cyprus and others), is named as itself and read from no country's outlets; elsewhere Natural Earth's default country lines apply. Each card shows the publisher's picture when the event has one (hotlinked, no referrer, publisher credited), the headline (a model's English headline marked as such), a mono meta line (severity marker, category, place, sources, age), who reported it, attestation and media chips, and the service it came through ("via GDELT Project"). Open, a card plays the post's clip on request. A click flies the camera straight down from 150 km and selects the report. - **Panel.** The right-rail NEWS panel (`src/ui/newsPanel.js`, `src/ui/news/`) has a time window at the top (the server's 24 h, or 12, 6, 3 or 1 h; the hotspot counts are read for the chosen window) and two sections. **Conflicts**: hotspot chips with counts, a severity floor (significant and above by default), a category filter, the chosen event, and a continuous feed, newest first. **Local**, while the camera is below 3,000 km: every report in the area at the view centre, newest first, named at the top ("News from …") and outlined on the globe, which then shows those reports instead of the hotspots' news; at sea, reports within 500 km. A disputed area of the bundled list (11, see `src/data/local_data/natural_earth/README.md`), or a territory Natural Earth gives no country code (Somaliland, Northern Cyprus and others), is named as itself and read from no country's outlets; elsewhere Natural Earth's default country lines apply. Each card shows the publisher's picture when the event has one (hotlinked, no referrer, publisher credited), the headline (a model's English headline marked as such), a mono meta line (severity marker, category, place, sources, age), who reported it, attestation and media chips, and the service it came through ("via GDELT Project"). Open, a card plays the post's clip on request. A click flies the camera straight down from 150 km and selects the report.
- **Notices.** A section whose key the server lacks says "{provider} key required" and names the variable; a refused key says "{provider} refused its key"; a spent daily allowance says "{provider} limit reached for today". The other section keeps working. - **Notices.** A section whose key the server lacks says "{provider} key required" and names the variable; a refused key says "{provider} refused its key"; a spent daily allowance says "{provider} limit reached for today". The other section keeps working.
+15 -2
View File
@@ -172,7 +172,8 @@ const emptyInView = () => ({
* *
* `pictureCards(cards, { selectedId })`, when given, draws the picture cards * `pictureCards(cards, { selectedId })`, when given, draws the picture cards
* instead of the layer: it is called on every render with one card per * instead of the layer: it is called on every render with one card per
* pictured event on the map, in rank order (`{ id, title, lat, lon, * pictured event on the map, at every height (zoomed out too, where the
* hotspots stand as beacons), in rank order (`{ id, title, lat, lon,
* publishedAt, severity, color, image, video, credit }`), and the selected * publishedAt, severity, color, image, video, credit }`), and the selected
* report's id or null, whose own card the layer still draws at its marker; it * report's id or null, whose own card the layer still draws at its marker; it
* is called with no cards when the layer is turned off. * is called with no cards when the layer is turned off.
@@ -691,6 +692,16 @@ export function createNewsLayer({
} }
} }
// What a host's picture cards are chosen from when the hotspots stand as
// beacons: every report the close-up view would mark, and the selection.
function cardable() {
const current = selected();
const all = localMode() ? localReports(NEWS_MAP_LIMIT) : mapReports;
return current && !all.some((r) => r.id === current.id)
? [...all, current]
: all;
}
// With a `pictureCards` function the host draws the picture cards: one per // With a `pictureCards` function the host draws the picture cards: one per
// event on the map with a picture (its most severe, then newest report), // event on the map with a picture (its most severe, then newest report),
// most severe then newest first, at its marker. The host chooses which // most severe then newest first, at its marker. The host chooses which
@@ -825,7 +836,9 @@ export function createNewsLayer({
horizon.update(); horizon.update();
updatePulse(); updatePulse();
if (drawPictureCards) if (drawPictureCards)
drawPictureCards(enabled ? hostCards(drawn) : [], { // At every height: zoomed out, a hotspot's reports give way to its
// beacon, but their pictures still stand (the host keeps them apart).
drawPictureCards(enabled ? hostCards(detail ? drawn : cardable()) : [], {
selectedId: enabled ? selectedId : null, selectedId: enabled ? selectedId : null,
}); });
else pictureCards(drawn, current, labels); else pictureCards(drawn, current, labels);
+28
View File
@@ -296,6 +296,34 @@ test('a host that draws the picture cards is handed one card per pictured event
assert.deepEqual(handed.at(-1), { cards: [], selectedId: null }); assert.deepEqual(handed.at(-1), { cards: [], selectedId: null });
}); });
test("a host's picture cards stay at every height, beacons or markers", async (t) => {
const reports = [
report('major', {
severity: 3,
event: {
size: 1,
lastAt: '2026-10-28T09:00:00.000Z',
sides: [],
image: { url: 'https://example.com/major.jpg', publisher: 'Example' },
},
}),
];
const handed = [];
const { layer } = fixture(t, {
// The whole globe: the hotspot stands as one beacon, its reports unmarked.
height: 20_000_000,
source: { getFeed: async () => ({ reports, nextBefore: null }) },
pictureCards: (cards) => handed.push(cards),
});
layer.enable();
await layer.update();
layer.setParams({ filters: { minSeverity: 0 } });
assert.deepEqual(
handed.at(-1).map((card) => card.id),
['major'],
);
});
test('picture cards must be drawn by a function', () => { test('picture cards must be drawn by a function', () => {
assert.throws( assert.throws(
() => () =>