mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
maps: OSM details carry the values installations published for the element
A place found through Photon or Overpass, a relation or a way, is no row of the TREK index, so the community values the index lays over its own rows never reached it. Opening its details now asks /v1/community for the element and lays what is published over the record, with the version, where the TREK index is in use. No answer leaves the details as they were.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { trekCommunityByKeys, type TrekCommunityValues } from './trek-places.client';
|
||||
import { normalizePlaceWebsite } from '@trek/shared';
|
||||
|
||||
/**
|
||||
* Community values for places TREK did not find in its own index: a relation
|
||||
* Photon returned, a way from Overpass. The index rows carry them already
|
||||
* (the service lays them over its own answers); these places get them from
|
||||
* /v1/community, so a value one installation sent shows on the next open of
|
||||
* the same place everywhere.
|
||||
*/
|
||||
|
||||
/** The service's key of an OSM details id: `relation:3389505` is `osm:relation/3389505`. */
|
||||
export function communityKeyOf(placeId: string): string | null {
|
||||
const match = /^(node|way|relation):(\d+)$/.exec(placeId);
|
||||
return match ? `osm:${match[1]}/${match[2]}` : null;
|
||||
}
|
||||
|
||||
/** The details record with what the community published laid over it; untouched where nothing was. */
|
||||
export function withCommunityValues(
|
||||
details: Record<string, unknown>,
|
||||
values: TrekCommunityValues | undefined,
|
||||
): Record<string, unknown> {
|
||||
if (!values?.community) return details;
|
||||
const out: Record<string, unknown> = { ...details };
|
||||
const website = normalizePlaceWebsite(values.contact?.website);
|
||||
if (website) out.website = website;
|
||||
if (values.contact?.phone) out.phone = values.contact.phone;
|
||||
if (values.contact?.email) out.email = values.contact.email;
|
||||
if (values.hours?.osm) out.opening_hours = values.hours.osm;
|
||||
if (values.description?.text) out.description = values.description.text;
|
||||
if (typeof values.community.version === 'number') out.community_version = values.community.version;
|
||||
return out;
|
||||
}
|
||||
|
||||
/** OSM details as Overpass and Photon gave them, with the community's values for the same element. */
|
||||
export async function osmDetailsWithCommunity(
|
||||
placeId: string,
|
||||
details: Record<string, unknown> | null,
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
const key = communityKeyOf(placeId);
|
||||
if (!details || !key) return details;
|
||||
const published = await trekCommunityByKeys([key]);
|
||||
return withCommunityValues(details, published[key]);
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
import { PlaceDetailsCache } from '../../db/entities/PlaceDetailsCache.entity';
|
||||
import type { PlaceDetailsCacheRepository } from '../../db/repositories/PlaceDetailsCache.repository';
|
||||
import { osmDetailsWithCommunity } from './community-overlay';
|
||||
import { indexPlaceDetails } from './maps-index.helpers';
|
||||
import { toApiLang, isGooglePlaceId, OSM_PLACE_ID } from './maps.helpers';
|
||||
import { PlacesProviderSelector } from './places-provider.selector';
|
||||
@@ -84,7 +85,11 @@ export class PlaceDetailsResolver {
|
||||
if (isAmapPlaceId(placeId)) return this.amapDetails(userId, placeId, lang);
|
||||
|
||||
// OSM details: placeId is "node:123456" or "way:123456" etc.
|
||||
if (placeId.includes(':')) return { place: await this.osm.elementDetails(placeId, lang) };
|
||||
if (placeId.includes(':')) {
|
||||
const place = await this.osm.elementDetails(placeId, lang);
|
||||
// What installations published for this element, like the index rows carry it.
|
||||
return { place: indexEnabled ? await osmDetailsWithCommunity(placeId, place) : place };
|
||||
}
|
||||
|
||||
// Google details
|
||||
// 'en' default, aligned with search/autocomplete and the MCP tools' ?? 'en'
|
||||
|
||||
@@ -637,6 +637,31 @@ export async function trekPlacesById(gers: string, opts: { lang?: string } = {})
|
||||
}
|
||||
}
|
||||
|
||||
/** What the service publishes for a place found elsewhere (Photon, Overpass): the overlay of its own rows. */
|
||||
export interface TrekCommunityValues {
|
||||
contact?: { website?: string | null; phone?: string | null; email?: string | null } | null;
|
||||
hours?: { osm?: string | null } | null;
|
||||
description?: { text?: string | null } | null;
|
||||
community?: { version?: number; fields?: string[] } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The published community values of places that are no rows of the index, by
|
||||
* key (`osm:relation/3389505`). Nothing published, or the service not
|
||||
* answering, is an empty map: the details are shown as they are.
|
||||
*/
|
||||
export async function trekCommunityByKeys(keys: string[]): Promise<Record<string, TrekCommunityValues>> {
|
||||
if (keys.length === 0) return {};
|
||||
try {
|
||||
const body = await getJson<{ places?: Record<string, TrekCommunityValues> }>('/v1/community', {
|
||||
keys: keys.slice(0, 50).join(','),
|
||||
});
|
||||
return body.places ?? {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a TREK place onto the record shape the client already reads.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Community values for places outside the index: the key a details id maps to,
|
||||
* what is laid over the record, and that a place without published values, or
|
||||
* a service that does not answer, leaves the details as they were.
|
||||
*/
|
||||
import { communityKeyOf, osmDetailsWithCommunity, withCommunityValues } from '../../../src/nest/maps/community-overlay';
|
||||
import { trekCommunityByKeys } from '../../../src/nest/maps/trek-places.client';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../../src/nest/maps/trek-places.client', () => ({ trekCommunityByKeys: vi.fn() }));
|
||||
const ask = vi.mocked(trekCommunityByKeys);
|
||||
|
||||
const DETAILS = {
|
||||
name: 'Umeda Sky Building',
|
||||
website: null,
|
||||
phone: null,
|
||||
opening_hours: null,
|
||||
osm_id: 'relation:3389505',
|
||||
};
|
||||
|
||||
beforeEach(() => ask.mockReset());
|
||||
|
||||
describe('community values on OSM details', () => {
|
||||
it('COMOV-001: maps an OSM details id onto the service key, and nothing else', () => {
|
||||
expect(communityKeyOf('relation:3389505')).toBe('osm:relation/3389505');
|
||||
expect(communityKeyOf('node:1')).toBe('osm:node/1');
|
||||
expect(communityKeyOf('way:22')).toBe('osm:way/22');
|
||||
expect(communityKeyOf('gers:abc')).toBeNull();
|
||||
expect(communityKeyOf('relation:12;out')).toBeNull();
|
||||
});
|
||||
|
||||
it('COMOV-002: lays the published values over the record, with the version', () => {
|
||||
const got = withCommunityValues(DETAILS, {
|
||||
contact: { website: 'www.skybldg.co.jp', phone: '+81 6 6440 3855', email: null },
|
||||
hours: { osm: 'Mo-Su 09:30-22:30' },
|
||||
description: { text: 'A tower of two linked blocks.' },
|
||||
community: { version: 3, fields: ['website', 'phone', 'opening_hours', 'description'] },
|
||||
});
|
||||
expect(got.website).toMatch(/^https:\/\/www\.skybldg\.co\.jp\/?$/);
|
||||
expect(got).toMatchObject({
|
||||
phone: '+81 6 6440 3855',
|
||||
opening_hours: 'Mo-Su 09:30-22:30',
|
||||
description: 'A tower of two linked blocks.',
|
||||
community_version: 3,
|
||||
name: 'Umeda Sky Building',
|
||||
});
|
||||
expect(got.email).toBeUndefined();
|
||||
});
|
||||
|
||||
it('COMOV-003: a place without published values keeps its record as it was', () => {
|
||||
expect(withCommunityValues(DETAILS, undefined)).toBe(DETAILS);
|
||||
expect(withCommunityValues(DETAILS, { contact: { website: 'https://x.example' } })).toBe(DETAILS);
|
||||
});
|
||||
|
||||
it('COMOV-004: asks the service once for the element and merges its answer', async () => {
|
||||
ask.mockResolvedValueOnce({
|
||||
'osm:relation/3389505': {
|
||||
contact: { website: 'https://www.skybldg.co.jp/' },
|
||||
community: { version: 1, fields: ['website'] },
|
||||
},
|
||||
});
|
||||
const got = await osmDetailsWithCommunity('relation:3389505', DETAILS);
|
||||
expect(ask).toHaveBeenCalledWith(['osm:relation/3389505']);
|
||||
expect(got?.website).toBe('https://www.skybldg.co.jp/');
|
||||
});
|
||||
|
||||
it('COMOV-005: no details, an id without a key, or an empty answer ask nothing more', async () => {
|
||||
expect(await osmDetailsWithCommunity('relation:3389505', null)).toBeNull();
|
||||
expect(await osmDetailsWithCommunity('gers:abc', DETAILS)).toBe(DETAILS);
|
||||
expect(ask).not.toHaveBeenCalled();
|
||||
ask.mockResolvedValueOnce({});
|
||||
expect(await osmDetailsWithCommunity('node:7', DETAILS)).toEqual(DETAILS);
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
trekPlacesArea,
|
||||
trekPlacesBaseUrl,
|
||||
trekPlacesById,
|
||||
trekCommunityByKeys,
|
||||
trekPlacesHighlights,
|
||||
trekPlacesMiss,
|
||||
trekPlacesNearby,
|
||||
@@ -1207,3 +1208,22 @@ describe('the layers breaker', () => {
|
||||
expect(calls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trekCommunityByKeys', () => {
|
||||
it('asks /v1/community for the keys and answers its places', async () => {
|
||||
const places = { 'osm:relation/3389505': { contact: { website: 'https://www.skybldg.co.jp/' } } };
|
||||
stubFetch({ places });
|
||||
expect(await trekCommunityByKeys(['osm:relation/3389505', 'osm:node/1'])).toEqual(places);
|
||||
const url = new URL(calls[0]?.url ?? 'http://missing');
|
||||
expect(url.pathname).toBe('/v1/community');
|
||||
expect(url.searchParams.get('keys')).toBe('osm:relation/3389505,osm:node/1');
|
||||
});
|
||||
|
||||
it('asks nothing without keys, and a failing service is an empty map', async () => {
|
||||
stubFetch({ places: {} });
|
||||
expect(await trekCommunityByKeys([])).toEqual({});
|
||||
expect(calls).toHaveLength(0);
|
||||
stubFetch({ error: 'down' }, 503);
|
||||
expect(await trekCommunityByKeys(['osm:node/1'])).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user