From 402a3755961b040eab6aa7d472228e9bac0f973d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pavel=20Zbytovsk=C3=BD?= Date: Wed, 16 Oct 2024 14:13:32 +0200 Subject: [PATCH] EditDialog: add PresetSelect (#506) --- .../EditDialog/EditContent/EditContent.tsx | 2 + .../EditContent/EditDialogActions.tsx | 2 +- .../EditContent/PresetSearchBox.tsx | 224 ++++++++++++++++++ .../EditDialog/EditContent/PresetSelect.tsx | 114 +++++++++ src/components/SearchBox/options/preset.tsx | 4 +- src/components/utils/Maki.tsx | 8 +- src/locales/vocabulary.js | 4 + .../tagging/__tests__/idTaggingScheme.test.ts | 9 +- src/services/tagging/data.ts | 21 +- src/services/tagging/fields.ts | 83 ++++--- src/services/tagging/idTaggingScheme.ts | 23 +- src/services/tagging/ourPresets.ts | 3 +- src/services/tagging/presets.ts | 27 ++- src/services/tagging/types/Fields.ts | 12 +- src/services/tagging/types/Presets.ts | 8 +- src/services/tagging/utils.ts | 1 + 16 files changed, 469 insertions(+), 76 deletions(-) create mode 100644 src/components/FeaturePanel/EditDialog/EditContent/PresetSearchBox.tsx create mode 100644 src/components/FeaturePanel/EditDialog/EditContent/PresetSelect.tsx create mode 100644 src/services/tagging/utils.ts diff --git a/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx b/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx index 7ffdb9ca2..489ba8344 100644 --- a/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx +++ b/src/components/FeaturePanel/EditDialog/EditContent/EditContent.tsx @@ -8,12 +8,14 @@ import { CommentField } from './CommentField'; import { OsmUserLogged } from './OsmUserLogged'; import { ContributionInfoBox } from './ContributionInfoBox'; import { OsmUserLoggedOut } from './OsmUserLoggedOut'; +import { PresetSelect } from './PresetSelect'; export const EditContent = () => ( <>
e.preventDefault()}> + diff --git a/src/components/FeaturePanel/EditDialog/EditContent/EditDialogActions.tsx b/src/components/FeaturePanel/EditDialog/EditContent/EditDialogActions.tsx index 68574da54..71931ed6a 100644 --- a/src/components/FeaturePanel/EditDialog/EditContent/EditDialogActions.tsx +++ b/src/components/FeaturePanel/EditDialog/EditContent/EditDialogActions.tsx @@ -7,7 +7,7 @@ import { useOsmAuthContext } from '../../../utils/OsmAuthContext'; import { useGetHandleSave } from '../useGetHandleSave'; const SaveButton = () => { - const { loggedIn, loading } = useOsmAuthContext(); + const { loggedIn } = useOsmAuthContext(); const { tags } = useEditContext(); const handleSave = useGetHandleSave(); diff --git a/src/components/FeaturePanel/EditDialog/EditContent/PresetSearchBox.tsx b/src/components/FeaturePanel/EditDialog/EditContent/PresetSearchBox.tsx new file mode 100644 index 000000000..68941c02e --- /dev/null +++ b/src/components/FeaturePanel/EditDialog/EditContent/PresetSearchBox.tsx @@ -0,0 +1,224 @@ +import React, { useMemo, useState } from 'react'; +import { + Box, + Button, + InputBase, + ListSubheader, + MenuItem, + Select, +} from '@mui/material'; +import SearchIcon from '@mui/icons-material/Search'; +import styled from '@emotion/styled'; +import Maki from '../../../utils/Maki'; +import { TranslatedPreset } from './PresetSelect'; +import { Setter } from '../../../../types'; +import { t } from '../../../../services/intl'; +import { SelectChangeEvent } from '@mui/material/Select/SelectInput'; +import { useEditContext } from '../EditContext'; +import { useBoolState } from '../../../helpers'; +import { useFeatureContext } from '../../../utils/FeatureContext'; +import { PROJECT_ID } from '../../../../services/project'; +import { useOsmAuthContext } from '../../../utils/OsmAuthContext'; +import { OsmType } from '../../../../services/types'; +import { geometryMatchesOsmType } from '../../../../services/tagging/presets'; + +// https://stackoverflow.com/a/70918883/671880 + +const containsText = (text: string, searchText: string) => + text.toLowerCase().indexOf(searchText.toLowerCase()) > -1; + +const StyledListSubheader = styled(ListSubheader)` + display: flex; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + padding: 6px 13px; + & > svg:first-of-type { + margin-right: 8px; + } +`; + +const emptyOptions = [ + 'amenity/cafe', + 'amenity/restaurant', + 'amenity/fast_food', + 'amenity/bar', + 'shop', + 'leisure/park', + 'amenity/place_of_worship', + ...(PROJECT_ID === 'openclimbing' + ? [ + 'climbing/route_bottom', + 'climbing/route', + 'climbing/crag', + // 'climbing/area', + ] + : []), +]; + +const Placeholder = styled.span` + color: ${({ theme }) => theme.palette.text.secondary}; +`; + +const renderOption = (option: TranslatedPreset) => + !option ? ( + {t('editdialog.preset_select.placeholder')} + ) : ( + <> + + + {option.name} + + ); + +const getFilteredOptions = ( + options: TranslatedPreset[], + searchText: string, + osmType: OsmType | undefined, +) => { + const filteredOptions = options + .filter(({ geometry }) => geometryMatchesOsmType(geometry, osmType)) + .filter(({ searchable }) => searchable === undefined || searchable) + .filter((option) => containsText(option.name, searchText)) + .map((option) => option.presetKey); + + if (searchText.length <= 2) { + return filteredOptions.splice(0, 50); // too many rows in select are slow + } + + return filteredOptions; +}; + +const useDisplayedOptions = ( + searchText: string, + options: TranslatedPreset[], +): string[] => { + const { feature } = useFeatureContext(); + return useMemo( + () => + searchText.length + ? getFilteredOptions(options, searchText, feature.osmMeta?.type) + : emptyOptions, + [feature.osmMeta?.type, options, searchText], + ); +}; + +const SearchRow = ({ + onChange, +}: { + onChange: (e: React.ChangeEvent) => void; +}) => ( + + + { + if (e.key !== 'Escape') { + e.stopPropagation(); + } + }} + /> + +); + +const useGetOnChange = ( + options: TranslatedPreset[], + value: string, + setValue: Setter, +) => { + const { setTagsEntries } = useEditContext().tags; + + return (e: SelectChangeEvent) => { + const oldPreset = options.find((o) => o.presetKey === value); + if (oldPreset) { + Object.entries(oldPreset.addTags ?? oldPreset.tags ?? {}).forEach( + (tag) => { + setTagsEntries((state) => + state.filter(([key, value]) => key !== tag[0] && value !== tag[1]), + ); + }, + ); + } + + const newPreset = options.find((o) => o.presetKey === e.target.value); + if (newPreset) { + const newTags = Object.entries(newPreset.addTags ?? newPreset.tags ?? {}); + setTagsEntries((state) => [...newTags, ...state]); + } + setValue(newPreset.presetKey); + }; +}; + +const getPaperMaxHeight = ( + selectRef: React.MutableRefObject, +) => { + if (!selectRef.current) { + return undefined; + } + const BOTTOM_PADDING = 50; + const rect = selectRef.current.getBoundingClientRect(); + const height = window.innerHeight - (rect.top + rect.height) - BOTTOM_PADDING; + return { + style: { + maxHeight: height, + }, + }; +}; + +type Props = { + value: string; + setValue: Setter; + options: TranslatedPreset[]; +}; +export const PresetSearchBox = ({ value, setValue, options }: Props) => { + const selectRef = React.useRef(null); + const { feature } = useFeatureContext(); + const { loggedIn } = useOsmAuthContext(); + const [enabled, enable] = useBoolState(feature.point || !loggedIn); + + const [searchText, setSearchText] = useState(''); + const displayedOptions = useDisplayedOptions(searchText, options); + + const onChange = useGetOnChange(options, value, setValue); + + return ( + <> + + {!enabled && ( + // TODO we may warn users that this is not usual operation, if this becomes an issue + + + + )} + + ); +}; diff --git a/src/components/FeaturePanel/EditDialog/EditContent/PresetSelect.tsx b/src/components/FeaturePanel/EditDialog/EditContent/PresetSelect.tsx new file mode 100644 index 000000000..523f435e5 --- /dev/null +++ b/src/components/FeaturePanel/EditDialog/EditContent/PresetSelect.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useState } from 'react'; +import { Box, Typography } from '@mui/material'; +import styled from '@emotion/styled'; +import { getPoiClass } from '../../../../services/getPoiClass'; +import { allPresets } from '../../../../services/tagging/data'; +import { + fetchSchemaTranslations, + getPresetTermsTranslation, + getPresetTranslation, +} from '../../../../services/tagging/translations'; +import { useFeatureContext } from '../../../utils/FeatureContext'; +import { PresetSearchBox } from './PresetSearchBox'; +import { useEditContext } from '../EditContext'; +import { Preset } from '../../../../services/tagging/types/Presets'; +import { getPresetForFeature } from '../../../../services/tagging/presets'; +import { Feature, FeatureTags } from '../../../../services/types'; +import { t } from '../../../../services/intl'; +import { Setter } from '../../../../types'; + +export type TranslatedPreset = Preset & { + name: string; + icon: string; +}; + +type PresetCacheItem = Preset & { name: string; icon: string; terms: string[] }; +type PresetsCache = PresetCacheItem[]; + +let presetsCache: PresetsCache | null = null; +const getTranslatedPresets = async (): Promise => { + if (presetsCache) { + return presetsCache; + } + + await fetchSchemaTranslations(); + + // resolve symlinks to {landuse...} etc + presetsCache = Object.values(allPresets) + .filter(({ locationSet }) => !locationSet?.include) + .filter(({ tags }) => Object.keys(tags).length > 0) + .map((preset) => { + return { + ...preset, + name: getPresetTranslation(preset.presetKey) ?? preset.presetKey, + icon: getPoiClass(preset.tags).class, + terms: getPresetTermsTranslation(preset.presetKey) ?? preset.terms, + }; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + + return presetsCache; +}; + +const Row = styled(Box)` + display: flex; + align-items: center; +`; + +const LabelWrapper = styled.div` + min-width: 44px; + margin-right: 1em; +`; + +const useMatchTags = ( + feature: Feature, + tags: FeatureTags, + setPreset: Setter, +) => { + useEffect(() => { + (async () => { + const updatedFeature: Feature = { + ...feature, + ...(feature.point ? { osmMeta: { type: 'node', id: -1 } } : {}), + tags, + }; + const foundPreset = getPresetForFeature(updatedFeature); // takes ~ 1 ms + const translatedPreset = (await getTranslatedPresets()).find( + (option) => option.presetKey === foundPreset.presetKey, + ); + setPreset(translatedPreset?.presetKey ?? ''); + })(); + }, [tags, feature, setPreset]); +}; + +const useOptions = () => { + const [options, setOptions] = useState([]); + useEffect(() => { + getTranslatedPresets().then((presets) => setOptions(presets)); + }, []); + return options; +}; + +export const PresetSelect = () => { + const { tags } = useEditContext().tags; + const [preset, setPreset] = useState(''); + const { feature } = useFeatureContext(); + const options = useOptions(); + useMatchTags(feature, tags, setPreset); + + if (options.length === 0) { + return null; + } + + return ( + + + + {t('editdialog.preset_select.label')} + + + + + + ); +}; diff --git a/src/components/SearchBox/options/preset.tsx b/src/components/SearchBox/options/preset.tsx index 2425b6a70..498610d5b 100644 --- a/src/components/SearchBox/options/preset.tsx +++ b/src/components/SearchBox/options/preset.tsx @@ -8,7 +8,7 @@ import { getPresetTermsTranslation, getPresetTranslation, } from '../../../services/tagging/translations'; -import { presets } from '../../../services/tagging/data'; +import { allPresets } from '../../../services/tagging/data'; import { PresetOption } from '../types'; import { t } from '../../../services/intl'; import { highlightText, IconPart } from '../utils'; @@ -29,7 +29,7 @@ const getPresetsForSearch = async () => { await fetchSchemaTranslations(); // resolve symlinks to {landuse...} etc - presetsForSearch = Object.values(presets) + presetsForSearch = Object.values(allPresets) .filter(({ searchable }) => searchable === undefined || searchable) .filter(({ locationSet }) => !locationSet?.include) .filter(({ tags }) => Object.keys(tags).length > 0) diff --git a/src/components/utils/Maki.tsx b/src/components/utils/Maki.tsx index de77cb71b..005eac2bd 100644 --- a/src/components/utils/Maki.tsx +++ b/src/components/utils/Maki.tsx @@ -1,6 +1,7 @@ import React from 'react'; import styled from '@emotion/styled'; import { icons } from '../../assets/icons'; +import { useUserThemeContext } from '../../helpers/theme'; const MakiImg = styled.img<{ $invert: boolean }>` line-height: 14px; @@ -16,6 +17,7 @@ type MakiProps = { style?: React.CSSProperties | undefined; size?: number; middle?: boolean | undefined; + themed?: boolean; }; const Maki = ({ @@ -25,7 +27,11 @@ const Maki = ({ style = undefined, size = 11, middle = undefined, + themed = false, }: MakiProps) => { + const { currentTheme } = useUserThemeContext(); + const invertFinal = themed ? currentTheme === 'dark' : invert; + const icon = icons.includes(ico) ? ico : 'information'; // console.log(icon, ' was: ',ico) return ( @@ -33,7 +39,7 @@ const Maki = ({ src={`/icons/${icon}_11.svg`} alt={ico} title={title ?? ico} - $invert={invert} + $invert={invertFinal} style={{ ...style, verticalAlign: middle ? 'middle' : undefined }} width={size} height={size} diff --git a/src/locales/vocabulary.js b/src/locales/vocabulary.js index ede2c51d8..85c5ec53d 100644 --- a/src/locales/vocabulary.js +++ b/src/locales/vocabulary.js @@ -202,6 +202,10 @@ export default {

If this is a mistake, you can manually revert the values and save it again.`, 'editsuccess.edit.urlLabel': `Your changes:`, 'editsuccess.edit.textLabel': 'Comment', + 'editdialog.preset_select.label': 'Type:', + 'editdialog.preset_select.placeholder': 'Select the type', + 'editdialog.preset_select.search_placeholder': 'Type to search...', + 'editdialog.preset_select.edit_button': 'Edit', 'tags.name': 'Name', 'tags.description': 'Description', diff --git a/src/services/tagging/__tests__/idTaggingScheme.test.ts b/src/services/tagging/__tests__/idTaggingScheme.test.ts index 240683c6f..0fcbd7065 100644 --- a/src/services/tagging/__tests__/idTaggingScheme.test.ts +++ b/src/services/tagging/__tests__/idTaggingScheme.test.ts @@ -3,7 +3,7 @@ import { getSchemaForFeature } from '../idTaggingScheme'; import { Feature } from '../../types'; import { mockSchemaTranslations } from '../translations'; import { intl } from '../../intl'; -import { computeAllFieldKeys } from '../fields'; +import { getFieldKeys } from '../fields'; intl.lang = 'en'; @@ -72,7 +72,7 @@ describe('idTaggingScheme', () => { } as unknown as Feature; const schema = getSchemaForFeature(featureWithTemplate); - const computedAllFieldKeys = computeAllFieldKeys(schema.preset); + const computedAllFieldKeys = getFieldKeys(schema.preset); expect(computedAllFieldKeys).toEqual([ 'amenity', @@ -133,6 +133,7 @@ describe('idTaggingScheme', () => { water: 'fountain', wikidata: 'Q94435643', wikimedia_commons: 'Category:Fountain (metro Malostranská)', + non_existing123: 'xxxx', }, } as unknown as Feature; @@ -144,10 +145,11 @@ describe('idTaggingScheme', () => { 'wikimedia_commons', ]); expect(schema.tagsWithFields.map((x: any) => x.field.fieldKey)).toEqual([ + 'natural', 'source', 'water', ]); - expect(schema.keysTodo).toEqual(['natural']); + expect(schema.keysTodo).toEqual(['non_existing123']); }); it('should remove from keysTodo if address is in restTags', () => { @@ -172,6 +174,7 @@ describe('idTaggingScheme', () => { expect(schema.presetKey).toEqual('historic/city_gate'); expect(schema.tagsWithFields.map((x: any) => x.field.fieldKey)).toEqual([ 'source', + 'tourism', ]); }); }); diff --git a/src/services/tagging/data.ts b/src/services/tagging/data.ts index e223d26e2..228f4f00b 100644 --- a/src/services/tagging/data.ts +++ b/src/services/tagging/data.ts @@ -1,22 +1,25 @@ import fieldsJson from '@openstreetmap/id-tagging-schema/dist/fields.json'; import presetsJson from '@openstreetmap/id-tagging-schema/dist/presets.json'; -import { Fields } from './types/Fields'; +import { Fields, RawFields } from './types/Fields'; import { Presets } from './types/Presets'; import { publishDbgObject } from '../../utils'; import { ourFields, ourPresets } from './ourPresets'; -export const fields = { ...fieldsJson, ...ourFields } as unknown as Fields; -Object.keys(fields).forEach((fieldKey) => { - fields[fieldKey].fieldKey = fieldKey; +export const allFields = { ...fieldsJson, ...ourFields } as unknown as Fields; +Object.keys(allFields).forEach((fieldKey) => { + allFields[fieldKey].fieldKey = fieldKey; }); -export const presets = { ...presetsJson, ...ourPresets } as unknown as Presets; -Object.keys(presets).forEach((presetKey) => { - presets[presetKey].presetKey = presetKey; +export const allPresets = { + ...presetsJson, + ...ourPresets, +} as unknown as Presets; +Object.keys(allPresets).forEach((presetKey) => { + allPresets[presetKey].presetKey = presetKey; }); -publishDbgObject('presets', presets); -publishDbgObject('fields', fields); +publishDbgObject('allPresets', allPresets); +publishDbgObject('allFields', allFields); // TODO build a key lookup table for fields by osm key ? // const fieldsByOsmKey = {}; diff --git a/src/services/tagging/fields.ts b/src/services/tagging/fields.ts index 8571c3f5b..ad09cf9b2 100644 --- a/src/services/tagging/fields.ts +++ b/src/services/tagging/fields.ts @@ -1,55 +1,50 @@ -// links like {shop}, are recursively resolved to their fields import { Preset } from './types/Presets'; -import { fields, presets } from './data'; +import { allFields, allPresets } from './data'; +import { deduplicate } from './utils'; import { Field } from './types/Fields'; +import { getFieldTranslation } from './translations'; -const getResolvedFields = (fieldKeys: string[]): string[] => - fieldKeys.flatMap((key) => { - if (key.match(/^{.*}$/)) { - const presetKey = key.substr(1, key.length - 2); - return getResolvedFields(presets[presetKey].fields); - } - return key; - }); +type FieldType = 'fields' | 'moreFields'; -const getResolvedMoreFields = (fieldKeys: string[]): string[] => +// links like {shop}, are recursively resolved to their fields +const resolveLinks = (fieldKeys: string[], type: FieldType): string[] => fieldKeys.flatMap((key) => { if (key.match(/^{.*}$/)) { const presetKey = key.substr(1, key.length - 2); - return getResolvedMoreFields(presets[presetKey].moreFields); + const linkedFields = allPresets[presetKey][type]; + return resolveLinks(linkedFields, type); } return key; }); -const getResolvedFieldsWithParents = ( - preset: Preset, - fieldType: 'fields' | 'moreFields', -): string[] => { +const resolveParents = (preset: Preset, type: FieldType): string[] => { const parts = preset.presetKey.split('/'); if (parts.length > 1) { const parentKey = parts.slice(0, parts.length - 1).join('/'); - const parentPreset = presets[parentKey]; + const parentPreset = allPresets[parentKey]; if (parentPreset) { - return [ - ...getResolvedFieldsWithParents(parentPreset, fieldType), - ...(preset[fieldType] ?? []), - ]; + return [...resolveParents(parentPreset, type), ...(preset[type] ?? [])]; } } - return preset[fieldType] ?? []; + return preset[type] ?? []; }; -export const computeAllFieldKeys = (preset: Preset) => { +const resolveFieldKeys = (preset: Preset, fieldType: FieldType) => + resolveLinks(resolveParents(preset, fieldType), fieldType); + +const resolveFields = (preset: Preset, fieldType: FieldType): Field[] => + resolveFieldKeys(preset, fieldType).map((key) => allFields[key]); + +const getUniversalFields = (): Field[] => + Object.values(allFields).filter((f) => f.universal); + +export const getFieldKeys = (preset: Preset): string[] => { const allFieldKeys = [ - ...getResolvedFields(getResolvedFieldsWithParents(preset, 'fields')), - ...getResolvedMoreFields( - getResolvedFieldsWithParents(preset, 'moreFields'), - ), - ...Object.values(fields) - .filter((f) => f.universal) - .map((f) => f.fieldKey), + ...resolveFieldKeys(preset, 'fields'), + ...resolveFieldKeys(preset, 'moreFields'), + ...getUniversalFields().map((f) => f.fieldKey), 'operator', 'architect', 'address', @@ -58,8 +53,32 @@ export const computeAllFieldKeys = (preset: Preset) => { .filter((f) => f !== 'image') // already covered in feature image .filter((f) => f !== 'source' && f !== 'check_date'); // lets leave these to tagsWithFields - // @ts-ignore - return [...new Set(allFieldKeys)]; + return deduplicate(allFieldKeys); +}; + +const translateFields = (fields: Field[]): Field[] => + fields.map((field) => { + const fieldTranslation = getFieldTranslation(field); + return { + ...field, + fieldTranslation: { label: `[${field.fieldKey}]`, ...fieldTranslation }, + }; + }); + +const eatPreset = (preset: Preset, fields: Field[]) => { + return fields.filter((field) => !preset.tags[field.key]); +}; + +export const getFields = (preset: Preset) => { + const fields = resolveFields(preset, 'fields'); + const moreFields = resolveFields(preset, 'moreFields'); + const universalFields = getUniversalFields(); + + return { + fields: eatPreset(preset, translateFields(fields)), + moreFields: translateFields(moreFields), + universalFields: translateFields(universalFields), + }; }; // TODO check - 1) field.options 2) strings.options diff --git a/src/services/tagging/idTaggingScheme.ts b/src/services/tagging/idTaggingScheme.ts index 560b3dfc5..4c6b2d828 100644 --- a/src/services/tagging/idTaggingScheme.ts +++ b/src/services/tagging/idTaggingScheme.ts @@ -1,14 +1,15 @@ import { Feature, FeatureTags } from '../types'; import { getFieldTranslation, getPresetTranslation } from './translations'; import { getPresetForFeature } from './presets'; -import { fields } from './data'; -import { computeAllFieldKeys, getValueForField } from './fields'; +import { allFields } from './data'; +import { getFieldKeys, getValueForField } from './fields'; import { Preset, UiField } from './types/Presets'; import { publishDbgObject } from '../../utils'; import { getShortId } from '../helpers'; import { Field } from './types/Fields'; import { DEBUG_ID_SCHEMA } from '../../config.mjs'; import { gradeSystemKeys } from '../../components/FeaturePanel/Climbing/utils/grades/gradeSystem'; +import { deduplicate } from './utils'; const logMoreMatchingFields = (matchingFields: Field[], key: string) => { if (DEBUG_ID_SCHEMA && matchingFields.length > 1) { @@ -21,20 +22,12 @@ const logMoreMatchingFields = (matchingFields: Field[], key: string) => { } }; -const deduplicate = (strings: string[]) => Array.from(new Set(strings)); - const getUiField = ( field: Field, keysTodo: KeysTodo, feature: Feature, key: string, ): UiField => { - // TODO this should be removed now the parsing works ok (+run tests) - if (field.type === 'typeCombo') { - keysTodo.remove(field.key); // ignores eg. railway=tram_stop on public_transport=stop_position - return undefined; - } - const value = feature.tags[key]; const keysInField = deduplicate([ @@ -66,12 +59,12 @@ const matchFieldsFromPreset = ( keysTodo: any, feature: Feature, ): UiField[] => { - const computedAllFieldKeys = computeAllFieldKeys(preset); - publishDbgObject('computedAllFieldKeys', computedAllFieldKeys); + const fieldKeys = getFieldKeys(preset); + publishDbgObject('all fieldKeys', fieldKeys); - return computedAllFieldKeys + return fieldKeys .map((fieldKey: string) => { - const field = fields[fieldKey]; + const field = allFields[fieldKey]; const key = field?.key; const keys = field?.keys; const includeThisField = keysTodo.has(key) || keysTodo.hasAny(keys); @@ -87,7 +80,7 @@ const matchFieldsFromPreset = ( const matchRestToFields = (keysTodo: KeysTodo, feature: Feature): UiField[] => keysTodo.mapOrSkip((key) => { - const matchingFields = Object.values(fields).filter( + const matchingFields = Object.values(allFields).filter( (f) => f.key === key || f.keys?.includes(key), // todo cache this ); logMoreMatchingFields(matchingFields, key); diff --git a/src/services/tagging/ourPresets.ts b/src/services/tagging/ourPresets.ts index fdaaa20bd..7aa577266 100644 --- a/src/services/tagging/ourPresets.ts +++ b/src/services/tagging/ourPresets.ts @@ -1,8 +1,9 @@ import { RawPresets } from './types/Presets'; +import type { RawFields } from './types/Fields'; // until https://github.com/openstreetmap/id-tagging-schema/pull/1113 is merged -export const ourFields = { +export const ourFields: RawFields = { 'climbing/summit_log': { key: 'climbing:summit_log', type: 'check', diff --git a/src/services/tagging/presets.ts b/src/services/tagging/presets.ts index 103268c38..7d396d154 100644 --- a/src/services/tagging/presets.ts +++ b/src/services/tagging/presets.ts @@ -1,5 +1,5 @@ -import { presets } from './data'; -import { Feature } from '../types'; +import { allPresets } from './data'; +import { Feature, OsmType } from '../types'; import { Preset } from './types/Presets'; import { DEBUG_ID_SCHEMA } from '../../config.mjs'; @@ -44,7 +44,6 @@ const matchScore = (_this, entityTags) => { } return score; - /* eslint-enable no-restricted-syntax,guard-for-in */ }; const index = { @@ -53,8 +52,28 @@ const index = { relation: [], }; +const osmTypeToGeometries = (osmType: string): Preset['geometry'] => { + if (osmType === 'node') { + return ['point']; + } else if (osmType === 'way') { + return ['line', 'area']; + } else if (osmType === 'relation') { + return ['relation']; + } + + return ['point']; +}; + +export const geometryMatchesOsmType = ( + presetGeometry: Preset['geometry'], + osmType: OsmType, +) => + osmTypeToGeometries(osmType).some((geometry) => + presetGeometry.includes(geometry), + ); + // build an index by geometry type -Object.values(presets).forEach((preset) => { +Object.values(allPresets).forEach((preset) => { const { geometry } = preset; geometry.forEach((geometryType) => { diff --git a/src/services/tagging/types/Fields.ts b/src/services/tagging/types/Fields.ts index 20bc66267..7cc4adeb0 100644 --- a/src/services/tagging/types/Fields.ts +++ b/src/services/tagging/types/Fields.ts @@ -1,5 +1,7 @@ // https://github.com/ideditor/schema-builder/blob/main/schemas/field.json +import type { FieldTranslation } from './Presets'; + type FieldType = | 'access' | 'address' @@ -38,6 +40,7 @@ type FieldType = export type Field = { // added by osmapp (not in schema) fieldKey: string; + fieldTranslation?: FieldTranslation; /** * Tag key whose value is to be displayed @@ -80,10 +83,7 @@ export type Field = { /** * If specified, only show the field for these kinds of geometry */ - geometry?: [ - 'point' | 'vertex' | 'line' | 'area' | 'relation', // minimal one entry - ...('point' | 'vertex' | 'line' | 'area' | 'relation')[], - ]; + geometry?: ('point' | 'vertex' | 'line' | 'area' | 'relation')[]; // minimal one entry /** * The default value for this field */ @@ -223,3 +223,7 @@ export type Field = { export type Fields = { [fieldKey: string]: Field; }; + +export type RawFields = { + [fieldKey: string]: Omit; +}; diff --git a/src/services/tagging/types/Presets.ts b/src/services/tagging/types/Presets.ts index 4d2b488a4..52992cd68 100644 --- a/src/services/tagging/types/Presets.ts +++ b/src/services/tagging/types/Presets.ts @@ -104,12 +104,12 @@ export type RawPresets = { export type FieldTranslation = { label: string; - placeholder: string; - terms: string; - options: { + placeholder?: string; + terms?: string; + options?: { [key: string]: { title: string; description: string }; }; - types: { + types?: { [key: string]: string; }; }; diff --git a/src/services/tagging/utils.ts b/src/services/tagging/utils.ts new file mode 100644 index 000000000..ca5eaa5bb --- /dev/null +++ b/src/services/tagging/utils.ts @@ -0,0 +1 @@ +export const deduplicate = (strings: string[]) => Array.from(new Set(strings));