Tonight
This commit is contained in:
parent
28323f145e
commit
94f9c0d594
76 changed files with 3238 additions and 1230 deletions
|
|
@ -21,6 +21,22 @@ interface HexagonSnapshot {
|
|||
bounds: Bounds;
|
||||
}
|
||||
|
||||
interface PostcodeFeature {
|
||||
type?: string;
|
||||
properties: {
|
||||
postcode: string;
|
||||
count: number;
|
||||
centroid: [number, number];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface PostcodeSnapshot {
|
||||
features: PostcodeFeature[];
|
||||
bounds: Bounds;
|
||||
}
|
||||
|
||||
export interface HexagonClickTarget {
|
||||
h3: string;
|
||||
x: number;
|
||||
|
|
@ -46,6 +62,7 @@ export class DashboardRecorder {
|
|||
private mapDataVersion = 0;
|
||||
private selectionStatsVersion = 0;
|
||||
private lastHexagons: HexagonSnapshot | null = null;
|
||||
private lastPostcodes: PostcodeSnapshot | null = null;
|
||||
|
||||
constructor(private readonly page: Page) {
|
||||
page.on('request', (request) => {
|
||||
|
|
@ -78,6 +95,9 @@ export class DashboardRecorder {
|
|||
}
|
||||
|
||||
async visibleHexagonTargets(limit = 8): Promise<HexagonClickTarget[]> {
|
||||
const postcodeTargets = await this.visiblePostcodeTargets(limit);
|
||||
if (postcodeTargets.length > 0) return postcodeTargets;
|
||||
|
||||
const snapshot = this.lastHexagons;
|
||||
if (!snapshot || snapshot.features.length === 0) {
|
||||
throw new Error('No recorded hexagon response is available for map clicking');
|
||||
|
|
@ -146,6 +166,11 @@ export class DashboardRecorder {
|
|||
}
|
||||
|
||||
if (kind === 'postcodes') {
|
||||
const body = await response.json().catch(() => null);
|
||||
const snapshot = parsePostcodeSnapshot(response.url(), body);
|
||||
if (snapshot) {
|
||||
this.lastPostcodes = snapshot;
|
||||
}
|
||||
this.mapDataVersion += 1;
|
||||
return;
|
||||
}
|
||||
|
|
@ -217,6 +242,56 @@ export class DashboardRecorder {
|
|||
.catch(() => false);
|
||||
return !loading && !connecting;
|
||||
}
|
||||
|
||||
private async visiblePostcodeTargets(limit: number): Promise<HexagonClickTarget[]> {
|
||||
const snapshot = this.lastPostcodes;
|
||||
if (!snapshot || snapshot.features.length === 0) return [];
|
||||
|
||||
const mapBox = await this.page.locator('[data-tutorial="map"]').boundingBox();
|
||||
if (!mapBox) throw new Error('Map container has no bounding box');
|
||||
|
||||
const projected = snapshot.features
|
||||
.filter((feature) => feature.properties.count > 0)
|
||||
.map((feature) => {
|
||||
const [lon, lat] = feature.properties.centroid;
|
||||
const point = projectFromBounds({ lat, lon }, snapshot.bounds, mapBox);
|
||||
if (!point) return null;
|
||||
const centerX = mapBox.x + mapBox.width / 2;
|
||||
const centerY = mapBox.y + mapBox.height / 2;
|
||||
const distanceFromCenter = Math.hypot(
|
||||
(point.x - centerX) / (mapBox.width / 2),
|
||||
(point.y - centerY) / (mapBox.height / 2)
|
||||
);
|
||||
return {
|
||||
h3: feature.properties.postcode,
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
count: feature.properties.count,
|
||||
score: feature.properties.count / (1 + distanceFromCenter * 0.25),
|
||||
};
|
||||
})
|
||||
.filter((target): target is HexagonClickTarget & { score: number } => target != null);
|
||||
|
||||
const onScreen = projected.filter(
|
||||
(target) =>
|
||||
target.x >= mapBox.x &&
|
||||
target.x <= mapBox.x + mapBox.width &&
|
||||
target.y >= mapBox.y &&
|
||||
target.y <= mapBox.y + mapBox.height
|
||||
);
|
||||
const clearOfChrome = onScreen.filter(
|
||||
(target) =>
|
||||
target.x >= mapBox.x + 80 &&
|
||||
target.x <= mapBox.x + mapBox.width - 130 &&
|
||||
target.y >= mapBox.y + 105 &&
|
||||
target.y <= mapBox.y + mapBox.height - 115
|
||||
);
|
||||
|
||||
const candidates = (clearOfChrome.length > 0 ? clearOfChrome : onScreen).sort(
|
||||
(a, b) => b.score - a.score
|
||||
);
|
||||
return candidates.slice(0, limit).map(({ score: _score, ...target }) => target);
|
||||
}
|
||||
}
|
||||
|
||||
function classifyApiRequest(rawUrl: string): ApiKind | null {
|
||||
|
|
@ -250,6 +325,18 @@ function parseHexagonSnapshot(rawUrl: string, body: unknown): HexagonSnapshot |
|
|||
return { features, bounds };
|
||||
}
|
||||
|
||||
function parsePostcodeSnapshot(rawUrl: string, body: unknown): PostcodeSnapshot | null {
|
||||
if (!isRecord(body) || !Array.isArray(body.features)) return null;
|
||||
const features = body.features.filter(isPostcodeFeature);
|
||||
if (features.length === 0) return null;
|
||||
|
||||
const url = new URL(rawUrl);
|
||||
const bounds = parseBounds(url.searchParams.get('bounds'));
|
||||
if (!bounds) return null;
|
||||
|
||||
return { features, bounds };
|
||||
}
|
||||
|
||||
function parseBounds(value: string | null): Bounds | null {
|
||||
if (!value) return null;
|
||||
const [south, west, north, east] = value.split(',').map(Number);
|
||||
|
|
@ -271,8 +358,21 @@ function isHexagonFeature(value: unknown): value is HexagonFeature {
|
|||
);
|
||||
}
|
||||
|
||||
function isPostcodeFeature(value: unknown): value is PostcodeFeature {
|
||||
if (!isRecord(value) || !isRecord(value.properties)) return false;
|
||||
const { postcode, count, centroid } = value.properties;
|
||||
return (
|
||||
typeof postcode === 'string' &&
|
||||
typeof count === 'number' &&
|
||||
Array.isArray(centroid) &&
|
||||
centroid.length === 2 &&
|
||||
typeof centroid[0] === 'number' &&
|
||||
typeof centroid[1] === 'number'
|
||||
);
|
||||
}
|
||||
|
||||
function projectFromBounds(
|
||||
feature: HexagonFeature,
|
||||
feature: { lat: number; lon: number },
|
||||
bounds: Bounds,
|
||||
mapBox: { x: number; y: number; width: number; height: number }
|
||||
): { x: number; y: number } | null {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue