This commit is contained in:
Andras Schmelczer 2026-05-06 23:13:58 +01:00
parent 94f9c0d594
commit 5c3b87f2d5
69 changed files with 1334 additions and 213 deletions

View file

@ -133,56 +133,59 @@ export const POI_DEFAULT_COLOR: [number, number, number] = [107, 114, 128];
/** POI category → icon/logo URL for branded and transport categories */
export const POI_CATEGORY_LOGOS: Record<string, string> = {
Airport: '/assets/twemoji/2708.png',
Aldi: 'https://geolytix.github.io/MapIcons/brands/aldi_24px.svg',
Amazon: 'https://geolytix.github.io/MapIcons/brands/amazon_fresh_alt_24px.svg',
Asda: 'https://geolytix.github.io/MapIcons/asda/asda_primary.svg',
'Asda Express': 'https://geolytix.github.io/MapIcons/asda/asda_express_24px.svg',
'Asda Living': 'https://geolytix.github.io/MapIcons/asda/asda_living_24px.svg',
'Asda PFS': 'https://geolytix.github.io/MapIcons/asda/asda_pfs_24px.svg',
Aldi: '/assets/poi-icons/logos/aldi.svg',
Amazon: '/assets/poi-icons/brands_2024/amazon_fresh.svg',
Asda: '/assets/poi-icons/logos/asda.svg',
'Asda Express': '/assets/poi-icons/logos/asda.svg',
'Asda Living': '/assets/poi-icons/logos/asda.svg',
'Asda PFS': '/assets/poi-icons/logos/asda.svg',
'Asda Supercentre': '/assets/poi-icons/logos/asda.svg',
'Asda Supermarket': '/assets/poi-icons/logos/asda.svg',
'Asda Superstore': '/assets/poi-icons/logos/asda.svg',
Bakery: '/assets/twemoji/1f950.png',
Booths: 'https://geolytix.github.io/MapIcons/brands/booths_24px.svg',
Budgens: 'https://geolytix.github.io/MapIcons/brands/budgens_24px.svg',
Booths: '/assets/poi-icons/brands_2024/booths.svg',
Budgens: '/assets/poi-icons/brands_2024/budgens.svg',
'Bus station': '/assets/twemoji/1f68c.png',
'Bus stop': '/assets/twemoji/1f68f.png',
'Butcher & Fishmonger': '/assets/twemoji/1f969.png',
Centra: 'https://geolytix.github.io/MapIcons/brands/centra_24px.svg',
'Co-op': 'https://geolytix.github.io/MapIcons/brands/coop_24px.svg',
COOK: 'https://geolytix.github.io/MapIcons/brands/cook.svg',
Centra: '/assets/poi-icons/logos/centra.svg',
'Co-op': '/assets/poi-icons/logos/coop.svg',
COOK: '/assets/poi-icons/brands_2024/cook.svg',
'Convenience Store': '/assets/twemoji/1f3ea.png',
Costco: 'https://geolytix.github.io/MapIcons/brands/costco_24px.svg',
Costco: '/assets/poi-icons/brands/costco.svg',
'Deli & Specialty': '/assets/twemoji/1f9c6.png',
'Dunnes Stores': 'https://geolytix.github.io/MapIcons/brands/dunnes_stores_24px.svg',
Farmfoods: 'https://geolytix.github.io/MapIcons/brands/farmfoods_updated_24px.svg',
'Dunnes Stores': '/assets/poi-icons/brands_2024/dunnes_stores.svg',
Farmfoods: '/assets/poi-icons/brands_2023/supermarkets/farmfoods.svg',
Ferry: '/assets/twemoji/26f4.png',
Greengrocer: '/assets/twemoji/1f96c.png',
'Heron Foods': 'https://geolytix.github.io/MapIcons/brands/heron_24px.svg',
Iceland: 'https://geolytix.github.io/MapIcons/brands/iceland_24px.svg',
Lidl: 'https://geolytix.github.io/MapIcons/brands/lidl_24px.svg',
Makro: 'https://geolytix.github.io/MapIcons/brands/makro_24px.svg',
'M&S': 'https://geolytix.github.io/MapIcons/brands/mns_24px.svg',
'M&S Clothing': 'https://geolytix.github.io/MapIcons/brands/mns_high_street_24px.svg',
'M&S Food': 'https://geolytix.github.io/MapIcons/brands/mns_food_24px.svg',
'M&S Hospital': 'https://geolytix.github.io/MapIcons/brands/mns_hospital_24px.svg',
'M&S MSA': 'https://geolytix.github.io/MapIcons/brands/mns_moto_24px.svg',
'M&S Outlet': 'https://geolytix.github.io/MapIcons/brands/mns_outlet_24px.svg',
Morrisons: 'https://geolytix.github.io/MapIcons/brands/morrisons_24px.svg',
'Morrisons Daily': 'https://geolytix.github.io/MapIcons/brands/morrisons_daily_24px.svg',
'Heron Foods': '/assets/poi-icons/brands_2023/supermarkets/heron_foods.svg',
Iceland: '/assets/poi-icons/logos/iceland.svg',
Lidl: '/assets/poi-icons/logos/lidl.svg',
Makro: '/assets/poi-icons/brands_2024/makro.svg',
'M&S': '/assets/poi-icons/brands/mns.svg',
'M&S Clothing': '/assets/poi-icons/brands/mns_high_street.svg',
'M&S Food': '/assets/poi-icons/brands/mns_food.svg',
'M&S Hospital': '/assets/poi-icons/brands/mns_hospital.svg',
'M&S MSA': '/assets/poi-icons/brands/mns_moto.svg',
'M&S Outlet': '/assets/poi-icons/brands/mns_outlet.svg',
Morrisons: '/assets/poi-icons/logos/morrisons.svg',
'Morrisons Daily': '/assets/poi-icons/brands_2024/morrisons_daily.svg',
'Off-Licence': '/assets/twemoji/1f377.png',
'Planet Organic': 'https://geolytix.github.io/MapIcons/logos/planet_organic_24px.svg',
'Planet Organic': '/assets/poi-icons/logos/planet_organic.svg',
'Rail station': '/assets/twemoji/1f686.png',
"Sainsbury's": 'https://geolytix.github.io/MapIcons/brands/sainsburys_24px.svg',
"Sainsbury's Local": 'https://geolytix.github.io/MapIcons/brands/sainsburys_local_24px.svg',
Spar: 'https://geolytix.github.io/MapIcons/brands/spar_24px.svg',
"Sainsbury's": '/assets/poi-icons/logos/sainsburys.svg',
"Sainsbury's Local": '/assets/poi-icons/brands_2024/sainsburys_local.svg',
Spar: '/assets/poi-icons/logos/spar.svg',
Supermarket: '/assets/twemoji/1f6d2.png',
Tesco: 'https://geolytix.github.io/MapIcons/brands/tesco_24px.svg',
'Tesco Express': 'https://geolytix.github.io/MapIcons/brands/tesco_express_24px.svg',
'Tesco Extra': 'https://geolytix.github.io/MapIcons/brands/tesco_extra_24px.svg',
Tesco: '/assets/poi-icons/logos/tesco.svg',
'Tesco Express': '/assets/poi-icons/logos/tesco_express.svg',
'Tesco Extra': '/assets/poi-icons/logos/tesco_extra.svg',
'Taxi rank': '/assets/twemoji/1f695.png',
'The Food Warehouse': 'https://geolytix.github.io/MapIcons/brands/iceland_food_warehouse_24px.svg',
'Tube station': 'https://geolytix.github.io/MapIcons/public_transport/london_tube.svg',
Waitrose: 'https://geolytix.github.io/MapIcons/brands/waitrose_24px.svg',
'Little Waitrose': 'https://geolytix.github.io/MapIcons/brands/little_waitrose_24px.svg',
'Whole Foods Market': 'https://geolytix.github.io/MapIcons/brands/wholefoods_24px.svg',
'The Food Warehouse': '/assets/poi-icons/logos/iceland.svg',
'Tube station': '/assets/poi-icons/public_transport/london_tube.svg',
Waitrose: '/assets/poi-icons/logos/waitrose.svg',
'Little Waitrose': '/assets/poi-icons/brands/little_waitrose.svg',
'Whole Foods Market': '/assets/poi-icons/brands_2024/wholefoods.svg',
};
/** Categories only shown when zoomed in past MINOR_POI_ZOOM_THRESHOLD */

