From 70301732f9cf7463cad5fb80c9ec16425fdd2725 Mon Sep 17 00:00:00 2001 From: "S. Kozyr" Date: Fri, 13 Dec 2024 15:14:22 +0200 Subject: [PATCH] Added unit tests and integration tests --- .gitignore | 1 + README.md | 28 ++ integration-tests/drules/.gitkeep | 0 integration-tests/full_drules_gen.py | 73 ++++ tests/__init__.py | 0 tests/assets/case-1-import/colors.mapcss | 6 + tests/assets/case-1-import/import1.mapcss | 1 + tests/assets/case-1-import/import2.mapcss | 1 + tests/assets/case-1-import/main.mapcss | 1 + .../case-2-generate-drules-mini/.gitignore | 7 + .../include/Roads.mapcss | 136 +++++++ .../include/Roads_label.mapcss | 75 ++++ .../include/colors.mapcss | 16 + .../include/priorities_1_BG-by-size.prio.txt | 16 + .../include/priorities_2_BG-top.prio.txt | 18 + .../include/priorities_3_FG.prio.txt | 41 ++ .../include/priorities_4_overlays.prio.txt | 61 +++ .../case-2-generate-drules-mini/main.mapcss | 133 +++++++ .../mapcss-dynamic.txt | 4 + .../mapcss-mapping.csv | 148 +++++++ .../case-2-generate-drules-mini/readme.md | 4 + .../assets/case-3-styles-validation/readme.md | 1 + tests/testCondition.py | 300 +++++++++++++++ tests/testEval.py | 124 ++++++ tests/testLibkomwm.py | 68 ++++ tests/testMapCSS.py | 364 ++++++++++++++++++ tests/testRule.py | 114 ++++++ tests/testStyleChooser.py | 297 ++++++++++++++ 28 files changed, 2038 insertions(+) create mode 100644 integration-tests/drules/.gitkeep create mode 100755 integration-tests/full_drules_gen.py create mode 100644 tests/__init__.py create mode 100644 tests/assets/case-1-import/colors.mapcss create mode 100644 tests/assets/case-1-import/import1.mapcss create mode 100644 tests/assets/case-1-import/import2.mapcss create mode 100644 tests/assets/case-1-import/main.mapcss create mode 100644 tests/assets/case-2-generate-drules-mini/.gitignore create mode 100644 tests/assets/case-2-generate-drules-mini/include/Roads.mapcss create mode 100644 tests/assets/case-2-generate-drules-mini/include/Roads_label.mapcss create mode 100644 tests/assets/case-2-generate-drules-mini/include/colors.mapcss create mode 100644 tests/assets/case-2-generate-drules-mini/include/priorities_1_BG-by-size.prio.txt create mode 100644 tests/assets/case-2-generate-drules-mini/include/priorities_2_BG-top.prio.txt create mode 100644 tests/assets/case-2-generate-drules-mini/include/priorities_3_FG.prio.txt create mode 100644 tests/assets/case-2-generate-drules-mini/include/priorities_4_overlays.prio.txt create mode 100644 tests/assets/case-2-generate-drules-mini/main.mapcss create mode 100644 tests/assets/case-2-generate-drules-mini/mapcss-dynamic.txt create mode 100644 tests/assets/case-2-generate-drules-mini/mapcss-mapping.csv create mode 100644 tests/assets/case-2-generate-drules-mini/readme.md create mode 100644 tests/assets/case-3-styles-validation/readme.md create mode 100644 tests/testCondition.py create mode 100644 tests/testEval.py create mode 100644 tests/testLibkomwm.py create mode 100644 tests/testMapCSS.py create mode 100644 tests/testRule.py create mode 100644 tests/testStyleChooser.py diff --git a/.gitignore b/.gitignore index bf52402..b589e30 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ src/tiles/ *pycache* *swp *bak +/.idea \ No newline at end of file diff --git a/README.md b/README.md index dfc5d17..79382c7 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,31 @@ Python dependencies: ```bash pip3 install -r requirements.txt ``` + +## Running unittests + +To run all unittests execute next command from project root folder: + +```bash +python3 -m unittest discover -s tests +``` + +this will search for all `test*.py` files within `tests` directory +and execute tests from those files. + +## Running integration tests + +File `integration-tests/full_drules_gen.py` is intended to generate drules +files for all 6 themes from main Organic Maps repo. It could be used to understand +which parts of the project are actually used by Organic Maps repo. + +Usage: + +```shell +cd integration-tests +python3 full_drules_gen.py -d ../../../data -o drules --txt +``` + +This command will run generation for styles - default light, default dark, +outdoors light, outdoors dark, vehicle light, vehicle dark and put `*.bin` +and `*.txt` files into 'drules' subfolder. diff --git a/integration-tests/drules/.gitkeep b/integration-tests/drules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/integration-tests/full_drules_gen.py b/integration-tests/full_drules_gen.py new file mode 100755 index 0000000..986a9af --- /dev/null +++ b/integration-tests/full_drules_gen.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import sys +from copy import deepcopy +from optparse import OptionParser +from pathlib import Path +import logging + +# Add `src` directory to the import paths +sys.path.insert(0, str(Path(__file__).parent.parent / 'src')) + +import libkomwm + +FORMAT = '%(asctime)s [%(levelname)s] %(message)s' +logging.basicConfig(format=FORMAT) +log = logging.getLogger('test_drules_gen') +log.setLevel(logging.INFO) + +styles = { + 'default_light': ['styles/default/light/style.mapcss', 'styles/default/include'], + 'default_dark': ['styles/default/dark/style.mapcss', 'styles/default/include'], + 'outdoors_light': ['styles/outdoors/light/style.mapcss', 'styles/outdoors/include'], + 'outdoors_dark': ['styles/outdoors/dark/style.mapcss', 'styles/outdoors/include'], + 'vehicle_light': ['styles/vehicle/light/style.mapcss', 'styles/vehicle/include'], + 'vehicle_dark': ['styles/vehicle/dark/style.mapcss', 'styles/vehicle/include'], +} + + +def full_styles_regenerate(options): + log.info("Start generating styles") + libkomwm.MULTIPROCESSING = False + prio_ranges_orig = deepcopy(libkomwm.prio_ranges) + + for name, (style_path, include_path) in styles.items(): + log.info(f"Generating {name} style ...") + + # Restore initial state + libkomwm.prio_ranges = deepcopy(prio_ranges_orig) + libkomwm.visibilities = {} + + options.filename = options.data + '/' + style_path + options.priorities_path = options.data + '/' + include_path + options.outfile = options.outdir + '/' + name + + # Run generation + libkomwm.komap_mapswithme(options) + log.info(f"Done!") + +def main(): + parser = OptionParser() + parser.add_option("-d", "--data-path", dest="data", + help="path to mapcss-mapping.csv and other files", metavar="PATH") + parser.add_option("-o", "--output-dir", dest="outdir", default="drules", + help="output directory", metavar="DIR") + parser.add_option("-f", "--minzoom", dest="minzoom", default=0, type="int", + help="minimal available zoom level", metavar="ZOOM") + parser.add_option("-t", "--maxzoom", dest="maxzoom", default=20, type="int", + help="maximal available zoom level", metavar="ZOOM") + parser.add_option("-x", "--txt", dest="txt", action="store_true", + help="create a text file for output", default=False) + + (options, args) = parser.parse_args() + + if options.data is None: + parser.error("Please specify base 'data' path.") + + if options.outdir is None: + parser.error("Please specify base output path.") + + full_styles_regenerate(options) + +if __name__ == '__main__': + main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/assets/case-1-import/colors.mapcss b/tests/assets/case-1-import/colors.mapcss new file mode 100644 index 0000000..db1c68b --- /dev/null +++ b/tests/assets/case-1-import/colors.mapcss @@ -0,0 +1,6 @@ +colors { + GuiText-color: #FFFFFF; + GuiText-opacity: 0.7; + Route-color: #0000FF; + Route-opacity: 0.5; +} diff --git a/tests/assets/case-1-import/import1.mapcss b/tests/assets/case-1-import/import1.mapcss new file mode 100644 index 0000000..cfa0b6a --- /dev/null +++ b/tests/assets/case-1-import/import1.mapcss @@ -0,0 +1 @@ +@import("import2.mapcss"); diff --git a/tests/assets/case-1-import/import2.mapcss b/tests/assets/case-1-import/import2.mapcss new file mode 100644 index 0000000..9ab0b8d --- /dev/null +++ b/tests/assets/case-1-import/import2.mapcss @@ -0,0 +1 @@ +@import("colors.mapcss"); diff --git a/tests/assets/case-1-import/main.mapcss b/tests/assets/case-1-import/main.mapcss new file mode 100644 index 0000000..ca997c1 --- /dev/null +++ b/tests/assets/case-1-import/main.mapcss @@ -0,0 +1 @@ +@import("import1.mapcss"); diff --git a/tests/assets/case-2-generate-drules-mini/.gitignore b/tests/assets/case-2-generate-drules-mini/.gitignore new file mode 100644 index 0000000..24a9e5d --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/.gitignore @@ -0,0 +1,7 @@ +classificator.txt +colors.txt +patterns.txt +style.bin.bin +style.bin.txt +types.txt +visibility.txt diff --git a/tests/assets/case-2-generate-drules-mini/include/Roads.mapcss b/tests/assets/case-2-generate-drules-mini/include/Roads.mapcss new file mode 100644 index 0000000..794dd08 --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/include/Roads.mapcss @@ -0,0 +1,136 @@ +/* ~~~~ CONTENT OF ROADS ~~~~~ + +1.Z-INDEX ROADS +2.WORLD LEVEL ROAD 4-9 ZOOM +3.TRUNK & MOTORWAY 6-22 ZOOM + 3.1 Trunk & Motorway 6-22 ZOOM + 3.2 Trunk & Motorway tunnel 12-22 ZOOM + 3.3 Trunk & Motorway bridge 13-22 ZOOM +4.PRIMARY 8-22 ZOOM + 4.1 Primary 8-22 ZOOM + 4.2 Primary tunnel 14-22 ZOOM + 4.3 Primary bridge 14-22 ZOOM +5.SECONDARY 10-22 ZOOM + 5.1 Secondary 10-22 ZOOM + 5.2 Secondary tunnel 16-22 ZOOM + 5.3 Secondary bridge 14-22 ZOOM +6.TERTIARY & UNCLASSIFIED 11-22 ZOOM + 6.1 Tertiary & Unclassified 11-22 ZOOM + 6.2 Tertiary & Unclassified tunnel 16-22 ZOOM + 6.3 Tertiary & Unclassified bridge 14-22 ZOOM +7.RESIDENTAL, ROAD, STREETS & SERVICE 12-22 ZOOM + 7.1 Residential, Road, Street 12-22 ZOOM + 7.2 Residential, Road, Street tunnel 16-22 ZOOM + 7.3 Residential, Road, Street bridge 14-22 ZOOM + 7.4 Service 15-22 ZOOM +8.OTHERS ROADS 13-22 ZOOM + 8.1 Pedestrian & ford 13-22 ZOOM + 8.2 Pedestrian & ford tunnel 16-22 ZOOM + 8.3 Pedestrian & other brige 13-22 ZOOM + 8.4 Cycleway 13-22 ZOOM + 8.5 Construction 13-22 ZOOM + 8.6 Track & Path 14-22 ZOOM + 8.7 Footway 15-22 ZOOM + 8.8 Steps 15-22 ZOOM + 8.9 Bridleway 14-22 ZOOM + 8.11 Runway 12-22 ZOOM +9.RAIL 11-22 ZOOM + 9.1 RAIL 11-22 ZOOM + 9.2 Rail tunnel 14-22 ZOOM + 9.3 Rail bridge 14-22 ZOOM + 9.4 Monorail 14-22 ZOOM + 9.5 Tram line 13-22 ZOOM + 9.6 Funicular 12-22 ZOOM +10.PISTE 12-22 ZOOM + 10.1 Lift 12-22 ZOOM + 10.2 Aerialway 12-22 ZOOM + 10.3 Piste & Route 14-22 ZOOM +11.FERRY +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +/* 2.WORLD LEVEL ROAD 4-9 ZOOM */ + +line|z6-9[highway=world_towns_level], +line|z4-9[highway=world_level], +{color: @trunk1;opacity: 1;} + +line|z4[highway=world_level] +{width: 0.5;} +line|z5-6[highway=world_level], +{width: 0.7;} +line|z6[highway=world_towns_level], +{width: 0.9;} +line|z7[highway=world_towns_level], +line|z7[highway=world_level] +{width: 0.7;} +line|z8[highway=world_towns_level], +line|z8[highway=world_level] +{width: 0.9;} +line|z9[highway=world_towns_level], +line|z9[highway=world_level] +{width: 0.8;} + +/* 3.TRUNK & MOTORWAY 6-22 ZOOM */ + +line|z6[highway=trunk], +line|z6[highway=motorway], +{color: @trunk0; opacity: 0.3;} +line|z7-9[highway=trunk], +line|z7-9[highway=motorway], +{color: @trunk0; opacity: 0.7;} + +line|z10-[highway=trunk], +line|z10-[highway=motorway], +{color: @trunk1; opacity: 0.7;} +line|z10-[highway=motorway_link], +line|z10-[highway=trunk_link], +{color: @primary0; opacity: 0.7;} + +/* 3.1 Trunk & Motorway 6-22 ZOOM */ + +line|z6[highway=trunk], +line|z6[highway=motorway], +{width: 0.8;} +line|z7[highway=trunk], +line|z7[highway=motorway] +{width: 0.9;} +line|z8[highway=trunk], +line|z8[highway=motorway] +{width: 1.1;} +line|z9[highway=trunk], +line|z9[highway=motorway] +{width: 1.2;} +line|z10[highway=trunk], +line|z10[highway=motorway] +{width: 1.5;} + +line|z10[highway=motorway_link], +line|z10[highway=trunk_link] +{width: 0.8;} + + +/* 4.PRIMARY 8-22 ZOOM */ + +line|z8-10[highway=primary], +{color: @primary0; opacity: 0.7;} + +/* 4.1 Primary 8-22 ZOOM */ + +line|z8[highway=primary], +{width: 0.7;} +line|z9[highway=primary], +{width: 0.8;} +line|z10[highway=primary], +{width: 1.2;} + +/* 5.SECONDARY 10-22 ZOOM */ + +line|z10[highway=secondary], +{color: @secondary0; opacity: 0.8;} + +/* 5.1 Secondary 10-22 ZOOM */ + +line|z10[highway=secondary], +{width: 1.2;} + diff --git a/tests/assets/case-2-generate-drules-mini/include/Roads_label.mapcss b/tests/assets/case-2-generate-drules-mini/include/Roads_label.mapcss new file mode 100644 index 0000000..718d47d --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/include/Roads_label.mapcss @@ -0,0 +1,75 @@ +/* ~~~~ CONTENT OF ROADS ~~~~~ + +1.Z-INDEX ROADS +2.SHIELD 10-22 ZOOM +3.TRUNK & MOTORWAY 10-22 ZOOM +4.PRIMARY 10-22 ZOOM +5.SECONDARY 10-22 ZOOM +6.RESIDENTAL & TERTIARY 12-22 ZOOM +7.ROAD, STREETS, UNCLASSIFIED & SERVICE 15-22 ZOOM +8.OTHERS ROADS 15-22 ZOOM +9.RAIL 15-22 ZOOM ???? + 9.1 Monorail 14-22 ZOOM + 9.2 Tram line 13-22 ZOOM + 9.3 Funicular 12-22 ZOOM +10.PISTE 12-22 ZOOM ???? + 10.1 Lift 12-22 ZOOM + 10.2 Aerialway 12-22 ZOOM + 10.3 Piste & Route 14-22 ZOOM +11.FERRY 10-22 ZOOM +12.ONEWAY ARROWS 15-22 ZOOM +13.JUNCTION 15-22 ZOOM +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*/ + +line[highway] +{text-position: line;} + +/* 2.SHIELD 10-22 ZOOM */ + +line|z10-[highway=motorway]::shield, +line|z10-[highway=trunk]::shield, +line|z10-[highway=motorway_link]::shield, +line|z10-[highway=trunk_link]::shield, +line|z10-[highway=primary]::shield, +{shield-font-size: 9;shield-text-color: @shield_text;shield-text-halo-radius: 0;shield-text-halo-color: @shield_text_halo;shield-color: @shield;shield-outline-radius: 1;shield-outline-color: @shield_outline;} + +line|z10[highway=motorway]::shield, +line|z10[highway=trunk]::shield, +line|z10[highway=motorway_link]::shield, +line|z10[highway=trunk_link]::shield, +line|z10[highway=primary]::shield, +{shield-min-distance: 85;} + +/* 3.TRUNK & MOTORWAY 10-22 ZOOM */ + +line|z10-[highway=trunk], +line|z10-[highway=motorway], +line|z10-[highway=motorway_link], +line|z10-[highway=trunk_link], +{text: name; text-halo-radius: 1; text-halo-color: @label_halo_medium;} + +line|z10-[highway=motorway], +line|z10-[highway=trunk], +{font-size: 11; text-color: @label_medium; text-halo-opacity: 0.9;} + +line|z10-[highway=motorway_link], +line|z10-[highway=trunk_link], +{font-size: 10; text-color: @label_medium; text-halo-opacity: 0.7;} + +/* 4.PRIMARY 10-22 ZOOM */ + +line|z10-[highway=primary], +{text: name; text-halo-radius: 1; text-halo-color: @label_halo_medium;} + +line|z10-[highway=primary], +{font-size: 10; text-color: @label_medium; text-halo-opacity: 0.7;} + +/* 5.SECONDARY 10-22 ZOOM */ + +line|z10-[highway=secondary], +{text: name; text-halo-radius: 1; text-halo-color: @label_halo_medium;} + +line|z10-[highway=secondary], +{font-size: 10; text-color: @label_light; text-halo-opacity: 0.7;} + diff --git a/tests/assets/case-2-generate-drules-mini/include/colors.mapcss b/tests/assets/case-2-generate-drules-mini/include/colors.mapcss new file mode 100644 index 0000000..d7b4817 --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/include/colors.mapcss @@ -0,0 +1,16 @@ +/* 5.1 All roads */ +@trunk0: #FF7326; +@trunk1: #FF7A26; +@primary0: #FF8726; +@secondary0: #FFB226; + +/* 6.1 Main labels */ +@label_medium: #333333; +@label_light: #444444; +@label_halo_medium: #EDEBDB; + +/* 6.4 Road labels */ +@shield_text: #000000; +@shield_text_halo: #000000; +@shield: #FFFFFF; +@shield_outline: #000000; diff --git a/tests/assets/case-2-generate-drules-mini/include/priorities_1_BG-by-size.prio.txt b/tests/assets/case-2-generate-drules-mini/include/priorities_1_BG-by-size.prio.txt new file mode 100644 index 0000000..385b1ab --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/include/priorities_1_BG-by-size.prio.txt @@ -0,0 +1,16 @@ +# This file is automatically re-formatted and re-sorted in priorities descending order +# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.) +# are generated automatically for information only. Custom formatting and comments are not preserved. +# +# BG-by-size geometry: background areas rendered below BG-top and everything else. +# Smaller areas are rendered above larger ones (area's size is estimated as the size of its' bounding box). +# So effectively priority values of BG-by-size areas are not used at the moment. +# But we might use them later for some special cases, e.g. to determine a main area type of a multi-type feature. +# Keep them in a logical importance order please. +# +# Priorities ranges' rendering order overview: +# - overlays (icons, captions...) +# - FG: foreground areas and lines +# - BG-top: water (linear and areal) +# - BG-by-size: landcover areas sorted by their size + diff --git a/tests/assets/case-2-generate-drules-mini/include/priorities_2_BG-top.prio.txt b/tests/assets/case-2-generate-drules-mini/include/priorities_2_BG-top.prio.txt new file mode 100644 index 0000000..9e47413 --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/include/priorities_2_BG-top.prio.txt @@ -0,0 +1,18 @@ +# This file is automatically re-formatted and re-sorted in priorities descending order +# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.) +# are generated automatically for information only. Custom formatting and comments are not preserved. +# +# BG-top geometry: background lines and areas that should be always below foreground ones +# (including e.g. layer=-10 underwater tunnels), but above background areas sorted by size (BG-by-size), +# because ordering by size doesn't always work with e.g. water mapped over a forest, +# so water should be on top of other landcover always, but linear waterways should be hidden beneath it. +# Still, e.g. a layer=-1 BG-top feature will be rendered under a layer=0 BG-by-size feature +# (so areal water tunnels are hidden beneath other landcover area) and a layer=1 landcover areas +# are displayed above layer=0 BG-top. +# +# Priorities ranges' rendering order overview: +# - overlays (icons, captions...) +# - FG: foreground areas and lines +# - BG-top: water (linear and areal) +# - BG-by-size: landcover areas sorted by their size + diff --git a/tests/assets/case-2-generate-drules-mini/include/priorities_3_FG.prio.txt b/tests/assets/case-2-generate-drules-mini/include/priorities_3_FG.prio.txt new file mode 100644 index 0000000..f249605 --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/include/priorities_3_FG.prio.txt @@ -0,0 +1,41 @@ +# This file is automatically re-formatted and re-sorted in priorities descending order +# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.) +# are generated automatically for information only. Custom formatting and comments are not preserved. +# +# FG geometry: foreground lines and areas (e.g. buildings) are rendered always below overlays +# and always on top of background geometry (BG-top & BG-by-size) even if a foreground feature +# is layer=-10 (as tunnels should be visibile over landcover and water). +# +# Priorities ranges' rendering order overview: +# - overlays (icons, captions...) +# - FG: foreground areas and lines +# - BG-top: water (linear and areal) +# - BG-by-size: landcover areas sorted by their size + +highway-motorway # line z6- (also has pathtext z10-, shield::shield z10-) +highway-motorway-bridge # line z6- (also has pathtext z10-, shield::shield z10-) +highway-motorway-tunnel # line z6- (also has pathtext z10-, shield::shield z10-) +highway-trunk # line z6- (also has pathtext z10-, shield::shield z10-) +highway-trunk-bridge # line z6- (also has pathtext z10-, shield::shield z10-) +highway-trunk-tunnel # line z6- (also has pathtext z10-, shield::shield z10-) +highway-world_level # line z4-9 +highway-world_towns_level # line z6-9 +=== 310 + +highway-primary # line z8- (also has pathtext z10-, shield::shield z10-) +highway-primary-bridge # line z8- (also has pathtext z10-, shield::shield z10-) +highway-primary-tunnel # line z8- (also has pathtext z10-, shield::shield z10-) +=== 290 + +highway-secondary # line z10- (also has pathtext z10-) +highway-secondary-bridge # line z10- (also has pathtext z10-) +highway-secondary-tunnel # line z10- (also has pathtext z10-) +=== 270 + +highway-motorway_link # line z10- (also has pathtext z10-, shield::shield z10-) +highway-motorway_link-bridge # line z10- (also has pathtext z10-, shield::shield z10-) +highway-motorway_link-tunnel # line z10- (also has pathtext z10-, shield::shield z10-) +highway-trunk_link # line z10- (also has pathtext z10-, shield::shield z10-) +highway-trunk_link-bridge # line z10- (also has pathtext z10-, shield::shield z10-) +highway-trunk_link-tunnel # line z10- (also has pathtext z10-, shield::shield z10-) +=== 228 diff --git a/tests/assets/case-2-generate-drules-mini/include/priorities_4_overlays.prio.txt b/tests/assets/case-2-generate-drules-mini/include/priorities_4_overlays.prio.txt new file mode 100644 index 0000000..2eaecb6 --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/include/priorities_4_overlays.prio.txt @@ -0,0 +1,61 @@ +# This file is automatically re-formatted and re-sorted in priorities descending order +# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.) +# are generated automatically for information only. Custom formatting and comments are not preserved. +# +# Overlays (icons, captions, path texts and shields) are rendered on top of all the geometry (lines, areas). +# Overlays don't overlap each other, instead the ones with higher priority displace the less important ones. +# Optional captions (which have an icon) are usually displayed only if there are no other overlays in their way +# (technically, max overlays priority value (10000) is subtracted from their priorities automatically). +# +# Priorities ranges' rendering order overview: +# - overlays (icons, captions...) +# - FG: foreground areas and lines +# - BG-top: water (linear and areal) +# - BG-by-size: landcover areas sorted by their size + +highway-motorway # pathtext z10- (also has shield::shield z10-, line z6-) +highway-motorway-bridge # pathtext z10- (also has shield::shield z10-, line z6-) +highway-motorway-tunnel # pathtext z10- (also has shield::shield z10-, line z6-) +highway-trunk # pathtext z10- (also has shield::shield z10-, line z6-) +highway-trunk-bridge # pathtext z10- (also has shield::shield z10-, line z6-) +highway-trunk-tunnel # pathtext z10- (also has shield::shield z10-, line z6-) +=== 6750 + +highway-motorway::shield # shield::shield z10- (also has pathtext z10-, line z6-) +highway-motorway-bridge::shield # shield::shield z10- (also has pathtext z10-, line z6-) +highway-motorway-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z6-) +highway-trunk::shield # shield::shield z10- (also has pathtext z10-, line z6-) +highway-trunk-bridge::shield # shield::shield z10- (also has pathtext z10-, line z6-) +highway-trunk-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z6-) +=== 6740 + +highway-primary # pathtext z10- (also has shield::shield z10-, line z8-) +highway-primary-bridge # pathtext z10- (also has shield::shield z10-, line z8-) +highway-primary-tunnel # pathtext z10- (also has shield::shield z10-, line z8-) +=== 6200 + +highway-motorway_link # pathtext z10- (also has shield::shield z10-, line z10-) +highway-motorway_link-bridge # pathtext z10- (also has shield::shield z10-, line z10-) +highway-motorway_link-tunnel # pathtext z10- (also has shield::shield z10-, line z10-) +highway-trunk_link # pathtext z10- (also has shield::shield z10-, line z10-) +highway-trunk_link-bridge # pathtext z10- (also has shield::shield z10-, line z10-) +highway-trunk_link-tunnel # pathtext z10- (also has shield::shield z10-, line z10-) +=== 6150 + +highway-motorway_link::shield # shield::shield z10- (also has pathtext z10-, line z10-) +highway-motorway_link-bridge::shield # shield::shield z10- (also has pathtext z10-, line z10-) +highway-motorway_link-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z10-) +highway-trunk_link::shield # shield::shield z10- (also has pathtext z10-, line z10-) +highway-trunk_link-bridge::shield # shield::shield z10- (also has pathtext z10-, line z10-) +highway-trunk_link-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z10-) +=== 6140 + +highway-secondary # pathtext z10- (also has line z10-) +highway-secondary-bridge # pathtext z10- (also has line z10-) +highway-secondary-tunnel # pathtext z10- (also has line z10-) +=== 5600 + +highway-primary::shield # shield::shield z10- (also has pathtext z10-, line z8-) +highway-primary-bridge::shield # shield::shield z10- (also has pathtext z10-, line z8-) +highway-primary-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z8-) +=== 2975 diff --git a/tests/assets/case-2-generate-drules-mini/main.mapcss b/tests/assets/case-2-generate-drules-mini/main.mapcss new file mode 100644 index 0000000..300b07d --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/main.mapcss @@ -0,0 +1,133 @@ +*::int_name +{ + text-offset: 1; +} + +@import("include/colors.mapcss"); +@import("include/Roads.mapcss"); +@import("include/Roads_label.mapcss"); + +colors +{ + GuiText-color: #4D4D4D; + GuiText-opacity: 0.86; + MyPositionAccuracy-color: #000000; + MyPositionAccuracy-opacity: 0.08; + Selection-color: #1E96F0; + Selection-opacity: 0.64; + Route-color: #0087FF; + RouteOutline-color: #055FCD; + RouteTrafficG0-color: #9B2300; + RouteTrafficG1-color: #E82705; + RouteTrafficG2-color: #E82705; + RouteTrafficG3-color: #FFE500; + RouteTrafficG3-opacity: 0.0; + RoutePedestrian-color: #1D339E; + RoutePedestrian-opacity: 0.8; + RouteBicycle-color: #9C27B0; + RouteBicycle-opacity: 0.8; + RouteRuler-color: #66347F; + RouteRuler-opacity: 0.9; + RoutePreview-color: #000000; + RoutePreview-opacity: 0.3; + RouteMaskCar-color: #000000; + RouteMaskCar-opacity: 0.3; + RouteFirstSegmentArrowsMaskCar-color: #033B80; + RouteFirstSegmentArrowsMaskCar-opacity: 0.0; + RouteArrowsMaskCar-color: #033B80; + RouteArrowsMaskCar-opacity: 0.2; + RouteMaskBicycle-color: #000000; + RouteMaskBicycle-opacity: 0.5; + RouteFirstSegmentArrowsMaskBicycle-color: #9C27B0; + RouteFirstSegmentArrowsMaskBicycle-opacity: 0.0; + RouteArrowsMaskBicycle-color: #9C27B0; + RouteArrowsMaskBicycle-opacity: 0.2; + RouteMaskPedestrian-color: #000000; + RouteMaskPedestrian-opacity: 0.5; + RouteFake-color: #A8A8A8; + RouteFakeOutline-color: #717171; + Arrow3D-color: #50AAFF; + Arrow3DObsolete-color: #82AAC8; + Arrow3DObsolete-opacity: 0.72; + Arrow3DShadow-color: #3C3C3C; + Arrow3DShadow-opacity: 0.24; + Arrow3DOutline-color: #FFFFFF; + TrackHumanSpeed-color: #1D339E; + TrackCarSpeed-color: #7C8EDE; + TrackPlaneSpeed-color: #A8B7ED; + TrackUnknownDistance-color: #616161; + TrafficG0-color: #7E1712; + TrafficG1-color: #E42300; + TrafficG2-color: #E42300; + TrafficG3-color: #FCDE00; + TrafficG3-opacity: 0.0; + TrafficG4-color: #39962E; + TrafficG5-color: #39962E; + TrafficTempBlock-color: #525252; + TrafficUnknown-color: #000000; + TrafficArrowLight-color: #FFFFFF; + TrafficArrowDark-color: #473635; + TrafficOutline-color: #E8E6DC; + RoadShieldBlackText-color: #000000; + RoadShieldWhiteText-color: #FFFFFF; + RoadShieldUKYellowText-color: #FFD400; + RoadShieldBlueBackground-color: #1A5EC1; + RoadShieldGreenBackground-color: #309302; + RoadShieldRedBackground-color: #E63534; + RoadShieldOrangeBackground-color: #FFBE00; + PoiHotelTextOutline-color: #FFFFFF; + PoiHotelTextOutline-opacity: 0.6; + PoiDeletedMask-color: #FFFFFF; + PoiDeletedMask-opacity: 0.3; + PoiVisitedMask-color: #FFFFFF; + PoiVisitedMask-opacity: 0.7; + DefaultTrackColor-color: #1E96F0; + RouteMarkPrimaryText-color: #000000; + RouteMarkPrimaryTextOutline-color: #FFFFFF; + RouteMarkSecondaryText-color: #000000; + RouteMarkSecondaryTextOutline-color: #FFFFFF; + TransitMarkPrimaryText-color: #000000; + TransitMarkPrimaryTextOutline-color: #FFFFFF; + TransitMarkSecondaryText-color: #000000; + TransitMarkSecondaryTextOutline-color: #FFFFFF; + TransitTransferOuterMarker-color: #000000; + TransitTransferInnerMarker-color: #FFFFFF; + TransitStopInnerMarker-color: #FFFFFF; + LocalAdsPrimaryText-color: #000000; + LocalAdsPrimaryTextOutline-color: #FFFFFF; + LocalAdsSecondaryText-color: #000000; + LocalAdsSecondaryTextOutline-color: #FFFFFF; + TransitBackground-color: #FFFFFF; + TransitBackground-opacity: 0.4; + BookmarkRed-color: #E51B23; + BookmarkPink-color: #FF4182; + BookmarkPurple-color: #9B24B2; + BookmarkDeepPurple-color: #6639BF; + BookmarkBlue-color: #0066CC; + BookmarkLightBlue-color: #249CF2; + BookmarkCyan-color: #14BECD; + BookmarkTeal-color: #00A58C; + BookmarkGreen-color: #3C8C3C; + BookmarkLime-color: #93BF39; + BookmarkYellow-color: #FFC800; + BookmarkOrange-color: #FF9600; + BookmarkDeepOrange-color: #F06432; + BookmarkBrown-color: #804633; + BookmarkGray-color: #737373; + BookmarkBlueGray-color: #597380; + SearchmarkPreparing-color: #597380; + SearchmarkNotAvailable-color: #597380; + SearchmarkSelectedNotAvailable-color: #F06432; + RatingBad-color: #F06432; + RatingGood-color: #3C8C3C; + RatingNone-color: #249CF2; + SearchmarkDefault-color: #249CF2; + RatingText-color: #FFFFFF; + UGCRatingText-color: #000000; + SpeedCameraMarkText-color: #FFFFFF; + SpeedCameraMarkBg-color: #F51E30; + SpeedCameraMarkOutline-color: #FFFFFF; + GuideCityMarkText-color: #6639BF; + GuideOutdoorMarkText-color: #3C8C3C; + HotelPriceText-color: #000000; +} diff --git a/tests/assets/case-2-generate-drules-mini/mapcss-dynamic.txt b/tests/assets/case-2-generate-drules-mini/mapcss-dynamic.txt new file mode 100644 index 0000000..60746b0 --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/mapcss-dynamic.txt @@ -0,0 +1,4 @@ +population +name +bbox_area +rating diff --git a/tests/assets/case-2-generate-drules-mini/mapcss-mapping.csv b/tests/assets/case-2-generate-drules-mini/mapcss-mapping.csv new file mode 100644 index 0000000..077f771 --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/mapcss-mapping.csv @@ -0,0 +1,148 @@ +highway|residential;2; +highway|service;3; +highway|unclassified;5; +highway|footway;7; +highway|track;8; +highway|tertiary;9; +highway|secondary;13; +highway|path;16; +highway|bus_stop;17; +highway|footway|sidewalk;[highway=footway][footway=sidewalk];;name;int_name;18; +highway|primary;27; +highway|service|parking_aisle;[highway=service][service=parking_aisle];;name;int_name;29; +moved:highway|road:05.2024;31;highway|road +deprecated:highway|track|grade2:04.2024;[highway=track][tracktype=grade2];x;name;int_name;32;highway|track +deprecated:highway|track|grade3:04.4024;[highway=track][tracktype=grade3];x;name;int_name;34;highway|track +highway|cycleway;37; +deprecated:highway|track|grade1:04.2024;[highway=track][tracktype=grade1];x;name;int_name;40;highway|track +highway|service|driveway;[highway=service][service=driveway];;name;int_name;42; +highway|motorway_link;44; +deprecated:highway|track|grade4:04.2024;[highway=track][tracktype=grade4];x;name;int_name;46;highway|track +highway|footway|crossing;[highway=footway][footway=crossing];;name;int_name;51; +highway|path|bicycle;[highway=path][bicycle=designated];;;;53; +highway|living_street;55; +highway|motorway;58; +highway|steps;59; +deprecated:highway|track|grade5:04.2024;[highway=track][tracktype=grade5];x;name;int_name;63;highway|track +highway|trunk;66; +highway|pedestrian;70; +highway|motorway|bridge;[highway=motorway][bridge?];;name;int_name;72; +highway|residential|bridge;[highway=residential][bridge?];;name;int_name;81; +highway|secondary|bridge;[highway=secondary][bridge?];;name;int_name;85; +highway|tertiary|bridge;[highway=tertiary][bridge?];;name;int_name;86; +highway|trunk_link;91; +highway|unclassified|bridge;[highway=unclassified][bridge?];;name;int_name;92; +highway|primary|bridge;[highway=primary][bridge?];;name;int_name;95; +highway|primary_link;96; +highway|footway|bridge;[highway=footway][bridge?];;name;int_name;98; +deprecated:highway|path|hiking:04.2024;[highway=path][route=hiking],[highway=path][sac_scale=hiking];x;name;int_name;113;highway|path +highway|trunk|bridge;[highway=trunk][bridge?];;name;int_name;116; +highway|motorway_junction;121; +highway|footway|bicycle;[highway=footway][bicycle=designated];;;;141; +highway|motorway_link|bridge;[highway=motorway_link][bridge?];;name;int_name;143; +deprecated:highway|footway|permissive:12.2023;[highway=footway][access=permissive],[highway=footway][foot=permissive];x;name;int_name;153;highway|footway +highway|pedestrian|area;[highway=pedestrian][area?];;name;int_name;158; +highway|construction;163; +highway|cycleway|bridge;[highway=cycleway][bridge?];;name;int_name;164; +deprecated:highway|path|mountain_hiking:04.2024;[highway=path][sac_scale=mountain_hiking];x;name;int_name;166;highway|path +highway|bridleway;168; +highway|secondary_link;177; +highway|footway|tunnel;[highway=footway][tunnel?],[highway=footway][location=underground];;name;int_name;183; +highway|track|bridge;[highway=track][bridge?];;name;int_name;193; +highway|path|bridge;[highway=path][bridge?];;name;int_name;194; +highway|service|bridge;[highway=service][bridge?];;name;int_name;203; +highway|service|area;[highway=service][area?];;name;int_name;226; +highway|residential|area;[highway=residential][area?];;name;int_name;227; +deprecated:highway|track|permissive:12.2023;[highway=track][access=permissive];x;name;int_name;229;highway|track +highway|cycleway|tunnel;[highway=cycleway][tunnel?];;name;int_name;232; +highway|unclassified|tunnel;[highway=unclassified][tunnel?];;name;int_name;235; +highway|residential|tunnel;[highway=residential][tunnel?];;name;int_name;238; +deprecated:highway|path|permissive:12.2023;[highway=path][access=permissive];x;name;int_name;240;highway|path +highway|trunk_link|bridge;[highway=trunk_link][bridge?];;name;int_name;261; +highway|service|tunnel;[highway=service][tunnel?];;name;int_name;263; +highway|tertiary|tunnel;[highway=tertiary][tunnel?];;name;int_name;269; +highway|tertiary_link;273; +highway|footway|area;[highway=footway][area?];;name;int_name;276; +highway|road|bridge;[highway=road][bridge?];;name;int_name;280; +highway|secondary|tunnel;[highway=secondary][tunnel?];;name;int_name;297; +deprecated:highway|path|demanding_mountain_hiking:04.2024;[highway=path][sac_scale=demanding_mountain_hiking];x;name;int_name;300;highway|path|difficult +highway|pedestrian|bridge;[highway=pedestrian][bridge?];;name;int_name;304; +highway|raceway;308; +highway|primary|tunnel;[highway=primary][tunnel?];;name;int_name;309; +highway|primary_link|bridge;[highway=primary_link][bridge?];;name;int_name;310; +deprecated:highway|footway|hiking:04.2024;[highway=footway][sac_scale=hiking];x;name;int_name;314;highway|path +highway|path|horse;[highway=path][horse?];;name;int_name;317; +highway|trunk|tunnel;[highway=trunk][tunnel?];;name;int_name;326; +highway|steps|tunnel;[highway=steps][tunnel?],[highway=steps][location=underground];;name;int_name;327; +highway|steps|bridge;[highway=steps][bridge?];;name;int_name;330; +highway|pedestrian|tunnel;[highway=pedestrian][tunnel?],[highway=pedestrian][location=underground];;name;int_name;332; +highway|path|tunnel;[highway=path][tunnel?],[highway=path][location=underground];;name;int_name;336; +deprecated:highway|path|alpine_hiking:04.2024;[highway=path][sac_scale=alpine_hiking];x;name;int_name;350;highway|path|expert +deprecated:highway|cycleway|permissive:12.2023;[highway=cycleway][access=permissive];x;name;int_name;353;highway|cycleway +highway|unclassified|area;[highway=unclassified][area?];;name;int_name;354; +deprecated:highway|footway|mountain_hiking:04.2024;[highway=footway][sac_scale=mountain_hiking];x;name;int_name;361;highway|path +deprecated:highway|service|driveway|bridge:01.2020;[highway=service][service=driveway][bridge?];x;name;int_name;362;highway|service|driveway +deprecated:highway|bridleway|permissive:12.2023;[highway=bridleway][access=permissive];x;name;int_name;370;highway|bridleway +highway|bridleway|bridge;[highway=bridleway][bridge?];;name;int_name;378; +deprecated:highway|service|driveway|tunnel:01.2020;[highway=service][service=driveway][tunnel?];x;name;int_name;379;highway|service|driveway +deprecated:highway|service|driveway|area:01.2020;[highway=service][service=driveway][area?];x;name;int_name;386;highway|service|driveway +deprecated:highway|path|demanding_alpine_hiking:04.2024;[highway=path][sac_scale=demanding_alpine_hiking];x;name;int_name;395;highway|path|expert +highway|secondary_link|bridge;[highway=secondary_link][bridge?];;name;int_name;397; +area:highway|living_street;401; +highway|living_street|bridge;[highway=living_street][bridge?];;name;int_name;407; +highway|road;411; +highway|motorway|tunnel;[highway=motorway][tunnel?];;name;int_name;416; +area:highway|service;418; +highway|road|tunnel;[highway=road][tunnel?];;name;int_name;423; +highway|ford;427; +area:highway|path;428; +highway|track|area;[highway=track][area?];;name;int_name;430; +deprecated:highway|path|difficult_alpine_hiking:04.2024;[highway=path][sac_scale=difficult_alpine_hiking];x;name;int_name;444;highway|path|expert +deprecated:highway|footway|demanding_mountain_hiking:04.2024;[highway=footway][sac_scale=demanding_mountain_hiking];x;name;int_name;452;highway|path|difficult +highway|living_street|tunnel;[highway=living_street][tunnel?];;name;int_name;457; +highway|path|difficult;[highway=path][_path_grade=difficult];;name;int_name;464; +highway|path|expert;[highway=path][_path_grade=expert];;name;int_name;465; +area:highway|steps;470; +highway|bridleway|tunnel;[highway=bridleway][tunnel?];;name;int_name;488; +highway|motorway_link|tunnel;[highway=motorway_link][tunnel?];;name;int_name;489; +highway|tertiary_link|bridge;[highway=tertiary_link][bridge?];;name;int_name;493; +highway|trunk_link|tunnel;[highway=trunk_link][tunnel?];;name;int_name;503; +highway|primary_link|tunnel;[highway=primary_link][tunnel?];;name;int_name;528; +deprecated:highway|footway|alpine_hiking:04.2024;[highway=footway][sac_scale=alpine_hiking];x;name;int_name;529;highway|path|expert +deprecated:amenity|speed_trap:10.2021;542;highway|speed_camera +area:highway|track;543; +area:highway|primary;544; +deprecated:highway|footway|demanding_alpine_hiking:04.2024;[highway=footway][sac_scale=demanding_alpine_hiking];x;name;int_name;555;highway|path|expert +highway|secondary_link|tunnel;[highway=secondary_link][tunnel?];;name;int_name;578; +highway|track|grade3|permissive;[highway=track][tracktype=grade3][access=permissive];x;name;int_name;591;highway|track +deprecated:highway|footway|difficult_alpine_hiking:04.2024;[highway=footway][sac_scale=difficult_alpine_hiking];x;name;int_name;627;highway|path|expert +highway|track|grade5|permissive;[highway=track][tracktype=grade5][access=permissive];x;name;int_name;631;highway|track +highway|tertiary_link|tunnel;[highway=tertiary_link][tunnel?];;name;int_name;634; +highway|track|grade4|permissive;[highway=track][tracktype=grade4][access=permissive];x;name;int_name;675;highway|track +highway|track|grade3|no-access;[highway=track][tracktype=grade3][access=no];x;name;int_name;821;highway|track +highway|track|grade4|no-access;[highway=track][tracktype=grade4][access=no];x;name;int_name;822;highway|track +highway|track|grade5|no-access;[highway=track][tracktype=grade5][access=no];x;name;int_name;823;highway|track +highway|track|no-access;[highway=track][access=no];;name;int_name;824; +deprecated:highway|service|busway:10.2023;[highway=service][service=busway];x;name;int_name;857;highway|busway +highway|busway;[highway=busway],[highway=service][service=busway],[highway=service][service=bus];;name;int_name;858; +highway|busway|bridge;[highway=busway][bridge?];;name;int_name;859; +highway|busway|tunnel;[highway=busway][tunnel?];;name;int_name;860; +area:highway|footway;866; +area:highway|residential;868; +area:highway|secondary;869; +area:highway|tertiary;870; +area:highway|pedestrian;873; +area:highway|unclassified;874; +area:highway|cycleway;877; +area:highway|motorway;879; +area:highway|trunk;880; +highway|speed_camera;991; +highway|world_level;1052; +highway|world_towns_level;1053; +highway|elevator;1059; +highway|rest_area;1080; +highway|traffic_signals;1081; +hwtag|nobicycle;1114; +hwtag|yesbicycle;1115; +hwtag|bidir_bicycle;1116; +highway|services;1173; diff --git a/tests/assets/case-2-generate-drules-mini/readme.md b/tests/assets/case-2-generate-drules-mini/readme.md new file mode 100644 index 0000000..d57323c --- /dev/null +++ b/tests/assets/case-2-generate-drules-mini/readme.md @@ -0,0 +1,4 @@ +Files for testLibkomwm.test_generate_drules_mini() method. + +These styles contain only zooms 0-10 and only highway=* rules. +So we can verify generated files content. diff --git a/tests/assets/case-3-styles-validation/readme.md b/tests/assets/case-3-styles-validation/readme.md new file mode 100644 index 0000000..a937244 --- /dev/null +++ b/tests/assets/case-3-styles-validation/readme.md @@ -0,0 +1 @@ +Files for testLibkomwm.test_generate_drules_validation_errors() method. diff --git a/tests/testCondition.py b/tests/testCondition.py new file mode 100644 index 0000000..5ab7b68 --- /dev/null +++ b/tests/testCondition.py @@ -0,0 +1,300 @@ +import re +import unittest +import sys +from pathlib import Path + +# Add `src` directory to the import paths +sys.path.insert(0, str(Path(__file__).parent.parent / 'src')) + +from mapcss import parseCondition +from mapcss.Condition import Condition + +class ConditionTest(unittest.TestCase): + + def test_parser_eq(self): + cond:Condition = parseCondition("natural=coastline") + self.assertEqual(cond.type, "eq") + self.assertEqual(cond.params, ("natural", "coastline")) + self.assertTrue(cond.test({'natural': 'coastline'})) + self.assertFalse(cond.test({'Natural': 'Coastline'})) + + cond = parseCondition(" highway\t=\tprimary") + self.assertEqual(cond.type, "eq") + self.assertEqual(cond.params, ("highway", "primary")) + self.assertTrue(cond.test({'highway': 'primary'})) + self.assertFalse(cond.test({'highway': 'secondary'})) + + cond = parseCondition(" admin_level = 3") + self.assertEqual(cond.type, "eq") + self.assertEqual(cond.params, ("admin_level", "3")) + self.assertTrue(cond.test({'admin_level': '3'})) + self.assertFalse(cond.test({'admin_level': '32'})) + + cond = Condition('eq', ("::class", "::*")) + self.assertEqual(cond.type, "eq") + self.assertEqual(cond.params, ("::class", "::*")) + self.assertEqual(cond.extract_tag(), "*") + self.assertEqual(cond.test({'any_key': 'any_value'}), "::*") + self.assertTrue(cond.test({'any_key': 'any_value'})) + + cond = Condition('eq', ("::class", "::int_name")) + self.assertEqual(cond.type, "eq") + self.assertEqual(cond.params, ("::class", "::int_name")) + self.assertEqual(cond.extract_tag(), "*") + self.assertEqual(cond.test({'any_key': 'any_value'}), "::int_name") + self.assertTrue(cond.test({'any_key': 'any_value'})) + + def test_parser_regex(self): + """ Test conditions in format natural =~/water.+/ + Note that such conditions are not used by Organic Maps styles. + """ + cond:Condition = parseCondition("natural =~/water.+/") + self.assertEqual(cond.type, "regex") + self.assertEqual(cond.params, ("natural", "water.+")) + self.assertEqual(type(cond.regex), re.Pattern) + self.assertTrue(cond.test({"natural": "waterway"})) + self.assertTrue(cond.test({"natural": "water123"})) + self.assertFalse(cond.test({"natural": "water"})) + self.assertFalse(cond.test({"natural": " waterway "})) + + def test_parser_ge(self): + cond:Condition = parseCondition("population>=0") + self.assertEqual(cond.type, ">=") + self.assertEqual(cond.params, ("population", "0")) + self.assertTrue(cond.test({"population": "0"})) + self.assertTrue(cond.test({"population": "100000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"population": "-1"})) + + cond:Condition = parseCondition("population >= 150000") + self.assertEqual(cond.type, ">=") + self.assertEqual(cond.params, ("population", "150000")) + self.assertTrue(cond.test({"population": "150000"})) + self.assertTrue(cond.test({"population": "250000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"population": "10000"})) + + cond:Condition = parseCondition("\tbbox_area >= 4000000") + self.assertEqual(cond.type, ">=") + self.assertEqual(cond.params, ("bbox_area", "4000000")) + self.assertTrue(cond.test({"bbox_area": "4000000"})) + self.assertTrue(cond.test({"bbox_area": "8000000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"bbox_area": "999"})) + + def test_parser_gt(self): + """ Test conditions in format population > 100000 + Note that such conditions are not used by Organic Maps styles. + """ + cond:Condition = parseCondition("population>0") + self.assertEqual(cond.type, ">") + self.assertEqual(cond.params, ("population", "0")) + self.assertTrue(cond.test({"population": "100"})) + self.assertFalse(cond.test({"population": "000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"population": "-1"})) + + cond:Condition = parseCondition("population > 150000") + self.assertEqual(cond.type, ">") + self.assertEqual(cond.params, ("population", "150000")) + self.assertTrue(cond.test({"population": "250000"})) + self.assertFalse(cond.test({"population": "150000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"population": "10000"})) + + cond:Condition = parseCondition("\tbbox_area > 4000000 ") + self.assertEqual(cond.type, ">") + self.assertEqual(cond.params, ("bbox_area", "4000000 ")) # TODO fix parser to exclude trailing space + self.assertTrue(cond.test({"bbox_area": "8000000"})) + self.assertFalse(cond.test({"bbox_area": "4000000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"bbox_area": "999"})) + + def test_parser_lt(self): + cond:Condition = parseCondition("population<40000") + self.assertEqual(cond.type, "<") + self.assertEqual(cond.params, ("population", "40000")) + self.assertTrue(cond.test({"population": "100"})) + self.assertTrue(cond.test({"population": "-1"})) + self.assertFalse(cond.test({"population": "40000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"population": "500000"})) + + cond:Condition = parseCondition("\tbbox_area < 4000000\n") + self.assertEqual(cond.type, "<") + self.assertEqual(cond.params, ("bbox_area", "4000000\n")) # TODO fix parser to exclude trailing \n + self.assertTrue(cond.test({"bbox_area": "100"})) + self.assertTrue(cond.test({"bbox_area": "-1"})) + self.assertTrue(cond.test({"bbox_area": "000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"bbox_area": "4000000"})) + self.assertFalse(cond.test({"bbox_area": "8000000"})) + + def test_parser_le(self): + """ Test conditions in format population <= 100000 + Note that such conditions are not used by Organic Maps styles. + """ + cond:Condition = parseCondition("population<=40000") + self.assertEqual(cond.type, "<=") + self.assertEqual(cond.params, ("population", "40000")) + self.assertTrue(cond.test({"population": "100"})) + self.assertTrue(cond.test({"population": "-1"})) + self.assertTrue(cond.test({"population": "40000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"population": "500000"})) + + cond:Condition = parseCondition("\tbbox_area <= 4000000\n") + self.assertEqual(cond.type, "<=") + self.assertEqual(cond.params, ("bbox_area", "4000000\n")) # TODO fix parser to exclude trailing \n + self.assertTrue(cond.test({"bbox_area": "100"})) + self.assertTrue(cond.test({"bbox_area": "-1"})) + self.assertTrue(cond.test({"bbox_area": "000"})) + self.assertTrue(cond.test({"bbox_area": "4000000"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"bbox_area": "8000000"})) + + def test_parser_ne(self): + cond:Condition = parseCondition("capital!=2") + self.assertEqual(cond.type, "ne") + self.assertEqual(cond.params, ("capital", "2")) + self.assertTrue(cond.test({"capital": "1"})) + self.assertTrue(cond.test({"capital": "22"})) + self.assertTrue(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"capital": "2"})) + + cond:Condition = parseCondition("\tcapital != 2") + self.assertEqual(cond.type, "ne") + self.assertEqual(cond.params, ("capital", "2")) + self.assertTrue(cond.test({"capital": "1"})) + self.assertTrue(cond.test({"capital": "22"})) + self.assertTrue(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"capital": "2"})) + + cond:Condition = parseCondition("garden:type != residential") + self.assertEqual(cond.type, "ne") + self.assertEqual(cond.params, ("garden:type", "residential")) + self.assertTrue(cond.test({"garden:type": "public"})) + self.assertTrue(cond.test({"garden:type": "res"})) + self.assertTrue(cond.test({"garden:type": "residential_plus"})) + self.assertTrue(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"garden:type": "residential"})) + + def test_parser_set(self): + cond:Condition = parseCondition("tunnel") + self.assertEqual(cond.type, "set") + self.assertEqual(cond.params, ("tunnel", )) + self.assertTrue(cond.test({"tunnel": "yes"})) + self.assertTrue(cond.test({"tunnel": "maybe"})) + self.assertTrue(cond.test({"tunnel": "+1"})) + self.assertFalse(cond.test({"highway": "secondary"})) + + cond:Condition = parseCondition("building\t") + self.assertEqual(cond.type, "set") + self.assertEqual(cond.params, ("building", )) + self.assertTrue(cond.test({"building": "yes"})) + self.assertTrue(cond.test({"building": "apartment"})) + self.assertTrue(cond.test({"building": "1"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"building:part": "yes"})) + + cond:Condition = parseCondition(" addr:housenumber ") + self.assertEqual(cond.type, "set") + self.assertEqual(cond.params, ("addr:housenumber", )) + self.assertTrue(cond.test({"addr:housenumber": "1"})) + self.assertTrue(cond.test({"addr:housenumber": "yes"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"addr:street": "Baker st"})) + + cond:Condition = parseCondition(" some-tag ") + self.assertEqual(cond.type, "set") + self.assertEqual(cond.params, ("some-tag", )) + self.assertTrue(cond.test({"some-tag": "1"})) + self.assertTrue(cond.test({"some-tag": "yes"})) + self.assertFalse(cond.test({"highway": "secondary"})) + self.assertFalse(cond.test({"some": "tag"})) + + def test_parser_unset(self): + cond:Condition = parseCondition("!tunnel") + self.assertEqual(cond.type, "unset") + self.assertEqual(cond.params, ("tunnel", )) + self.assertTrue(cond.test({"capital": "1"})) + self.assertFalse(cond.test({"tunnel": "yes"})) + self.assertFalse(cond.test({"tunnel": "no"})) + + cond:Condition = parseCondition("\t!name ") + self.assertEqual(cond.type, "unset") + self.assertEqual(cond.params, ("name", )) + self.assertTrue(cond.test({"capital": "1"})) + self.assertTrue(cond.test({"int_name": "1"})) + self.assertFalse(cond.test({"name": "London"})) + + def test_parser_false(self): + """ Test conditions in format some_tag = no + Note that such conditions are not used by Organic Maps styles. + """ + cond:Condition = parseCondition("access=no") + self.assertEqual(cond.type, "false") + self.assertEqual(cond.params, ("access", )) + #self.assertTrue(cond.test({"access": "no"})) # test is not implemented for `false` condition + #self.assertTrue(cond.test({"access": "private"})) # test is not implemented for `false` condition + self.assertFalse(cond.test({"tunnel": "yes"})) + + def test_parser_invTrue(self): + """ Test conditions in format [!some_tag?] It works the same way as [some_tag != yes] + Note that such conditions are not used by Organic Maps styles. + """ + cond:Condition = parseCondition("!oneway?") + self.assertEqual(cond.type, "ne") + self.assertEqual(cond.params, ("oneway", "yes")) + self.assertTrue(cond.test({"oneway": "no"})) + self.assertTrue(cond.test({"oneway": "nobody_knows"})) + self.assertTrue(cond.test({"access": "private"})) + self.assertFalse(cond.test({"oneway": "yes"})) + + cond:Condition = parseCondition("\t! intermittent ?\n") + self.assertEqual(cond.type, "ne") + self.assertEqual(cond.params, ("intermittent", "yes")) + self.assertTrue(cond.test({"intermittent": "no"})) + self.assertTrue(cond.test({"intermittent": "maybe"})) + self.assertTrue(cond.test({"access": "private"})) + self.assertFalse(cond.test({"intermittent": "yes"})) + + def test_parser_true(self): + """ Test conditions in format [some_tag?] It works the same way as [some_tag = yes] """ + cond:Condition = parseCondition("area?") + self.assertEqual(cond.type, "true") + self.assertEqual(cond.params, ("area", )) + self.assertTrue(cond.test({"area": "yes"})) + self.assertFalse(cond.test({"area": "no"})) + self.assertFalse(cond.test({"access": "private"})) + self.assertFalse(cond.test({"oneway": "nobody_knows"})) + + cond:Condition = parseCondition("\tbridge ? ") + self.assertEqual(cond.type, "true") + self.assertEqual(cond.params, ("bridge", )) + self.assertTrue(cond.test({"bridge": "yes"})) + self.assertFalse(cond.test({"bridge": "no"})) + self.assertFalse(cond.test({"access": "private"})) + self.assertFalse(cond.test({"bridge": "maybe"})) + + def test_untrue(self): + """ parseCondition(...) doesn't support this type of condition. + Not sure if it's ever used. + """ + cond:Condition = Condition("untrue", "access") + self.assertEqual(cond.type, "untrue") + self.assertEqual(cond.params, ("access", )) + self.assertTrue(cond.test({"access": "no"})) + self.assertFalse(cond.test({"access": "private"})) + self.assertFalse(cond.test({"oneway": "yes"})) + + def test_parser_errors(self): + with self.assertRaises(Exception): + parseCondition("! tunnel") + with self.assertRaises(Exception): + """ Symbol '-' is only supported in simple 'set' rule. E.g. [key-with-dash] + But not in 'unset' rule [!key-with-dash] """ + parseCondition("key-with-dash?") + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testEval.py b/tests/testEval.py new file mode 100644 index 0000000..94edc2a --- /dev/null +++ b/tests/testEval.py @@ -0,0 +1,124 @@ +import unittest +import sys +from pathlib import Path + +# Add `src` directory to the import paths +sys.path.insert(0, str(Path(__file__).parent.parent / 'src')) + +from mapcss.Eval import Eval + +class EvalTest(unittest.TestCase): + """ Test eval(...) feature for CSS properties. + NOTE: eval() is not used in Organic Maps styles. We can drop it completely. + """ + def test_eval_tag(self): + a = Eval("""eval( tag("lanes") )""") + self.assertEqual(a.compute({"lanes": "4"}), "4") + self.assertEqual(a.compute({"natural": "trees"}), "") + self.assertSetEqual(a.extract_tags(), {"lanes"}) + + def test_eval_prop(self): + a = Eval("""eval( prop("dpi") / 2 )""") + self.assertEqual(a.compute({"lanes": "4"}, {"dpi": 144}), "72") + self.assertEqual(a.compute({"lanes": "4"}, {"orientation": "vertical"}), "") + self.assertSetEqual(a.extract_tags(), set()) + + def test_eval_num(self): + a = Eval("""eval( num(tag("lanes")) + 2 )""") + self.assertEqual(a.compute({"lanes": "4"}), "6") + self.assertEqual(a.compute({"lanes": "many"}), "2") + self.assertSetEqual(a.extract_tags(), {"lanes"}) + + def test_eval_metric(self): + a = Eval("""eval( metric(tag("height")) )""") + self.assertEqual(a.compute({"height": "512"}), "512") + self.assertEqual(a.compute({"height": "10m"}), "10") + self.assertEqual(a.compute({"height": " 10m"}), "10") + self.assertEqual(a.compute({"height": "500cm"}), "5") + self.assertEqual(a.compute({"height": "500 cm"}), "5") + self.assertEqual(a.compute({"height": "250CM"}), "2.5") + self.assertEqual(a.compute({"height": "250 CM"}), "2.5") + self.assertEqual(a.compute({"height": "30см"}), "0.3") + self.assertEqual(a.compute({"height": " 30 см"}), "0.3") + self.assertEqual(a.compute({"height": "1200 mm"}), "1.2") + self.assertEqual(a.compute({"height": "2400MM"}), "2.4") + self.assertEqual(a.compute({"height": "2800 мм"}), "2.8") + self.assertSetEqual(a.extract_tags(), {"height"}) + + def test_eval_metric_with_scale(self): + a = Eval("""eval( metric(tag("height")) )""") + self.assertEqual(a.compute({"height": "512"}, xscale=4), "2048") + self.assertEqual(a.compute({"height": "512"}, zscale=4), "512") + self.assertEqual(a.compute({"height": "10m"}, xscale=4), "40") + self.assertEqual(a.compute({"height": " 10m"}, xscale=4), "40") + self.assertEqual(a.compute({"height": "500cm"}, xscale=4), "20") + self.assertEqual(a.compute({"height": "500 cm"}, xscale=4), "20") + self.assertEqual(a.compute({"height": "250CM"}, xscale=4), "10") + self.assertEqual(a.compute({"height": "250 CM"}, xscale=4), "10") + self.assertEqual(a.compute({"height": "30см"}, xscale=4), "1.2") + self.assertEqual(a.compute({"height": " 30 см"}, xscale=4), "1.2") + self.assertEqual(a.compute({"height": "1200 mm"}, xscale=4), "4.8") + self.assertEqual(a.compute({"height": "2400MM"}, xscale=4), "9.6") + self.assertEqual(a.compute({"height": "2800 мм"}, xscale=4), "11.2") + self.assertSetEqual(a.extract_tags(), {"height"}) + + def test_eval_zmetric(self): + a = Eval("""eval( zmetric(tag("depth")) )""") + self.assertEqual(a.compute({"depth": "512"}), "256") + self.assertEqual(a.compute({"depth": "10m"}), "5") + self.assertEqual(a.compute({"depth": " 10m"}), "5") + self.assertEqual(a.compute({"depth": "500cm"}), "2.5") + self.assertEqual(a.compute({"depth": "500 cm"}), "2.5") + self.assertEqual(a.compute({"depth": "250CM"}), "1.25") + self.assertEqual(a.compute({"depth": "250 CM"}), "1.25") + self.assertEqual(a.compute({"depth": "30см"}), "0.15") + self.assertEqual(a.compute({"depth": " 30 см"}), "0.15") + self.assertEqual(a.compute({"depth": "1200 mm"}), "0.6") + self.assertEqual(a.compute({"depth": "2400MM"}), "1.2") + self.assertEqual(a.compute({"depth": "2800 мм"}), "1.4") + self.assertSetEqual(a.extract_tags(), {"depth"}) + + def test_eval_str(self): + a = Eval("""eval( str( num(tag("width")) - 200 ) )""") + self.assertEqual(a.compute({"width": "400"}), "200.0") + self.assertSetEqual(a.extract_tags(), {"width"}) + + def test_eval_any(self): + a = Eval("""eval( any(tag("building"), tag("building:part"), "no") )""") + self.assertEqual(a.compute({"building": "apartment"}), "apartment") + self.assertEqual(a.compute({"building:part": "roof"}), "roof") + self.assertEqual(a.compute({"junction": "roundabout"}), "no") + self.assertSetEqual(a.extract_tags(), {"building", "building:part"}) + + def test_eval_min(self): + a = Eval("""eval( min( num(tag("building:levels")) * 3, 50) )""") + self.assertEqual(a.compute({"natural": "wood"}), "0") + self.assertEqual(a.compute({"building:levels": "0"}), "0") + self.assertEqual(a.compute({"building:levels": "10"}), "30") + self.assertEqual(a.compute({"building:levels": "30"}), "50") + self.assertSetEqual(a.extract_tags(), {"building:levels"}) + + def test_eval_max(self): + a = Eval("""eval( max( tag("speed:limit"), 60) )""") + self.assertEqual(a.compute({"natural": "wood"}), "60") + self.assertEqual(a.compute({"speed:limit": "30"}), "60") + self.assertEqual(a.compute({"speed:limit": "60"}), "60") + self.assertEqual(a.compute({"speed:limit": "90"}), "90") + self.assertSetEqual(a.extract_tags(), {"speed:limit"}) + + def test_eval_cond(self): + a = Eval("""eval( cond( boolean(tag("oneway")), 200, 100) )""") + self.assertEqual(a.compute({"natural": "wood"}), "100") + self.assertEqual(a.compute({"oneway": "yes"}), "200") + self.assertEqual(a.compute({"oneway": "no"}), "100") + self.assertEqual(a.compute({"oneway": "true"}), "200") + self.assertEqual(a.compute({"oneway": "probably no"}), "200") + self.assertSetEqual(a.extract_tags(), {"oneway"}) + + def test_complex_eval(self): + a = Eval(""" eval( any( metric(tag("height")), metric ( num(tag("building:levels")) * 3), metric("1m"))) """) + self.assertEqual(a.compute({"building:levels": "3"}), "9") + self.assertSetEqual(a.extract_tags(), {"height", "building:levels"}) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testLibkomwm.py b/tests/testLibkomwm.py new file mode 100644 index 0000000..c2f3e52 --- /dev/null +++ b/tests/testLibkomwm.py @@ -0,0 +1,68 @@ +import unittest +import sys +from pathlib import Path +from copy import deepcopy + +# Add `src` directory to the import paths +sys.path.insert(0, str(Path(__file__).parent.parent / 'src')) + +import libkomwm +from libkomwm import komap_mapswithme + + +class LibKomwmTest(unittest.TestCase): + def test_generate_drules_mini(self): + assets_dir = Path(__file__).parent / 'assets' / 'case-2-generate-drules-mini' + + class Options(object): + pass + + options = Options() + options.data = None + options.minzoom = 0 + options.maxzoom = 10 + options.txt = True + options.filename = str( assets_dir / "main.mapcss" ) + options.outfile = str( assets_dir / "style_output" ) + options.priorities_path = str( assets_dir / "include" ) + + try: + # Save state + libkomwm.MULTIPROCESSING = False + prio_ranges_orig = deepcopy(libkomwm.prio_ranges) + libkomwm.visibilities = {} + + # Run style generation + komap_mapswithme(options) + + # Restore state + libkomwm.prio_ranges = prio_ranges_orig + libkomwm.MULTIPROCESSING = True + libkomwm.visibilities = {} + + # Check that types.txt contains 1173 lines + with open(assets_dir / "types.txt", "rt") as typesFile: + lines = [l.strip() for l in typesFile] + self.assertEqual(len(lines), 1173, "Generated types.txt file should contain 1173 lines") + self.assertEqual(len([l for l in lines if l!="mapswithme"]), 148, "Actual types count should be 148 as in mapcss-mapping.csv") + + # Check that style_output.bin has 20 styles + with open(assets_dir / "style_output.bin", "rb") as protobuf_file: + protobuf_data = protobuf_file.read() + drules = libkomwm.ContainerProto() + drules.ParseFromString(protobuf_data) + + self.assertEqual(len(drules.cont), 20, "Generated style_output.bin should contain 20 styles") + + finally: + # Clean up generated files + files2delete = ["classificator.txt", "colors.txt", "patterns.txt", "style_output.bin", + "style_output.txt", "types.txt", "visibility.txt"] + for filename in files2delete: + (assets_dir / filename).unlink(missing_ok=True) + + def test_generate_drules_validation_errors(self): + assets_dir = Path(__file__).parent / 'assets' / 'case-3-styles-validation' + # TODO: needs refactoring of libkomwm.validation_errors_count to have a list + # of validation errors. + self.assertTrue(True) diff --git a/tests/testMapCSS.py b/tests/testMapCSS.py new file mode 100644 index 0000000..c5ffa6d --- /dev/null +++ b/tests/testMapCSS.py @@ -0,0 +1,364 @@ +import unittest +import sys +from pathlib import Path + +# Add `src` directory to the import paths +sys.path.insert(0, str(Path(__file__).parent.parent / 'src')) + +from mapcss import parseDeclaration, MapCSS + + +class MapCSSTest(unittest.TestCase): + def test_declarations(self): + decl = parseDeclaration(""" linejoin: round; """) + self.assertEqual(len(decl), 1) + self.assertEqual(decl[0], {"linejoin": "round"}) + + decl = parseDeclaration("""\tlinejoin :\nround ; """) + self.assertEqual(len(decl), 1) + self.assertEqual(decl[0], {"linejoin": "round"}) + + decl = parseDeclaration(""" icon-image: parking_private-s.svg; text: "name"; """) + self.assertEqual(len(decl), 1) + self.assertEqual(decl[0], { + "icon-image": "parking_private-s.svg", + "text": "name" + }) + + decl = parseDeclaration(""" + pattern-offset: 90\t; + pattern-image:\tarrow-m.svg ; + pattern-spacing: @trunk0 ;""") + self.assertEqual(len(decl), 1) + self.assertEqual(decl[0], { + "pattern-offset": "90", + "pattern-image": "arrow-m.svg", + "pattern-spacing": "@trunk0", + }) + + def test_parse_variables(self): + parser = MapCSS() + parser.parse(""" +@city_label: #999999; +@country_label: #444444; +@wave_length: 25; +""") + self.assertEqual(parser.variables, { + "city_label": "#999999", + "country_label": "#444444", + "wave_length": "25" + }) + + def test_parse_colors(self): + parser = MapCSS() + parser.parse(""" +@city_label : #999999; +@country_label: #444444 ; + @wave_length: 25; +""") + self.assertEqual(parser.variables, { + "city_label": "#999999", + "country_label": "#444444", + "wave_length": "25" + }) + + def test_parse_import(self): + parser = MapCSS() + mapcssFile = Path(__file__).parent / 'assets' / 'case-1-import' / 'main.mapcss' + parser.parse(filename=str(mapcssFile)) + + colors = parser.get_colors() + self.assertEqual(colors, { + "GuiText-color": (1.0, 1.0, 1.0), + "GuiText-opacity": 0.7, + "Route-color": (0.0, 0.0, 1.0), + "Route-opacity": 0.5, + }) + + def test_parse_basic_chooser(self): + parser = MapCSS() + static_tags = {"tourism": True, "office": True, + "craft": True, "amenity": True} + parser.parse(""" +node|z17-[tourism], +area|z17-[tourism], +node|z18-[office], +area|z18-[office], +node|z18-[craft], +area|z18-[craft], +node|z19-[amenity], +area|z19-[amenity], +{text: name; text-color: #000030; text-offset: 1;} +""", static_tags=static_tags) + + self.assertEqual(len(parser.choosers), 1) + self.assertEqual(len(parser.choosers[0].ruleChains), 8) + + def test_parse_basic_chooser_2(self): + parser = MapCSS() + static_tags = {"highway": True} + parser.parse(""" +@trunk0: #FF7326; + +line|z6[highway=trunk], +line|z6[highway=motorway], +{color: @trunk0; opacity: 0.3;} +line|z7-9[highway=trunk], +line|z7-9[highway=motorway], +{color: @trunk0; opacity: 0.7;} +""", static_tags=static_tags) + + self.assertEqual(len(parser.choosers), 2) + self.assertEqual(len(parser.choosers[0].ruleChains), 2) + self.assertEqual(parser.choosers[0].ruleChains[0].subject, 'line') + self.assertEqual(parser.choosers[0].selzooms, [6, 6]) + self.assertEqual(parser.choosers[1].selzooms, [7, 9]) + + rule, object_id = parser.choosers[0].testChains({"highway": "trunk"}) + self.assertEqual(object_id, "::default") + + def test_parse_basic_chooser_3(self): + parser = MapCSS() + static_tags = {"addr:housenumber": True, "addr:street": False} + parser.parse(""" +/* Some Comment Here */ + +/* + This sample is borrowed from Organic Maps Basemap_label.mapcss file + */ +node|z18-[addr:housenumber][addr:street]::int_name +{text: int_name; text-color: #65655E; text-position: center;} +""", static_tags=static_tags) + + building_tags = {"building": "yes", "addr:housenumber": "12", "addr:street": "Baker street"} + + # Check that mapcss parsed correctly + self.assertEqual(len(parser.choosers), 1) + styleChooser = parser.choosers[0] + self.assertEqual(len(styleChooser.ruleChains), 1) + self.assertEqual(styleChooser.selzooms, [18, 19]) + rule, object_id = styleChooser.testChains(building_tags) + self.assertEqual(object_id, "::int_name") + + rule = styleChooser.ruleChains[0] + self.assertEqual(rule.subject, 'node') + self.assertEqual(rule.extract_tags(), {'addr:housenumber', 'addr:street'}) + + def test_parse_basic_chooser_class(self): + parser = MapCSS() + parser.parse(""" +way|z-13::* +{ + linejoin: round; +} +""") + + # Check that mapcss parsed correctly + self.assertEqual(len(parser.choosers), 1) + styleChooser = parser.choosers[0] + self.assertEqual(len(styleChooser.ruleChains), 1) + self.assertEqual(styleChooser.selzooms, [0, 13]) + rule, object_id = styleChooser.testChains({}) + self.assertEqual(object_id, "::*") + + rule = styleChooser.ruleChains[0] + self.assertEqual(rule.subject, 'way') + self.assertEqual(rule.extract_tags(), {'*'}) + + def test_parse_basic_chooser_class_2(self): + parser = MapCSS() + parser.parse(""" +way|z10-::* +{ + linejoin: round; +} +""") + + # Check that mapcss parsed correctly + self.assertEqual(len(parser.choosers), 1) + styleChooser = parser.choosers[0] + self.assertEqual(len(styleChooser.ruleChains), 1) + self.assertEqual(styleChooser.selzooms, [10, 19]) + rule, object_id = styleChooser.testChains({}) + self.assertEqual(object_id, "::*") + + rule = styleChooser.ruleChains[0] + self.assertEqual(rule.subject, 'way') + self.assertEqual(rule.extract_tags(), {'*'}) + + def test_parse_basic_chooser_colors(self): + parser = MapCSS() + parser.parse(""" +way|z-6::* +{ + linejoin: round; +} + +colors { + GuiText-color: #FFFFFF; + GuiText-opacity: 0.7; + MyPositionAccuracy-color: #FFFFFF; + MyPositionAccuracy-opacity: 0.06; + Selection-color: #FFFFFF; + Selection-opacity: 0.64; + Route-color: #0000FF; + RouteOutline-color: #00FFFF; +} +""") + + # Check that colors from mapcss parsed correctly + colors = parser.get_colors() + self.assertEqual(colors, { + "GuiText-color": (1.0, 1.0, 1.0), + "GuiText-opacity": 0.7, + "MyPositionAccuracy-color": (1.0, 1.0, 1.0), + "MyPositionAccuracy-opacity": 0.06, + "Selection-color": (1.0, 1.0, 1.0), + "Selection-opacity": 0.64, + "Route-color": (0.0, 0.0, 1.0), + "RouteOutline-color": (0.0, 1.0, 1.0) + }) + + def test_parser_choosers_tree(self): + parser = MapCSS() + static_tags = {"tourism": True, "office": True, + "craft": True, "amenity": True} + + parser.parse(""" +node|z17-[office=lawyer], +area|z17-[office=lawyer], +{text: name;text-color: #444444;text-offset: 1;font-size: 10;} + +node|z17-[tourism], +area|z17-[tourism], +node|z18-[office], +area|z18-[office], +node|z18-[craft], +area|z18-[craft], +node|z19-[amenity], +area|z19-[amenity], +{text: name; text-color: #000030; text-offset: 1;} + +node|z18-[office], +area|z18-[office], +node|z18-[craft], +area|z18-[craft], +{font-size: 11;} + +node|z17-[office=lawyer], +area|z17-[office=lawyer] +{icon-image: lawyer-m.svg;} +""", static_tags=static_tags) + + for obj_type in ["line", "area", "node"]: + parser.build_choosers_tree("tourism", obj_type, "tourism") + parser.build_choosers_tree("office", obj_type, "office") + parser.build_choosers_tree("craft", obj_type, "craft") + parser.build_choosers_tree("amenity", obj_type, "amenity") + + parser.finalize_choosers_tree() + + # Pick style for zoom = 17 + styles18 = parser.get_style("office", "node", {"office": "lawyer"}, + zoom=18, xscale=1, zscale=1, filter_by_runtime_conditions=False) + + self.assertEqual(len(styles18), 1), + self.assertEqual(styles18[0], {'object-id': '::default', + 'font-size': '11', + 'text': 'name', + 'text-color': (0, 0, 16*3/255), + 'text-offset': 1.0, + 'icon-image': 'lawyer-m.svg'}) + + # Pick style for zoom = 17 + styles17 = parser.get_style("office", "node", {"office": "lawyer"}, + zoom=17, xscale=1, zscale=1, filter_by_runtime_conditions=False) + + self.assertEqual(len(styles17), 1), + self.assertEqual(styles17[0], {'object-id': '::default', + 'font-size': '10', + 'text': 'name', + 'text-color': (68/255, 68/255, 68/255), + 'text-offset': 1.0, + 'icon-image': 'lawyer-m.svg'}) + + # Pick style for zoom = 15 + styles15 = parser.get_style("office", "node", {"office": "lawyer"}, + zoom=15, xscale=1, zscale=1, filter_by_runtime_conditions=False) + + self.assertEqual(styles15, []), + + def test_parser_choosers_tree_with_classes(self): + parser = MapCSS() + static_tags = {"highway": True} + + parser.parse(""" +line|z10-[highway=motorway]::shield, +line|z10-[highway=trunk]::shield, +line|z10-[highway=motorway_link]::shield, +line|z10-[highway=trunk_link]::shield, +line|z10-[highway=primary]::shield, +line|z11-[highway=primary_link]::shield, +line|z12-[highway=secondary]::shield, +line|z13-[highway=tertiary]::shield, +line|z15-[highway=residential]::shield, +{ + shield-font-size: 9; + shield-text-color: #000000; + shield-text-halo-radius: 0; + shield-color: #FFFFFF; + shield-outline-radius: 1; +} + +line|z12-[highway=residential], +line|z12-[highway=tertiary], +line|z18-[highway=tertiary_link] +{ + text: name; + text-color: #333333; + text-halo-opacity: 0.8; + text-halo-radius: 1; +} + +line|z12-13[highway=residential], +line|z12-13[highway=tertiary] +{ + font-size: 12; + text-color: #444444; +} +""", static_tags=static_tags) + + parser.build_choosers_tree("highway", "line", "highway") + parser.finalize_choosers_tree() + + # Pick style for zoom = 10 + styles10 = parser.get_style("highway", "line", {"highway": "primary"}, + zoom=10, xscale=1, zscale=1, filter_by_runtime_conditions=False) + + self.assertEqual(len(styles10), 1), + self.assertEqual(styles10[0], {'object-id': '::shield', + 'shield-font-size': '9', + 'shield-text-color': (0.0, 0.0, 0.0), + 'shield-text-halo-radius': 0.0, + 'shield-color': (1.0, 1.0, 1.0), + 'shield-outline-radius': 1.0}) + + # Pick style for zoom = 15. Expecting two `object-id` values: '::shield' and '::default' + styles15 = parser.get_style("highway", "line", {"highway": "tertiary"}, + zoom=15, xscale=1, zscale=1, filter_by_runtime_conditions=False) + + self.assertEqual(len(styles15), 2), + self.assertEqual(styles15[0], {'object-id': '::shield', + 'shield-font-size': '9', + 'shield-text-color': (0.0, 0.0, 0.0), + 'shield-text-halo-radius': 0.0, + 'shield-color': (1.0, 1.0, 1.0), + 'shield-outline-radius': 1.0}) + self.assertEqual(styles15[1], {'object-id': '::default', + 'text': 'name', + 'text-color': (51/255, 51/255, 51/255), + 'text-halo-opacity': 0.8, + 'text-halo-radius': 1.0}) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testRule.py b/tests/testRule.py new file mode 100644 index 0000000..f3eec89 --- /dev/null +++ b/tests/testRule.py @@ -0,0 +1,114 @@ +import unittest +import sys +from pathlib import Path + +# Add `src` directory to the import paths +sys.path.insert(0, str(Path(__file__).parent.parent / 'src')) + +from mapcss.Rule import Rule +from mapcss.Condition import Condition +from mapcss import parseCondition + +class RuleTest(unittest.TestCase): + def test_rule_subject(self): + self.assertEqual(Rule().subject, "") + self.assertEqual(Rule("*").subject, "") + self.assertEqual(Rule("way").subject, "way") + self.assertEqual(Rule("area").subject, "area") + self.assertEqual(Rule("node").subject, "node") + self.assertEqual(Rule("planet").subject, "planet") + + def test_rule_type_matches(self): + self.assertCountEqual(Rule().type_matches, ('area', 'line', 'way', 'node')) + self.assertCountEqual(Rule("*").type_matches, ('area', 'line', 'way', 'node')) + self.assertCountEqual(Rule("way").type_matches, ('area', 'line', 'way')) + self.assertCountEqual(Rule("area").type_matches, ('area', 'way')) + self.assertCountEqual(Rule("node").type_matches, ('node', )) + self.assertCountEqual(Rule("planet").type_matches, set()) + + def test_rule_with_conditions(self): + rule = Rule() + rule.conditions = [ + parseCondition("aeroway=aerodrome"), + parseCondition("aerodrome=international") + ] + + tt = rule.test({ + "aeroway": "aerodrome", + "aerodrome": "international", + "name": "JFK" + }) + self.assertTrue(tt) + self.assertEqual(tt, "::default") + + self.assertCountEqual(rule.extract_tags(), ["aeroway", "aerodrome"]) + + # Negative test cases + self.assertFalse(rule.test({ + "aeroway": "aerodrome", + "name": "JFK" + })) + + def test_rule_with_class(self): + rule = Rule() + rule.conditions = [ + parseCondition("highway=unclassified"), + parseCondition("bridge?"), + Condition("eq", ("::class", "::bridgeblack")) + ] + + tt = rule.test({ + "highway": "unclassified", + "bridge": "yes", + "layer": "1" + }) + self.assertTrue(tt) + self.assertEqual(tt, "::bridgeblack") + + self.assertCountEqual(rule.extract_tags(), ["highway", "bridge"]) + + # Negative test cases + self.assertFalse(rule.test({ + "highway": "unclassified", + "bridge": "no", + "layer": "1" + })) + self.assertFalse(rule.test({ + "highway": "unclassified", + "tunnel": "yes", + "layer": "-1" + })) + + def test_tags_from_rule_with_class(self): + # Class condition doesn't add new tags + rule = Rule() + rule.conditions = [ + parseCondition("highway=unclassified"), + parseCondition("bridge?"), + Condition("eq", ("::class", "::bridgeblack")), + ] + + self.assertCountEqual(rule.extract_tags(), ["highway", "bridge"]) + + # Class condition doesn't add new tags + rule = Rule() + rule.conditions = [ + parseCondition("highway=unclassified"), + Condition("eq", ("::class", "::*")), + parseCondition("bridge?"), + ] + + self.assertCountEqual(rule.extract_tags(), ["highway", "bridge"]) + + # BUT having class as a first item overrides all the others + rule = Rule() + rule.conditions = [ + Condition("eq", ("::class", "::int_name")), + parseCondition("highway=unclassified"), + parseCondition("bridge?"), + ] + + self.assertCountEqual(rule.extract_tags(), ["*"]) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testStyleChooser.py b/tests/testStyleChooser.py new file mode 100644 index 0000000..a359e58 --- /dev/null +++ b/tests/testStyleChooser.py @@ -0,0 +1,297 @@ +import unittest +import sys +from pathlib import Path + +from mapcss.Rule import Rule + +# Add `src` directory to the import paths +sys.path.insert(0, str(Path(__file__).parent.parent / 'src')) + +from mapcss import parseCondition, Condition +from mapcss.Eval import Eval +from mapcss.StyleChooser import StyleChooser, make_nice_style + + +class StyleChooserTest(unittest.TestCase): + def test_rules_chain(self): + sc = StyleChooser((0, 16)) + + sc.newObject() + sc.addCondition(parseCondition("highway=footway")) + sc.addCondition(parseCondition("footway=sidewalk")) + + sc.newObject() + sc.addCondition(parseCondition("highway=footway")) + sc.addCondition(parseCondition("footway=crossing")) + sc.addCondition(Condition("eq", ("::class", "::*"))) + + self.assertTrue( sc.testChains({ "highway": "footway", "footway": "sidewalk" }) ) + self.assertTrue( sc.testChains({ "highway": "footway", "footway": "crossing" }) ) + self.assertFalse( sc.testChains({ "highway": "footway"}) ) + self.assertFalse( sc.testChains({ "highway": "residential", "footway": "crossing" }) ) + + rule1, tt = sc.testChains({ "highway": "footway", "footway": "sidewalk" }) + self.assertEqual(tt, "::default") + + rule2, tt = sc.testChains({ "highway": "footway", "footway": "crossing" }) + self.assertEqual(tt, "::*") + + self.assertNotEqual(rule1, rule2) + + def test_zoom(self): + sc = StyleChooser((0, 16)) + + sc.newObject() + sc.addZoom( (10, 19) ) + sc.addCondition(parseCondition("railway=station")) + sc.addCondition(parseCondition("transport=subway")) + sc.addCondition(parseCondition("city=yerevan")) + + sc.newObject() + sc.addZoom( (4, 15) ) + sc.addCondition(parseCondition("railway=station")) + sc.addCondition(parseCondition("transport=subway")) + sc.addCondition(parseCondition("city=yokohama")) + + rule1, tt = sc.testChains({ "railway": "station", "transport": "subway", "city": "yerevan" }) + self.assertEqual(rule1.minZoom, 10) + self.assertEqual(rule1.maxZoom, 19) + + rule2, tt = sc.testChains({ "railway": "station", "transport": "subway", "city": "yokohama" }) + self.assertEqual(rule2.minZoom, 4) + self.assertEqual(rule2.maxZoom, 15) + + def test_extract_tags(self): + sc = StyleChooser((0, 16)) + + sc.newObject() + sc.addCondition(parseCondition("aerialway=rope_tow")) + + sc.newObject() + sc.addCondition(parseCondition("piste:type=downhill")) + + self.assertSetEqual(sc.extract_tags(), {"aerialway", "piste:type"}) + + sc = StyleChooser((0, 16)) + + sc.newObject() + sc.addCondition(parseCondition("aeroway=terminal")) + sc.addCondition(parseCondition("building")) + + sc.newObject() + sc.addCondition(parseCondition("waterway=dam")) + sc.addCondition(parseCondition("building:part")) + + self.assertSetEqual(sc.extract_tags(), {"waterway", "building:part", "building", "aeroway"}) + + def test_make_nice_style(self): + style = make_nice_style({ + "outline-color": "none", + "bg-color": "red", + "dash-color": "#ffff00", + "front-color": "rgb(0, 255, 255)", + "line-width": Eval("""eval(min(tag("line_width"), 10))"""), + "outline-width": "2.5", + "arrow-opacity": "0.5", + "offset-2": "20", + "border-radius": "4", + "line-extrude": "16", + "dashes": "3,3,1.5,3", + "wrong-dashes": "yes, yes, yes, no", + "make-nice": True, + "additional-len": 44.5 + }) + + expectedStyle = { + "outline-color": "none", + "bg-color": (1.0, 0.0, 0.0), + "dash-color": (1.0, 1.0, 0.0), + "front-color": (0.0, 1.0, 1.0), + "line-width": Eval("""eval(min(tag("line_width"), 10))"""), + "outline-width": 2.5, + "arrow-opacity": 0.5, + "offset-2": 20.0, + "border-radius": 4.0, + "line-extrude": 16.0, + "dashes": [3.0, 3.0, 1.5, 3.0], + "wrong-dashes": [], + "make-nice": True, + "additional-len": 44.5 + } + + self.assertEqual(style, expectedStyle) + + def test_add_styles(self): + sc = StyleChooser((15, 19)) + sc.newObject() + sc.addStyles([{ + "width": "1.3", + "opacity": "0.6", + "bg-color": "blue" + }]) + sc.addStyles([{ + "color": "#FFFFFF", + "casing-width": "+10" + }]) + + self.assertEqual(len(sc.styles), 2) + self.assertEqual(sc.styles[0], { + "width": 1.3, + "opacity": 0.6, + "bg-color": (0.0, 0.0, 1.0) + }) + self.assertEqual(sc.styles[1], { + "color": (1.0, 1.0, 1.0), + "casing-width": 5.0 + }) + + def test_update_styles(self): + styles = [{"primary_color": (1.0, 1.0, 1.0)}] + + sc = StyleChooser((15, 19)) + sc.newObject() + sc.addStyles([{ + "width": "1.3", + "opacity": "0.6", + "bg-color": """eval( prop("primary_color") )""", # Check that property from `styles` is applied + "text-offset": """eval( cond( boolean(tag("oneway")), 10, 5) )""" # Check that tags are applied + }]) + + object_tags = {"highway": "service", + "oneway": "yes"} + new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False) + expected_new_styles = { + "width": 1.3, + "opacity": 0.6, + "bg-color": (1.0, 1.0, 1.0), + "text-offset": 10.0, + "object-id": "::default" + } + + self.assertEqual(len(new_styles), 2) + self.assertEqual(new_styles[-1], expected_new_styles) + + def test_update_styles_2(self): + styles = [] + + sc = StyleChooser((15, 19)) + + sc.newObject() + sc.addCondition(Condition("eq", ("::class", "::int_name") )) # Class should be added to the style + sc.addCondition(parseCondition("oneway?")) + + sc.addStyles([{ + "width": "1.3", + "bg-color": "black" + }]) + + object_tags = {"highway": "service", "oneway": "yes"} + new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False) + expected_new_styles = { + "width": 1.3, + "bg-color": (0.0, 0.0, 0.0), + "object-id": "::int_name" # Check that class from sc.ruleChains is added to the style + } + + self.assertEqual(len(new_styles), 1) + self.assertEqual(new_styles[-1], expected_new_styles) + + + def test_update_styles_by_class(self): + # Predefined styles + styles = [{ + "some-width": 2.5, + "object-id": "::flats" + }, + { + "some-width": 3.5, + "object-id": "::bridgeblack" + }, + { + "some-width": 4.5, + "object-id": "::default" + }] + + sc = StyleChooser((15, 19)) + + sc.newObject() + sc.addCondition(Condition("eq", ("::class", "::flats") )) # `sc` styles should apply only to `::flats` class + sc.addCondition(parseCondition("oneway?")) + + sc.newObject() + sc.addCondition(Condition("eq", ("::class", "::bridgeblack") )) # This class is ignored by StyleChooser + sc.addCondition(parseCondition("oneway?")) + + sc.addStyles([{ + "some-width": "1.5", + "other-offset": "4" + }]) + + object_tags = {"highway": "service", "oneway": "yes"} + + # Apply new style to predefined styles with filter by class + new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False) + + expected_new_styles = [{ # The first style changes + "some-width": 1.5, + "other-offset": 4.0, + "object-id": "::flats" + }, + { # Style not changed (class is not `::flats`) + "some-width": 3.5, + "object-id": "::bridgeblack" + }, + { # Style not changed (class is not `::flats`) + "some-width": 4.5, + "object-id": "::default" + }] + + self.assertEqual(len(new_styles), 3) + self.assertEqual(new_styles, expected_new_styles) + + + def test_update_styles_by_class_all(self): + # Predefined styles + styles = [{ # This is applied to StyleChooser styles + "some-width": 2.5, + "corner-radius": 2.5, + "object-id": "::*" + }, + { + "some-width": 3.5, + "object-id": "::bridgeblack" + }] + + sc = StyleChooser((15, 19)) + + sc.newObject() + sc.addCondition(parseCondition("tunnel")) + + sc.addStyles([{ + "some-width": "1.5", + "other-offset": "4" + }]) + object_tags = {"highway": "service", "tunnel": "yes"} + + # Apply new style to predefined styles with filter by class + new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False) + + # Check that new style with new `object-id` is added. + # This style is built from `styles[0]` and styles from `sc` + expected_new_style = { + "some-width": 1.5, + "corner-radius": 2.5, + "other-offset": 4.0, + "object-id": "::default" # New class, never listed in `styles` + } + + self.assertEqual(len(new_styles), 3) + self.assertEqual(new_styles[-1], expected_new_style) + + + def test_runtime_conditions(self): + # TODO: Create test with sc.addRuntimeCondition(Condition(condType, ('extra_tag', cond))) + pass + +if __name__ == '__main__': + unittest.main()