Author SHA1 Message Date
nq 529ab7cd5f v.093 2026-10-16 17:59:16 +00:00
nq 857f64ce81 v09 2026-10-16 17:38:51 +03:00
21 changed files with 2975 additions and 39 deletions
+1
View File
@@ -0,0 +1 @@
See README_V093.md for current diagnosis and changes. Historical v0.92 notes were superseded.
+130
View File
@@ -0,0 +1,130 @@
# BGMountains Garmin IMG → native OSM → OsmAnd OBF — v0.92
**v0.92** fixes the v0.8–v0.9 synthetic marker/marina fallback approach. It aims to create a **native OsmAnd map** without Garmin TYP icon PNGs or a custom `.render.xml` on the phone.
The **only two user-editable translation files** are:
1. `config/poi-mapping.csv` (Garmin point type → native OSM tag(s), Bulgarian naming prefix, legend, priority, historic icon pointer, example coordinates)
2. `config/line-mapping.csv` (Garmin extended line type → OSM way tags, color-marked hiking relation policy and `osmc:symbol`)
Everything under the generated build/output directory is an **intermediate or report**, not a third configuration. You don't have to supply `poi_types.xml`, `rendering_types.xml`, an icons CSV or a custom rendering style.
## Requirements
Ubuntu/Debian: `sudo apt install build-essential cmake libpng-dev libsqlite3-dev python3 openjdk-21-jre-headless unzip`
To **compile an OBF**, provide an official [OsmAndMapCreator](https://osmand.net/docs/technical/map-creation/create-offline-maps-yourself/) installation containing `utilities.sh`, or use the automatic download option (downloads from OsmAnd). **Python, CMake and `make` by themselves do NOT build OBF**.
## Full map: one command
```sh
python3 scripts/build_bgmountains.py \
--img '/path/to/gmapsupp.img' \
--out './output' \
--creator '/path/to/OsmAndMapCreator'
```
If you don't have MapCreator:
```sh
python3 scripts/build_bgmountains.py \
--img '/path/to/gmapsupp.img' --out './output' --download-creator
```
It compiles C++, extracts the Garmin IMG to OSM, uses the two mapping files, assigns stable Bulgarian hashes to unnamed POIs, produces route relations, conservatively attaches matched overlays to actual `highway=*` ways, performs a semantic QA audit, and runs OsmAndMapCreator. The **only file to copy onto your phone is the resulting `.obf`**. Enable hiking routes and relevant POI filters in OsmAnd as necessary. No third-party icons or renderer file is included or required.
For a fast test that stops before the Java OBF stage:
```sh
python3 scripts/build_bgmountains.py \
--img '/path/to/gmapsupp.img' \
--out './vihren-test' --bbox '23.36,41.70,23.42,41.76' --prepare-only
```
Parallelism remains **disabled by default** (`--jobs 1`). For opt-in concurrent IMG extraction append `--jobs 4`. The Python mapping, relation, QA and Java steps are not parallelized by this switch.
Use `--no-route-attachment` to keep source overlay geometries as hiking-relation members instead of attaching matched real path/road ways. `--keep-raw` retains the intermediate IMG-native XML for debugging.
## Editing the 2 CSV files
### `config/poi-mapping.csv`
| Column | What it means |
|---|---|
| `code` | Garmin POI type, in hex; never changed in provenance |
| `source_count` | Count from source inventory; informational only |
| `legend_bg`, `legend_en` | Original BGMountains legend meaning (where verified) |
| `meaning_confidence` | `legend` / `source_inferred`; review unknowns |
| `osm_tags` | `k=v;k=v` for **truthful standard** OSM semantics; no fake category for icons |
| `label_bg` | Prefix for otherwise nameless objects, with deterministic alphanumeric suffix |
| `priority` | `critical`, `normal` or `terrain`; currently emits `bgm:priority` metadata |
| `zoom_hint` | Desirable zoom target for future style/QA work, **not an OsmAnd override** |
| `action` | `replace` removes earlier semantics, `append` keeps them, `raw` keeps provenance only, `drop` discards the POI |
| `legacy_icon` | `TYP:<hex>` reference for older Garmin icon, for auditing only |
| `example_lat`, `example_lon`, `example_name` | Sample place from original source inventory to inspect |
If a source code is known, edit `osm_tags` and set `action=replace`. Example: `0x6414` → `amenity=drinking_water`. If it isn't known, leave it as `raw` until you inspect the old legend and sample coordinates. Nothing will invent a marina, information board or drinking tap for an unknown symbol. You may append multiple compatible tags separated by semicolons.
**Water/shelter priority rules already populated**: `0x6414` drinking tap, `0x6511` spring, `0x6500` water intake, `0x6417` well, `0x6609` hut, `0x660a` basic refuge, `0x661e` shelter and `0x6402` building. For requested naming, unnamed `0x6402` uses the prefix **„Заслон - Сграда“**; its factual OSM classification stays `building=yes` (a generic building must not falsely be declared an actual lifesaving shelter). Verify shelter suitability on the ground before relying on it.
`0x6600` numeric elevation labels preserve `ele=*` instead of being misclassified as peaks, buildings or missing POI categories. `0x650b` winter waymarkers and `0x660b` trail markers remain Garmin-sourced nodes rather than incorrect water/marina/guidepost POIs.
### `config/line-mapping.csv`
| Column | Meaning |
|---|---|
| `code`, `source_count` | Garmin extended polyline code/count |
| `legend_bg`, `legacy_color` | Known description and source TYP base color |
| `osm_way_tags` | Additional truthful `key=value;key=value` tags on source ways |
| `route_mode` | `relation` builds OSM hiking routes; `preserve` keeps raw way without assuming it is a trail |
| `osmc_symbol` | Standard OSMC syntax, e.g. `red:white:red_bar` |
| `network` | `lwn` (local walking), change if there is evidence for a larger network |
| `route_confidence` | `color_only` means inferred from color but pattern not independently established |
| `priority` | Descriptive category only, not stock OsmAnd zoom control |
Colored candidate hiking overlays become `type=route` + `route=hiking` + `osmc:symbol=*` relations. All retain the original `garmin:type` and source line metadata. Matching route overlay lines to existing physical `highway=*` geometry avoids generating fake navigable duplicate paths. A physical road segment is used only if at least 90% of an overlay's segments match and the physical way extends at most 10% beyond it; unmatched overlays remain relation members, flagged in `route-attachment.json`.
**A source color is not conclusive proof of a painted bar pattern**. All automatic bar-style assignments from color-only evidence explicitly say `route_confidence=color_only`; check the original map and choose the right `osmc_symbol` for each code before treating that detail as verified. See [OSM trail markings](https://wiki.openstreetmap.org/wiki/Trail_markings) and [OSMC symbol syntax](https://wiki.openstreetmap.org/wiki/Key:osmc:symbol).
## New IMG / future map: generate blank templates
```sh
python3 scripts/build_bgmountains.py \
--img '/path/to/another-garmin.img' \
--out './new-map-template' --template
```
The result contains `poi-mapping.TEMPLATE.csv` with **all discovered types, counts and sample POI coordinates**, and `line-mapping.TEMPLATE.csv` with all discovered extended line types/counts. Unknowns start as `action=raw`, `route_mode=preserve` and **without semantic tags**. You must fill them based on that map's own legend and examples; BGMountains mappings must not be assumed to apply to another Garmin map. To build it, point `--poi-config` / `--line-config` to the new two reviewed files.
## Outputs / validation
| Path | Purpose |
|---|---|
| `BGMountains-native.osm` | Final translated, standard-tagged OSM intermediate |
| `source-pois.csv` | Per-point source type, mapset, coordinates and name |
| `discovered-pois.csv` | Observed type inventory with up to 5 sample coordinates |
| `route-report.json` | Coloured overlays/route relation counts |
| `route-attachment.json` | Reused physical path vs unmatched overlay counts |
| `semantic-audit.json` | Detailed native category statistics, unknown types and example positions |
| `extract.log`, `translate.log`, `audit.log` | Pipeline logs |
| `mapcreator.log` | Java OBF build log when compilation is attempted |
| `obf-inspection.txt` | BinaryInspector POI summary (if inspector is installed) |
| `*.obf` | Final file for OsmAnd (only if MapCreator succeeds) |
Tests:
```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 2
ctest --test-dir build --output-on-failure
```
## What native stock OsmAnd cannot guarantee
- OsmAnd's **own rendering style controls icon selection, label collision and minimum zoom**; `zoom_hint=12` does not change it. To force all water/hut icons to zoom 11 or 12, a custom OsmAnd style/app configuration is unavoidable. v0.92 deliberately does **not** install one.
- Not every valid OSM tag has a stock icon, especially water intakes, bridge points, elevation points and trail blazes. We prefer correct/metadata-only tags over false marinas/boards.
- A valid hiking relation is not a guarantee that stock OsmAnd renders every segment. Inspect `route-attachment.json` and verify on-device.
- The OSM file parser and XML/route structural tests do **not** establish that the POIs survived final OBF indexing. Use `mapcreator.log`, `obf-inspection.txt` and actual OsmAnd app checks. We cannot claim a complete nationwide OBF was compiled/tested in this environment unless MapCreator is available.
- Map-derived sample coordinates and legend crosswalk are subject to BGMountains data licensing; do not relicense or import them to public OSM automatically.
**v0.92 does not use the Garmin icon PNGs or `BGMountains-outdoor.render.xml`**. You may remove older BGMountains OBFS from OsmAnd to avoid duplicates before installing the new one.
+76
View File
@@ -0,0 +1,76 @@
# BGMountains v0.93 — bounded-memory translation and private-map POI fallback
This is a **source patch release**, building on v0.92. All user translation choices live in exactly two files:
1. `config/poi-mapping.csv`: Garmin hex → OSM semantic tags; Bulgarian legend name and old TYP code; `action`, priority, source count and examples.
2. `config/line-mapping.csv`: Garmin line hex → OSM way tags, `osmc:symbol`, route relation choice, marking color and confidence.
`cpp-poi-override.csv` and `cpp-name-labels.csv` are generated **internal intermediates**, not control files; do not edit them.
## Direct diagnosis of the supplied nohup
The same appended logfile shows **different historical attempts**: an early `java.lang.OutOfMemoryError: Java heap space` in OsmAndMapCreator, followed in the last attempt by `java.io.IOException: No space left on device` while `IndexCreator.extractOsmToNodesDB` built the Java/SQLite intermediate database. Earlier `poi-coverage.log` saying `PASS` refers only to OSM semantic checks. It is not a successful OBF creation signal.
## Changes
- Streaming OSM parser clears the XML **root**, preventing an accumulating empty Element for every object.
- Expensive optional `attach_native_routes.py` step is opt-in (`--attach-routes`); default keeps existing routable highway network unchanged, and generates hiking-route relations from Garmin colored overlays. Without attachment, those relations describe overlay geometry; they are not proof of route-based navigation along the existing path ways.
- Unclassified POIs now receive `place=locality`, `bgm:synthetic_fallback=place_locality`, `bgm:classification=unverified`, and a generated Bulgarian name `Неуточнено - <stable hash>` **when the source name was empty or previously synthetic**. Genuine source names remain untouched. Raw `garmin:type` and mapset information are preserved.
- **Exception:** `0x6600` numeric spot heights and other elevation annotations (`ele=*`) are not fabricated as localities. They remain elevations rather than false place icons. Known native tags are never overwritten by the fallback.
- Native categories for water, drinking water, shelters/huts, bridges and more remain sourced from existing two mapping files. No extracted Garmin artwork or phone-side custom stylesheet is required.
- Semantic audit differentiates actual unclassified records, synthetic locality fallbacks and non-POI elevation annotations, and includes example coordinates. `PASS` still does not prove an Android icon renders.
- Conservative free-disk preflight before invoking MapCreator, based on `max(2 GiB, 5*OSM size + 1 GiB)` free in the output filesystem. The estimate is based on the reported >25 GiB scratch need for ~6 GiB OSM; it is *not a guarantee*.
- JVM heap via `JAVA_TOOL_OPTIONS` as well as `JAVA_OPTS`, for MapCreator builds that ignore the latter; use e.g. `--java-opts '-Xms512M -Xmx6G'` (subject to host RAM). **A large heap cannot solve out-of-disk.**
- OBF success only if a newly generated nonempty `.obf` exists, not a stale file from an earlier run.
## Quick regional test — no Java, no large output
```sh
python3 scripts/build_bgmountains.py \
--img '/path/to/gmapsupp.img' \
--bbox '23.30,41.67,23.50,41.84' \
--out './regional-test' \
--prepare-only
cat regional-test/semantic-audit.json
cat regional-test/route-report.json
```
`--jobs 1` is the default. `--jobs 4` is optional; on the source IMG, serial and parallel runs had equal POI counts and candidate colored overlay counts but a slight discrepancy in connected route groups (78 vs 82). **Do not assume identical routing topology with parallel extraction yet.**
To test a truly unfamiliar map and generate configuration templates:
```sh
python3 scripts/build_bgmountains.py --img another.img --bbox 'W,S,E,N' \
--out ./new-map --template
# Review: new-map/poi-mapping.TEMPLATE.csv and line-mapping.TEMPLATE.csv
```
The templates are intentionally blank for unknown source meanings; when used by the translation pipeline, unknown POIs will receive the private `place=locality` fallback. A new map should not blindly reuse BGMountains meanings.
## Full OBF build — only on a machine with enough free scratch/RAM
```sh
python3 scripts/build_bgmountains.py \
--img '/path/to/gmapsupp.img' \
--out '/large-volume/bgmountains-v093' \
--creator '/path/to/OsmAndMapCreator' \
--java-opts '-Xms512M -Xmx8G'
```
Or first use `--prepare-only` and later `--osm '/path/to/IMG-raw.osm'` to skip the C++ extraction **if** that raw original file was saved (`--keep-raw`). `--osm` expects the *raw* IMG-derived OSM, not the already translated native OSM.
The preflight checks output filesystem free space; for shared temp partitions, also check `df -h /tmp "$PWD"` and `df -i`. An OBF may still fail if actual scratch is elsewhere. Only override the preflight (`--ignore-disk-check`) when you have independently confirmed scratch capacity.
## Limitations / honesty
- `place=locality` is a **synthetic label hack chosen for this PRIVATE map**, not a correct definition of all unknown source objects and not suitable for direct uploading into OSM.
- Stock OsmAnd decides icon set, zoom threshold, collision suppression and Nearby POI search category. A native OSM tag increases compatibility but cannot guarantee every icon is shown or listed in Nearby without a mobile test.
- Route colors represented through native OSM hiking relations using `osmc:symbol` for identified Garmin colored overlay types. Color/pattern interpretations with limited source evidence remain explicitly provisional.
- No full-country OBF creation has been performed as part of this patch validation, per the user's storage constraints. The final national MapCreator step remains to be verified on the user's workstation.
## Regression verification
- Unit tests: `python3 -m unittest discover -s tests -p 'test_v093.py' -v`
- Previous v0.92 tests: `python3 -m unittest discover -s tests -p 'test_v092.py' -v`
- Regional real IMG: 1,915 POIs, 296 elevation annotations, 150 synthetic localities, zero unclassified after fallback, 273 route-overlay candidates. Single-worker result has 78 connected route groups. The v0.93 code has not been validated end-to-end against stock OsmAnd on Android.
+51
View File
@@ -0,0 +1,51 @@
code,source_count,legend_bg,legacy_color,osm_way_tags,route_mode,osmc_symbol,network,route_confidence,priority,note
0x10e1f,237835,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f12,217991,Неустановена линейна декорация,#e6e6e6,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f16,36027,Неустановена линейна декорация,#e6e6e6,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e0b,15148,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f17,10965,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#ff3041,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e13,10607,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e12,8662,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f14,8475,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f11,6436,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f0a,6231,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#ff0000,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e0a,4347,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#0030ff,,relation,blue:white:blue_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f00,4197,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f0f,4098,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#399500,,relation,green:white:green_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e11,3926,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e05,3011,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#ffff00,,relation,yellow:white:yellow_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e0c,2375,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e1c,2045,Неустановена линейна декорация,#ffffff,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f13,1575,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e19,1469,Неустановена линейна декорация,#525252,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e09,1074,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#0030ff,,relation,blue:white:blue_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e04,1048,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#ffff00,,relation,yellow:white:yellow_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e16,636,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10504,591,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#bd3041,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f0e,488,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#399500,,relation,green:white:green_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10401,394,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#bd3041,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e1b,368,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#ff0000,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10602,354,Неустановена линейна декорация,#777777,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e1d,327,Неустановена линейна декорация,#ffffff,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e18,292,Неустановена линейна декорация,#414141,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e0d,267,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10108,232,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#bd3041,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e1a,205,Неустановена линейна декорация,#ffffff,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e03,199,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#ffff00,,relation,yellow:white:yellow_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e15,184,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f05,162,Неустановена линейна декорация,#7b3000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e1e,162,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e08,119,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#0030ff,,relation,blue:white:blue_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e17,110,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#ff0000,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e14,109,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f15,98,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f04,93,Неустановена линейна декорация,#7b3000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10606,78,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#bd3041,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e02,60,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#ffff00,,relation,yellow:white:yellow_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e0e,56,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10402,47,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#bd3041,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f03,9,Неустановена линейна декорация,#7b3000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10f01,4,Неустановена линейна декорация,#7b3000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10e0f,4,Неустановена линейна декорация,#000000,,preserve,,lwn,unknown,terrain,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10503,3,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#bd3041,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
0x10403,3,"Туристическа линейна маркировка (цветът е потвърден, шарката не)",#bd3041,,relation,red:white:red_bar,lwn,color_only,critical,Provisional symbol until verified against legend; overlay is NOT a routable path
1 code source_count legend_bg legacy_color osm_way_tags route_mode osmc_symbol network route_confidence priority note
2 0x10e1f 237835 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
3 0x10f12 217991 Неустановена линейна декорация #e6e6e6 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
4 0x10f16 36027 Неустановена линейна декорация #e6e6e6 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
5 0x10e0b 15148 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
6 0x10f17 10965 Туристическа линейна маркировка (цветът е потвърден, шарката не) #ff3041 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
7 0x10e13 10607 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
8 0x10e12 8662 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
9 0x10f14 8475 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
10 0x10f11 6436 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
11 0x10f0a 6231 Туристическа линейна маркировка (цветът е потвърден, шарката не) #ff0000 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
12 0x10e0a 4347 Туристическа линейна маркировка (цветът е потвърден, шарката не) #0030ff relation blue:white:blue_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
13 0x10f00 4197 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
14 0x10f0f 4098 Туристическа линейна маркировка (цветът е потвърден, шарката не) #399500 relation green:white:green_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
15 0x10e11 3926 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
16 0x10e05 3011 Туристическа линейна маркировка (цветът е потвърден, шарката не) #ffff00 relation yellow:white:yellow_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
17 0x10e0c 2375 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
18 0x10e1c 2045 Неустановена линейна декорация #ffffff preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
19 0x10f13 1575 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
20 0x10e19 1469 Неустановена линейна декорация #525252 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
21 0x10e09 1074 Туристическа линейна маркировка (цветът е потвърден, шарката не) #0030ff relation blue:white:blue_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
22 0x10e04 1048 Туристическа линейна маркировка (цветът е потвърден, шарката не) #ffff00 relation yellow:white:yellow_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
23 0x10e16 636 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
24 0x10504 591 Туристическа линейна маркировка (цветът е потвърден, шарката не) #bd3041 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
25 0x10f0e 488 Туристическа линейна маркировка (цветът е потвърден, шарката не) #399500 relation green:white:green_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
26 0x10401 394 Туристическа линейна маркировка (цветът е потвърден, шарката не) #bd3041 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
27 0x10e1b 368 Туристическа линейна маркировка (цветът е потвърден, шарката не) #ff0000 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
28 0x10602 354 Неустановена линейна декорация #777777 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
29 0x10e1d 327 Неустановена линейна декорация #ffffff preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
30 0x10e18 292 Неустановена линейна декорация #414141 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
31 0x10e0d 267 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
32 0x10108 232 Туристическа линейна маркировка (цветът е потвърден, шарката не) #bd3041 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
33 0x10e1a 205 Неустановена линейна декорация #ffffff preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
34 0x10e03 199 Туристическа линейна маркировка (цветът е потвърден, шарката не) #ffff00 relation yellow:white:yellow_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
35 0x10e15 184 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
36 0x10f05 162 Неустановена линейна декорация #7b3000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
37 0x10e1e 162 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
38 0x10e08 119 Туристическа линейна маркировка (цветът е потвърден, шарката не) #0030ff relation blue:white:blue_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
39 0x10e17 110 Туристическа линейна маркировка (цветът е потвърден, шарката не) #ff0000 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
40 0x10e14 109 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
41 0x10f15 98 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
42 0x10f04 93 Неустановена линейна декорация #7b3000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
43 0x10606 78 Туристическа линейна маркировка (цветът е потвърден, шарката не) #bd3041 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
44 0x10e02 60 Туристическа линейна маркировка (цветът е потвърден, шарката не) #ffff00 relation yellow:white:yellow_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
45 0x10e0e 56 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
46 0x10402 47 Туристическа линейна маркировка (цветът е потвърден, шарката не) #bd3041 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
47 0x10f03 9 Неустановена линейна декорация #7b3000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
48 0x10f01 4 Неустановена линейна декорация #7b3000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
49 0x10e0f 4 Неустановена линейна декорация #000000 preserve lwn unknown terrain Provisional symbol until verified against legend; overlay is NOT a routable path
50 0x10503 3 Туристическа линейна маркировка (цветът е потвърден, шарката не) #bd3041 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
51 0x10403 3 Туристическа линейна маркировка (цветът е потвърден, шарката не) #bd3041 relation red:white:red_bar lwn color_only critical Provisional symbol until verified against legend; overlay is NOT a routable path
File diff suppressed because it is too large Load Diff
+75
View File
@@ -0,0 +1,75 @@
# Original legend crosswalk, all 67 historic icon matches
Reference table generated from the earlier reviewed Garmin TYP/legend matching data. **Documentation only**; the two CSV files under `config/` are the only user-editable translation controls. The original icon PNGs are not needed, used, or packaged. Where no native semantic tag is defensible it is shown as “unclassified”.
| Source Garmin hex | Legend BG | Legend EN | Old TYP icon | v0.92 OSM tags | Priority | Sample lat, lon |
|---|---|---|---|---|---|---|
| 0x200 | Столица | Capital City | TYP:0x200 | place=city;capital=yes | normal | 41.0612941, 28.8116670 |
| 0x800 | Град >75 000 | Large City | TYP:0x800 | place=city | normal | 43.9025259, 22.2788572 |
| 0x900 | Град <75 000 | Town | TYP:0x900 | place=town | normal | 42.0570374, 24.5459247 |
| 0x2c05 | Училище | School | TYP:0x2c05 | amenity=school | normal | 41.7214394, 24.6845198 |
| 0x2c0b | Църква | Church | TYP:0x2c0b | amenity=place_of_worship | normal | 43.0217957, 24.8099613 |
| 0x2d04 | Казино | Casino | TYP:0x2d04 | unclassified | normal | — |
| 0x2d08 | Пързалка | Ice Rink | TYP:0x2d08 | unclassified | normal | — |
| 0x2e05 | Аптека | Pharmacy | TYP:0x2e05 | amenity=pharmacy | normal | 42.7442837, 25.3431416 |
| 0x2e07 | Магазин за дрехи | Clothes shop | TYP:0x2e07 | shop=clothes | normal | 43.0698609, 25.6080151 |
| 0x2f0a | Синагога | Synagogue | TYP:0x2f0a | amenity=place_of_worship;religion=jewish | normal | 41.5711713, 23.7286234 |
| 0x2f0c | Джамия | Mosque | TYP:0x2f0c | amenity=place_of_worship;religion=muslim | normal | 44.0549612, 26.7991304 |
| 0x2f0e | Автомивка | Car Wash | TYP:0x2f0e | amenity=car_wash | normal | 42.2939301, 22.7035904 |
| 0x3004 | Съд | Courthouse | TYP:0x3004 | unclassified | normal | — |
| 0x3008 | Пожарна | Fire Station | TYP:0x3008 | amenity=fire_station | normal | 42.5666571, 27.6193285 |
| 0x4700 | Пристанище | Port | TYP:0x4700 | unclassified | normal | 41.6431189, 25.2845192 |
| 0x6400 | Кариера | Quarry | TYP:0x6400 | landuse=quarry | normal | 40.8776808, 23.9436078 |
| 0x6401 | Мост | Bridge | TYP:0x6401 | man_made=bridge | critical | 42.6645684, 22.9691291 |
| 0x6402 | Сграда | Building | TYP:0x6402 | building=yes | critical | 42.5507569, 23.2217932 |
| 0x6403 | Гробище, оброк | Graveyard/Tombstone | TYP:0x6403 | landuse=cemetery | normal | 43.0418801, 23.5914874 |
| 0x6404 | Църква | Church | TYP:0x6404 | amenity=place_of_worship | normal | 42.1896243, 27.0861053 |
| 0x6405 | Ловна хижа | Hunting Lodge | TYP:0x6405 | building=hut | normal | 42.7514720, 27.2207522 |
| 0x6406 | Бариера | Barrier/Gate | TYP:0x6406 | barrier=gate | normal | 41.5502286, 24.7455668 |
| 0x6407 | Пикник | Picnic Site | TYP:0x6407 | tourism=picnic_site | normal | 41.9156528, 25.3233576 |
| 0x6408 | ПСС | Mountain Rescue | TYP:0x6408 | emergency=mountain_rescue | critical | 42.6026416, 23.2414269 |
| 0x6409 | Паметник | Monument | TYP:0x6409 | historic=monument | normal | 43.1282043, 23.6027098 |
| 0x640a | Опасност | Danger | TYP:0x640a | hazard=yes | normal | 43.1507993, 25.3284001 |
| 0x640b | Параклис | Chapel | TYP:0x640b | amenity=place_of_worship | normal | 41.9598985, 24.3688774 |
| 0x640c | Мина | Mine Shaft | TYP:0x640c | man_made=mine_shaft | normal | 43.0497336, 22.9328442 |
| 0x640d | Лифт | Lift | TYP:0x640d | aerialway=station | normal | 42.6056242, 23.2995987 |
| 0x640e | Спирка | Bus Stop/Station | TYP:0x640e | highway=bus_stop | normal | 43.3513427, 25.1430917 |
| 0x640f | Гробище, оброк | Graveyard/Tombstone | TYP:0x640f | landuse=cemetery | normal | 42.0345068, 22.4919534 |
| 0x6411 | Кула | Tower | TYP:0x6411 | man_made=tower | normal | 41.7411375, 24.8182654 |
| 0x6412 | Начало пътека | Trailhead | TYP:0x6412 | unclassified | normal | — |
| 0x6413 | Тунел | Tunnel | TYP:0x6413 | unclassified | normal | 42.6872063, 23.3418703 |
| 0x6414 | Чешма | Drinking Water | TYP:0x6414 | amenity=drinking_water | critical | 43.1117034, 23.7205982 |
| 0x6415 | Място за бивакуване | Camp Site | TYP:0x6415 | tourism=camp_site | critical | 41.7506647, 25.1023221 |
| 0x6416 | Руини | Ruins | TYP:0x6416 | historic=ruins | normal | 41.5940452, 24.3094182 |
| 0x6417 | Кладенец | Well | TYP:0x6417 | man_made=water_well | critical | 42.2628808, 24.2223430 |
| 0x641a | Църква | Church | TYP:0x641a | amenity=place_of_worship | normal | 42.9599547, 23.0566549 |
| 0x641b | Църква | Church | TYP:0x641b | unclassified | normal | — |
| 0x641c | Спирка | Bus Stop/Station | TYP:0x641c | highway=bus_stop | normal | 42.8542113, 25.3382707 |
| 0x6500 | Каптаж | Reservoir | TYP:0x6500 | man_made=water_intake | critical | 42.0310307, 24.7559094 |
| 0x6501 | Ресторант | Restaurant | TYP:0x6501 | amenity=restaurant | normal | 43.3456135, 28.0715060 |
| 0x6502 | Брод | River Passage | TYP:0x6502 | ford=yes | normal | 42.7929711, 25.2992177 |
| 0x6503 | Кръстопът | Crossroad | TYP:0x6503 | unclassified | normal | 43.4149003, 28.4942007 |
| 0x6505 | Магазин | Shop | TYP:0x6505 | unclassified | normal | — |
| 0x6506 | Музей | Museum | TYP:0x6506 | tourism=museum | normal | 42.1463013, 24.7487855 |
| 0x650b | Зимна маркировка | Winter Marking | TYP:0x650b | unclassified | normal | 42.0000887, 23.3820820 |
| 0x650c | Задънена улица | Dead End | TYP:0x650c | unclassified | normal | — |
| 0x6511 | Извор | Spring | TYP:0x6511 | natural=spring | critical | 42.8732872, 23.3371925 |
| 0x6600 | Кота | Triangulation | TYP:0x6600 | unclassified | terrain | 41.2312603, 22.6785922 |
| 0x6601 | Природен феномен | Natural Phenomenon | TYP:0x6601 | unclassified | terrain | 42.9589677, 25.2119064 |
| 0x6602 | Гледка | Viewpoint | TYP:0x6602 | tourism=viewpoint | normal | 41.7438412, 23.4641790 |
| 0x6603 | Кота | Triangulation | TYP:0x6603 | man_made=survey_point | normal | 42.5520873, 23.8540435 |
| 0x6608 | Хотел | Hotel | TYP:0x6608 | tourism=hotel | critical | 42.8794885, 27.8957462 |
| 0x6609 | Хижа | Hut | TYP:0x6609 | tourism=alpine_hut | critical | 43.2779145, 27.1159101 |
| 0x660a | Заслон | Refuge | TYP:0x660a | amenity=shelter;shelter_type=basic_hut | critical | 42.8157377, 23.8160634 |
| 0x660b | Маркировка | Mark | TYP:0x660b | unclassified | normal | 43.9378452, 22.4072385 |
| 0x6610 | Поляна | Meadow | TYP:0x6610 | unclassified | terrain | 42.8212094, 26.1542845 |
| 0x6613 | Водопад | Waterfall | TYP:0x6613 | waterway=waterfall | normal | 42.7435327, 24.6398234 |
| 0x6614 | Скала | Rock | TYP:0x6614 | natural=rock | terrain | 42.7437901, 25.2693915 |
| 0x6615 | Пещера | Cave | TYP:0x6615 | natural=cave_entrance | normal | 41.8003178, 23.3853436 |
| 0x6617 | Връх | Peak | TYP:0x6617 | natural=peak | normal | 41.7893529, 22.9647946 |
| 0x6618 | Дърво | Tree | TYP:0x6618 | natural=tree | normal | 42.7752686, 23.9033318 |
| 0x6619 | Праисторически обект | Prehistoric site | TYP:0x6619 | historic=archaeological_site | normal | 42.5703692, 25.3767228 |
| 0x661a | Античен обект | Antiquity site | TYP:0x661a | historic=archaeological_site | normal | 41.7153454, 26.0307527 |
| 0x661b | Средновековен обект | Medieval site | TYP:0x661b | historic=archaeological_site | normal | 42.0922279, 24.2505813 |
**Caveats:** “unclassified” means the converter retains provenance but does not forge an amenity to force an icon. `zoom_hint` is nonfunctional on stock OsmAnd unless a style is separately changed. Neither legend-derived geometry nor icons may be redistributed under arbitrary licenses.
+13
View File
@@ -0,0 +1,13 @@
{
"status": "PASS_preparation_only",
"subset": "Vihren region; no national OSM/OBF build performed",
"bbox": "23.30,41.67,23.50,41.84",
"workers": 4,
"total_pois": 1915,
"unknown_fallback": 150,
"spot_heights_preserved": 296,
"unclassified": 0,
"hiking_route_relations": 82,
"candidate_overlay_segments": 273,
"caveat": "Serial route grouping differs (78 vs 82). OBF rendering not validated."
}
+63
View File
@@ -0,0 +1,63 @@
{
"release": "v0.92",
"test_environment": "Linux runtime, C++17, Python 3",
"unit_tests": "3/3 CTest cases passed; v0.92 suite 5 individual tests",
"source_img_size_bytes": 441942016,
"vihren": {
"bbox": "23.36,41.70,23.42,41.76",
"pois": 221,
"ways": 5597,
"colored_source_segments": 53,
"hiking_route_relations": 17,
"physical_highway_segments_reused": 48,
"unmatched_overlay_segments": 5,
"serial_vs_parallel": "identical counts, mapping, and relation statistics"
},
"plovdiv": {
"bbox": "24.63,42.02,24.67,42.05",
"example_pois": [
{
"type": "0x6602",
"lat": 42.0346141,
"lon": 24.6528697,
"osm": "tourism=viewpoint"
},
{
"type": "0x6500",
"lat": 42.0347643,
"lon": 24.6522689,
"osm": "man_made=water_intake"
},
{
"type": "0x6500",
"lat": 42.0340991,
"lon": 24.6520114,
"osm": "man_made=water_intake"
}
]
},
"elevation": {
"bbox": "22.29,43.88,22.34,43.93",
"spot_height_points": 23,
"with_ele_tag": 23
},
"future_template": {
"tested": true,
"discovered_poi_types_in_bbox": 21,
"discovered_line_types_in_bbox": 16
},
"obf_build": {
"status": "NOT_TESTED",
"reason": "Official OsmAndMapCreator is not installed and runtime has no network access"
},
"stock_icon_visibility": {
"status": "NOT_VERIFIED",
"reason": "Stock OsmAnd icon and zoom behaviour depends on app version, built-in map style and settings"
},
"notes": [
"zero independent Garmin icon PNGs packaged",
"two editable CSV mapping files",
"C++ jobs default 1",
"Native OSM route relations, no custom phone renderer"
]
}
+15
View File
@@ -0,0 +1,15 @@
{
"status": "PASS",
"counts": {
"pois": 101,
"has_native_semantics": 43,
"generated_name": 42,
"has_bgm_poi_tag": 101,
"needs_custom_style": 58
},
"catalog_types": 13,
"exported_types": 13,
"note": "POIs without standard OSM categories are valid for this PRIVATE map because bgm_poi uses custom index and renderer. They require importing the style; this does not prove OBF persistence or screen visibility.",
"errors": [],
"missing_types_from_catalog": []
}
+102
View File
@@ -0,0 +1,102 @@
mapset,kind,code,lat,lon,name
"02234083","classic","0x6614",41.7204094,23.3722329,"Скала - 3310ACQWEHI"
"02234083","classic","0x6616",41.7381334,23.3741426,"Гергийца (2589)"
"02234083","classic","0x660c",41.7340136,23.3734131,"Порта"
"02234083","classic","0x6616",41.7392492,23.3764601,"Безимен (2571)"
"02234083","classic","0x6616",41.7404938,23.3828115,"Безимен (2554)"
"02234083","classic","0x6616",41.7317605,23.3725762,"Безимен (2543)"
"02234083","classic","0x6615",41.7433262,23.3739281,"Дупка"
"02234083","classic","0x660c",41.7268896,23.3712673,"Синанишка порта"
"02234083","classic","0x6600",41.7258811,23.3701301,"2467"
"02234083","classic","0x6616",41.7444634,23.3775759,"Муратова чука (2449)"
"02234083","classic","0x6503",41.7358160,23.3705378,"Рзкл"
"02234083","classic","0x6414",41.7449141,23.3817816,"Вода (поток)"
"02234083","classic","0x2800",41.7440128,23.3833480,"Гергийца"
"02234083","classic","0x6600",41.7351937,23.4019947,"2592"
"02234083","classic","0x6600",41.7336702,23.4037971,"2609"
"02234083","classic","0x6616",41.7310739,23.4095478,"(2636)"
"02234083","classic","0x660c",41.7308378,23.4024239,"Седл Малко-Голямо Спано поле-2527"
"02234083","classic","0x6616",41.7301941,23.3993340,"Спанополски чукар (2576)"
"02234083","classic","0x6600",41.7301941,23.3989048,"2576"
"02234083","classic","0x6602",41.7262030,23.4098053,"Панорама Башлийски и Бъндеришки чукар"
"02234083","classic","0x6614",41.7244649,23.4084320,"Скала - ZZC90NYTOCF"
"02234083","classic","0x6600",41.7223191,23.4024024,"2404"
"02234083","classic","0x650b",41.7234993,23.3853221,"Зимна маркировка - QRY7DY72FMJ"
"02234083","classic","0x650b",41.7258811,23.3845925,"Зимна маркировка - 8JT9VM2UB7C"
"02234083","classic","0x650b",41.7268896,23.3845067,"Зимна маркировка - L9KQU03N0Y5"
"02234083","classic","0x650b",41.7277479,23.3846354,"Зимна маркировка - BRWNXVGHJ2A"
"02234083","classic","0x650b",41.7285633,23.3850217,"Зимна маркировка - QCHZ1FXG1S6"
"02234083","classic","0x650b",41.7290139,23.3851933,"Зимна маркировка - L9UJHM8M3L5"
"02234083","classic","0x650b",41.7302155,23.3854723,"Зимна маркировка - ZLGO6R0CWUL"
"02234083","classic","0x6511",41.7302585,23.3907509,"Вода - Извор - SZ7MXII06CB"
"02234083","classic","0x650b",41.7324471,23.3863521,"Зимна маркировка - 0PO1Y4QNTKQ"
"02234083","classic","0x650b",41.7333913,23.3868670,"Зимна маркировка - S0OOBD9DMT9"
"02234083","classic","0x650b",41.7340779,23.3873820,"Зимна маркировка - ZJJ69H95896"
"02234083","classic","0x650b",41.7346787,23.3876395,"Зимна маркировка - LRUKQQLW46P"
"02234083","classic","0x650b",41.7201734,23.3826399,"Зимна маркировка - ECDE8HOEXT6"
"02234083","classic","0x650b",41.7204738,23.3879399,"Зимна маркировка - BJH9BWY0S43"
"02234083","classic","0x650b",41.7204738,23.3852148,"Зимна маркировка - EON21NTVSS2"
"02234083","classic","0x650b",41.7205381,23.3832836,"Зимна маркировка - BJ2IFV25F7B"
"02234083","classic","0x650b",41.7206669,23.3861375,"Зимна маркировка - KN9AQOZ1709"
"02234083","classic","0x650b",41.7206669,23.3843994,"Зимна маркировка - UPFR04SCKJ6"
"02234083","classic","0x6503",41.7207956,23.3865023,"Рзкл жълта/кафява маркировка"
"02234083","classic","0x650b",41.7208600,23.3870173,"Зимна маркировка - ETSMTX323F0"
"02234083","classic","0x650b",41.7209244,23.3838630,"Зимна маркировка - 2CH7W5QYBST"
"02234083","classic","0x650b",41.7210317,23.3819532,"Зимна маркировка - 0H20QTLK2D8"
"02234083","classic","0x650b",41.7217612,23.3817601,"Зимна маркировка - UR9S90UU4PI"
"02234083","classic","0x650b",41.7224050,23.3814812,"Зимна маркировка - 0WITPVLXNZ5"
"02234083","classic","0x650b",41.7226624,23.3854723,"Зимна маркировка - KKJ6VOB1UHB"
"02234083","classic","0x6503",41.7229199,23.3789277,"Рзкл зимна"
"02234083","classic","0x640a",41.7336702,23.4035397,"Опасно слизане"
"02234083","classic","0x2800",41.7288423,23.3857727,"Големо Спано поле"
"02234083","classic","0x2800",41.7323613,23.4036040,"Караулите"
"02234083","classic","0x2800",41.7231774,23.4089899,"Малко Спано поле"
"02234083","classic","0x6600",41.7415237,23.3972311,"2501"
"02234083","classic","0x660c",41.7417812,23.3968449,"Бъндеришка порта"
"02234083","classic","0x6600",41.7420816,23.3896351,"2468"
"02234083","classic","0x6600",41.7421889,23.4056425,"2351"
"02234083","classic","0x6600",41.7431545,23.4037542,"2314"
"02234083","classic","0x6600",41.7439270,23.4045053,"2304"
"02234083","classic","0x6600",41.7442489,23.4093547,"2229"
"02234083","classic","0x6616",41.7449570,23.3969307,"Муратов връх (2669)"
"02234083","classic","0x6600",41.7452145,23.4076166,"2241"
"02234083","classic","0x6600",41.7461371,23.4070802,"2248"
"02234083","classic","0x660c",41.7486048,23.3911800,"Муратова премка"
"02234083","classic","0x660c",41.7495489,23.3981752,"Влахински превал"
"02234083","classic","0x660c",41.7514372,23.3985186,"Влахински превал"
"02234083","classic","0x6616",41.7519951,23.3843350,"Гредаро (2605)"
"02234083","classic","0x6602",41.7353010,23.4009218,"Иглата на Караулите"
"02234083","classic","0x6600",41.7354727,23.4006214,"2586"
"02234083","classic","0x6600",41.7358804,23.3998060,"2578"
"02234083","classic","0x6600",41.7360520,23.3994627,"2597"
"02234083","classic","0x6600",41.7374468,23.4049559,"2504"
"02234083","classic","0x6503",41.7538404,23.3998060,"Рзкл зимно ребро от Муратово езеро"
"02234083","classic","0x6511",41.7506433,23.3959866,"Вода - Извор - D7562E5MBLA"
"02234083","classic","0x650b",41.7363524,23.3882833,"Зимна маркировка - ML7GHHLGGVB"
"02234083","classic","0x6503",41.7493987,23.3964372,"Кръстопът - JS2LQVK6Z0Y"
"02234083","classic","0x6503",41.7450428,23.4058356,"Рзкл Муратово ез."
"02234083","classic","0x650b",41.7410517,23.3955145,"Зимна маркировка - ZVN3B0ZG6Z7"
"02234083","classic","0x650b",41.7415667,23.3965015,"Зимна маркировка - C0GJITD9Q3J"
"02234083","classic","0x6511",41.7416954,23.3946991,"Вода - Извор - YY24QUNHLSL"
"02234083","classic","0x650b",41.7419100,23.3969736,"Зимна маркировка - N2XVWARW03F"
"02234083","classic","0x650b",41.7400217,23.3932185,"Зимна маркировка - T5WTMHMWT4K"
"02234083","classic","0x650b",41.7395926,23.3919525,"Зимна маркировка - UFCHM8SJ1D7"
"02234083","classic","0x650b",41.7391205,23.3905363,"Зимна маркировка - O9IF7BPSJR6"
"02234083","classic","0x650b",41.7378759,23.3894420,"Зимна маркировка - XCWILHV9I1K"
"02234083","classic","0x6503",41.7445707,23.4080887,"Рзкл Муратово езеро"
"02234083","classic","0x6414",41.7549562,23.3888626,"Вода - Чешма - XSX8SGXW0S5"
"02234083","classic","0x6414",41.7544627,23.3887553,"Вода - Чешма - X8HNWYW73EC"
"02234083","classic","0x6415",41.7402792,23.3953857,"Бивак"
"02234083","classic","0x640a",41.7359018,23.3995056,"Опасно слизане"
"02234083","classic","0x6415",41.7453432,23.4067583,"Бивак"
"02234083","classic","0x6414",41.7509651,23.4011364,"Вода - Чешма - 4DAF49MGS8O"
"02234083","classic","0x640a",41.7456007,23.3963728,"Опасно слизане"
"02234083","classic","0x6414",41.7461586,23.4052134,"Вода (поток)"
"02234083","classic","0x6414",41.7388630,23.3864164,"Вода - Чешма - 6B4RAQRPAHT"
"02234083","classic","0x6414",41.7479610,23.4026814,"Вода - Чешма - B107V8QHR57"
"02234083","classic","0x6414",41.7468023,23.3872318,"Вода (поток)"
"02234083","classic","0x6415",41.7465878,23.4065437,"Бивак"
"02234083","classic","0x2800",41.7509651,23.3984327,"Влахински превал"
"02234083","classic","0x2800",41.7442060,23.4046984,"Муратов циркус"
"02234083","classic","0x2800",41.7368460,23.3897853,"Циркус Големо Спано поле"
"02234083","classic","0x2800",41.7449570,23.4051275,"Овинати езера"
1 mapset kind code lat lon name
2 02234083 classic 0x6614 41.7204094 23.3722329 Скала - 3310ACQWEHI
3 02234083 classic 0x6616 41.7381334 23.3741426 Гергийца (2589)
4 02234083 classic 0x660c 41.7340136 23.3734131 Порта
5 02234083 classic 0x6616 41.7392492 23.3764601 Безимен (2571)
6 02234083 classic 0x6616 41.7404938 23.3828115 Безимен (2554)
7 02234083 classic 0x6616 41.7317605 23.3725762 Безимен (2543)
8 02234083 classic 0x6615 41.7433262 23.3739281 Дупка
9 02234083 classic 0x660c 41.7268896 23.3712673 Синанишка порта
10 02234083 classic 0x6600 41.7258811 23.3701301 2467
11 02234083 classic 0x6616 41.7444634 23.3775759 Муратова чука (2449)
12 02234083 classic 0x6503 41.7358160 23.3705378 Рзкл
13 02234083 classic 0x6414 41.7449141 23.3817816 Вода (поток)
14 02234083 classic 0x2800 41.7440128 23.3833480 Гергийца
15 02234083 classic 0x6600 41.7351937 23.4019947 2592
16 02234083 classic 0x6600 41.7336702 23.4037971 2609
17 02234083 classic 0x6616 41.7310739 23.4095478 (2636)
18 02234083 classic 0x660c 41.7308378 23.4024239 Седл Малко-Голямо Спано поле-2527
19 02234083 classic 0x6616 41.7301941 23.3993340 Спанополски чукар (2576)
20 02234083 classic 0x6600 41.7301941 23.3989048 2576
21 02234083 classic 0x6602 41.7262030 23.4098053 Панорама Башлийски и Бъндеришки чукар
22 02234083 classic 0x6614 41.7244649 23.4084320 Скала - ZZC90NYTOCF
23 02234083 classic 0x6600 41.7223191 23.4024024 2404
24 02234083 classic 0x650b 41.7234993 23.3853221 Зимна маркировка - QRY7DY72FMJ
25 02234083 classic 0x650b 41.7258811 23.3845925 Зимна маркировка - 8JT9VM2UB7C
26 02234083 classic 0x650b 41.7268896 23.3845067 Зимна маркировка - L9KQU03N0Y5
27 02234083 classic 0x650b 41.7277479 23.3846354 Зимна маркировка - BRWNXVGHJ2A
28 02234083 classic 0x650b 41.7285633 23.3850217 Зимна маркировка - QCHZ1FXG1S6
29 02234083 classic 0x650b 41.7290139 23.3851933 Зимна маркировка - L9UJHM8M3L5
30 02234083 classic 0x650b 41.7302155 23.3854723 Зимна маркировка - ZLGO6R0CWUL
31 02234083 classic 0x6511 41.7302585 23.3907509 Вода - Извор - SZ7MXII06CB
32 02234083 classic 0x650b 41.7324471 23.3863521 Зимна маркировка - 0PO1Y4QNTKQ
33 02234083 classic 0x650b 41.7333913 23.3868670 Зимна маркировка - S0OOBD9DMT9
34 02234083 classic 0x650b 41.7340779 23.3873820 Зимна маркировка - ZJJ69H95896
35 02234083 classic 0x650b 41.7346787 23.3876395 Зимна маркировка - LRUKQQLW46P
36 02234083 classic 0x650b 41.7201734 23.3826399 Зимна маркировка - ECDE8HOEXT6
37 02234083 classic 0x650b 41.7204738 23.3879399 Зимна маркировка - BJH9BWY0S43
38 02234083 classic 0x650b 41.7204738 23.3852148 Зимна маркировка - EON21NTVSS2
39 02234083 classic 0x650b 41.7205381 23.3832836 Зимна маркировка - BJ2IFV25F7B
40 02234083 classic 0x650b 41.7206669 23.3861375 Зимна маркировка - KN9AQOZ1709
41 02234083 classic 0x650b 41.7206669 23.3843994 Зимна маркировка - UPFR04SCKJ6
42 02234083 classic 0x6503 41.7207956 23.3865023 Рзкл жълта/кафява маркировка
43 02234083 classic 0x650b 41.7208600 23.3870173 Зимна маркировка - ETSMTX323F0
44 02234083 classic 0x650b 41.7209244 23.3838630 Зимна маркировка - 2CH7W5QYBST
45 02234083 classic 0x650b 41.7210317 23.3819532 Зимна маркировка - 0H20QTLK2D8
46 02234083 classic 0x650b 41.7217612 23.3817601 Зимна маркировка - UR9S90UU4PI
47 02234083 classic 0x650b 41.7224050 23.3814812 Зимна маркировка - 0WITPVLXNZ5
48 02234083 classic 0x650b 41.7226624 23.3854723 Зимна маркировка - KKJ6VOB1UHB
49 02234083 classic 0x6503 41.7229199 23.3789277 Рзкл зимна
50 02234083 classic 0x640a 41.7336702 23.4035397 Опасно слизане
51 02234083 classic 0x2800 41.7288423 23.3857727 Големо Спано поле
52 02234083 classic 0x2800 41.7323613 23.4036040 Караулите
53 02234083 classic 0x2800 41.7231774 23.4089899 Малко Спано поле
54 02234083 classic 0x6600 41.7415237 23.3972311 2501
55 02234083 classic 0x660c 41.7417812 23.3968449 Бъндеришка порта
56 02234083 classic 0x6600 41.7420816 23.3896351 2468
57 02234083 classic 0x6600 41.7421889 23.4056425 2351
58 02234083 classic 0x6600 41.7431545 23.4037542 2314
59 02234083 classic 0x6600 41.7439270 23.4045053 2304
60 02234083 classic 0x6600 41.7442489 23.4093547 2229
61 02234083 classic 0x6616 41.7449570 23.3969307 Муратов връх (2669)
62 02234083 classic 0x6600 41.7452145 23.4076166 2241
63 02234083 classic 0x6600 41.7461371 23.4070802 2248
64 02234083 classic 0x660c 41.7486048 23.3911800 Муратова премка
65 02234083 classic 0x660c 41.7495489 23.3981752 Влахински превал
66 02234083 classic 0x660c 41.7514372 23.3985186 Влахински превал
67 02234083 classic 0x6616 41.7519951 23.3843350 Гредаро (2605)
68 02234083 classic 0x6602 41.7353010 23.4009218 Иглата на Караулите
69 02234083 classic 0x6600 41.7354727 23.4006214 2586
70 02234083 classic 0x6600 41.7358804 23.3998060 2578
71 02234083 classic 0x6600 41.7360520 23.3994627 2597
72 02234083 classic 0x6600 41.7374468 23.4049559 2504
73 02234083 classic 0x6503 41.7538404 23.3998060 Рзкл зимно ребро от Муратово езеро
74 02234083 classic 0x6511 41.7506433 23.3959866 Вода - Извор - D7562E5MBLA
75 02234083 classic 0x650b 41.7363524 23.3882833 Зимна маркировка - ML7GHHLGGVB
76 02234083 classic 0x6503 41.7493987 23.3964372 Кръстопът - JS2LQVK6Z0Y
77 02234083 classic 0x6503 41.7450428 23.4058356 Рзкл Муратово ез.
78 02234083 classic 0x650b 41.7410517 23.3955145 Зимна маркировка - ZVN3B0ZG6Z7
79 02234083 classic 0x650b 41.7415667 23.3965015 Зимна маркировка - C0GJITD9Q3J
80 02234083 classic 0x6511 41.7416954 23.3946991 Вода - Извор - YY24QUNHLSL
81 02234083 classic 0x650b 41.7419100 23.3969736 Зимна маркировка - N2XVWARW03F
82 02234083 classic 0x650b 41.7400217 23.3932185 Зимна маркировка - T5WTMHMWT4K
83 02234083 classic 0x650b 41.7395926 23.3919525 Зимна маркировка - UFCHM8SJ1D7
84 02234083 classic 0x650b 41.7391205 23.3905363 Зимна маркировка - O9IF7BPSJR6
85 02234083 classic 0x650b 41.7378759 23.3894420 Зимна маркировка - XCWILHV9I1K
86 02234083 classic 0x6503 41.7445707 23.4080887 Рзкл Муратово езеро
87 02234083 classic 0x6414 41.7549562 23.3888626 Вода - Чешма - XSX8SGXW0S5
88 02234083 classic 0x6414 41.7544627 23.3887553 Вода - Чешма - X8HNWYW73EC
89 02234083 classic 0x6415 41.7402792 23.3953857 Бивак
90 02234083 classic 0x640a 41.7359018 23.3995056 Опасно слизане
91 02234083 classic 0x6415 41.7453432 23.4067583 Бивак
92 02234083 classic 0x6414 41.7509651 23.4011364 Вода - Чешма - 4DAF49MGS8O
93 02234083 classic 0x640a 41.7456007 23.3963728 Опасно слизане
94 02234083 classic 0x6414 41.7461586 23.4052134 Вода (поток)
95 02234083 classic 0x6414 41.7388630 23.3864164 Вода - Чешма - 6B4RAQRPAHT
96 02234083 classic 0x6414 41.7479610 23.4026814 Вода - Чешма - B107V8QHR57
97 02234083 classic 0x6414 41.7468023 23.3872318 Вода (поток)
98 02234083 classic 0x6415 41.7465878 23.4065437 Бивак
99 02234083 classic 0x2800 41.7509651 23.3984327 Влахински превал
100 02234083 classic 0x2800 41.7442060 23.4046984 Муратов циркус
101 02234083 classic 0x2800 41.7368460 23.3897853 Циркус Големо Спано поле
102 02234083 classic 0x2800 41.7449570 23.4051275 Овинати езера
+14
View File
@@ -0,0 +1,14 @@
code,kind,count,named,default_tags,has_typ_icon,configured_rules,example1_lat,example1_lon,example1_name,example1_map,example2_lat,example2_lon,example2_name,example2_map,example3_lat,example3_lon,example3_name,example3_map,example4_lat,example4_lon,example4_name,example4_map,example5_lat,example5_lon,example5_name,example5_map
"0x2800",classic,8,8,"place=locality",no,1,41.7440128,23.3833480,"Гергийца","02234083",41.7509651,23.3984327,"Влахински превал","02234083",41.7288423,23.3857727,"Големо Спано поле","02234083",41.7231774,23.4089899,"Малко Спано поле","02234083",41.7442060,23.4046984,"Муратов циркус","02234083"
"0x640a",classic,3,3,"garmin:legend=danger",yes,1,41.7456007,23.3963728,"Опасно слизане","02234083",41.7359018,23.3995056,"Опасно слизане","02234083",41.7336702,23.4035397,"Опасно слизане","02234083",,,,,,,,
"0x6414",classic,8,8,"garmin:legend=drinking_water;amenity=drinking_water",yes,1,41.7544627,23.3887553,"Вода - Чешма - X8HNWYW73EC","02234083",41.7388630,23.3864164,"Вода - Чешма - 6B4RAQRPAHT","02234083",41.7468023,23.3872318,"Вода (поток)","02234083",41.7509651,23.4011364,"Вода - Чешма - 4DAF49MGS8O","02234083",41.7449141,23.3817816,"Вода (поток)","02234083"
"0x6415",classic,3,3,"garmin:legend=camp_site;tourism=camp_site",yes,1,41.7402792,23.3953857,"Бивак","02234083",41.7453432,23.4067583,"Бивак","02234083",41.7465878,23.4065437,"Бивак","02234083",,,,,,,,
"0x6503",classic,7,7,"garmin:legend=crossroad",yes,1,41.7538404,23.3998060,"Рзкл зимно ребро от Муратово езеро","02234083",41.7445707,23.4080887,"Рзкл Муратово езеро","02234083",41.7358160,23.3705378,"Рзкл","02234083",41.7493987,23.3964372,"Кръстопът - JS2LQVK6Z0Y","02234083",41.7229199,23.3789277,"Рзкл зимна","02234083"
"0x650b",classic,31,31,"garmin:legend=winter_marking",yes,1,41.7419100,23.3969736,"Зимна маркировка - N2XVWARW03F","02234083",41.7285633,23.3850217,"Зимна маркировка - QCHZ1FXG1S6","02234083",41.7205381,23.3832836,"Зимна маркировка - BJ2IFV25F7B","02234083",41.7395926,23.3919525,"Зимна маркировка - UFCHM8SJ1D7","02234083",41.7324471,23.3863521,"Зимна маркировка - 0PO1Y4QNTKQ","02234083"
"0x6511",classic,3,3,"garmin:legend=spring;natural=spring",yes,1,41.7506433,23.3959866,"Вода - Извор - D7562E5MBLA","02234083",41.7416954,23.3946991,"Вода - Извор - YY24QUNHLSL","02234083",41.7302585,23.3907509,"Вода - Извор - SZ7MXII06CB","02234083",,,,,,,,
"0x6600",classic,17,17,"garmin:legend=triangulation",yes,1,41.7415237,23.3972311,"2501","02234083",41.7421889,23.4056425,"2351","02234083",41.7442489,23.4093547,"2229","02234083",41.7452145,23.4076166,"2241","02234083",41.7336702,23.4037971,"2609","02234083"
"0x6602",classic,2,2,"garmin:legend=viewpoint;tourism=viewpoint",yes,1,41.7262030,23.4098053,"Панорама Башлийски и Бъндеришки чукар","02234083",41.7353010,23.4009218,"Иглата на Караулите","02234083",,,,,,,,,,,,
"0x660c",classic,7,7,"natural=saddle",yes,1,41.7514372,23.3985186,"Влахински превал","02234083",41.7417812,23.3968449,"Бъндеришка порта","02234083",41.7495489,23.3981752,"Влахински превал","02234083",41.7486048,23.3911800,"Муратова премка","02234083",41.7340136,23.3734131,"Порта","02234083"
"0x6614",classic,2,2,"garmin:legend=rock",yes,1,41.7204094,23.3722329,"Скала - 3310ACQWEHI","02234083",41.7244649,23.4084320,"Скала - ZZC90NYTOCF","02234083",,,,,,,,,,,,
"0x6615",classic,1,1,"garmin:legend=cave;natural=cave_entrance",yes,1,41.7433262,23.3739281,"Дупка","02234083",,,,,,,,,,,,,,,,
"0x6616",classic,9,9,"natural=peak",no,1,41.7404938,23.3828115,"Безимен (2554)","02234083",41.7381334,23.3741426,"Гергийца (2589)","02234083",41.7317605,23.3725762,"Безимен (2543)","02234083",41.7444634,23.3775759,"Муратова чука (2449)","02234083",41.7301941,23.3993340,"Спанополски чукар (2576)","02234083"
1 code kind count named default_tags has_typ_icon configured_rules example1_lat example1_lon example1_name example1_map example2_lat example2_lon example2_name example2_map example3_lat example3_lon example3_name example3_map example4_lat example4_lon example4_name example4_map example5_lat example5_lon example5_name example5_map
2 0x2800 classic 8 8 place=locality no 1 41.7440128 23.3833480 Гергийца 02234083 41.7509651 23.3984327 Влахински превал 02234083 41.7288423 23.3857727 Големо Спано поле 02234083 41.7231774 23.4089899 Малко Спано поле 02234083 41.7442060 23.4046984 Муратов циркус 02234083
3 0x640a classic 3 3 garmin:legend=danger yes 1 41.7456007 23.3963728 Опасно слизане 02234083 41.7359018 23.3995056 Опасно слизане 02234083 41.7336702 23.4035397 Опасно слизане 02234083
4 0x6414 classic 8 8 garmin:legend=drinking_water;amenity=drinking_water yes 1 41.7544627 23.3887553 Вода - Чешма - X8HNWYW73EC 02234083 41.7388630 23.3864164 Вода - Чешма - 6B4RAQRPAHT 02234083 41.7468023 23.3872318 Вода (поток) 02234083 41.7509651 23.4011364 Вода - Чешма - 4DAF49MGS8O 02234083 41.7449141 23.3817816 Вода (поток) 02234083
5 0x6415 classic 3 3 garmin:legend=camp_site;tourism=camp_site yes 1 41.7402792 23.3953857 Бивак 02234083 41.7453432 23.4067583 Бивак 02234083 41.7465878 23.4065437 Бивак 02234083
6 0x6503 classic 7 7 garmin:legend=crossroad yes 1 41.7538404 23.3998060 Рзкл зимно ребро от Муратово езеро 02234083 41.7445707 23.4080887 Рзкл Муратово езеро 02234083 41.7358160 23.3705378 Рзкл 02234083 41.7493987 23.3964372 Кръстопът - JS2LQVK6Z0Y 02234083 41.7229199 23.3789277 Рзкл зимна 02234083
7 0x650b classic 31 31 garmin:legend=winter_marking yes 1 41.7419100 23.3969736 Зимна маркировка - N2XVWARW03F 02234083 41.7285633 23.3850217 Зимна маркировка - QCHZ1FXG1S6 02234083 41.7205381 23.3832836 Зимна маркировка - BJ2IFV25F7B 02234083 41.7395926 23.3919525 Зимна маркировка - UFCHM8SJ1D7 02234083 41.7324471 23.3863521 Зимна маркировка - 0PO1Y4QNTKQ 02234083
8 0x6511 classic 3 3 garmin:legend=spring;natural=spring yes 1 41.7506433 23.3959866 Вода - Извор - D7562E5MBLA 02234083 41.7416954 23.3946991 Вода - Извор - YY24QUNHLSL 02234083 41.7302585 23.3907509 Вода - Извор - SZ7MXII06CB 02234083
9 0x6600 classic 17 17 garmin:legend=triangulation yes 1 41.7415237 23.3972311 2501 02234083 41.7421889 23.4056425 2351 02234083 41.7442489 23.4093547 2229 02234083 41.7452145 23.4076166 2241 02234083 41.7336702 23.4037971 2609 02234083
10 0x6602 classic 2 2 garmin:legend=viewpoint;tourism=viewpoint yes 1 41.7262030 23.4098053 Панорама Башлийски и Бъндеришки чукар 02234083 41.7353010 23.4009218 Иглата на Караулите 02234083
11 0x660c classic 7 7 natural=saddle yes 1 41.7514372 23.3985186 Влахински превал 02234083 41.7417812 23.3968449 Бъндеришка порта 02234083 41.7495489 23.3981752 Влахински превал 02234083 41.7486048 23.3911800 Муратова премка 02234083 41.7340136 23.3734131 Порта 02234083
12 0x6614 classic 2 2 garmin:legend=rock yes 1 41.7204094 23.3722329 Скала - 3310ACQWEHI 02234083 41.7244649 23.4084320 Скала - ZZC90NYTOCF 02234083
13 0x6615 classic 1 1 garmin:legend=cave;natural=cave_entrance yes 1 41.7433262 23.3739281 Дупка 02234083
14 0x6616 classic 9 9 natural=peak no 1 41.7404938 23.3828115 Безимен (2554) 02234083 41.7381334 23.3741426 Гергийца (2589) 02234083 41.7317605 23.3725762 Безимен (2543) 02234083 41.7444634 23.3775759 Муратова чука (2449) 02234083 41.7301941 23.3993340 Спанополски чукар (2576) 02234083
+12
View File
@@ -0,0 +1,12 @@
{
"source_pois": 101,
"expected_after_drops": 101,
"declared_drops": 0,
"osm_pois": 101,
"missing": 0,
"extra": 0,
"rule_tag_violations": {},
"examples": [],
"status": "PASS",
"limitation": "Same IMG decoder is used for manifest and OSM: this validates export fidelity, not ground truth or OBF persistence."
}
Binary file not shown.
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""Prefer existing physical highway ways as native hiking-route relation members.
Never invent highway=path. Only attach where exact coordinate-node edge coverage >=90%
and physical-way overrun <=10%; otherwise preserve original overlay member.
"""
import argparse,collections,json,pathlib,xml.etree.ElementTree as ET
from native_translate import mapping,tagdict,top_level_elements
def edges(refs):
for a,b in zip(refs,refs[1:]):
if a!=b:yield (a,b) if a<b else (b,a)
def match(src,dst,line_cfg,report):
candidates={c for c,r in mapping(line_cfg).items() if r.get('route_mode')=='relation'}
overlay_refs={};lookup=collections.defaultdict(set)
with open(src,'rb') as f:
for e in top_level_elements(f):
if e.tag=='way':
t=tagdict(e)
if t.get('garmin:type') in candidates and t.get('garmin:feature')=='extended_polyline':
oid=int(e.get('id'))
ns=[int(n.get('ref')) for n in e.findall('nd')]
if len(ns)>=2:
overlay_refs[oid]=ns
for edge in edges(ns):lookup[edge].add(oid)
e.clear()
elif e.tag in ('node','relation'):e.clear()
matches=collections.defaultdict(collections.Counter);physical_len={};written=0
with open(src,'rb') as f,open(dst,'w',encoding='utf-8') as out:
out.write('<?xml version="1.0" encoding="UTF-8"?>\n<osm version="0.6" generator="garmin2osm-v0.92" upload="never">\n')
replaced={}; relations=0
for e in top_level_elements(f):
if e.tag=='way':
t=tagdict(e)
if 'highway' in t:
pid=int(e.get('id'));ns=[int(n.get('ref')) for n in e.findall('nd')]
physical_len[pid]=max(0,len(ns)-1)
for edge in edges(ns):
for oid in lookup.get(edge,()):matches[oid][pid]+=1
elif e.tag=='relation':
if not replaced:
for oid,refs in overlay_refs.items():
nseg=max(1,len(refs)-1)
for pid,matched in matches.get(oid,{}).most_common():
# Not allowed to extend a colored marking beyond its actual physical segment.
if matched/nseg>=0.9 and physical_len.get(pid,10**9)<=nseg*1.1:
replaced[oid]=pid;break
tt=tagdict(e)
if tt.get('route')=='hiking' and tt.get('bgm:source_feature')=='extended_polyline':
seen=set();relations+=1
for m in list(e.findall('member')):
val=int(m.get('ref'))
ref=str(replaced.get(val,val))
if ref in seen:e.remove(m)
else:m.set('ref',ref);seen.add(ref)
out.write(ET.tostring(e,encoding='unicode')+'\n');written+=1;e.clear()
out.write('</osm>\n')
summary={'candidate_overlay_ways':len(overlay_refs),'attached_to_existing_highway':len(replaced),
'unmatched_overlay_members':len(overlay_refs)-len(replaced),'hiking_relations':relations,
'policy':'Attach only with >=90% shared physical segments and <=10% physical overrun. No invented routable ways.'}
pathlib.Path(report).write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
return summary
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('--osm',required=True);p.add_argument('--out',required=True);p.add_argument('--lines',required=True);p.add_argument('--report',required=True)
a=p.parse_args();print(json.dumps(match(a.osm,a.out,a.lines,a.report),ensure_ascii=False,indent=2))
if __name__=='__main__':main()
+57 -23
View File
@@ -84,7 +84,7 @@ def write_xml(tree,path):
tree.write(path,encoding='utf-8',xml_declaration=True) tree.write(path,encoding='utf-8',xml_declaration=True)
ET.parse(path) ET.parse(path)
def make_configs(poi_base,render_base,catalog,folder,threshold,frequent_zoom=15,rare_zoom=17): def make_configs(poi_base,render_base,catalog,folder,threshold,frequent_zoom=15,rare_zoom=17,route_styles=()):
# Keep entire upstream definitions! Replacing with a standalone tiny XML is # Keep entire upstream definitions! Replacing with a standalone tiny XML is
# precisely what used to make ordinary semantic POIs disappear. # precisely what used to make ordinary semantic POIs disappear.
poi=ET.parse(poi_base); proot=poi.getroot() poi=ET.parse(poi_base); proot=poi.getroot()
@@ -108,10 +108,15 @@ def make_configs(poi_base,render_base,catalog,folder,threshold,frequent_zoom=15,
code=r['code'] code=r['code']
ET.SubElement(target,'type',tag='bgm_poi',value='type_'+code, ET.SubElement(target,'type',tag='bgm_poi',value='type_'+code,
minzoom=str(frequent_zoom) if r['count']>=threshold else str(rare_zoom)) minzoom=str(frequent_zoom) if r['count']>=threshold else str(rare_zoom))
# Route overlays have their OWN map-index main types, not only an ignored metadata tag.
# Do not map decorative overlays to 'highway=path': that would create bogus routing.
for route in route_styles:
if route.get('show','yes').lower() not in ('yes','1','true'): continue
ET.SubElement(target,'type',tag='bgm_route',value='type_'+route['code'],minzoom=route['display_minzoom'])
rp=folder/'rendering_types.bgmountains.xml' rp=folder/'rendering_types.bgmountains.xml'
write_xml(rendering,rp) write_xml(rendering,rp)
summary={ summary={
'catalog_types':len(catalog), 'catalog_types':len(catalog), 'route_overlay_types':len(route_styles),
'poi_count_in_catalog':sum(r.get('sample_count',r['count']) for r in catalog), 'poi_count_in_catalog':sum(r.get('sample_count',r['count']) for r in catalog),
'types_at_or_above_threshold':sum(r['count']>=threshold for r in catalog), 'types_at_or_above_threshold':sum(r['count']>=threshold for r in catalog),
'frequent_index_minzoom':frequent_zoom, 'rare_index_minzoom':rare_zoom, 'frequent_index_minzoom':frequent_zoom, 'rare_index_minzoom':rare_zoom,
@@ -124,10 +129,26 @@ def make_configs(poi_base,render_base,catalog,folder,threshold,frequent_zoom=15,
(folder/'type-index-summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') (folder/'type-index-summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
return pa,rp,summary return pa,rp,summary
def make_style(icon_csv, folder, catalog): def read_route_styles(path):
"""Optional addon for otherwise unclassified types using preinstalled OsmAnd icons. with Path(path).open(encoding='utf-8-sig',newline='') as f: rows=list(csv.DictReader(f))
if not rows or not {'code','color','show','display_minzoom','width'}.issubset(rows[0]):
raise SystemExit('Invalid trail-styles.csv: '+str(path))
seen=set()
for r in rows:
code='0x'+format(int(r['code'],0),'x')
if code in seen: raise SystemExit('Duplicate route style type: '+code)
seen.add(code);r['code']=code
if not (len(r['color'])==7 and r['color'][0]=='#' and all(c in '0123456789abcdefABCDEF' for c in r['color'][1:])):
raise SystemExit('Invalid CSS color in route type '+code)
if not (0.2<=float(r['width'])<=15 and 1<=int(r['display_minzoom'])<=21):
raise SystemExit('Invalid route width/zoom '+code)
return rows
This cannot install Garmin bitmap icons and must be enabled in OsmAnd. def make_style(icon_csv,folder,catalog,route_styles=()):
"""Selectable OsmAnd map style that maps BGM indexes to built-in icons.
The .obf itself only embeds geometry/tags. Android requires installing and
selecting this renderer; stock app ignores bgm_poi and bgm_route styling.
""" """
known={r['code'] for r in catalog} known={r['code'] for r in catalog}
with Path(icon_csv).open(encoding='utf-8-sig',newline='') as f: with Path(icon_csv).open(encoding='utf-8-sig',newline='') as f:
@@ -135,26 +156,37 @@ def make_style(icon_csv, folder, catalog):
visible=[] visible=[]
for r in rows: for r in rows:
code='0x'+format(int(r['code'],0),'x') code='0x'+format(int(r['code'],0),'x')
if code not in known:continue # bbox builds are smaller than national config if code not in known or r['show_in_custom_style'].strip().lower() not in ('yes','true','1'):
if r['show_in_custom_style'].strip().lower() not in ('yes','true','1'):continue continue
name=r['osmand_builtin_icon'].strip() name=r['osmand_builtin_icon'].strip()
if not name or not name.replace('_','').replace('-','').isalnum(): if not name or not name.replace('_','').replace('-','').isalnum():
raise SystemExit('Invalid OsmAnd icon in '+str(icon_csv)+' for '+code) raise SystemExit('Invalid OsmAnd icon: '+repr(name)+' code='+code)
zoom=int(r['minzoom']) zoom=int(r['minzoom'])
if not 1<=zoom<=21:raise SystemExit('Invalid icon zoom for '+code) if not 1<=zoom<=21: raise SystemExit('Bad icon zoom: '+code)
visible.append((code,name,zoom)) visible.append((code,name,zoom))
root=ET.Element('renderingStyle',name='BGMountains custom POIs',depends='default', root=ET.Element('renderingStyle',name='BGMountains outdoor v0.9',
defaultColor='#fff8ee',version='1') depends='default',defaultColor='#fff8ee',version='1')
order=ET.SubElement(root,'order') order=ET.SubElement(root,'order');points=ET.SubElement(root,'point')
points=ET.SubElement(root,'point') lines=ET.SubElement(root,'line')
for code,name,zoom in visible: for code,name,zoom in visible:
ET.SubElement(order,'case',tag='bgm_poi',value='type_'+code,minzoom=str(zoom), ET.SubElement(order,'case',tag='bgm_poi',value='type_'+code,
point='true',objectType='1',order='128') minzoom=str(zoom),point='true',objectType='1',order='120')
ET.SubElement(points,'case',tag='bgm_poi',value='type_'+code,minzoom=str(zoom), ET.SubElement(points,'case',tag='bgm_poi',value='type_'+code,minzoom=str(zoom),
icon=name,iconOrder='10',iconVisibleSize='12') icon=name,iconOrder='11',iconVisibleSize='16',intersectionSizeFactor='0.7')
path=folder/'BGMountains-custom.render.xml' route_count=0
for route in route_styles:
if route.get('show','yes').lower() not in ('yes','true','1'):continue
route_count+=1
code=route['code'];zoom=route['display_minzoom']
ET.SubElement(order,'case',tag='bgm_route',value='type_'+code,
minzoom=zoom,objectType='2',order='125')
# Color and width faithfully recover TYP base line color, but NOT its bitmap
# segment pattern. A precise pattern renderer requires a custom app build.
ET.SubElement(lines,'case',tag='bgm_route',value='type_'+code,
minzoom=zoom,color=route['color'],strokeWidth=route['width'],cap='ROUND')
path=folder/'BGMountains-outdoor.render.xml'
write_xml(ET.ElementTree(root),path) write_xml(ET.ElementTree(root),path)
return path,len(visible) return path,len(visible),route_count
def obtain_creator(cache, offline=False): def obtain_creator(cache, offline=False):
"""Opt-in download from official OsmAnd nightly build (large file).""" """Opt-in download from official OsmAnd nightly build (large file)."""
@@ -210,7 +242,8 @@ def main():
p.add_argument('--threshold',type=int,default=100,help='Frequent-type occurrence threshold') p.add_argument('--threshold',type=int,default=100,help='Frequent-type occurrence threshold')
p.add_argument('--frequent-zoom',type=int,default=15,help='Store frequent raw map icons at zoom N and above') p.add_argument('--frequent-zoom',type=int,default=15,help='Store frequent raw map icons at zoom N and above')
p.add_argument('--rare-zoom',type=int,default=17,help='Store rare raw map icons at zoom N and above') p.add_argument('--rare-zoom',type=int,default=17,help='Store rare raw map icons at zoom N and above')
p.add_argument('--icons-config',default=str(ROOT/'config/osmand-icons.csv'),help='OsmAnd built-in icon choices for custom addon style') p.add_argument('--icons-config',default=str(ROOT/'config/osmand-icons.csv'),help='OsmAnd icon mappings')
p.add_argument('--trail-styles',default=str(ROOT/'config/trail-styles.csv'),help='Garmin TYP coloured route overlay styles')
p.add_argument('--poi-overrides',default=str(ROOT/'config/poi-overrides-all-types.csv')) p.add_argument('--poi-overrides',default=str(ROOT/'config/poi-overrides-all-types.csv'))
p.add_argument('--poi-name-fixes',default=str(ROOT/'config/poi-name-fixes.csv')) p.add_argument('--poi-name-fixes',default=str(ROOT/'config/poi-name-fixes.csv'))
p.add_argument('--poi-labels',default=str(ROOT/'config/poi-labels-bg.csv'),help='Bulgarian prefixes for stable generated names on nameless Garmin POIs') p.add_argument('--poi-labels',default=str(ROOT/'config/poi-labels-bg.csv'),help='Bulgarian prefixes for stable generated names on nameless Garmin POIs')
@@ -272,9 +305,10 @@ def main():
folder=dest/'osmand-config';folder.mkdir(exist_ok=True) folder=dest/'osmand-config';folder.mkdir(exist_ok=True)
poi_base=base_xml(args.poi_base,creator,'poi',dest/'upstream',args.offline) poi_base=base_xml(args.poi_base,creator,'poi',dest/'upstream',args.offline)
rendering_base=base_xml(args.rendering_base,creator,'rendering',dest/'upstream',args.offline) rendering_base=base_xml(args.rendering_base,creator,'rendering',dest/'upstream',args.offline)
poi_cfg,render_cfg,summary=make_configs(poi_base,rendering_base,rows,folder,args.threshold,args.frequent_zoom,args.rare_zoom) route_styles=read_route_styles(args.trail_styles)
style,nicons=make_style(args.icons_config,folder,rows) poi_cfg,render_cfg,summary=make_configs(poi_base,rendering_base,rows,folder,args.threshold,args.frequent_zoom,args.rare_zoom,route_styles)
print('Indexed POI types:',summary['catalog_types'],'frequent:',summary['types_at_or_above_threshold'],'custom style icons:',nicons,flush=True) style,nicons,nlines=make_style(args.icons_config,folder,rows,route_styles)
print('Indexed POI types:',summary['catalog_types'],'frequent:',summary['types_at_or_above_threshold'],'custom icons:',nicons,'colored route types:',nlines,flush=True)
if args.prepare_only: if args.prepare_only:
print('PREPARED (no OBF generated). Files at:',dest) print('PREPARED (no OBF generated). Files at:',dest)
return 0 return 0
@@ -299,7 +333,7 @@ def main():
'created_at_utc':dt.datetime.now(dt.timezone.utc).isoformat(), 'created_at_utc':dt.datetime.now(dt.timezone.utc).isoformat(),
'obf':str(built),'obf_bytes':built.stat().st_size, 'obf':str(built),'obf_bytes':built.stat().st_size,
'input_osm':str(osm),'poi_types':str(poi_cfg),'rendering_types':str(render_cfg), 'input_osm':str(osm),'poi_types':str(poi_cfg),'rendering_types':str(render_cfg),
'custom_style':str(style), 'custom_style_icon_rules':nicons, 'custom_style':str(style), 'custom_style_icon_rules':nicons, 'custom_route_rules':nlines,
'summary':summary 'summary':summary
},ensure_ascii=False,indent=2)+'\n',encoding='utf-8') },ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
return 0 return 0
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env python3
"""v0.93: streaming IMG/OSM native-OsmAnd translation and honest QA.
Only user-editable controls: config/poi-mapping.csv and config/line-mapping.csv.
"""
import argparse,base64,collections,csv,hashlib,json,pathlib,re,sqlite3,sys,xml.etree.ElementTree as ET
from xml.sax.saxutils import escape,quoteattr
def top_level_elements(source):
"""Bounded-memory XML pull iterator. ElementTree .clear() alone leaves one
empty Element per processed object in the root; clearing the root prevents
nationwide Garmin files from exhausting RAM during multi-pass conversion.
Callers must fully process each element before requesting the next.
"""
context=ET.iterparse(source,events=('start','end'))
event, root=next(context)
if event != 'start' or root.tag != 'osm':
raise ValueError('Expected an OSM XML root element')
depth=1
for event,elem in context:
if event=='start':depth+=1;continue
if depth==2 and elem.tag in ('node','way','relation'):
yield elem
root.clear()
depth-=1
def tagdict(elem):return {x.get('k'):x.get('v','') for x in elem.findall('tag')}
def set_tags(elem,d):
for child in list(elem):
if child.tag=='tag':elem.remove(child)
for k,v in d.items():
if v is not None and v!='':ET.SubElement(elem,'tag',k=k,v=str(v))
def read_csv(p):
with open(p,encoding='utf-8-sig',newline='') as f:
lines=(line for line in f if not line.startswith('#'))
yield from csv.DictReader(lines)
def code(value):return hex(int(value,0)).lower()
def mapping(path):
ret={}
for row in read_csv(path):
try:c=code(row['code'])
except Exception as ex:raise ValueError('Invalid code in '+str(path)+': '+str(row)) from ex
if c in ret:raise ValueError(f'Duplicate code {c} in {path}')
ret[c]=row
return ret
def tags_from_string(v):
d={}
for x in v.split(';'):
if not x:continue
if '=' not in x:raise ValueError(f'Invalid OSM tag {x!r}')
k,val=x.split('=',1)
if not k or not val:raise ValueError(f'Invalid OSM tag {x!r}')
if k in ('name','garmin:type','garmin:feature','garmin:mapset','bgm:generated_name'):
raise ValueError('Reserved mapping key: '+k)
d[k]=val
return d
# User-requested PRIVATE-map presentation fallback. A locality is a safe stock-map
# display tag, but this is synthetically applied to unknown source types and is
# therefore NOT suitable for public OSM imports. Elevation annotations are excluded.
SEMANTIC_KEYS = frozenset(('amenity','tourism','emergency','natural','man_made','place',
'historic','shop','leisure','building','waterway','railway','power','aeroway',
'highway','hazard','ford','water','water_source','landuse','barrier',
'aerialway','craft','military','geological','public_transport','sport'))
GENERATED_SUFFIX = re.compile(r' - ([A-Z0-9]{7,16})$')
def stable_unknown_hash(elem,tags):
name=tags.get('name','')
m=GENERATED_SUFFIX.search(name) if tags.get('bgm:generated_name') == 'yes' else None
if m:return m.group(1) # preserve previously generated stable identity
seed='|'.join((tags.get('garmin:mapset',''),tags.get('garmin:type',''),
elem.get('lat',''),elem.get('lon','')))
return base64.b32encode(hashlib.blake2s(seed.encode('utf-8'),digest_size=7).digest()).decode().rstrip('=')[:11]
def add_native_unknown_fallback(elem,tags,rule):
"""Only unclassified POIs. Named POIs retain their genuine source names."""
if any(k in tags for k in SEMANTIC_KEYS):return False
if tags.get('garmin:type') == '0x6600' or 'ele' in tags:
return False # spot heights and elevation annotations are not unknown places
if rule and rule.get('action','').strip().lower()=='drop':return False
tags['place']='locality'
tags['bgm:synthetic_fallback']='place_locality'
tags['bgm:classification']='unverified'
name=tags.get('name','').strip()
if not name or tags.get('bgm:generated_name')=='yes' or name.startswith('Garmin обект '):
tags['name']='Неуточнено - '+stable_unknown_hash(elem,tags)
tags['bgm:generated_name']='yes'
return True
def make_cpp_rules(pois,out,labels):
"""C++ v0.9 uses these internal stage files; they are NOT user control files."""
with open(out,'w',encoding='utf-8',newline='') as f,open(labels,'w',encoding='utf-8',newline='') as g:
w=csv.writer(f);w.writerow(['code','name_contains','mode','tags','note'])
l=csv.writer(g);l.writerow(['code','label_bg'])
for c,row in sorted(pois.items(),key=lambda t:int(t[0],0)):
action=row.get('action','replace').strip().lower()
if action not in ('replace','append','raw','drop'):raise ValueError('Invalid action for '+c)
vals=tags_from_string(row.get('osm_tags',''))
if action=='raw':action='replace';vals={}
if action=='drop':vals={}
w.writerow([c,'',action,';'.join(k+'='+v for k,v in vals.items()),'v0.92 compiled from poi-mapping.csv'])
if row.get('label_bg'):l.writerow([c,row['label_bg']])
def inspect_counts(osm,pois,lines,out,strict=False):
seen=collections.Counter();unresolved=collections.Counter();annotations=collections.Counter();fallback=collections.Counter();bad=collections.Counter();routes=collections.Counter();examples={};category=collections.Counter();n=0
with open(osm,'rb') as src:
for el in top_level_elements(src):
if el.tag=='node':
t=tagdict(el)
if t.get('garmin:feature')=='poi':
n+=1;c=t.get('garmin:type','');seen[c]+=1
if c not in pois:bad[c]+=1
if t.get('bgm:synthetic_fallback')=='place_locality':fallback[c]+=1
if c=='0x6600' and 'ele' in t:
annotations['spot_height']+=1
elif not any(k in t for k in ('amenity','tourism','emergency','natural','man_made','place','historic','shop','leisure','building','waterway','railway','power','aeroway','highway','ele','hazard','ford','water','water_source','landuse','barrier','aerialway','craft','geological')):
unresolved[c]+=1
if len(examples.get(c,[]))<5:examples.setdefault(c,[]).append([el.get('lat'),el.get('lon'),t.get('name','')])
for k in ('amenity','tourism','emergency','natural','man_made','building','historic','shop','leisure'):
if k in t:category[f'{k}={t[k]}']+=1
elif el.tag=='relation':
t=tagdict(el)
if t.get('route')=='hiking':routes[t.get('garmin:type','unknown')]+=1
el.clear()
summary={'status':'FAIL' if strict and (bad or unresolved) else ('WARN' if bad or unresolved else 'PASS'),
'total_pois':n,'mapped_types':len(pois),'observed_types':len(seen),
'source_codes_not_in_config':dict(bad),'unclassified_counts':dict(unresolved),
'unclassified_total':sum(unresolved.values()),'synthetic_locality_fallback_total':sum(fallback.values()),'synthetic_locality_counts':dict(fallback),'annotations_not_missing_pois':dict(annotations),'route_relation_counts':dict(routes),
'native_semantic_categories':dict(category.most_common(100)),
'sample_coordinates':examples,
'warning':'OSM audit only; stock icons/search not guaranteed. Synthetic localities are private-map fallback, not valid public OSM classification. Elevation-only objects deliberately have no artificial POI icon.'}
pathlib.Path(out).write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
return summary
def transform(source,dest,pois,lines,report,strict=False):
"""Streaming write; route metadata held in SQLite so nationwide builds do not need RAM for all nodes/ways.
Native route relations are built only from selected Garmin colored overlay ways,
NOT from invented highway=path edges.
"""
dbpath=pathlib.Path(dest).with_suffix('.edges.sqlite')
if dbpath.exists():raise FileExistsError(dbpath)
db=sqlite3.connect(dbpath)
db.execute('PRAGMA synchronous=OFF');db.execute('PRAGMA journal_mode=MEMORY')
db.execute('CREATE TABLE edges (code TEXT, way INTEGER, a INTEGER, b INTEGER)')
poi_count=0; ways=0; candidate_edges=0; emitted=0; synthetic=0; kinds=collections.Counter();seen=collections.Counter();gaps=collections.Counter()
with open(source,'rb') as inp,open(dest,'w',encoding='utf-8') as out:
out.write('<?xml version="1.0" encoding="UTF-8"?>\n<osm version="0.6" generator="garmin2osm-v0.93" upload="never">\n')
for el in top_level_elements(inp):
if el.tag=='node':
tags=tagdict(el)
if tags.get('garmin:feature')=='poi':
poi_count+=1;c=tags.get('garmin:type','')
if c in pois:
row=pois[c];tags['bgm:legend_bg']=row.get('legend_bg','') or row.get('label_bg','')
tags['bgm:priority']=row.get('priority','normal')
# only provenance; no false POI semantic category inserted here
tags.pop('bgm_poi',None);tags.pop('bgm:synthetic_icon',None)
# Unclassified Garmin POIs intentionally remain raw: stock OsmAnd
# cannot visually draw a unique icon for each without installed resources.
else:gaps[c]+=1;tags.pop('bgm_poi',None)
if add_native_unknown_fallback(el,tags,pois.get(c)):
synthetic+=1
set_tags(el,tags);seen[c]+=1
out.write(ET.tostring(el,encoding='unicode')+'\n');emitted+=1
elif el.tag=='way':
ways+=1;tags=tagdict(el)
if tags.get('garmin:feature')=='extended_polyline' or 'bgm_route' in tags:
c=tags.get('garmin:type','')
r=lines.get(c)
tags.pop('bgm_route',None)
# Raw geometry and colour may still be needed for diagnosis;
# no duplicate road geometry or misleading water/terrain categories.
if r:
tags['bgm:line_legend']=r.get('legend_bg','')
tags['bgm:translation_status']=r.get('route_confidence','unknown')
tags.update(tags_from_string(r.get('osm_way_tags','')))
if r.get('route_mode','').lower()=='relation':
nref=[int(x.attrib['ref']) for x in el.findall('nd')]
if len(nref)>=2:
db.execute('INSERT INTO edges VALUES (?,?,?,?)',(c,int(el.get('id')),nref[0],nref[-1]));candidate_edges+=1
else:gaps[c]+=1
set_tags(el,tags)
out.write(ET.tostring(el,encoding='unicode')+'\n');emitted+=1
elif el.tag=='relation':
out.write(ET.tostring(el,encoding='unicode')+'\n');emitted+=1
el.clear()
db.commit()
# Use topological unions on endpoint node refs to avoid one nationwide route per color.
grouped=collections.defaultdict(list)
for c,w,a,b in db.execute('SELECT code,way,a,b FROM edges ORDER BY code,way'):
grouped[c].append((w,a,b))
relations=0;components=0;largest=0
for c,edges in sorted(grouped.items()):
parent=list(range(len(edges)));rank=[0]*len(edges)
def root(i):
while parent[i]!=i:
parent[i]=parent[parent[i]];i=parent[i]
return i
def union(a,b):
ra,rb=root(a),root(b)
if ra==rb:return
if rank[ra]<rank[rb]:ra,rb=rb,ra
parent[rb]=ra
if rank[ra]==rank[rb]:rank[ra]+=1
endpoint={}
for i,(w,a,b) in enumerate(edges):
for n in (a,b):
if n in endpoint:union(i,endpoint[n])
else:endpoint[n]=i
comps=collections.defaultdict(list)
for i,(w,a,b) in enumerate(edges):comps[root(i)].append(w)
for members in comps.values():
components+=1;largest=max(largest,len(members));relations+=1
# Deterministic, negative IDs because prior C++ IDs are positive
rel=ET.Element('relation',id=str(-relations),version='1')
for wid in sorted(members):ET.SubElement(rel,'member',type='way',ref=str(wid),role='')
row=lines[c]
tags={'type':'route','route':'hiking','network':row.get('network') or 'lwn',
'garmin:type':c,'bgm:source_feature':'extended_polyline',
'bgm:confidence':row.get('route_confidence','color_only')}
if row.get('osmc_symbol'):tags['osmc:symbol']=row['osmc_symbol']
if row.get('legacy_color'):tags['colour']=row['legacy_color']
# Use a non-confusing internal identifier, NOT a fabricated official route name.
tags['ref']='BG'+c.replace('0x','').upper()
set_tags(rel,tags)
out.write(ET.tostring(rel,encoding='unicode')+'\n')
out.write('</osm>\n')
db.close();dbpath.unlink(missing_ok=True)
result={'pois':poi_count,'synthetic_locality_fallbacks':synthetic,'ways':ways,'route_candidate_ways':candidate_edges,
'route_relations':relations,'route_components':components,'largest_component_ways':largest,
'source_types_missing_mapping':dict(gaps),'notes':['Synthetic place=locality on unknown features is PRIVATE-map-only' ,'Native OSM route relations; no custom phone-side stylesheet','Display zoom remains controlled by stock OsmAnd, not config zoom_hint','Route symbol derived from Garmin color; color_only mappings require review']}
pathlib.Path(report).write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
return result
def template(catalog,poi_file,line_file,osm=None):
"""Future map: source-driven, abstains from guessing semantic identities."""
if pathlib.Path(poi_file).exists() or pathlib.Path(line_file).exists():raise FileExistsError('Refusing to overwrite mapping templates')
pr=[]
for row in read_csv(catalog):
pr.append({'code':row['code'],'source_count':row['count'],'legend_bg':'','legend_en':'',
'meaning_confidence':'unknown','osm_tags':'','label_bg':f'Garmin обект {row["code"]}',
'priority':'normal','zoom_hint':'17','action':'raw',
'note':f'SAMPLE {row.get("example1_lat","")}, {row.get("example1_lon","")}, {row.get("example1_name","")}'})
lc=collections.Counter()
if osm:
with open(osm,'rb') as inp:
for el in top_level_elements(inp):
if el.tag=='way':
t=tagdict(el)
if t.get('garmin:feature')=='extended_polyline':lc[t.get('garmin:type','')]+=1
el.clear()
lr=[{'code':k,'source_count':v,'legend_bg':'','legacy_color':'', 'osm_way_tags':'',
'route_mode':'preserve','osmc_symbol':'','network':'lwn','route_confidence':'unknown',
'priority':'normal','note':'Unmapped type: check Garmin legend and geometry before enabling'} for k,v in sorted(lc.items())]
with open(poi_file,'w',encoding='utf-8',newline='') as f:
w=csv.DictWriter(f,fieldnames=list(pr[0]) if pr else ['code','source_count','legend_bg','legend_en','meaning_confidence','osm_tags','label_bg','priority','zoom_hint','action','note']);w.writeheader();w.writerows(pr)
with open(line_file,'w',encoding='utf-8',newline='') as f:
w=csv.DictWriter(f,fieldnames=list(lr[0]) if lr else ['code','source_count','legend_bg','legacy_color','osm_way_tags','route_mode','osmc_symbol','network','route_confidence','priority','note']);w.writeheader();w.writerows(lr)
return {'poi_types':len(pr),'line_types':len(lr)}
def main():
p=argparse.ArgumentParser(description=__doc__);sub=p.add_subparsers(dest='mode',required=True)
for cmd in ['prepare','transform','audit','template']:
s=sub.add_parser(cmd);s.add_argument('--poi',required=True);s.add_argument('--lines',required=True)
if cmd=='prepare':s.add_argument('--out',required=True)
elif cmd=='transform':s.add_argument('--osm',required=True);s.add_argument('--out',required=True);s.add_argument('--report',required=True)
elif cmd=='audit':s.add_argument('--osm',required=True);s.add_argument('--out',required=True);s.add_argument('--strict',action='store_true')
else:s.add_argument('--catalog',required=True);s.add_argument('--osm')
a=p.parse_args()
if a.mode=='template':res=template(a.catalog,a.poi,a.lines,a.osm)
else:
ps=mapping(a.poi);ls=mapping(a.lines)
if a.mode=='prepare':
out=pathlib.Path(a.out);out.mkdir(parents=True,exist_ok=True)
make_cpp_rules(ps,out/'cpp-poi-override.csv',out/'cpp-name-labels.csv');res={'poi_types':len(ps),'line_types':len(ls)}
elif a.mode=='transform':res=transform(a.osm,a.out,ps,ls,a.report)
else:res=inspect_counts(a.osm,ps,ls,a.out,a.strict)
print(json.dumps(res,ensure_ascii=False,indent=2));return 0
if __name__=='__main__':sys.exit(main())
+11 -6
View File
@@ -23,9 +23,13 @@ Options parse(int argc,char**argv){Options o;for(int i=1;i<argc;i++){std::string
if(o.input.empty())fail("--input required");if(o.output.empty()&&!o.inspect&&o.poiTypesOut.empty()&&o.poiManifest.empty()&&o.iconsDir.empty())fail("--output, --inspect or an inventory output required");if(!o.poiAdditions.empty()&&o.output.empty())fail("--poi-additions requires --output");if(o.jobs<1||o.jobs>64)fail("--jobs must be 1..64");if(o.examples<1||o.examples>20)fail("--poi-examples must be 1..20");if(o.clip&&(!o.bbox.valid()||o.bbox.w< -180||o.bbox.e>180||o.bbox.s< -90||o.bbox.n>90))fail("Invalid bbox");return o;} if(o.input.empty())fail("--input required");if(o.output.empty()&&!o.inspect&&o.poiTypesOut.empty()&&o.poiManifest.empty()&&o.iconsDir.empty())fail("--output, --inspect or an inventory output required");if(!o.poiAdditions.empty()&&o.output.empty())fail("--poi-additions requires --output");if(o.jobs<1||o.jobs>64)fail("--jobs must be 1..64");if(o.examples<1||o.examples>20)fail("--poi-examples must be 1..20");if(o.clip&&(!o.bbox.valid()||o.bbox.w< -180||o.bbox.e>180||o.bbox.s< -90||o.bbox.n>90))fail("Invalid bbox");return o;}
Tags lineTags(int t,bool ext,const std::string&name){Tags v;tag(v,"garmin:type",code(t));tag(v,"garmin:feature",ext?"extended_polyline":"polyline");tag(v,"name",name); Tags lineTags(int t,bool ext,const std::string&name){Tags v;tag(v,"garmin:type",code(t));tag(v,"garmin:feature",ext?"extended_polyline":"polyline");tag(v,"name",name);
if(ext){ // Physical trail identity not proven: preserve Garmin overlay, mark it as a hiking path geometry. if(ext){ // TYP route overlay is graphical, NOT independently verified routable geometry.
tag(v,"highway","path");tag(v,"foot","yes");tag(v,"trailblazed","yes");tag(v,"garmin:route_overlay","yes"); tag(v,"bgm_route","type_"+code(t));tag(v,"garmin:route_overlay","yes");
const auto it=gStyles.lines.find(t);if(it!=gStyles.lines.end()&&it->second.color)tag(v,"colour",hexColor(it->second.color));return v; const auto it=gStyles.lines.find(t);if(it!=gStyles.lines.end()){
if(it->second.color)tag(v,"colour",hexColor(it->second.color));
if(it->second.hasPattern())tag(v,"bgm:line_pattern","garmin_bitmap");
}
return v;
} }
if(t<=0x0c){const char*klass=nullptr; if(t<=0x0c){const char*klass=nullptr;
switch(t){case 0:klass="road";break;case 1:klass="motorway";break;case 2:klass="primary";break;case 3:klass="secondary";break;case 4:klass="tertiary";break;case 5:klass="unclassified";break;case 6:klass="residential";break;case 7:klass="service";break;case 8:case 9:case 11:klass="motorway_link";break;case 10:klass="track";break;case 12:klass="residential";break;} switch(t){case 0:klass="road";break;case 1:klass="motorway";break;case 2:klass="primary";break;case 3:klass="secondary";break;case 4:klass="tertiary";break;case 5:klass="unclassified";break;case 6:klass="residential";break;case 7:klass="service";break;case 8:case 9:case 11:klass="motorway_link";break;case 10:klass="track";break;case 12:klass="residential";break;}
@@ -44,7 +48,7 @@ Tags areaTags(int t,const std::string&name){Tags v;tag(v,"garmin:type",code(t));
case 0x05:tag(v,"amenity","parking");break;case 0x07:tag(v,"aeroway","aerodrome");break; case 0x05:tag(v,"amenity","parking");break;case 0x07:tag(v,"aeroway","aerodrome");break;
case 0x0b:tag(v,"amenity","hospital");break;case 0x0c:tag(v,"landuse","industrial");break; case 0x0b:tag(v,"amenity","hospital");break;case 0x0c:tag(v,"landuse","industrial");break;
case 0x13:tag(v,"building","yes");break;case 0x17:tag(v,"leisure","park");break; case 0x13:tag(v,"building","yes");break;case 0x17:tag(v,"leisure","park");break;
case 0x18:tag(v,"leisure","golf_course");break;case 0x09:tag(v,"leisure","marina");break; case 0x18:tag(v,"leisure","sports_centre");break;case 0x09:tag(v,"landuse","retail");break;
case 0x14:case 0x15:case 0x16:tag(v,"boundary","national_park");break; case 0x14:case 0x15:case 0x16:tag(v,"boundary","national_park");break;
case 0x28:case 0x32:tag(v,"natural","water");tag(v,"water","sea");break; case 0x28:case 0x32:tag(v,"natural","water");tag(v,"water","sea");break;
case 0x3c:case 0x3d:case 0x40:case 0x41:tag(v,"natural","water");break; case 0x3c:case 0x3d:case 0x40:case 0x41:tag(v,"natural","water");break;
@@ -69,13 +73,14 @@ Tags poiTags(int t,const std::string&name){Tags v;tag(v,"garmin:type",code(t));t
else if(t==0x6407){tag(v,"tourism","picnic_site");} else if(t==0x6407){tag(v,"tourism","picnic_site");}
else if(t==0x640d){tag(v,"aerialway","station");} else if(t==0x640d){tag(v,"aerialway","station");}
else if(t==0x6404||t==0x640b||t==0x641a||t==0x2c0b){tag(v,"amenity","place_of_worship");} else if(t==0x6404||t==0x640b||t==0x641a||t==0x2c0b){tag(v,"amenity","place_of_worship");}
else if(t==0x6602||t==0x6608){tag(v,"tourism","viewpoint");} else if(t==0x6602){tag(v,"tourism","viewpoint");}
else if(t==0x6608){tag(v,"tourism","hotel");}
else if(t==0x2800){tag(v,"place","locality");} else if(t==0x2800){tag(v,"place","locality");}
else if(t==0x6600){ // Geographic spot heights; don't misclassify elevations as villages. else if(t==0x6600){ // Geographic spot heights; don't misclassify elevations as villages.
try{size_t parsed=0;int n=std::stoi(name,&parsed);if(parsed==name.size()&&n>=0&&n<=8848)tag(v,"ele",std::to_string(n));}catch(...){} try{size_t parsed=0;int n=std::stoi(name,&parsed);if(parsed==name.size()&&n>=0&&n<=8848)tag(v,"ele",std::to_string(n));}catch(...){}
} }
else if(t==0x6614){tag(v,"garmin:marker","distance_or_index");} else if(t==0x6614){tag(v,"garmin:marker","distance_or_index");}
else if(t==0x6500){tag(v,"man_made","water_works");} else if(t==0x6500){tag(v,"man_made","water_intake");}
else if(t==0x6402&&name.find("Посетителски")!=std::string::npos){tag(v,"tourism","information");tag(v,"information","office");} else if(t==0x6402&&name.find("Посетителски")!=std::string::npos){tag(v,"tourism","information");tag(v,"information","office");}
else if(t==0x6411){tag(v,"man_made","tower");} else if(t==0x6411){tag(v,"man_made","tower");}
// 0x6414 handled above as drinking_water: conflicting old water_well branch removed. // 0x6414 handled above as drinking_water: conflicting old water_well branch removed.
+3 -3
View File
@@ -14,13 +14,13 @@ class TestLocalDisplay(unittest.TestCase):
c=x['code'];self.assertEqual(r[c]['mode'],'replace',c) c=x['code'];self.assertEqual(r[c]['mode'],'replace',c)
self.assertTrue(names[c],c) self.assertTrue(names[c],c)
t=r[c]['tags'] t=r[c]['tags']
self.assertTrue(any(a+'=' in t for a in ['amenity','shop','tourism','natural','man_made','leisure','historic','place','building','emergency','power','highway','waterway','sport','industrial']),c) self.assertTrue(t,c) # BGMountains custom POI index handles non-standard types
self.assertNotIn('tags,,',t) self.assertNotIn('tags,,',t)
def test_key_places(self): def test_key_places(self):
r={x['code']:x for x in load('config/poi-overrides-all-types.csv')} r={x['code']:x for x in load('config/poi-overrides-all-types.csv')}
self.assertIn('man_made=water_intake',r['0x6500']['tags']) self.assertIn('man_made=water_intake',r['0x6500']['tags'])
self.assertIn('bgm:synthetic_icon=yes',r['0x6500']['tags']) self.assertNotIn('bgm:synthetic_icon=yes',r['0x6500']['tags'])
self.assertIn('tourism=information',r['0x6500']['tags']) self.assertNotIn('tourism=information',r['0x6500']['tags'])
self.assertIn('tourism=viewpoint',r['0x6602']['tags']) self.assertIn('tourism=viewpoint',r['0x6602']['tags'])
self.assertNotIn('bgm:synthetic_icon',r['0x6602']['tags']) self.assertNotIn('bgm:synthetic_icon',r['0x6602']['tags'])
def test_all_legacy_codes_retained_for_review(self): def test_all_legacy_codes_retained_for_review(self):
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""v0.92 offline tests: mapping discipline, native routes, names, future-map templates."""
import csv,importlib.util,json,pathlib,subprocess,sys,tempfile,unittest,xml.etree.ElementTree as ET
ROOT=pathlib.Path(__file__).resolve().parents[1]
spec=importlib.util.spec_from_file_location('translator',ROOT/'scripts/native_translate.py')
t=importlib.util.module_from_spec(spec);spec.loader.exec_module(t)
POI=ROOT/'config/poi-mapping.csv';LINES=ROOT/'config/line-mapping.csv'
class Native092(unittest.TestCase):
def test_native_source_semantics(self):
rows=t.mapping(POI)
self.assertGreaterEqual(len(rows),263)
for c,tags in [('0x6414','amenity=drinking_water'),('0x6511','natural=spring'),('0x6500','man_made=water_intake'),('0x660a','amenity=shelter'),('0x6609','tourism=alpine_hut'),('0x6401','man_made=bridge')]:
self.assertIn(tags,rows[c]['osm_tags'])
self.assertEqual(rows['0x6402']['label_bg'],'Заслон - Сграда')
self.assertIn(rows['0x6600']['action'],('append','raw'))
self.assertFalse(any('leisure=marina' in r['osm_tags'] for r in rows.values()))
self.assertFalse(any('tourism=information;information=board' in r['osm_tags'] for r in rows.values()))
self.assertTrue(all('garmin' not in r['osm_tags'] for r in rows.values()))
self.assertTrue(all(int(rows[k]['zoom_hint'])<=12 for k in ('0x6414','0x6511','0x6500','0x660a')))
def test_config_only_two_and_types(self):
self.assertEqual(set(p.name for p in (ROOT/'config').glob('*.csv')),{'poi-mapping.csv','line-mapping.csv'})
self.assertTrue(any(x.get('route_mode')=='relation' for x in t.mapping(LINES).values()))
for line in t.mapping(LINES).values():
if line['route_mode']=='relation':self.assertTrue(line['osmc_symbol'])
def test_native_relation_members_and_poi_provenance(self):
with tempfile.TemporaryDirectory() as directory:
d=pathlib.Path(directory);src=d/'sample.osm';dst=d/'native.osm';report=d/'report.json'
line=next(r for r in t.mapping(LINES).values() if r['route_mode']=='relation')
typ=line['code']
xml=['<?xml version="1.0" encoding="UTF-8"?><osm version="0.6">']
for i in range(1,5):xml.append(f'<node id="{i}" lat="41.{i}" lon="23.{i}"/>')
xml+=['<node id="100" lat="41.7" lon="23.4"><tag k="garmin:type" v="0x6414"/><tag k="garmin:feature" v="poi"/><tag k="name" v="Вода - Чешма - HASH"/><tag k="amenity" v="drinking_water"/><tag k="bgm_poi" v="type_0x6414"/></node>']
for i,refs in [(10,[1,2]),(11,[2,3])]:
xml.append(f'<way id="{i}">'+''.join(f'<nd ref="{n}"/>' for n in refs)+f'<tag k="garmin:feature" v="extended_polyline"/><tag k="garmin:type" v="{typ}"/><tag k="bgm_route" v="type_{typ}"/></way>')
xml.append('</osm>');src.write_text('\n'.join(xml))
result=t.transform(src,dst,t.mapping(POI),t.mapping(LINES),report)
self.assertEqual(result['route_candidate_ways'],2);self.assertEqual(result['route_relations'],1)
root=ET.parse(dst).getroot();rel=root.find('relation');tags=t.tagdict(rel)
self.assertEqual(tags['route'],'hiking');self.assertTrue(tags['osmc:symbol'].endswith('_bar'))
self.assertEqual(sorted(m.attrib['ref'] for m in rel.findall('member')),['10','11'])
node=next(n for n in root.findall('node') if n.get('id')=='100');p=t.tagdict(node)
self.assertEqual(p['amenity'],'drinking_water');self.assertEqual(p['name'],'Вода - Чешма - HASH')
self.assertEqual(p['garmin:type'],'0x6414');self.assertNotIn('bgm_poi',p)
for way in root.findall('way'):self.assertNotIn('highway',t.tagdict(way))
def test_route_attachment_to_real_way_only(self):
with tempfile.TemporaryDirectory() as directory:
d=pathlib.Path(directory);src=d/'src.osm';dst=d/'out.osm';rep=d/'report.json'
typ=next(row['code'] for row in t.mapping(LINES).values() if row['route_mode']=='relation')
parts=['<osm version="0.6">','<node id="1" lat="41" lon="23"/>','<node id="2" lat="41.1" lon="23.1"/>',
'<node id="3" lat="41.2" lon="23.2"/>','<way id="22"><nd ref="1"/><nd ref="2"/><nd ref="3"/><tag k="highway" v="path"/></way>',
'<way id="33"><nd ref="1"/><nd ref="2"/><nd ref="3"/><tag k="garmin:type" v="'+typ+'"/><tag k="garmin:feature" v="extended_polyline"/></way>',
'<relation id="-1"><member type="way" ref="33" role=""/><tag k="route" v="hiking"/><tag k="bgm:source_feature" v="extended_polyline"/></relation></osm>']
src.write_text('\n'.join(parts))
subprocess.run([sys.executable,str(ROOT/'scripts/attach_native_routes.py'),'--osm',src,'--out',dst,'--lines',LINES,'--report',rep],check=True,capture_output=True)
report=json.loads(rep.read_text());self.assertEqual(report['attached_to_existing_highway'],1)
rel=ET.parse(dst).getroot().find('relation');self.assertEqual(rel.find('member').get('ref'),'22')
def test_template_for_new_map_without_false_defaults(self):
with tempfile.TemporaryDirectory() as directory:
d=pathlib.Path(directory)
p=d/'inventory.csv';p.write_text('code,count,example1_lat,example1_lon,example1_name\n0x999,2,42.123,24.321,Unknown place\n')
xml=d/'one.osm';xml.write_text('<osm><node id="1" lat="42" lon="24"/><node id="2" lat="43" lon="25"/><way id="3"><nd ref="1"/><nd ref="2"/><tag k="garmin:feature" v="extended_polyline"/><tag k="garmin:type" v="0x11009"/></way></osm>')
a=d/'newpoi.csv';b=d/'newlines.csv';stats=t.template(p,a,b,xml)
self.assertEqual(stats,{'poi_types':1,'line_types':1})
q=list(t.read_csv(a))[0];self.assertEqual(q['action'],'raw');self.assertEqual(q['osm_tags'],'');self.assertIn('42.123',q['note'])
r=list(t.read_csv(b))[0];self.assertEqual(r['route_mode'],'preserve');self.assertEqual(r['osmc_symbol'],'')
if __name__=='__main__':unittest.main()
+59
View File
@@ -0,0 +1,59 @@
"""Regression coverage for large XML handling and no false-locality fallbacks."""
import csv,importlib.util,os,pathlib,sys,tempfile,unittest,xml.etree.ElementTree as ET
ROOT=pathlib.Path(__file__).resolve().parents[1]
spec=importlib.util.spec_from_file_location('native_translate', ROOT/'scripts/native_translate.py')
t=importlib.util.module_from_spec(spec);spec.loader.exec_module(t)
class Reliability093(unittest.TestCase):
def test_unknowns_receive_private_locality_and_stable_name(self):
from tempfile import TemporaryDirectory
with TemporaryDirectory() as d:
out=pathlib.Path(d)
src=out/'input.osm';dst=out/'output.osm';report=out/'report.json'
src.write_text('''<osm version="0.6"><node id="5" lat="42.1345678" lon="24.2345678"><tag k="garmin:feature" v="poi"/><tag k="garmin:type" v="0x9999"/><tag k="garmin:mapset" v="1234"/><tag k="bgm:generated_name" v="yes"/><tag k="name" v="Garmin обект 0x9999 - Q3ABCDEFG9"/></node><node id="6" lat="42.2" lon="24.2"><tag k="garmin:feature" v="poi"/><tag k="garmin:type" v="0x6600"/><tag k="ele" v="176"/><tag k="name" v="176"/></node></osm>''')
t.transform(src,dst,t.mapping(ROOT/'config/poi-mapping.csv'),t.mapping(ROOT/'config/line-mapping.csv'),report)
nodes=ET.parse(dst).getroot().findall('node');a=t.tagdict(nodes[0]);b=t.tagdict(nodes[1])
self.assertEqual(a['place'],'locality');self.assertEqual(a['name'],'Неуточнено - Q3ABCDEFG9')
self.assertEqual(a['garmin:type'],'0x9999');self.assertEqual(a['bgm:classification'],'unverified')
self.assertNotIn('place',b);self.assertEqual(b['ele'],'176')
data=t.inspect_counts(dst,t.mapping(ROOT/'config/poi-mapping.csv'),t.mapping(ROOT/'config/line-mapping.csv'),out/'audit.json')
self.assertEqual(data['synthetic_locality_fallback_total'],1)
def test_known_bgm_types_are_not_invented_localities(self):
cfg=t.mapping(ROOT/'config/poi-mapping.csv')
self.assertEqual(cfg['0x6600']['osm_tags'],'')
self.assertNotIn('place=locality',cfg['0x6600']['osm_tags'])
self.assertEqual(cfg['0x650b']['osm_tags'],'tourism=information;information=route_marker;hiking=yes')
self.assertEqual(cfg['0x660b']['osm_tags'],'tourism=information;information=trail_blaze;hiking=yes')
def test_fallback_does_not_overwrite_known_native_types(self):
with tempfile.TemporaryDirectory() as d:
src=pathlib.Path(d)/'before.osm';out=pathlib.Path(d)/'after.osm';rep=pathlib.Path(d)/'rep.json'
src.write_text('''<osm><node id="1" lat="42" lon="24"><tag k="garmin:feature" v="poi"/><tag k="garmin:type" v="0x6400"/><tag k="landuse" v="quarry"/><tag k="name" v="Кариера П"/></node><node id="2" lat="42" lon="24"><tag k="garmin:feature" v="poi"/><tag k="garmin:type" v="0x6406"/><tag k="barrier" v="gate"/></node><node id="3" lat="42" lon="24"><tag k="garmin:feature" v="poi"/><tag k="garmin:type" v="0x640d"/><tag k="aerialway" v="station"/></node></osm>''')
t.transform(src,out,t.mapping(ROOT/'config/poi-mapping.csv'),t.mapping(ROOT/'config/line-mapping.csv'),rep)
nodes=ET.parse(out).getroot().findall('node')
self.assertTrue(all('place' not in t.tagdict(e) for e in nodes))
self.assertEqual(t.tagdict(nodes[0])['name'],'Кариера П')
def test_root_memory_does_not_accumulate(self):
import tracemalloc
with tempfile.TemporaryDirectory() as d:
path=pathlib.Path(d)/'many.osm'
with path.open('w') as f:
f.write('<osm version="0.6">\n')
for i in range(130000):f.write(f'<node id="{i+1}" lat="42.1" lon="24.2"><tag k="x" v="y"/></node>\n')
f.write('</osm>')
tracemalloc.start()
c=0
with open(path,'rb') as f:
for e in t.top_level_elements(f):
c+=1
self.assertEqual(t.tagdict(e)['x'],'y')
_,peak=tracemalloc.get_traced_memory();tracemalloc.stop()
self.assertEqual(c,130000)
self.assertLess(peak,15_000_000, f'Unbounded XML root growth {peak}')
def test_audit_spot_height_and_markers(self):
with tempfile.TemporaryDirectory() as d:
path=pathlib.Path(d)/'sample.osm';report=pathlib.Path(d)/'report.json'
path.write_text('''<osm version="0.6"><node id="1" lat="43" lon="22"><tag k="garmin:feature" v="poi"/><tag k="garmin:type" v="0x6600"/><tag k="ele" v="132"/></node><node id="2" lat="43" lon="22"><tag k="garmin:feature" v="poi"/><tag k="garmin:type" v="0x650b"/><tag k="tourism" v="information"/><tag k="information" v="route_marker"/></node><node id="3" lat="43" lon="22"><tag k="garmin:feature" v="poi"/><tag k="garmin:type" v="0x6502"/><tag k="ford" v="yes"/></node></osm>''')
stats=t.inspect_counts(path,t.mapping(ROOT/'config/poi-mapping.csv'),t.mapping(ROOT/'config/line-mapping.csv'),report)
self.assertEqual(stats['unclassified_total'],0)
self.assertEqual(stats['annotations_not_missing_pois']['spot_height'],1)
if __name__=='__main__':unittest.main()
+8 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Audit whether IMG-derived POIs have both OSM display tags and names. """Audit IMG-derived POIs for custom indexability and truthfully report native semantic tags.
This measures converted OSM, NOT actual OsmAnd screen visibility nor .obf indexing. This measures converted OSM, NOT actual OsmAnd screen visibility nor .obf indexing.
Output inventory always includes Garmin codes and example coordinates. Output inventory always includes Garmin codes and example coordinates.
@@ -23,18 +23,20 @@ def main():
if tags.get('garmin:feature')!='poi':el.clear();continue if tags.get('garmin:feature')!='poi':el.clear();continue
code=tags.get('garmin:type','').lower();counts[code]+=1;overview['pois']+=1 code=tags.get('garmin:type','').lower();counts[code]+=1;overview['pois']+=1
name=tags.get('name','');kind=[f'{k}={tags[k]}' for k in CLASS_KEYS if k in tags] name=tags.get('name','');kind=[f'{k}={tags[k]}' for k in CLASS_KEYS if k in tags]
if not kind:bad.append((code,name,el.get('lat'),el.get('lon'),'no_osm_semantic_tag'));overview['unclassified']+=1 if not kind:overview['needs_custom_style']+=1
else:overview['has_native_semantics']+=1
if not name:overview['nameless']+=1 if not name:overview['nameless']+=1
if tags.get('bgm:generated_name')=='yes':stats[code]['generated_name']+=1;overview['generated_name']+=1 if tags.get('bgm:generated_name')=='yes':stats[code]['generated_name']+=1;overview['generated_name']+=1
if tags.get('bgm:synthetic_icon')=='yes':stats[code]['synthetic_icon']+=1;overview['synthetic_icon']+=1 if tags.get('bgm:synthetic_icon')=='yes':stats[code]['synthetic_icon']+=1;overview['synthetic_icon']+=1
if not tags.get('bgm_poi'):overview['missing_raw_type_index_tag']+=1 if not tags.get('bgm_poi'):overview['missing_raw_type_index_tag']+=1
else:overview['has_bgm_poi_tag']+=1
if len(samples[code])<args.samples:samples[code].append((el.get('lat'),el.get('lon'),name, '|'.join(kind))) if len(samples[code])<args.samples:samples[code].append((el.get('lat'),el.get('lon'),name, '|'.join(kind)))
el.clear() el.clear()
rows=[] rows=[]
for code in sorted(counts,key=lambda x:(-counts[x],x)): for code in sorted(counts,key=lambda x:(-counts[x],x)):
base=catalog.get(code,{}) base=catalog.get(code,{})
row=dict(code=code,osm_poi_count=counts[code],nationwide_type_count=base.get('count',''), row=dict(code=code,osm_poi_count=counts[code],nationwide_type_count=base.get('count',''),
icon_strategy=('synthetic' if stats[code]['synthetic_icon'] else 'native-osm'), icon_strategy=('native+custom' if samples[code][0][3] else 'custom_style_required'),
synthetic_count=stats[code]['synthetic_icon'],generated_name_count=stats[code]['generated_name'], synthetic_count=stats[code]['synthetic_icon'],generated_name_count=stats[code]['generated_name'],
legend=base.get('verified_legend_bg',''),review_status=base.get('review_status',''), legend=base.get('verified_legend_bg',''),review_status=base.get('review_status',''),
example_lat=samples[code][0][0],example_lon=samples[code][0][1], example_lat=samples[code][0][0],example_lon=samples[code][0][1],
@@ -44,11 +46,11 @@ def main():
path=Path(args.csv);path.parent.mkdir(parents=True,exist_ok=True) path=Path(args.csv);path.parent.mkdir(parents=True,exist_ok=True)
with path.open('w',encoding='utf-8',newline='') as f: with path.open('w',encoding='utf-8',newline='') as f:
if rows:w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows) if rows:w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
summary=dict(status='PASS' if not bad and not overview['nameless'] and not overview['missing_raw_type_index_tag'] else 'FAIL', summary=dict(status='PASS' if not overview['nameless'] and not overview['missing_raw_type_index_tag'] else 'FAIL',
counts=dict(overview),catalog_types=len(catalog),exported_types=len(counts), counts=dict(overview),catalog_types=len(catalog),exported_types=len(counts),
note='Audit is for OSM tags only. A real .obf inspector comparison and OsmAnd visual inspection remain required.', note='POIs without standard OSM categories are valid for this PRIVATE map because bgm_poi uses custom index and renderer. They require importing the style; this does not prove OBF persistence or screen visibility.',
errors=bad[:100],missing_types_from_catalog=sorted(set(counts)-set(catalog))) errors=bad[:100],missing_types_from_catalog=sorted(set(counts)-set(catalog)))
Path(args.json).write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n',encoding='utf-8') Path(args.json).write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps(summary,ensure_ascii=False,indent=2)) print(json.dumps(summary,ensure_ascii=False,indent=2))
return 0 if summary['status']=='PASS' and not summary['missing_types_from_catalog'] else 0 return 0 if summary['status']=='PASS' and not summary['missing_types_from_catalog'] else 2
if __name__=='__main__':raise SystemExit(main()) if __name__=='__main__':raise SystemExit(main())