Compare commits

...
Sign in to create a new pull request.

46 commits

Author SHA1 Message Date
6e17463575 Fixed regex condition. Fixed unclosed files.
Added TODO comments
2024-12-13 18:21:11 +02:00
70301732f9 Added unit tests and integration tests 2024-12-13 18:21:11 +02:00
2850ec5077 Require protobuf 3.21+
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2024-12-02 01:02:50 +03:00
2796db7ae3 Allow <1 dashdot values
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-11-25 22:17:59 -03:00
b9d308f0cd Generate drules for z20
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-10-16 12:24:06 -03:00
51864cec29 Validate presence of text-color and text-offset attributes
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-10-14 17:48:59 -03:00
611abc0d72 Validate pathtexts and shields without lines
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-09-16 19:55:51 -03:00
5b22c7a14c Remove int_name, add a warning for invalid dashdot size
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-09-16 17:27:36 -03:00
d58e496bd4 Make shields prioritizable independently
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-09-07 16:34:11 -03:00
432e0b0d67 Fail on missing priorities errors
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-09-06 13:49:57 -03:00
ff3f8324f8 Fix fill-color: none; handling.
Fixes #19.

Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-24 07:13:19 +02:00
feb3b87800 Add a ': none;' syntax for disabling area and icon drules
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-22 23:10:17 -03:00
Andrew Shkrob
ac01664b98 [3party] Update protobuf to v4.23.4
Signed-off-by: Andrew Shkrob <andrew.shkrob.social@yandex.by>
2023-08-21 10:44:37 -03:00
e256a119ab Display drules with automatic priorities
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-18 18:01:11 -03:00
dea0efb9ee Sort drule types in comments in logical order
And some other minor changes.

Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-18 18:01:11 -03:00
04f5f44137 Allow setting negative overlays priorities to e.g. put icons below automatic optional captions
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-14 12:49:03 -03:00
24b8586c23 Update comments
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-11 20:16:16 -03:00
024810fb85 Validate visibilities
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
a03d74f588 Load priorities from files without explicit drule types specified
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
0e23a0e798 Remove drule types from prio.txt files
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
fbecca40c3 Make optional captions below other overlays 2023-08-07 11:59:25 -03:00
2d5c553af8 Add comments with visibility range and other drules info
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
b30c3bee84 Load grouped priorities
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
41e9860567 Group types with same priorities
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
98755e5d6a Add auto-fixing of captions/pathtexts priorities being higher than icon/shield
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
8c5e152621 Disable priorities compression by default
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
9c914f097b Compress / re-space priorities evenly
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
58dcaaba51 Load priorities from *.prio.txt files
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
0850cde772 Dump priorities into separate *.prio.txt files
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
b34963026a Calculate minVisibleScale for overlays only
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
492906e130 Prepend minVisibleScale to priority values
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
8e8f17bd29 Remove unused zoom 0 from drules output
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
ebe1ced85c Remove empty casing dashdot definitions
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-07-08 15:20:54 -03:00
2f311e7504 Add a bg-top priorities range
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-06-23 09:21:13 -03:00
dbba1c41e0 Allow comments in mapcss-mapping.csv
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-06-20 21:56:13 +02:00
d580b748f2 Make captions optional by default
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-03-26 12:08:57 -03:00
5b160e185f Remove apply_for_type symbol attribute
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-15 11:33:00 -03:00
9b38690e69 Optimize choosers by discarding non-matching rules
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
27b41b5e3f Add zoom into choosers optimization tree
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
70fa78f39b Optimize frequently called functions
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
41498a6ec5 Process unique runtime conditions once only
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
4e29892282 Optimize looking for runtime conditions in selectors
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
a6daa7121b Make per-tag sets of selectors more precise and small
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-01-21 12:57:14 -03:00
471abbd754 Look for missing base_width to use with casing-width
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-01-20 11:29:46 -03:00
32cc5aafb6 Add casing-width-add support
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-01-20 11:29:46 -03:00
Viktor Govako
f9de08f90f
Merge pull request #4 from organicmaps/vng-fix
Don't mix "casing-" for line and area rules.
2023-01-17 17:30:35 -03:00
37 changed files with 2804 additions and 1410 deletions

1
.gitignore vendored
View file

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

4
README
View file

@ -1,4 +0,0 @@
Kothic Map Renderer, patched for MWM use.
python src/komap.py -r mapswithme -s [path to repo]/data/styles/normal.mapcss -o [path to repo]/data/drules_proto

37
README.md Normal file
View file

@ -0,0 +1,37 @@
Kothic Mapcss parser/processor tailored for Organic Maps use.
Dependencies:
* Python >= 3.8
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()

3
requirements.txt Normal file
View file

