mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
feat(ferratas): keep loaded via ferratas in a tile cache on server and device, group segments, own look and count badge
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { EntityRepositoryType, PrimaryKeyProp, defineEntity, p } from '@mikro-orm/core';
|
||||
import { FerrataTilesRepository } from '../repositories/FerrataTiles.repository';
|
||||
|
||||
export class FerrataTiles {
|
||||
[EntityRepositoryType]?: FerrataTilesRepository;
|
||||
[PrimaryKeyProp]?: 'tile_key';
|
||||
tile_key!: string;
|
||||
payload!: string;
|
||||
fetched_at!: number;
|
||||
}
|
||||
|
||||
export const FerrataTilesSchema = defineEntity({
|
||||
class: FerrataTiles,
|
||||
repository: () => FerrataTilesRepository,
|
||||
properties: {
|
||||
tile_key: p.text().primary(),
|
||||
payload: p.text(),
|
||||
fetched_at: p.integer(),
|
||||
},
|
||||
});
|
||||
@@ -31,6 +31,7 @@ import { DocumentConnectionsSchema } from './DocumentConnections.entity';
|
||||
import { DocumentProviderFieldsSchema } from './DocumentProviderFields.entity';
|
||||
import { DocumentProvidersSchema } from './DocumentProviders.entity';
|
||||
import { DocumentSyncItemsSchema } from './DocumentSyncItems.entity';
|
||||
import { FerrataTilesSchema } from './FerrataTiles.entity';
|
||||
import { FileLinksSchema } from './FileLinks.entity';
|
||||
import { GoogleApiUsageSchema } from './GoogleApiUsage.entity';
|
||||
import { GooglePlacePhotoMetaSchema } from './GooglePlacePhotoMeta.entity';
|
||||
@@ -165,6 +166,7 @@ export const ALL_ENTITIES: readonly EntitySchema[] = [
|
||||
DocumentProviderFieldsSchema,
|
||||
DocumentProvidersSchema,
|
||||
DocumentSyncItemsSchema,
|
||||
FerrataTilesSchema,
|
||||
FileLinksSchema,
|
||||
GoogleApiUsageSchema,
|
||||
GooglePlacePhotoMetaSchema,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { JsonColumn } from '../utils/json-column';
|
||||
import { FERRATAS_RESULT_CAP, ferrataSchema, type Ferrata } from '@trek/shared';
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
@@ -106,3 +107,20 @@ export const BUDGET_ITEM_TICKET: JsonColumn<{ items: { price?: string | number;
|
||||
.loose(),
|
||||
fallback: () => null,
|
||||
};
|
||||
|
||||
/** What one cell of the via ferrata grid holds: its ferratas, and whether the Overpass answer they came from was cut. */
|
||||
export interface FerrataTilePayload {
|
||||
ferratas: Ferrata[];
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* One via ferrata grid cell, as Overpass answered it. Checked against the wire
|
||||
* contract, so a stored row can go out as it is. A broken row reads as a cell
|
||||
* never fetched (null), and the next request asks Overpass for it again.
|
||||
*/
|
||||
export const FERRATA_TILE_PAYLOAD: JsonColumn<FerrataTilePayload | null> = {
|
||||
column: 'ferrata_tiles.payload',
|
||||
schema: z.object({ ferratas: z.array(ferrataSchema).max(FERRATAS_RESULT_CAP), truncated: z.boolean() }).nullable(),
|
||||
fallback: () => null,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Generated by scripts/generate-db-types.ts from the migrated schema. Do not edit by hand:
|
||||
// change the migration, then run `npm run gen:db-types`.
|
||||
|
||||
export interface FerrataTilesTable {
|
||||
tile_key: string;
|
||||
payload: string;
|
||||
fetched_at: number;
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import type { DocumentConnectionsTable } from './DocumentConnections.table';
|
||||
import type { DocumentProviderFieldsTable } from './DocumentProviderFields.table';
|
||||
import type { DocumentProvidersTable } from './DocumentProviders.table';
|
||||
import type { DocumentSyncItemsTable } from './DocumentSyncItems.table';
|
||||
import type { FerrataTilesTable } from './FerrataTiles.table';
|
||||
import type { FileLinksTable } from './FileLinks.table';
|
||||
import type { GoogleApiUsageTable } from './GoogleApiUsage.table';
|
||||
import type { GooglePlacePhotoMetaTable } from './GooglePlacePhotoMeta.table';
|
||||
@@ -174,6 +175,7 @@ export interface DB {
|
||||
document_provider_fields: DocumentProviderFieldsTable;
|
||||
document_providers: DocumentProvidersTable;
|
||||
document_sync_items: DocumentSyncItemsTable;
|
||||
ferrata_tiles: FerrataTilesTable;
|
||||
file_links: FileLinksTable;
|
||||
google_api_usage: GoogleApiUsageTable;
|
||||
google_place_photo_meta: GooglePlacePhotoMetaTable;
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
/**
|
||||
* `ferrata_tiles` keeps what Overpass answered for one cell of the via
|
||||
* ferrata grid (0.05 degrees a side), so a restart does not send every map
|
||||
* pan back to the public mirrors.
|
||||
*
|
||||
* `tile_key` names the cell by its grid indices (`"<lat index>,<lng index>"`),
|
||||
* `payload` is the JSON of the cell's ferratas and whether the answer was cut,
|
||||
* and `fetched_at` is the epoch of that answer in seconds (not ms, so it fits
|
||||
* the 32-bit integer this column is on Postgres). Nothing references the
|
||||
* table and nothing deletes from it: an old cell is served and refreshed, not
|
||||
* dropped.
|
||||
*/
|
||||
export class Migration20200101043200_via_ferrata_tiles_outlive_a_restart extends Migration {
|
||||
override name = 'Migration20200101043200_via_ferrata_tiles_outlive_a_restart';
|
||||
|
||||
override up(): void {
|
||||
this.addSql(`
|
||||
CREATE TABLE IF NOT EXISTS ferrata_tiles (
|
||||
tile_key TEXT PRIMARY KEY NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
fetched_at INTEGER NOT NULL
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { FerrataTiles } from '../entities/FerrataTiles.entity';
|
||||
import { TrekRepository } from './_shared/trek-repository';
|
||||
|
||||
export interface FerrataTileRow {
|
||||
tile_key: string;
|
||||
payload: string;
|
||||
/** When Overpass answered for the cell, as an epoch in seconds. */
|
||||
fetched_at: number;
|
||||
}
|
||||
|
||||
export class FerrataTilesRepository extends TrekRepository<FerrataTiles> {
|
||||
/** The stored cells among `keys`, in no particular order; a key never stored is simply absent. */
|
||||
async findByKeys(keys: readonly string[]): Promise<FerrataTileRow[]> {
|
||||
if (keys.length === 0) return [];
|
||||
const rows = await this.find({ tile_key: { $in: [...keys] } }, { fields: ['tile_key', 'payload', 'fetched_at'] });
|
||||
return rows.map((row) => ({ tile_key: row.tile_key, payload: row.payload, fetched_at: row.fetched_at }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes every cell, replacing the payload and fetch time of one already
|
||||
* stored. The caller wraps it in a transaction, so a batch lands whole.
|
||||
*/
|
||||
async upsertTiles(rows: readonly FerrataTileRow[]): Promise<void> {
|
||||
if (rows.length === 0) return;
|
||||
await this.upsertMany(
|
||||
rows.map((row) => ({ ...row })),
|
||||
{ onConflictFields: ['tile_key'], onConflictAction: 'merge' },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user