Added unit tests and integration tests

This commit is contained in:
Sergiy Kozyr 2024-12-13 15:14:22 +02:00 committed by Sergiy Kozyr
parent 2850ec5077
commit 70301732f9
28 changed files with 2038 additions and 0 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ src/tiles/
*pycache*
*swp
*bak
/.idea

View file

@ -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.

View file

View file

@ -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()

0
tests/__init__.py Normal file
View file

View file

@ -0,0 +1,6 @@
colors {
GuiText-color: #FFFFFF;
GuiText-opacity: 0.7;
Route-color: #0000FF;
Route-opacity: 0.5;
}

View file

@ -0,0 +1 @@
@import("import2.mapcss");

View file

@ -0,0 +1 @@
@import("colors.mapcss");

View file

@ -0,0 +1 @@
@import("import1.mapcss");

View file

@ -0,0 +1,7 @@
classificator.txt
colors.txt
patterns.txt
style.bin.bin
style.bin.txt
types.txt
visibility.txt

View file

@ -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;}

View file

@ -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;}

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;
}

View file

@ -0,0 +1,4 @@
population
name
bbox_area
rating

View file

@ -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;
Can't render this file because it has a wrong number of fields in line 38.

View file

@ -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.

View file

@ -0,0 +1 @@
Files for testLibkomwm.test_generate_drules_validation_errors() method.

300
tests/testCondition.py Normal file
View file

@ -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()

124
tests/testEval.py Normal file
View file

@ -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()

68
tests/testLibkomwm.py Normal file
View file

@ -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)

364
tests/testMapCSS.py Normal file
View file

@ -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()

114
tests/testRule.py Normal file
View file

@ -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()

297
tests/testStyleChooser.py Normal file
View file

@ -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()