Compare commits
46 commits
Author | SHA1 | Date | |
---|---|---|---|
6e17463575 | |||
70301732f9 | |||
2850ec5077 | |||
2796db7ae3 | |||
b9d308f0cd | |||
51864cec29 | |||
611abc0d72 | |||
5b22c7a14c | |||
d58e496bd4 | |||
432e0b0d67 | |||
ff3f8324f8 | |||
feb3b87800 | |||
|
ac01664b98 | ||
e256a119ab | |||
dea0efb9ee | |||
04f5f44137 | |||
24b8586c23 | |||
024810fb85 | |||
a03d74f588 | |||
0e23a0e798 | |||
fbecca40c3 | |||
2d5c553af8 | |||
b30c3bee84 | |||
41e9860567 | |||
98755e5d6a | |||
8c5e152621 | |||
9c914f097b | |||
58dcaaba51 | |||
0850cde772 | |||
b34963026a | |||
492906e130 | |||
8e8f17bd29 | |||
ebe1ced85c | |||
2f311e7504 | |||
dbba1c41e0 | |||
d580b748f2 | |||
5b160e185f | |||
9b38690e69 | |||
27b41b5e3f | |||
70fa78f39b | |||
41498a6ec5 | |||
4e29892282 | |||
a6daa7121b | |||
471abbd754 | |||
32cc5aafb6 | |||
|
f9de08f90f |
37 changed files with 2804 additions and 1410 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ src/tiles/
|
|||
*pycache*
|
||||
*swp
|
||||
*bak
|
||||
/.idea
|
4
README
4
README
|
@ -1,4 +0,0 @@
|
|||
Kothic Map Renderer, patched for MWM use.
|
||||
|
||||
python src/komap.py -r mapswithme -s [path to repo]/data/styles/normal.mapcss -o [path to repo]/data/drules_proto
|
||||
|
37
README.md
Normal file
37
README.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
Kothic Mapcss parser/processor tailored for Organic Maps use.
|
||||
|
||||
Dependencies:
|
||||
* Python >= 3.8
|
||||
|
||||
Python dependencies:
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
## Running unittests
|
||||
|
||||
To run all unittests execute next command from project root folder:
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s tests
|
||||
```
|
||||
|
||||
this will search for all `test*.py` files within `tests` directory
|
||||
and execute tests from those files.
|
||||
|
||||
## Running integration tests
|
||||
|
||||
File `integration-tests/full_drules_gen.py` is intended to generate drules
|
||||
files for all 6 themes from main Organic Maps repo. It could be used to understand
|
||||
which parts of the project are actually used by Organic Maps repo.
|
||||
|
||||
Usage:
|
||||
|
||||
```shell
|
||||
cd integration-tests
|
||||
python3 full_drules_gen.py -d ../../../data -o drules --txt
|
||||
```
|
||||
|
||||
This command will run generation for styles - default light, default dark,
|
||||
outdoors light, outdoors dark, vehicle light, vehicle dark and put `*.bin`
|
||||
and `*.txt` files into 'drules' subfolder.
|
0
integration-tests/drules/.gitkeep
Normal file
0
integration-tests/drules/.gitkeep
Normal file
73
integration-tests/full_drules_gen.py
Executable file
73
integration-tests/full_drules_gen.py
Executable file
|
@ -0,0 +1,73 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
from copy import deepcopy
|
||||
from optparse import OptionParser
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
# Add `src` directory to the import paths
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
||||
|
||||
import libkomwm
|
||||
|
||||
FORMAT = '%(asctime)s [%(levelname)s] %(message)s'
|
||||
logging.basicConfig(format=FORMAT)
|
||||
log = logging.getLogger('test_drules_gen')
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
styles = {
|
||||
'default_light': ['styles/default/light/style.mapcss', 'styles/default/include'],
|
||||
'default_dark': ['styles/default/dark/style.mapcss', 'styles/default/include'],
|
||||
'outdoors_light': ['styles/outdoors/light/style.mapcss', 'styles/outdoors/include'],
|
||||
'outdoors_dark': ['styles/outdoors/dark/style.mapcss', 'styles/outdoors/include'],
|
||||
'vehicle_light': ['styles/vehicle/light/style.mapcss', 'styles/vehicle/include'],
|
||||
'vehicle_dark': ['styles/vehicle/dark/style.mapcss', 'styles/vehicle/include'],
|
||||
}
|
||||
|
||||
|
||||
def full_styles_regenerate(options):
|
||||
log.info("Start generating styles")
|
||||
libkomwm.MULTIPROCESSING = False
|
||||
prio_ranges_orig = deepcopy(libkomwm.prio_ranges)
|
||||
|
||||
for name, (style_path, include_path) in styles.items():
|
||||
log.info(f"Generating {name} style ...")
|
||||
|
||||
# Restore initial state
|
||||
libkomwm.prio_ranges = deepcopy(prio_ranges_orig)
|
||||
libkomwm.visibilities = {}
|
||||
|
||||
options.filename = options.data + '/' + style_path
|
||||
options.priorities_path = options.data + '/' + include_path
|
||||
options.outfile = options.outdir + '/' + name
|
||||
|
||||
# Run generation
|
||||
libkomwm.komap_mapswithme(options)
|
||||
log.info(f"Done!")
|
||||
|
||||
def main():
|
||||
parser = OptionParser()
|
||||
parser.add_option("-d", "--data-path", dest="data",
|
||||
help="path to mapcss-mapping.csv and other files", metavar="PATH")
|
||||
parser.add_option("-o", "--output-dir", dest="outdir", default="drules",
|
||||
help="output directory", metavar="DIR")
|
||||
parser.add_option("-f", "--minzoom", dest="minzoom", default=0, type="int",
|
||||
help="minimal available zoom level", metavar="ZOOM")
|
||||
parser.add_option("-t", "--maxzoom", dest="maxzoom", default=20, type="int",
|
||||
help="maximal available zoom level", metavar="ZOOM")
|
||||
parser.add_option("-x", "--txt", dest="txt", action="store_true",
|
||||
help="create a text file for output", default=False)
|
||||
|
||||
(options, args) = parser.parse_args()
|
||||
|
||||
if options.data is None:
|
||||
parser.error("Please specify base 'data' path.")
|
||||
|
||||
if options.outdir is None:
|
||||
parser.error("Please specify base output path.")
|
||||
|
||||
full_styles_regenerate(options)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# The core is using protobuf 3.3.0 still (3party/protobuf/), so no point to require newer versions.
|
||||
# E.g. Ubuntu 24.04 LTS ships with python3-protobuf 3.21.12 and it works fine.
|
||||
protobuf~=3.21.0
|
File diff suppressed because it is too large
Load diff
681
src/libkomwm.py
681
src/libkomwm.py
|
@ -2,11 +2,13 @@ from mapcss import MapCSS
|
|||
from optparse import OptionParser
|
||||
import os
|
||||
import csv
|
||||
import sys
|
||||
import functools
|
||||
from sys import exit
|
||||
from itertools import chain
|
||||
from multiprocessing import Pool, set_start_method
|
||||
from collections import OrderedDict
|
||||
import mapcss.webcolors
|
||||
from drules_struct_pb2 import *
|
||||
|
||||
whatever_to_hex = mapcss.webcolors.webcolors.whatever_to_hex
|
||||
whatever_to_cairo = mapcss.webcolors.webcolors.whatever_to_cairo
|
||||
|
@ -14,19 +16,81 @@ whatever_to_cairo = mapcss.webcolors.webcolors.whatever_to_cairo
|
|||
PROFILE = False
|
||||
MULTIPROCESSING = True
|
||||
|
||||
# If path to the protobuf EGG is specified then apply it before import drules_struct_pb2
|
||||
PROTOBUF_EGG_PATH = os.environ.get("PROTOBUF_EGG_PATH")
|
||||
if PROTOBUF_EGG_PATH:
|
||||
# another version of protobuf may be installed, override it
|
||||
for i in range(len(sys.path)):
|
||||
if -1 != sys.path[i].find("protobuf-"):
|
||||
sys.path[i] = PROTOBUF_EGG_PATH
|
||||
sys.path.append(PROTOBUF_EGG_PATH)
|
||||
# Priority values defined in *.prio.txt files are adjusted
|
||||
# to fit into the following "priorities ranges":
|
||||
# [-10000; 10000): overlays (icons, captions...)
|
||||
# [0; 1000) : FG - foreground areas and lines
|
||||
# [-1000; 0) : BG-top - water, linear and areal, rendered just on top of landcover
|
||||
# (-2000; -1000) : BG-by-size - landcover areas, later in core sorted by their bbox size
|
||||
# The core renderer then re-adjusts those ranges as necessary to accomodate
|
||||
# for special behavior and features' layer=* values.
|
||||
# See drape_frontend/stylist.cpp for the details of layering logic.
|
||||
|
||||
from drules_struct_pb2 import *
|
||||
# Priority range for area and line drules. Should be same as drule::kLayerPriorityRange.
|
||||
LAYER_PRIORITY_RANGE = 1000
|
||||
# Should be same as drule::kOverlaysMaxPriority. The overlays range is [-kOverlaysMaxPriority; kOverlaysMaxPriority),
|
||||
# negative values are used for optional captions which are below most other overlays.
|
||||
OVERLAYS_MAX_PRIORITY = 10000
|
||||
|
||||
WIDTH_SCALE = 1.0
|
||||
# Drules are arranged into following ranges.
|
||||
PRIO_OVERLAYS = 'overlays'
|
||||
PRIO_FG = 'FG'
|
||||
PRIO_BG_TOP = 'BG-top'
|
||||
PRIO_BG_BY_SIZE = 'BG-by-size'
|
||||
|
||||
prio_ranges = {
|
||||
PRIO_OVERLAYS: {'pos': 4, 'base': 0, 'priorities': {}},
|
||||
PRIO_FG: {'pos': 3, 'base': 0, 'priorities': {}},
|
||||
PRIO_BG_TOP: {'pos': 2, 'base': -1000, 'priorities': {}},
|
||||
PRIO_BG_BY_SIZE: {'pos': 1, 'base': -2000, 'priorities': {}},
|
||||
}
|
||||
|
||||
visibilities = {}
|
||||
|
||||
prio_ranges[PRIO_OVERLAYS]['comment'] = f'''
|
||||
Overlays (icons, captions, path texts and shields) are rendered on top of all the geometry (lines, areas).
|
||||
Overlays don't overlap each other, instead the ones with higher priority displace the less important ones.
|
||||
Optional captions (which have an icon) are usually displayed only if there are no other overlays in their way
|
||||
(technically, max overlays priority value ({OVERLAYS_MAX_PRIORITY}) is subtracted from their priorities automatically).
|
||||
'''
|
||||
|
||||
prio_ranges[PRIO_FG]['comment'] = '''
|
||||
FG geometry: foreground lines and areas (e.g. buildings) are rendered always below overlays
|
||||
and always on top of background geometry (BG-top & BG-by-size) even if a foreground feature
|
||||
is layer=-10 (as tunnels should be visibile over landcover and water).
|
||||
'''
|
||||
prio_ranges[PRIO_BG_TOP]['comment'] = '''
|
||||
BG-top geometry: background lines and areas that should be always below foreground ones
|
||||
(including e.g. layer=-10 underwater tunnels), but above background areas sorted by size (BG-by-size),
|
||||
because ordering by size doesn't always work with e.g. water mapped over a forest,
|
||||
so water should be on top of other landcover always, but linear waterways should be hidden beneath it.
|
||||
Still, e.g. a layer=-1 BG-top feature will be rendered under a layer=0 BG-by-size feature
|
||||
(so areal water tunnels are hidden beneath other landcover area) and a layer=1 landcover areas
|
||||
are displayed above layer=0 BG-top.
|
||||
'''
|
||||
prio_ranges[PRIO_BG_BY_SIZE]['comment'] = '''
|
||||
BG-by-size geometry: background areas rendered below BG-top and everything else.
|
||||
Smaller areas are rendered above larger ones (area's size is estimated as the size of its' bounding box).
|
||||
So effectively priority values of BG-by-size areas are not used at the moment.
|
||||
But we might use them later for some special cases, e.g. to determine a main area type of a multi-type feature.
|
||||
Keep them in a logical importance order please.
|
||||
'''
|
||||
|
||||
COMMENT_AUTOFORMAT = '''This file is automatically re-formatted and re-sorted in priorities descending order
|
||||
when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)
|
||||
are generated automatically for information only. Custom formatting and comments are not preserved.
|
||||
'''
|
||||
|
||||
COMMENT_RANGES_OVERVIEW = '''
|
||||
Priorities ranges' rendering order overview:
|
||||
- overlays (icons, captions...)
|
||||
- FG: foreground areas and lines
|
||||
- BG-top: water (linear and areal)
|
||||
- BG-by-size: landcover areas sorted by their size
|
||||
'''
|
||||
|
||||
# TODO: Implement better error handling
|
||||
validation_errors_count = 0
|
||||
|
||||
def to_boolean(s):
|
||||
s = s.lower()
|
||||
|
@ -41,6 +105,9 @@ def mwm_encode_color(colors, st, prefix='', default='black'):
|
|||
if prefix:
|
||||
prefix += "-"
|
||||
opacity = hex(255 - int(255 * float(st.get(prefix + "opacity", 1))))
|
||||
# TODO: Refactoring idea: here color is converted from float to hex. While MapCSS class
|
||||
# reads colors from *.mapcss files and converts to float. How about changing MapCSS
|
||||
# to keep hex values and avoid Hex->Float->Hex operations?
|
||||
color = whatever_to_hex(st.get(prefix + 'color', default))[1:]
|
||||
result = int(opacity + color, 16)
|
||||
colors.add(result)
|
||||
|
@ -55,6 +122,7 @@ def mwm_encode_image(st, prefix='icon', bgprefix='symbol'):
|
|||
return False
|
||||
# strip last ".svg"
|
||||
handle = st.get(prefix + "image")[:-4]
|
||||
# TODO: return `handle` only once
|
||||
return handle, handle
|
||||
|
||||
|
||||
|
@ -72,21 +140,33 @@ def query_style(args):
|
|||
|
||||
results = []
|
||||
for zoom in range(minzoom, maxzoom + 1):
|
||||
runtime_conditions_arr = []
|
||||
|
||||
all_runtime_conditions_arr = []
|
||||
# Get runtime conditions which are used for class 'cl' on zoom 'zoom'
|
||||
if "area" not in cltags:
|
||||
runtime_conditions_arr.extend(style.get_runtime_rules(clname, "line", cltags, zoom))
|
||||
runtime_conditions_arr.extend(style.get_runtime_rules(clname, "area", cltags, zoom))
|
||||
all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "line", cltags, zoom))
|
||||
all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "area", cltags, zoom))
|
||||
if "area" not in cltags:
|
||||
runtime_conditions_arr.extend(style.get_runtime_rules(clname, "node", cltags, zoom))
|
||||
all_runtime_conditions_arr.extend(style.get_runtime_rules(clname, "node", cltags, zoom))
|
||||
|
||||
# If there is no any runtime conditions, do not filter style by runtime conditions
|
||||
if len(runtime_conditions_arr) == 0:
|
||||
runtime_conditions_arr = []
|
||||
if len(all_runtime_conditions_arr) == 0:
|
||||
# If there is no runtime conditions, do not filter style by runtime conditions
|
||||
runtime_conditions_arr.append(None)
|
||||
elif len(all_runtime_conditions_arr) == 1:
|
||||
runtime_conditions_arr = all_runtime_conditions_arr
|
||||
else:
|
||||
# Keep unique conditions only
|
||||
runtime_conditions_arr.append(all_runtime_conditions_arr.pop(0))
|
||||
for new_rt_conditions in all_runtime_conditions_arr:
|
||||
conditions_unique = True
|
||||
for rt_conditions in runtime_conditions_arr:
|
||||
if new_rt_conditions == rt_conditions:
|
||||
conditions_unique = False
|
||||
break
|
||||
if conditions_unique:
|
||||
runtime_conditions_arr.append(new_rt_conditions)
|
||||
|
||||
for runtime_conditions in runtime_conditions_arr:
|
||||
has_icons_for_areas = False
|
||||
zstyle = {}
|
||||
|
||||
# Get style for class 'cl' on zoom 'zoom' with corresponding runtime conditions
|
||||
|
@ -94,19 +174,292 @@ def query_style(args):
|
|||
linestyle = style.get_style_dict(clname, "line", cltags, zoom, olddict=zstyle, filter_by_runtime_conditions=runtime_conditions)
|
||||
zstyle = linestyle
|
||||
areastyle = style.get_style_dict(clname, "area", cltags, zoom, olddict=zstyle, filter_by_runtime_conditions=runtime_conditions)
|
||||
for st in list(areastyle.values()):
|
||||
if "icon-image" in st or 'symbol-shape' in st or 'symbol-image' in st:
|
||||
has_icons_for_areas = True
|
||||
break
|
||||
zstyle = areastyle
|
||||
if "area" not in cltags:
|
||||
nodestyle = style.get_style_dict(clname, "node", cltags, zoom, olddict=zstyle, filter_by_runtime_conditions=runtime_conditions)
|
||||
zstyle = nodestyle
|
||||
|
||||
results.append((cl, zoom, has_icons_for_areas, runtime_conditions, list(zstyle.values())))
|
||||
results.append((cl, zoom, runtime_conditions, list(zstyle.values())))
|
||||
return results
|
||||
|
||||
def get_priorities_filename(prio_range, path):
|
||||
return os.path.join(path, f'priorities_{prio_ranges[prio_range]["pos"]}_{prio_range}.prio.txt')
|
||||
|
||||
def load_priorities(prio_range, path, classif, compress = False):
|
||||
def print_warning(msg):
|
||||
print(f'WARNING: {msg} in {fname}:\n\t{line}')
|
||||
|
||||
priority_max = OVERLAYS_MAX_PRIORITY if prio_range == PRIO_OVERLAYS else LAYER_PRIORITY_RANGE
|
||||
priority_min = -OVERLAYS_MAX_PRIORITY if prio_range == PRIO_OVERLAYS else 0
|
||||
fname = get_priorities_filename(prio_range, path)
|
||||
with open(fname, 'r') as f:
|
||||
group = []
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
# Strip comments.
|
||||
line = line.split('#', 1)[0].strip()
|
||||
if not line:
|
||||
continue
|
||||
tokens = line.split()
|
||||
if len(tokens) > 2:
|
||||
print_warning('skipping malformed line')
|
||||
continue
|
||||
if tokens[0] == "===":
|
||||
try:
|
||||
priority = int(tokens[1])
|
||||
except ValueError:
|
||||
print_warning('skipping invalid priority value')
|
||||
else:
|
||||
if priority >= priority_min and priority < priority_max:
|
||||
if len(group):
|
||||
for key in group:
|
||||
prio_ranges[prio_range]['priorities'][key] = priority
|
||||
else:
|
||||
print_warning('skipping empty priority group')
|
||||
else:
|
||||
print_warning(f'skipping out of [{priority_min};{priority_max}) range priority value')
|
||||
group = []
|
||||
else:
|
||||
cl = tokens[0]
|
||||
object_id = ''
|
||||
oid_pos = cl.find('::')
|
||||
if oid_pos != -1:
|
||||
object_id = cl[oid_pos:]
|
||||
cl = cl[0:oid_pos]
|
||||
if cl not in classif:
|
||||
print_warning('unknown classificator type')
|
||||
key = (cl, object_id)
|
||||
if key in prio_ranges[prio_range]['priorities']:
|
||||
print_warning(f'overriding previously set priority value {prio_ranges[prio_range]["priorities"][key]}')
|
||||
group.append(key)
|
||||
|
||||
if len(group):
|
||||
line = group
|
||||
print_warning(f'skipping last types groups with no priority set')
|
||||
|
||||
if prio_range == PRIO_OVERLAYS:
|
||||
for key in prio_ranges[PRIO_OVERLAYS]['priorities'].keys():
|
||||
main_prio_id = None
|
||||
if key[1].startswith('caption'):
|
||||
main_prio_id = (key[0], key[1].replace('caption', 'icon'))
|
||||
if key[1].startswith('pathtext'):
|
||||
main_prio_id = (key[0], key[1].replace('pathtext', 'shield'))
|
||||
if main_prio_id is not None and main_prio_id in prio_ranges[PRIO_OVERLAYS]['priorities']:
|
||||
main_prio = prio_ranges[PRIO_OVERLAYS]['priorities'][main_prio_id]
|
||||
if prio_ranges[PRIO_OVERLAYS]['priorities'][key] > main_prio:
|
||||
print(f'WARNING: {key} priority is higher than {main_prio_id}, making it equal')
|
||||
prio_ranges[PRIO_OVERLAYS]['priorities'][key] = main_prio
|
||||
|
||||
# TODO: update compression logic to handle icons put inbetween automatic optional captions priorities.
|
||||
if compress:
|
||||
print(f'Compressing {prio_range} priorities into a (0;{priority_max}) range:')
|
||||
unique_prios = set(prio_ranges[prio_range]['priorities'].values())
|
||||
print(f'\tunique priorities values: {len(unique_prios)}')
|
||||
# Keep gaps at the range borders.
|
||||
base_idx = 1
|
||||
if 0 not in unique_prios:
|
||||
base_idx = 0
|
||||
unique_prios.add(0)
|
||||
unique_prios.add(priority_max)
|
||||
step = min(priority_max / len(unique_prios), 10)
|
||||
print(f'\tnew step between priorities: {step}')
|
||||
unique_prios = sorted(unique_prios)
|
||||
for prio_id in prio_ranges[prio_range]['priorities'].keys():
|
||||
idx = unique_prios.index(prio_ranges[prio_range]['priorities'][prio_id])
|
||||
prio_ranges[prio_range]['priorities'][prio_id] = int(step * (base_idx + idx))
|
||||
|
||||
|
||||
def store_visibility(cl, dr_type, object_id, zoom, auto_comment = None):
|
||||
if object_id == '::default':
|
||||
object_id = ''
|
||||
dr_type_comment = (dr_type, auto_comment)
|
||||
if cl not in visibilities:
|
||||
visibilities[cl] = {}
|
||||
if dr_type_comment not in visibilities[cl]:
|
||||
visibilities[cl][dr_type_comment] = {}
|
||||
if object_id not in visibilities[cl][dr_type_comment]:
|
||||
visibilities[cl][dr_type_comment][object_id] = set()
|
||||
visibilities[cl][dr_type_comment][object_id].add(zoom)
|
||||
|
||||
|
||||
def prettify_zooms(zooms, maxzoom):
|
||||
|
||||
def add_zrange(first, last, result, maxzoom):
|
||||
first = str(first)
|
||||
last = str(last)
|
||||
if last == str(maxzoom):
|
||||
zrange = first + '-'
|
||||
elif first == last:
|
||||
zrange = first
|
||||
else:
|
||||
zrange = first + '-' + last
|
||||
if result != '':
|
||||
result += ','
|
||||
result += zrange
|
||||
return result
|
||||
|
||||
zooms = sorted(zooms)
|
||||
first = zooms.pop(0)
|
||||
prev = first
|
||||
result = ''
|
||||
for zoom in zooms:
|
||||
if zoom == prev + 1:
|
||||
prev = zoom
|
||||
else:
|
||||
result = add_zrange(first, prev, result, maxzoom)
|
||||
first = zoom
|
||||
prev = zoom
|
||||
return 'z' + add_zrange(first, prev, result, maxzoom)
|
||||
|
||||
|
||||
def validate_visibilities(maxzoom):
|
||||
for cl, dr_types_comments in visibilities.items():
|
||||
for dr_type_comment, object_ids in dr_types_comments.items():
|
||||
for object_id, zooms in object_ids.items():
|
||||
zoom_range = prettify_zooms(zooms, maxzoom)
|
||||
if zoom_range.find(',') != -1:
|
||||
print(f'WARNING: non-contiguous visibility range {zoom_range} for {cl} {dr_type_comment}{object_id}')
|
||||
|
||||
dr_type = dr_type_comment[0]
|
||||
icon_dr_type_comment = ('icon', None)
|
||||
if (dr_type == 'caption' and icon_dr_type_comment in dr_types_comments and
|
||||
object_id in dr_types_comments[icon_dr_type_comment]):
|
||||
icon_zooms = sorted(dr_types_comments[icon_dr_type_comment][object_id])
|
||||
if min(zooms) < icon_zooms[0]:
|
||||
print(f'WARNING: caption {zoom_range} appears before icon {prettify_zooms(icon_zooms, maxzoom)}'
|
||||
f' for {cl}{object_id}')
|
||||
|
||||
line_dr_type_comment = ('line', None)
|
||||
if dr_type in ('pathtext', 'shield'):
|
||||
lines_min_zoom = maxzoom + 1
|
||||
if line_dr_type_comment in dr_types_comments:
|
||||
lines_min_zoom = maxzoom + 1
|
||||
for line_object_id, line_zooms in dr_types_comments[line_dr_type_comment].items():
|
||||
min_zoom = min(line_zooms)
|
||||
if min_zoom < lines_min_zoom:
|
||||
lines_min_zoom = min_zoom
|
||||
min_zoom = min(zooms)
|
||||
if min_zoom < lines_min_zoom:
|
||||
missing_zooms = prettify_zooms(range(min_zoom, lines_min_zoom), maxzoom)
|
||||
print(f'ERROR: {dr_type} without line at {missing_zooms} for {cl}{object_id}')
|
||||
global validation_errors_count
|
||||
validation_errors_count += 1
|
||||
|
||||
def dump_priorities(prio_range, path, maxzoom):
|
||||
with open(get_priorities_filename(prio_range, path), 'w') as outfile:
|
||||
comment = COMMENT_AUTOFORMAT + prio_ranges[prio_range]['comment'] + COMMENT_RANGES_OVERVIEW
|
||||
for s in comment.splitlines():
|
||||
outfile.write(f'# {s}'.rstrip() + '\n')
|
||||
outfile.write('\n')
|
||||
|
||||
if len(prio_ranges[prio_range]['priorities']):
|
||||
dr_types_order = (('icon', 'caption', 'pathtext', 'shield', 'line', 'area') if prio_range == PRIO_OVERLAYS
|
||||
else ('line', 'area', 'icon', 'caption', 'pathtext', 'shield'))
|
||||
comment_auto_captions = '''
|
||||
All automatic optional captions priorities are below 0.
|
||||
They follow the order of their correspoding icons.
|
||||
'''
|
||||
|
||||
prios = sorted(prio_ranges[prio_range]['priorities'].items(),
|
||||
key = lambda item: (OVERLAYS_MAX_PRIORITY - item[1], item[0][0], item[0][1]))
|
||||
group_prio = prios[0][1]
|
||||
group = ''
|
||||
group_comment = '# '
|
||||
for p in prios:
|
||||
if p[1] != group_prio:
|
||||
if prio_range == PRIO_OVERLAYS and comment_auto_captions and group_prio < 0:
|
||||
for s in comment_auto_captions.splitlines():
|
||||
outfile.write(f'# {s.strip()}'.rstrip() + '\n')
|
||||
outfile.write('\n')
|
||||
comment_auto_captions = None
|
||||
outfile.write(f'{group}{group_comment}=== {group_prio}\n\n')
|
||||
group_prio = p[1]
|
||||
group = ''
|
||||
group_comment = '# '
|
||||
|
||||
cl = p[0][0]
|
||||
object_id = p[0][1]
|
||||
auto_dr_type = None
|
||||
auto_comment = None
|
||||
if len(p[0]) == 4:
|
||||
auto_dr_type = p[0][2]
|
||||
auto_comment = p[0][3]
|
||||
|
||||
line_drules = ''
|
||||
other_drules = ''
|
||||
if cl in visibilities:
|
||||
for dr_type_comment in sorted(visibilities[cl].keys(), key = lambda drt: dr_types_order.index(drt[0])):
|
||||
for oid in sorted(visibilities[cl][dr_type_comment].keys()):
|
||||
dr_type, dr_auto_comment = dr_type_comment
|
||||
dr_zoom = dr_type + oid
|
||||
if dr_auto_comment is not None:
|
||||
dr_zoom = f'{dr_zoom}({dr_auto_comment})'
|
||||
dr_zoom += ' ' + prettify_zooms(visibilities[cl][dr_type_comment][oid], maxzoom)
|
||||
# Drules matching this prio_range and object_id and
|
||||
# - an auto priority dr_type match or
|
||||
# - any other non-auto dr_type suitable
|
||||
is_auto_dr_match = dr_type == auto_dr_type and dr_auto_comment == auto_comment
|
||||
is_not_auto_dr = auto_dr_type is None and dr_auto_comment is None
|
||||
is_suitable_for_range = (
|
||||
(prio_range == PRIO_OVERLAYS and dr_type in ('icon', 'caption', 'pathtext', 'shield')) or
|
||||
(prio_range in (PRIO_FG, PRIO_BG_TOP) and dr_type in ('line', 'area')) or
|
||||
(prio_range == PRIO_BG_BY_SIZE and dr_type == 'area'))
|
||||
if oid == object_id and (is_auto_dr_match or is_not_auto_dr and is_suitable_for_range):
|
||||
if line_drules:
|
||||
line_drules += ' and '
|
||||
line_drules += dr_zoom
|
||||
else:
|
||||
# Drules from other prio_ranges or with other object_ids.
|
||||
if other_drules:
|
||||
other_drules += ', '
|
||||
other_drules += dr_zoom
|
||||
if object_id:
|
||||
cl += object_id
|
||||
if not line_drules:
|
||||
if other_drules:
|
||||
line_drules = "WARNING: no drule defined for the priority"
|
||||
else:
|
||||
line_drules = "WARNING: no style defined (the type will be not included into map data)"
|
||||
print(f'{line_drules} for {cl} in {prio_range}')
|
||||
|
||||
info = '# ' + line_drules
|
||||
if other_drules:
|
||||
info += f' (also has {other_drules})'
|
||||
if auto_dr_type is None:
|
||||
group_comment = ''
|
||||
else:
|
||||
cl = '# ' + cl
|
||||
group += f'{cl:50} {info}\n'
|
||||
|
||||
outfile.write(f'{group}{group_comment}=== {group_prio}\n')
|
||||
|
||||
def get_drape_priority(cl, dr_type, object_id, auto_dr_type = None, auto_comment = None, auto_prio_mod = 0):
|
||||
if object_id == '::default':
|
||||
object_id = ''
|
||||
prio_id = (cl, object_id)
|
||||
|
||||
ranges_to_check = (PRIO_OVERLAYS, )
|
||||
if dr_type == 'line':
|
||||
ranges_to_check = (PRIO_FG, PRIO_BG_TOP)
|
||||
elif dr_type == 'area':
|
||||
ranges_to_check = (PRIO_BG_BY_SIZE, PRIO_BG_TOP, PRIO_FG)
|
||||
for r in ranges_to_check:
|
||||
if prio_id in prio_ranges[r]['priorities']:
|
||||
priority = prio_ranges[r]['priorities'][prio_id]
|
||||
if auto_dr_type is not None:
|
||||
min_priority = -OVERLAYS_MAX_PRIORITY if r == PRIO_OVERLAYS else 0
|
||||
priority = max(priority + auto_prio_mod, min_priority)
|
||||
auto_prio_id = (cl, object_id, auto_dr_type, auto_comment)
|
||||
prio_ranges[r]['priorities'][auto_prio_id] = priority
|
||||
return priority + prio_ranges[r]['base']
|
||||
|
||||
print(f'ERROR: priority is not set for {dr_type} {cl}{object_id}')
|
||||
global validation_errors_count
|
||||
validation_errors_count += 1
|
||||
return 0
|
||||
|
||||
|
||||
# TODO: Split large function to smaller ones
|
||||
def komap_mapswithme(options):
|
||||
if options.data and os.path.isdir(options.data):
|
||||
ddir = options.data
|
||||
|
@ -117,6 +470,7 @@ def komap_mapswithme(options):
|
|||
class_order = []
|
||||
class_tree = {}
|
||||
|
||||
# TODO: Introduce new function to parse `colors.txt` for better testability
|
||||
colors_file_name = os.path.join(ddir, 'colors.txt')
|
||||
colors = set()
|
||||
if os.path.exists(colors_file_name):
|
||||
|
@ -125,6 +479,7 @@ def komap_mapswithme(options):
|
|||
colors.add(int(colorLine))
|
||||
colors_in_file.close()
|
||||
|
||||
# TODO: Introduce new function to parse `patterns.txt` for better testability
|
||||
patterns = []
|
||||
def addPattern(dashes):
|
||||
if dashes and dashes not in patterns:
|
||||
|
@ -140,32 +495,14 @@ def komap_mapswithme(options):
|
|||
# Build classificator tree from mapcss-mapping.csv file
|
||||
types_file = open(os.path.join(ddir, 'types.txt'), "w")
|
||||
|
||||
# Mapcss-mapping format
|
||||
#
|
||||
# A CSV table mapping tags to types. Some types can be deemed obsolete, either completely or replaced with a different type.
|
||||
#
|
||||
# Example row: highway|bus_stop;[highway=bus_stop];;name;int_name;22; (mind the last semicolon!)
|
||||
# It contains:
|
||||
# - type name: "highway|bus_stop" ('|' is converted to '-' internally)
|
||||
# - mapcss selector for tags: "[highway=bus_stop]" (you can group selectors and use e.g. [oneway?])
|
||||
# - "x" for an obsolete type or an empty cell otherwise
|
||||
# - primary title tag (usually "name")
|
||||
# - secondary title tag (usually "int_name")
|
||||
# - type id, sequential starting from 1
|
||||
# - replacement type for an obsolete tag, if exists
|
||||
#
|
||||
# A shorter format for above example: highway|bus_stop;22;
|
||||
# It leaves only columns 1, 6 and 7. For obsolete types with no replacement put "x" into the last column.
|
||||
# Obviously it works only for simple types that are produced from tags replacing '=' with '|'.
|
||||
#
|
||||
# An example of type with replacement:
|
||||
# highway|unsurfaced|disused;[highway=unsurfaced][disused?];x;name;int_name;838;highway|unclassified
|
||||
|
||||
# The mapcss-mapping.csv format is described inside the file itself.
|
||||
# TODO: introduce new function to parse 'mapcss-mapping.csv' for better testability
|
||||
cnt = 1
|
||||
unique_types_check = set()
|
||||
for row in csv.reader(open(os.path.join(ddir, 'mapcss-mapping.csv')), delimiter=';'):
|
||||
if len(row) <= 1:
|
||||
# Allow for empty lines and comments that do not contain ';' symbol
|
||||
mapping_file = open(os.path.join(ddir, 'mapcss-mapping.csv'))
|
||||
for row in csv.reader(mapping_file, delimiter=';'):
|
||||
if len(row) <= 1 or row[0].startswith('#'):
|
||||
# Allow for empty lines and comment lines starting with '#'.
|
||||
continue
|
||||
if len(row) == 3:
|
||||
# Short format: type name, type id, x / replacement type name
|
||||
|
@ -210,8 +547,17 @@ def komap_mapswithme(options):
|
|||
print("mapswithme", file=types_file)
|
||||
class_tree[cl] = row[0]
|
||||
class_order.sort()
|
||||
mapping_file.close()
|
||||
types_file.close()
|
||||
|
||||
output = ''
|
||||
for prio_range in prio_ranges.keys():
|
||||
load_priorities(prio_range, options.priorities_path, unique_types_check, compress = False)
|
||||
output += f'{"" if not output else ", "}{len(prio_ranges[prio_range]["priorities"])} {prio_range}'
|
||||
print(f'Loaded priorities: {output}.')
|
||||
|
||||
del unique_types_check
|
||||
|
||||
# Get all mapcss static tags which are used in mapcss-mapping.csv
|
||||
# This is a dict with main_tag flags (True = appears first in types)
|
||||
mapcss_static_tags = {}
|
||||
|
@ -219,26 +565,34 @@ def komap_mapswithme(options):
|
|||
for i, t in enumerate(v.keys()):
|
||||
mapcss_static_tags[t] = mapcss_static_tags.get(t, True) and i == 0
|
||||
|
||||
# TODO: Introduce new function to parse `mapcss-dynamic.txt` for better testability
|
||||
# Get all mapcss dynamic tags from mapcss-dynamic.txt
|
||||
mapcss_dynamic_tags = set([line.rstrip() for line in open(os.path.join(ddir, 'mapcss-dynamic.txt'))])
|
||||
with open(os.path.join(ddir, 'mapcss-dynamic.txt')) as dynamic_file:
|
||||
mapcss_dynamic_tags = set([line.rstrip() for line in dynamic_file])
|
||||
|
||||
# Parse style mapcss
|
||||
global style
|
||||
style = MapCSS(options.minzoom, options.maxzoom + 1)
|
||||
style.parse(filename=options.filename, static_tags=mapcss_static_tags,
|
||||
style = MapCSS(options.minzoom, options.maxzoom)
|
||||
style.parse(clamp=False, stretch=LAYER_PRIORITY_RANGE,
|
||||
filename=options.filename, static_tags=mapcss_static_tags,
|
||||
dynamic_tags=mapcss_dynamic_tags)
|
||||
|
||||
# Build optimization tree - class/type -> StyleChoosers
|
||||
# Build optimization tree - class/zoom/type -> StyleChoosers
|
||||
clname_cltag_unique = set()
|
||||
for cl in class_order:
|
||||
clname = cl if cl.find('-') == -1 else cl[:cl.find('-')]
|
||||
cltags = classificator[cl]
|
||||
style.build_choosers_tree(clname, "line", cltags)
|
||||
style.build_choosers_tree(clname, "area", cltags)
|
||||
style.build_choosers_tree(clname, "node", cltags)
|
||||
style.restore_choosers_order("line")
|
||||
style.restore_choosers_order("area")
|
||||
style.restore_choosers_order("node")
|
||||
# Get first tag of the class/type.
|
||||
cltag = next(iter(classificator[cl].keys()))
|
||||
clname_cltag = clname + '$' + cltag
|
||||
if clname_cltag not in clname_cltag_unique:
|
||||
clname_cltag_unique.add(clname_cltag)
|
||||
style.build_choosers_tree(clname, "line", cltag)
|
||||
style.build_choosers_tree(clname, "area", cltag)
|
||||
style.build_choosers_tree(clname, "node", cltag)
|
||||
|
||||
style.finalize_choosers_tree()
|
||||
|
||||
# TODO: Introduce new function to work with colors for better testability
|
||||
# Get colors section from style
|
||||
style_colors = {}
|
||||
raw_style_colors = style.get_colors()
|
||||
|
@ -251,8 +605,6 @@ def komap_mapswithme(options):
|
|||
|
||||
visibility = {}
|
||||
|
||||
bgpos = 0
|
||||
|
||||
dr_linecaps = {'none': BUTTCAP, 'butt': BUTTCAP, 'round': ROUNDCAP}
|
||||
dr_linejoins = {'none': NOJOIN, 'bevel': BEVELJOIN, 'round': ROUNDJOIN}
|
||||
|
||||
|
@ -278,12 +630,14 @@ def komap_mapswithme(options):
|
|||
|
||||
all_draw_elements = set()
|
||||
|
||||
# TODO: refactor next for-loop for readability and testability
|
||||
global validation_errors_count
|
||||
for results in imapfunc(query_style, ((cl, classificator[cl], options.minzoom, options.maxzoom) for cl in class_order)):
|
||||
for result in results:
|
||||
cl, zoom, has_icons_for_areas, runtime_conditions, zstyle = result
|
||||
cl, zoom, runtime_conditions, zstyle = result
|
||||
|
||||
# First, sort rules by 'object-id' in captions (primary, secondary, none ..);
|
||||
# Then by 'z-index' in ascending order.
|
||||
# First, sort rules by ::object-id in captions (primary, secondary, none ..)
|
||||
# then by other ::object-id in ascending order.
|
||||
def rule_sort_key(dict_):
|
||||
first = 0
|
||||
if dict_.get('text'):
|
||||
|
@ -291,12 +645,12 @@ def komap_mapswithme(options):
|
|||
first = 1
|
||||
if str(dict_.get('text')) == 'none':
|
||||
first = 2
|
||||
return (first, int(dict_.get('z-index', 0)))
|
||||
return (first, dict_.get('object-id'))
|
||||
|
||||
zstyle.sort(key = rule_sort_key)
|
||||
|
||||
# For debug purpose.
|
||||
# if str(cl) == 'entrance' and int(zoom) == 19:
|
||||
# if str(cl) == 'highway-path' and int(zoom) == 19:
|
||||
# print(cl)
|
||||
# print(zstyle)
|
||||
|
||||
|
@ -309,6 +663,7 @@ def komap_mapswithme(options):
|
|||
if dr_cont is None:
|
||||
dr_cont = ClassifElementProto()
|
||||
dr_cont.name = cl
|
||||
dr_lines_objects = {}
|
||||
|
||||
visstring = ["0"] * (options.maxzoom - options.minzoom + 1)
|
||||
|
||||
|
@ -322,9 +677,9 @@ def komap_mapswithme(options):
|
|||
st = dict([(k, v) for k, v in st.items() if str(v).strip(" 0.")])
|
||||
if 'width' in st or 'pattern-image' in st:
|
||||
has_lines = True
|
||||
if 'icon-image' in st or 'symbol-shape' in st or 'symbol-image' in st:
|
||||
if 'icon-image' in st and st.get('icon-image') != 'none' or 'symbol-shape' in st or 'symbol-image' in st:
|
||||
has_icons = True
|
||||
if 'fill-color' in st:
|
||||
if 'fill-color' in st and st.get('fill-color') != 'none':
|
||||
has_fills = True
|
||||
|
||||
has_text = None
|
||||
|
@ -341,6 +696,9 @@ def komap_mapswithme(options):
|
|||
|
||||
visstring[zoom] = "1"
|
||||
|
||||
if zoom == 0:
|
||||
continue
|
||||
|
||||
dr_element = DrawElementProto()
|
||||
dr_element.scale = zoom
|
||||
|
||||
|
@ -349,23 +707,36 @@ def komap_mapswithme(options):
|
|||
dr_element.apply_if.append(str(rc))
|
||||
|
||||
for st in zstyle:
|
||||
if st.get('-x-kot-layer') == 'top':
|
||||
st['z-index'] = float(st.get('z-index', 0)) + 15001.
|
||||
elif st.get('-x-kot-layer') == 'bottom':
|
||||
st['z-index'] = float(st.get('z-index', 0)) - 15001.
|
||||
|
||||
if st.get('casing-width') not in (None, 0): # and (st.get('width') or st.get('fill-color')):
|
||||
if st.get('casing-width') not in (None, 0) or st.get('casing-width-add') is not None: # and (st.get('width') or st.get('fill-color')):
|
||||
is_area_st = 'fill-color' in st
|
||||
if has_lines and not is_area_st and st.get('casing-linecap', 'butt') == 'butt':
|
||||
dr_line = LineRuleProto()
|
||||
dr_line.width = (st.get('width', 0) * WIDTH_SCALE) + (st.get('casing-width') * WIDTH_SCALE * 2)
|
||||
|
||||
base_width = st.get('width', 0)
|
||||
if base_width == 0:
|
||||
for wst in zstyle:
|
||||
if wst.get('width') not in (None, 0):
|
||||
# Rail bridge styles use width from ::dash object instead of ::default.
|
||||
if base_width == 0 or wst.get('object-id') != '::default':
|
||||
base_width = wst.get('width', 0)
|
||||
# 'casing-width' has precedence over 'casing-width-add'.
|
||||
if st.get('casing-width') in (None, 0):
|
||||
st['casing-width'] = base_width + st.get('casing-width-add')
|
||||
base_width = 0
|
||||
|
||||
dr_line.width = round(base_width + st.get('casing-width') * 2, 2)
|
||||
dr_line.color = mwm_encode_color(colors, st, "casing")
|
||||
if '-x-me-casing-line-priority' in st:
|
||||
dr_line.priority = int(st.get('-x-me-casing-line-priority'))
|
||||
if st.get('object-id') == '::default':
|
||||
# An automatic casing line should be rendered below the "main" line, hence auto priority -1.
|
||||
auto_comment = 'casing'
|
||||
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'), 'line', auto_comment, -1)
|
||||
store_visibility(cl, 'line', st.get('object-id'), zoom, auto_comment)
|
||||
else:
|
||||
dr_line.priority = min(int(st.get('z-index', 0) + 999), 20000)
|
||||
dashes = st.get('casing-dashes', st.get('dashes', []))
|
||||
dr_line.dashdot.dd.extend(dashes)
|
||||
# A casing line explicitly defined via ::object_id.
|
||||
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'))
|
||||
store_visibility(cl, 'line', st.get('object-id'), zoom)
|
||||
for i in st.get('casing-dashes', st.get('dashes', [])):
|
||||
dr_line.dashdot.dd.extend([float(i)])
|
||||
addPattern(dr_line.dashdot.dd)
|
||||
dr_line.cap = dr_linecaps.get(st.get('casing-linecap', 'butt'), BUTTCAP)
|
||||
dr_line.join = dr_linejoins.get(st.get('casing-linejoin', 'round'), ROUNDJOIN)
|
||||
|
@ -373,12 +744,12 @@ def komap_mapswithme(options):
|
|||
|
||||
if has_fills and is_area_st and float(st.get('fill-opacity', 1)) > 0:
|
||||
dr_element.area.border.color = mwm_encode_color(colors, st, "casing")
|
||||
dr_element.area.border.width = st.get('casing-width', 0) * WIDTH_SCALE
|
||||
dr_element.area.border.width = st.get('casing-width', 0)
|
||||
|
||||
# Let's try without this additional line style overhead. Needed only for casing in road endings.
|
||||
# if st.get('casing-linecap', st.get('linecap', 'round')) != 'butt':
|
||||
# dr_line = LineRuleProto()
|
||||
# dr_line.width = (st.get('width', 0) * WIDTH_SCALE) + (st.get('casing-width') * WIDTH_SCALE * 2)
|
||||
# dr_line.width = st.get('width', 0) + (st.get('casing-width') * 2)
|
||||
# dr_line.color = mwm_encode_color(colors, st, "casing")
|
||||
# dr_line.priority = -15000
|
||||
# dashes = st.get('casing-dashes', st.get('dashes', []))
|
||||
|
@ -390,17 +761,15 @@ def komap_mapswithme(options):
|
|||
if has_lines:
|
||||
if st.get('width'):
|
||||
dr_line = LineRuleProto()
|
||||
dr_line.width = (st.get('width', 0) * WIDTH_SCALE)
|
||||
dr_line.width = st.get('width', 0)
|
||||
dr_line.color = mwm_encode_color(colors, st)
|
||||
for i in st.get('dashes', []):
|
||||
dr_line.dashdot.dd.extend([max(float(i), 1) * WIDTH_SCALE])
|
||||
dr_line.dashdot.dd.extend([float(i)])
|
||||
addPattern(dr_line.dashdot.dd)
|
||||
dr_line.cap = dr_linecaps.get(st.get('linecap', 'butt'), BUTTCAP)
|
||||
dr_line.join = dr_linejoins.get(st.get('linejoin', 'round'), ROUNDJOIN)
|
||||
if '-x-me-line-priority' in st:
|
||||
dr_line.priority = int(st.get('-x-me-line-priority'))
|
||||
else:
|
||||
dr_line.priority = min((int(st.get('z-index', 0)) + 1000), 20000)
|
||||
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'))
|
||||
store_visibility(cl, 'line', st.get('object-id'), zoom)
|
||||
dr_element.lines.extend([dr_line])
|
||||
if st.get('pattern-image'):
|
||||
dr_line = LineRuleProto()
|
||||
|
@ -410,46 +779,38 @@ def komap_mapswithme(options):
|
|||
dr_line.pathsym.name = icon[0]
|
||||
dr_line.pathsym.step = float(st.get('pattern-spacing', 0)) - 16
|
||||
dr_line.pathsym.offset = st.get('pattern-offset', 0)
|
||||
if '-x-me-line-priority' in st:
|
||||
dr_line.priority = int(st.get('-x-me-line-priority'))
|
||||
else:
|
||||
dr_line.priority = int(st.get('z-index', 0)) + 1000
|
||||
dr_line.priority = get_drape_priority(cl, 'line', st.get('object-id'))
|
||||
store_visibility(cl, 'line', st.get('object-id'), zoom)
|
||||
dr_element.lines.extend([dr_line])
|
||||
if st.get('shield-font-size'):
|
||||
dr_element.shield.height = int(st.get('shield-font-size', 10))
|
||||
dr_element.shield.text_color = mwm_encode_color(colors, st, "shield-text")
|
||||
if st.get('shield-text-halo-radius', 0) != 0:
|
||||
dr_element.shield.text_stroke_color = mwm_encode_color(colors, st, "shield-text-halo", "white")
|
||||
dr_element.shield.color = mwm_encode_color(colors, st, "shield")
|
||||
if st.get('shield-outline-radius', 0) != 0:
|
||||
dr_element.shield.stroke_color = mwm_encode_color(colors, st, "shield-outline", "white")
|
||||
if '-x-me-shield-priority' in st:
|
||||
dr_element.shield.priority = int(st.get('-x-me-shield-priority'))
|
||||
else:
|
||||
dr_element.shield.priority = min(19100, (16000 + int(st.get('z-index', 0))))
|
||||
if st.get('shield-min-distance', 0) != 0:
|
||||
dr_element.shield.min_distance = int(st.get('shield-min-distance', 0))
|
||||
|
||||
if st.get('shield-font-size'):
|
||||
dr_element.shield.height = int(st.get('shield-font-size', 10))
|
||||
dr_element.shield.text_color = mwm_encode_color(colors, st, "shield-text")
|
||||
if st.get('shield-text-halo-radius', 0) != 0:
|
||||
dr_element.shield.text_stroke_color = mwm_encode_color(colors, st, "shield-text-halo", "white")
|
||||
dr_element.shield.color = mwm_encode_color(colors, st, "shield")
|
||||
if st.get('shield-outline-radius', 0) != 0:
|
||||
dr_element.shield.stroke_color = mwm_encode_color(colors, st, "shield-outline", "white")
|
||||
dr_element.shield.priority = get_drape_priority(cl, 'shield', st.get('object-id'))
|
||||
store_visibility(cl, 'shield', st.get('object-id'), zoom)
|
||||
if st.get('shield-min-distance', 0) != 0:
|
||||
dr_element.shield.min_distance = int(st.get('shield-min-distance', 0))
|
||||
|
||||
if has_icons:
|
||||
if st.get('icon-image'):
|
||||
if not has_icons_for_areas:
|
||||
dr_element.symbol.apply_for_type = 1
|
||||
if st.get('icon-image') and st.get('icon-image') != 'none':
|
||||
icon = mwm_encode_image(st)
|
||||
dr_element.symbol.name = icon[0]
|
||||
if '-x-me-icon-priority' in st:
|
||||
dr_element.symbol.priority = int(st.get('-x-me-icon-priority'))
|
||||
else:
|
||||
dr_element.symbol.priority = min(19100, (16000 + int(st.get('z-index', 0))))
|
||||
dr_element.symbol.priority = get_drape_priority(cl, 'icon', st.get('object-id'))
|
||||
store_visibility(cl, 'icon', st.get('object-id'), zoom)
|
||||
if 'icon-min-distance' in st:
|
||||
dr_element.symbol.min_distance = int(st.get('icon-min-distance', 0))
|
||||
has_icons = False
|
||||
if st.get('symbol-shape'):
|
||||
# TODO: not used in current styles; do "circles" work in drape at all?
|
||||
dr_element.circle.radius = float(st.get('symbol-size'))
|
||||
dr_element.circle.color = mwm_encode_color(colors, st, 'symbol-fill')
|
||||
if '-x-me-symbol-priority' in st:
|
||||
dr_element.circle.priority = int(st.get('-x-me-symbol-priority'))
|
||||
else:
|
||||
dr_element.circle.priority = min(19000, (14000 + int(st.get('z-index', 0))))
|
||||
dr_element.circle.priority = get_drape_priority(cl, 'circle', st.get('object-id'))
|
||||
store_visibility(cl, 'circle', st.get('object-id'), zoom)
|
||||
has_icons = False
|
||||
|
||||
if has_text and st.get('text') and st.get('text') != 'none':
|
||||
|
@ -457,22 +818,28 @@ def komap_mapswithme(options):
|
|||
has_text = has_text[:2]
|
||||
|
||||
dr_text = dr_element.caption
|
||||
base_z = 15000
|
||||
text_priority_key = 'caption'
|
||||
if st.get('text-position', 'center') == 'line':
|
||||
dr_text = dr_element.path_text
|
||||
base_z = 16000
|
||||
text_priority_key = 'pathtext'
|
||||
|
||||
dr_cur_subtext = dr_text.primary
|
||||
for sp in has_text:
|
||||
dr_cur_subtext.height = int(float(sp.get('font-size', "10").split(",")[0]))
|
||||
if 'text-color' not in st:
|
||||
print(f'ERROR: text-color not set for z{zoom} {cl}')
|
||||
validation_errors_count += 1
|
||||
dr_cur_subtext.color = mwm_encode_color(colors, sp, "text")
|
||||
if st.get('text-halo-radius', 0) != 0:
|
||||
dr_cur_subtext.stroke_color = mwm_encode_color(colors, sp, "text-halo", "white")
|
||||
if 'text-offset' in sp or 'text-offset-y' in sp:
|
||||
dr_cur_subtext.offset_y = int(sp.get('text-offset-y', sp.get('text-offset', 0)))
|
||||
if 'text-offset-x' in sp:
|
||||
elif 'text-offset-x' in sp:
|
||||
dr_cur_subtext.offset_x = int(sp.get('text-offset-x', 0))
|
||||
if 'text' in sp and sp.get('text') != 'name':
|
||||
elif st.get('text-position', 'center') == 'center' and dr_element.symbol.priority:
|
||||
print(f'ERROR: an icon is present, but caption\'s text-offset is not set for z{zoom} {cl}')
|
||||
validation_errors_count += 1
|
||||
if 'text' in sp and sp.get('text') not in ('name', 'int_name'):
|
||||
dr_cur_subtext.text = sp.get('text')
|
||||
if 'text-optional' in sp:
|
||||
is_valid, value = to_boolean(sp.get('text-optional', ''))
|
||||
|
@ -480,40 +847,37 @@ def komap_mapswithme(options):
|
|||
dr_cur_subtext.is_optional = value
|
||||
else:
|
||||
dr_cur_subtext.is_optional = True
|
||||
elif text_priority_key == 'caption' and dr_element.symbol.priority:
|
||||
# On by default for all captions (not path texts) with icons.
|
||||
dr_cur_subtext.is_optional = True
|
||||
dr_cur_subtext = dr_text.secondary
|
||||
|
||||
# Priority is assigned from the first (primary) rule.
|
||||
if '-x-me-text-priority' in st:
|
||||
dr_text.priority = int(st.get('-x-me-text-priority'))
|
||||
auto_comment = None
|
||||
if text_priority_key == 'caption' and dr_element.symbol.priority:
|
||||
# A caption with an icon.
|
||||
# Mandatory captions use icon's priority.
|
||||
auto_prio_mod = 0
|
||||
auto_comment = 'mandatory'
|
||||
if dr_text.primary.is_optional:
|
||||
# Optional captions are automatically placed below most other overlays.
|
||||
auto_comment = 'optional'
|
||||
auto_prio_mod = -OVERLAYS_MAX_PRIORITY
|
||||
dr_text.priority = get_drape_priority(cl, 'icon', st.get('object-id'),
|
||||
text_priority_key, auto_comment, auto_prio_mod)
|
||||
else:
|
||||
dr_text.priority = min(19000, (base_z + int(st.get('z-index', 0))))
|
||||
if '-x-me-min-text-priority' in st:
|
||||
min_priority = int(st.get('-x-me-min-text-priority'))
|
||||
dr_text.priority = max(min_priority, dr_text.priority)
|
||||
# A pathtext or a standalone caption.
|
||||
dr_text.priority = get_drape_priority(cl, text_priority_key, st.get('object-id'))
|
||||
|
||||
store_visibility(cl, text_priority_key, st.get('object-id'), zoom, auto_comment)
|
||||
|
||||
# Process captions block once.
|
||||
has_text = None
|
||||
|
||||
if has_fills:
|
||||
if ('fill-color' in st) and (float(st.get('fill-opacity', 1)) > 0):
|
||||
if 'fill-color' in st and st.get('fill-color') != 'none' and float(st.get('fill-opacity', 1)) > 0:
|
||||
dr_element.area.color = mwm_encode_color(colors, st, "fill")
|
||||
priority = 0
|
||||
if st.get('fill-position', 'foreground') == 'background':
|
||||
if 'z-index' not in st:
|
||||
bgpos -= 1
|
||||
priority = bgpos - 16000
|
||||
else:
|
||||
zzz = int(st.get('z-index', 0))
|
||||
if zzz > 0:
|
||||
priority = zzz - 16000
|
||||
else:
|
||||
priority = zzz - 16700
|
||||
else:
|
||||
priority = (int(st.get('z-index', 0)) + 1 + 1000)
|
||||
if '-x-me-area-priority' in st:
|
||||
dr_element.area.priority = int(st.get('-x-me-area-priority'))
|
||||
else:
|
||||
dr_element.area.priority = priority
|
||||
dr_element.area.priority = get_drape_priority(cl, 'area', st.get('object-id'))
|
||||
store_visibility(cl, 'area', st.get('object-id'), zoom)
|
||||
has_fills = False
|
||||
|
||||
str_dr_element = dr_cont.name + "/" + str(dr_element)
|
||||
|
@ -527,6 +891,20 @@ def komap_mapswithme(options):
|
|||
|
||||
visibility["world|" + class_tree[cl] + "|"] = "".join(visstring)
|
||||
|
||||
validate_visibilities(options.maxzoom)
|
||||
|
||||
if validation_errors_count:
|
||||
print()
|
||||
exit('FAILED to write regenerated drules files!\n'
|
||||
f'There are {validation_errors_count} validation errors (see in the log above).\n'
|
||||
'Fix all errors first and re-run.')
|
||||
|
||||
output = ''
|
||||
for prio_range in prio_ranges.keys():
|
||||
dump_priorities(prio_range, options.priorities_path, options.maxzoom)
|
||||
output += f'{"" if not output else ", "}{len(prio_ranges[prio_range]["priorities"])} {prio_range}'
|
||||
print(f'Re-formated priorities files: {output}.')
|
||||
|
||||
# Write drules_proto.bin and drules_proto.txt files
|
||||
|
||||
drules_bin = open(os.path.join(options.outfile + '.bin'), "wb")
|
||||
|
@ -557,6 +935,7 @@ def komap_mapswithme(options):
|
|||
return -1
|
||||
viskeys.sort(key=functools.cmp_to_key(cmprepl))
|
||||
|
||||
# TODO: Introduce new function to dump `visibility.txt` and `classificator.txt` for better testability
|
||||
visibility_file = open(os.path.join(ddir, 'visibility.txt'), "w")
|
||||
classificator_file = open(os.path.join(ddir, 'classificator.txt'), "w")
|
||||
|
||||
|
@ -579,11 +958,13 @@ def komap_mapswithme(options):
|
|||
visibility_file.close()
|
||||
classificator_file.close()
|
||||
|
||||
# TODO: Introduce new function to dump `colors.txt` for better testability
|
||||
colors_file = open(colors_file_name, "w")
|
||||
for c in sorted(colors):
|
||||
colors_file.write("%d\n" % (c))
|
||||
colors_file.close()
|
||||
|
||||
# TODO: Introduce new function to dump `patterns.txt` for better testability
|
||||
patterns_file = open(patterns_file_name, "w")
|
||||
for p in patterns:
|
||||
patterns_file.write("%s\n" % (' '.join(str(elem) for elem in p)))
|
||||
|
@ -596,12 +977,14 @@ def main():
|
|||
help="read MapCSS stylesheet from FILE", metavar="FILE")
|
||||
parser.add_option("-f", "--minzoom", dest="minzoom", default=0, type="int",
|
||||
help="minimal available zoom level", metavar="ZOOM")
|
||||
parser.add_option("-t", "--maxzoom", dest="maxzoom", default=19, type="int",
|
||||
parser.add_option("-t", "--maxzoom", dest="maxzoom", default=20, type="int",
|
||||
help="maximal available zoom level", metavar="ZOOM")
|
||||
parser.add_option("-o", "--output-file", dest="outfile", default="-",
|
||||
help="output filename", metavar="FILE")
|
||||
parser.add_option("-x", "--txt", dest="txt", action="store_true",
|
||||
help="create a text file for output", default=False)
|
||||
parser.add_option("-p", "--priorities-path", dest="priorities_path",
|
||||
help="path to priorities *.prio.txt files", metavar="PATH")
|
||||
parser.add_option("-d", "--data-path", dest="data",
|
||||
help="path to mapcss-mapping.csv and other files", metavar="PATH")
|
||||
|
||||
|
@ -613,6 +996,10 @@ def main():
|
|||
if options.outfile == "-":
|
||||
parser.error("Please specify base output path.")
|
||||
|
||||
if (options.priorities_path is None or not os.path.isdir(options.priorities_path)):
|
||||
parser.error("A path to priorities *.prio.txt files is required.")
|
||||
options.priorities_path = os.path.normpath(options.priorities_path)
|
||||
|
||||
komap_mapswithme(options)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -24,7 +24,7 @@ class Condition:
|
|||
params = (params,)
|
||||
self.params = params # e.g. ('highway','primary')
|
||||
if typez == "regex":
|
||||
self.regex = re.compile(self.params[0], re.I)
|
||||
self.regex = re.compile(self.params[1], re.I)
|
||||
|
||||
def extract_tag(self):
|
||||
if self.params[0][:2] == "::" or self.type == "regex":
|
||||
|
@ -33,49 +33,51 @@ class Condition:
|
|||
|
||||
def test(self, tags):
|
||||
"""
|
||||
Test a hash against this condition
|
||||
Test tags against this condition
|
||||
"""
|
||||
t = self.type
|
||||
params = self.params
|
||||
if t == 'eq': # don't compare tags against sublayers
|
||||
|
||||
if t == 'eq':
|
||||
# Don't compare tags against sublayers
|
||||
if params[0][:2] == "::":
|
||||
return params[1]
|
||||
try:
|
||||
if t == 'eq':
|
||||
return tags[params[0]] == params[1]
|
||||
if t == 'ne':
|
||||
return tags.get(params[0], "") != params[1]
|
||||
if t == 'regex':
|
||||
return bool(self.regex.match(tags[params[0]]))
|
||||
if t == 'true':
|
||||
return tags.get(params[0]) == 'yes'
|
||||
if t == 'untrue':
|
||||
return tags.get(params[0]) == 'no'
|
||||
if t == 'set':
|
||||
if params[0] in tags:
|
||||
return tags[params[0]] != ''
|
||||
return False
|
||||
if t == 'unset':
|
||||
if params[0] in tags:
|
||||
return tags[params[0]] == ''
|
||||
return True
|
||||
if t == '<':
|
||||
return (Number(tags[params[0]]) < Number(params[1]))
|
||||
if t == '<=':
|
||||
return (Number(tags[params[0]]) <= Number(params[1]))
|
||||
if t == '>':
|
||||
return (Number(tags[params[0]]) > Number(params[1]))
|
||||
if t == '>=':
|
||||
return (Number(tags[params[0]]) >= Number(params[1]))
|
||||
except KeyError:
|
||||
pass
|
||||
return (params[0] in tags and tags[params[0]] == params[1])
|
||||
if t == 'ne':
|
||||
return (params[0] not in tags or tags[params[0]] != params[1])
|
||||
if t == 'true':
|
||||
return tags.get(params[0]) == 'yes'
|
||||
if t == 'untrue':
|
||||
return tags.get(params[0]) == 'no'
|
||||
if t == 'set':
|
||||
if params[0] in tags:
|
||||
return tags[params[0]] != ''
|
||||
return False
|
||||
if t == 'unset':
|
||||
if params[0] in tags:
|
||||
return tags[params[0]] == ''
|
||||
return True
|
||||
|
||||
if params[0] not in tags:
|
||||
return False
|
||||
if t == 'regex':
|
||||
return bool(self.regex.match(tags[params[0]]))
|
||||
if t == '<':
|
||||
return (Number(tags[params[0]]) < Number(params[1]))
|
||||
if t == '<=':
|
||||
return (Number(tags[params[0]]) <= Number(params[1]))
|
||||
if t == '>':
|
||||
return (Number(tags[params[0]]) > Number(params[1]))
|
||||
if t == '>=':
|
||||
return (Number(tags[params[0]]) >= Number(params[1]))
|
||||
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
t = self.type
|
||||
params = self.params
|
||||
if t == 'eq' and params[0][:2] == "::":
|
||||
return "::%s" % (params[1])
|
||||
return "%s" % (params[1])
|
||||
if t == 'eq':
|
||||
return "%s=%s" % (params[0], params[1])
|
||||
if t == 'ne':
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with kothic. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('mapcss.Eval')
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
class Eval():
|
||||
def __init__(self, s='eval()'):
|
||||
|
@ -57,6 +61,8 @@ class Eval():
|
|||
"any": fake_compute,
|
||||
"min": fake_compute,
|
||||
"max": fake_compute,
|
||||
"cond": fake_compute,
|
||||
"boolean": fake_compute,
|
||||
})
|
||||
return tags
|
||||
|
||||
|
@ -99,12 +105,15 @@ class Eval():
|
|||
return "{:.4g}".format(result)
|
||||
|
||||
return str(result)
|
||||
except:
|
||||
except Exception as e:
|
||||
logger.warning(f"Error evaluating expression `{self.expr_text}`", e)
|
||||
return ""
|
||||
|
||||
def __repr__(self):
|
||||
return "eval(%s)" % self.expr_text
|
||||
|
||||
def __eq__(self, other):
|
||||
return type(self) == type(other) and self.expr_text == other.expr_text
|
||||
|
||||
def m_boolean(expr):
|
||||
expr = str(expr)
|
||||
|
|
|
@ -25,7 +25,7 @@ type_matches = {
|
|||
|
||||
class Rule():
|
||||
def __init__(self, s=''):
|
||||
self.runtime_conditions = []
|
||||
self.runtime_conditions = None
|
||||
self.conditions = []
|
||||
# self.isAnd = True
|
||||
self.minZoom = 0
|
||||
|
@ -33,17 +33,12 @@ class Rule():
|
|||
if s == "*":
|
||||
s = ""
|
||||
self.subject = s # "", "way", "node" or "relation"
|
||||
self.type_matches = type_matches[s] if s in type_matches else set()
|
||||
|
||||
def __repr__(self):
|
||||
return "%s|z%s-%s %s %s" % (self.subject, self.minZoom, self.maxZoom, self.conditions, self.runtime_conditions)
|
||||
|
||||
def test(self, obj, tags, zoom):
|
||||
if (zoom < self.minZoom) or (zoom > self.maxZoom):
|
||||
return False
|
||||
|
||||
if (self.subject != '') and not _test_feature_compatibility(obj, self.subject, tags):
|
||||
return False
|
||||
|
||||
def test(self, tags):
|
||||
subpart = "::default"
|
||||
for condition in self.conditions:
|
||||
res = condition.test(tags)
|
||||
|
@ -59,33 +54,10 @@ class Rule():
|
|||
def extract_tags(self):
|
||||
a = set()
|
||||
for condition in self.conditions:
|
||||
a.add(condition.extract_tag())
|
||||
if "*" in a:
|
||||
a = set(["*"])
|
||||
break
|
||||
tag = condition.extract_tag()
|
||||
if tag != '*':
|
||||
a.add(tag)
|
||||
elif len(a) == 0:
|
||||
return set(["*"])
|
||||
|
||||
return a
|
||||
|
||||
|
||||
def _test_feature_compatibility(f1, f2, tags={}):
|
||||
"""
|
||||
Checks if feature of type f1 is compatible with f2.
|
||||
"""
|
||||
if f2 == f1:
|
||||
return True
|
||||
if f2 not in ("way", "area", "line"):
|
||||
return False
|
||||
elif f2 == "way" and f1 == "line":
|
||||
return True
|
||||
elif f2 == "way" and f1 == "area":
|
||||
return True
|
||||
elif f2 == "area" and f1 in ("way", "area"):
|
||||
# if ":area" in tags:
|
||||
return True
|
||||
# else:
|
||||
# return False
|
||||
elif f2 == "line" and f1 in ("way", "line", "area"):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
# print f1, f2, True
|
||||
return True
|
||||
|
|
|
@ -30,7 +30,7 @@ def make_nice_style(r):
|
|||
"checking and nicifying style table"
|
||||
if type(b) == TYPE_EVAL:
|
||||
ra[a] = b
|
||||
elif "color" in a:
|
||||
elif "color" in a and b.strip() != 'none':
|
||||
"parsing color value to 3-tuple"
|
||||
# print "res:", b
|
||||
if b and (type(b) != tuple):
|
||||
|
@ -40,7 +40,7 @@ def make_nice_style(r):
|
|||
ra[a] = colorparser(b)
|
||||
elif b:
|
||||
ra[a] = b
|
||||
elif any(x in a for x in ("width", "z-index", "opacity", "offset", "radius", "extrude")):
|
||||
elif any(x in a for x in ("width", "opacity", "offset", "radius", "extrude")):
|
||||
"these things are float's or not in table at all"
|
||||
try:
|
||||
ra[a] = float(b)
|
||||
|
@ -76,6 +76,8 @@ class StyleChooser:
|
|||
The styles property is an array of all the style objects to be drawn
|
||||
if any of the ruleChains evaluate to true.
|
||||
"""
|
||||
# TODO: use logging for debug logs
|
||||
|
||||
def __repr__(self):
|
||||
return "{(%s) : [%s] }\n" % (self.ruleChains, self.styles)
|
||||
|
||||
|
@ -87,6 +89,7 @@ class StyleChooser:
|
|||
self.selzooms = None
|
||||
self.compatible_types = set()
|
||||
self.has_evals = False
|
||||
self.has_runtime_conditions = False
|
||||
self.cached_tags = None
|
||||
|
||||
def extract_tags(self):
|
||||
|
@ -96,62 +99,35 @@ class StyleChooser:
|
|||
for r in self.ruleChains:
|
||||
a.update(r.extract_tags())
|
||||
if "*" in a:
|
||||
a.clear()
|
||||
a.add("*")
|
||||
a = set('*')
|
||||
break
|
||||
if self.has_evals and "*" not in a:
|
||||
for s in self.styles:
|
||||
for v in list(s.values()):
|
||||
if type(v) == self.eval_type:
|
||||
a.update(v.extract_tags())
|
||||
if "*" in a or len(a) == 0:
|
||||
a.clear()
|
||||
a.add("*")
|
||||
if len(a) == 0:
|
||||
a = set('*')
|
||||
self.cached_tags = a
|
||||
return a
|
||||
|
||||
def get_runtime_conditions(self, ftype, tags, zoom):
|
||||
if self.selzooms:
|
||||
if zoom < self.selzooms[0] or zoom > self.selzooms[1]:
|
||||
return None
|
||||
def get_runtime_conditions(self, tags):
|
||||
if not self.has_runtime_conditions:
|
||||
return None
|
||||
|
||||
rule_and_object_id = self.testChain(self.ruleChains, ftype, tags, zoom)
|
||||
rule_and_object_id = self.testChains(tags)
|
||||
|
||||
if not rule_and_object_id:
|
||||
return None
|
||||
|
||||
rule = rule_and_object_id[0]
|
||||
|
||||
if (len(rule.runtime_conditions) == 0):
|
||||
return None
|
||||
|
||||
return rule.runtime_conditions
|
||||
|
||||
def isCorrespondingRule(self, filter_by_runtime_conditions, rule):
|
||||
# If rule can be applied according to runtime conditions, then
|
||||
# function return true, else it returns false
|
||||
if len(rule.runtime_conditions) == 0:
|
||||
return True
|
||||
if filter_by_runtime_conditions is None:
|
||||
return True
|
||||
if filter_by_runtime_conditions == rule.runtime_conditions:
|
||||
return True
|
||||
# Actually we should check rule.runtime_conditions is a subset of filter_by_runtime_conditions
|
||||
for r in rule.runtime_conditions:
|
||||
if r not in filter_by_runtime_conditions:
|
||||
return False
|
||||
return True
|
||||
|
||||
def updateStyles(self, sl, ftype, tags, zoom, xscale, zscale, filter_by_runtime_conditions):
|
||||
# TODO: Rename to "applyStyles"
|
||||
def updateStyles(self, sl, tags, xscale, zscale, filter_by_runtime_conditions):
|
||||
# Are any of the ruleChains fulfilled?
|
||||
if self.selzooms:
|
||||
if zoom < self.selzooms[0] or zoom > self.selzooms[1]:
|
||||
return sl
|
||||
|
||||
#if ftype not in self.compatible_types:
|
||||
#return sl
|
||||
|
||||
rule_and_object_id = self.testChain(self.ruleChains, ftype, tags, zoom)
|
||||
rule_and_object_id = self.testChains(tags)
|
||||
|
||||
if not rule_and_object_id:
|
||||
return sl
|
||||
|
@ -159,7 +135,9 @@ class StyleChooser:
|
|||
rule = rule_and_object_id[0]
|
||||
object_id = rule_and_object_id[1]
|
||||
|
||||
if not self.isCorrespondingRule(filter_by_runtime_conditions, rule):
|
||||
if (filter_by_runtime_conditions is not None
|
||||
and rule.runtime_conditions is not None
|
||||
and filter_by_runtime_conditions != rule.runtime_conditions):
|
||||
return sl
|
||||
|
||||
for r in self.styles:
|
||||
|
@ -168,6 +146,7 @@ class StyleChooser:
|
|||
for a, b in r.items():
|
||||
"calculating eval()'s"
|
||||
if type(b) == self.eval_type:
|
||||
# TODO: Move next block to a separate function
|
||||
combined_style = {}
|
||||
for t in sl:
|
||||
combined_style.update(t)
|
||||
|
@ -203,12 +182,12 @@ class StyleChooser:
|
|||
|
||||
return sl
|
||||
|
||||
def testChain(self, chain, obj, tags, zoom):
|
||||
def testChains(self, tags):
|
||||
"""
|
||||
Tests an object against a chain
|
||||
"""
|
||||
for r in chain:
|
||||
tt = r.test(obj, tags, zoom)
|
||||
for r in self.ruleChains:
|
||||
tt = r.test(tags)
|
||||
if tt:
|
||||
return r, tt
|
||||
return False
|
||||
|
@ -249,14 +228,18 @@ class StyleChooser:
|
|||
"""
|
||||
adds into the current ruleChain (existing Rule)
|
||||
"""
|
||||
self.ruleChains[-1].runtime_conditions.append(c)
|
||||
self.ruleChains[-1].runtime_conditions.sort()
|
||||
if self.ruleChains[-1].runtime_conditions is None:
|
||||
self.ruleChains[-1].runtime_conditions = [c]
|
||||
self.has_runtime_conditions = True
|
||||
else:
|
||||
self.ruleChains[-1].runtime_conditions.append(c)
|
||||
|
||||
def addStyles(self, a):
|
||||
# print "addStyle ", a
|
||||
"""
|
||||
adds to this.styles
|
||||
"""
|
||||
# TODO: move next for-loop to a new method. Don't call it on every style append
|
||||
for r in self.ruleChains:
|
||||
if not self.selzooms:
|
||||
self.selzooms = [r.minZoom, r.maxZoom]
|
||||
|
@ -277,9 +260,6 @@ class StyleChooser:
|
|||
b = str(float(b) / 2)
|
||||
except:
|
||||
pass
|
||||
if "text" == a[-4:]:
|
||||
if b[:5] != "eval(":
|
||||
b = "eval(tag(\"" + b + "\"))"
|
||||
if b[:5] == "eval(":
|
||||
b = Eval(b)
|
||||
self.has_evals = True
|
||||
|
|
|
@ -22,9 +22,10 @@ from .StyleChooser import StyleChooser
|
|||
from .Condition import Condition
|
||||
|
||||
|
||||
NEEDED_KEYS = set(["width", "casing-width", "fill-color", "fill-image", "icon-image", "text", "extrude",
|
||||
"background-image", "background-color", "pattern-image", "shield-text", "symbol-shape"])
|
||||
NEEDED_KEYS = set(["width", "casing-width", "casing-width-add", "fill-color", "fill-image", "icon-image", "text", "extrude",
|
||||
"background-image", "background-color", "pattern-image", "shield-color", "symbol-shape"])
|
||||
|
||||
# TODO: Unused constant
|
||||
WHITESPACE = re.compile(r'\s+ ', re.S | re.X)
|
||||
|
||||
COMMENT = re.compile(r'\/\* .*? \*\/ \s* ', re.S | re.X)
|
||||
|
@ -40,29 +41,30 @@ VARIABLE_SET = re.compile(r'@([a-z][\w\d]*) \s* : \s* (.+?) \s* ; \s* ', re.S |
|
|||
UNKNOWN = re.compile(r'(\S+) \s* ', re.S | re.X)
|
||||
|
||||
ZOOM_MINMAX = re.compile(r'(\d+)\-(\d+) $', re.S | re.X)
|
||||
ZOOM_MIN = re.compile(r'(\d+)\- $', re.S | re.X)
|
||||
ZOOM_MAX = re.compile(r' \-(\d+) $', re.S | re.X)
|
||||
ZOOM_MIN = re.compile(r'(\d+)\- $', re.S | re.X)
|
||||
ZOOM_MAX = re.compile(r' \-(\d+) $', re.S | re.X)
|
||||
ZOOM_SINGLE = re.compile(r' (\d+) $', re.S | re.X)
|
||||
|
||||
CONDITION_TRUE = re.compile(r'\s* ([:\w]+) \s* [?] \s* $', re.I | re.S | re.X)
|
||||
# TODO: move to Condition.py
|
||||
CONDITION_TRUE = re.compile(r'\s* ([:\w]+) \s* [?] \s* $', re.I | re.S | re.X)
|
||||
CONDITION_invTRUE = re.compile(r'\s* [!] \s* ([:\w]+) \s* [?] \s* $', re.I | re.S | re.X)
|
||||
CONDITION_FALSE = re.compile(r'\s* ([:\w]+) \s* = \s* no \s* $', re.I | re.S | re.X)
|
||||
CONDITION_SET = re.compile(r'\s* ([-:\w]+) \s* $', re.S | re.X)
|
||||
CONDITION_UNSET = re.compile(r'\s* !([:\w]+) \s* $', re.S | re.X)
|
||||
CONDITION_EQ = re.compile(r'\s* ([:\w]+) \s* = \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_NE = re.compile(r'\s* ([:\w]+) \s* != \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_GT = re.compile(r'\s* ([:\w]+) \s* > \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_GE = re.compile(r'\s* ([:\w]+) \s* >= \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_LT = re.compile(r'\s* ([:\w]+) \s* < \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_LE = re.compile(r'\s* ([:\w]+) \s* <= \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_REGEX = re.compile(r'\s* ([:\w]+) \s* =~\/ \s* (.+) \/ \s* $', re.S | re.X)
|
||||
CONDITION_FALSE = re.compile(r'\s* ([:\w]+) \s* = \s* no \s* $', re.I | re.S | re.X)
|
||||
CONDITION_SET = re.compile(r'\s* ([-:\w]+) \s* $', re.S | re.X)
|
||||
CONDITION_UNSET = re.compile(r'\s* !([:\w]+) \s* $', re.S | re.X)
|
||||
CONDITION_EQ = re.compile(r'\s* ([:\w]+) \s* = \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_NE = re.compile(r'\s* ([:\w]+) \s* != \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_GT = re.compile(r'\s* ([:\w]+) \s* > \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_GE = re.compile(r'\s* ([:\w]+) \s* >= \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_LT = re.compile(r'\s* ([:\w]+) \s* < \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_LE = re.compile(r'\s* ([:\w]+) \s* <= \s* (.+) \s* $', re.S | re.X)
|
||||
CONDITION_REGEX = re.compile(r'\s* ([:\w]+) \s* =~\/ \s* (.+) \/ \s* $', re.S | re.X)
|
||||
|
||||
ASSIGNMENT_EVAL = re.compile(r"\s* (\S+) \s* \: \s* eval \s* \( \s* ' (.+?) ' \s* \) \s* $", re.I | re.S | re.X)
|
||||
ASSIGNMENT = re.compile(r'\s* (\S+) \s* \: \s* (.+?) \s* $', re.S | re.X)
|
||||
SET_TAG_EVAL = re.compile(r"\s* set \s+(\S+)\s* = \s* eval \s* \( \s* ' (.+?) ' \s* \) \s* $", re.I | re.S | re.X)
|
||||
SET_TAG = re.compile(r'\s* set \s+(\S+)\s* = \s* (.+?) \s* $', re.I | re.S | re.X)
|
||||
SET_TAG_TRUE = re.compile(r'\s* set \s+(\S+)\s* $', re.I | re.S | re.X)
|
||||
EXIT = re.compile(r'\s* exit \s* $', re.I | re.S | re.X)
|
||||
ASSIGNMENT = re.compile(r'\s* (\S+) \s* \: \s* (.+?) \s* $', re.S | re.X)
|
||||
SET_TAG_EVAL = re.compile(r"\s* set \s+(\S+)\s* = \s* eval \s* \( \s* ' (.+?) ' \s* \) \s* $", re.I | re.S | re.X)
|
||||
SET_TAG = re.compile(r'\s* set \s+(\S+)\s* = \s* (.+?) \s* $', re.I | re.S | re.X)
|
||||
SET_TAG_TRUE = re.compile(r'\s* set \s+(\S+)\s* $', re.I | re.S | re.X)
|
||||
EXIT = re.compile(r'\s* exit \s* $', re.I | re.S | re.X)
|
||||
|
||||
oNONE = 0
|
||||
oZOOM = 2
|
||||
|
@ -73,6 +75,7 @@ oDECLARATION = 6
|
|||
oSUBPART = 7
|
||||
oVARIABLE_SET = 8
|
||||
|
||||
# TODO: Following block of variables is never used
|
||||
DASH = re.compile(r'\-/g')
|
||||
COLOR = re.compile(r'color$/')
|
||||
BOLD = re.compile(r'^bold$/i')
|
||||
|
@ -81,6 +84,7 @@ UNDERLINE = re.compile(r'^underline$/i')
|
|||
CAPS = re.compile(r'^uppercase$/i')
|
||||
CENTER = re.compile(r'^center$/i')
|
||||
|
||||
# TODO: Remove unused HEX variable
|
||||
HEX = re.compile(r'^#([0-9a-f]+)$/i')
|
||||
VARIABLE = re.compile(r'@([a-z][\w\d]*)')
|
||||
|
||||
|
@ -96,8 +100,9 @@ class MapCSS():
|
|||
self.scalepair = (minscale, maxscale)
|
||||
self.choosers = []
|
||||
self.choosers_by_type = {}
|
||||
self.choosers_by_type_and_tag = {}
|
||||
self.choosers_by_type_zoom_tag = {}
|
||||
self.variables = {}
|
||||
self.unused_variables = set()
|
||||
self.style_loaded = False
|
||||
|
||||
def parseZoom(self, s):
|
||||
|
@ -110,47 +115,67 @@ class MapCSS():
|
|||
elif ZOOM_SINGLE.match(s):
|
||||
return float(ZOOM_SINGLE.match(s).groups()[0]), float(ZOOM_SINGLE.match(s).groups()[0])
|
||||
else:
|
||||
# TODO: Should we raise an exception here?
|
||||
logging.error("unparsed zoom: %s" % s)
|
||||
|
||||
def build_choosers_tree(self, clname, type, tags={}):
|
||||
if type not in self.choosers_by_type_and_tag:
|
||||
self.choosers_by_type_and_tag[type] = {}
|
||||
if clname not in self.choosers_by_type_and_tag[type]:
|
||||
self.choosers_by_type_and_tag[type][clname] = set()
|
||||
def build_choosers_tree(self, clname, type, cltag):
|
||||
if type not in self.choosers_by_type_zoom_tag:
|
||||
self.choosers_by_type_zoom_tag[type] = {}
|
||||
for zoom in range(self.minscale, self.maxscale + 1):
|
||||
if zoom not in self.choosers_by_type_zoom_tag[type]:
|
||||
self.choosers_by_type_zoom_tag[type][zoom] = {}
|
||||
if clname not in self.choosers_by_type_zoom_tag[type][zoom]:
|
||||
self.choosers_by_type_zoom_tag[type][zoom][clname] = {'arr': [], 'set': set()}
|
||||
if type in self.choosers_by_type:
|
||||
for chooser in self.choosers_by_type[type]:
|
||||
for tag in chooser.extract_tags():
|
||||
if tag == "*" or tag in tags:
|
||||
if chooser not in self.choosers_by_type_and_tag[type][clname]:
|
||||
self.choosers_by_type_and_tag[type][clname].add(chooser)
|
||||
break
|
||||
chooser_tags = chooser.extract_tags()
|
||||
if '*' in chooser_tags or cltag in chooser_tags:
|
||||
for zoom in range(int(chooser.selzooms[0]), int(chooser.selzooms[1]) + 1):
|
||||
if chooser not in self.choosers_by_type_zoom_tag[type][zoom][clname]['set']:
|
||||
self.choosers_by_type_zoom_tag[type][zoom][clname]['arr'].append(chooser)
|
||||
self.choosers_by_type_zoom_tag[type][zoom][clname]['set'].add(chooser)
|
||||
|
||||
def finalize_choosers_tree(self):
|
||||
for ftype in self.choosers_by_type_zoom_tag.keys():
|
||||
for zoom in self.choosers_by_type_zoom_tag[ftype].keys():
|
||||
for clname in self.choosers_by_type_zoom_tag[ftype][zoom].keys():
|
||||
# Discard unneeded unique set of choosers.
|
||||
self.choosers_by_type_zoom_tag[ftype][zoom][clname] = self.choosers_by_type_zoom_tag[ftype][zoom][clname]['arr']
|
||||
for i in range(0, len(self.choosers_by_type_zoom_tag[ftype][zoom][clname])):
|
||||
chooser = self.choosers_by_type_zoom_tag[ftype][zoom][clname][i]
|
||||
optimized = StyleChooser(chooser.scalepair)
|
||||
optimized.styles = chooser.styles
|
||||
optimized.eval_type = chooser.eval_type
|
||||
optimized.has_evals = chooser.has_evals
|
||||
optimized.has_runtime_conditions = chooser.has_runtime_conditions
|
||||
optimized.selzooms = [zoom, zoom]
|
||||
optimized.ruleChains = []
|
||||
for rule in chooser.ruleChains:
|
||||
# Discard chooser's rules that don't match type or zoom.
|
||||
if ftype in rule.type_matches and zoom >= rule.minZoom and zoom <= rule.maxZoom:
|
||||
optimized.ruleChains.append(rule)
|
||||
self.choosers_by_type_zoom_tag[ftype][zoom][clname][i] = optimized
|
||||
|
||||
def restore_choosers_order(self, type):
|
||||
ethalon_choosers = self.choosers_by_type[type]
|
||||
for tag, choosers_for_tag in list(self.choosers_by_type_and_tag[type].items()):
|
||||
tmp = []
|
||||
for ec in ethalon_choosers:
|
||||
if ec in choosers_for_tag:
|
||||
tmp.append(ec)
|
||||
self.choosers_by_type_and_tag[type][tag] = tmp
|
||||
|
||||
def get_runtime_rules(self, clname, type, tags, zoom):
|
||||
"""
|
||||
Returns array of runtime_conditions which are used for clname/type/tags/zoom
|
||||
"""
|
||||
runtime_rules = []
|
||||
if type in self.choosers_by_type_and_tag:
|
||||
for chooser in self.choosers_by_type_and_tag[type][clname]:
|
||||
runtime_conditions = chooser.get_runtime_conditions(type, tags, zoom)
|
||||
if type in self.choosers_by_type_zoom_tag:
|
||||
for chooser in self.choosers_by_type_zoom_tag[type][zoom][clname]:
|
||||
runtime_conditions = chooser.get_runtime_conditions(tags)
|
||||
if runtime_conditions:
|
||||
runtime_rules.append(runtime_conditions)
|
||||
return runtime_rules
|
||||
|
||||
# TODO: Renamed to `get_styles` because it returns a list of styles for each class `::XXX`
|
||||
# Refactoring idea: Maybe return dict with `object-id` as a key
|
||||
def get_style(self, clname, type, tags, zoom, xscale, zscale, filter_by_runtime_conditions):
|
||||
style = []
|
||||
if type in self.choosers_by_type_and_tag:
|
||||
for chooser in self.choosers_by_type_and_tag[type][clname]:
|
||||
style = chooser.updateStyles(style, type, tags, zoom, xscale, zscale, filter_by_runtime_conditions)
|
||||
if type in self.choosers_by_type_zoom_tag:
|
||||
for chooser in self.choosers_by_type_zoom_tag[type][zoom][clname]:
|
||||
style = chooser.updateStyles(style, tags, xscale, zscale, filter_by_runtime_conditions)
|
||||
style = [x for x in style if x["object-id"] != "::*"]
|
||||
for x in style:
|
||||
for k, v in [('width', 0), ('casing-width', 0)]:
|
||||
|
@ -190,6 +215,8 @@ class MapCSS():
|
|||
|
||||
def get_variable(self, m):
|
||||
name = m.group()[1:]
|
||||
if name in self.unused_variables:
|
||||
self.unused_variables.remove(name)
|
||||
if not name in self.variables:
|
||||
raise Exception("Variable not found: " + str(format(name)))
|
||||
return self.variables[name] if name in self.variables else m.group()
|
||||
|
@ -202,7 +229,8 @@ class MapCSS():
|
|||
if filename:
|
||||
basepath = os.path.dirname(filename)
|
||||
if not css:
|
||||
css = open(filename).read()
|
||||
with open(filename) as css_file:
|
||||
css = css_file.read()
|
||||
if not self.style_loaded:
|
||||
self.choosers = []
|
||||
|
||||
|
@ -322,7 +350,8 @@ class MapCSS():
|
|||
import_filename = os.path.join(basepath, IMPORT.match(css).groups()[0])
|
||||
try:
|
||||
css = IMPORT.sub("", css, 1)
|
||||
import_text = open(import_filename, "r").read()
|
||||
with open(import_filename, "r") as import_file:
|
||||
import_text = import_file.read()
|
||||
stck[-1][1] = css # store remained part
|
||||
stck.append([import_filename, import_text, import_text])
|
||||
wasBroken = True
|
||||
|
@ -335,6 +364,7 @@ class MapCSS():
|
|||
name = VARIABLE_SET.match(css).groups()[0]
|
||||
log.debug("variable set found: %s" % name)
|
||||
self.variables[name] = VARIABLE_SET.match(css).groups()[1]
|
||||
self.unused_variables.add( name )
|
||||
css = VARIABLE_SET.sub("", css, 1)
|
||||
previous = oVARIABLE_SET
|
||||
|
||||
|
@ -342,7 +372,7 @@ class MapCSS():
|
|||
elif UNKNOWN.match(css):
|
||||
raise Exception("Unknown construction: " + UNKNOWN.match(css).group())
|
||||
|
||||
# Must be unreacheable
|
||||
# Must be unreachable
|
||||
else:
|
||||
raise Exception("Unexpected construction: " + css)
|
||||
|
||||
|
@ -360,10 +390,13 @@ class MapCSS():
|
|||
css_orig = stck[-1][2] # original
|
||||
css = stck[-1][1] # remained
|
||||
line = css_orig[:-len(css)].count("\n") + 1
|
||||
# TODO: Handle filename is None
|
||||
msg = str(e) + "\nFile: " + filename + "\nLine: " + str(line)
|
||||
# TODO: Print stack trace of original exception `e`
|
||||
raise Exception(msg)
|
||||
|
||||
try:
|
||||
# TODO: Drop support of z-index because `clamp` is always False and z-index properties unused in Organic Maps)
|
||||
if clamp:
|
||||
"clamp z-indexes, so they're tightly following integers"
|
||||
zindex = set()
|
||||
|
@ -372,18 +405,19 @@ class MapCSS():
|
|||
zindex.add(float(stylez.get('z-index', 0)))
|
||||
zindex = list(zindex)
|
||||
zindex.sort()
|
||||
zoffset = len([x for x in zindex if x < 0])
|
||||
for chooser in self.choosers:
|
||||
for stylez in chooser.styles:
|
||||
if 'z-index' in stylez:
|
||||
res = zindex.index(float(stylez.get('z-index', 0))) - zoffset
|
||||
res = zindex.index(float(stylez.get('z-index', 0)))
|
||||
if stretch:
|
||||
stylez['z-index'] = 1. * res / len(zindex) * stretch
|
||||
stylez['z-index'] = stretch * res / len(zindex)
|
||||
else:
|
||||
stylez['z-index'] = res
|
||||
except TypeError:
|
||||
# TODO: Better error handling here
|
||||
pass
|
||||
|
||||
# Group MapCSS styles by object type: 'area', 'line', 'way', 'node'
|
||||
for chooser in self.choosers:
|
||||
for t in chooser.compatible_types:
|
||||
if t not in self.choosers_by_type:
|
||||
|
@ -391,7 +425,11 @@ class MapCSS():
|
|||
else:
|
||||
self.choosers_by_type[t].append(chooser)
|
||||
|
||||
if self.unused_variables:
|
||||
# TODO: Do not print warning here. Instead let libkomwn.komap_mapswithme(...) analyze unused_variables
|
||||
print(f"Warning: Unused variables: {', '.join(self.unused_variables)}")
|
||||
|
||||
# TODO: move to Condition.py
|
||||
def parseCondition(s):
|
||||
log = logging.getLogger('mapcss.parser.condition')
|
||||
|
||||
|
@ -472,7 +510,7 @@ def parseDeclaration(s):
|
|||
logging.debug("%s == %s" % (tzz[0], tzz[1]))
|
||||
else:
|
||||
logging.debug("unknown %s" % (a))
|
||||
return [t]
|
||||
return [t] # TODO: don't wrap `t` dict into a list. Return `t` instead.
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
6
tests/assets/case-1-import/colors.mapcss
Normal file
6
tests/assets/case-1-import/colors.mapcss
Normal file
|
@ -0,0 +1,6 @@
|
|||
colors {
|
||||
GuiText-color: #FFFFFF;
|
||||
GuiText-opacity: 0.7;
|
||||
Route-color: #0000FF;
|
||||
Route-opacity: 0.5;
|
||||
}
|
1
tests/assets/case-1-import/import1.mapcss
Normal file
1
tests/assets/case-1-import/import1.mapcss
Normal file
|
@ -0,0 +1 @@
|
|||
@import("import2.mapcss");
|
1
tests/assets/case-1-import/import2.mapcss
Normal file
1
tests/assets/case-1-import/import2.mapcss
Normal file
|
@ -0,0 +1 @@
|
|||
@import("colors.mapcss");
|
1
tests/assets/case-1-import/main.mapcss
Normal file
1
tests/assets/case-1-import/main.mapcss
Normal file
|
@ -0,0 +1 @@
|
|||
@import("import1.mapcss");
|
7
tests/assets/case-2-generate-drules-mini/.gitignore
vendored
Normal file
7
tests/assets/case-2-generate-drules-mini/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
classificator.txt
|
||||
colors.txt
|
||||
patterns.txt
|
||||
style.bin.bin
|
||||
style.bin.txt
|
||||
types.txt
|
||||
visibility.txt
|
136
tests/assets/case-2-generate-drules-mini/include/Roads.mapcss
Normal file
136
tests/assets/case-2-generate-drules-mini/include/Roads.mapcss
Normal file
|
@ -0,0 +1,136 @@
|
|||
/* ~~~~ CONTENT OF ROADS ~~~~~
|
||||
|
||||
1.Z-INDEX ROADS
|
||||
2.WORLD LEVEL ROAD 4-9 ZOOM
|
||||
3.TRUNK & MOTORWAY 6-22 ZOOM
|
||||
3.1 Trunk & Motorway 6-22 ZOOM
|
||||
3.2 Trunk & Motorway tunnel 12-22 ZOOM
|
||||
3.3 Trunk & Motorway bridge 13-22 ZOOM
|
||||
4.PRIMARY 8-22 ZOOM
|
||||
4.1 Primary 8-22 ZOOM
|
||||
4.2 Primary tunnel 14-22 ZOOM
|
||||
4.3 Primary bridge 14-22 ZOOM
|
||||
5.SECONDARY 10-22 ZOOM
|
||||
5.1 Secondary 10-22 ZOOM
|
||||
5.2 Secondary tunnel 16-22 ZOOM
|
||||
5.3 Secondary bridge 14-22 ZOOM
|
||||
6.TERTIARY & UNCLASSIFIED 11-22 ZOOM
|
||||
6.1 Tertiary & Unclassified 11-22 ZOOM
|
||||
6.2 Tertiary & Unclassified tunnel 16-22 ZOOM
|
||||
6.3 Tertiary & Unclassified bridge 14-22 ZOOM
|
||||
7.RESIDENTAL, ROAD, STREETS & SERVICE 12-22 ZOOM
|
||||
7.1 Residential, Road, Street 12-22 ZOOM
|
||||
7.2 Residential, Road, Street tunnel 16-22 ZOOM
|
||||
7.3 Residential, Road, Street bridge 14-22 ZOOM
|
||||
7.4 Service 15-22 ZOOM
|
||||
8.OTHERS ROADS 13-22 ZOOM
|
||||
8.1 Pedestrian & ford 13-22 ZOOM
|
||||
8.2 Pedestrian & ford tunnel 16-22 ZOOM
|
||||
8.3 Pedestrian & other brige 13-22 ZOOM
|
||||
8.4 Cycleway 13-22 ZOOM
|
||||
8.5 Construction 13-22 ZOOM
|
||||
8.6 Track & Path 14-22 ZOOM
|
||||
8.7 Footway 15-22 ZOOM
|
||||
8.8 Steps 15-22 ZOOM
|
||||
8.9 Bridleway 14-22 ZOOM
|
||||
8.11 Runway 12-22 ZOOM
|
||||
9.RAIL 11-22 ZOOM
|
||||
9.1 RAIL 11-22 ZOOM
|
||||
9.2 Rail tunnel 14-22 ZOOM
|
||||
9.3 Rail bridge 14-22 ZOOM
|
||||
9.4 Monorail 14-22 ZOOM
|
||||
9.5 Tram line 13-22 ZOOM
|
||||
9.6 Funicular 12-22 ZOOM
|
||||
10.PISTE 12-22 ZOOM
|
||||
10.1 Lift 12-22 ZOOM
|
||||
10.2 Aerialway 12-22 ZOOM
|
||||
10.3 Piste & Route 14-22 ZOOM
|
||||
11.FERRY
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*/
|
||||
|
||||
/* 2.WORLD LEVEL ROAD 4-9 ZOOM */
|
||||
|
||||
line|z6-9[highway=world_towns_level],
|
||||
line|z4-9[highway=world_level],
|
||||
{color: @trunk1;opacity: 1;}
|
||||
|
||||
line|z4[highway=world_level]
|
||||
{width: 0.5;}
|
||||
line|z5-6[highway=world_level],
|
||||
{width: 0.7;}
|
||||
line|z6[highway=world_towns_level],
|
||||
{width: 0.9;}
|
||||
line|z7[highway=world_towns_level],
|
||||
line|z7[highway=world_level]
|
||||
{width: 0.7;}
|
||||
line|z8[highway=world_towns_level],
|
||||
line|z8[highway=world_level]
|
||||
{width: 0.9;}
|
||||
line|z9[highway=world_towns_level],
|
||||
line|z9[highway=world_level]
|
||||
{width: 0.8;}
|
||||
|
||||
/* 3.TRUNK & MOTORWAY 6-22 ZOOM */
|
||||
|
||||
line|z6[highway=trunk],
|
||||
line|z6[highway=motorway],
|
||||
{color: @trunk0; opacity: 0.3;}
|
||||
line|z7-9[highway=trunk],
|
||||
line|z7-9[highway=motorway],
|
||||
{color: @trunk0; opacity: 0.7;}
|
||||
|
||||
line|z10-[highway=trunk],
|
||||
line|z10-[highway=motorway],
|
||||
{color: @trunk1; opacity: 0.7;}
|
||||
line|z10-[highway=motorway_link],
|
||||
line|z10-[highway=trunk_link],
|
||||
{color: @primary0; opacity: 0.7;}
|
||||
|
||||
/* 3.1 Trunk & Motorway 6-22 ZOOM */
|
||||
|
||||
line|z6[highway=trunk],
|
||||
line|z6[highway=motorway],
|
||||
{width: 0.8;}
|
||||
line|z7[highway=trunk],
|
||||
line|z7[highway=motorway]
|
||||
{width: 0.9;}
|
||||
line|z8[highway=trunk],
|
||||
line|z8[highway=motorway]
|
||||
{width: 1.1;}
|
||||
line|z9[highway=trunk],
|
||||
line|z9[highway=motorway]
|
||||
{width: 1.2;}
|
||||
line|z10[highway=trunk],
|
||||
line|z10[highway=motorway]
|
||||
{width: 1.5;}
|
||||
|
||||
line|z10[highway=motorway_link],
|
||||
line|z10[highway=trunk_link]
|
||||
{width: 0.8;}
|
||||
|
||||
|
||||
/* 4.PRIMARY 8-22 ZOOM */
|
||||
|
||||
line|z8-10[highway=primary],
|
||||
{color: @primary0; opacity: 0.7;}
|
||||
|
||||
/* 4.1 Primary 8-22 ZOOM */
|
||||
|
||||
line|z8[highway=primary],
|
||||
{width: 0.7;}
|
||||
line|z9[highway=primary],
|
||||
{width: 0.8;}
|
||||
line|z10[highway=primary],
|
||||
{width: 1.2;}
|
||||
|
||||
/* 5.SECONDARY 10-22 ZOOM */
|
||||
|
||||
line|z10[highway=secondary],
|
||||
{color: @secondary0; opacity: 0.8;}
|
||||
|
||||
/* 5.1 Secondary 10-22 ZOOM */
|
||||
|
||||
line|z10[highway=secondary],
|
||||
{width: 1.2;}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/* ~~~~ CONTENT OF ROADS ~~~~~
|
||||
|
||||
1.Z-INDEX ROADS
|
||||
2.SHIELD 10-22 ZOOM
|
||||
3.TRUNK & MOTORWAY 10-22 ZOOM
|
||||
4.PRIMARY 10-22 ZOOM
|
||||
5.SECONDARY 10-22 ZOOM
|
||||
6.RESIDENTAL & TERTIARY 12-22 ZOOM
|
||||
7.ROAD, STREETS, UNCLASSIFIED & SERVICE 15-22 ZOOM
|
||||
8.OTHERS ROADS 15-22 ZOOM
|
||||
9.RAIL 15-22 ZOOM ????
|
||||
9.1 Monorail 14-22 ZOOM
|
||||
9.2 Tram line 13-22 ZOOM
|
||||
9.3 Funicular 12-22 ZOOM
|
||||
10.PISTE 12-22 ZOOM ????
|
||||
10.1 Lift 12-22 ZOOM
|
||||
10.2 Aerialway 12-22 ZOOM
|
||||
10.3 Piste & Route 14-22 ZOOM
|
||||
11.FERRY 10-22 ZOOM
|
||||
12.ONEWAY ARROWS 15-22 ZOOM
|
||||
13.JUNCTION 15-22 ZOOM
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*/
|
||||
|
||||
line[highway]
|
||||
{text-position: line;}
|
||||
|
||||
/* 2.SHIELD 10-22 ZOOM */
|
||||
|
||||
line|z10-[highway=motorway]::shield,
|
||||
line|z10-[highway=trunk]::shield,
|
||||
line|z10-[highway=motorway_link]::shield,
|
||||
line|z10-[highway=trunk_link]::shield,
|
||||
line|z10-[highway=primary]::shield,
|
||||
{shield-font-size: 9;shield-text-color: @shield_text;shield-text-halo-radius: 0;shield-text-halo-color: @shield_text_halo;shield-color: @shield;shield-outline-radius: 1;shield-outline-color: @shield_outline;}
|
||||
|
||||
line|z10[highway=motorway]::shield,
|
||||
line|z10[highway=trunk]::shield,
|
||||
line|z10[highway=motorway_link]::shield,
|
||||
line|z10[highway=trunk_link]::shield,
|
||||
line|z10[highway=primary]::shield,
|
||||
{shield-min-distance: 85;}
|
||||
|
||||
/* 3.TRUNK & MOTORWAY 10-22 ZOOM */
|
||||
|
||||
line|z10-[highway=trunk],
|
||||
line|z10-[highway=motorway],
|
||||
line|z10-[highway=motorway_link],
|
||||
line|z10-[highway=trunk_link],
|
||||
{text: name; text-halo-radius: 1; text-halo-color: @label_halo_medium;}
|
||||
|
||||
line|z10-[highway=motorway],
|
||||
line|z10-[highway=trunk],
|
||||
{font-size: 11; text-color: @label_medium; text-halo-opacity: 0.9;}
|
||||
|
||||
line|z10-[highway=motorway_link],
|
||||
line|z10-[highway=trunk_link],
|
||||
{font-size: 10; text-color: @label_medium; text-halo-opacity: 0.7;}
|
||||
|
||||
/* 4.PRIMARY 10-22 ZOOM */
|
||||
|
||||
line|z10-[highway=primary],
|
||||
{text: name; text-halo-radius: 1; text-halo-color: @label_halo_medium;}
|
||||
|
||||
line|z10-[highway=primary],
|
||||
{font-size: 10; text-color: @label_medium; text-halo-opacity: 0.7;}
|
||||
|
||||
/* 5.SECONDARY 10-22 ZOOM */
|
||||
|
||||
line|z10-[highway=secondary],
|
||||
{text: name; text-halo-radius: 1; text-halo-color: @label_halo_medium;}
|
||||
|
||||
line|z10-[highway=secondary],
|
||||
{font-size: 10; text-color: @label_light; text-halo-opacity: 0.7;}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/* 5.1 All roads */
|
||||
@trunk0: #FF7326;
|
||||
@trunk1: #FF7A26;
|
||||
@primary0: #FF8726;
|
||||
@secondary0: #FFB226;
|
||||
|
||||
/* 6.1 Main labels */
|
||||
@label_medium: #333333;
|
||||
@label_light: #444444;
|
||||
@label_halo_medium: #EDEBDB;
|
||||
|
||||
/* 6.4 Road labels */
|
||||
@shield_text: #000000;
|
||||
@shield_text_halo: #000000;
|
||||
@shield: #FFFFFF;
|
||||
@shield_outline: #000000;
|
|
@ -0,0 +1,16 @@
|
|||
# This file is automatically re-formatted and re-sorted in priorities descending order
|
||||
# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)
|
||||
# are generated automatically for information only. Custom formatting and comments are not preserved.
|
||||
#
|
||||
# BG-by-size geometry: background areas rendered below BG-top and everything else.
|
||||
# Smaller areas are rendered above larger ones (area's size is estimated as the size of its' bounding box).
|
||||
# So effectively priority values of BG-by-size areas are not used at the moment.
|
||||
# But we might use them later for some special cases, e.g. to determine a main area type of a multi-type feature.
|
||||
# Keep them in a logical importance order please.
|
||||
#
|
||||
# Priorities ranges' rendering order overview:
|
||||
# - overlays (icons, captions...)
|
||||
# - FG: foreground areas and lines
|
||||
# - BG-top: water (linear and areal)
|
||||
# - BG-by-size: landcover areas sorted by their size
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# This file is automatically re-formatted and re-sorted in priorities descending order
|
||||
# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)
|
||||
# are generated automatically for information only. Custom formatting and comments are not preserved.
|
||||
#
|
||||
# BG-top geometry: background lines and areas that should be always below foreground ones
|
||||
# (including e.g. layer=-10 underwater tunnels), but above background areas sorted by size (BG-by-size),
|
||||
# because ordering by size doesn't always work with e.g. water mapped over a forest,
|
||||
# so water should be on top of other landcover always, but linear waterways should be hidden beneath it.
|
||||
# Still, e.g. a layer=-1 BG-top feature will be rendered under a layer=0 BG-by-size feature
|
||||
# (so areal water tunnels are hidden beneath other landcover area) and a layer=1 landcover areas
|
||||
# are displayed above layer=0 BG-top.
|
||||
#
|
||||
# Priorities ranges' rendering order overview:
|
||||
# - overlays (icons, captions...)
|
||||
# - FG: foreground areas and lines
|
||||
# - BG-top: water (linear and areal)
|
||||
# - BG-by-size: landcover areas sorted by their size
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# This file is automatically re-formatted and re-sorted in priorities descending order
|
||||
# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)
|
||||
# are generated automatically for information only. Custom formatting and comments are not preserved.
|
||||
#
|
||||
# FG geometry: foreground lines and areas (e.g. buildings) are rendered always below overlays
|
||||
# and always on top of background geometry (BG-top & BG-by-size) even if a foreground feature
|
||||
# is layer=-10 (as tunnels should be visibile over landcover and water).
|
||||
#
|
||||
# Priorities ranges' rendering order overview:
|
||||
# - overlays (icons, captions...)
|
||||
# - FG: foreground areas and lines
|
||||
# - BG-top: water (linear and areal)
|
||||
# - BG-by-size: landcover areas sorted by their size
|
||||
|
||||
highway-motorway # line z6- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-motorway-bridge # line z6- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-motorway-tunnel # line z6- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-trunk # line z6- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-trunk-bridge # line z6- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-trunk-tunnel # line z6- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-world_level # line z4-9
|
||||
highway-world_towns_level # line z6-9
|
||||
=== 310
|
||||
|
||||
highway-primary # line z8- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-primary-bridge # line z8- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-primary-tunnel # line z8- (also has pathtext z10-, shield::shield z10-)
|
||||
=== 290
|
||||
|
||||
highway-secondary # line z10- (also has pathtext z10-)
|
||||
highway-secondary-bridge # line z10- (also has pathtext z10-)
|
||||
highway-secondary-tunnel # line z10- (also has pathtext z10-)
|
||||
=== 270
|
||||
|
||||
highway-motorway_link # line z10- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-motorway_link-bridge # line z10- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-motorway_link-tunnel # line z10- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-trunk_link # line z10- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-trunk_link-bridge # line z10- (also has pathtext z10-, shield::shield z10-)
|
||||
highway-trunk_link-tunnel # line z10- (also has pathtext z10-, shield::shield z10-)
|
||||
=== 228
|
|
@ -0,0 +1,61 @@
|
|||
# This file is automatically re-formatted and re-sorted in priorities descending order
|
||||
# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)
|
||||
# are generated automatically for information only. Custom formatting and comments are not preserved.
|
||||
#
|
||||
# Overlays (icons, captions, path texts and shields) are rendered on top of all the geometry (lines, areas).
|
||||
# Overlays don't overlap each other, instead the ones with higher priority displace the less important ones.
|
||||
# Optional captions (which have an icon) are usually displayed only if there are no other overlays in their way
|
||||
# (technically, max overlays priority value (10000) is subtracted from their priorities automatically).
|
||||
#
|
||||
# Priorities ranges' rendering order overview:
|
||||
# - overlays (icons, captions...)
|
||||
# - FG: foreground areas and lines
|
||||
# - BG-top: water (linear and areal)
|
||||
# - BG-by-size: landcover areas sorted by their size
|
||||
|
||||
highway-motorway # pathtext z10- (also has shield::shield z10-, line z6-)
|
||||
highway-motorway-bridge # pathtext z10- (also has shield::shield z10-, line z6-)
|
||||
highway-motorway-tunnel # pathtext z10- (also has shield::shield z10-, line z6-)
|
||||
highway-trunk # pathtext z10- (also has shield::shield z10-, line z6-)
|
||||
highway-trunk-bridge # pathtext z10- (also has shield::shield z10-, line z6-)
|
||||
highway-trunk-tunnel # pathtext z10- (also has shield::shield z10-, line z6-)
|
||||
=== 6750
|
||||
|
||||
highway-motorway::shield # shield::shield z10- (also has pathtext z10-, line z6-)
|
||||
highway-motorway-bridge::shield # shield::shield z10- (also has pathtext z10-, line z6-)
|
||||
highway-motorway-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z6-)
|
||||
highway-trunk::shield # shield::shield z10- (also has pathtext z10-, line z6-)
|
||||
highway-trunk-bridge::shield # shield::shield z10- (also has pathtext z10-, line z6-)
|
||||
highway-trunk-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z6-)
|
||||
=== 6740
|
||||
|
||||
highway-primary # pathtext z10- (also has shield::shield z10-, line z8-)
|
||||
highway-primary-bridge # pathtext z10- (also has shield::shield z10-, line z8-)
|
||||
highway-primary-tunnel # pathtext z10- (also has shield::shield z10-, line z8-)
|
||||
=== 6200
|
||||
|
||||
highway-motorway_link # pathtext z10- (also has shield::shield z10-, line z10-)
|
||||
highway-motorway_link-bridge # pathtext z10- (also has shield::shield z10-, line z10-)
|
||||
highway-motorway_link-tunnel # pathtext z10- (also has shield::shield z10-, line z10-)
|
||||
highway-trunk_link # pathtext z10- (also has shield::shield z10-, line z10-)
|
||||
highway-trunk_link-bridge # pathtext z10- (also has shield::shield z10-, line z10-)
|
||||
highway-trunk_link-tunnel # pathtext z10- (also has shield::shield z10-, line z10-)
|
||||
=== 6150
|
||||
|
||||
highway-motorway_link::shield # shield::shield z10- (also has pathtext z10-, line z10-)
|
||||
highway-motorway_link-bridge::shield # shield::shield z10- (also has pathtext z10-, line z10-)
|
||||
highway-motorway_link-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z10-)
|
||||
highway-trunk_link::shield # shield::shield z10- (also has pathtext z10-, line z10-)
|
||||
highway-trunk_link-bridge::shield # shield::shield z10- (also has pathtext z10-, line z10-)
|
||||
highway-trunk_link-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z10-)
|
||||
=== 6140
|
||||
|
||||
highway-secondary # pathtext z10- (also has line z10-)
|
||||
highway-secondary-bridge # pathtext z10- (also has line z10-)
|
||||
highway-secondary-tunnel # pathtext z10- (also has line z10-)
|
||||
=== 5600
|
||||
|
||||
highway-primary::shield # shield::shield z10- (also has pathtext z10-, line z8-)
|
||||
highway-primary-bridge::shield # shield::shield z10- (also has pathtext z10-, line z8-)
|
||||
highway-primary-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z8-)
|
||||
=== 2975
|
133
tests/assets/case-2-generate-drules-mini/main.mapcss
Normal file
133
tests/assets/case-2-generate-drules-mini/main.mapcss
Normal file
|
@ -0,0 +1,133 @@
|
|||
*::int_name
|
||||
{
|
||||
text-offset: 1;
|
||||
}
|
||||
|
||||
@import("include/colors.mapcss");
|
||||
@import("include/Roads.mapcss");
|
||||
@import("include/Roads_label.mapcss");
|
||||
|
||||
colors
|
||||
{
|
||||
GuiText-color: #4D4D4D;
|
||||
GuiText-opacity: 0.86;
|
||||
MyPositionAccuracy-color: #000000;
|
||||
MyPositionAccuracy-opacity: 0.08;
|
||||
Selection-color: #1E96F0;
|
||||
Selection-opacity: 0.64;
|
||||
Route-color: #0087FF;
|
||||
RouteOutline-color: #055FCD;
|
||||
RouteTrafficG0-color: #9B2300;
|
||||
RouteTrafficG1-color: #E82705;
|
||||
RouteTrafficG2-color: #E82705;
|
||||
RouteTrafficG3-color: #FFE500;
|
||||
RouteTrafficG3-opacity: 0.0;
|
||||
RoutePedestrian-color: #1D339E;
|
||||
RoutePedestrian-opacity: 0.8;
|
||||
RouteBicycle-color: #9C27B0;
|
||||
RouteBicycle-opacity: 0.8;
|
||||
RouteRuler-color: #66347F;
|
||||
RouteRuler-opacity: 0.9;
|
||||
RoutePreview-color: #000000;
|
||||
RoutePreview-opacity: 0.3;
|
||||
RouteMaskCar-color: #000000;
|
||||
RouteMaskCar-opacity: 0.3;
|
||||
RouteFirstSegmentArrowsMaskCar-color: #033B80;
|
||||
RouteFirstSegmentArrowsMaskCar-opacity: 0.0;
|
||||
RouteArrowsMaskCar-color: #033B80;
|
||||
RouteArrowsMaskCar-opacity: 0.2;
|
||||
RouteMaskBicycle-color: #000000;
|
||||
RouteMaskBicycle-opacity: 0.5;
|
||||
RouteFirstSegmentArrowsMaskBicycle-color: #9C27B0;
|
||||
RouteFirstSegmentArrowsMaskBicycle-opacity: 0.0;
|
||||
RouteArrowsMaskBicycle-color: #9C27B0;
|
||||
RouteArrowsMaskBicycle-opacity: 0.2;
|
||||
RouteMaskPedestrian-color: #000000;
|
||||
RouteMaskPedestrian-opacity: 0.5;
|
||||
RouteFake-color: #A8A8A8;
|
||||
RouteFakeOutline-color: #717171;
|
||||
Arrow3D-color: #50AAFF;
|
||||
Arrow3DObsolete-color: #82AAC8;
|
||||
Arrow3DObsolete-opacity: 0.72;
|
||||
Arrow3DShadow-color: #3C3C3C;
|
||||
Arrow3DShadow-opacity: 0.24;
|
||||
Arrow3DOutline-color: #FFFFFF;
|
||||
TrackHumanSpeed-color: #1D339E;
|
||||
TrackCarSpeed-color: #7C8EDE;
|
||||
TrackPlaneSpeed-color: #A8B7ED;
|
||||
TrackUnknownDistance-color: #616161;
|
||||
TrafficG0-color: #7E1712;
|
||||
TrafficG1-color: #E42300;
|
||||
TrafficG2-color: #E42300;
|
||||
TrafficG3-color: #FCDE00;
|
||||
TrafficG3-opacity: 0.0;
|
||||
TrafficG4-color: #39962E;
|
||||
TrafficG5-color: #39962E;
|
||||
TrafficTempBlock-color: #525252;
|
||||
TrafficUnknown-color: #000000;
|
||||
TrafficArrowLight-color: #FFFFFF;
|
||||
TrafficArrowDark-color: #473635;
|
||||
TrafficOutline-color: #E8E6DC;
|
||||
RoadShieldBlackText-color: #000000;
|
||||
RoadShieldWhiteText-color: #FFFFFF;
|
||||
RoadShieldUKYellowText-color: #FFD400;
|
||||
RoadShieldBlueBackground-color: #1A5EC1;
|
||||
RoadShieldGreenBackground-color: #309302;
|
||||
RoadShieldRedBackground-color: #E63534;
|
||||
RoadShieldOrangeBackground-color: #FFBE00;
|
||||
PoiHotelTextOutline-color: #FFFFFF;
|
||||
PoiHotelTextOutline-opacity: 0.6;
|
||||
PoiDeletedMask-color: #FFFFFF;
|
||||
PoiDeletedMask-opacity: 0.3;
|
||||
PoiVisitedMask-color: #FFFFFF;
|
||||
PoiVisitedMask-opacity: 0.7;
|
||||
DefaultTrackColor-color: #1E96F0;
|
||||
RouteMarkPrimaryText-color: #000000;
|
||||
RouteMarkPrimaryTextOutline-color: #FFFFFF;
|
||||
RouteMarkSecondaryText-color: #000000;
|
||||
RouteMarkSecondaryTextOutline-color: #FFFFFF;
|
||||
TransitMarkPrimaryText-color: #000000;
|
||||
TransitMarkPrimaryTextOutline-color: #FFFFFF;
|
||||
TransitMarkSecondaryText-color: #000000;
|
||||
TransitMarkSecondaryTextOutline-color: #FFFFFF;
|
||||
TransitTransferOuterMarker-color: #000000;
|
||||
TransitTransferInnerMarker-color: #FFFFFF;
|
||||
TransitStopInnerMarker-color: #FFFFFF;
|
||||
LocalAdsPrimaryText-color: #000000;
|
||||
LocalAdsPrimaryTextOutline-color: #FFFFFF;
|
||||
LocalAdsSecondaryText-color: #000000;
|
||||
LocalAdsSecondaryTextOutline-color: #FFFFFF;
|
||||
TransitBackground-color: #FFFFFF;
|
||||
TransitBackground-opacity: 0.4;
|
||||
BookmarkRed-color: #E51B23;
|
||||
BookmarkPink-color: #FF4182;
|
||||
BookmarkPurple-color: #9B24B2;
|
||||
BookmarkDeepPurple-color: #6639BF;
|
||||
BookmarkBlue-color: #0066CC;
|
||||
BookmarkLightBlue-color: #249CF2;
|
||||
BookmarkCyan-color: #14BECD;
|
||||
BookmarkTeal-color: #00A58C;
|
||||
BookmarkGreen-color: #3C8C3C;
|
||||
BookmarkLime-color: #93BF39;
|
||||
BookmarkYellow-color: #FFC800;
|
||||
BookmarkOrange-color: #FF9600;
|
||||
BookmarkDeepOrange-color: #F06432;
|
||||
BookmarkBrown-color: #804633;
|
||||
BookmarkGray-color: #737373;
|
||||
BookmarkBlueGray-color: #597380;
|
||||
SearchmarkPreparing-color: #597380;
|
||||
SearchmarkNotAvailable-color: #597380;
|
||||
SearchmarkSelectedNotAvailable-color: #F06432;
|
||||
RatingBad-color: #F06432;
|
||||
RatingGood-color: #3C8C3C;
|
||||
RatingNone-color: #249CF2;
|
||||
SearchmarkDefault-color: #249CF2;
|
||||
RatingText-color: #FFFFFF;
|
||||
UGCRatingText-color: #000000;
|
||||
SpeedCameraMarkText-color: #FFFFFF;
|
||||
SpeedCameraMarkBg-color: #F51E30;
|
||||
SpeedCameraMarkOutline-color: #FFFFFF;
|
||||
GuideCityMarkText-color: #6639BF;
|
||||
GuideOutdoorMarkText-color: #3C8C3C;
|
||||
HotelPriceText-color: #000000;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
population
|
||||
name
|
||||
bbox_area
|
||||
rating
|
148
tests/assets/case-2-generate-drules-mini/mapcss-mapping.csv
Normal file
148
tests/assets/case-2-generate-drules-mini/mapcss-mapping.csv
Normal file
|
@ -0,0 +1,148 @@
|
|||
highway|residential;2;
|
||||
highway|service;3;
|
||||
highway|unclassified;5;
|
||||
highway|footway;7;
|
||||
highway|track;8;
|
||||
highway|tertiary;9;
|
||||
highway|secondary;13;
|
||||
highway|path;16;
|
||||
highway|bus_stop;17;
|
||||
highway|footway|sidewalk;[highway=footway][footway=sidewalk];;name;int_name;18;
|
||||
highway|primary;27;
|
||||
highway|service|parking_aisle;[highway=service][service=parking_aisle];;name;int_name;29;
|
||||
moved:highway|road:05.2024;31;highway|road
|
||||
deprecated:highway|track|grade2:04.2024;[highway=track][tracktype=grade2];x;name;int_name;32;highway|track
|
||||
deprecated:highway|track|grade3:04.4024;[highway=track][tracktype=grade3];x;name;int_name;34;highway|track
|
||||
highway|cycleway;37;
|
||||
deprecated:highway|track|grade1:04.2024;[highway=track][tracktype=grade1];x;name;int_name;40;highway|track
|
||||
highway|service|driveway;[highway=service][service=driveway];;name;int_name;42;
|
||||
highway|motorway_link;44;
|
||||
deprecated:highway|track|grade4:04.2024;[highway=track][tracktype=grade4];x;name;int_name;46;highway|track
|
||||
highway|footway|crossing;[highway=footway][footway=crossing];;name;int_name;51;
|
||||
highway|path|bicycle;[highway=path][bicycle=designated];;;;53;
|
||||
highway|living_street;55;
|
||||
highway|motorway;58;
|
||||
highway|steps;59;
|
||||
deprecated:highway|track|grade5:04.2024;[highway=track][tracktype=grade5];x;name;int_name;63;highway|track
|
||||
highway|trunk;66;
|
||||
highway|pedestrian;70;
|
||||
highway|motorway|bridge;[highway=motorway][bridge?];;name;int_name;72;
|
||||
highway|residential|bridge;[highway=residential][bridge?];;name;int_name;81;
|
||||
highway|secondary|bridge;[highway=secondary][bridge?];;name;int_name;85;
|
||||
highway|tertiary|bridge;[highway=tertiary][bridge?];;name;int_name;86;
|
||||
highway|trunk_link;91;
|
||||
highway|unclassified|bridge;[highway=unclassified][bridge?];;name;int_name;92;
|
||||
highway|primary|bridge;[highway=primary][bridge?];;name;int_name;95;
|
||||
highway|primary_link;96;
|
||||
highway|footway|bridge;[highway=footway][bridge?];;name;int_name;98;
|
||||
deprecated:highway|path|hiking:04.2024;[highway=path][route=hiking],[highway=path][sac_scale=hiking];x;name;int_name;113;highway|path
|
||||
highway|trunk|bridge;[highway=trunk][bridge?];;name;int_name;116;
|
||||
highway|motorway_junction;121;
|
||||
highway|footway|bicycle;[highway=footway][bicycle=designated];;;;141;
|
||||
highway|motorway_link|bridge;[highway=motorway_link][bridge?];;name;int_name;143;
|
||||
deprecated:highway|footway|permissive:12.2023;[highway=footway][access=permissive],[highway=footway][foot=permissive];x;name;int_name;153;highway|footway
|
||||
highway|pedestrian|area;[highway=pedestrian][area?];;name;int_name;158;
|
||||
highway|construction;163;
|
||||
highway|cycleway|bridge;[highway=cycleway][bridge?];;name;int_name;164;
|
||||
deprecated:highway|path|mountain_hiking:04.2024;[highway=path][sac_scale=mountain_hiking];x;name;int_name;166;highway|path
|
||||
highway|bridleway;168;
|
||||
highway|secondary_link;177;
|
||||
highway|footway|tunnel;[highway=footway][tunnel?],[highway=footway][location=underground];;name;int_name;183;
|
||||
highway|track|bridge;[highway=track][bridge?];;name;int_name;193;
|
||||
highway|path|bridge;[highway=path][bridge?];;name;int_name;194;
|
||||
highway|service|bridge;[highway=service][bridge?];;name;int_name;203;
|
||||
highway|service|area;[highway=service][area?];;name;int_name;226;
|
||||
highway|residential|area;[highway=residential][area?];;name;int_name;227;
|
||||
deprecated:highway|track|permissive:12.2023;[highway=track][access=permissive];x;name;int_name;229;highway|track
|
||||
highway|cycleway|tunnel;[highway=cycleway][tunnel?];;name;int_name;232;
|
||||
highway|unclassified|tunnel;[highway=unclassified][tunnel?];;name;int_name;235;
|
||||
highway|residential|tunnel;[highway=residential][tunnel?];;name;int_name;238;
|
||||
deprecated:highway|path|permissive:12.2023;[highway=path][access=permissive];x;name;int_name;240;highway|path
|
||||
highway|trunk_link|bridge;[highway=trunk_link][bridge?];;name;int_name;261;
|
||||
highway|service|tunnel;[highway=service][tunnel?];;name;int_name;263;
|
||||
highway|tertiary|tunnel;[highway=tertiary][tunnel?];;name;int_name;269;
|
||||
highway|tertiary_link;273;
|
||||
highway|footway|area;[highway=footway][area?];;name;int_name;276;
|
||||
highway|road|bridge;[highway=road][bridge?];;name;int_name;280;
|
||||
highway|secondary|tunnel;[highway=secondary][tunnel?];;name;int_name;297;
|
||||
deprecated:highway|path|demanding_mountain_hiking:04.2024;[highway=path][sac_scale=demanding_mountain_hiking];x;name;int_name;300;highway|path|difficult
|
||||
highway|pedestrian|bridge;[highway=pedestrian][bridge?];;name;int_name;304;
|
||||
highway|raceway;308;
|
||||
highway|primary|tunnel;[highway=primary][tunnel?];;name;int_name;309;
|
||||
highway|primary_link|bridge;[highway=primary_link][bridge?];;name;int_name;310;
|
||||
deprecated:highway|footway|hiking:04.2024;[highway=footway][sac_scale=hiking];x;name;int_name;314;highway|path
|
||||
highway|path|horse;[highway=path][horse?];;name;int_name;317;
|
||||
highway|trunk|tunnel;[highway=trunk][tunnel?];;name;int_name;326;
|
||||
highway|steps|tunnel;[highway=steps][tunnel?],[highway=steps][location=underground];;name;int_name;327;
|
||||
highway|steps|bridge;[highway=steps][bridge?];;name;int_name;330;
|
||||
highway|pedestrian|tunnel;[highway=pedestrian][tunnel?],[highway=pedestrian][location=underground];;name;int_name;332;
|
||||
highway|path|tunnel;[highway=path][tunnel?],[highway=path][location=underground];;name;int_name;336;
|
||||
deprecated:highway|path|alpine_hiking:04.2024;[highway=path][sac_scale=alpine_hiking];x;name;int_name;350;highway|path|expert
|
||||
deprecated:highway|cycleway|permissive:12.2023;[highway=cycleway][access=permissive];x;name;int_name;353;highway|cycleway
|
||||
highway|unclassified|area;[highway=unclassified][area?];;name;int_name;354;
|
||||
deprecated:highway|footway|mountain_hiking:04.2024;[highway=footway][sac_scale=mountain_hiking];x;name;int_name;361;highway|path
|
||||
deprecated:highway|service|driveway|bridge:01.2020;[highway=service][service=driveway][bridge?];x;name;int_name;362;highway|service|driveway
|
||||
deprecated:highway|bridleway|permissive:12.2023;[highway=bridleway][access=permissive];x;name;int_name;370;highway|bridleway
|
||||
highway|bridleway|bridge;[highway=bridleway][bridge?];;name;int_name;378;
|
||||
deprecated:highway|service|driveway|tunnel:01.2020;[highway=service][service=driveway][tunnel?];x;name;int_name;379;highway|service|driveway
|
||||
deprecated:highway|service|driveway|area:01.2020;[highway=service][service=driveway][area?];x;name;int_name;386;highway|service|driveway
|
||||
deprecated:highway|path|demanding_alpine_hiking:04.2024;[highway=path][sac_scale=demanding_alpine_hiking];x;name;int_name;395;highway|path|expert
|
||||
highway|secondary_link|bridge;[highway=secondary_link][bridge?];;name;int_name;397;
|
||||
area:highway|living_street;401;
|
||||
highway|living_street|bridge;[highway=living_street][bridge?];;name;int_name;407;
|
||||
highway|road;411;
|
||||
highway|motorway|tunnel;[highway=motorway][tunnel?];;name;int_name;416;
|
||||
area:highway|service;418;
|
||||
highway|road|tunnel;[highway=road][tunnel?];;name;int_name;423;
|
||||
highway|ford;427;
|
||||
area:highway|path;428;
|
||||
highway|track|area;[highway=track][area?];;name;int_name;430;
|
||||
deprecated:highway|path|difficult_alpine_hiking:04.2024;[highway=path][sac_scale=difficult_alpine_hiking];x;name;int_name;444;highway|path|expert
|
||||
deprecated:highway|footway|demanding_mountain_hiking:04.2024;[highway=footway][sac_scale=demanding_mountain_hiking];x;name;int_name;452;highway|path|difficult
|
||||
highway|living_street|tunnel;[highway=living_street][tunnel?];;name;int_name;457;
|
||||
highway|path|difficult;[highway=path][_path_grade=difficult];;name;int_name;464;
|
||||
highway|path|expert;[highway=path][_path_grade=expert];;name;int_name;465;
|
||||
area:highway|steps;470;
|
||||
highway|bridleway|tunnel;[highway=bridleway][tunnel?];;name;int_name;488;
|
||||
highway|motorway_link|tunnel;[highway=motorway_link][tunnel?];;name;int_name;489;
|
||||
highway|tertiary_link|bridge;[highway=tertiary_link][bridge?];;name;int_name;493;
|
||||
highway|trunk_link|tunnel;[highway=trunk_link][tunnel?];;name;int_name;503;
|
||||
highway|primary_link|tunnel;[highway=primary_link][tunnel?];;name;int_name;528;
|
||||
deprecated:highway|footway|alpine_hiking:04.2024;[highway=footway][sac_scale=alpine_hiking];x;name;int_name;529;highway|path|expert
|
||||
deprecated:amenity|speed_trap:10.2021;542;highway|speed_camera
|
||||
area:highway|track;543;
|
||||
area:highway|primary;544;
|
||||
deprecated:highway|footway|demanding_alpine_hiking:04.2024;[highway=footway][sac_scale=demanding_alpine_hiking];x;name;int_name;555;highway|path|expert
|
||||
highway|secondary_link|tunnel;[highway=secondary_link][tunnel?];;name;int_name;578;
|
||||
highway|track|grade3|permissive;[highway=track][tracktype=grade3][access=permissive];x;name;int_name;591;highway|track
|
||||
deprecated:highway|footway|difficult_alpine_hiking:04.2024;[highway=footway][sac_scale=difficult_alpine_hiking];x;name;int_name;627;highway|path|expert
|
||||
highway|track|grade5|permissive;[highway=track][tracktype=grade5][access=permissive];x;name;int_name;631;highway|track
|
||||
highway|tertiary_link|tunnel;[highway=tertiary_link][tunnel?];;name;int_name;634;
|
||||
highway|track|grade4|permissive;[highway=track][tracktype=grade4][access=permissive];x;name;int_name;675;highway|track
|
||||
highway|track|grade3|no-access;[highway=track][tracktype=grade3][access=no];x;name;int_name;821;highway|track
|
||||
highway|track|grade4|no-access;[highway=track][tracktype=grade4][access=no];x;name;int_name;822;highway|track
|
||||
highway|track|grade5|no-access;[highway=track][tracktype=grade5][access=no];x;name;int_name;823;highway|track
|
||||
highway|track|no-access;[highway=track][access=no];;name;int_name;824;
|
||||
deprecated:highway|service|busway:10.2023;[highway=service][service=busway];x;name;int_name;857;highway|busway
|
||||
highway|busway;[highway=busway],[highway=service][service=busway],[highway=service][service=bus];;name;int_name;858;
|
||||
highway|busway|bridge;[highway=busway][bridge?];;name;int_name;859;
|
||||
highway|busway|tunnel;[highway=busway][tunnel?];;name;int_name;860;
|
||||
area:highway|footway;866;
|
||||
area:highway|residential;868;
|
||||
area:highway|secondary;869;
|
||||
area:highway|tertiary;870;
|
||||
area:highway|pedestrian;873;
|
||||
area:highway|unclassified;874;
|
||||
area:highway|cycleway;877;
|
||||
area:highway|motorway;879;
|
||||
area:highway|trunk;880;
|
||||
highway|speed_camera;991;
|
||||
highway|world_level;1052;
|
||||
highway|world_towns_level;1053;
|
||||
highway|elevator;1059;
|
||||
highway|rest_area;1080;
|
||||
highway|traffic_signals;1081;
|
||||
hwtag|nobicycle;1114;
|
||||
hwtag|yesbicycle;1115;
|
||||
hwtag|bidir_bicycle;1116;
|
||||
highway|services;1173;
|
Can't render this file because it has a wrong number of fields in line 38.
|
4
tests/assets/case-2-generate-drules-mini/readme.md
Normal file
4
tests/assets/case-2-generate-drules-mini/readme.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
Files for testLibkomwm.test_generate_drules_mini() method.
|
||||
|
||||
These styles contain only zooms 0-10 and only highway=* rules.
|
||||
So we can verify generated files content.
|
1
tests/assets/case-3-styles-validation/readme.md
Normal file
1
tests/assets/case-3-styles-validation/readme.md
Normal file
|
@ -0,0 +1 @@
|
|||
Files for testLibkomwm.test_generate_drules_validation_errors() method.
|
300
tests/testCondition.py
Normal file
300
tests/testCondition.py
Normal file
|
@ -0,0 +1,300 @@
|
|||
import re
|
||||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add `src` directory to the import paths
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
||||
|
||||
from mapcss import parseCondition
|
||||
from mapcss.Condition import Condition
|
||||
|
||||
class ConditionTest(unittest.TestCase):
|
||||
|
||||
def test_parser_eq(self):
|
||||
cond:Condition = parseCondition("natural=coastline")
|
||||
self.assertEqual(cond.type, "eq")
|
||||
self.assertEqual(cond.params, ("natural", "coastline"))
|
||||
self.assertTrue(cond.test({'natural': 'coastline'}))
|
||||
self.assertFalse(cond.test({'Natural': 'Coastline'}))
|
||||
|
||||
cond = parseCondition(" highway\t=\tprimary")
|
||||
self.assertEqual(cond.type, "eq")
|
||||
self.assertEqual(cond.params, ("highway", "primary"))
|
||||
self.assertTrue(cond.test({'highway': 'primary'}))
|
||||
self.assertFalse(cond.test({'highway': 'secondary'}))
|
||||
|
||||
cond = parseCondition(" admin_level = 3")
|
||||
self.assertEqual(cond.type, "eq")
|
||||
self.assertEqual(cond.params, ("admin_level", "3"))
|
||||
self.assertTrue(cond.test({'admin_level': '3'}))
|
||||
self.assertFalse(cond.test({'admin_level': '32'}))
|
||||
|
||||
cond = Condition('eq', ("::class", "::*"))
|
||||
self.assertEqual(cond.type, "eq")
|
||||
self.assertEqual(cond.params, ("::class", "::*"))
|
||||
self.assertEqual(cond.extract_tag(), "*")
|
||||
self.assertEqual(cond.test({'any_key': 'any_value'}), "::*")
|
||||
self.assertTrue(cond.test({'any_key': 'any_value'}))
|
||||
|
||||
cond = Condition('eq', ("::class", "::int_name"))
|
||||
self.assertEqual(cond.type, "eq")
|
||||
self.assertEqual(cond.params, ("::class", "::int_name"))
|
||||
self.assertEqual(cond.extract_tag(), "*")
|
||||
self.assertEqual(cond.test({'any_key': 'any_value'}), "::int_name")
|
||||
self.assertTrue(cond.test({'any_key': 'any_value'}))
|
||||
|
||||
def test_parser_regex(self):
|
||||
""" Test conditions in format natural =~/water.+/
|
||||
Note that such conditions are not used by Organic Maps styles.
|
||||
"""
|
||||
cond:Condition = parseCondition("natural =~/water.+/")
|
||||
self.assertEqual(cond.type, "regex")
|
||||
self.assertEqual(cond.params, ("natural", "water.+"))
|
||||
self.assertEqual(type(cond.regex), re.Pattern)
|
||||
self.assertTrue(cond.test({"natural": "waterway"}))
|
||||
self.assertTrue(cond.test({"natural": "water123"}))
|
||||
self.assertFalse(cond.test({"natural": "water"}))
|
||||
self.assertFalse(cond.test({"natural": " waterway "}))
|
||||
|
||||
def test_parser_ge(self):
|
||||
cond:Condition = parseCondition("population>=0")
|
||||
self.assertEqual(cond.type, ">=")
|
||||
self.assertEqual(cond.params, ("population", "0"))
|
||||
self.assertTrue(cond.test({"population": "0"}))
|
||||
self.assertTrue(cond.test({"population": "100000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"population": "-1"}))
|
||||
|
||||
cond:Condition = parseCondition("population >= 150000")
|
||||
self.assertEqual(cond.type, ">=")
|
||||
self.assertEqual(cond.params, ("population", "150000"))
|
||||
self.assertTrue(cond.test({"population": "150000"}))
|
||||
self.assertTrue(cond.test({"population": "250000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"population": "10000"}))
|
||||
|
||||
cond:Condition = parseCondition("\tbbox_area >= 4000000")
|
||||
self.assertEqual(cond.type, ">=")
|
||||
self.assertEqual(cond.params, ("bbox_area", "4000000"))
|
||||
self.assertTrue(cond.test({"bbox_area": "4000000"}))
|
||||
self.assertTrue(cond.test({"bbox_area": "8000000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"bbox_area": "999"}))
|
||||
|
||||
def test_parser_gt(self):
|
||||
""" Test conditions in format population > 100000
|
||||
Note that such conditions are not used by Organic Maps styles.
|
||||
"""
|
||||
cond:Condition = parseCondition("population>0")
|
||||
self.assertEqual(cond.type, ">")
|
||||
self.assertEqual(cond.params, ("population", "0"))
|
||||
self.assertTrue(cond.test({"population": "100"}))
|
||||
self.assertFalse(cond.test({"population": "000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"population": "-1"}))
|
||||
|
||||
cond:Condition = parseCondition("population > 150000")
|
||||
self.assertEqual(cond.type, ">")
|
||||
self.assertEqual(cond.params, ("population", "150000"))
|
||||
self.assertTrue(cond.test({"population": "250000"}))
|
||||
self.assertFalse(cond.test({"population": "150000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"population": "10000"}))
|
||||
|
||||
cond:Condition = parseCondition("\tbbox_area > 4000000 ")
|
||||
self.assertEqual(cond.type, ">")
|
||||
self.assertEqual(cond.params, ("bbox_area", "4000000 ")) # TODO fix parser to exclude trailing space
|
||||
self.assertTrue(cond.test({"bbox_area": "8000000"}))
|
||||
self.assertFalse(cond.test({"bbox_area": "4000000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"bbox_area": "999"}))
|
||||
|
||||
def test_parser_lt(self):
|
||||
cond:Condition = parseCondition("population<40000")
|
||||
self.assertEqual(cond.type, "<")
|
||||
self.assertEqual(cond.params, ("population", "40000"))
|
||||
self.assertTrue(cond.test({"population": "100"}))
|
||||
self.assertTrue(cond.test({"population": "-1"}))
|
||||
self.assertFalse(cond.test({"population": "40000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"population": "500000"}))
|
||||
|
||||
cond:Condition = parseCondition("\tbbox_area < 4000000\n")
|
||||
self.assertEqual(cond.type, "<")
|
||||
self.assertEqual(cond.params, ("bbox_area", "4000000\n")) # TODO fix parser to exclude trailing \n
|
||||
self.assertTrue(cond.test({"bbox_area": "100"}))
|
||||
self.assertTrue(cond.test({"bbox_area": "-1"}))
|
||||
self.assertTrue(cond.test({"bbox_area": "000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"bbox_area": "4000000"}))
|
||||
self.assertFalse(cond.test({"bbox_area": "8000000"}))
|
||||
|
||||
def test_parser_le(self):
|
||||
""" Test conditions in format population <= 100000
|
||||
Note that such conditions are not used by Organic Maps styles.
|
||||
"""
|
||||
cond:Condition = parseCondition("population<=40000")
|
||||
self.assertEqual(cond.type, "<=")
|
||||
self.assertEqual(cond.params, ("population", "40000"))
|
||||
self.assertTrue(cond.test({"population": "100"}))
|
||||
self.assertTrue(cond.test({"population": "-1"}))
|
||||
self.assertTrue(cond.test({"population": "40000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"population": "500000"}))
|
||||
|
||||
cond:Condition = parseCondition("\tbbox_area <= 4000000\n")
|
||||
self.assertEqual(cond.type, "<=")
|
||||
self.assertEqual(cond.params, ("bbox_area", "4000000\n")) # TODO fix parser to exclude trailing \n
|
||||
self.assertTrue(cond.test({"bbox_area": "100"}))
|
||||
self.assertTrue(cond.test({"bbox_area": "-1"}))
|
||||
self.assertTrue(cond.test({"bbox_area": "000"}))
|
||||
self.assertTrue(cond.test({"bbox_area": "4000000"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"bbox_area": "8000000"}))
|
||||
|
||||
def test_parser_ne(self):
|
||||
cond:Condition = parseCondition("capital!=2")
|
||||
self.assertEqual(cond.type, "ne")
|
||||
self.assertEqual(cond.params, ("capital", "2"))
|
||||
self.assertTrue(cond.test({"capital": "1"}))
|
||||
self.assertTrue(cond.test({"capital": "22"}))
|
||||
self.assertTrue(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"capital": "2"}))
|
||||
|
||||
cond:Condition = parseCondition("\tcapital != 2")
|
||||
self.assertEqual(cond.type, "ne")
|
||||
self.assertEqual(cond.params, ("capital", "2"))
|
||||
self.assertTrue(cond.test({"capital": "1"}))
|
||||
self.assertTrue(cond.test({"capital": "22"}))
|
||||
self.assertTrue(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"capital": "2"}))
|
||||
|
||||
cond:Condition = parseCondition("garden:type != residential")
|
||||
self.assertEqual(cond.type, "ne")
|
||||
self.assertEqual(cond.params, ("garden:type", "residential"))
|
||||
self.assertTrue(cond.test({"garden:type": "public"}))
|
||||
self.assertTrue(cond.test({"garden:type": "res"}))
|
||||
self.assertTrue(cond.test({"garden:type": "residential_plus"}))
|
||||
self.assertTrue(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"garden:type": "residential"}))
|
||||
|
||||
def test_parser_set(self):
|
||||
cond:Condition = parseCondition("tunnel")
|
||||
self.assertEqual(cond.type, "set")
|
||||
self.assertEqual(cond.params, ("tunnel", ))
|
||||
self.assertTrue(cond.test({"tunnel": "yes"}))
|
||||
self.assertTrue(cond.test({"tunnel": "maybe"}))
|
||||
self.assertTrue(cond.test({"tunnel": "+1"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
|
||||
cond:Condition = parseCondition("building\t")
|
||||
self.assertEqual(cond.type, "set")
|
||||
self.assertEqual(cond.params, ("building", ))
|
||||
self.assertTrue(cond.test({"building": "yes"}))
|
||||
self.assertTrue(cond.test({"building": "apartment"}))
|
||||
self.assertTrue(cond.test({"building": "1"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"building:part": "yes"}))
|
||||
|
||||
cond:Condition = parseCondition(" addr:housenumber ")
|
||||
self.assertEqual(cond.type, "set")
|
||||
self.assertEqual(cond.params, ("addr:housenumber", ))
|
||||
self.assertTrue(cond.test({"addr:housenumber": "1"}))
|
||||
self.assertTrue(cond.test({"addr:housenumber": "yes"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"addr:street": "Baker st"}))
|
||||
|
||||
cond:Condition = parseCondition(" some-tag ")
|
||||
self.assertEqual(cond.type, "set")
|
||||
self.assertEqual(cond.params, ("some-tag", ))
|
||||
self.assertTrue(cond.test({"some-tag": "1"}))
|
||||
self.assertTrue(cond.test({"some-tag": "yes"}))
|
||||
self.assertFalse(cond.test({"highway": "secondary"}))
|
||||
self.assertFalse(cond.test({"some": "tag"}))
|
||||
|
||||
def test_parser_unset(self):
|
||||
cond:Condition = parseCondition("!tunnel")
|
||||
self.assertEqual(cond.type, "unset")
|
||||
self.assertEqual(cond.params, ("tunnel", ))
|
||||
self.assertTrue(cond.test({"capital": "1"}))
|
||||
self.assertFalse(cond.test({"tunnel": "yes"}))
|
||||
self.assertFalse(cond.test({"tunnel": "no"}))
|
||||
|
||||
cond:Condition = parseCondition("\t!name ")
|
||||
self.assertEqual(cond.type, "unset")
|
||||
self.assertEqual(cond.params, ("name", ))
|
||||
self.assertTrue(cond.test({"capital": "1"}))
|
||||
self.assertTrue(cond.test({"int_name": "1"}))
|
||||
self.assertFalse(cond.test({"name": "London"}))
|
||||
|
||||
def test_parser_false(self):
|
||||
""" Test conditions in format some_tag = no
|
||||
Note that such conditions are not used by Organic Maps styles.
|
||||
"""
|
||||
cond:Condition = parseCondition("access=no")
|
||||
self.assertEqual(cond.type, "false")
|
||||
self.assertEqual(cond.params, ("access", ))
|
||||
#self.assertTrue(cond.test({"access": "no"})) # test is not implemented for `false` condition
|
||||
#self.assertTrue(cond.test({"access": "private"})) # test is not implemented for `false` condition
|
||||
self.assertFalse(cond.test({"tunnel": "yes"}))
|
||||
|
||||
def test_parser_invTrue(self):
|
||||
""" Test conditions in format [!some_tag?] It works the same way as [some_tag != yes]
|
||||
Note that such conditions are not used by Organic Maps styles.
|
||||
"""
|
||||
cond:Condition = parseCondition("!oneway?")
|
||||
self.assertEqual(cond.type, "ne")
|
||||
self.assertEqual(cond.params, ("oneway", "yes"))
|
||||
self.assertTrue(cond.test({"oneway": "no"}))
|
||||
self.assertTrue(cond.test({"oneway": "nobody_knows"}))
|
||||
self.assertTrue(cond.test({"access": "private"}))
|
||||
self.assertFalse(cond.test({"oneway": "yes"}))
|
||||
|
||||
cond:Condition = parseCondition("\t! intermittent ?\n")
|
||||
self.assertEqual(cond.type, "ne")
|
||||
self.assertEqual(cond.params, ("intermittent", "yes"))
|
||||
self.assertTrue(cond.test({"intermittent": "no"}))
|
||||
self.assertTrue(cond.test({"intermittent": "maybe"}))
|
||||
self.assertTrue(cond.test({"access": "private"}))
|
||||
self.assertFalse(cond.test({"intermittent": "yes"}))
|
||||
|
||||
def test_parser_true(self):
|
||||
""" Test conditions in format [some_tag?] It works the same way as [some_tag = yes] """
|
||||
cond:Condition = parseCondition("area?")
|
||||
self.assertEqual(cond.type, "true")
|
||||
self.assertEqual(cond.params, ("area", ))
|
||||
self.assertTrue(cond.test({"area": "yes"}))
|
||||
self.assertFalse(cond.test({"area": "no"}))
|
||||
self.assertFalse(cond.test({"access": "private"}))
|
||||
self.assertFalse(cond.test({"oneway": "nobody_knows"}))
|
||||
|
||||
cond:Condition = parseCondition("\tbridge ? ")
|
||||
self.assertEqual(cond.type, "true")
|
||||
self.assertEqual(cond.params, ("bridge", ))
|
||||
self.assertTrue(cond.test({"bridge": "yes"}))
|
||||
self.assertFalse(cond.test({"bridge": "no"}))
|
||||
self.assertFalse(cond.test({"access": "private"}))
|
||||
self.assertFalse(cond.test({"bridge": "maybe"}))
|
||||
|
||||
def test_untrue(self):
|
||||
""" parseCondition(...) doesn't support this type of condition.
|
||||
Not sure if it's ever used.
|
||||
"""
|
||||
cond:Condition = Condition("untrue", "access")
|
||||
self.assertEqual(cond.type, "untrue")
|
||||
self.assertEqual(cond.params, ("access", ))
|
||||
self.assertTrue(cond.test({"access": "no"}))
|
||||
self.assertFalse(cond.test({"access": "private"}))
|
||||
self.assertFalse(cond.test({"oneway": "yes"}))
|
||||
|
||||
def test_parser_errors(self):
|
||||
with self.assertRaises(Exception):
|
||||
parseCondition("! tunnel")
|
||||
with self.assertRaises(Exception):
|
||||
""" Symbol '-' is only supported in simple 'set' rule. E.g. [key-with-dash]
|
||||
But not in 'unset' rule [!key-with-dash] """
|
||||
parseCondition("key-with-dash?")
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
124
tests/testEval.py
Normal file
124
tests/testEval.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add `src` directory to the import paths
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
||||
|
||||
from mapcss.Eval import Eval
|
||||
|
||||
class EvalTest(unittest.TestCase):
|
||||
""" Test eval(...) feature for CSS properties.
|
||||
NOTE: eval() is not used in Organic Maps styles. We can drop it completely.
|
||||
"""
|
||||
def test_eval_tag(self):
|
||||
a = Eval("""eval( tag("lanes") )""")
|
||||
self.assertEqual(a.compute({"lanes": "4"}), "4")
|
||||
self.assertEqual(a.compute({"natural": "trees"}), "")
|
||||
self.assertSetEqual(a.extract_tags(), {"lanes"})
|
||||
|
||||
def test_eval_prop(self):
|
||||
a = Eval("""eval( prop("dpi") / 2 )""")
|
||||
self.assertEqual(a.compute({"lanes": "4"}, {"dpi": 144}), "72")
|
||||
self.assertEqual(a.compute({"lanes": "4"}, {"orientation": "vertical"}), "")
|
||||
self.assertSetEqual(a.extract_tags(), set())
|
||||
|
||||
def test_eval_num(self):
|
||||
a = Eval("""eval( num(tag("lanes")) + 2 )""")
|
||||
self.assertEqual(a.compute({"lanes": "4"}), "6")
|
||||
self.assertEqual(a.compute({"lanes": "many"}), "2")
|
||||
self.assertSetEqual(a.extract_tags(), {"lanes"})
|
||||
|
||||
def test_eval_metric(self):
|
||||
a = Eval("""eval( metric(tag("height")) )""")
|
||||
self.assertEqual(a.compute({"height": "512"}), "512")
|
||||
self.assertEqual(a.compute({"height": "10m"}), "10")
|
||||
self.assertEqual(a.compute({"height": " 10m"}), "10")
|
||||
self.assertEqual(a.compute({"height": "500cm"}), "5")
|
||||
self.assertEqual(a.compute({"height": "500 cm"}), "5")
|
||||
self.assertEqual(a.compute({"height": "250CM"}), "2.5")
|
||||
self.assertEqual(a.compute({"height": "250 CM"}), "2.5")
|
||||
self.assertEqual(a.compute({"height": "30см"}), "0.3")
|
||||
self.assertEqual(a.compute({"height": " 30 см"}), "0.3")
|
||||
self.assertEqual(a.compute({"height": "1200 mm"}), "1.2")
|
||||
self.assertEqual(a.compute({"height": "2400MM"}), "2.4")
|
||||
self.assertEqual(a.compute({"height": "2800 мм"}), "2.8")
|
||||
self.assertSetEqual(a.extract_tags(), {"height"})
|
||||
|
||||
def test_eval_metric_with_scale(self):
|
||||
a = Eval("""eval( metric(tag("height")) )""")
|
||||
self.assertEqual(a.compute({"height": "512"}, xscale=4), "2048")
|
||||
self.assertEqual(a.compute({"height": "512"}, zscale=4), "512")
|
||||
self.assertEqual(a.compute({"height": "10m"}, xscale=4), "40")
|
||||
self.assertEqual(a.compute({"height": " 10m"}, xscale=4), "40")
|
||||
self.assertEqual(a.compute({"height": "500cm"}, xscale=4), "20")
|
||||
self.assertEqual(a.compute({"height": "500 cm"}, xscale=4), "20")
|
||||
self.assertEqual(a.compute({"height": "250CM"}, xscale=4), "10")
|
||||
self.assertEqual(a.compute({"height": "250 CM"}, xscale=4), "10")
|
||||
self.assertEqual(a.compute({"height": "30см"}, xscale=4), "1.2")
|
||||
self.assertEqual(a.compute({"height": " 30 см"}, xscale=4), "1.2")
|
||||
self.assertEqual(a.compute({"height": "1200 mm"}, xscale=4), "4.8")
|
||||
self.assertEqual(a.compute({"height": "2400MM"}, xscale=4), "9.6")
|
||||
self.assertEqual(a.compute({"height": "2800 мм"}, xscale=4), "11.2")
|
||||
self.assertSetEqual(a.extract_tags(), {"height"})
|
||||
|
||||
def test_eval_zmetric(self):
|
||||
a = Eval("""eval( zmetric(tag("depth")) )""")
|
||||
self.assertEqual(a.compute({"depth": "512"}), "256")
|
||||
self.assertEqual(a.compute({"depth": "10m"}), "5")
|
||||
self.assertEqual(a.compute({"depth": " 10m"}), "5")
|
||||
self.assertEqual(a.compute({"depth": "500cm"}), "2.5")
|
||||
self.assertEqual(a.compute({"depth": "500 cm"}), "2.5")
|
||||
self.assertEqual(a.compute({"depth": "250CM"}), "1.25")
|
||||
self.assertEqual(a.compute({"depth": "250 CM"}), "1.25")
|
||||
self.assertEqual(a.compute({"depth": "30см"}), "0.15")
|
||||
self.assertEqual(a.compute({"depth": " 30 см"}), "0.15")
|
||||
self.assertEqual(a.compute({"depth": "1200 mm"}), "0.6")
|
||||
self.assertEqual(a.compute({"depth": "2400MM"}), "1.2")
|
||||
self.assertEqual(a.compute({"depth": "2800 мм"}), "1.4")
|
||||
self.assertSetEqual(a.extract_tags(), {"depth"})
|
||||
|
||||
def test_eval_str(self):
|
||||
a = Eval("""eval( str( num(tag("width")) - 200 ) )""")
|
||||
self.assertEqual(a.compute({"width": "400"}), "200.0")
|
||||
self.assertSetEqual(a.extract_tags(), {"width"})
|
||||
|
||||
def test_eval_any(self):
|
||||
a = Eval("""eval( any(tag("building"), tag("building:part"), "no") )""")
|
||||
self.assertEqual(a.compute({"building": "apartment"}), "apartment")
|
||||
self.assertEqual(a.compute({"building:part": "roof"}), "roof")
|
||||
self.assertEqual(a.compute({"junction": "roundabout"}), "no")
|
||||
self.assertSetEqual(a.extract_tags(), {"building", "building:part"})
|
||||
|
||||
def test_eval_min(self):
|
||||
a = Eval("""eval( min( num(tag("building:levels")) * 3, 50) )""")
|
||||
self.assertEqual(a.compute({"natural": "wood"}), "0")
|
||||
self.assertEqual(a.compute({"building:levels": "0"}), "0")
|
||||
self.assertEqual(a.compute({"building:levels": "10"}), "30")
|
||||
self.assertEqual(a.compute({"building:levels": "30"}), "50")
|
||||
self.assertSetEqual(a.extract_tags(), {"building:levels"})
|
||||
|
||||
def test_eval_max(self):
|
||||
a = Eval("""eval( max( tag("speed:limit"), 60) )""")
|
||||
self.assertEqual(a.compute({"natural": "wood"}), "60")
|
||||
self.assertEqual(a.compute({"speed:limit": "30"}), "60")
|
||||
self.assertEqual(a.compute({"speed:limit": "60"}), "60")
|
||||
self.assertEqual(a.compute({"speed:limit": "90"}), "90")
|
||||
self.assertSetEqual(a.extract_tags(), {"speed:limit"})
|
||||
|
||||
def test_eval_cond(self):
|
||||
a = Eval("""eval( cond( boolean(tag("oneway")), 200, 100) )""")
|
||||
self.assertEqual(a.compute({"natural": "wood"}), "100")
|
||||
self.assertEqual(a.compute({"oneway": "yes"}), "200")
|
||||
self.assertEqual(a.compute({"oneway": "no"}), "100")
|
||||
self.assertEqual(a.compute({"oneway": "true"}), "200")
|
||||
self.assertEqual(a.compute({"oneway": "probably no"}), "200")
|
||||
self.assertSetEqual(a.extract_tags(), {"oneway"})
|
||||
|
||||
def test_complex_eval(self):
|
||||
a = Eval(""" eval( any( metric(tag("height")), metric ( num(tag("building:levels")) * 3), metric("1m"))) """)
|
||||
self.assertEqual(a.compute({"building:levels": "3"}), "9")
|
||||
self.assertSetEqual(a.extract_tags(), {"height", "building:levels"})
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
68
tests/testLibkomwm.py
Normal file
68
tests/testLibkomwm.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from copy import deepcopy
|
||||
|
||||
# Add `src` directory to the import paths
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
||||
|
||||
import libkomwm
|
||||
from libkomwm import komap_mapswithme
|
||||
|
||||
|
||||
class LibKomwmTest(unittest.TestCase):
|
||||
def test_generate_drules_mini(self):
|
||||
assets_dir = Path(__file__).parent / 'assets' / 'case-2-generate-drules-mini'
|
||||
|
||||
class Options(object):
|
||||
pass
|
||||
|
||||
options = Options()
|
||||
options.data = None
|
||||
options.minzoom = 0
|
||||
options.maxzoom = 10
|
||||
options.txt = True
|
||||
options.filename = str( assets_dir / "main.mapcss" )
|
||||
options.outfile = str( assets_dir / "style_output" )
|
||||
options.priorities_path = str( assets_dir / "include" )
|
||||
|
||||
try:
|
||||
# Save state
|
||||
libkomwm.MULTIPROCESSING = False
|
||||
prio_ranges_orig = deepcopy(libkomwm.prio_ranges)
|
||||
libkomwm.visibilities = {}
|
||||
|
||||
# Run style generation
|
||||
komap_mapswithme(options)
|
||||
|
||||
# Restore state
|
||||
libkomwm.prio_ranges = prio_ranges_orig
|
||||
libkomwm.MULTIPROCESSING = True
|
||||
libkomwm.visibilities = {}
|
||||
|
||||
# Check that types.txt contains 1173 lines
|
||||
with open(assets_dir / "types.txt", "rt") as typesFile:
|
||||
lines = [l.strip() for l in typesFile]
|
||||
self.assertEqual(len(lines), 1173, "Generated types.txt file should contain 1173 lines")
|
||||
self.assertEqual(len([l for l in lines if l!="mapswithme"]), 148, "Actual types count should be 148 as in mapcss-mapping.csv")
|
||||
|
||||
# Check that style_output.bin has 20 styles
|
||||
with open(assets_dir / "style_output.bin", "rb") as protobuf_file:
|
||||
protobuf_data = protobuf_file.read()
|
||||
drules = libkomwm.ContainerProto()
|
||||
drules.ParseFromString(protobuf_data)
|
||||
|
||||
self.assertEqual(len(drules.cont), 20, "Generated style_output.bin should contain 20 styles")
|
||||
|
||||
finally:
|
||||
# Clean up generated files
|
||||
files2delete = ["classificator.txt", "colors.txt", "patterns.txt", "style_output.bin",
|
||||
"style_output.txt", "types.txt", "visibility.txt"]
|
||||
for filename in files2delete:
|
||||
(assets_dir / filename).unlink(missing_ok=True)
|
||||
|
||||
def test_generate_drules_validation_errors(self):
|
||||
assets_dir = Path(__file__).parent / 'assets' / 'case-3-styles-validation'
|
||||
# TODO: needs refactoring of libkomwm.validation_errors_count to have a list
|
||||
# of validation errors.
|
||||
self.assertTrue(True)
|
364
tests/testMapCSS.py
Normal file
364
tests/testMapCSS.py
Normal file
|
@ -0,0 +1,364 @@
|
|||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add `src` directory to the import paths
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
||||
|
||||
from mapcss import parseDeclaration, MapCSS
|
||||
|
||||
|
||||
class MapCSSTest(unittest.TestCase):
|
||||
def test_declarations(self):
|
||||
decl = parseDeclaration(""" linejoin: round; """)
|
||||
self.assertEqual(len(decl), 1)
|
||||
self.assertEqual(decl[0], {"linejoin": "round"})
|
||||
|
||||
decl = parseDeclaration("""\tlinejoin :\nround ; """)
|
||||
self.assertEqual(len(decl), 1)
|
||||
self.assertEqual(decl[0], {"linejoin": "round"})
|
||||
|
||||
decl = parseDeclaration(""" icon-image: parking_private-s.svg; text: "name"; """)
|
||||
self.assertEqual(len(decl), 1)
|
||||
self.assertEqual(decl[0], {
|
||||
"icon-image": "parking_private-s.svg",
|
||||
"text": "name"
|
||||
})
|
||||
|
||||
decl = parseDeclaration("""
|
||||
pattern-offset: 90\t;
|
||||
pattern-image:\tarrow-m.svg ;
|
||||
pattern-spacing: @trunk0 ;""")
|
||||
self.assertEqual(len(decl), 1)
|
||||
self.assertEqual(decl[0], {
|
||||
"pattern-offset": "90",
|
||||
"pattern-image": "arrow-m.svg",
|
||||
"pattern-spacing": "@trunk0",
|
||||
})
|
||||
|
||||
def test_parse_variables(self):
|
||||
parser = MapCSS()
|
||||
parser.parse("""
|
||||
@city_label: #999999;
|
||||
@country_label: #444444;
|
||||
@wave_length: 25;
|
||||
""")
|
||||
self.assertEqual(parser.variables, {
|
||||
"city_label": "#999999",
|
||||
"country_label": "#444444",
|
||||
"wave_length": "25"
|
||||
})
|
||||
|
||||
def test_parse_colors(self):
|
||||
parser = MapCSS()
|
||||
parser.parse("""
|
||||
@city_label : #999999;
|
||||
@country_label: #444444 ;
|
||||
@wave_length: 25;
|
||||
""")
|
||||
self.assertEqual(parser.variables, {
|
||||
"city_label": "#999999",
|
||||
"country_label": "#444444",
|
||||
"wave_length": "25"
|
||||
})
|
||||
|
||||
def test_parse_import(self):
|
||||
parser = MapCSS()
|
||||
mapcssFile = Path(__file__).parent / 'assets' / 'case-1-import' / 'main.mapcss'
|
||||
parser.parse(filename=str(mapcssFile))
|
||||
|
||||
colors = parser.get_colors()
|
||||
self.assertEqual(colors, {
|
||||
"GuiText-color": (1.0, 1.0, 1.0),
|
||||
"GuiText-opacity": 0.7,
|
||||
"Route-color": (0.0, 0.0, 1.0),
|
||||
"Route-opacity": 0.5,
|
||||
})
|
||||
|
||||
def test_parse_basic_chooser(self):
|
||||
parser = MapCSS()
|
||||
static_tags = {"tourism": True, "office": True,
|
||||
"craft": True, "amenity": True}
|
||||
parser.parse("""
|
||||
node|z17-[tourism],
|
||||
area|z17-[tourism],
|
||||
node|z18-[office],
|
||||
area|z18-[office],
|
||||
node|z18-[craft],
|
||||
area|z18-[craft],
|
||||
node|z19-[amenity],
|
||||
area|z19-[amenity],
|
||||
{text: name; text-color: #000030; text-offset: 1;}
|
||||
""", static_tags=static_tags)
|
||||
|
||||
self.assertEqual(len(parser.choosers), 1)
|
||||
self.assertEqual(len(parser.choosers[0].ruleChains), 8)
|
||||
|
||||
def test_parse_basic_chooser_2(self):
|
||||
parser = MapCSS()
|
||||
static_tags = {"highway": True}
|
||||
parser.parse("""
|
||||
@trunk0: #FF7326;
|
||||
|
||||
line|z6[highway=trunk],
|
||||
line|z6[highway=motorway],
|
||||
{color: @trunk0; opacity: 0.3;}
|
||||
line|z7-9[highway=trunk],
|
||||
line|z7-9[highway=motorway],
|
||||
{color: @trunk0; opacity: 0.7;}
|
||||
""", static_tags=static_tags)
|
||||
|
||||
self.assertEqual(len(parser.choosers), 2)
|
||||
self.assertEqual(len(parser.choosers[0].ruleChains), 2)
|
||||
self.assertEqual(parser.choosers[0].ruleChains[0].subject, 'line')
|
||||
self.assertEqual(parser.choosers[0].selzooms, [6, 6])
|
||||
self.assertEqual(parser.choosers[1].selzooms, [7, 9])
|
||||
|
||||
rule, object_id = parser.choosers[0].testChains({"highway": "trunk"})
|
||||
self.assertEqual(object_id, "::default")
|
||||
|
||||
def test_parse_basic_chooser_3(self):
|
||||
parser = MapCSS()
|
||||
static_tags = {"addr:housenumber": True, "addr:street": False}
|
||||
parser.parse("""
|
||||
/* Some Comment Here */
|
||||
|
||||
/*
|
||||
This sample is borrowed from Organic Maps Basemap_label.mapcss file
|
||||
*/
|
||||
node|z18-[addr:housenumber][addr:street]::int_name
|
||||
{text: int_name; text-color: #65655E; text-position: center;}
|
||||
""", static_tags=static_tags)
|
||||
|
||||
building_tags = {"building": "yes", "addr:housenumber": "12", "addr:street": "Baker street"}
|
||||
|
||||
# Check that mapcss parsed correctly
|
||||
self.assertEqual(len(parser.choosers), 1)
|
||||
styleChooser = parser.choosers[0]
|
||||
self.assertEqual(len(styleChooser.ruleChains), 1)
|
||||
self.assertEqual(styleChooser.selzooms, [18, 19])
|
||||
rule, object_id = styleChooser.testChains(building_tags)
|
||||
self.assertEqual(object_id, "::int_name")
|
||||
|
||||
rule = styleChooser.ruleChains[0]
|
||||
self.assertEqual(rule.subject, 'node')
|
||||
self.assertEqual(rule.extract_tags(), {'addr:housenumber', 'addr:street'})
|
||||
|
||||
def test_parse_basic_chooser_class(self):
|
||||
parser = MapCSS()
|
||||
parser.parse("""
|
||||
way|z-13::*
|
||||
{
|
||||
linejoin: round;
|
||||
}
|
||||
""")
|
||||
|
||||
# Check that mapcss parsed correctly
|
||||
self.assertEqual(len(parser.choosers), 1)
|
||||
styleChooser = parser.choosers[0]
|
||||
self.assertEqual(len(styleChooser.ruleChains), 1)
|
||||
self.assertEqual(styleChooser.selzooms, [0, 13])
|
||||
rule, object_id = styleChooser.testChains({})
|
||||
self.assertEqual(object_id, "::*")
|
||||
|
||||
rule = styleChooser.ruleChains[0]
|
||||
self.assertEqual(rule.subject, 'way')
|
||||
self.assertEqual(rule.extract_tags(), {'*'})
|
||||
|
||||
def test_parse_basic_chooser_class_2(self):
|
||||
parser = MapCSS()
|
||||
parser.parse("""
|
||||
way|z10-::*
|
||||
{
|
||||
linejoin: round;
|
||||
}
|
||||
""")
|
||||
|
||||
# Check that mapcss parsed correctly
|
||||
self.assertEqual(len(parser.choosers), 1)
|
||||
styleChooser = parser.choosers[0]
|
||||
self.assertEqual(len(styleChooser.ruleChains), 1)
|
||||
self.assertEqual(styleChooser.selzooms, [10, 19])
|
||||
rule, object_id = styleChooser.testChains({})
|
||||
self.assertEqual(object_id, "::*")
|
||||
|
||||
rule = styleChooser.ruleChains[0]
|
||||
self.assertEqual(rule.subject, 'way')
|
||||
self.assertEqual(rule.extract_tags(), {'*'})
|
||||
|
||||
def test_parse_basic_chooser_colors(self):
|
||||
parser = MapCSS()
|
||||
parser.parse("""
|
||||
way|z-6::*
|
||||
{
|
||||
linejoin: round;
|
||||
}
|
||||
|
||||
colors {
|
||||
GuiText-color: #FFFFFF;
|
||||
GuiText-opacity: 0.7;
|
||||
MyPositionAccuracy-color: #FFFFFF;
|
||||
MyPositionAccuracy-opacity: 0.06;
|
||||
Selection-color: #FFFFFF;
|
||||
Selection-opacity: 0.64;
|
||||
Route-color: #0000FF;
|
||||
RouteOutline-color: #00FFFF;
|
||||
}
|
||||
""")
|
||||
|
||||
# Check that colors from mapcss parsed correctly
|
||||
colors = parser.get_colors()
|
||||
self.assertEqual(colors, {
|
||||
"GuiText-color": (1.0, 1.0, 1.0),
|
||||
"GuiText-opacity": 0.7,
|
||||
"MyPositionAccuracy-color": (1.0, 1.0, 1.0),
|
||||
"MyPositionAccuracy-opacity": 0.06,
|
||||
"Selection-color": (1.0, 1.0, 1.0),
|
||||
"Selection-opacity": 0.64,
|
||||
"Route-color": (0.0, 0.0, 1.0),
|
||||
"RouteOutline-color": (0.0, 1.0, 1.0)
|
||||
})
|
||||
|
||||
def test_parser_choosers_tree(self):
|
||||
parser = MapCSS()
|
||||
static_tags = {"tourism": True, "office": True,
|
||||
"craft": True, "amenity": True}
|
||||
|
||||
parser.parse("""
|
||||
node|z17-[office=lawyer],
|
||||
area|z17-[office=lawyer],
|
||||
{text: name;text-color: #444444;text-offset: 1;font-size: 10;}
|
||||
|
||||
node|z17-[tourism],
|
||||
area|z17-[tourism],
|
||||
node|z18-[office],
|
||||
area|z18-[office],
|
||||
node|z18-[craft],
|
||||
area|z18-[craft],
|
||||
node|z19-[amenity],
|
||||
area|z19-[amenity],
|
||||
{text: name; text-color: #000030; text-offset: 1;}
|
||||
|
||||
node|z18-[office],
|
||||
area|z18-[office],
|
||||
node|z18-[craft],
|
||||
area|z18-[craft],
|
||||
{font-size: 11;}
|
||||
|
||||
node|z17-[office=lawyer],
|
||||
area|z17-[office=lawyer]
|
||||
{icon-image: lawyer-m.svg;}
|
||||
""", static_tags=static_tags)
|
||||
|
||||
for obj_type in ["line", "area", "node"]:
|
||||
parser.build_choosers_tree("tourism", obj_type, "tourism")
|
||||
parser.build_choosers_tree("office", obj_type, "office")
|
||||
parser.build_choosers_tree("craft", obj_type, "craft")
|
||||
parser.build_choosers_tree("amenity", obj_type, "amenity")
|
||||
|
||||
parser.finalize_choosers_tree()
|
||||
|
||||
# Pick style for zoom = 17
|
||||
styles18 = parser.get_style("office", "node", {"office": "lawyer"},
|
||||
zoom=18, xscale=1, zscale=1, filter_by_runtime_conditions=False)
|
||||
|
||||
self.assertEqual(len(styles18), 1),
|
||||
self.assertEqual(styles18[0], {'object-id': '::default',
|
||||
'font-size': '11',
|
||||
'text': 'name',
|
||||
'text-color': (0, 0, 16*3/255),
|
||||
'text-offset': 1.0,
|
||||
'icon-image': 'lawyer-m.svg'})
|
||||
|
||||
# Pick style for zoom = 17
|
||||
styles17 = parser.get_style("office", "node", {"office": "lawyer"},
|
||||
zoom=17, xscale=1, zscale=1, filter_by_runtime_conditions=False)
|
||||
|
||||
self.assertEqual(len(styles17), 1),
|
||||
self.assertEqual(styles17[0], {'object-id': '::default',
|
||||
'font-size': '10',
|
||||
'text': 'name',
|
||||
'text-color': (68/255, 68/255, 68/255),
|
||||
'text-offset': 1.0,
|
||||
'icon-image': 'lawyer-m.svg'})
|
||||
|
||||
# Pick style for zoom = 15
|
||||
styles15 = parser.get_style("office", "node", {"office": "lawyer"},
|
||||
zoom=15, xscale=1, zscale=1, filter_by_runtime_conditions=False)
|
||||
|
||||
self.assertEqual(styles15, []),
|
||||
|
||||
def test_parser_choosers_tree_with_classes(self):
|
||||
parser = MapCSS()
|
||||
static_tags = {"highway": True}
|
||||
|
||||
parser.parse("""
|
||||
line|z10-[highway=motorway]::shield,
|
||||
line|z10-[highway=trunk]::shield,
|
||||
line|z10-[highway=motorway_link]::shield,
|
||||
line|z10-[highway=trunk_link]::shield,
|
||||
line|z10-[highway=primary]::shield,
|
||||
line|z11-[highway=primary_link]::shield,
|
||||
line|z12-[highway=secondary]::shield,
|
||||
line|z13-[highway=tertiary]::shield,
|
||||
line|z15-[highway=residential]::shield,
|
||||
{
|
||||
shield-font-size: 9;
|
||||
shield-text-color: #000000;
|
||||
shield-text-halo-radius: 0;
|
||||
shield-color: #FFFFFF;
|
||||
shield-outline-radius: 1;
|
||||
}
|
||||
|
||||
line|z12-[highway=residential],
|
||||
line|z12-[highway=tertiary],
|
||||
line|z18-[highway=tertiary_link]
|
||||
{
|
||||
text: name;
|
||||
text-color: #333333;
|
||||
text-halo-opacity: 0.8;
|
||||
text-halo-radius: 1;
|
||||
}
|
||||
|
||||
line|z12-13[highway=residential],
|
||||
line|z12-13[highway=tertiary]
|
||||
{
|
||||
font-size: 12;
|
||||
text-color: #444444;
|
||||
}
|
||||
""", static_tags=static_tags)
|
||||
|
||||
parser.build_choosers_tree("highway", "line", "highway")
|
||||
parser.finalize_choosers_tree()
|
||||
|
||||
# Pick style for zoom = 10
|
||||
styles10 = parser.get_style("highway", "line", {"highway": "primary"},
|
||||
zoom=10, xscale=1, zscale=1, filter_by_runtime_conditions=False)
|
||||
|
||||
self.assertEqual(len(styles10), 1),
|
||||
self.assertEqual(styles10[0], {'object-id': '::shield',
|
||||
'shield-font-size': '9',
|
||||
'shield-text-color': (0.0, 0.0, 0.0),
|
||||
'shield-text-halo-radius': 0.0,
|
||||
'shield-color': (1.0, 1.0, 1.0),
|
||||
'shield-outline-radius': 1.0})
|
||||
|
||||
# Pick style for zoom = 15. Expecting two `object-id` values: '::shield' and '::default'
|
||||
styles15 = parser.get_style("highway", "line", {"highway": "tertiary"},
|
||||
zoom=15, xscale=1, zscale=1, filter_by_runtime_conditions=False)
|
||||
|
||||
self.assertEqual(len(styles15), 2),
|
||||
self.assertEqual(styles15[0], {'object-id': '::shield',
|
||||
'shield-font-size': '9',
|
||||
'shield-text-color': (0.0, 0.0, 0.0),
|
||||
'shield-text-halo-radius': 0.0,
|
||||
'shield-color': (1.0, 1.0, 1.0),
|
||||
'shield-outline-radius': 1.0})
|
||||
self.assertEqual(styles15[1], {'object-id': '::default',
|
||||
'text': 'name',
|
||||
'text-color': (51/255, 51/255, 51/255),
|
||||
'text-halo-opacity': 0.8,
|
||||
'text-halo-radius': 1.0})
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
114
tests/testRule.py
Normal file
114
tests/testRule.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add `src` directory to the import paths
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
||||
|
||||
from mapcss.Rule import Rule
|
||||
from mapcss.Condition import Condition
|
||||
from mapcss import parseCondition
|
||||
|
||||
class RuleTest(unittest.TestCase):
|
||||
def test_rule_subject(self):
|
||||
self.assertEqual(Rule().subject, "")
|
||||
self.assertEqual(Rule("*").subject, "")
|
||||
self.assertEqual(Rule("way").subject, "way")
|
||||
self.assertEqual(Rule("area").subject, "area")
|
||||
self.assertEqual(Rule("node").subject, "node")
|
||||
self.assertEqual(Rule("planet").subject, "planet")
|
||||
|
||||
def test_rule_type_matches(self):
|
||||
self.assertCountEqual(Rule().type_matches, ('area', 'line', 'way', 'node'))
|
||||
self.assertCountEqual(Rule("*").type_matches, ('area', 'line', 'way', 'node'))
|
||||
self.assertCountEqual(Rule("way").type_matches, ('area', 'line', 'way'))
|
||||
self.assertCountEqual(Rule("area").type_matches, ('area', 'way'))
|
||||
self.assertCountEqual(Rule("node").type_matches, ('node', ))
|
||||
self.assertCountEqual(Rule("planet").type_matches, set())
|
||||
|
||||
def test_rule_with_conditions(self):
|
||||
rule = Rule()
|
||||
rule.conditions = [
|
||||
parseCondition("aeroway=aerodrome"),
|
||||
parseCondition("aerodrome=international")
|
||||
]
|
||||
|
||||
tt = rule.test({
|
||||
"aeroway": "aerodrome",
|
||||
"aerodrome": "international",
|
||||
"name": "JFK"
|
||||
})
|
||||
self.assertTrue(tt)
|
||||
self.assertEqual(tt, "::default")
|
||||
|
||||
self.assertCountEqual(rule.extract_tags(), ["aeroway", "aerodrome"])
|
||||
|
||||
# Negative test cases
|
||||
self.assertFalse(rule.test({
|
||||
"aeroway": "aerodrome",
|
||||
"name": "JFK"
|
||||
}))
|
||||
|
||||
def test_rule_with_class(self):
|
||||
rule = Rule()
|
||||
rule.conditions = [
|
||||
parseCondition("highway=unclassified"),
|
||||
parseCondition("bridge?"),
|
||||
Condition("eq", ("::class", "::bridgeblack"))
|
||||
]
|
||||
|
||||
tt = rule.test({
|
||||
"highway": "unclassified",
|
||||
"bridge": "yes",
|
||||
"layer": "1"
|
||||
})
|
||||
self.assertTrue(tt)
|
||||
self.assertEqual(tt, "::bridgeblack")
|
||||
|
||||
self.assertCountEqual(rule.extract_tags(), ["highway", "bridge"])
|
||||
|
||||
# Negative test cases
|
||||
self.assertFalse(rule.test({
|
||||
"highway": "unclassified",
|
||||
"bridge": "no",
|
||||
"layer": "1"
|
||||
}))
|
||||
self.assertFalse(rule.test({
|
||||
"highway": "unclassified",
|
||||
"tunnel": "yes",
|
||||
"layer": "-1"
|
||||
}))
|
||||
|
||||
def test_tags_from_rule_with_class(self):
|
||||
# Class condition doesn't add new tags
|
||||
rule = Rule()
|
||||
rule.conditions = [
|
||||
parseCondition("highway=unclassified"),
|
||||
parseCondition("bridge?"),
|
||||
Condition("eq", ("::class", "::bridgeblack")),
|
||||
]
|
||||
|
||||
self.assertCountEqual(rule.extract_tags(), ["highway", "bridge"])
|
||||
|
||||
# Class condition doesn't add new tags
|
||||
rule = Rule()
|
||||
rule.conditions = [
|
||||
parseCondition("highway=unclassified"),
|
||||
Condition("eq", ("::class", "::*")),
|
||||
parseCondition("bridge?"),
|
||||
]
|
||||
|
||||
self.assertCountEqual(rule.extract_tags(), ["highway", "bridge"])
|
||||
|
||||
# BUT having class as a first item overrides all the others
|
||||
rule = Rule()
|
||||
rule.conditions = [
|
||||
Condition("eq", ("::class", "::int_name")),
|
||||
parseCondition("highway=unclassified"),
|
||||
parseCondition("bridge?"),
|
||||
]
|
||||
|
||||
self.assertCountEqual(rule.extract_tags(), ["*"])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
297
tests/testStyleChooser.py
Normal file
297
tests/testStyleChooser.py
Normal file
|
@ -0,0 +1,297 @@
|
|||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from mapcss.Rule import Rule
|
||||
|
||||
# Add `src` directory to the import paths
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
|
||||
|
||||
from mapcss import parseCondition, Condition
|
||||
from mapcss.Eval import Eval
|
||||
from mapcss.StyleChooser import StyleChooser, make_nice_style
|
||||
|
||||
|
||||
class StyleChooserTest(unittest.TestCase):
|
||||
def test_rules_chain(self):
|
||||
sc = StyleChooser((0, 16))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(parseCondition("highway=footway"))
|
||||
sc.addCondition(parseCondition("footway=sidewalk"))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(parseCondition("highway=footway"))
|
||||
sc.addCondition(parseCondition("footway=crossing"))
|
||||
sc.addCondition(Condition("eq", ("::class", "::*")))
|
||||
|
||||
self.assertTrue( sc.testChains({ "highway": "footway", "footway": "sidewalk" }) )
|
||||
self.assertTrue( sc.testChains({ "highway": "footway", "footway": "crossing" }) )
|
||||
self.assertFalse( sc.testChains({ "highway": "footway"}) )
|
||||
self.assertFalse( sc.testChains({ "highway": "residential", "footway": "crossing" }) )
|
||||
|
||||
rule1, tt = sc.testChains({ "highway": "footway", "footway": "sidewalk" })
|
||||
self.assertEqual(tt, "::default")
|
||||
|
||||
rule2, tt = sc.testChains({ "highway": "footway", "footway": "crossing" })
|
||||
self.assertEqual(tt, "::*")
|
||||
|
||||
self.assertNotEqual(rule1, rule2)
|
||||
|
||||
def test_zoom(self):
|
||||
sc = StyleChooser((0, 16))
|
||||
|
||||
sc.newObject()
|
||||
sc.addZoom( (10, 19) )
|
||||
sc.addCondition(parseCondition("railway=station"))
|
||||
sc.addCondition(parseCondition("transport=subway"))
|
||||
sc.addCondition(parseCondition("city=yerevan"))
|
||||
|
||||
sc.newObject()
|
||||
sc.addZoom( (4, 15) )
|
||||
sc.addCondition(parseCondition("railway=station"))
|
||||
sc.addCondition(parseCondition("transport=subway"))
|
||||
sc.addCondition(parseCondition("city=yokohama"))
|
||||
|
||||
rule1, tt = sc.testChains({ "railway": "station", "transport": "subway", "city": "yerevan" })
|
||||
self.assertEqual(rule1.minZoom, 10)
|
||||
self.assertEqual(rule1.maxZoom, 19)
|
||||
|
||||
rule2, tt = sc.testChains({ "railway": "station", "transport": "subway", "city": "yokohama" })
|
||||
self.assertEqual(rule2.minZoom, 4)
|
||||
self.assertEqual(rule2.maxZoom, 15)
|
||||
|
||||
def test_extract_tags(self):
|
||||
sc = StyleChooser((0, 16))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(parseCondition("aerialway=rope_tow"))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(parseCondition("piste:type=downhill"))
|
||||
|
||||
self.assertSetEqual(sc.extract_tags(), {"aerialway", "piste:type"})
|
||||
|
||||
sc = StyleChooser((0, 16))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(parseCondition("aeroway=terminal"))
|
||||
sc.addCondition(parseCondition("building"))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(parseCondition("waterway=dam"))
|
||||
sc.addCondition(parseCondition("building:part"))
|
||||
|
||||
self.assertSetEqual(sc.extract_tags(), {"waterway", "building:part", "building", "aeroway"})
|
||||
|
||||
def test_make_nice_style(self):
|
||||
style = make_nice_style({
|
||||
"outline-color": "none",
|
||||
"bg-color": "red",
|
||||
"dash-color": "#ffff00",
|
||||
"front-color": "rgb(0, 255, 255)",
|
||||
"line-width": Eval("""eval(min(tag("line_width"), 10))"""),
|
||||
"outline-width": "2.5",
|
||||
"arrow-opacity": "0.5",
|
||||
"offset-2": "20",
|
||||
"border-radius": "4",
|
||||
"line-extrude": "16",
|
||||
"dashes": "3,3,1.5,3",
|
||||
"wrong-dashes": "yes, yes, yes, no",
|
||||
"make-nice": True,
|
||||
"additional-len": 44.5
|
||||
})
|
||||
|
||||
expectedStyle = {
|
||||
"outline-color": "none",
|
||||
"bg-color": (1.0, 0.0, 0.0),
|
||||
"dash-color": (1.0, 1.0, 0.0),
|
||||
"front-color": (0.0, 1.0, 1.0),
|
||||
"line-width": Eval("""eval(min(tag("line_width"), 10))"""),
|
||||
"outline-width": 2.5,
|
||||
"arrow-opacity": 0.5,
|
||||
"offset-2": 20.0,
|
||||
"border-radius": 4.0,
|
||||
"line-extrude": 16.0,
|
||||
"dashes": [3.0, 3.0, 1.5, 3.0],
|
||||
"wrong-dashes": [],
|
||||
"make-nice": True,
|
||||
"additional-len": 44.5
|
||||
}
|
||||
|
||||
self.assertEqual(style, expectedStyle)
|
||||
|
||||
def test_add_styles(self):
|
||||
sc = StyleChooser((15, 19))
|
||||
sc.newObject()
|
||||
sc.addStyles([{
|
||||
"width": "1.3",
|
||||
"opacity": "0.6",
|
||||
"bg-color": "blue"
|
||||
}])
|
||||
sc.addStyles([{
|
||||
"color": "#FFFFFF",
|
||||
"casing-width": "+10"
|
||||
}])
|
||||
|
||||
self.assertEqual(len(sc.styles), 2)
|
||||
self.assertEqual(sc.styles[0], {
|
||||
"width": 1.3,
|
||||
"opacity": 0.6,
|
||||
"bg-color": (0.0, 0.0, 1.0)
|
||||
})
|
||||
self.assertEqual(sc.styles[1], {
|
||||
"color": (1.0, 1.0, 1.0),
|
||||
"casing-width": 5.0
|
||||
})
|
||||
|
||||
def test_update_styles(self):
|
||||
styles = [{"primary_color": (1.0, 1.0, 1.0)}]
|
||||
|
||||
sc = StyleChooser((15, 19))
|
||||
sc.newObject()
|
||||
sc.addStyles([{
|
||||
"width": "1.3",
|
||||
"opacity": "0.6",
|
||||
"bg-color": """eval( prop("primary_color") )""", # Check that property from `styles` is applied
|
||||
"text-offset": """eval( cond( boolean(tag("oneway")), 10, 5) )""" # Check that tags are applied
|
||||
}])
|
||||
|
||||
object_tags = {"highway": "service",
|
||||
"oneway": "yes"}
|
||||
new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False)
|
||||
expected_new_styles = {
|
||||
"width": 1.3,
|
||||
"opacity": 0.6,
|
||||
"bg-color": (1.0, 1.0, 1.0),
|
||||
"text-offset": 10.0,
|
||||
"object-id": "::default"
|
||||
}
|
||||
|
||||
self.assertEqual(len(new_styles), 2)
|
||||
self.assertEqual(new_styles[-1], expected_new_styles)
|
||||
|
||||
def test_update_styles_2(self):
|
||||
styles = []
|
||||
|
||||
sc = StyleChooser((15, 19))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(Condition("eq", ("::class", "::int_name") )) # Class should be added to the style
|
||||
sc.addCondition(parseCondition("oneway?"))
|
||||
|
||||
sc.addStyles([{
|
||||
"width": "1.3",
|
||||
"bg-color": "black"
|
||||
}])
|
||||
|
||||
object_tags = {"highway": "service", "oneway": "yes"}
|
||||
new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False)
|
||||
expected_new_styles = {
|
||||
"width": 1.3,
|
||||
"bg-color": (0.0, 0.0, 0.0),
|
||||
"object-id": "::int_name" # Check that class from sc.ruleChains is added to the style
|
||||
}
|
||||
|
||||
self.assertEqual(len(new_styles), 1)
|
||||
self.assertEqual(new_styles[-1], expected_new_styles)
|
||||
|
||||
|
||||
def test_update_styles_by_class(self):
|
||||
# Predefined styles
|
||||
styles = [{
|
||||
"some-width": 2.5,
|
||||
"object-id": "::flats"
|
||||
},
|
||||
{
|
||||
"some-width": 3.5,
|
||||
"object-id": "::bridgeblack"
|
||||
},
|
||||
{
|
||||
"some-width": 4.5,
|
||||
"object-id": "::default"
|
||||
}]
|
||||
|
||||
sc = StyleChooser((15, 19))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(Condition("eq", ("::class", "::flats") )) # `sc` styles should apply only to `::flats` class
|
||||
sc.addCondition(parseCondition("oneway?"))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(Condition("eq", ("::class", "::bridgeblack") )) # This class is ignored by StyleChooser
|
||||
sc.addCondition(parseCondition("oneway?"))
|
||||
|
||||
sc.addStyles([{
|
||||
"some-width": "1.5",
|
||||
"other-offset": "4"
|
||||
}])
|
||||
|
||||
object_tags = {"highway": "service", "oneway": "yes"}
|
||||
|
||||
# Apply new style to predefined styles with filter by class
|
||||
new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False)
|
||||
|
||||
expected_new_styles = [{ # The first style changes
|
||||
"some-width": 1.5,
|
||||
"other-offset": 4.0,
|
||||
"object-id": "::flats"
|
||||
},
|
||||
{ # Style not changed (class is not `::flats`)
|
||||
"some-width": 3.5,
|
||||
"object-id": "::bridgeblack"
|
||||
},
|
||||
{ # Style not changed (class is not `::flats`)
|
||||
"some-width": 4.5,
|
||||
"object-id": "::default"
|
||||
}]
|
||||
|
||||
self.assertEqual(len(new_styles), 3)
|
||||
self.assertEqual(new_styles, expected_new_styles)
|
||||
|
||||
|
||||
def test_update_styles_by_class_all(self):
|
||||
# Predefined styles
|
||||
styles = [{ # This is applied to StyleChooser styles
|
||||
"some-width": 2.5,
|
||||
"corner-radius": 2.5,
|
||||
"object-id": "::*"
|
||||
},
|
||||
{
|
||||
"some-width": 3.5,
|
||||
"object-id": "::bridgeblack"
|
||||
}]
|
||||
|
||||
sc = StyleChooser((15, 19))
|
||||
|
||||
sc.newObject()
|
||||
sc.addCondition(parseCondition("tunnel"))
|
||||
|
||||
sc.addStyles([{
|
||||
"some-width": "1.5",
|
||||
"other-offset": "4"
|
||||
}])
|
||||
object_tags = {"highway": "service", "tunnel": "yes"}
|
||||
|
||||
# Apply new style to predefined styles with filter by class
|
||||
new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False)
|
||||
|
||||
# Check that new style with new `object-id` is added.
|
||||
# This style is built from `styles[0]` and styles from `sc`
|
||||
expected_new_style = {
|
||||
"some-width": 1.5,
|
||||
"corner-radius": 2.5,
|
||||
"other-offset": 4.0,
|
||||
"object-id": "::default" # New class, never listed in `styles`
|
||||
}
|
||||
|
||||
self.assertEqual(len(new_styles), 3)
|
||||
self.assertEqual(new_styles[-1], expected_new_style)
|
||||
|
||||
|
||||
def test_runtime_conditions(self):
|
||||
# TODO: Create test with sc.addRuntimeCondition(Condition(condType, ('extra_tag', cond)))
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
Add table
Reference in a new issue