View file

@ -62,8 +62,7 @@ function pointInPolygon(point: Point, polygon: Point[]): boolean {
if (current[1] > point[1] !== previous[1] > point[1]) {
const x =
((previous[0] - current[0]) * (point[1] - current[1])) /
(previous[1] - current[1]) +
((previous[0] - current[0]) * (point[1] - current[1])) / (previous[1] - current[1]) +
current[0];
if (point[0] < x) inside = !inside;
}
@ -92,9 +91,7 @@ export function hasMatchingHexagonAtResolution(
hexagons: HexagonData[],
resolution: number
): boolean {
return hexagons.some(
(hexagon) => hexagon.count > 0 && getResolution(hexagon.h3) === resolution
);
return hexagons.some((hexagon) => hexagon.count > 0 && getResolution(hexagon.h3) === resolution);
}
export function findOverlappingMatchingHexagon(

View file

@ -1,9 +1,12 @@
import { describe, expect, it } from 'vitest';
import { existsSync } from 'fs';
import { join } from 'path';
import {
DENSITY_GRADIENT,
ENUM_PALETTE,
FEATURE_GRADIENT,
POI_CATEGORY_LOGOS,
SMALLEST_VISIBLE_HEXAGON_RESOLUTION,
} from './consts';
import {
@ -52,12 +55,25 @@ describe('map utilities', () => {
});
it('prefers POI category logos before falling back to emoji icons', () => {
expect(getPoiIconUrl('Waitrose', '🛒')).toBe(
'https://geolytix.github.io/MapIcons/brands/waitrose_24px.svg'
expect(getPoiIconUrl('Waitrose', '🛒')).toBe('/assets/poi-icons/brands/waitrose_24px.svg');
expect(getPoiIconUrl('Iceland', '🛒', 'The Food Warehouse')).toBe(
'/assets/poi-icons/brands/iceland_food_warehouse_24px.svg'
);
expect(getPoiIconUrl("Sainsbury's", '🛒', undefined, 'Sainsburys Earlsfield Local')).toBe(
'/assets/poi-icons/brands/sainsburys_local_24px.svg'
);
expect(getPoiIconUrl('Unknown category', '🛒')).toBe('/assets/twemoji/1f6d2.png');
});
it('keeps POI icon URLs bundled locally', () => {
expect(Object.values(POI_CATEGORY_LOGOS).filter((url) => /^https?:\/\//.test(url))).toEqual([]);
expect(
Object.values(POI_CATEGORY_LOGOS)
.filter((url) => url.startsWith('/assets/poi-icons/'))
.filter((url) => !existsSync(join(process.cwd(), 'public', url.slice(1))))
).toEqual([]);
});
it('returns fallback, filtered, enum, feature, and density colors', () => {
expect(
getFeatureFillColor(

View file

@ -241,7 +241,60 @@ export function emojiToTwemojiUrl(emoji: string): string {
return `${TWEMOJI_BASE}${hex}.png`;
}
export function getPoiIconUrl(category: string, emoji: string): string {
function inferPoiIconCategory(category: string, name?: string): string | undefined {
if (!name) return undefined;
const text = `${category} ${name}`.toLowerCase();
switch (category) {
case 'Asda':
if (text.includes('asda express') || text.includes(' express')) return 'Asda Express';
if (text.includes('asda living')) return 'Asda Living';
if (text.includes('asda pfs') || /\bpfs\b/.test(text)) return 'Asda PFS';
return undefined;
case 'Iceland':
return text.includes('food warehouse') ? 'The Food Warehouse' : undefined;
case 'M&S':
if (text.includes('hospital')) return 'M&S Hospital';
if (text.includes('moto')) return 'M&S MSA';
if (text.includes('outlet')) return 'M&S Outlet';
if (
text.includes('foodhall') ||
text.includes('simply food') ||
text.includes('food to go') ||
text.includes(' bp') ||
/\bsf\b/.test(text)
) {
return 'M&S Food';
}
if (text.includes('clothing')) return 'M&S Clothing';
return undefined;
case 'Morrisons':
return text.includes('morrisons daily') || text.includes('morrisons dailly')
? 'Morrisons Daily'
: undefined;
case "Sainsbury's":
return text.includes('local') ? "Sainsbury's Local" : undefined;
case 'Tesco':
if (text.includes('tesco extra')) return 'Tesco Extra';
if (text.includes('tesco express') || text.includes(' express')) return 'Tesco Express';
return undefined;
case 'Waitrose':
return text.includes('little waitrose') ? 'Little Waitrose' : undefined;
default:
return undefined;
}
}
export function getPoiIconUrl(
category: string,
emoji: string,
iconCategory?: string,
name?: string
): string {
const resolvedIconCategory = iconCategory || inferPoiIconCategory(category, name);
if (resolvedIconCategory && POI_CATEGORY_LOGOS[resolvedIconCategory]) {
return POI_CATEGORY_LOGOS[resolvedIconCategory];
}
return POI_CATEGORY_LOGOS[category] ?? emojiToTwemojiUrl(emoji);
}

View file

@ -8,7 +8,6 @@ import {
import {
SCHOOL_FILTER_NAME,
createSchoolFilterKey,
getSchoolBackendFeatureName,
getSchoolFilterConfig,
isSchoolFilterName,
type SchoolDistance,