@ -0,0 +1,3 @@
# The core is using protobuf 3.3.0 still (3party/protobuf/), so no point to require newer versions.
# E.g. Ubuntu 24.04 LTS ships with python3-protobuf 3.21.12 and it works fine.
protobuf~=3.21.0

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,13 @@ from mapcss import MapCSS
from optparse import OptionParser
import os
import csv
import sys
import functools
from sys import exit
from itertools import chain
from multiprocessing import Pool, set_start_method
from collections import OrderedDict
import mapcss.webcolors
from drules_struct_pb2 import *
whatever_to_hex = mapcss.webcolors.webcolors.whatever_to_hex
whatever_to_cairo = mapcss.webcolors.webcolors.whatever_to_cairo
@ -14,19 +16,81 @@ whatever_to_cairo = mapcss.webcolors.webcolors.whatever_to_cairo
PROFILE = False
MULTIPROCESSING = True
# If path to the protobuf EGG is specified then apply it before import drules_struct_pb2
PROTOBUF_EGG_PATH = os.environ.get("PROTOBUF_EGG_PATH")
if PROTOBUF_EGG_PATH:
# another version of protobuf may be installed, override it
for i in range(len(sys.path)):
if -1 != sys.path[i].find("protobuf-"):
sys.path[i] = PROTOBUF_EGG_PATH
sys.path.append(PROTOBUF_EGG_PATH)
# Priority values defined in *.prio.txt files are adjusted
# to fit into the following "priorities ranges":
# [-10000; 10000): overlays (icons, captions...)
# [0; 1000) : FG - foreground areas and lines
# [-1000; 0) : BG-top - water, linear and areal, rendered just on top of landcover
# (-2000; -1000) : BG-by-size - landcover areas, later in core sorted by their bbox size
# The core renderer then re-adjusts those ranges as necessary to accomodate
# for special behavior and features' layer=* values.
# See drape_frontend/stylist.cpp for the details of layering logic.
from drules_struct_pb2 import *
# Priority range for area and line drules. Should be same as drule::kLayerPriorityRange.
LAYER_PRIORITY_RANGE = 1000
# Should be same as drule::kOverlaysMaxPriority. The overlays range is [-kOverlaysMaxPriority; kOverlaysMaxPriority),
# negative values are used for optional captions which are below most other overlays.
OVERLAYS_MAX_PRIORITY = 10000
WIDTH_SCALE = 1.0
# Drules are arranged into following ranges.
PRIO_OVERLAYS = 'overlays'
PRIO_FG = 'FG'
PRIO_BG_TOP = 'BG-top'
PRIO_BG_BY_SIZE = 'BG-by-size'
prio_ranges = {
PRIO_OVERLAYS: {'pos': 4, 'base': 0, 'priorities': {}},
PRIO_FG: {'pos': 3, 'base': 0, 'priorities': {}},
PRIO_BG_TOP: {'pos': 2, 'base': -1000, 'priorities': {}},
PRIO_BG_BY_SIZE: {'pos': 1, 'base': -2000, 'priorities': {}},
}
visibilities = {}
prio_ranges[PRIO_OVERLAYS]['comment'] = f'''
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 ({OVERLAYS_MAX_PRIORITY}) is subtracted from their priorities automatically).
'''
prio_ranges[PRIO_FG]['comment'] = '''
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).
'''
prio_ranges[PRIO_BG_TOP]['comment'] = '''
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.
'''
prio_ranges[PRIO_BG_BY_SIZE]['comment'] = '''
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.
'''
COMMENT_AUTOFORMAT = '''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.
'''
COMMENT_RANGES_OVERVIEW = '''
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
'''
# TODO: Implement better error handling
validation_errors_count = 0
def to_boolean(s):
s = s.lower()
@ -41,6 +105,9 @@ def mwm_encode_color(colors, st, prefix='', default='black'):
if prefix:
prefix += "-"
opacity = hex(255 - int(255 * float(st.get(prefix + "opacity", 1))))
# TODO: Refactoring idea: here color is converted from float to hex. While MapCSS class
# reads colors from *.mapcss files and converts to float. How about changing MapCSS
# to keep hex values and avoid Hex->Float->Hex operations?
color = whatever_to_hex(st.get(prefix + 'color', default))[1:]
result = int(opacity + color, 16)
colors.add(result)
@ -55,6 +122,7 @@ def mwm_encode_image(st, prefix='icon', bgprefix='symbol'):
return False
# strip last ".svg"
handle = st.get(prefix + "image")[:-4]
# TODO: return `handle` only once
return handle, handle
@ -72,21 +140,33 @@ def query_style(args):
results = []
for zoom in range(minzoom, maxzoom + 1):
runtime_conditions_arr = []
all_runtime_conditions_arr = []
# Get runtime conditions which are used for class 'cl' on zoom 'zoom'
if "area" not in cltags:
runtime_conditions_arr.extend(style.get_runtime_rules(clname, "line", cltags, zoom))
runtime_conditions_arr.extend(style.get_runtime_rules(clname, "area", cltags, zoom))
all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "line", cltags, zoom))
all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "area", cltags, zoom))
if "area" not in cltags:
runtime_conditions_arr.extend(style.get_runtime_rules(clname, "node", cltags, zoom))
all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "node", cltags, zoom))
# If there is no any runtime conditions, do not filter style by runtime conditions
if len(runtime_conditions_arr) == 0:
runtime_conditions_arr = []
if len(all_runtime_conditions_arr) == 0:
# If there is no runtime conditions, do not filter style by runtime conditions
runtime_conditions_arr.append(None)
elif len(all_runtime_conditions_arr) == 1:
runtime_conditions_arr = all_runtime_conditions_arr
else:
# Keep unique conditions only
runtime_conditions_arr.append(all_runtime_conditions_arr.pop(0))
for new_rt_conditions in all_runtime_conditions_arr:
conditions_unique = True
for rt_conditions in runtime_conditions_arr:
if new_rt_conditions == rt_conditions:
conditions_unique = False
break
if conditions_unique:
runtime_conditions_arr.append(new_rt_conditions)
for runtime_conditions in runtime_conditions_arr:
has_icons_for_areas = False
zstyle = {}
# Get style for class 'cl' on zoom 'zoom' with corresponding runtime conditions
@ -94,19 +174,292 @@ def query_style(args):
linestyle = style.get_style_dict(clname, "line", cltags, zoom, olddict=zstyle, filter_by_runtime_conditions=runtime_conditions)
zstyle = linestyle
areastyle = style.get_style_dict(clname, "area", cltags, zoom, olddict=zstyle, filter_by_runtime_conditions=runtime_conditions)
for st in list(areastyle.values()):
if "icon-image" in st or 'symbol-shape' in st or 'symbol-image' in st:
has_icons_for_areas = True
break
zstyle = areastyle
if "area" not in cltags:
nodestyle = style.get_style_dict(clname, "node", cltags, zoom, olddict=zstyle, filter_by_runtime_conditions=runtime_conditions)
zstyle = nodestyle
results.append((cl, zoom, has_icons_for_areas, runtime_conditions, list(zstyle.values())))
results.append((cl, zoom, runtime_conditions, list(zstyle.values())))
return results
def get_priorities_filename(prio_range, path):
return os.path.join(path, f'priorities_{prio_ranges[prio_range]["pos"]}_{prio_range}.prio.txt')
def load_priorities(prio_range, path, classif, compress = False):
def print_warning(msg):
print(f'WARNING: {msg} in {fname}:\n\t{line}')
priority_max = OVERLAYS_MAX_PRIORITY if prio_range == PRIO_OVERLAYS else LAYER_PRIORITY_RANGE
priority_min = -OVERLAYS_MAX_PRIORITY if prio_range == PRIO_OVERLAYS else 0
fname = get_priorities_filename(prio_range, path)
with open(fname, 'r') as f:
group = []
for line in f:
line = line.strip()
# Strip comments.
line = line.split('#', 1)[0].strip()
if not line:
continue
tokens = line.split()
if len(tokens) > 2:
print_warning('skipping malformed line')
continue
if tokens[0] == "===":
try:
priority = int(tokens[1])
except ValueError:
print_warning('skipping invalid priority value')
else:
if priority >= priority_min and priority < priority_max:
if len(group):
for key in group:
prio_ranges[prio_range]['priorities'][key] = priority
else:
print_warning('skipping empty priority group')
else:
print_warning(f'skipping out of [{priority_min};{priority_max}) range priority value')
group = []
else:
cl = tokens[0]
object_id = ''
oid_pos = cl.find('::')
if oid_pos != -1:
object_id = cl[oid_pos:]
cl = cl[0:oid_pos]
if cl not in classif:
print_warning('unknown classificator type')
key = (cl, object_id)
if key in prio_ranges[prio_range]['priorities']:
print_warning(f'overriding previously set priority value {prio_ranges[prio_range]["priorities"][key]}')
group.append(key)
if len(group):
line = group
print_warning(f'skipping last types groups with no priority set')
if prio_range == PRIO_OVERLAYS:
for key in prio_ranges[PRIO_OVERLAYS]['priorities'].keys():
main_prio_id = None
if key[1].startswith('caption'):
main_prio_id = (key[0], key[1].replace('caption', 'icon'))
if key[1].startswith('pathtext'):
main_prio_id = (key[0], key[1].replace('pathtext', 'shield'))
if main_prio_id is not None and main_prio_id in prio_ranges[PRIO_OVERLAYS]['priorities']:
main_prio = prio_ranges[PRIO_OVERLAYS]['priorities'][main_prio_id]
if prio_ranges[PRIO_OVERLAYS]['priorities'][key] > main_prio:
print(f'WARNING: {key} priority is higher than {main_prio_id}, making it equal')
prio_ranges[PRIO_OVERLAYS]['priorities'][key] = main_prio
# TODO: update compression logic to handle icons put inbetween automatic optional captions priorities.
if compress:
print(f'Compressing {prio_range} priorities into a (0;{priority_max}) range:')
unique_prios = set(prio_ranges[prio_range]['priorities'].values())
print(f'\tunique priorities values: {len(unique_prios)}')
# Keep gaps at the range borders.
base_idx = 1
if 0 not in unique_prios:
base_idx = 0
unique_prios.add(0)
unique_prios.add(priority_max)
step = min(priority_max / len(unique_prios), 10)
print(f'\tnew step between priorities: {step}')
unique_prios = sorted(unique_prios)
for prio_id in prio_ranges[prio_range]['priorities'].keys():
idx = unique_prios.index(prio_ranges[prio_range]['priorities'][prio_id])
prio_ranges[prio_range]['priorities'][prio_id] = int(step * (base_idx + idx))
def store_visibility(cl, dr_type, object_id, zoom, auto_comment = None):
if object_id == '::default':
object_id = ''
dr_type_comment = (dr_type, auto_comment)
if cl not in visibilities:
visibilities[cl] = {}
if dr_type_comment not in visibilities[cl]:
visibilities[cl][dr_type_comment] = {}
if object_id not in visibilities[cl][dr_type_comment]:
visibilities[cl][dr_type_comment][object_id] = set()
visibilities[cl][dr_type_comment][object_id].add(zoom)
def prettify_zooms(zooms, maxzoom):
def add_zrange(first, last, result, maxzoom):
first = str(first)
last = str(last)
if last == str(maxzoom):
zrange = first + '-'
elif first == last:
zrange = first
else:
zrange = first + '-' + last
if result != '':
result += ','
result += zrange
return result
zooms = sorted(zooms)
first = zooms.pop(0)
prev = first
result = ''
for zoom in zooms:
if zoom == prev + 1:
prev = zoom
else:
result = add_zrange(first, prev, result, maxzoom)
first = zoom
prev = zoom
return 'z' + add_zrange(first, prev, result, maxzoom)
def validate_visibilities(maxzoom):
for cl, dr_types_comments in visibilities.items():
for dr_type_comment, object_ids in dr_types_comments.items():
for object_id, zooms in object_ids.items():
zoom_range = prettify_zooms(zooms, maxzoom)
if zoom_range.find(',') != -1:
print(f'WARNING: non-contiguous visibility range {zoom_range} for {cl} {dr_type_comment}{object_id}')
dr_type = dr_type_comment[0]
icon_dr_type_comment = ('icon', None)
if (dr_type == 'caption' and icon_dr_type_comment in dr_types_comments and
object_id in dr_types_comments[icon_dr_type_comment]):
icon_zooms = sorted(dr_types_comments[icon_dr_type_comment][object_id])
if min(zooms) < icon_zooms[0]:
print(f'WARNING: caption {zoom_range} appears before icon {prettify_zooms(icon_zooms, maxzoom)}'
f' for {cl}{object_id}')
line_dr_type_comment = ('line', None)
if dr_type in ('pathtext', 'shield'):
lines_min_zoom = maxzoom + 1
if line_dr_type_comment in dr_types_comments:
lines_min_zoom = maxzoom + 1
for line_object_id, line_zooms in dr_types_comments[line_dr_type_comment].items():
min_zoom = min(line_zooms)
if min_zoom < lines_min_zoom:
lines_min_zoom = min_zoom
min_zoom = min(zooms)
if min_zoom < lines_min_zoom:
missing_zooms = prettify_zooms(range(min_zoom, lines_min_zoom), maxzoom)
print(f'ERROR: {dr_type} without line at {missing_zooms} for {cl}{object_id}')
global validation_errors_count
validation_errors_count += 1
def dump_priorities(prio_range, path, maxzoom):
with open(get_priorities_filename(prio_range, path), 'w') as outfile:
comment = COMMENT_AUTOFORMAT + prio_ranges[prio_range]['comment'] + COMMENT_RANGES_OVERVIEW
for s in comment.splitlines():
outfile.write(f'# {s}'.rstrip() + '\n')
outfile.write('\n')
if len(prio_ranges[prio_range]['priorities']):
dr_types_order = (('icon', 'caption', 'pathtext', 'shield', 'line', 'area') if prio_range == PRIO_OVERLAYS
else ('line', 'area', 'icon', 'caption', 'pathtext', 'shield'))
comment_auto_captions = '''
All automatic optional captions priorities are below 0.
They follow the order of their correspoding icons.
'''
prios = sorted(prio_ranges[prio_range]['priorities'].items(),
key = lambda item: (OVERLAYS_MAX_PRIORITY - item[1], item[0][0], item[0][1]))
group_prio = prios[0][1]
group = ''
group_comment = '# '
for p in prios:
if p[1] != group_prio:
if prio_range == PRIO_OVERLAYS and comment_auto_captions and group_prio < 0:
for s in comment_auto_captions.splitlines():
outfile.write(f'# {s.strip()}'.rstrip() + '\n')
outfile.write('\n')
comment_auto_captions = None
outfile.write(f'{group}{group_comment}=== {group_prio}\n\n')
group_prio = p[1]
group = ''
group_comment = '# '
cl = p[0][0]
object_id = p[0][1]
auto_dr_type = None
auto_comment = None
if len(p[0]) == 4:
auto_dr_type = p[0][2]
auto_comment = p[0][3]
line_drules = ''
other_drules = ''
if cl in visibilities:
for dr_type_comment in sorted(visibilities[cl].keys(), key = lambda drt: dr_types_order.index(drt[0])):
for oid in sorted(visibilities[cl][dr_type_comment].keys()):
dr_type, dr_auto_comment = dr_type_comment
dr_zoom = dr_type + oid
if dr_auto_comment is not None:
dr_zoom = f'{dr_zoom}({dr_auto_comment})'
dr_zoom += ' ' + prettify_zooms(visibilities[cl][dr_type_comment][oid], maxzoom)
# Drules matching this prio_range and object_id and
# - an auto priority dr_type match or
# - any other non-auto dr_type suitable
is_auto_dr_match = dr_type == auto_dr_type and dr_auto_comment == auto_comment
is_not_auto_dr = auto_dr_type is None and dr_auto_comment is None
is_suitable_for_range = (
(prio_range == PRIO_OVERLAYS and dr_type in ('icon', 'caption', 'pathtext', 'shield')) or
(prio_range in (PRIO_FG, PRIO_BG_TOP) and dr_type in ('line', 'area')) or
(prio_range == PRIO_BG_BY_SIZE and dr_type == 'area'))
if oid == object_id and (is_auto_dr_match or is_not_auto_dr and is_suitable_for_range):
if line_drules:
line_drules += ' and '
line_drules += dr_zoom
else:
# Drules from other prio_ranges or with other object_ids.
if other_drules:
other_drules += ', '
other_drules += dr_zoom
if object_id:
cl += object_id
if not line_drules:
if other_drules:
line_drules = "WARNING: no drule defined for the priority"
else:
line_drules = "WARNING: no style defined (the type will be not included into map data)"
print(f'{line_drules} for {cl} in {prio_range}')
info = '# ' + line_drules
if other_drules:
info += f' (also has {other_drules})'
if auto_dr_type is None:
group_comment = ''
else:
cl = '# ' + cl
group += f'{cl:50} {info}\n'
outfile.write(f'{group}{group_comment}=== {group_prio}\n')
def get_drape_priority(cl, dr_type, object_id, auto_dr_type = None, auto_comment = None, auto_prio_mod = 0):
if object_id == '::default':
object_id = ''
prio_id = (cl, object_id)
ranges_to_check = (PRIO_OVERLAYS, )
if dr_type == 'line':
ranges_to_check = (PRIO_FG, PRIO_BG_TOP)
elif dr_type == 'area':
ranges_to_check = (PRIO_BG_BY_SIZE, PRIO_BG_TOP, PRIO_FG)
for r in ranges_to_check:
if prio_id in prio_ranges[r]['priorities']:
priority = prio_ranges[r]['priorities'][prio_id]
if auto_dr_type is not None:
min_priority = -OVERLAYS_MAX_PRIORITY if r == PRIO_OVERLAYS else 0
priority = max(priority + auto_prio_mod, min_priority)
auto_prio_id = (cl, object_id, auto_dr_type, auto_comment)
prio_ranges[r]['priorities'][auto_prio_id] = priority
return priority + prio_ranges[r]['base']
print(f'ERROR: priority is not set for {dr_type} {cl}{object_id}')
global validation_errors_count
validation_errors_count += 1
return 0
# TODO: Split large function to smaller ones
def komap_mapswithme(options):
if options.data and os.path.isdir(options.data):
ddir = options.data
@ -117,6 +470,7 @@ def komap_mapswithme(options):
class_order = []
class_tree = {}
# TODO: Introduce new function to parse `colors.txt` for better testability
colors_file_name = os.path.join(ddir, 'colors.txt')
colors = set()
if os.path.exists(colors_file_name):
@ -125,6 +479,7 @@ def komap_mapswithme(options):
colors.add(int(colorLine))
colors_in_file.close()
# TODO: Introduce new function to parse `patterns.txt` for better testability
patterns = []
def addPattern(dashes):
if dashes and dashes not in patterns:
@ -140,32 +495,14 @@ def komap_mapswithme(options):
# Build classificator tree from mapcss-mapping.csv file
types_file = open(os.path.join(ddir, 'types.txt'), "w")
# Mapcss-mapping format
#
# A CSV table mapping tags to types. Some types can be deemed obsolete, either completely or replaced with a different type.
#
# Example row: highway|bus_stop;[highway=bus_stop];;name;int_name;22; (mind the last semicolon!)
# It contains:
# - type name: "highway|bus_stop" ('|' is converted to '-' internally)
# - mapcss selector for tags: "[highway=bus_stop]" (you can group selectors and use e.g. [oneway?])
# - "x" for an obsolete type or an empty cell otherwise
# - primary title tag (usually "name")
# - secondary title tag (usually "int_name")
# - type id, sequential starting from 1
# - replacement type for an obsolete tag, if exists
#
# A shorter format for above example: highway|bus_stop;22;
# It leaves only columns 1, 6 and 7. For obsolete types with no replacement put "x" into the last column.
# Obviously it works only for simple types that are produced from tags replacing '=' with '|'.
#
# An example of type with replacement:
# highway|unsurfaced|disused;[highway=unsurfaced][disused?];x;name;int_name;838;highway|unclassified
# The mapcss-mapping.csv format is described inside the file itself.
# TODO: introduce new function to parse 'mapcss-mapping.csv' for better testability
cnt = 1
unique_types_check = set()
for row in csv.reader(open(os.path.join(ddir, 'mapcss-mapping.csv')), delimiter=';'):
if len(row) <= 1:
# Allow for empty lines and comments that do not contain ';' symbol
mapping_file = open(os.path.join(ddir, 'mapcss-mapping.csv'))
for row in csv.reader(mapping_file, delimiter=';'):
if len(row) <= 1 or row[0].startswith('#'):
# Allow for empty lines and comment lines starting with '#'.
continue
if len(row) == 3:
# Short format: type name, type id, x / replacement type name
@ -210,8 +547,17 @@ def komap_mapswithme(options):
print("mapswithme", file=types_file)
class_tree[cl] = row[0]
class_order.sort()
mapping_file.close()
types_file.close()
output = ''
for prio_range in prio_ranges.keys():
load_priorities(prio_range, options.priorities_path, unique_types_check, compress = False)
output += f'{"" if not output else ", "}{len(prio_ranges[prio_range]["priorities"])} {prio_range}'
print(f'Loaded priorities: {output}.')
del unique_types_check
# Get all mapcss static tags which are used in mapcss-mapping.csv
# This is a dict with main_tag flags (True = appears first in types)
mapcss_static_tags = {}
@ -219,26 +565,34 @@ def komap_mapswithme(options):
for i, t in enumerate(v.keys()):
mapcss_static_tags[t] = mapcss_static_tags.get(t, True) and i == 0
# TODO: Introduce new function to parse `mapcss-dynamic.txt` for better testability
# Get all mapcss dynamic tags from mapcss-dynamic.txt
mapcss_dynamic_tags = set([line.rstrip() for line in open(os.path.join(ddir, 'mapcss-dynamic.txt'))])
with open(os.path.join(ddir, 'mapcss-dynamic.txt')) as dynamic_file:
mapcss_dynamic_tags = set([line.rstrip() for line in dynamic_file])
# Parse style mapcss
global style
style = MapCSS(options.minzoom, options.maxzoom + 1)
style.parse(filename=options.filename, static_tags=mapcss_static_tags,
style = MapCSS(options.minzoom, options.maxzoom)
style.parse(clamp=False, stretch=LAYER_PRIORITY_RANGE,
filename=options.filename, static_tags=mapcss_static_tags,
dynamic_tags=mapcss_dynamic_tags)
# Build optimization tree - class/type -> StyleChoosers
# Build optimization tree - class/zoom/type -> StyleChoosers
clname_cltag_unique = set()
for cl in class_order:
clname = cl if cl.find('-') == -1 else cl[:cl.find('-')]
cltags = classificator[cl]
style.build_choosers_tree(clname, "line", cltags)
style.build_choosers_tree(clname, "area", cltags)
style.build_choosers_tree(clname, "node", cltags)
style.restore_choosers_order("line")
style.restore_choosers_order("area")
style.restore_choosers_order("node")
# Get first tag of the class/type.
cltag = next(iter(classificator[cl].keys()))
clname_cltag = clname + '$' + cltag
if clname_cltag not in clname_cltag_unique:
clname_cltag_unique.add(clname_cltag)
style.build_choosers_tree(clname, "line", cltag)
style.build_choosers_tree(clname, "area", cltag)
style.build_choosers_tree(clname, "node", cltag)
style.finalize_choosers_tree()
# TODO: Introduce new function to work with colors for better testability
# Get colors section from style
style_colors = {}
raw_style_colors = style.get_colors()
@ -251,8 +605,6 @@ def komap_mapswithme(options):
visibility = {}
bgpos = 0
dr_linecaps = {'none': BUTTCAP, 'butt': BUTTCAP, 'round': ROUNDCAP}
dr_linejoins = {'none': NOJOIN, 'bevel': BEVELJOIN, 'round': ROUNDJOIN}
@ -278,12 +630,14 @@ def komap_mapswithme(options):
all_draw_elements = set()
# TODO: refactor next for-loop for readability and testability
global validation_errors_count
for results in imapfunc(query_style, ((cl, classificator[cl], options.minzoom, options.maxzoom) for cl in class_order)):
for result in results:
cl, zoom, has_icons_for_areas, runtime_conditions, zstyle = result
cl, zoom, runtime_conditions, zstyle = result
# First, sort rules by 'object-id' in captions (primary, secondary, none ..);
# Then by 'z-index' in ascending order.
# First, sort rules by ::object-id in captions (primary, secondary, none ..)
# then by other ::object-id in ascending order.
def rule_sort_key(dict_):
first = 0
if dict_.get('text'):
@ -291,12 +645,12 @@ def komap_mapswithme(options):
first = 1
if str(dict_.get('text')) == 'none':
first = 2
return (first, int(dict_.get('z-index', 0)))
return (first, dict_.get('object-id'))
zstyle.sort(key = rule_sort_key)
# For debug purpose.
# if str(cl) == 'entrance' and int(zoom) == 19:
# if str(cl) == 'highway-path' and int(zoom) == 19:
# print(cl)
# print(zstyle)
@ -309,6 +663,7 @@ def komap_mapswithme(options):
if dr_cont is None:
dr_cont = ClassifElementProto()
dr_cont.name = cl
dr_lines_objects = {}
visstring = ["0"] * (options.maxzoom - options.minzoom + 1)
@ -322,9 +677,9 @@ def komap_mapswithme(options):
st = dict([(k, v) for k, v in st.items() if str(v).strip(" 0.")])
if 'width' in st or 'pattern-image' in st:
has_lines = True
if 'icon-image' in st or 'symbol-shape' in st or 'symbol-image' in st:
if 'icon-image' in st and st.get('icon-image') != 'none' or 'symbol-shape' in st or 'symbol-image' in st:
has_icons = True
if 'fill-color' in st:
if 'fill-color' in st and st.get('fill-color') != 'none':
has_fills = True
has_text = None
@ -341,6 +696,9 @@ def komap_mapswithme(options):
visstring[zoom] = "1"
if zoom == 0:
continue
dr_element = DrawElementProto()
dr_element.scale = zoom
@ -349,23 +707,36 @@ def komap_mapswithme(options):
dr_element.apply_if.append(str(rc))
for st in zstyle:
if st.get('-x-kot-layer') == 'top':
st['z-index'] = float(st.get('z-index', 0)) + 15001.
elif st.get('-x-kot-layer') == 'bottom':
st['z-index'] = float(st.get('z-index', 0)) - 15001.
if st.get('casing-width') not in (None, 0): # and (st.get('width') or st.get('fill-color')):
if st.get('casing-width') not in (None, 0) or st.get('casing-width-add') is not None: # and (st.get('width') or st.get('fill-color')):
is_area_st = 'fill-color' in st
if has_lines and not is_area_st and st.get('casing-linecap', 'butt') == 'butt':
dr_line = LineRuleProto()
dr_line.width = (st.get('width', 0) * WIDTH_SCALE) + (st.get('casing-width') * WIDTH_SCALE * 2)
base_width = st.get('width', 0)
if base_width == 0:
for wst in zstyle:
if wst.get('width') not in (None, 0):
# Rail bridge styles use width from ::dash object instead of ::default.
if base_width == 0 or wst.get('object-id') != '::default':
base_width = wst.get('width', 0)
# 'casing-width' has precedence over 'casing-width-add'.
if st.get('casing-width') in (None, 0):
st['casing-width'] = base_width + st.get('casing-width-add')
base_width = 0
dr_line.width = round(base_width + st.get('casing-width') * 2, 2)
dr_line.color = mwm_encode_color(colors, st, "casing")
if '-x-me-casing-line-priority' in st:
dr_line.priority = int(st.get('-x-me-casing-line-priority'))
if st.get('object-id') == '::default':
# An automatic casing line should be rendered below the "main" line, hence auto priority -1.
auto_comment = 'casing'
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'), 'line', auto_comment, -1)
store_visibility(cl, 'line', st.get('object-id'), zoom, auto_comment)
else:
dr_line.priority = min(int(st.get('z-index', 0) + 999), 20000)
dashes = st.get('casing-dashes', st.get('dashes', []))
dr_line.dashdot.dd.extend(dashes)
# A casing line explicitly defined via ::object_id.
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'))
store_visibility(cl, 'line', st.get('object-id'), zoom)
for i in st.get('casing-dashes', st.get('dashes', [])):
dr_line.dashdot.dd.extend([float(i)])
addPattern(dr_line.dashdot.dd)
dr_line.cap = dr_linecaps.get(st.get('casing-linecap', 'butt'), BUTTCAP)
dr_line.join = dr_linejoins.get(st.get('casing-linejoin', 'round'), ROUNDJOIN)
@ -373,12 +744,12 @@ def komap_mapswithme(options):
if has_fills and is_area_st and float(st.get('fill-opacity', 1)) > 0:
dr_element.area.border.color = mwm_encode_color(colors, st, "casing")
dr_element.area.border.width = st.get('casing-width', 0) * WIDTH_SCALE
dr_element.area.border.width = st.get('casing-width', 0)
# Let's try without this additional line style overhead. Needed only for casing in road endings.
# if st.get('casing-linecap', st.get('linecap', 'round')) != 'butt':
# dr_line = LineRuleProto()
# dr_line.width = (st.get('width', 0) * WIDTH_SCALE) + (st.get('casing-width') * WIDTH_SCALE * 2)
# dr_line.width = st.get('width', 0) + (st.get('casing-width') * 2)
# dr_line.color = mwm_encode_color(colors, st, "casing")
# dr_line.priority = -15000
# dashes = st.get('casing-dashes', st.get('dashes', []))
@ -390,17 +761,15 @@ def komap_mapswithme(options):
if has_lines:
if st.get('width'):
dr_line = LineRuleProto()
dr_line.width = (st.get('width', 0) * WIDTH_SCALE)
dr_line.width = st.get('width', 0)
dr_line.color = mwm_encode_color(colors, st)
for i in st.get('dashes', []):
dr_line.dashdot.dd.extend([max(float(i), 1) * WIDTH_SCALE])
dr_line.dashdot.dd.extend([float(i)])
addPattern(dr_line.dashdot.dd)
dr_line.cap = dr_linecaps.get(st.get('linecap', 'butt'), BUTTCAP)
dr_line.join = dr_linejoins.get(st.get('linejoin', 'round'), ROUNDJOIN)
if '-x-me-line-priority' in st:
dr_line.priority = int(st.get('-x-me-line-priority'))
else:
dr_line.priority = min((int(st.get('z-index', 0)) + 1000), 20000)
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'))
store_visibility(cl, 'line', st.get('object-id'), zoom)
dr_element.lines.extend([dr_line])
if st.get('pattern-image'):
dr_line = LineRuleProto()
@ -410,46 +779,38 @@ def komap_mapswithme(options):
dr_line.pathsym.name = icon[0]
dr_line.pathsym.step = float(st.get('pattern-spacing', 0)) - 16
dr_line.pathsym.offset = st.get('pattern-offset', 0)
if '-x-me-line-priority' in st:
dr_line.priority = int(st.get('-x-me-line-priority'))
else:
dr_line.priority = int(st.get('z-index', 0)) + 1000
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'))
store_visibility(cl, 'line', st.get('object-id'), zoom)
dr_element.lines.extend([dr_line])
if st.get('shield-font-size'):
dr_element.shield.height = int(st.get('shield-font-size', 10))
dr_element.shield.text_color = mwm_encode_color(colors, st, "shield-text")
if st.get('shield-text-halo-radius', 0) != 0:
dr_element.shield.text_stroke_color = mwm_encode_color(colors, st, "shield-text-halo", "white")
dr_element.shield.color = mwm_encode_color(colors, st, "shield")
if st.get('shield-outline-radius', 0) != 0:
dr_element.shield.stroke_color = mwm_encode_color(colors, st, "shield-outline", "white")
if '-x-me-shield-priority' in st:
dr_element.shield.priority = int(st.get('-x-me-shield-priority'))
else:
dr_element.shield.priority = min(19100, (16000 + int(st.get('z-index', 0))))
if st.get('shield-min-distance', 0) != 0:
dr_element.shield.min_distance = int(st.get('shield-min-distance', 0))
if st.get('shield-font-size'):
dr_element.shield.height = int(st.get('shield-font-size', 10))
dr_element.shield.text_color = mwm_encode_color(colors, st, "shield-text")
if st.get('shield-text-halo-radius', 0) != 0:
dr_element.shield.text_stroke_color = mwm_encode_color(colors, st, "shield-text-halo", "white")
dr_element.shield.color = mwm_encode_color(colors, st, "shield")
if st.get('shield-outline-radius', 0) != 0:
dr_element.shield.stroke_color = mwm_encode_color(colors, st, "shield-outline", "white")
dr_element.shield.priority = get_drape_priority(cl, 'shield', st.get('object-id'))
store_visibility(cl, 'shield', st.get('object-id'), zoom)
if st.get('shield-min-distance', 0) != 0:
dr_element.shield.min_distance = int(st.get('shield-min-distance', 0))
if has_icons:
if st.get('icon-image'):
if not has_icons_for_areas:
dr_element.symbol.apply_for_type = 1
if st.get('icon-image') and st.get('icon-image') != 'none':
icon = mwm_encode_image(st)
dr_element.symbol.name = icon[0]
if '-x-me-icon-priority' in st:
dr_element.symbol.priority = int(st.get('-x-me-icon-priority'))
else:
dr_element.symbol.priority = min(19100, (16000 + int(st.get('z-index', 0))))
dr_element.symbol.priority = get_drape_priority(cl, 'icon', st.get('object-id'))
store_visibility(cl, 'icon', st.get('object-id'), zoom)
if 'icon-min-distance' in st:
dr_element.symbol.min_distance = int(st.get('icon-min-distance', 0))
has_icons = False
if st.get('symbol-shape'):
# TODO: not used in current styles; do "circles" work in drape at all?
dr_element.circle.radius = float(st.get('symbol-size'))
dr_element.circle.color = mwm_encode_color(colors, st, 'symbol-fill')
if '-x-me-symbol-priority' in st:
dr_element.circle.priority = int(st.get('-x-me-symbol-priority'))
else:
dr_element.circle.priority = min(19000, (14000 + int(st.get('z-index', 0))))
dr_element.circle.priority = get_drape_priority(cl, 'circle', st.get('object-id'))
store_visibility(cl, 'circle', st.get('object-id'), zoom)
has_icons = False
if has_text and st.get('text') and st.get('text') != 'none':
@ -457,22 +818,28 @@ def komap_mapswithme(options):
has_text = has_text[:2]
dr_text = dr_element.caption
base_z = 15000
text_priority_key = 'caption'
if st.get('text-position', 'center') == 'line':
dr_text = dr_element.path_text
base_z = 16000
text_priority_key = 'pathtext'
dr_cur_subtext = dr_text.primary
for sp in has_text:
dr_cur_subtext.height = int(float(sp.get('font-size', "10").split(",")[0]))
if 'text-color' not in st:
print(f'ERROR: text-color not set for z{zoom} {cl}')
validation_errors_count += 1
dr_cur_subtext.color = mwm_encode_color(colors, sp, "text")
if st.get('text-halo-radius', 0) != 0:
dr_cur_subtext.stroke_color = mwm_encode_color(colors, sp, "text-halo", "white")
if 'text-offset' in sp or 'text-offset-y' in sp:
dr_cur_subtext.offset_y = int(sp.get('text-offset-y', sp.get('text-offset', 0)))
if 'text-offset-x' in sp:
elif 'text-offset-x' in sp:
dr_cur_subtext.offset_x = int(sp.get('text-offset-x', 0))
if 'text' in sp and sp.get('text') != 'name':
elif st.get('text-position', 'center') == 'center' and dr_element.symbol.priority:
print(f'ERROR: an icon is present, but caption\'s text-offset is not set for z{zoom} {cl}')
validation_errors_count += 1
if 'text' in sp and sp.get('text') not in ('name', 'int_name'):
dr_cur_subtext.text = sp.get('text')
if 'text-optional' in sp:
is_valid, value = to_boolean(sp.get('text-optional', ''))
@ -480,40 +847,37 @@ def komap_mapswithme(options):
dr_cur_subtext.is_optional = value
else:
dr_cur_subtext.is_optional = True
elif text_priority_key == 'caption' and dr_element.symbol.priority:
# On by default for all captions (not path texts) with icons.
dr_cur_subtext.is_optional = True
dr_cur_subtext = dr_text.secondary
# Priority is assigned from the first (primary) rule.
if '-x-me-text-priority' in st:
dr_text.priority = int(st.get('-x-me-text-priority'))
auto_comment = None
if text_priority_key == 'caption' and dr_element.symbol.priority:
# A caption with an icon.
# Mandatory captions use icon's priority.
auto_prio_mod = 0
auto_comment = 'mandatory'
if dr_text.primary.is_optional:
# Optional captions are automatically placed below most other overlays.
auto_comment = 'optional'
auto_prio_mod = -OVERLAYS_MAX_PRIORITY
dr_text.priority = get_drape_priority(cl, 'icon', st.get('object-id'),
text_priority_key, auto_comment, auto_prio_mod)
else:
dr_text.priority = min(19000, (base_z + int(st.get('z-index', 0))))
if '-x-me-min-text-priority' in st:
min_priority = int(st.get('-x-me-min-text-priority'))
dr_text.priority = max(min_priority, dr_text.priority)
# A pathtext or a standalone caption.
dr_text.priority = get_drape_priority(cl, text_priority_key, st.get('object-id'))
store_visibility(cl, text_priority_key, st.get('object-id'), zoom, auto_comment)
# Process captions block once.
has_text = None
if has_fills:
if ('fill-color' in st) and (float(st.get('fill-opacity', 1)) > 0):
if 'fill-color' in st and st.get('fill-color') != 'none' and float(st.get('fill-opacity', 1)) > 0:
dr_element.area.color = mwm_encode_color(colors, st, "fill")
priority = 0
if st.get('fill-position', 'foreground') == 'background':
if 'z-index' not in st:
bgpos -= 1
priority = bgpos - 16000
else:
zzz = int(st.get('z-index', 0))
if zzz > 0:
priority = zzz - 16000
else:
priority = zzz - 16700
else:
priority = (int(st.get('z-index', 0)) + 1 + 1000)
if '-x-me-area-priority' in st:
dr_element.area.priority = int(st.get('-x-me-area-priority'))
else:
dr_element.area.priority = priority
dr_element.area.priority = get_drape_priority(cl, 'area', st.get('object-id'))
store_visibility(cl, 'area', st.get('object-id'), zoom)
has_fills = False
str_dr_element = dr_cont.name + "/" + str(dr_element)
@ -527,6 +891,20 @@ def komap_mapswithme(options):
visibility["world|" + class_tree[cl] + "|"] = "".join(visstring)
validate_visibilities(options.maxzoom)
if validation_errors_count:
print()
exit('FAILED to write regenerated drules files!\n'
f'There are {validation_errors_count} validation errors (see in the log above).\n'
'Fix all errors first and re-run.')
output = ''
for prio_range in prio_ranges.keys():
dump_priorities(prio_range, options.priorities_path, options.maxzoom)
output += f'{"" if not output else ", "}{len(prio_ranges[prio_range]["priorities"])} {prio_range}'
print(f'Re-formated priorities files: {output}.')
# Write drules_proto.bin and drules_proto.txt files
drules_bin = open(os.path.join(options.outfile + '.bin'), "wb")
@ -557,6 +935,7 @@ def komap_mapswithme(options):
return -1
viskeys.sort(key=functools.cmp_to_key(cmprepl))
# TODO: Introduce new function to dump `visibility.txt` and `classificator.txt` for better testability
visibility_file = open(os.path.join(ddir, 'visibility.txt'), "w")
classificator_file = open(os.path.join(ddir, 'classificator.txt'), "w")
@ -579,11 +958,13 @@ def komap_mapswithme(options):
visibility_file.close()
classificator_file.close()
# TODO: Introduce new function to dump `colors.txt` for better testability
colors_file = open(colors_file_name, "w")
for c in sorted(colors):
colors_file.write("%d\n" % (c))
colors_file.close()
# TODO: Introduce new function to dump `patterns.txt` for better testability
patterns_file = open(patterns_file_name, "w")
for p in patterns:
patterns_file.write("%s\n" % (' '.join(str(elem) for elem in p)))
@ -596,12 +977,14 @@ def main():
help="read MapCSS stylesheet from FILE", metavar="FILE")
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=19, type="int",
parser.add_option("-t", "--maxzoom", dest="maxzoom", default=20, type="int",
help="maximal available zoom level", metavar="ZOOM")
parser.add_option("-o", "--output-file", dest="outfile", default="-",
help="output filename", metavar="FILE")
parser.add_option("-x", "--txt", dest="txt", action="store_true",
help="create a text file for output", default=False)
parser.add_option("-p", "--priorities-path", dest="priorities_path",
help="path to priorities *.prio.txt files", metavar="PATH")
parser.add_option("-d", "--data-path", dest="data",
help="path to mapcss-mapping.csv and other files", metavar="PATH")
@ -613,6 +996,10 @@ def main():
if options.outfile == "-":
parser.error("Please specify base output path.")
if (options.priorities_path is None or not os.path.isdir(options.priorities_path)):
parser.error("A path to priorities *.prio.txt files is required.")
options.priorities_path = os.path.normpath(options.priorities_path)
komap_mapswithme(options)
if __name__ == '__main__':

