├── .nvmrc ├── .husky ├── pre-push ├── post-merge └── post-checkout ├── apps └── traffic-sign-tool │ ├── .prettierignore │ ├── app │ ├── (content) │ │ └── imprint │ │ │ └── .TODO │ ├── _components │ │ ├── utils │ │ │ └── isDev.ts │ │ ├── catalyst │ │ │ ├── .gitignore │ │ │ └── link.tsx │ │ ├── links │ │ │ ├── linkStyles.ts │ │ │ ├── osmtoolsUrl.test.ts │ │ │ ├── osmtoolsUrl.ts │ │ │ ├── ExternalLink.tsx │ │ │ ├── buttonStyles.ts │ │ │ └── CopyButton.tsx │ │ ├── types │ │ │ ├── types.ts │ │ │ └── reset.d.ts │ │ └── layout │ │ │ ├── StateHelper.tsx │ │ │ ├── TailwindResponsiveHelper.tsx │ │ │ ├── Header.tsx │ │ │ └── fonts │ │ │ └── fonts.ts │ ├── (signs) │ │ ├── DE │ │ │ ├── _contryPrefix.const.ts │ │ │ ├── wiki │ │ │ │ └── _components │ │ │ │ │ ├── PackageSvg.tsx │ │ │ │ │ └── Tablelize.tsx │ │ │ ├── page.tsx │ │ │ ├── signs │ │ │ │ └── page.tsx │ │ │ ├── check-sign-combinations │ │ │ │ └── page.tsx │ │ │ └── taginfo │ │ │ │ └── _components │ │ │ │ ├── TagSignImages.tsx │ │ │ │ ├── TagRecommendations.tsx │ │ │ │ └── TagComments.tsx │ │ ├── _components │ │ │ ├── types.ts │ │ │ ├── store │ │ │ │ ├── useParamQ.nuqs.ts │ │ │ │ ├── useParamQCount.zustand.ts │ │ │ │ └── CountryPrefixContext.tsx │ │ │ ├── wiki │ │ │ │ ├── Tag.tsx │ │ │ │ ├── WikiLinkKey.tsx │ │ │ │ ├── WikiLinkListTrafficSignValues.tsx │ │ │ │ ├── WikiLinkify.tsx │ │ │ │ └── WikiLinkValue.tsx │ │ │ ├── PageApp │ │ │ │ ├── results │ │ │ │ │ ├── ResultTagRecommendations │ │ │ │ │ │ └── TagList.tsx │ │ │ │ │ ├── ResultTagRecommendations.tsx │ │ │ │ │ ├── ResultDebug.tsx │ │ │ │ │ ├── ResultComments.tsx │ │ │ │ │ └── ResultTrafficSignTag.tsx │ │ │ │ ├── selectedSigns │ │ │ │ │ ├── utils │ │ │ │ │ │ └── useRaisedShadow.ts │ │ │ │ │ └── SelectedSignImage.tsx │ │ │ │ ├── signGroups │ │ │ │ │ ├── SignGridSearchQuery.tsx │ │ │ │ │ └── SignGrid.tsx │ │ │ │ ├── SelectedSignsColumn.tsx │ │ │ │ └── ResultColumn.tsx │ │ │ ├── ResultColumn.tsx │ │ │ ├── PageApp.tsx │ │ │ ├── PageCheckSignCombinations.tsx │ │ │ ├── PackageSvgTrafficSign.tsx │ │ │ └── PageAllSigns.tsx │ │ └── page.tsx │ ├── not-found.tsx │ ├── icon.svg │ ├── globals.css │ └── layout.tsx │ ├── .env.development │ ├── .env.production │ ├── postcss.config.mjs │ ├── vitest.config.ts │ ├── .gitignore │ ├── CONTRIBUTING.md │ ├── next.config.mjs │ ├── tsconfig.json │ ├── eslint.config.mjs │ └── package.json ├── CNAME ├── packages ├── internal_taginfo │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── fetch.ts │ ├── bun.lockb │ ├── package.json │ └── tsconfig.json ├── internal_svgs │ ├── .gitignore │ ├── src │ │ ├── data-svgs │ │ │ ├── index.ts │ │ │ └── DE │ │ │ │ └── svgs │ │ │ │ ├── DE_224.svg │ │ │ │ ├── DE_295.svg │ │ │ │ ├── DE_340.svg │ │ │ │ ├── DE_201_51.svg │ │ │ │ ├── DE_626_20.svg │ │ │ │ ├── DE_330_1.svg │ │ │ │ ├── DE_357.svg │ │ │ │ ├── DE_1010_53.svg │ │ │ │ ├── DE_330_2.svg │ │ │ │ ├── DE_136_10.svg │ │ │ │ ├── DE_1053_37.svg │ │ │ │ ├── DE_267.svg │ │ │ │ ├── DE_1053_33.svg │ │ │ │ ├── DE_205.svg │ │ │ │ ├── DE_278_110.svg │ │ │ │ ├── DE_331_1.svg │ │ │ │ ├── DE_299.svg │ │ │ │ ├── DE_1010_50.svg │ │ │ │ ├── DE_301.svg │ │ │ │ ├── DE_278_100.svg │ │ │ │ ├── DE_278_120.svg │ │ │ │ ├── DE_357_51.svg │ │ │ │ ├── DE_103_20.svg │ │ │ │ ├── DE_1010_54.svg │ │ │ │ ├── DE_103_10.svg │ │ │ │ ├── DE_278_10.svg │ │ │ │ ├── DE_278__47__.svg │ │ │ │ ├── DE_1004_31__2__.svg │ │ │ │ ├── DE_331_2.svg │ │ │ │ ├── DE_278_40.svg │ │ │ │ ├── DE_278_70.svg │ │ │ │ └── DE_278_60.svg │ │ ├── index.ts │ │ ├── svg.d.ts │ │ └── utils │ │ │ ├── cleanupDirectories.ts │ │ │ ├── prepareDirectoryCountry.ts │ │ │ ├── prepareDirectorySvgs.ts │ │ │ ├── downloadAndOptimizeSvg.ts │ │ │ ├── optimizeSvg.ts │ │ │ └── getFileUrlFromWikiApi.ts │ ├── tsconfig.json │ └── package.json ├── internal_wiki │ ├── .gitignore │ ├── src │ │ ├── index.ts │ │ ├── parseWiki │ │ │ ├── types.ts │ │ │ └── parseWiki.ts │ │ └── downloadWiki │ │ │ └── downloadWiki.ts │ ├── tsconfig.json │ └── package.json └── traffic-sign-converter │ ├── src │ ├── .gitignore │ ├── types │ │ ├── svg.d.ts │ │ ├── types.ts │ │ └── reset.d.ts │ ├── signsToTags │ │ ├── utils │ │ │ ├── uniqueArray.ts │ │ │ ├── collectHighwayValues.test.ts │ │ │ ├── collectHighwayValues.ts │ │ │ ├── splitIntoSignGroups.ts │ │ │ ├── sortTags.test.ts │ │ │ ├── collectUniqueTags.ts │ │ │ ├── collectUniqueTags.test.ts │ │ │ ├── sortTags.ts │ │ │ └── splitIntoSignGroups.test.ts │ │ ├── showSignsToTagsWarning.ts │ │ ├── signsToComments.ts │ │ ├── signsToComments.test.ts │ │ ├── showSignsToTagsWarning.test.ts │ │ └── signsToTags.ts │ ├── utils │ │ ├── toTag.ts │ │ ├── combineSignIdSignValue.ts │ │ ├── createSvgFilename.ts │ │ ├── transformToSignState.ts │ │ ├── getSignsMap.ts │ │ ├── createSvgImportname.ts │ │ ├── combineSignIdSignValue.test.ts │ │ ├── getSignBySignIdAndCheckValue.test.ts │ │ ├── getSignBySignIdAndCheckValue.ts │ │ ├── signsByDescriptiveName.test.ts │ │ └── signsByDescriptiveName.ts │ ├── trafficSignTagToSigns │ │ └── utils │ │ │ ├── splitIntoSignValueParts.ts │ │ │ ├── splitSignIdSignValue.ts │ │ │ ├── removeKeys.ts │ │ │ ├── spliIntoSignValueParts.test.ts │ │ │ ├── removeCountryPrefix.ts │ │ │ ├── splitSignIdSignValue.test.ts │ │ │ └── removeKeys.test.ts │ ├── tagsToSigns │ │ ├── tagsToSigns.test.ts │ │ ├── TODO.md │ │ └── tagsToSigns.ts │ ├── data-definitions │ │ ├── namedTrafficSignValues.ts │ │ ├── countryDefinitions.ts │ │ └── DE │ │ │ └── data │ │ │ └── notice.ts │ └── index.ts │ ├── .gitignore │ ├── CONTRIBUTING.md │ ├── eslint.config.mjs │ ├── tsconfig.json │ ├── README.md │ └── CHANGELOG.md ├── .npmrc ├── vitest.workspace.ts ├── pnpm-workspace.yaml ├── .vscode └── settings.json ├── .gitignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── issue-template.md └── workflows │ └── package-traffic-sign-converter-ci.yml ├── .prettierrc.mjs ├── script-new-data.ts ├── turbo.json ├── script-new-svg.ts ├── NODE_VERSION.md ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.21.1 2 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | pnpm turbo check 2 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/.prettierignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | trafficsigns.osm-verkehrswende.org 2 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(content)/imprint/.TODO: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/internal_taginfo/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/internal_svgs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | -------------------------------------------------------------------------------- /packages/internal_wiki/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | use-node-version=22.21.1 3 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | export default ['apps/*', 'packages/*'] 2 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ENV=development 2 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/.env.production: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ENV=production 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/data-svgs/index.ts: -------------------------------------------------------------------------------- 1 | export * as SvgsDE from './DE/index.js' 2 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/index.ts: -------------------------------------------------------------------------------- 1 | // Utils 2 | export { optimizeSvg } from './utils/optimizeSvg.js' 3 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/.gitignore: -------------------------------------------------------------------------------- 1 | # We keep the data in packages/interna_svgs/* 2 | data-svgs 3 | -------------------------------------------------------------------------------- /packages/internal_wiki/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as trafficSignsWiki } from './data/trafficSignsWiki.json' 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/utils/isDev.ts: -------------------------------------------------------------------------------- 1 | export const isDev = process.env.NEXT_PUBLIC_ENV === 'development' 2 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /packages/internal_taginfo/README.md: -------------------------------------------------------------------------------- 1 | # internal/taginfo 2 | 3 | Some helper scripts to generate data on traffic signs in OSM. 4 | -------------------------------------------------------------------------------- /packages/internal_taginfo/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as taginfoTrafficSignData } from '../data/taginfoTrafficSignData.json' 2 | -------------------------------------------------------------------------------- /packages/internal_taginfo/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osmberlin/osm-traffic-sign-tool/HEAD/packages/internal_taginfo/bun.lockb -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/types/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: string 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/catalyst/.gitignore: -------------------------------------------------------------------------------- 1 | # Only checking in those parts of the Catalyst library that are using 2 | _unused/* 3 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/links/linkStyles.ts: -------------------------------------------------------------------------------- 1 | export const linkStyle = 'underline underline-offset-2 hover:decoration-2 decoration-pink-500' 2 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/.gitignore: -------------------------------------------------------------------------------- 1 | # System 2 | .DS_Store 3 | 4 | # Env 5 | .env 6 | .env*.local 7 | 8 | # Package 9 | /node_modules 10 | /dist 11 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/utils/uniqueArray.ts: -------------------------------------------------------------------------------- 1 | export function uniqueArray(arr: string[]): string[] { 2 | return Array.from(new Set(arr)) 3 | } 4 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/types/types.ts: -------------------------------------------------------------------------------- 1 | // https://twitter.com/mattpocockuk/status/1653403198885904387 2 | export type Prettify = { [K in keyof T]: T[K] } & {} 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .turbo 4 | *.log 5 | .next 6 | dist 7 | dist-ssr 8 | *.local 9 | .env 10 | .cache 11 | server/dist 12 | public/dist 13 | .turbo 14 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/types/types.ts: -------------------------------------------------------------------------------- 1 | // https://twitter.com/mattpocockuk/status/1653403198885904387 2 | export type Prettify = { [K in keyof T]: T[K] } & {} 3 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/types/reset.d.ts: -------------------------------------------------------------------------------- 1 | // https://www.totaltypescript.com/ts-reset fixes .filter(Boolean) and similar cases 2 | import '@total-typescript/ts-reset' 3 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/DE/_contryPrefix.const.ts: -------------------------------------------------------------------------------- 1 | import { CountryPrefixType } from '@osm-traffic-signs/converter' 2 | 3 | export const countryPrefix = 'DE' satisfies CountryPrefixType 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | # Via https://github.com/bakeruk/modern-typescript-monorepo-example/blob/main/.husky/post-merge 2 | # Automate and ensure dependencies are installed/synced with the branch's codebase 3 | pnpm install 4 | -------------------------------------------------------------------------------- /.husky/post-checkout: -------------------------------------------------------------------------------- 1 | # Via https://github.com/bakeruk/modern-typescript-monorepo-example/blob/main/.husky/post-checkout 2 | # Automate and ensure dependencies are installed/synced with the branch's codebase 3 | pnpm install 4 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/types.ts: -------------------------------------------------------------------------------- 1 | import { CountryPrefixType, SignType } from '@osm-traffic-signs/converter' 2 | 3 | export type PageProps = { countryPrefix: CountryPrefixType; trafficSignData: SignType[] } 4 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | include: ['app/**/*.test.ts'], 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/types/reset.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/total-typescript/ts-reset fixes .filter(Boolean) and similar cases 2 | // https://www.totaltypescript.com/ts-reset 3 | import '@total-typescript/ts-reset' 4 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/toTag.ts: -------------------------------------------------------------------------------- 1 | export type TagObjectType = { key: string; value: string } 2 | 3 | export const toTag = ({ key, value }: TagObjectType) => { 4 | return `${key}=${value}` satisfies `${string}=${string}` 5 | } 6 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/trafficSignTagToSigns/utils/splitIntoSignValueParts.ts: -------------------------------------------------------------------------------- 1 | export const splitIntoSignValueParts = (input: string) => { 2 | const split = input.split(/[,;](?![^\[\]]*\])/) 3 | return split.map((s) => s.trim()) 4 | } 5 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/store/useParamQ.nuqs.ts: -------------------------------------------------------------------------------- 1 | import { parseAsString, useQueryState } from 'nuqs' 2 | 3 | export const useParamQ = () => { 4 | const [paramQ, setParamQ] = useQueryState('q', parseAsString) 5 | 6 | return { paramQ, setParamQ } 7 | } 8 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/utils/cleanupDirectories.ts: -------------------------------------------------------------------------------- 1 | import { rimraf } from 'rimraf' 2 | 3 | /** @description Remove all files in this folder structure */ 4 | export const cleanupDirectories = (countryDirectory: string) => { 5 | rimraf.sync(`${countryDirectory}/*`) 6 | } 7 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to @osm-traffic-signs/converter 2 | 3 | ## TODO 4 | 5 | This section is TODO. Please let me know what you where looking for. 6 | 7 | ## Release 8 | 9 | 1. Manually update the changelog 10 | 2. `pnpm run release:patch` 11 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/combineSignIdSignValue.ts: -------------------------------------------------------------------------------- 1 | // Output is "DE:123", "DE:123[4.4]", "123-45" 2 | export const combineSignIdSignValue = (signId: string, signValue: string | number | undefined) => { 3 | if (signValue) { 4 | return `${signId}[${signValue}]` 5 | } 6 | 7 | return signId 8 | } 9 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

