Merge pull request #14 from avalon-vanguard/feat/neighbours-named
Name the neighbours that have a name, before the ones that only have a number
This commit is contained in:
+12
-14
@@ -17,23 +17,21 @@ test.describe('Neighbour jump', () => {
|
|||||||
await expect(readout).toHaveText('Sol', { timeout: 30_000 });
|
await expect(readout).toHaveText('Sol', { timeout: 30_000 });
|
||||||
|
|
||||||
// Its nearest neighbours are named around the edge of the view; each is a button that flies
|
// Its nearest neighbours are named around the edge of the view; each is a button that flies
|
||||||
// there. Whichever one the ring names, not a star named here: the catalogue is refreshed on
|
// there. Barnard's Star by name, on purpose: the ring ranks named stars ahead of survey
|
||||||
// a schedule, and a refresh reorders which four are nearest — a hand-picked name made this a
|
// designations, and the four nearest named to the Sun — Proxima, Rigil Kentaurus, Toliman,
|
||||||
// test of the catalogue's contents rather than of the ring. Which of them survives the
|
// Barnard's — are a fact about space, not about which catalogue was refreshed last. Before
|
||||||
// declutter is a property of the view, so the first is as good as any.
|
// that preference, a Gaia row duplicating Proxima took fourth place and this very label was
|
||||||
const neighbour = page.getByRole('button', { name: /^Go to .+ pc away$/ }).first();
|
// the one that vanished, so it is also the regression test for it. Barnard's rather than the
|
||||||
|
// Alpha Centauri trio because those three share one bearing and are decluttered down to
|
||||||
|
// whichever the label pass reaches first.
|
||||||
|
const neighbour = page.getByRole('button', { name: /Barnard's Star/ });
|
||||||
await expect(neighbour).toBeVisible({ timeout: 30_000 });
|
await expect(neighbour).toBeVisible({ timeout: 30_000 });
|
||||||
const label = (await neighbour.getAttribute('aria-label'))!;
|
await neighbour.click();
|
||||||
const name = /^Go to (.+), [\d.]+ pc away$/.exec(label)![1];
|
|
||||||
|
|
||||||
// Clicked by the name just read rather than through `neighbour`, which re-resolves `.first()`
|
await expect(readout).toHaveText("Barnard's Star", { timeout: 45_000 });
|
||||||
// and could land on a different star if the label pass reorders the ring in between.
|
|
||||||
await page.getByRole('button', { name: label, exact: true }).click();
|
|
||||||
|
|
||||||
await expect(readout).toHaveText(name, { timeout: 45_000 });
|
|
||||||
// And from there the walk goes on: the new system names its own neighbours. Which ones is
|
// And from there the walk goes on: the new system names its own neighbours. Which ones is
|
||||||
// not asserted — neighbours sharing a bearing are decluttered, so which of them survives is
|
// not asserted — several of Barnard's nearest share a bearing, so which of them survives
|
||||||
// a property of the view, not a fact about the catalogue.
|
// the declutter is a property of the view, not a fact about the catalogue.
|
||||||
await expect(page.locator('.map-label--ghost')).not.toHaveCount(0, { timeout: 30_000 });
|
await expect(page.locator('.map-label--ghost')).not.toHaveCount(0, { timeout: 30_000 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { DeepSkyRecord } from '../../shared/models/deepsky.model';
|
|||||||
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
||||||
import { applyMilkyWaySkybox, createGlowSprite } from '../../shared/rendering/skybox';
|
import { applyMilkyWaySkybox, createGlowSprite } from '../../shared/rendering/skybox';
|
||||||
import { loadCachedTexture, MILKY_WAY_SKYBOX_PATH, SUN_TEXTURE_PATH } from '../../shared/rendering/texture-catalog';
|
import { loadCachedTexture, MILKY_WAY_SKYBOX_PATH, SUN_TEXTURE_PATH } from '../../shared/rendering/texture-catalog';
|
||||||
|
import { isDesignation } from '../../shared/models/star-catalog';
|
||||||
import { StarRecord } from '../../shared/models/star.model';
|
import { StarRecord } from '../../shared/models/star.model';
|
||||||
import { Bookmark } from '../../shared/state/bookmarks.store';
|
import { Bookmark } from '../../shared/state/bookmarks.store';
|
||||||
import { NavigationStore, ViewLevel } from '../../shared/state/navigation.store';
|
import { NavigationStore, ViewLevel } from '../../shared/state/navigation.store';
|
||||||
@@ -855,7 +856,14 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
|
|||||||
// position: a neighbour whose separation rounds to what no separation prints as is not a
|
// position: a neighbour whose separation rounds to what no separation prints as is not a
|
||||||
// place to go, it is the same place. Compared through the formatter rather than against a
|
// place to go, it is the same place. Compared through the formatter rather than against a
|
||||||
// hand-picked epsilon, so the rule stays "would print as zero" whatever the formatter does.
|
// hand-picked epsilon, so the rule stays "would print as zero" whatever the formatter does.
|
||||||
.nearest(origin.id, NEIGHBOUR_COUNT * 2)
|
//
|
||||||
|
// Named stars first, survey designations only where fewer than that are in reach: a ring
|
||||||
|
// that exists to say where you are is not helped by "Gaia DR3 5853498713190525696" —
|
||||||
|
// least of all when that is the same star as the Proxima Centauri printed beside it.
|
||||||
|
.nearestPreferring(origin.id, NEIGHBOUR_COUNT * 2, (point) => {
|
||||||
|
const star = this.starsById.get(point.id);
|
||||||
|
return star !== undefined && !isDesignation(star);
|
||||||
|
})
|
||||||
.filter((neighbour) => formatParsecs(neighbour.distancePc) !== formatParsecs(0))
|
.filter((neighbour) => formatParsecs(neighbour.distancePc) !== formatParsecs(0))
|
||||||
.slice(0, NEIGHBOUR_COUNT)
|
.slice(0, NEIGHBOUR_COUNT)
|
||||||
.flatMap((neighbour) => {
|
.flatMap((neighbour) => {
|
||||||
|
|||||||
@@ -74,6 +74,16 @@ describe('StarNeighbourhood', () => {
|
|||||||
expect(ids(index.nearest(4, 2, (point) => point.id % 2 === 0))).toEqual([2, 6]);
|
expect(ids(index.nearest(4, 2, (point) => point.id % 2 === 0))).toEqual([2, 6]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('puts the stars it is told to prefer first, and fills with the rest only when short', () => {
|
||||||
|
const index = new StarNeighbourhood(line(10));
|
||||||
|
const even = (point: StarPoint) => point.id % 2 === 0;
|
||||||
|
|
||||||
|
// Enough even stars: the odd ones next door, though nearer, do not get a look in.
|
||||||
|
expect(ids(index.nearestPreferring(4, 2, even))).toEqual([2, 6]);
|
||||||
|
// Not enough: every even star in reach, then the nearest of the others.
|
||||||
|
expect(ids(index.nearestPreferring(4, 6, even))).toEqual([2, 6, 0, 8, 3, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
it('answers nothing for a star it has never heard of', () => {
|
it('answers nothing for a star it has never heard of', () => {
|
||||||
const index = new StarNeighbourhood(line(3));
|
const index = new StarNeighbourhood(line(3));
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,20 @@ export class StarNeighbourhood {
|
|||||||
return index === undefined ? undefined : this.points[index];
|
return index === undefined ? undefined : this.points[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like `nearest`, but the stars `prefer` accepts come first, and the rest only fill what is
|
||||||
|
* left. The preferred pass exhausts the search before the fill runs, so a preferred star is
|
||||||
|
* never outranked by an ordinary one that happens to be closer — that is the point of asking.
|
||||||
|
*/
|
||||||
|
nearestPreferring(id: number, count: number, prefer: (point: StarPoint) => boolean): Neighbour[] {
|
||||||
|
const preferred = this.nearest(id, count, prefer);
|
||||||
|
if (preferred.length >= count) {
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
const taken = new Set(preferred.map((neighbour) => neighbour.id));
|
||||||
|
return preferred.concat(this.nearest(id, count - preferred.length, (point) => !taken.has(point.id)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `count` stars nearest to `id`, nearest first, excluding the star itself.
|
* The `count` stars nearest to `id`, nearest first, excluding the star itself.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { BYTES_PER_STAR_META, BYTES_PER_STAR_POSITION, decodeStarCatalog, encodeStarCatalog } from './star-catalog';
|
import { BYTES_PER_STAR_META, BYTES_PER_STAR_POSITION, decodeStarCatalog, encodeStarCatalog, isDesignation } from './star-catalog';
|
||||||
import { StarRecord } from './star.model';
|
import { StarRecord } from './star.model';
|
||||||
|
|
||||||
const STARS: StarRecord[] = [
|
const STARS: StarRecord[] = [
|
||||||
@@ -131,4 +131,18 @@ describe('star catalogue provenance and derived names', () => {
|
|||||||
expect(named.index.names).toEqual(['Some Proper Name']);
|
expect(named.index.names).toEqual(['Some Proper Name']);
|
||||||
expect(decodeStarCatalog(named.index, named.positions, named.meta)[0].name).toBe('Some Proper Name');
|
expect(decodeStarCatalog(named.index, named.positions, named.meta)[0].name).toBe('Some Proper Name');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tells a name somebody gave from the designation a survey generates', () => {
|
||||||
|
expect(isDesignation(decoded[0])).toBe(false);
|
||||||
|
expect(isDesignation(decoded[1])).toBe(true);
|
||||||
|
// The real catalogue names Gaia stars by the survey's nineteen-digit source id, which no
|
||||||
|
// 32-bit row id can equal — so it is the prefix that decides, not a round trip through the id.
|
||||||
|
expect(isDesignation({ ...MIXED[1], name: 'Gaia DR3 5853498713190525696' })).toBe(true);
|
||||||
|
expect(isDesignation({ ...MIXED[1], name: 'Proxima Centauri' })).toBe(false);
|
||||||
|
// HYG's own last resort, once every designation it knows has come up empty. Upper case,
|
||||||
|
// whatever case the source id is in.
|
||||||
|
expect(isDesignation({ ...MIXED[0], name: 'HYG 5' })).toBe(true);
|
||||||
|
expect(isDesignation({ ...MIXED[0], source: undefined, name: 'HYG 5' })).toBe(true);
|
||||||
|
expect(isDesignation({ ...MIXED[0], name: 'HD 48915' })).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -66,11 +66,14 @@ export interface StarCatalogIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How a source names a star that has no name of its own. `Gaia DR3 <id>` for Gaia; HYG's own
|
* How a source names a star that has no name of its own. `Gaia DR3 <id>` for Gaia; `HYG <id>`
|
||||||
* fallbacks already produce real designations, so it never needs this.
|
* for HYG, whose ETL reaches for that only after a proper name, Bayer, Flamsteed, HD, Gliese and
|
||||||
|
* HIP have all come up empty (`tools/etl/fetchStars.ts`) — none in the current catalogue, but
|
||||||
|
* the path is there, and a name made that way is no more a name than Gaia's.
|
||||||
*/
|
*/
|
||||||
const DESIGNATION_PREFIXES: Readonly<Record<string, string>> = {
|
const DESIGNATION_PREFIXES: Readonly<Record<string, string>> = {
|
||||||
gaia: 'Gaia DR3'
|
gaia: 'Gaia DR3',
|
||||||
|
hyg: 'HYG'
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StarMetaColumns {
|
interface StarMetaColumns {
|
||||||
@@ -161,6 +164,18 @@ function designationFor(prefix: string | undefined, id: number): string | undefi
|
|||||||
return prefix === undefined ? undefined : `${prefix} ${id}`;
|
return prefix === undefined ? undefined : `${prefix} ${id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a star's name is only the designation its source generates, rather than anything
|
||||||
|
* somebody called it. Judged by the prefix alone: the number after it is the survey's own id —
|
||||||
|
* nineteen digits for Gaia — which the 32-bit row id `designationFor` prints cannot hold, so a
|
||||||
|
* round-trip through the id would call every one of those stars named.
|
||||||
|
*/
|
||||||
|
export function isDesignation(star: StarRecord): boolean {
|
||||||
|
// No source at all is a single-catalogue build, whose fallback is HYG's — see `decodeStarCatalog`.
|
||||||
|
const prefix = star.source === undefined ? DESIGNATION_PREFIXES['hyg'] : (DESIGNATION_PREFIXES[star.source] ?? star.source);
|
||||||
|
return star.name.startsWith(`${prefix} `);
|
||||||
|
}
|
||||||
|
|
||||||
/** Rebuilds the star records the app works with from the three loaded assets. */
|
/** Rebuilds the star records the app works with from the three loaded assets. */
|
||||||
export function decodeStarCatalog(index: StarCatalogIndex, positions: Float32Array, meta: ArrayBuffer): StarRecord[] {
|
export function decodeStarCatalog(index: StarCatalogIndex, positions: Float32Array, meta: ArrayBuffer): StarRecord[] {
|
||||||
const columns = metaColumns(meta, index.count);
|
const columns = metaColumns(meta, index.count);
|
||||||
|
|||||||
Reference in New Issue
Block a user