View file

@ -24,7 +24,7 @@ class Condition:
params = (params,)
self.params = params # e.g. ('highway','primary')
if typez == "regex":
self.regex = re.compile(self.params[0], re.I)
self.regex = re.compile(self.params[1], re.I)
def extract_tag(self):
if self.params[0][:2] == "::" or self.type == "regex":
@ -33,49 +33,51 @@ class Condition:
def test(self, tags):
"""
Test a hash against this condition
Test tags against this condition
"""
t = self.type
params = self.params
if t == 'eq': # don't compare tags against sublayers
if t == 'eq':
# Don't compare tags against sublayers
if params[0][:2] == "::":
return params[1]
try:
if t == 'eq':
return tags[params[0]] == params[1]
if t == 'ne':
return tags.get(params[0], "") != params[1]
if t == 'regex':
return bool(self.regex.match(tags[params[0]]))
if t == 'true':
return tags.get(params[0]) == 'yes'
if t == 'untrue':
return tags.get(params[0]) == 'no'
if t == 'set':
if params[0] in tags:
return tags[params[0]] != ''
return False
if t == 'unset':
if params[0] in tags:
return tags[params[0]] == ''
return True
if t == '<':
return (Number(tags[params[0]]) < Number(params[1]))
if t == '<=':
return (Number(tags[params[0]]) <= Number(params[1]))
if t == '>':
return (Number(tags[params[0]]) > Number(params[1]))
if t == '>=':
return (Number(tags[params[0]]) >= Number(params[1]))
except KeyError:
pass
return (params[0] in tags and tags[params[0]] == params[1])
if t == 'ne':
return (params[0] not in tags or tags[params[0]] != params[1])
if t == 'true':
return tags.get(params[0]) == 'yes'
if t == 'untrue':
return tags.get(params[0]) == 'no'
if t == 'set':
if params[0] in tags:
return tags[params[0]] != ''
return False
if t == 'unset':
if params[0] in tags:
return tags[params[0]] == ''
return True
if params[0] not in tags:
return False
if t == 'regex':
return bool(self.regex.match(tags[params[0]]))
if t == '<':
return (Number(tags[params[0]]) < Number(params[1]))
if t == '<=':
return (Number(tags[params[0]]) <= Number(params[1]))
if t == '>':
return (Number(tags[params[0]]) > Number(params[1]))
if t == '>=':
return (Number(tags[params[0]]) >= Number(params[1]))
return False
def __repr__(self):
t = self.type
params = self.params
if t == 'eq' and params[0][:2] == "::":
return "::%s" % (params[1])
return "%s" % (params[1])
if t == 'eq':
return "%s=%s" % (params[0], params[1])
if t == 'ne':

