Optionally add tag to place photo
Some checks failed
CI / Lint (pull_request) Successful in 52s
CI / Test (pull_request) Failing after 56s

This commit is contained in:
2026-06-05 17:48:55 +04:00
parent 70d2fe1c6c
commit 200100686d
12 changed files with 542 additions and 0 deletions

View File

@@ -7,3 +7,8 @@ export function humanizeOsmTag(text) {
w.replace(/^\w/, (c) => c.toUpperCase())
);
}
export function capitalize(text) {
if (typeof text !== 'string' || !text) return '';
return text.charAt(0).toUpperCase() + text.slice(1);
}

View File

@@ -30,6 +30,11 @@ export function parsePlacePhotos(events) {
const allPhotos = [];
for (const event of sortedEvents) {
const eventTagValues = event.tags
.filter((t) => t[0] === 't')
.map((t) => t[1])
.filter(Boolean);
// Find all imeta tags
const imetas = event.tags.filter((t) => t[0] === 'imeta');
for (const imeta of imetas) {
@@ -70,6 +75,7 @@ export function parsePlacePhotos(events) {
isLandscape,
aspectRatio,
placeIdentifier,
tags: eventTagValues,
});
}
}

View File

@@ -0,0 +1,32 @@
import { POI_CATEGORIES } from './poi-categories';
import { getMatchingPoiCategoryIds } from './poi-category-matcher';
export const CATEGORY_TAGS = {
restaurants: ['food', 'menu', 'vibe', 'front'],
coffee: ['food', 'menu', 'vibe', 'front'],
groceries: ['front', 'food'],
'things-to-do': ['architecture', 'amenities', 'vibe', 'front'],
accommodation: ['rooms', 'amenities', 'food', 'vibe', 'front'],
};
export function getSuggestedPhotoTags(place) {
const osmTags = place?.osmTags || place?.tags || {};
const categoryIds = getMatchingPoiCategoryIds(osmTags, POI_CATEGORIES);
const suggested = [];
for (const categoryId of categoryIds) {
const tags = CATEGORY_TAGS[categoryId];
if (!Array.isArray(tags)) continue;
for (const tag of tags) {
if (!suggested.includes(tag)) {
suggested.push(tag);
}
}
}
if (suggested.length === 0) {
return [];
}
return suggested;
}

View File

@@ -0,0 +1,95 @@
export function getMatchingPoiCategories(osmTags, categories) {
if (!Array.isArray(categories) || !osmTags) return [];
return categories.filter((category) => {
if (!Array.isArray(category.filter)) return false;
return category.filter.some((filterStr) =>
matchesFilter(osmTags, filterStr)
);
});
}
export function getMatchingPoiCategoryIds(osmTags, categories) {
return getMatchingPoiCategories(osmTags, categories).map((c) => c.id);
}
function matchesFilter(osmTags, filterStr) {
const clauses = parseOverpassClauses(filterStr);
if (clauses.length === 0) return false;
return clauses.every((clause) => matchesClause(osmTags, clause));
}
function parseOverpassClauses(filterStr) {
if (!filterStr) return [];
const matches = filterStr.match(/\[[^\]]+\]/g);
if (!matches) return [];
return matches
.map((raw) => parseClause(raw.slice(1, -1).trim()))
.filter(Boolean);
}
function parseClause(content) {
const presenceMatch = content.match(/^"([^"]+)"$/);
if (presenceMatch) {
return { type: 'presence', key: presenceMatch[1] };
}
const equalsMatch = content.match(/^"([^"]+)"\s*=\s*"([^"]*)"$/);
if (equalsMatch) {
return { type: 'equals', key: equalsMatch[1], value: equalsMatch[2] };
}
const regexMatch = content.match(/^"([^"]+)"\s*~\s*"([^"]*)"$/);
if (regexMatch) {
return {
type: 'regex',
key: regexMatch[1],
pattern: regexMatch[2],
regex: new RegExp(regexMatch[2]),
};
}
const notRegexMatch = content.match(/^"([^"]+)"\s*!~\s*"([^"]*)"$/);
if (notRegexMatch) {
return {
type: 'not-regex',
key: notRegexMatch[1],
pattern: notRegexMatch[2],
regex: new RegExp(notRegexMatch[2]),
};
}
return null;
}
function matchesClause(osmTags, clause) {
const tagValues = getTagValues(osmTags, clause.key);
switch (clause.type) {
case 'presence':
return tagValues.length > 0;
case 'equals':
return tagValues.some((value) => value === clause.value);
case 'regex':
return tagValues.some((value) => clause.regex.test(value));
case 'not-regex':
return (
tagValues.length === 0 ||
!tagValues.some((value) => clause.regex.test(value))
);
default:
return false;
}
}
function getTagValues(osmTags, key) {
if (!osmTags || !key) return [];
const rawValue = osmTags[key];
if (rawValue === undefined || rawValue === null) return [];
return String(rawValue)
.split(';')
.map((value) => value.trim())
.filter(Boolean);
}