Not Found

7 |

Could not find requested resource

8 | Return Home 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/tagsToSigns/tagsToSigns.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { tagsToSigns } from './tagsToSigns.js' 3 | 4 | test('osmToTrafficSign', () => { 5 | const result = tagsToSigns('DE', ['highway=footway', 'foot=designated']) 6 | expect(result[0]?.osmValuePart).toBe('239') 7 | }) 8 | -------------------------------------------------------------------------------- /packages/internal_wiki/src/parseWiki/types.ts: -------------------------------------------------------------------------------- 1 | export type TrafficSignsWiki = { 2 | [key: string]: TrafficSignWiki 3 | } 4 | 5 | export type TrafficSignWikiMap = [string, TrafficSignWiki] 6 | 7 | export type TrafficSignWiki = { 8 | sign: string 9 | imageSvg: string 10 | imageUrl: string 11 | name: string 12 | osmTags: string 13 | comments: string 14 | } 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: For bug reports, comments or requests 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Link 11 | 12 | 13 | 14 | ## Issue 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/DE/wiki/_components/PackageSvg.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic' 2 | 3 | export const PackageSvg = ({ name }: { name: string }) => { 4 | const SvgComponent = dynamic(() => import(`@internal/wiki/${name}`), { ssr: false }) 5 | 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/links/osmtoolsUrl.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { osmtoolsUrl } from './osmtoolsUrl.js' 3 | 4 | describe('osmtoolsUrl()', () => { 5 | test('works', () => { 6 | const result = osmtoolsUrl('DE:237,1022-10,1024-17;250') 7 | expect(result).toBe('http://osmtools.de/traffic_signs/?signs=237,1022-10,1024-17,250') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** @type {import("prettier").Config} */ 3 | const prettierConfig = { 4 | semi: false, 5 | singleQuote: true, 6 | arrowParens: 'always', 7 | printWidth: 100, 8 | plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'], 9 | tailwindFunctions: ['clsx'], 10 | tailwindAttributes: ['className', 'class'], 11 | } 12 | 13 | export default prettierConfig 14 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/links/osmtoolsUrl.ts: -------------------------------------------------------------------------------- 1 | import { splitIntoSignValueParts } from '@osm-traffic-signs/converter' 2 | 3 | export const osmtoolsUrl = (value: string) => { 4 | const splitTrafficSignValues = splitIntoSignValueParts(value) 5 | // Param cannot be excaped or its ignored… 6 | return `http://osmtools.de/traffic_signs/?signs=${splitTrafficSignValues.map((v) => v.replace(`DE:`, '')).join(',')}` 7 | } 8 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/tagsToSigns/TODO.md: -------------------------------------------------------------------------------- 1 | # Input 2 | 3 | ``` 4 | highway=* 5 | bicycle_road=yes 6 | vehicle=destination 7 | ``` 8 | 9 | # Output 10 | 11 | ``` 12 | // TODO some data structure of recommendatoins, which could be mulite set of signs 13 | // Traffic Sign Data Array of Objects 14 | // … which in turn can be parsed to Tags via 15 | ``` 16 | 17 | `TODO`: This method does the wrong thing ATM 18 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/trafficSignTagToSigns/utils/splitSignIdSignValue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} urlKey - Input like "DE:123", "DE:123[4.4]", "123-45" 3 | */ 4 | export const splitSignIdSignValue = (urlKey: string) => { 5 | // TODO: This code is dirty and should be an regex… 6 | return { 7 | signId: urlKey.split('[').at(0)!, 8 | signValue: urlKey.split('[').at(1)?.replace(']', ''), 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/DE/page.tsx: -------------------------------------------------------------------------------- 1 | import { countryDefinitions } from '@osm-traffic-signs/converter' 2 | import { PageApp } from '../_components/PageApp' 3 | import { countryPrefix } from './_contryPrefix.const' 4 | 5 | export default async function AppDe() { 6 | const trafficSignData = countryDefinitions[countryPrefix] 7 | 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/showSignsToTagsWarning.ts: -------------------------------------------------------------------------------- 1 | import type { SignStateType } from '../data-definitions/TrafficSignDataTypes.js' 2 | import { splitIntoSignGroups } from './utils/splitIntoSignGroups.js' 3 | 4 | export const showSignsToTagsWarning = (signs: SignStateType[]) => { 5 | const signGroups = splitIntoSignGroups(signs) 6 | return signGroups.some((group) => group.filter((s) => s.kind !== 'traffic_sign').length > 1) 7 | } 8 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/data-svgs/DE/svgs/DE_224.svg: -------------------------------------------------------------------------------- 1 | 2 | Verkehrszeichen 224 - Haltestelle 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/DE/signs/page.tsx: -------------------------------------------------------------------------------- 1 | import { countryDefinitions } from '@osm-traffic-signs/converter' 2 | import { PageAllApp } from '../../_components/PageAllSigns' 3 | import { countryPrefix } from '../_contryPrefix.const' 4 | 5 | export default async function SignsPage() { 6 | const trafficSignData = countryDefinitions[countryPrefix] 7 | 8 | return 9 | } 10 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from '@eslint/eslintrc' 2 | 3 | const compat = new FlatCompat({ 4 | // import.meta.dirname is available after Node.js v20.11.0 5 | baseDirectory: import.meta.dirname, 6 | }) 7 | 8 | const eslintConfig = [ 9 | ...compat.config({ 10 | extends: ['plugin:prettier/recommended'], 11 | plugins: ['prettier'], 12 | rules: {}, 13 | }), 14 | ] 15 | export default eslintConfig 16 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/data-definitions/namedTrafficSignValues.ts: -------------------------------------------------------------------------------- 1 | // Those are values that do not get prefixed by a country code 2 | // but represent a generalized reference to a corresponding traffic sign. 3 | export const namedTrafficSignValues = [ 4 | 'city_limit', 5 | 'maxspeed', 6 | 'none', 7 | 'destination', 8 | 'yes', 9 | // 'no', 10 | 'variable', 11 | 'hazard', 12 | 'signals', 13 | 'give_way', 14 | 'stop', 15 | 'variable_message', 16 | ] 17 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/links/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import { linkStyle } from './linkStyles' 2 | 3 | type Props = { href?: string; className?: string; blank?: boolean; children: React.ReactNode } 4 | 5 | export const ExternalLink = ({ href, className, blank, children }: Props) => { 6 | const target = blank ? '_blank' : undefined 7 | 8 | return ( 9 | 10 | {children} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/DE/check-sign-combinations/page.tsx: -------------------------------------------------------------------------------- 1 | import { countryDefinitions } from '@osm-traffic-signs/converter' 2 | import { PageCheckSignCombinations } from '../../_components/PageCheckSignCombinations' 3 | import { countryPrefix } from '../_contryPrefix.const' 4 | 5 | export default async function SignsPage() { 6 | const trafficSignData = countryDefinitions[countryPrefix] 7 | 8 | return ( 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/createSvgFilename.ts: -------------------------------------------------------------------------------- 1 | import type { CountryPrefixType } from '../data-definitions/countryDefinitions.js' 2 | import type { SignType } from '../data-definitions/TrafficSignDataTypes.js' 3 | import { createSvgImportname } from './createSvgImportname.js' 4 | 5 | /** @description Optimize name to be used as JS import/export names and filename. */ 6 | export const createSvgFilename = (countryPrefix: CountryPrefixType, input: SignType | string) => { 7 | return `${createSvgImportname(countryPrefix, input)}.svg` 8 | } 9 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/catalyst/link.tsx: -------------------------------------------------------------------------------- 1 | import * as Headless from '@headlessui/react' 2 | import type { Route } from 'next' 3 | import NextLink, { type LinkProps } from 'next/link' 4 | import { forwardRef } from 'react' 5 | 6 | export const Link = forwardRef(function Link( 7 | props: LinkProps & React.ComponentPropsWithoutRef<'a'>, 8 | ref: React.ForwardedRef, 9 | ) { 10 | return ( 11 | 12 | 13 | 14 | ) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/signsToComments.ts: -------------------------------------------------------------------------------- 1 | import type { SignComentType, SignStateType } from '../data-definitions/TrafficSignDataTypes.js' 2 | 3 | export const signsToComments = (signs: SignStateType[]) => { 4 | const signCommentsMap: Map = new Map() 5 | 6 | for (const sign of signs) { 7 | if (sign.recodgnizedSign === false) continue 8 | 9 | if (sign.comments?.length) { 10 | signCommentsMap.set(sign.osmValuePart, sign.comments) 11 | } 12 | } 13 | 14 | return signCommentsMap 15 | } 16 | -------------------------------------------------------------------------------- /script-new-data.ts: -------------------------------------------------------------------------------- 1 | import colors from '@colors/colors' 2 | import { $ } from 'bun' 3 | import path from 'node:path' 4 | 5 | const basePath = path.resolve(import.meta.dir) 6 | let cdPath = basePath 7 | 8 | cdPath = path.join(basePath, 'packages/traffic-sign-converter') 9 | console.log(colors.bgBlue.white('\n\n1. Update the traffic sign data'), cdPath) 10 | await $`cd ${cdPath} && pnpm run build` 11 | 12 | cdPath = path.join(basePath, 'apps/traffic-sign-tool') 13 | console.log(colors.bgBlue.white('\n\n2. Run the app'), cdPath) 14 | await $`cd ${cdPath} && pnpm run dev` 15 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/transformToSignState.ts: -------------------------------------------------------------------------------- 1 | import type { CountryPrefixType } from '../data-definitions/countryDefinitions.js' 2 | import type { SignStateType, SignType } from '../data-definitions/TrafficSignDataTypes.js' 3 | import { createSvgImportname } from './createSvgImportname.js' 4 | 5 | export const transformToSignState = (countryPrefix: CountryPrefixType, sign: SignType) => { 6 | return { 7 | ...sign, 8 | recodgnizedSign: true, 9 | svgName: createSvgImportname(countryPrefix, sign.osmValuePart), 10 | } satisfies SignStateType 11 | } 12 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/utils/prepareDirectoryCountry.ts: -------------------------------------------------------------------------------- 1 | import type { CountryPrefixType } from '@osm-traffic-signs/converter' 2 | import fs from 'node:fs' 3 | import path from 'node:path' 4 | 5 | /** @description Create `data/DE` */ 6 | export const prepareDirectoryCountry = (countryPrefix: CountryPrefixType) => { 7 | const countryDirectory = path.join(__dirname, '../data-svgs', countryPrefix) 8 | 9 | // Create folders 10 | if (!fs.existsSync(countryDirectory)) { 11 | fs.mkdirSync(countryDirectory, { recursive: true }) 12 | } 13 | 14 | return countryDirectory 15 | } 16 | -------------------------------------------------------------------------------- /packages/internal_wiki/src/downloadWiki/downloadWiki.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | const downloadWikiPage = async () => { 4 | const url = 'https://wiki.openstreetmap.org/wiki/DE:Verkehrszeichen_in_Deutschland' 5 | 6 | const response = await fetch(url) 7 | 8 | const html = await response.text() 9 | 10 | const filePath = path.join(__dirname, 'tmp/wikipage.html') 11 | Bun.write(filePath, html) 12 | } 13 | 14 | downloadWikiPage() 15 | .then(() => console.log('Wiki page downloaded successfully')) 16 | .catch((error) => console.error('Error downloading wiki page:', error)) 17 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /.turbo/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribute to trafficsigns.osm-verkehrswende.org 2 | 3 | ## TODO 4 | 5 | This section is TODO. Please let me know what you where looking for. 6 | 7 | ## Release 8 | 9 | 1. Increase the version… 10 | - Update the [CHANGELOG](./CHANGELOG.md) 11 | - Update the [package version](./package.json) 12 | 2. Maybe release [the related package](../../packages/traffic-sign-converter/CONTRIBUTING.md) 13 | 3. Push to `main` 14 | 15 | Github acttions will release a new version of the app at [trafficsigns.osm-verkehrswende.org](https://trafficsigns.osm-verkehrswende.org). 16 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/utils/prepareDirectorySvgs.ts: -------------------------------------------------------------------------------- 1 | import type { CountryPrefixType } from '@osm-traffic-signs/converter' 2 | import fs from 'node:fs' 3 | import path from 'node:path' 4 | import { prepareDirectoryCountry } from './prepareDirectoryCountry.js' 5 | 6 | /** @description Create `data/DE/svgs` */ 7 | export const prepareDirectorySvgs = (countryPrefix: CountryPrefixType) => { 8 | const svgDirectory = path.join(prepareDirectoryCountry(countryPrefix), 'svgs') 9 | 10 | if (!fs.existsSync(svgDirectory)) { 11 | fs.mkdirSync(svgDirectory) 12 | } 13 | 14 | return svgDirectory 15 | } 16 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "inputs": ["$TURBO_DEFAULT", ".env*"], 6 | "outputs": ["dist/**", ".next/**", "!.next/cache/**"], 7 | "dependsOn": ["^build"] 8 | }, 9 | "test": {}, 10 | "test:watch": { 11 | "cache": false, 12 | "persistent": true 13 | }, 14 | "lint": {}, 15 | "dev": { 16 | "cache": false, 17 | "persistent": true 18 | }, 19 | "check": { 20 | "cache": false 21 | }, 22 | "bleach": { 23 | "cache": false 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/utils/collectHighwayValues.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { countryDefinitions } from '../../data-definitions/countryDefinitions.js' 3 | import { signsStateByDescriptiveName } from '../../utils/signsByDescriptiveName.js' 4 | import { collectHighwayValues } from './collectHighwayValues.js' 5 | 6 | test('collectHighwayValues', () => { 7 | const data = countryDefinitions.DE 8 | 9 | const signs = signsStateByDescriptiveName('DE', data, ['Fahrradstraße', 'Anlieger frei']) 10 | expect(collectHighwayValues(signs)).toMatchObject(['cycleway', 'residential', 'service']) 11 | }) 12 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/data-definitions/countryDefinitions.ts: -------------------------------------------------------------------------------- 1 | import { trafficSignDataDE } from './DE/trafficSignDataDE.js' 2 | import type { SignType } from './TrafficSignDataTypes.js' 3 | 4 | // Data Definitions per Country 5 | export const countryDefinitions = { 6 | DE: trafficSignDataDE, 7 | } as const 8 | 9 | export type CountryPrefixType = keyof typeof countryDefinitions 10 | 11 | export const countries = Object.keys(countryDefinitions) as [CountryPrefixType] 12 | 13 | export const countryDefinitionMap = new Map( 14 | Object.entries(countryDefinitions) as [CountryPrefixType, SignType[]][], 15 | ) 16 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/getSignsMap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | countryDefinitions, 3 | type CountryPrefixType, 4 | } from '../data-definitions/countryDefinitions.js' 5 | import type { SignStateType } from '../data-definitions/TrafficSignDataTypes.js' 6 | import { transformToSignState } from './transformToSignState.js' 7 | 8 | export const getSignsMap = (countryPrefix: CountryPrefixType) => { 9 | const signsMap = new Map() 10 | 11 | for (const sign of countryDefinitions[countryPrefix]) { 12 | signsMap.set(sign.osmValuePart, transformToSignState(countryPrefix, sign)) 13 | } 14 | 15 | return signsMap 16 | } 17 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/store/useParamQCount.zustand.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | export type TParamQCount = { 4 | paramQCount: number | undefined 5 | actions: { setParamQCount: (count: number) => void } 6 | } 7 | 8 | const useParamQCount = create()((set) => ({ 9 | paramQCount: undefined, 10 | actions: { 11 | setParamQCount: (count) => { 12 | set({ paramQCount: count }) 13 | }, 14 | }, 15 | })) 16 | 17 | export const useParamQCountNumber = () => useParamQCount((state) => state.paramQCount) 18 | 19 | export const useParamQCountActions = () => useParamQCount((state) => state.actions) 20 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/links/buttonStyles.ts: -------------------------------------------------------------------------------- 1 | export const buttonStyle = 2 | 'inline-flex items-center rounded-full border border-transparent bg-violet-600 px-3 py-1.5 text-xs font-regular text-white shadow-xs enabled:hover:bg-violet-800 focus:outline-hidden focus:ring-2 focus:ring-violet-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:text-gray-900' 3 | 4 | export const buttonStyleSecondary = 5 | 'inline-flex items-center rounded-full border border-violet-50 bg-violet-200 px-3 py-1.5 text-xs font-regular text-violet-700 enabled:hover:bg-violet-300 focus:outline-hidden focus:ring-2 focus:ring-violet-500 focus:ring-offset-2 disabled:bg-gray-300 disabled:text-gray-900' 6 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Route } from 'next' 3 | import { useRouter, useSearchParams } from 'next/navigation' 4 | import { useEffect } from 'react' 5 | import { countryPrefixFallback } from './_components/store/CountryPrefixContext' 6 | 7 | export default function Redirect() { 8 | const router = useRouter() 9 | const params = useSearchParams() 10 | 11 | useEffect(() => { 12 | const url = new URL(`/${countryPrefixFallback}`, window.location.href) 13 | for (const [key, value] of params) { 14 | url.searchParams.append(key, value) 15 | } 16 | router.push(url.toString() as Route) 17 | }, [params, router]) 18 | 19 | return null 20 | } 21 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/utils/collectHighwayValues.ts: -------------------------------------------------------------------------------- 1 | import type { SignStateType } from '../../data-definitions/TrafficSignDataTypes.js' 2 | import { uniqueArray } from './uniqueArray.js' 3 | 4 | export const collectHighwayValues = (signs: SignStateType[] | undefined) => { 5 | if (!Array.isArray(signs)) return [] 6 | 7 | const all = signs 8 | .filter((sign) => sign.recodgnizedSign === true) 9 | .map((sign) => { 10 | return sign.tagRecommendations 11 | }) 12 | .map((tags) => { 13 | return tags.highwayValues 14 | }) 15 | .flat() 16 | .filter(Boolean) 17 | 18 | const deduplicated = uniqueArray(all) 19 | 20 | return deduplicated 21 | } 22 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/createSvgImportname.ts: -------------------------------------------------------------------------------- 1 | import type { CountryPrefixType } from '../data-definitions/countryDefinitions.js' 2 | import type { SignType } from '../data-definitions/TrafficSignDataTypes.js' 3 | 4 | /** @description Optimize name to be used as JS import/export names and filename. */ 5 | export const createSvgImportname = (countryPrefix: CountryPrefixType, input: SignType | string) => { 6 | const string = typeof input === 'string' ? input : input.osmValuePart 7 | 8 | const withDoubleUnderscores = string.replace(/\[/g, '__').replace(/\]/g, '__') 9 | const validIdentifier = withDoubleUnderscores.replace(/[^a-zA-Z0-9_]/g, '_') 10 | 11 | return `${countryPrefix}_${validIdentifier}` 12 | } 13 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/trafficSignTagToSigns/utils/removeKeys.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc Removes all keys that include the term `traffic_sign` but preserves values with equal signs (shout those exist…) 3 | * Examples: `traffic_sign`, `traffic_sign:backward, `bicycle:right:traffic_sign` 4 | */ 5 | export const removeKeys = (input: string) => { 6 | const split = input.split('=') 7 | 8 | const likelyTag = split[0] 9 | 10 | // Check if left of "=" is the tag, return the right part 11 | if (likelyTag?.includes('traffic_sign')) { 12 | // We cannot use `delete` here because this leaves the empty slot which we would join afterwards 13 | split.splice(0, 1) 14 | return split.join('=') 15 | } 16 | 17 | return input 18 | } 19 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/tagsToSigns/tagsToSigns.ts: -------------------------------------------------------------------------------- 1 | import { 2 | countryDefinitions, 3 | type CountryPrefixType, 4 | } from '../data-definitions/countryDefinitions.js' 5 | import type { SignType } from '../data-definitions/TrafficSignDataTypes.js' 6 | 7 | export const tagsToSigns = (countryPrefix: CountryPrefixType, osmTags: string[]) => { 8 | const signCandidates: SignType[] = [] 9 | 10 | for (const sign of countryDefinitions[countryPrefix]) { 11 | const identifyingTags: string[] = 12 | sign.identifyingTags?.map((tag) => `${tag.key}=${tag.value}`) || [] 13 | 14 | if (osmTags.every((tag) => identifyingTags.includes(tag))) { 15 | signCandidates.push(sign) 16 | } 17 | } 18 | 19 | return signCandidates 20 | } 21 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/combineSignIdSignValue.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { combineSignIdSignValue } from './combineSignIdSignValue.js' 3 | 4 | describe('combineSignIdSignValue()', () => { 5 | test('Primary sign "DE:123"', () => { 6 | const result = combineSignIdSignValue('DE:123', undefined) 7 | expect(result).toMatch('123') 8 | }) 9 | 10 | test('Primary sign with value "DE:123[4.4]"', () => { 11 | const result = combineSignIdSignValue('DE:123', '4.4') 12 | expect(result).toMatch('123[4.4]') 13 | }) 14 | 15 | test('Secondary sign "1234-56"', () => { 16 | const result = combineSignIdSignValue('1234-56', undefined) 17 | expect(result).toMatch('1234-56') 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/wiki/Tag.tsx: -------------------------------------------------------------------------------- 1 | import { WikiLinkKey } from './WikiLinkKey' 2 | import { WikiLinkValue } from './WikiLinkValue' 3 | 4 | type Props = { 5 | tagKey: string 6 | tagValue: string | string[] 7 | } 8 | 9 | export const Tag = ({ tagKey, tagValue }: Props) => { 10 | if (tagKey === 'traffic_sign') { 11 | return ( 12 | 13 | 14 | = 15 | {tagValue} 16 | 17 | ) 18 | } 19 | 20 | return ( 21 | 22 | 23 | = 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/utils/splitIntoSignGroups.ts: -------------------------------------------------------------------------------- 1 | import type { SignStateType } from '../../data-definitions/TrafficSignDataTypes.js' 2 | 3 | export const splitIntoSignGroups = (signs: SignStateType[]) => { 4 | const groups: SignStateType[][] = [] 5 | let currentGroup: SignStateType[] = [] 6 | 7 | signs.forEach((sign) => { 8 | if (sign.kind === 'traffic_sign') { 9 | if (currentGroup.length > 0) { 10 | groups.push(currentGroup) 11 | } 12 | currentGroup = [sign] 13 | } else if (['exception_modifier', 'condition_modifier'].includes(sign.kind)) { 14 | currentGroup.push(sign) 15 | } 16 | }) 17 | 18 | if (currentGroup.length > 0) { 19 | groups.push(currentGroup) 20 | } 21 | 22 | return groups 23 | } 24 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/data-definitions/DE/data/notice.ts: -------------------------------------------------------------------------------- 1 | import type { SignType } from '../../TrafficSignDataTypes.js' 2 | 3 | export const _notice: SignType[] = [ 4 | { 5 | osmValuePart: '354', 6 | signId: '354', 7 | name: 'Zeichen 354', 8 | descriptiveName: 'Wasserschutzgebiet', 9 | description: null, 10 | kind: 'traffic_sign', 11 | tagRecommendations: { 12 | highwayValues: [], 13 | uniqueTags: [{ key: 'hazmat:water', value: 'permissive' }], 14 | }, 15 | comments: [], 16 | catalogue: { 17 | signCategory: 'traffic_sign', 18 | }, 19 | image: { 20 | sourceUrl: 21 | 'https://wiki.openstreetmap.org/wiki/File:Zeichen_354_-_Wasserschutzgebiet,_StVO_1988.svg', 22 | licence: 'Public Domain', 23 | }, 24 | }, 25 | ] 26 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** @type {import('next').NextConfig} */ 3 | const nextConfig = { 4 | reactStrictMode: true, 5 | experimental: { 6 | typedRoutes: true, 7 | // https://nextjs.org/docs/app/api-reference/config/next-config-js/reactCompiler 8 | reactCompiler: true, 9 | }, 10 | // NOTE: `redirects` don't work with `output: 'export'` 11 | // Docs: https://nextjs.org/docs/app/building-your-application/routing/redirecting#redirects-in-nextconfigjs 12 | // Docs: https://nextjs.org/docs/app/api-reference/next-config-js/redirects 13 | 14 | // Deploy to Github Pages 15 | // Docs: https://github.com/gregrickaby/nextjs-github-pages?tab=readme-ov-file#nextjs-config 16 | output: 'export', 17 | images: { unoptimized: true }, 18 | } 19 | 20 | export default nextConfig 21 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "target": "ES2022", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "noErrorTruncation": true, // https://stackoverflow.com/a/53131824 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@app/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/utils/sortTags.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { sortTags } from './sortTags.js' 3 | 4 | describe('sortTags()', () => { 5 | test('sort keys', () => { 6 | const signs = new Map([ 7 | ['golf_cart', 'somevalue'], 8 | ['highway', 'somevalue'], 9 | ['traffic_sign', 'somevalue'], 10 | ['hgv', 'somevalue'], 11 | ['hazard', 'somevalue'], 12 | ['something', 'somevalue'], 13 | ['winter_service', 'somevalue'], 14 | ]) 15 | const result = sortTags(signs) 16 | expect(Array.from(result.keys())).toMatchObject([ 17 | 'highway', 18 | 'hgv', 19 | 'golf_cart', 20 | 'hazard', 21 | 'something', 22 | 'winter_service', 23 | 'traffic_sign', 24 | ]) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/PageApp/results/ResultTagRecommendations/TagList.tsx: -------------------------------------------------------------------------------- 1 | import { clsx } from 'clsx' 2 | import { Tag } from '../../../wiki/Tag' 3 | 4 | type Props = { tags: Map; className?: string } 5 | 6 | export const TagList = ({ tags, className }: Props) => { 7 | return ( 8 |
    9 | {Array.from(tags).map(([key, value]) => { 10 | return ( 11 |
  • 18 | 19 |
  • 20 | ) 21 | })} 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import nextVitals from 'eslint-config-next/core-web-vitals' 2 | import nextTs from 'eslint-config-next/typescript' 3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' 4 | import reactCompiler from 'eslint-plugin-react-compiler' 5 | import { defineConfig, globalIgnores } from 'eslint/config' 6 | 7 | const eslintConfig = defineConfig([ 8 | ...nextVitals, 9 | ...nextTs, 10 | eslintPluginPrettierRecommended, 11 | reactCompiler.configs.recommended, 12 | // Override default ignores of eslint-config-next 13 | globalIgnores([ 14 | // Default ignores of eslint-config-next: 15 | '.next/**', 16 | 'out/**', 17 | 'build/**', 18 | 'next-env.d.ts', 19 | // Custom ignores: 20 | './app/_components/catalyst/_unused/*', 21 | ]), 22 | ]) 23 | 24 | export default eslintConfig 25 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/DE/taginfo/_components/TagSignImages.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { SelectedSignImage } from '@app/app/(signs)/_components/PageApp/selectedSigns/SelectedSignImage' 3 | import { useCountryPrefixWithFallback } from '@app/app/(signs)/_components/store/CountryPrefixContext' 4 | import { trafficSignTagToSigns } from '@osm-traffic-signs/converter' 5 | 6 | type Props = { value: string } 7 | 8 | export const TagSignImages = ({ value }: Props) => { 9 | const { countryPrefix } = useCountryPrefixWithFallback() 10 | const signs = trafficSignTagToSigns(value, countryPrefix) 11 | 12 | return ( 13 |
    14 | {signs.map((sign) => { 15 | return ( 16 |
  • 17 | 18 |
  • 19 | ) 20 | })} 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/signsToComments.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { countryDefinitions } from '../data-definitions/countryDefinitions.js' 3 | import { signsStateByDescriptiveName } from '../utils/signsByDescriptiveName.js' 4 | import { signsToComments } from './signsToComments.js' 5 | 6 | describe('signsToComments()', () => { 7 | const data = countryDefinitions.DE 8 | 9 | test('Two groups, Empty is ignored', () => { 10 | const signs = signsStateByDescriptiveName('DE', data, [ 11 | 'Gehweg', 12 | 'Radfahrer frei', 13 | 'Personenkraftwagen frei', 14 | ]) 15 | const result = signsToComments(signs) 16 | expect(result.size).toBe(2) 17 | expect(result.has('239')).toBeTruthy() 18 | expect(result.has('1022-10')).toBeFalsy() 19 | expect(result.has('1024-10')).toBeTruthy() 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /packages/internal_taginfo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/taginfo", 3 | "type": "module", 4 | "private": true, 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "dev": "tsc --watch", 11 | "build": "tsc", 12 | "taginfo": "pnpm run taginfo:fetch", 13 | "taginfo:fetch": "bun run ./src/fetch.ts && prettier ./data --write", 14 | "bleach": "rm -rf .turbo && rm -rf node_modules", 15 | "type-check": "tsc --skipLibCheck --noEmit", 16 | "lint": "tsc --noEmit", 17 | "format": "prettier . --write", 18 | "check": "pnpm run type-check && pnpm run format && pnpm run lint", 19 | "test": "", 20 | "test:watch": "", 21 | "preinstall": "npx only-allow pnpm" 22 | }, 23 | "devDependencies": { 24 | "cheerio": "^1.1.2", 25 | "zod": "^4.2.1" 26 | }, 27 | "peerDependencies": { 28 | "typescript": "^5.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/getSignBySignIdAndCheckValue.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { getSignBySignIdAndCheckValue } from './getSignBySignIdAndCheckValue.js' 3 | import { getSignsMap } from './getSignsMap.js' 4 | 5 | describe('getSignBySignIdAndCheckValue()', () => { 6 | const signsMap = getSignsMap('DE') 7 | 8 | test('Pick the one of one', () => { 9 | const sign = getSignBySignIdAndCheckValue(signsMap, '237', undefined) 10 | expect(sign?.osmValuePart).toMatch('237') 11 | }) 12 | 13 | test('Pick the non value sign of two', () => { 14 | const sign = getSignBySignIdAndCheckValue(signsMap, '274.1', undefined) 15 | expect(sign?.osmValuePart).toMatch('274.1') 16 | }) 17 | 18 | test('Pick the "with value" sign of two', () => { 19 | const sign = getSignBySignIdAndCheckValue(signsMap, '274.1', 44) 20 | expect(sign?.osmValuePart).toMatch('274.1[47]') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/trafficSignTagToSigns/utils/spliIntoSignValueParts.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { splitIntoSignValueParts } from './splitIntoSignValueParts.js' 3 | 4 | describe('splitIntoSignValueParts()', () => { 5 | test(';,', () => { 6 | const result = splitIntoSignValueParts('a;b,c;d') 7 | expect(result).toMatchObject(['a', 'b', 'c', 'd']) 8 | }) 9 | test('[,]', () => { 10 | const result = splitIntoSignValueParts('a[x,y]') 11 | expect(result).toMatchObject(['a[x,y]']) 12 | }) 13 | test('complex', () => { 14 | const result = splitIntoSignValueParts('a;b,c[x,y];d,e,f[y,x]') 15 | expect(result).toMatchObject(['a', 'b', 'c[x,y]', 'd', 'e', 'f[y,x]']) 16 | }) 17 | test('cleanup whitespace', () => { 18 | const result = splitIntoSignValueParts(' a ; b , c[x.y] ; d , e , f[y,x]') 19 | expect(result).toMatchObject(['a', 'b', 'c[x.y]', 'd', 'e', 'f[y,x]']) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /script-new-svg.ts: -------------------------------------------------------------------------------- 1 | import colors from '@colors/colors' 2 | import { $ } from 'bun' 3 | import path from 'node:path' 4 | 5 | const basePath = path.resolve(import.meta.dir) 6 | let cdPath = basePath 7 | 8 | cdPath = path.join(basePath, 'packages/traffic-sign-converter') 9 | console.log(colors.bgBlue.white('\n\n1. Update the traffic sign data'), cdPath) 10 | await $`cd ${cdPath} && pnpm run build` 11 | 12 | cdPath = path.join(basePath, 'packages/internal_svgs') 13 | console.log(colors.bgBlue.white('\n\n2. Download new signs'), cdPath) 14 | await $`cd ${cdPath} && pnpm run updateSvgs` 15 | 16 | cdPath = path.join(basePath, 'packages/traffic-sign-converter') 17 | console.log(colors.bgBlue.white('\n\n3. Build the traffic sign data with the new sign'), cdPath) 18 | await $`cd ${cdPath} && pnpm run build` 19 | 20 | cdPath = path.join(basePath, 'apps/traffic-sign-tool') 21 | console.log(colors.bgBlue.white('\n\n4. Run the app'), cdPath) 22 | await $`cd ${cdPath} && pnpm run dev` 23 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/ResultColumn.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useParamSigns } from '@app/app/(signs)/_components/store/useParamSigns.nuqs' 3 | import { ResultComments } from './PageApp/results/ResultComments' 4 | import { ResultTagRecommendations } from './PageApp/results/ResultTagRecommendations' 5 | import { ResultTrafficSignTag } from './PageApp/results/ResultTrafficSignTag' 6 | 7 | export const ResultColumn = () => { 8 | const { paramSigns } = useParamSigns() 9 | 10 | if (!paramSigns.length) { 11 | return ( 12 | <> 13 |

Recommended Tags

14 |

15 | Select a traffic sign to display recommended tags … 16 |

17 | 18 | ) 19 | } 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/trafficSignTagToSigns/utils/removeCountryPrefix.ts: -------------------------------------------------------------------------------- 1 | import type { CountryPrefixType } from '../../data-definitions/countryDefinitions.js' 2 | 3 | /** 4 | * @param `input` – For example "DE:123", "DE:123[4.4]", "123-45", "DE:123,123-45;DE:567" 5 | * @param `countryPrefix` – For example "DE", see `countryPrefixes` 6 | */ 7 | export const removeCountryPrefix = (input: string, countryPrefix: CountryPrefixType) => { 8 | return ( 9 | input 10 | .replaceAll(`${countryPrefix}:`, '') 11 | // Just in case we also replace the lower case version 12 | .replaceAll(`${countryPrefix.toLocaleLowerCase()}:`, '') 13 | ) 14 | } 15 | 16 | /** 17 | * @param `input` – For example `["DE:111","1212-12","DE:222"]` 18 | * @param `countryPrefix` – For example "DE", see `countryPrefixes` 19 | */ 20 | export const removeCountryPrefixes = (input: string[], countryPrefix: CountryPrefixType) => { 21 | return input.map((i) => removeCountryPrefix(i, countryPrefix)) 22 | } 23 | -------------------------------------------------------------------------------- /packages/internal_taginfo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Source https://www.totaltypescript.com/how-to-create-an-npm-package */ 3 | "compilerOptions": { 4 | /* Base Options: */ 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es2022", 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | "lib": ["es2022"], 14 | 15 | /* Strictness */ 16 | "strict": true, 17 | "noUncheckedIndexedAccess": true, 18 | "noImplicitOverride": true, 19 | 20 | /* If transpiling with TypeScript: */ 21 | "module": "NodeNext", 22 | "outDir": "dist", 23 | "rootDir": "src", 24 | "sourceMap": true, 25 | 26 | /* AND if you're building for a library: */ 27 | "declaration": true, 28 | /* AND if you're building for a library in a monorepo: */ 29 | "declarationMap": true 30 | }, 31 | "include": ["."], 32 | "exclude": ["dist", "node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/layout/StateHelper.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { isDev } from '../utils/isDev' 3 | 4 | type Props = { 5 | state: Record 6 | } 7 | 8 | export const StateHelper = ({ state }: Props) => { 9 | if (!isDev) { 10 | return null 11 | } 12 | 13 | return ( 14 |
15 | {Object.entries(state).map(([nameKey, stateValues]) => { 16 | return ( 17 |
18 | 19 | State {nameKey} 20 | 21 |
22 |               {JSON.stringify(stateValues, undefined, 2)}
23 |             
24 |
25 | ) 26 | })} 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/wiki/WikiLinkKey.tsx: -------------------------------------------------------------------------------- 1 | import { ExternalLink } from '@app/app/_components/links/ExternalLink' 2 | 3 | type Props = { 4 | osmKey: string 5 | lang?: 'DE' | 'US' 6 | } 7 | 8 | export const WikiLinkKey = ({ osmKey, lang = 'DE' }: Props) => { 9 | const wikiLink = 'https://wiki.openstreetmap.org/wiki/' 10 | const splitKeys: string[] = osmKey.split(':') 11 | 12 | const links = splitKeys.map((keyPart) => { 13 | const link = `${wikiLink}${lang}:Key:${keyPart}` 14 | return { 15 | link, 16 | keyPart, 17 | } 18 | }) 19 | 20 | return ( 21 | <> 22 | {links.map(({ link, keyPart }, index) => ( 23 | 29 | {keyPart} 30 | {index < links.length - 1 && ':'} 31 | 32 | ))} 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/store/CountryPrefixContext.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { CountryPrefixType } from '@osm-traffic-signs/converter' 3 | import { createContext, useContext } from 'react' 4 | 5 | const CountryPrefixContext = createContext(undefined) 6 | 7 | export type CountryPrefixProviderProps = { 8 | children: React.ReactElement 9 | countryPrefix: CountryPrefixType 10 | } 11 | export const CountryPrefixProvider = ({ children, countryPrefix }: CountryPrefixProviderProps) => { 12 | return ( 13 | {children} 14 | ) 15 | } 16 | 17 | export const countryPrefixFallback = 'DE' 18 | 19 | export const useCountryPrefixWithFallback = () => { 20 | const countryPrefix = useContext(CountryPrefixContext) 21 | return { countryPrefix: countryPrefix || countryPrefixFallback } 22 | } 23 | 24 | export const useCountryPrefix = () => { 25 | const countryPrefix = useContext(CountryPrefixContext) 26 | return { countryPrefix } 27 | } 28 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/data-svgs/DE/svgs/DE_295.svg: -------------------------------------------------------------------------------- 1 | 2 | Verkehrszeichen 295 - Fahrstreifenbegrenzung und Fahrbahnbegrenzung 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/data-svgs/DE/svgs/DE_340.svg: -------------------------------------------------------------------------------- 1 | 2 | Verkehrszeichen 340 - Leitlinie 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/internal_svgs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Source https://www.totaltypescript.com/how-to-create-an-npm-package */ 3 | "compilerOptions": { 4 | /* Base Options: */ 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es2022", 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | "lib": ["es2022"], 14 | 15 | /* Strictness */ 16 | "strict": true, 17 | "noUncheckedIndexedAccess": true, 18 | "noImplicitOverride": true, 19 | 20 | /* If transpiling with TypeScript: */ 21 | "module": "NodeNext", 22 | "outDir": "dist", 23 | "rootDir": "src", 24 | "sourceMap": true, 25 | 26 | /* AND if you're building for a library: */ 27 | "declaration": true, 28 | /* AND if you're building for a library in a monorepo: */ 29 | "declarationMap": true, 30 | 31 | // For my SVGs 32 | "allowSyntheticDefaultImports": true 33 | }, 34 | "include": [".", "src/svg.d.ts"], 35 | "exclude": ["dist", "node_modules"] 36 | } 37 | -------------------------------------------------------------------------------- /packages/internal_wiki/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Source https://www.totaltypescript.com/how-to-create-an-npm-package */ 3 | "compilerOptions": { 4 | /* Base Options: */ 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es2022", 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | "lib": ["es2022"], 14 | 15 | /* Strictness */ 16 | "strict": true, 17 | "noUncheckedIndexedAccess": true, 18 | "noImplicitOverride": true, 19 | 20 | /* If transpiling with TypeScript: */ 21 | "module": "NodeNext", 22 | "outDir": "dist", 23 | "rootDir": "src", 24 | "sourceMap": true, 25 | 26 | /* AND if you're building for a library: */ 27 | "declaration": true, 28 | /* AND if you're building for a library in a monorepo: */ 29 | "declarationMap": true, 30 | 31 | // For my SVGs 32 | "allowSyntheticDefaultImports": true 33 | }, 34 | "include": [".", "./src/globals.d.ts"], 35 | "exclude": ["dist", "node_modules"] 36 | } 37 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/DE/taginfo/_components/TagRecommendations.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useCountryPrefixWithFallback } from '@app/app/(signs)/_components/store/CountryPrefixContext' 3 | import { Tag } from '@app/app/(signs)/_components/wiki/Tag' 4 | import { signsToTags, trafficSignTagToSigns } from '@osm-traffic-signs/converter' 5 | 6 | type Props = { value: string } 7 | 8 | // Weitgehend ein Duplikat von apps/traffic-sign-tool/app/(app)/[countryPrefix]/_components/results/ResultTagRecommendations.tsx 9 | export const TagRecommendations = ({ value }: Props) => { 10 | const { countryPrefix } = useCountryPrefixWithFallback() 11 | const signs = trafficSignTagToSigns(value, countryPrefix) 12 | const aggregatedTagsMap = signsToTags(signs, countryPrefix) 13 | 14 | return ( 15 |
    16 | {Array.from(aggregatedTagsMap).map(([key, value]) => { 17 | return ( 18 |
  • 19 | 20 |
  • 21 | ) 22 | })} 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /NODE_VERSION.md: -------------------------------------------------------------------------------- 1 | # How the node version is set up 2 | 3 | It looks like there is no standard on how to define the node version on pnpm + github action. 4 | This is the setup we use for now 5 | 6 | **Node version is specified in…** 7 | 8 | - `./package.json` as `engines` for ?? 9 | - `./.npmrc` as `use-node-version` for pnpm 10 | - `./.nvmrc` for github actions 11 | 12 | **Node version is inverred in…** 13 | 14 | - Github actions 15 | 16 | **Which value …** 17 | 18 | It looks like pnpm requires a explict version. 19 | So no way to just say `lts/Jod` ([Node version names](https://nodejs.org/en/about/previous-releases)). 20 | 21 | > The correct syntax for stable release is strictly X.Y.Z or release/X.Y.Z 22 | 23 | The best way to get the version string is to use `node -v` and remove the `v`. 24 | 25 | **A few links…** 26 | 27 | - pnpm does not read `nvmrc` https://github.com/pnpm/pnpm/issues/4471 28 | - docs https://pnpm.io/npmrc#use-node-version 29 | - github actions does not read `pnpmrc` https://github.com/actions/setup-node/issues/1130 30 | - and seems to not cache things https://github.com/pnpm/action-setup/issues/70 31 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/data-svgs/DE/svgs/DE_201_51.svg: -------------------------------------------------------------------------------- 1 | 2 | Verkehrszeichen 201-51 - Andreaskreuz — stehend 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/PageApp/selectedSigns/utils/useRaisedShadow.ts: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { animate, MotionValue, useMotionValue } from 'framer-motion' 3 | import { useEffect } from 'react' 4 | 5 | const inactiveShadow = '0px 0px 0px rgba(0,0,0,0.8)' 6 | 7 | // Thanks to the example code by framer motion 8 | export function useRaisedShadow(value: MotionValue) { 9 | const boxShadow = useMotionValue(inactiveShadow) 10 | 11 | useEffect(() => { 12 | let isActive = false 13 | const onChange = (latest: number) => { 14 | const wasActive = isActive 15 | if (latest === 0) { 16 | isActive = false 17 | if (isActive !== wasActive) { 18 | animate(boxShadow, inactiveShadow) 19 | } 20 | } else { 21 | isActive = true 22 | if (isActive !== wasActive) { 23 | animate(boxShadow, '5px 5px 10px rgba(0,0,0,0.3)') 24 | } 25 | } 26 | } 27 | const unsubscribeOnChange = value.on('change', onChange) 28 | 29 | return () => { 30 | unsubscribeOnChange() 31 | } 32 | }, [value, boxShadow]) 33 | 34 | return boxShadow 35 | } 36 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/trafficSignTagToSigns/utils/splitSignIdSignValue.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { splitSignIdSignValue } from './splitSignIdSignValue.js' 3 | 4 | describe('splitSignIdSignValue()', () => { 5 | test('Primary sign "123"', () => { 6 | const input = '123' 7 | const result = splitSignIdSignValue(input) 8 | expect(result).toMatchObject({ signId: '123', signValue: undefined }) 9 | }) 10 | 11 | test('Primary sign with value "123[4.4]"', () => { 12 | const input = '123[4.4]' 13 | const result = splitSignIdSignValue(input) 14 | expect(result).toMatchObject({ signId: '123', signValue: '4.4' }) 15 | }) 16 | 17 | test('Secondary sign "1234-56"', () => { 18 | const input = '1234-56' 19 | const result = splitSignIdSignValue(input) 20 | expect(result).toMatchObject({ signId: '1234-56', signValue: undefined }) 21 | }) 22 | 23 | test('Secondary sign "1234-56"', () => { 24 | const input = '1234-56' 25 | const result = splitSignIdSignValue(input) 26 | expect(result).toMatchObject({ signId: '1234-56', signValue: undefined }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Source https://www.totaltypescript.com/how-to-create-an-npm-package */ 3 | "compilerOptions": { 4 | /* Base Options: */ 5 | "esModuleInterop": true, 6 | "skipLibCheck": true, 7 | "target": "es2022", 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | "isolatedModules": true, 12 | "verbatimModuleSyntax": true, 13 | "lib": ["es2022"], 14 | 15 | /* Strictness */ 16 | "strict": true, 17 | "noUncheckedIndexedAccess": true, 18 | "noImplicitOverride": true, 19 | 20 | /* If transpiling with TypeScript: */ 21 | "module": "NodeNext", 22 | "outDir": "dist", 23 | "rootDir": "src", 24 | "sourceMap": true, 25 | 26 | /* AND if you're building for a library: */ 27 | "declaration": true, 28 | /* AND if you're building for a library in a monorepo: */ 29 | "declarationMap": true, 30 | 31 | // Usefull 32 | "noErrorTruncation": true // https://stackoverflow.com/a/53131824 33 | }, 34 | "include": ["."], 35 | "exclude": ["dist", "build", "node_modules", "eslint.config.mjs"] 36 | } 37 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/showSignsToTagsWarning.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { countryDefinitions } from '../data-definitions/countryDefinitions.js' 3 | import { signsStateByDescriptiveName } from '../utils/signsByDescriptiveName.js' 4 | import { showSignsToTagsWarning } from './showSignsToTagsWarning.js' 5 | 6 | describe('showSignsToTagsWarning()', () => { 7 | const data = countryDefinitions.DE 8 | 9 | test('All groups have one or less modifier signs => no warning', () => { 10 | const signs = signsStateByDescriptiveName('DE', data, [ 11 | 'Verkehrsberuhigter Bereich', 12 | 'Gehweg', 13 | 'Radfahrer frei', 14 | ]) 15 | const result = showSignsToTagsWarning(signs) 16 | expect(result).toBe(false) 17 | }) 18 | 19 | test('Any group with more than two modifier signs => show warning', () => { 20 | const signs = signsStateByDescriptiveName('DE', data, [ 21 | 'Verkehrsberuhigter Bereich', 22 | 'Gehweg', 23 | 'Radfahrer frei', 24 | 'Personenkraftwagen frei', 25 | ]) 26 | const result = showSignsToTagsWarning(signs) 27 | expect(result).toBe(true) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/getSignBySignIdAndCheckValue.ts: -------------------------------------------------------------------------------- 1 | import type { SignType } from '../data-definitions/TrafficSignDataTypes.js' 2 | import type { getSignsMap } from './getSignsMap.js' 3 | 4 | export const getSignBySignIdAndCheckValue = ( 5 | map: ReturnType, 6 | signId: SignType['signId'], 7 | signValue: SignType['signValue'], 8 | ) => { 9 | const matches = [] 10 | 11 | // Case 1: For `signId` there is only one match in `map` => Return this 12 | for (const [_, value] of map.entries()) { 13 | if (value.signId === signId) { 14 | matches.push(value) 15 | } 16 | } 17 | if (matches.length === 1) { 18 | return matches[0] 19 | } 20 | 21 | // There are multiple results for `signId` 22 | // Case 2: If our input sign has a signValue => Return the match that also has a sign value 23 | // Case 3: If our input sign has doe snot have a signValue => Return the corresponding sign 24 | if (matches.length > 1) { 25 | if (!signValue) { 26 | return matches.find((value) => !value.signValue) || undefined 27 | } else { 28 | return matches.find((value) => value.signValue) || undefined 29 | } 30 | } 31 | 32 | return undefined 33 | } 34 | -------------------------------------------------------------------------------- /packages/internal_wiki/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/wiki", 3 | "type": "module", 4 | "private": true, 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "dev": "tsc --watch", 11 | "build": "pnpm run build:clean && pnpm run build:ts", 12 | "build:clean": "rimraf dist", 13 | "build:ts": "tsc", 14 | "wiki": "pnpm run wiki:downloadWiki && pnpm run wiki:parseWiki", 15 | "wiki:downloadWiki": "bun ./src/downloadWiki/downloadWiki.ts && prettier ./ --write", 16 | "wiki:parseWiki": "bun ./src/parseWiki/parseWiki.ts && prettier ./ --write", 17 | "bleach": "rm -rf .turbo && rm -rf node_modules", 18 | "type-check": "tsc --skipLibCheck --noEmit", 19 | "lint": "tsc --noEmit", 20 | "format": "prettier . --write", 21 | "check": "pnpm run type-check && pnpm run format && pnpm run lint", 22 | "test": "", 23 | "test:watch": "", 24 | "preinstall": "npx only-allow pnpm" 25 | }, 26 | "devDependencies": { 27 | "@internal/svgs": "workspace:*", 28 | "@osm-traffic-signs/converter": "workspace:*", 29 | "cheerio": "^1.1.2", 30 | "rimraf": "^6.1.2", 31 | "typescript": "^5.9.3", 32 | "zod": "^4.2.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/package-traffic-sign-converter-ci.yml: -------------------------------------------------------------------------------- 1 | # See https://www.totaltypescript.com/how-to-create-an-npm-package#8-set-up-our-ci-with-github-actions 2 | name: Package 'Traffic Sign Converter' — CI 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | ci: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Install pnpm 24 | # Will use the version from package.json https://github.com/pnpm/action-setup?tab=readme-ov-file#version 25 | uses: pnpm/action-setup@v4 26 | 27 | - name: Use Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | # See ./NODE_VERSION.md 31 | node-version-file: '.nvmrc' 32 | cache: 'pnpm' 33 | 34 | - name: Install dependencies 35 | run: pnpm install 36 | working-directory: packages/traffic-sign-converter 37 | 38 | - name: Run Check 39 | run: pnpm run check 40 | working-directory: packages/traffic-sign-converter 41 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/PageApp.tsx: -------------------------------------------------------------------------------- 1 | import { ResultColumn } from './PageApp/ResultColumn' 2 | import { SelectedSignsColumn } from './PageApp/SelectedSignsColumn' 3 | import { SignSelectionColumn } from './PageApp/SignSelectionColumn' 4 | import { CountryPrefixProvider } from './store/CountryPrefixContext' 5 | import { PageProps } from './types' 6 | 7 | export const PageApp = ({ countryPrefix, trafficSignData }: PageProps) => { 8 | return ( 9 | 10 |
11 |
12 | 13 |
14 |
15 |

Selected Signs

16 | 17 |
18 |
19 | 20 |
21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/signsToTags/utils/collectUniqueTags.ts: -------------------------------------------------------------------------------- 1 | import type { SignStateType } from '../../data-definitions/TrafficSignDataTypes.js' 2 | 3 | export const collectUniqueTags = (signs: SignStateType[]) => { 4 | const mergedUniqueTags: Map = new Map() 5 | 6 | signs 7 | .filter((sign) => sign.recodgnizedSign === true) 8 | .forEach((sign) => { 9 | const { uniqueTags } = sign.tagRecommendations 10 | 11 | if (uniqueTags) { 12 | for (const uniqueTag of uniqueTags) { 13 | const template = 14 | 'valueTemplate' in uniqueTag && uniqueTag.valueTemplate ? uniqueTag.valueTemplate : '$' 15 | const value = 16 | 'value' in uniqueTag 17 | ? uniqueTag.value 18 | : template.replace( 19 | '$', 20 | String(sign.signValue) || sign.valuePrompt?.defaultValue || '', 21 | ) 22 | 23 | if (!value) { 24 | console.warn('ERROR', Boolean(value), 'should never be empty') 25 | } 26 | 27 | mergedUniqueTags.set(uniqueTag.key, { key: uniqueTag.key, value }) 28 | } 29 | } 30 | }) 31 | 32 | return Array.from(mergedUniqueTags.values()) 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "osm-traffic-sign-tools-monorepo", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo run build", 6 | "dev": "turbo run dev --parallel", 7 | "test": "turbo run test", 8 | "test:watch": "turbo run test:watch", 9 | "lint": "turbo run lint", 10 | "bleach": "turbo run bleach && rm -rf node_modules", 11 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 12 | "preinstall": "npx only-allow pnpm", 13 | "prepare": "husky", 14 | "updatePackages:major": "pnpm dlx taze -r major --includeLocked --write && pnpm install", 15 | "updatePackages:minor": "pnpm dlx taze -r minor --includeLocked --write && pnpm install" 16 | }, 17 | "devDependencies": { 18 | "@colors/colors": "^1.6.0", 19 | "@types/bun": "latest", 20 | "eslint": "^9.39.2", 21 | "husky": "^9.1.7", 22 | "postcss": "^8.5.6", 23 | "prettier": "^3.7.4", 24 | "prettier-plugin-organize-imports": "^4.3.0", 25 | "prettier-plugin-tailwindcss": "^0.7.2", 26 | "svgo": "^4.0.0", 27 | "turbo": "^2.7.1", 28 | "vitest": "^4.0.16" 29 | }, 30 | "packageManager": "pnpm@10.26.2", 31 | "engines": { 32 | "node": ">=22.21.1" 33 | }, 34 | "pnpm": { 35 | "onlyBuiltDependencies": [ 36 | "sharp" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @plugin '@tailwindcss/typography'; 3 | @plugin '@tailwindcss/forms'; 4 | 5 | @theme { 6 | /* 7 | // We only support some weights, see `app/_components/layout/fonts/fonts.ts` 8 | // Docs https://tailwindcss.com/docs/font-weight 9 | // thin: '100', 10 | // extralight: '200', 11 | light: '300', 12 | normal: '400', 13 | // medium: '500', 14 | semibold: '600', 15 | bold: '700', 16 | // extrabold: '800', 17 | // black: '900', 18 | */ 19 | --font-weight-*: initial; 20 | --font-weight-light: 300; 21 | --font-weight-normal: 400; 22 | --font-weight-semibold: 600; 23 | --font-weight-bold: 700; 24 | 25 | --font-mono: 26 | var(--font-fira-code), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 27 | 'Courier New', monospace; 28 | --font-sans: 29 | var(--font-fira-sans), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 30 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 31 | --font-condensed: 32 | var(--font-fira-condensed), ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 33 | 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; 34 | --font-serif: var(--font-noto-serif), ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; 35 | } 36 | 37 | @layer base { 38 | } 39 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/(signs)/_components/wiki/WikiLinkListTrafficSignValues.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { 3 | namedTrafficSignValues, 4 | splitIntoSignValueParts, 5 | splitSignIdSignValue, 6 | } from '@osm-traffic-signs/converter' 7 | import { useCountryPrefixWithFallback } from '../store/CountryPrefixContext' 8 | import { WikiLinkValue } from '../wiki/WikiLinkValue' 9 | 10 | type Props = { value: string; inline?: boolean } 11 | 12 | export const WikiLinkListTrafficSignValues = ({ value, inline }: Props) => { 13 | const { countryPrefix } = useCountryPrefixWithFallback() 14 | const splitTrafficSignValue = splitIntoSignValueParts(value) 15 | const signValues = splitTrafficSignValue.map((part) => splitSignIdSignValue(part).signId) 16 | 17 | if (!countryPrefix) return null 18 | 19 | return ( 20 |
    21 | {signValues.map((part) => { 22 | const prefix = namedTrafficSignValues.includes(part) ? '' : `${countryPrefix}:` 23 | 24 | return ( 25 |
  • 26 | 30 |
  • 31 | ) 32 | })} 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/layout/TailwindResponsiveHelper.tsx: -------------------------------------------------------------------------------- 1 | import { isDev } from '../utils/isDev' 2 | 3 | export const TailwindResponsiveHelper = () => { 4 | if (!isDev) return null 5 | 6 | return ( 7 | <> 8 | 12 | 13 | {' '} 14 | 📱{' '} 15 | 16 | 17 | {' '} 18 | sm{' '} 19 | 20 | 21 | {' '} 22 | md{' '} 23 | 24 | 25 | {' '} 26 | lg{' '} 27 | 28 | 29 | {' '} 30 | xl{' '} 31 | 32 | 33 | {' '} 34 | 2xl{' '} 35 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/signsByDescriptiveName.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest' 2 | import { countryDefinitions } from '../data-definitions/countryDefinitions.js' 3 | import { signsByDescriptiveName } from './signsByDescriptiveName.js' 4 | 5 | describe('signsByDescriptiveName()', () => { 6 | const data = countryDefinitions.DE 7 | 8 | test('Check one exact match', () => { 9 | const signs = signsByDescriptiveName(data, ['Fahrradstraße']) 10 | expect(signs.length).toBe(1) 11 | }) 12 | 13 | test('Check two exact matches', () => { 14 | const signs = signsByDescriptiveName(data, ['Fahrradstraße', 'Anlieger frei']) 15 | expect(signs.length).toBe(2) 16 | }) 17 | 18 | test('Check one "startsWith" match', () => { 19 | const signs = signsByDescriptiveName(data, ['Fahrradstr']) 20 | expect(signs.length).toBe(1) 21 | }) 22 | 23 | test('Check one "startsWith" match', () => { 24 | const signs = signsByDescriptiveName(data, ['Fahrradstr', 'Anlieger fr']) 25 | expect(signs.length).toBe(2) 26 | }) 27 | 28 | test('Real case', () => { 29 | const signs = signsByDescriptiveName(data, [ 30 | 'Verbot für Krafträder, auch mit Beiwagen, Kleinkrafträder und Mofas', 31 | 'Krafträder auch mit Beiwagen, Krafträder und Mofas frei', 32 | ]) 33 | expect(signs.length).toBe(2) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/traffic-sign-converter/src/utils/signsByDescriptiveName.ts: -------------------------------------------------------------------------------- 1 | import type { CountryPrefixType } from '../data-definitions/countryDefinitions.js' 2 | import type { SignType } from '../data-definitions/TrafficSignDataTypes.js' 3 | import { transformToSignState } from './transformToSignState.js' 4 | 5 | /** @description Looks at the start of the `descriptiveName` property */ 6 | export const signsByDescriptiveName = (signDefinition: SignType[], names: string[]) => { 7 | const signs = names 8 | .map((name) => { 9 | const exactMatch = signDefinition.find((s) => s.descriptiveName == name) 10 | const startsWithMatch = signDefinition.find((s) => s.descriptiveName.startsWith(name)) 11 | return exactMatch || startsWithMatch 12 | }) 13 | .filter(Boolean) 14 | 15 | if (signs.length !== names.length) { 16 | console.info('ERROR', { 17 | nameCount: names.length, 18 | signCount: signs.length, 19 | missing: names.filter((n) => !signs.some((s) => s.descriptiveName === n)), 20 | }) 21 | } 22 | 23 | return signs 24 | } 25 | 26 | export const signsStateByDescriptiveName = ( 27 | countryPrefix: CountryPrefixType, 28 | signDefinition: SignType[], 29 | names: string[], 30 | ) => { 31 | const signs = signsByDescriptiveName(signDefinition, names) 32 | return signs.map((sign) => transformToSignState(countryPrefix, sign)) 33 | } 34 | -------------------------------------------------------------------------------- /packages/internal_svgs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@internal/svgs", 3 | "type": "module", 4 | "private": true, 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "dev": "tsc --watch", 11 | "build": "pnpm run build:clean && pnpm run build:ts && pnpm run build:copy-svgs", 12 | "build:clean": "rimraf dist", 13 | "build:ts": "tsc", 14 | "build:copy-svgs": "copyfiles --flat './src/data/svgs/*' './dist/data/svgs'", 15 | "updateSvgs": "npm run updateSvgs:clean && npm run updateSvgs:download && npm run updateSvgs:format", 16 | "updateSvgs:clean": "rimraf ./src/data-svgs", 17 | "updateSvgs:download": "bun ./src/downloadAndOptimizeSvgs.ts", 18 | "updateSvgs:format": "prettier ./src/data-svgs --write", 19 | "bleach": "rm -rf .turbo && rm -rf node_modules", 20 | "type-check": "tsc --skipLibCheck --noEmit", 21 | "lint": "tsc --noEmit", 22 | "format": "prettier . --write", 23 | "check": "pnpm run type-check && pnpm run format && pnpm run lint", 24 | "test": "", 25 | "test:watch": "", 26 | "preinstall": "npx only-allow pnpm" 27 | }, 28 | "devDependencies": { 29 | "@osm-traffic-signs/converter": "workspace:*", 30 | "cheerio": "^1.1.2", 31 | "copyfiles": "^2.4.1", 32 | "rimraf": "^6.1.2", 33 | "svgo": "^4.0.0", 34 | "typescript": "^5.9.3", 35 | "zod": "^4.2.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/_components/links/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { useState } from 'react' 3 | import { buttonStyle, buttonStyleSecondary } from './buttonStyles' 4 | 5 | // Inspired by https://usehooks-ts.com/react-hook/use-copy-to-clipboard 6 | 7 | type CopyButtonProps = { 8 | text: string 9 | secondary?: boolean 10 | children: React.ReactNode 11 | } 12 | 13 | export const CopyButton = ({ text, secondary = false, children }: CopyButtonProps) => { 14 | const [copiedText, setCopiedText] = useState(null) 15 | 16 | const copy = async (text: string): Promise => { 17 | if (!navigator?.clipboard) { 18 | console.warn('Clipboard not supported') 19 | return false 20 | } 21 | 22 | // Try to save to clipboard then save it in the state if worked 23 | try { 24 | await navigator.clipboard.writeText(text) 25 | setCopiedText(text) 26 | return true 27 | } catch (error) { 28 | console.warn('Copy failed', error) 29 | setCopiedText(null) 30 | return false 31 | } 32 | } 33 | 34 | return ( 35 |
36 | 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /packages/internal_svgs/src/data-svgs/DE/svgs/DE_626_20.svg: -------------------------------------------------------------------------------- 1 | 2 | Verkehrszeichen 626-20 - Leitplatte, Aufstellung links 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /apps/traffic-sign-tool/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { clsx } from 'clsx' 2 | import { Metadata } from 'next' 3 | import { NuqsAdapter } from 'nuqs/adapters/next/app' 4 | import { Suspense } from 'react' 5 | import { fontClasses } from './_components/layout/fonts/fonts' 6 | import { Footer } from './_components/layout/Footer' 7 | import { Header } from './_components/layout/Header' 8 | import { TailwindResponsiveHelper } from './_components/layout/TailwindResponsiveHelper' 9 | import './globals.css' 10 | 11 | export const metadata: Metadata = { 12 | title: 'OSM Traffic Sign Tool 2', 13 | description: 'Generate OpenStreetMap tagging recommendations based from traffic signs.', 14 | } 15 | 16 | type Props = Readonly<{ children: React.ReactNode }> 17 | 18 | export default function RootLayout({ children }: Props) { 19 | return ( 20 | 21 | 22 | 23 |
24 | {/* Suspense: See https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout */} 25 | 26 |
{children}
27 |
28 |