View file

@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License
# along with kothic. If not, see <http://www.gnu.org/licenses/>.
import logging
logger = logging.getLogger('mapcss.Eval')
logger.setLevel(logging.ERROR)
class Eval():
def __init__(self, s='eval()'):
@ -57,6 +61,8 @@ class Eval():
"any": fake_compute,
"min": fake_compute,
"max": fake_compute,
"cond": fake_compute,
"boolean": fake_compute,
})
return tags
@ -99,12 +105,15 @@ class Eval():
return "{:.4g}".format(result)
return str(result)
except:
except Exception as e:
logger.warning(f"Error evaluating expression `{self.expr_text}`", e)
return ""
def __repr__(self):
return "eval(%s)" % self.expr_text
def __eq__(self, other):
return type(self) == type(other) and self.expr_text == other.expr_text
def m_boolean(expr):
expr = str(expr)

View file

@ -25,7 +25,7 @@ type_matches = {
class Rule():
def __init__(self, s=''):
self.runtime_conditions = []
self.runtime_conditions = None
self.conditions = []
# self.isAnd = True
self.minZoom = 0
@ -33,17 +33,12 @@ class Rule():
if s == "*":
s = ""
self.subject = s # "", "way", "node" or "relation"
self.type_matches = type_matches[s] if s in type_matches else set()
def __repr__(self):
return "%s|z%s-%s %s %s" % (self.subject, self.minZoom, self.maxZoom, self.conditions, self.runtime_conditions)
def test(self, obj, tags, zoom):
if (zoom < self.minZoom) or (zoom > self.maxZoom):
return False
if (self.subject != '') and not _test_feature_compatibility(obj, self.subject, tags):
return False
def test(self, tags):
subpart = "::default"
for condition in self.conditions:
res = condition.test(tags)
@ -59,33 +54,10 @@ class Rule():
def extract_tags(self):
a = set()
for condition in self.conditions:
a.add(condition.extract_tag())
if "*" in a:
a = set(["*"])
break
tag = condition.extract_tag()
if tag != '*':
a.add(tag)
elif len(a) == 0:
return set(["*"])
return a
def _test_feature_compatibility(f1, f2, tags={}):
"""
Checks if feature of type f1 is compatible with f2.
"""
if f2 == f1:
return True
if f2 not in ("way", "area", "line"):
return False
elif f2 == "way" and f1 == "line":
return True
elif f2 == "way" and f1 == "area":
return True
elif f2 == "area" and f1 in ("way", "area"):
# if ":area" in tags:
return True
# else:
# return False
elif f2 == "line" and f1 in ("way", "line", "area"):
return True
else:
return False
# print f1, f2, True
return True

View file

@ -30,7 +30,7 @@ def make_nice_style(r):
"checking and nicifying style table"
if type(b) == TYPE_EVAL:
ra[a] = b
elif "color" in a:
elif "color" in a and b.strip() != 'none':
"parsing color value to 3-tuple"
# print "res:", b
if b and (type(b) != tuple):
@ -40,7 +40,7 @@ def make_nice_style(r):
ra[a] = colorparser(b)
elif b:
ra[a] = b
elif any(x in a for x in ("width", "z-index", "opacity", "offset", "radius", "extrude")):
elif any(x in a for x in ("width", "opacity", "offset", "radius", "extrude")):
"these things are float's or not in table at all"
try:
ra[a] = float(b)
@ -76,6 +76,8 @@ class StyleChooser:
The styles property is an array of all the style objects to be drawn
if any of the ruleChains evaluate to true.
"""
# TODO: use logging for debug logs
def __repr__(self):
return "{(%s) : [%s] }\n" % (self.ruleChains, self.styles)
@ -87,6 +89,7 @@ class StyleChooser:
self.selzooms = None
self.compatible_types = set()
self.has_evals = False
self.has_runtime_conditions = False
self.cached_tags = None
def extract_tags(self):
@ -96,62 +99,35 @@ class StyleChooser:
for r in self.ruleChains:
a.update(r.extract_tags())
if "*" in a:
a.clear()
a.add("*")
a = set('*')
break
if self.has_evals and "*" not in a:
for s in self.styles:
for v in list(s.values()):
if type(v) == self.eval_type:
a.update(v.extract_tags())
if "*" in a or len(a) == 0:
a.clear()
a.add("*")
if len(a) == 0:
a = set('*')
self.cached_tags = a
return a
def get_runtime_conditions(self, ftype, tags, zoom):
if self.selzooms:
if zoom < self.selzooms[0] or zoom > self.selzooms[1]:
return None
def get_runtime_conditions(self, tags):
if not self.has_runtime_conditions:
return None
rule_and_object_id = self.testChain(self.ruleChains, ftype, tags, zoom)
rule_and_object_id = self.testChains(tags)
if not rule_and_object_id:
return None
rule = rule_and_object_id[0]
if (len(rule.runtime_conditions) == 0):
return None
return rule.runtime_conditions
def isCorrespondingRule(self, filter_by_runtime_conditions, rule):
# If rule can be applied according to runtime conditions, then
# function return true, else it returns false
if len(rule.runtime_conditions) == 0:
return True
if filter_by_runtime_conditions is None:
return True
if filter_by_runtime_conditions == rule.runtime_conditions:
return True
# Actually we should check rule.runtime_conditions is a subset of filter_by_runtime_conditions
for r in rule.runtime_conditions:
if r not in filter_by_runtime_conditions:
return False
return True
def updateStyles(self, sl, ftype, tags, zoom, xscale, zscale, filter_by_runtime_conditions):
# TODO: Rename to "applyStyles"
def updateStyles(self, sl, tags, xscale, zscale, filter_by_runtime_conditions):
# Are any of the ruleChains fulfilled?
if self.selzooms:
if zoom < self.selzooms[0] or zoom > self.selzooms[1]:
return sl
#if ftype not in self.compatible_types:
#return sl
rule_and_object_id = self.testChain(self.ruleChains, ftype, tags, zoom)
rule_and_object_id = self.testChains(tags)
if not rule_and_object_id:
return sl
@ -159,7 +135,9 @@ class StyleChooser:
rule = rule_and_object_id[0]
object_id = rule_and_object_id[1]
if not self.isCorrespondingRule(filter_by_runtime_conditions, rule):
if (filter_by_runtime_conditions is not None
and rule.runtime_conditions is not None
and filter_by_runtime_conditions != rule.runtime_conditions):
return sl
for r in self.styles:
@ -168,6 +146,7 @@ class StyleChooser:
for a, b in r.items():
"calculating eval()'s"
if type(b) == self.eval_type:
# TODO: Move next block to a separate function
combined_style = {}
for t in sl:
combined_style.update(t)
@ -203,12 +182,12 @@ class StyleChooser:
return sl
def testChain(self, chain, obj, tags, zoom):
def testChains(self, tags):
"""
Tests an object against a chain
"""
for r in chain:
tt = r.test(obj, tags, zoom)
for r in self.ruleChains:
tt = r.test(tags)
if tt:
return r, tt
return False
@ -249,14 +228,18 @@ class StyleChooser:
"""
adds into the current ruleChain (existing Rule)
"""
self.ruleChains[-1].runtime_conditions.append(c)
self.ruleChains[-1].runtime_conditions.sort()
if self.ruleChains[-1].runtime_conditions is None:
self.ruleChains[-1].runtime_conditions = [c]
self.has_runtime_conditions = True
else:
self.ruleChains[-1].runtime_conditions.append(c)
def addStyles(self, a):
# print "addStyle ", a
"""
adds to this.styles
"""
# TODO: move next for-loop to a new method. Don't call it on every style append
for r in self.ruleChains:
if not self.selzooms:
self.selzooms = [r.minZoom, r.maxZoom]
@ -277,9 +260,6 @@ class StyleChooser:
b = str(float(b) / 2)
except:
pass
if "text" == a[-4:]:
if b[:5] != "eval(":
b = "eval(tag(\"" + b + "\"))"
if b[:5] == "eval(":
b = Eval(b)
self.has_evals = True

View file

@ -22,9 +22,10 @@ from .StyleChooser import StyleChooser
from .Condition import Condition
NEEDED_KEYS = set(["width", "casing-width", "fill-color", "fill-image", "icon-image", "text", "extrude",
"background-image", "background-color", "pattern-image", "shield-text", "symbol-shape"])
NEEDED_KEYS = set(["width", "casing-width", "casing-width-add", "fill-color", "fill-image", "icon-image", "text", "extrude",
"background-image", "background-color", "pattern-image", "shield-color", "symbol-shape"])
# TODO: Unused constant
WHITESPACE = re.compile(r'\s+ ', re.S | re.X)
COMMENT = re.compile(r'\/\* .*? \*\/ \s* ', re.S | re.X)
@ -40,29 +41,30 @@ VARIABLE_SET = re.compile(r'@([a-z][\w\d]*) \s* : \s* (.+?) \s* ; \s* ', re.S |
UNKNOWN = re.compile(r'(\S+) \s* ', re.S | re.X)
ZOOM_MINMAX = re.compile(r'(\d+)\-(\d+) $', re.S | re.X)
ZOOM_MIN = re.compile(r'(\d+)\- $', re.S | re.X)
ZOOM_MAX = re.compile(r' \-(\d+) $', re.S | re.X)
ZOOM_MIN = re.compile(r'(\d+)\- $', re.S | re.X)
ZOOM_MAX = re.compile(r' \-(\d+) $', re.S | re.X)
ZOOM_SINGLE = re.compile(r' (\d+) $', re.S | re.X)
CONDITION_TRUE = re.compile(r'\s* ([:\w]+) \s* [?] \s* $', re.I | re.S | re.X)
# TODO: move to Condition.py
CONDITION_TRUE = re.compile(r'\s* ([:\w]+) \s* [?] \s* $', re.I | re.S | re.X)
CONDITION_invTRUE = re.compile(r'\s* [!] \s* ([:\w]+) \s* [?] \s* $', re.I | re.S | re.X)
CONDITION_FALSE = re.compile(r'\s* ([:\w]+) \s* = \s* no \s* $', re.I | re.S | re.X)
CONDITION_SET = re.compile(r'\s* ([-:\w]+) \s* $', re.S | re.X)
CONDITION_UNSET = re.compile(r'\s* !([:\w]+) \s* $', re.S | re.X)
CONDITION_EQ = re.compile(r'\s* ([:\w]+) \s* = \s* (.+) \s* $', re.S | re.X)
CONDITION_NE = re.compile(r'\s* ([:\w]+) \s* != \s* (.+) \s* $', re.S | re.X)
CONDITION_GT = re.compile(r'\s* ([:\w]+) \s* > \s* (.+) \s* $', re.S | re.X)
CONDITION_GE = re.compile(r'\s* ([:\w]+) \s* >= \s* (.+) \s* $', re.S | re.X)
CONDITION_LT = re.compile(r'\s* ([:\w]+) \s* < \s* (.+) \s* $', re.S | re.X)
CONDITION_LE = re.compile(r'\s* ([:\w]+) \s* <= \s* (.+) \s* $', re.S | re.X)
CONDITION_REGEX = re.compile(r'\s* ([:\w]+) \s* =~\/ \s* (.+) \/ \s* $', re.S | re.X)
CONDITION_FALSE = re.compile(r'\s* ([:\w]+) \s* = \s* no \s* $', re.I | re.S | re.X)
CONDITION_SET = re.compile(r'\s* ([-:\w]+) \s* $', re.S | re.X)
CONDITION_UNSET = re.compile(r'\s* !([:\w]+) \s* $', re.S | re.X)
CONDITION_EQ = re.compile(r'\s* ([:\w]+) \s* = \s* (.+) \s* $', re.S | re.X)
CONDITION_NE = re.compile(r'\s* ([:\w]+) \s* != \s* (.+) \s* $', re.S | re.X)
CONDITION_GT = re.compile(r'\s* ([:\w]+) \s* > \s* (.+) \s* $', re.S | re.X)
CONDITION_GE = re.compile(r'\s* ([:\w]+) \s* >= \s* (.+) \s* $', re.S | re.X)
CONDITION_LT = re.compile(r'\s* ([:\w]+) \s* < \s* (.+) \s* $', re.S | re.X)
CONDITION_LE = re.compile(r'\s* ([:\w]+) \s* <= \s* (.+) \s* $', re.S | re.X)
CONDITION_REGEX = re.compile(r'\s* ([:\w]+) \s* =~\/ \s* (.+) \/ \s* $', re.S | re.X)
ASSIGNMENT_EVAL = re.compile(r"\s* (\S+) \s* \: \s* eval \s* \( \s* ' (.+?) ' \s* \) \s* $", re.I | re.S | re.X)
ASSIGNMENT = re.compile(r'\s* (\S+) \s* \: \s* (.+?) \s* $', re.S | re.X)
SET_TAG_EVAL = re.compile(r"\s* set \s+(\S+)\s* = \s* eval \s* \( \s* ' (.+?) ' \s* \) \s* $", re.I | re.S | re.X)
SET_TAG = re.compile(r'\s* set \s+(\S+)\s* = \s* (.+?) \s* $', re.I | re.S | re.X)
SET_TAG_TRUE = re.compile(r'\s* set \s+(\S+)\s* $', re.I | re.S | re.X)
EXIT = re.compile(r'\s* exit \s* $', re.I | re.S | re.X)
ASSIGNMENT = re.compile(r'\s* (\S+) \s* \: \s* (.+?) \s* $', re.S | re.X)
SET_TAG_EVAL = re.compile(r"\s* set \s+(\S+)\s* = \s* eval \s* \( \s* ' (.+?) ' \s* \) \s* $", re.I | re.S | re.X)
SET_TAG = re.compile(r'\s* set \s+(\S+)\s* = \s* (.+?) \s* $', re.I | re.S | re.X)
SET_TAG_TRUE = re.compile(r'\s* set \s+(\S+)\s* $', re.I | re.S | re.X)
EXIT = re.compile(r'\s* exit \s* $', re.I | re.S | re.X)
oNONE = 0
oZOOM = 2
@ -73,6 +75,7 @@ oDECLARATION = 6
oSUBPART = 7
oVARIABLE_SET = 8
# TODO: Following block of variables is never used
DASH = re.compile(r'\-/g')
COLOR = re.compile(r'color$/')
BOLD = re.compile(r'^bold$/i')
@ -81,6 +84,7 @@ UNDERLINE = re.compile(r'^underline$/i')
CAPS = re.compile(r'^uppercase$/i')
CENTER = re.compile(r'^center$/i')
# TODO: Remove unused HEX variable
HEX = re.compile(r'^#([0-9a-f]+)$/i')
VARIABLE = re.compile(r'@([a-z][\w\d]*)')
@ -96,8 +100,9 @@ class MapCSS():
self.scalepair = (minscale, maxscale)
self.choosers = []
self.choosers_by_type = {}
self.choosers_by_type_and_tag = {}
self.choosers_by_type_zoom_tag = {}
self.variables = {}
self.unused_variables = set()
self.style_loaded = False
def parseZoom(self, s):
@ -110,47 +115,67 @@ class MapCSS():
elif ZOOM_SINGLE.match(s):
return float(ZOOM_SINGLE.match(s).groups()[0]), float(ZOOM_SINGLE.match(s).groups()[0])
else:
# TODO: Should we raise an exception here?
logging.error("unparsed zoom: %s" % s)
def build_choosers_tree(self, clname, type, tags={}):
if type not in self.choosers_by_type_and_tag:
self.choosers_by_type_and_tag[type] = {}
if clname not in self.choosers_by_type_and_tag[type]:
self.choosers_by_type_and_tag[type][clname] = set()
def build_choosers_tree(self, clname, type, cltag):
if type not in self.choosers_by_type_zoom_tag:
self.choosers_by_type_zoom_tag[type] = {}
for zoom in range(self.minscale, self.maxscale + 1):
if zoom not in self.choosers_by_type_zoom_tag[type]:
self.choosers_by_type_zoom_tag[type][zoom] = {}
if clname not in self.choosers_by_type_zoom_tag[type][zoom]:
self.choosers_by_type_zoom_tag[type][zoom][clname] = {'arr': [], 'set': set()}
if type in self.choosers_by_type:
for chooser in self.choosers_by_type[type]:
for tag in chooser.extract_tags():
if tag == "*" or tag in tags:
if chooser not in self.choosers_by_type_and_tag[type][clname]:
self.choosers_by_type_and_tag[type][clname].add(chooser)
break
chooser_tags = chooser.extract_tags()
if '*' in chooser_tags or cltag in chooser_tags:
for zoom in range(int(chooser.selzooms[0]), int(chooser.selzooms[1]) + 1):
if chooser not in self.choosers_by_type_zoom_tag[type][zoom][clname]['set']:
self.choosers_by_type_zoom_tag[type][zoom][clname]['arr'].append(chooser)
self.choosers_by_type_zoom_tag[type][zoom][clname]['set'].add(chooser)
def finalize_choosers_tree(self):
for ftype in self.choosers_by_type_zoom_tag.keys():
for zoom in self.choosers_by_type_zoom_tag[ftype].keys():
for clname in self.choosers_by_type_zoom_tag[ftype][zoom].keys():
# Discard unneeded unique set of choosers.
self.choosers_by_type_zoom_tag[ftype][zoom][clname] = self.choosers_by_type_zoom_tag[ftype][zoom][clname]['arr']
for i in range(0, len(self.choosers_by_type_zoom_tag[ftype][zoom][clname])):
chooser = self.choosers_by_type_zoom_tag[ftype][zoom][clname][i]
optimized = StyleChooser(chooser.scalepair)
optimized.styles = chooser.styles
optimized.eval_type = chooser.eval_type
optimized.has_evals = chooser.has_evals
optimized.has_runtime_conditions = chooser.has_runtime_conditions
optimized.selzooms = [zoom, zoom]
optimized.ruleChains = []
for rule in chooser.ruleChains:
# Discard chooser's rules that don't match type or zoom.
if ftype in rule.type_matches and zoom >= rule.minZoom and zoom <= rule.maxZoom:
optimized.ruleChains.append(rule)
self.choosers_by_type_zoom_tag[ftype][zoom][clname][i] = optimized
def restore_choosers_order(self, type):
ethalon_choosers = self.choosers_by_type[type]
for tag, choosers_for_tag in list(self.choosers_by_type_and_tag[type].items()):
tmp = []
for ec in ethalon_choosers:
if ec in choosers_for_tag:
tmp.append(ec)
self.choosers_by_type_and_tag[type][tag] = tmp
def get_runtime_rules(self, clname, type, tags, zoom):
"""
Returns array of runtime_conditions which are used for clname/type/tags/zoom
"""
runtime_rules = []
if type in self.choosers_by_type_and_tag:
for chooser in self.choosers_by_type_and_tag[type][clname]:
runtime_conditions = chooser.get_runtime_conditions(type, tags, zoom)
if type in self.choosers_by_type_zoom_tag:
for chooser in self.choosers_by_type_zoom_tag[type][zoom][clname]:
runtime_conditions = chooser.get_runtime_conditions(tags)
if runtime_conditions:
runtime_rules.append(runtime_conditions)
return runtime_rules
# TODO: Renamed to `get_styles` because it returns a list of styles for each class `::XXX`
# Refactoring idea: Maybe return dict with `object-id` as a key
def get_style(self, clname, type, tags, zoom, xscale, zscale, filter_by_runtime_conditions):
style = []
if type in self.choosers_by_type_and_tag:
for chooser in self.choosers_by_type_and_tag[type][clname]:
style = chooser.updateStyles(style, type, tags, zoom, xscale, zscale, filter_by_runtime_conditions)
if type in self.choosers_by_type_zoom_tag:
for chooser in self.choosers_by_type_zoom_tag[type][zoom][clname]:
style = chooser.updateStyles(style, tags, xscale, zscale, filter_by_runtime_conditions)
style = [x for x in style if x["object-id"] != "::*"]
for x in style:
for k, v in [('width', 0), ('casing-width', 0)]:
@ -190,6 +215,8 @@ class MapCSS():
def get_variable(self, m):
name = m.group()[1:]
if name in self.unused_variables:
self.unused_variables.remove(name)
if not name in self.variables:
raise Exception("Variable not found: " + str(format(name)))
return self.variables[name] if name in self.variables else m.group()
@ -202,7 +229,8 @@ class MapCSS():
if filename:
basepath = os.path.dirname(filename)
if not css:
css = open(filename).read()
with open(filename) as css_file:
css = css_file.read()
if not self.style_loaded:
self.choosers = []
@ -322,7 +350,8 @@ class MapCSS():
import_filename = os.path.join(basepath, IMPORT.match(css).groups()[0])
try:
css = IMPORT.sub("", css, 1)
import_text = open(import_filename, "r").read()
with open(import_filename, "r") as import_file:
import_text = import_file.read()
stck[-1][1] = css # store remained part
stck.append([import_filename, import_text, import_text])
wasBroken = True
@ -335,6 +364,7 @@ class MapCSS():
name = VARIABLE_SET.match(css).groups()[0]
log.debug("variable set found: %s" % name)
self.variables[name] = VARIABLE_SET.match(css).groups()[1]
self.unused_variables.add( name )
css = VARIABLE_SET.sub("", css, 1)
previous = oVARIABLE_SET
@ -342,7 +372,7 @@ class MapCSS():
elif UNKNOWN.match(css):
raise Exception("Unknown construction: " + UNKNOWN.match(css).group())
# Must be unreacheable
# Must be unreachable
else:
raise Exception("Unexpected construction: " + css)
@ -360,10 +390,13 @@ class MapCSS():
css_orig = stck[-1][2] # original
css = stck[-1][1] # remained
line = css_orig[:-len(css)].count("\n") + 1
# TODO: Handle filename is None
msg = str(e) + "\nFile: " + filename + "\nLine: " + str(line)
# TODO: Print stack trace of original exception `e`
raise Exception(msg)
try:
# TODO: Drop support of z-index because `clamp` is always False and z-index properties unused in Organic Maps)
if clamp:
"clamp z-indexes, so they're tightly following integers"
zindex = set()
@ -372,18 +405,19 @@ class MapCSS():
zindex.add(float(stylez.get('z-index', 0)))
zindex = list(zindex)
zindex.sort()
zoffset = len([x for x in zindex if x < 0])
for chooser in self.choosers:
for stylez in chooser.styles:
if 'z-index' in stylez:
res = zindex.index(float(stylez.get('z-index', 0))) - zoffset
res = zindex.index(float(stylez.get('z-index', 0)))
if stretch:
stylez['z-index'] = 1. * res / len(zindex) * stretch
stylez['z-index'] = stretch * res / len(zindex)
else:
stylez['z-index'] = res
except TypeError:
# TODO: Better error handling here
pass
# Group MapCSS styles by object type: 'area', 'line', 'way', 'node'
for chooser in self.choosers:
for t in chooser.compatible_types:
if t not in self.choosers_by_type:
@ -391,7 +425,11 @@ class MapCSS():
else:
self.choosers_by_type[t].append(chooser)
if self.unused_variables:
# TODO: Do not print warning here. Instead let libkomwn.komap_mapswithme(...) analyze unused_variables
print(f"Warning: Unused variables: {', '.join(self.unused_variables)}")
# TODO: move to Condition.py
def parseCondition(s):
log = logging.getLogger('mapcss.parser.condition')
@ -472,7 +510,7 @@ def parseDeclaration(s):
logging.debug("%s == %s" % (tzz[0], tzz[1]))
else:
logging.debug("unknown %s" % (a))
return [t]
return [t] # TODO: don't wrap `t` dict into a list. Return `t` instead.
if __name__ == "__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()