Compare commits

..

No commits in common. "master" and "vng-fix" have entirely different histories.

37 changed files with 1410 additions and 2804 deletions

1
.gitignore vendored
View file

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

4
README Normal file
View file

@ -0,0 +1,4 @@
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

View file

@ -1,37 +0,0 @@
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

@ -1,73 +0,0 @@
#!/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()

View file

@ -1,3 +0,0 @@
# 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,13 +2,11 @@ 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
@ -16,81 +14,19 @@ whatever_to_cairo = mapcss.webcolors.webcolors.whatever_to_cairo
PROFILE = False
MULTIPROCESSING = True
# 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.
# 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 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
from drules_struct_pb2 import *
# Drules are arranged into following ranges.
PRIO_OVERLAYS = 'overlays'
PRIO_FG = 'FG'
PRIO_BG_TOP = 'BG-top'
PRIO_BG_BY_SIZE = 'BG-by-size'
WIDTH_SCALE = 1.0
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()
@ -105,9 +41,6 @@ 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)
@ -122,7 +55,6 @@ 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
@ -140,33 +72,21 @@ def query_style(args):
results = []
for zoom in range(minzoom, maxzoom + 1):
all_runtime_conditions_arr = []
runtime_conditions_arr = []
# Get runtime conditions which are used for class 'cl' on zoom 'zoom'
if "area" not in cltags:
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))
runtime_conditions_arr.extend(style.get_runtime_rules(clname, "line", cltags, zoom))
runtime_conditions_arr.extend(style.get_runtime_rules(clname, "area", cltags, zoom))
if "area" not in cltags:
all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "node", cltags, zoom))
runtime_conditions_arr.extend(style.get_runtime_rules(clname, "node", cltags, zoom))
runtime_conditions_arr = []
if len(all_runtime_conditions_arr) == 0:
# If there is no runtime conditions, do not filter style by runtime conditions
# If there is no any runtime conditions, do not filter style by runtime conditions
if len(runtime_conditions_arr) == 0:
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
@ -174,292 +94,19 @@ 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, runtime_conditions, list(zstyle.values())))
results.append((cl, zoom, has_icons_for_areas, 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
@ -470,7 +117,6 @@ 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):
@ -479,7 +125,6 @@ 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:
@ -495,14 +140,32 @@ def komap_mapswithme(options):
# Build classificator tree from mapcss-mapping.csv file
types_file = open(os.path.join(ddir, 'types.txt'), "w")
# The mapcss-mapping.csv format is described inside the file itself.
# TODO: introduce new function to parse 'mapcss-mapping.csv' for better testability
# 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
cnt = 1
unique_types_check = set()
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 '#'.
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
continue
if len(row) == 3:
# Short format: type name, type id, x / replacement type name
@ -547,17 +210,8 @@ 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 = {}
@ -565,34 +219,26 @@ 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
with open(os.path.join(ddir, 'mapcss-dynamic.txt')) as dynamic_file:
mapcss_dynamic_tags = set([line.rstrip() for line in dynamic_file])
mapcss_dynamic_tags = set([line.rstrip() for line in open(os.path.join(ddir, 'mapcss-dynamic.txt'))])
# Parse style mapcss
global style
style = MapCSS(options.minzoom, options.maxzoom)
style.parse(clamp=False, stretch=LAYER_PRIORITY_RANGE,
filename=options.filename, static_tags=mapcss_static_tags,
style = MapCSS(options.minzoom, options.maxzoom + 1)
style.parse(filename=options.filename, static_tags=mapcss_static_tags,
dynamic_tags=mapcss_dynamic_tags)
# Build optimization tree - class/zoom/type -> StyleChoosers
clname_cltag_unique = set()
# Build optimization tree - class/type -> StyleChoosers
for cl in class_order:
clname = cl if cl.find('-') == -1 else cl[:cl.find('-')]
# 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)
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")
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()
@ -605,6 +251,8 @@ def komap_mapswithme(options):
visibility = {}
bgpos = 0
dr_linecaps = {'none': BUTTCAP, 'butt': BUTTCAP, 'round': ROUNDCAP}
dr_linejoins = {'none': NOJOIN, 'bevel': BEVELJOIN, 'round': ROUNDJOIN}
@ -630,14 +278,12 @@ 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, runtime_conditions, zstyle = result
cl, zoom, has_icons_for_areas, runtime_conditions, zstyle = result
# First, sort rules by ::object-id in captions (primary, secondary, none ..)
# then by other ::object-id in ascending order.
# First, sort rules by 'object-id' in captions (primary, secondary, none ..);
# Then by 'z-index' in ascending order.
def rule_sort_key(dict_):
first = 0
if dict_.get('text'):
@ -645,12 +291,12 @@ def komap_mapswithme(options):
first = 1
if str(dict_.get('text')) == 'none':
first = 2
return (first, dict_.get('object-id'))
return (first, int(dict_.get('z-index', 0)))
zstyle.sort(key = rule_sort_key)
# For debug purpose.
# if str(cl) == 'highway-path' and int(zoom) == 19:
# if str(cl) == 'entrance' and int(zoom) == 19:
# print(cl)
# print(zstyle)
@ -663,7 +309,6 @@ 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)
@ -677,9 +322,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 and st.get('icon-image') != 'none' or 'symbol-shape' in st or 'symbol-image' in st:
if 'icon-image' in st or 'symbol-shape' in st or 'symbol-image' in st:
has_icons = True
if 'fill-color' in st and st.get('fill-color') != 'none':
if 'fill-color' in st:
has_fills = True
has_text = None
@ -696,9 +341,6 @@ def komap_mapswithme(options):
visstring[zoom] = "1"
if zoom == 0:
continue
dr_element = DrawElementProto()
dr_element.scale = zoom
@ -707,36 +349,23 @@ def komap_mapswithme(options):
dr_element.apply_if.append(str(rc))
for st in zstyle:
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')):
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')):
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()
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.width = (st.get('width', 0) * WIDTH_SCALE) + (st.get('casing-width') * WIDTH_SCALE * 2)
dr_line.color = mwm_encode_color(colors, st, "casing")
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)
if '-x-me-casing-line-priority' in st:
dr_line.priority = int(st.get('-x-me-casing-line-priority'))
else:
# 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)])
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)
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)
@ -744,12 +373,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)
dr_element.area.border.width = st.get('casing-width', 0) * WIDTH_SCALE
# 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) + (st.get('casing-width') * 2)
# dr_line.width = (st.get('width', 0) * WIDTH_SCALE) + (st.get('casing-width') * WIDTH_SCALE * 2)
# dr_line.color = mwm_encode_color(colors, st, "casing")
# dr_line.priority = -15000
# dashes = st.get('casing-dashes', st.get('dashes', []))
@ -761,15 +390,17 @@ def komap_mapswithme(options):
if has_lines:
if st.get('width'):
dr_line = LineRuleProto()
dr_line.width = st.get('width', 0)
dr_line.width = (st.get('width', 0) * WIDTH_SCALE)
dr_line.color = mwm_encode_color(colors, st)
for i in st.get('dashes', []):
dr_line.dashdot.dd.extend([float(i)])
dr_line.dashdot.dd.extend([max(float(i), 1) * WIDTH_SCALE])
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)
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'))
store_visibility(cl, 'line', st.get('object-id'), zoom)
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_element.lines.extend([dr_line])
if st.get('pattern-image'):
dr_line = LineRuleProto()
@ -779,38 +410,46 @@ 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)
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'))
store_visibility(cl, 'line', st.get('object-id'), zoom)
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_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")
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 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 has_icons:
if st.get('icon-image') and st.get('icon-image') != 'none':
if st.get('icon-image'):
if not has_icons_for_areas:
dr_element.symbol.apply_for_type = 1
icon = mwm_encode_image(st)
dr_element.symbol.name = icon[0]
dr_element.symbol.priority = get_drape_priority(cl, 'icon', st.get('object-id'))
store_visibility(cl, 'icon', st.get('object-id'), zoom)
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))))
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')
dr_element.circle.priority = get_drape_priority(cl, 'circle', st.get('object-id'))
store_visibility(cl, 'circle', st.get('object-id'), zoom)
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))))
has_icons = False
if has_text and st.get('text') and st.get('text') != 'none':
@ -818,28 +457,22 @@ def komap_mapswithme(options):
has_text = has_text[:2]
dr_text = dr_element.caption
text_priority_key = 'caption'
base_z = 15000
if st.get('text-position', 'center') == 'line':
dr_text = dr_element.path_text
text_priority_key = 'pathtext'
base_z = 16000
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)))
elif 'text-offset-x' in sp:
if 'text-offset-x' in sp:
dr_cur_subtext.offset_x = int(sp.get('text-offset-x', 0))
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'):
if 'text' in sp and sp.get('text') != 'name':
dr_cur_subtext.text = sp.get('text')
if 'text-optional' in sp:
is_valid, value = to_boolean(sp.get('text-optional', ''))
@ -847,37 +480,40 @@ 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
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)
# 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'))
else:
# 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)
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)
# Process captions block once.
has_text = None
if has_fills:
if 'fill-color' in st and st.get('fill-color') != 'none' and float(st.get('fill-opacity', 1)) > 0:
if ('fill-color' in st) and (float(st.get('fill-opacity', 1)) > 0):
dr_element.area.color = mwm_encode_color(colors, st, "fill")
dr_element.area.priority = get_drape_priority(cl, 'area', st.get('object-id'))
store_visibility(cl, 'area', st.get('object-id'), zoom)
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
has_fills = False
str_dr_element = dr_cont.name + "/" + str(dr_element)
@ -891,20 +527,6 @@ 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")
@ -935,7 +557,6 @@ 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")
@ -958,13 +579,11 @@ 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)))
@ -977,14 +596,12 @@ 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=20, type="int",
parser.add_option("-t", "--maxzoom", dest="maxzoom", default=19, 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")
@ -996,10 +613,6 @@ 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[1], re.I)
self.regex = re.compile(self.params[0], re.I)
def extract_tag(self):
if self.params[0][:2] == "::" or self.type == "regex":
@ -33,51 +33,49 @@ class Condition:
def test(self, tags):
"""
Test tags against this condition
Test a hash 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]
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]))
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 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,10 +15,6 @@
# 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()'):
@ -61,8 +57,6 @@ class Eval():
"any": fake_compute,
"min": fake_compute,
"max": fake_compute,
"cond": fake_compute,
"boolean": fake_compute,
})
return tags
@ -105,15 +99,12 @@ class Eval():
return "{:.4g}".format(result)
return str(result)
except Exception as e:
logger.warning(f"Error evaluating expression `{self.expr_text}`", e)
except:
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 = None
self.runtime_conditions = []
self.conditions = []
# self.isAnd = True
self.minZoom = 0
@ -33,12 +33,17 @@ 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, tags):
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
subpart = "::default"
for condition in self.conditions:
res = condition.test(tags)
@ -54,10 +59,33 @@ class Rule():
def extract_tags(self):
a = set()
for condition in self.conditions:
tag = condition.extract_tag()
if tag != '*':
a.add(tag)
elif len(a) == 0:
return set(["*"])
a.add(condition.extract_tag())
if "*" in a:
a = set(["*"])
break
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 and b.strip() != 'none':
elif "color" in a:
"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", "opacity", "offset", "radius", "extrude")):
elif any(x in a for x in ("width", "z-index", "opacity", "offset", "radius", "extrude")):
"these things are float's or not in table at all"
try:
ra[a] = float(b)
@ -76,8 +76,6 @@ 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)
@ -89,7 +87,6 @@ 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):
@ -99,35 +96,62 @@ class StyleChooser:
for r in self.ruleChains:
a.update(r.extract_tags())
if "*" in a:
a = set('*')
a.clear()
a.add("*")
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 len(a) == 0:
a = set('*')
if "*" in a or len(a) == 0:
a.clear()
a.add("*")
self.cached_tags = a
return a
def get_runtime_conditions(self, tags):
if not self.has_runtime_conditions:
return None
def get_runtime_conditions(self, ftype, tags, zoom):
if self.selzooms:
if zoom < self.selzooms[0] or zoom > self.selzooms[1]:
return None
rule_and_object_id = self.testChains(tags)
rule_and_object_id = self.testChain(self.ruleChains, ftype, tags, zoom)
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
# TODO: Rename to "applyStyles"
def updateStyles(self, sl, tags, xscale, zscale, filter_by_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):
# Are any of the ruleChains fulfilled?
rule_and_object_id = self.testChains(tags)
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)
if not rule_and_object_id:
return sl
@ -135,9 +159,7 @@ class StyleChooser:
rule = rule_and_object_id[0]
object_id = rule_and_object_id[1]
if (filter_by_runtime_conditions is not None
and rule.runtime_conditions is not None
and filter_by_runtime_conditions != rule.runtime_conditions):
if not self.isCorrespondingRule(filter_by_runtime_conditions, rule):
return sl
for r in self.styles:
@ -146,7 +168,6 @@ 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)
@ -182,12 +203,12 @@ class StyleChooser:
return sl
def testChains(self, tags):
def testChain(self, chain, obj, tags, zoom):
"""
Tests an object against a chain
"""
for r in self.ruleChains:
tt = r.test(tags)
for r in chain:
tt = r.test(obj, tags, zoom)
if tt:
return r, tt
return False
@ -228,18 +249,14 @@ class StyleChooser:
"""
adds into the current ruleChain (existing Rule)
"""
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)
self.ruleChains[-1].runtime_conditions.append(c)
self.ruleChains[-1].runtime_conditions.sort()
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]
@ -260,6 +277,9 @@ 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,10 +22,9 @@ from .StyleChooser import StyleChooser
from .Condition import Condition
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"])
NEEDED_KEYS = set(["width", "casing-width", "fill-color", "fill-image", "icon-image", "text", "extrude",
"background-image", "background-color", "pattern-image", "shield-text", "symbol-shape"])
# TODO: Unused constant
WHITESPACE = re.compile(r'\s+ ', re.S | re.X)
COMMENT = re.compile(r'\/\* .*? \*\/ \s* ', re.S | re.X)
@ -41,30 +40,29 @@ 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)
# TODO: move to Condition.py
CONDITION_TRUE = re.compile(r'\s* ([:\w]+) \s* [?] \s* $', re.I | re.S | re.X)
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
@ -75,7 +73,6 @@ 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')
@ -84,7 +81,6 @@ 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]*)')
@ -100,9 +96,8 @@ class MapCSS():
self.scalepair = (minscale, maxscale)
self.choosers = []
self.choosers_by_type = {}
self.choosers_by_type_zoom_tag = {}
self.choosers_by_type_and_tag = {}
self.variables = {}
self.unused_variables = set()
self.style_loaded = False
def parseZoom(self, s):
@ -115,67 +110,47 @@ 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, 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()}
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()
if type in self.choosers_by_type:
for chooser in self.choosers_by_type[type]:
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
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
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_zoom_tag:
for chooser in self.choosers_by_type_zoom_tag[type][zoom][clname]:
runtime_conditions = chooser.get_runtime_conditions(tags)
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 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_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)
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)
style = [x for x in style if x["object-id"] != "::*"]
for x in style:
for k, v in [('width', 0), ('casing-width', 0)]:
@ -215,8 +190,6 @@ 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()
@ -229,8 +202,7 @@ class MapCSS():
if filename:
basepath = os.path.dirname(filename)
if not css:
with open(filename) as css_file:
css = css_file.read()
css = open(filename).read()
if not self.style_loaded:
self.choosers = []
@ -350,8 +322,7 @@ class MapCSS():
import_filename = os.path.join(basepath, IMPORT.match(css).groups()[0])
try:
css = IMPORT.sub("", css, 1)
with open(import_filename, "r") as import_file:
import_text = import_file.read()
import_text = open(import_filename, "r").read()
stck[-1][1] = css # store remained part
stck.append([import_filename, import_text, import_text])
wasBroken = True
@ -364,7 +335,6 @@ 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
@ -372,7 +342,7 @@ class MapCSS():
elif UNKNOWN.match(css):
raise Exception("Unknown construction: " + UNKNOWN.match(css).group())
# Must be unreachable
# Must be unreacheable
else:
raise Exception("Unexpected construction: " + css)
@ -390,13 +360,10 @@ 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()
@ -405,19 +372,18 @@ 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)))
res = zindex.index(float(stylez.get('z-index', 0))) - zoffset
if stretch:
stylez['z-index'] = stretch * res / len(zindex)
stylez['z-index'] = 1. * res / len(zindex) * stretch
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:
@ -425,11 +391,7 @@ 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')
@ -510,7 +472,7 @@ def parseDeclaration(s):
logging.debug("%s == %s" % (tzz[0], tzz[1]))
else:
logging.debug("unknown %s" % (a))
return [t] # TODO: don't wrap `t` dict into a list. Return `t` instead.
return [t]
if __name__ == "__main__":

View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,136 +0,0 @@
/* ~~~~ 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

@ -1,75 +0,0 @@
/* ~~~~ 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

@ -1,16 +0,0 @@
/* 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

@ -1,16 +0,0 @@
# 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

@ -1,18 +0,0 @@
# 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

@ -1,41 +0,0 @@
# 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

@ -1,61 +0,0 @@
# 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

@ -1,133 +0,0 @@
*::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

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

View file

@ -1,148 +0,0 @@
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

@ -1,4 +0,0 @@
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

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

View file

@ -1,300 +0,0 @@
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()

View file

@ -1,124 +0,0 @@
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()

View file

@ -1,68 +0,0 @@
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)

View file

@ -1,364 +0,0 @@
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()

View file

@ -1,114 +0,0 @@
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()

View file

@ -1,297 +0,0 @@
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()