Compare commits

..

80 commits

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

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

Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-18 18:01:11 -03:00
04f5f44137 Allow setting negative overlays priorities to e.g. put icons below automatic optional captions
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-14 12:49:03 -03:00
24b8586c23 Update comments
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-11 20:16:16 -03:00
024810fb85 Validate visibilities
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
a03d74f588 Load priorities from files without explicit drule types specified
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
0e23a0e798 Remove drule types from prio.txt files
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
fbecca40c3 Make optional captions below other overlays 2023-08-07 11:59:25 -03:00
2d5c553af8 Add comments with visibility range and other drules info
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
b30c3bee84 Load grouped priorities
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
41e9860567 Group types with same priorities
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
98755e5d6a Add auto-fixing of captions/pathtexts priorities being higher than icon/shield
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
8c5e152621 Disable priorities compression by default
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
9c914f097b Compress / re-space priorities evenly
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
58dcaaba51 Load priorities from *.prio.txt files
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
0850cde772 Dump priorities into separate *.prio.txt files
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
b34963026a Calculate minVisibleScale for overlays only
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
492906e130 Prepend minVisibleScale to priority values
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
8e8f17bd29 Remove unused zoom 0 from drules output
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-08-07 11:59:25 -03:00
ebe1ced85c Remove empty casing dashdot definitions
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-07-08 15:20:54 -03:00
2f311e7504 Add a bg-top priorities range
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-06-23 09:21:13 -03:00
dbba1c41e0 Allow comments in mapcss-mapping.csv
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-06-20 21:56:13 +02:00
d580b748f2 Make captions optional by default
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-03-26 12:08:57 -03:00
5b160e185f Remove apply_for_type symbol attribute
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-15 11:33:00 -03:00
9b38690e69 Optimize choosers by discarding non-matching rules
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
27b41b5e3f Add zoom into choosers optimization tree
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
70fa78f39b Optimize frequently called functions
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
41498a6ec5 Process unique runtime conditions once only
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
4e29892282 Optimize looking for runtime conditions in selectors
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-02-10 12:31:30 -03:00
a6daa7121b Make per-tag sets of selectors more precise and small
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-01-21 12:57:14 -03:00
471abbd754 Look for missing base_width to use with casing-width
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-01-20 11:29:46 -03:00
32cc5aafb6 Add casing-width-add support
Signed-off-by: Konstantin Pastbin <konstantin.pastbin@gmail.com>
2023-01-20 11:29:46 -03:00
Viktor Govako
f9de08f90f
Merge pull request #4 from organicmaps/vng-fix
Don't mix "casing-" for line and area rules.
2023-01-17 17:30:35 -03:00
Viktor Govako
d6a507cfc0 Don't mix "casing-" for line and area rules.
Signed-off-by: Viktor Govako <viktor.govako@gmail.com>
2022-06-07 16:45:54 +03:00
Viktor Govako
01d4e4586c
Merge pull request #3 from organicmaps/vng-python3
Python3 + priorities ascending order.
2022-05-17 11:50:11 +03:00
Viktor Govako
89b59de837 Python3 migration.
Signed-off-by: Viktor Govako <viktor.govako@gmail.com>
2022-05-16 17:06:19 +03:00
Viktor Govako
367da7e676 Added -x-me-min-text-priority hack.
Signed-off-by: Viktor Govako <viktor.govako@gmail.com>
2022-05-16 12:29:40 +03:00
Evgeniy A. Dushistov
967fcbc17f migration to python3
Signed-off-by: Evgeniy A. Dushistov <dushistov@mail.ru>
2021-05-02 22:41:05 +02:00
mpimenov
cbaff545dd
Merge pull request #37 from tatiana-yan/mark_original_type
Mark original type to distinguish it among replacing types.
2020-02-28 16:23:06 +03:00
tatiana-yan
d26470bc11 Mark original type to distinguish it among replacing types. 2020-02-28 16:22:16 +03:00
Ilya Zverev
0fb640bd0e Fix static tags detection by skipping obsolete lines 2017-12-28 15:40:02 +03:00
Roman Kuznetsov
2dc0d72e3f Merge pull request #35 from mapsme/extra_tag
Support extra tags for libkomwm
2017-07-27 16:49:40 +03:00
Ilya Zverev
2ef9e75495 Remove hardcoded sport key reference 2017-07-27 16:26:21 +03:00
Ilya Zverev
071089b704 Support extra tags for libkomwm 2017-07-27 16:03:28 +03:00
Roman Kuznetsov
01addafa2e Updated drules after protobuf updating 2017-07-10 15:58:01 +03:00
Roman Kuznetsov
edf671e62a Merge pull request #33 from mapsme/no_dup
Remove printing of a message
2017-03-22 17:41:31 +03:00
Ilya Zverev
21bdff8b77 Remove print 2017-03-22 17:39:48 +03:00
Ilya Zverev
17d84174bf Merge pull request #32 from rokuz/casing-fixes
Added strict checking of casing necessity
2017-02-14 13:40:42 +04:00
r.kuznetsov
c5bda708d1 Added strict checking of casing necessity 2017-02-14 12:38:14 +03:00
Ilya Zverev
8312fed1e7 Merge pull request #30 from rokuz/added-area-border-color
Added border color for areas
2017-02-14 12:56:02 +04:00
r.kuznetsov
9e80259b86 Added casing(border) color to areas 2017-02-14 11:35:03 +03:00
Ilya Zverev
c2e53c52d7 Merge pull request #29 from therearesomewhocallmetim/fix_duplicates
Added a check for duplicates.
2017-02-10 17:17:01 +04:00
Timofey
93795f7aef Added a check for duplicates. 2017-02-10 15:28:07 +03:00
Ilya Zverev
027fcfda67 Merge pull request #28 from rokuz/rename-proto
Renamed fields according to protobuf
2017-02-09 15:28:54 +04:00
r.kuznetsov
853c8595f8 Renamed fields according to protobuf 2017-02-09 14:27:49 +03:00
Ilya Zverev
0fabae77ca Merge pull request #27 from rokuz/style-fix
Some pretty renaming
2017-02-08 19:04:44 +04:00
r.kuznetsov
5d2f32c0df Some pretty renaming 2017-02-08 18:03:12 +03:00
Ilya Zverev
993ef6b88a Merge pull request #26 from rokuz/added-colors-section
Added colors section
2017-02-08 14:05:39 +04:00
r.kuznetsov
64d9ba6b63 Review fixes 2017-02-08 13:00:30 +03:00
Roman Kuznetsov
cb9d19f210 Merge pull request #24 from mapsme/datapath
Allow specifying data path
2017-02-07 11:46:11 +03:00
r.kuznetsov
2d0c2df3dd Added colors section 2017-02-03 17:03:51 +03:00
Ilya Zverev
8bd066810b Merge pull request #25 from rokuz/shield-rendering
Added shields rendering
2017-01-31 15:47:57 +04:00
r.kuznetsov
7e57de2da9 Added shield rendering 2017-01-26 18:04:46 +03:00
Ilya Zverev
d0309ca25d Allow specifying data path 2016-11-16 11:30:02 +03:00
Sergey Yershov
1ffa4306fe Merge pull request #23 from mapsme/check_dup
Check for type duplicates
2016-11-11 13:32:01 +03:00
Ilya Zverev
cb2dc4258d Check for type duplicates 2016-11-11 13:17:07 +03:00
Sergey Yershov
987cf9f223 Merge pull request #22 from mapsme/multi
Multiprocessing for style building
2016-11-10 18:09:31 +03:00
38 changed files with 2973 additions and 1331 deletions

1
.gitignore vendored
View file

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

4
README
View file

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

37
README.md Normal file
View file

@ -0,0 +1,37 @@
Kothic Mapcss parser/processor tailored for Organic Maps use.
Dependencies:
* Python >= 3.8
Python dependencies:
```bash
pip3 install -r requirements.txt
```
## Running unittests
To run all unittests execute next command from project root folder:
```bash
python3 -m unittest discover -s tests
```
this will search for all `test*.py` files within `tests` directory
and execute tests from those files.
## Running integration tests
File `integration-tests/full_drules_gen.py` is intended to generate drules
files for all 6 themes from main Organic Maps repo. It could be used to understand
which parts of the project are actually used by Organic Maps repo.
Usage:
```shell
cd integration-tests
python3 full_drules_gen.py -d ../../../data -o drules --txt
```
This command will run generation for styles - default light, default dark,
outdoors light, outdoors dark, vehicle light, vehicle dark and put `*.bin`
and `*.txt` files into 'drules' subfolder.

View file

View file

@ -0,0 +1,73 @@
#!/usr/bin/env python3
import sys
from copy import deepcopy
from optparse import OptionParser
from pathlib import Path
import logging
# Add `src` directory to the import paths
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
import libkomwm
FORMAT = '%(asctime)s [%(levelname)s] %(message)s'
logging.basicConfig(format=FORMAT)
log = logging.getLogger('test_drules_gen')
log.setLevel(logging.INFO)
styles = {
'default_light': ['styles/default/light/style.mapcss', 'styles/default/include'],
'default_dark': ['styles/default/dark/style.mapcss', 'styles/default/include'],
'outdoors_light': ['styles/outdoors/light/style.mapcss', 'styles/outdoors/include'],
'outdoors_dark': ['styles/outdoors/dark/style.mapcss', 'styles/outdoors/include'],
'vehicle_light': ['styles/vehicle/light/style.mapcss', 'styles/vehicle/include'],
'vehicle_dark': ['styles/vehicle/dark/style.mapcss', 'styles/vehicle/include'],
}
def full_styles_regenerate(options):
log.info("Start generating styles")
libkomwm.MULTIPROCESSING = False
prio_ranges_orig = deepcopy(libkomwm.prio_ranges)
for name, (style_path, include_path) in styles.items():
log.info(f"Generating {name} style ...")
# Restore initial state
libkomwm.prio_ranges = deepcopy(prio_ranges_orig)
libkomwm.visibilities = {}
options.filename = options.data + '/' + style_path
options.priorities_path = options.data + '/' + include_path
options.outfile = options.outdir + '/' + name
# Run generation
libkomwm.komap_mapswithme(options)
log.info(f"Done!")
def main():
parser = OptionParser()
parser.add_option("-d", "--data-path", dest="data",
help="path to mapcss-mapping.csv and other files", metavar="PATH")
parser.add_option("-o", "--output-dir", dest="outdir", default="drules",
help="output directory", metavar="DIR")
parser.add_option("-f", "--minzoom", dest="minzoom", default=0, type="int",
help="minimal available zoom level", metavar="ZOOM")
parser.add_option("-t", "--maxzoom", dest="maxzoom", default=20, type="int",
help="maximal available zoom level", metavar="ZOOM")
parser.add_option("-x", "--txt", dest="txt", action="store_true",
help="create a text file for output", default=False)
(options, args) = parser.parse_args()
if options.data is None:
parser.error("Please specify base 'data' path.")
if options.outdir is None:
parser.error("Please specify base output path.")
full_styles_regenerate(options)
if __name__ == '__main__':
main()

3
requirements.txt Normal file
View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,7 @@ class Condition:
params = (params,) params = (params,)
self.params = params # e.g. ('highway','primary') self.params = params # e.g. ('highway','primary')
if typez == "regex": 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): def extract_tag(self):
if self.params[0][:2] == "::" or self.type == "regex": if self.params[0][:2] == "::" or self.type == "regex":
@ -33,55 +33,57 @@ class Condition:
def test(self, tags): def test(self, tags):
""" """
Test a hash against this condition Test tags against this condition
""" """
t = self.type t = self.type
params = self.params 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] == "::": if params[0][:2] == "::":
return params[1] return params[1]
try: return (params[0] in tags and tags[params[0]] == params[1])
if t == 'eq': if t == 'ne':
return tags[params[0]] == params[1] return (params[0] not in tags or tags[params[0]] != params[1])
if t == 'ne': if t == 'true':
return tags.get(params[0], "") != params[1] return tags.get(params[0]) == 'yes'
if t == 'regex': if t == 'untrue':
return bool(self.regex.match(tags[params[0]])) return tags.get(params[0]) == 'no'
if t == 'true': if t == 'set':
return tags.get(params[0]) == 'yes' if params[0] in tags:
if t == 'untrue': return tags[params[0]] != ''
return tags.get(params[0]) == 'no' return False
if t == 'set': if t == 'unset':
if params[0] in tags: if params[0] in tags:
return tags[params[0]] != '' return tags[params[0]] == ''
return False return True
if t == 'unset':
if params[0] in tags: if params[0] not in tags:
return tags[params[0]] == '' return False
return True if t == 'regex':
if t == '<': return bool(self.regex.match(tags[params[0]]))
return (Number(tags[params[0]]) < Number(params[1])) if t == '<':
if t == '<=': return (Number(tags[params[0]]) < Number(params[1]))
return (Number(tags[params[0]]) <= Number(params[1])) if t == '<=':
if t == '>': return (Number(tags[params[0]]) <= Number(params[1]))
return (Number(tags[params[0]]) > Number(params[1])) if t == '>':
if t == '>=': return (Number(tags[params[0]]) > Number(params[1]))
return (Number(tags[params[0]]) >= Number(params[1])) if t == '>=':
except KeyError: return (Number(tags[params[0]]) >= Number(params[1]))
pass
return False return False
def __repr__(self): def __repr__(self):
t = self.type t = self.type
params = self.params params = self.params
if t == 'eq' and params[0][:2] == "::": if t == 'eq' and params[0][:2] == "::":
return "::%s" % (params[1]) return "%s" % (params[1])
if t == 'eq': if t == 'eq':
return "%s=%s" % (params[0], params[1]) return "%s=%s" % (params[0], params[1])
if t == 'ne': if t == 'ne':
return "%s=%s" % (params[0], params[1]) return "%s!=%s" % (params[0], params[1])
if t == 'regex': if t == 'regex':
return "%s=~/%s/" % (params[0], params[1]); return "%s=~/%s/" % (params[0], params[1])
if t == 'true': if t == 'true':
return "%s?" % (params[0]) return "%s?" % (params[0])
if t == 'untrue': if t == 'untrue':
@ -103,6 +105,9 @@ class Condition:
def __eq__(self, a): def __eq__(self, a):
return (self.params == a.params) and (self.type == a.type) return (self.params == a.params) and (self.type == a.type)
def __lt__(self, a):
return (self.params < a.params) or (self.type < a.type)
def Number(tt): def Number(tt):
""" """
Wrap float() not to produce exceptions Wrap float() not to produce exceptions

View file

@ -15,6 +15,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with kothic. If not, see <http://www.gnu.org/licenses/>. # along with kothic. If not, see <http://www.gnu.org/licenses/>.
import logging
logger = logging.getLogger('mapcss.Eval')
logger.setLevel(logging.ERROR)
class Eval(): class Eval():
def __init__(self, s='eval()'): def __init__(self, s='eval()'):
@ -44,8 +48,11 @@ class Eval():
# print self.expr_text # print self.expr_text
tags = set([]) tags = set([])
def tags_add(x):
tags.add(x)
return 0
a = eval(self.expr, {}, { a = eval(self.expr, {}, {
"tag": lambda x: max([tags.add(x), 0]), "tag": lambda x: tags_add(x),
"prop": lambda x: 0, "prop": lambda x: 0,
"num": lambda x: 0, "num": lambda x: 0,
"metric": fake_compute, "metric": fake_compute,
@ -54,6 +61,8 @@ class Eval():
"any": fake_compute, "any": fake_compute,
"min": fake_compute, "min": fake_compute,
"max": fake_compute, "max": fake_compute,
"cond": fake_compute,
"boolean": fake_compute,
}) })
return tags return tags
@ -69,7 +78,7 @@ class Eval():
pass pass
""" """
try: try:
return str(eval(self.expr, {}, { result = eval(self.expr, {}, {
"tag": lambda x: tags.get(x, ""), "tag": lambda x: tags.get(x, ""),
"prop": lambda x: props.get(x, ""), "prop": lambda x: props.get(x, ""),
"num": m_num, "num": m_num,
@ -81,13 +90,30 @@ class Eval():
"max": m_max, "max": m_max,
"cond": m_cond, "cond": m_cond,
"boolean": m_boolean "boolean": m_boolean
})) })
except:
if type(result) == float:
# In Python2 and Python3 float to string behaves differently
# Python 2:
# >>> str(2.8 + 0.4)
# '3.2'
# Python 3:
# >>> str(2.8 + 0.4)
# '3.1999999999999997'
#
# See https://stackoverflow.com/q/25898733 for details
return "{:.4g}".format(result)
return str(result)
except Exception as e:
logger.warning(f"Error evaluating expression `{self.expr_text}`", e)
return "" return ""
def __repr__(self): def __repr__(self):
return "eval(%s)" % self.expr_text 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): def m_boolean(expr):
expr = str(expr) expr = str(expr)
@ -176,6 +202,6 @@ def m_metric(x, t):
if __name__ == "__main__": if __name__ == "__main__":
a = Eval(""" eval( any( metric(tag("height")), metric ( num(tag("building:levels")) * 3), metric("1m"))) """) a = Eval(""" eval( any( metric(tag("height")), metric ( num(tag("building:levels")) * 3), metric("1m"))) """)
print repr(a) print(repr(a))
print a.compute({"building:levels": "3"}) print(a.compute({"building:levels": "3"}))
print a.extract_tags() print(a.extract_tags())

View file

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

View file

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

View file

@ -18,13 +18,14 @@
import re import re
import os import os
import logging import logging
from StyleChooser import StyleChooser from .StyleChooser import StyleChooser
from Condition import Condition from .Condition import Condition
NEEDED_KEYS = set(["width", "casing-width", "fill-color", "fill-image", "icon-image", "text", "extrude", NEEDED_KEYS = set(["width", "casing-width", "casing-width-add", "fill-color", "fill-image", "icon-image", "text", "extrude",
"background-image", "background-color", "pattern-image", "shield-text", "symbol-shape"]) "background-image", "background-color", "pattern-image", "shield-color", "symbol-shape"])
# TODO: Unused constant
WHITESPACE = re.compile(r'\s+ ', re.S | re.X) WHITESPACE = re.compile(r'\s+ ', re.S | re.X)
COMMENT = 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) UNKNOWN = re.compile(r'(\S+) \s* ', re.S | re.X)
ZOOM_MINMAX = re.compile(r'(\d+)\-(\d+) $', 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_MIN = re.compile(r'(\d+)\- $', re.S | re.X)
ZOOM_MAX = 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) 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_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_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_SET = re.compile(r'\s* ([-:\w]+) \s* $', re.S | re.X)
CONDITION_UNSET = 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_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_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_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_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_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_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_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_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) 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_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 = 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) 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) EXIT = re.compile(r'\s* exit \s* $', re.I | re.S | re.X)
oNONE = 0 oNONE = 0
oZOOM = 2 oZOOM = 2
@ -73,6 +75,7 @@ oDECLARATION = 6
oSUBPART = 7 oSUBPART = 7
oVARIABLE_SET = 8 oVARIABLE_SET = 8
# TODO: Following block of variables is never used
DASH = re.compile(r'\-/g') DASH = re.compile(r'\-/g')
COLOR = re.compile(r'color$/') COLOR = re.compile(r'color$/')
BOLD = re.compile(r'^bold$/i') BOLD = re.compile(r'^bold$/i')
@ -81,6 +84,7 @@ UNDERLINE = re.compile(r'^underline$/i')
CAPS = re.compile(r'^uppercase$/i') CAPS = re.compile(r'^uppercase$/i')
CENTER = re.compile(r'^center$/i') CENTER = re.compile(r'^center$/i')
# TODO: Remove unused HEX variable
HEX = re.compile(r'^#([0-9a-f]+)$/i') HEX = re.compile(r'^#([0-9a-f]+)$/i')
VARIABLE = re.compile(r'@([a-z][\w\d]*)') VARIABLE = re.compile(r'@([a-z][\w\d]*)')
@ -96,8 +100,9 @@ class MapCSS():
self.scalepair = (minscale, maxscale) self.scalepair = (minscale, maxscale)
self.choosers = [] self.choosers = []
self.choosers_by_type = {} self.choosers_by_type = {}
self.choosers_by_type_and_tag = {} self.choosers_by_type_zoom_tag = {}
self.variables = {} self.variables = {}
self.unused_variables = set()
self.style_loaded = False self.style_loaded = False
def parseZoom(self, s): def parseZoom(self, s):
@ -110,47 +115,67 @@ class MapCSS():
elif ZOOM_SINGLE.match(s): elif ZOOM_SINGLE.match(s):
return float(ZOOM_SINGLE.match(s).groups()[0]), float(ZOOM_SINGLE.match(s).groups()[0]) return float(ZOOM_SINGLE.match(s).groups()[0]), float(ZOOM_SINGLE.match(s).groups()[0])
else: else:
# TODO: Should we raise an exception here?
logging.error("unparsed zoom: %s" % s) logging.error("unparsed zoom: %s" % s)
def build_choosers_tree(self, clname, type, tags={}): def build_choosers_tree(self, clname, type, cltag):
if type not in self.choosers_by_type_and_tag: if type not in self.choosers_by_type_zoom_tag:
self.choosers_by_type_and_tag[type] = {} self.choosers_by_type_zoom_tag[type] = {}
if clname not in self.choosers_by_type_and_tag[type]: for zoom in range(self.minscale, self.maxscale + 1):
self.choosers_by_type_and_tag[type][clname] = set() 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: if type in self.choosers_by_type:
for chooser in self.choosers_by_type[type]: for chooser in self.choosers_by_type[type]:
for tag in chooser.extract_tags(): chooser_tags = chooser.extract_tags()
if tag == "*" or tag in tags: if '*' in chooser_tags or cltag in chooser_tags:
if chooser not in self.choosers_by_type_and_tag[type][clname]: for zoom in range(int(chooser.selzooms[0]), int(chooser.selzooms[1]) + 1):
self.choosers_by_type_and_tag[type][clname].add(chooser) if chooser not in self.choosers_by_type_zoom_tag[type][zoom][clname]['set']:
break 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 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): def get_runtime_rules(self, clname, type, tags, zoom):
""" """
Returns array of runtime_conditions which are used for clname/type/tags/zoom Returns array of runtime_conditions which are used for clname/type/tags/zoom
""" """
runtime_rules = [] runtime_rules = []
if type in self.choosers_by_type_and_tag: if type in self.choosers_by_type_zoom_tag:
for chooser in self.choosers_by_type_and_tag[type][clname]: for chooser in self.choosers_by_type_zoom_tag[type][zoom][clname]:
runtime_conditions = chooser.get_runtime_conditions(type, tags, zoom) runtime_conditions = chooser.get_runtime_conditions(tags)
if runtime_conditions: if runtime_conditions:
runtime_rules.append(runtime_conditions) runtime_rules.append(runtime_conditions)
return runtime_rules 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): def get_style(self, clname, type, tags, zoom, xscale, zscale, filter_by_runtime_conditions):
style = [] style = []
if type in self.choosers_by_type_and_tag: if type in self.choosers_by_type_zoom_tag:
for chooser in self.choosers_by_type_and_tag[type][clname]: for chooser in self.choosers_by_type_zoom_tag[type][zoom][clname]:
style = chooser.updateStyles(style, type, tags, zoom, xscale, zscale, filter_by_runtime_conditions) style = chooser.updateStyles(style, tags, xscale, zscale, filter_by_runtime_conditions)
style = [x for x in style if x["object-id"] != "::*"] style = [x for x in style if x["object-id"] != "::*"]
for x in style: for x in style:
for k, v in [('width', 0), ('casing-width', 0)]: for k, v in [('width', 0), ('casing-width', 0)]:
@ -164,6 +189,12 @@ class MapCSS():
style = st style = st
return style return style
def get_colors(self):
colors = self.choosers_by_type.get("colors")
if colors is not None:
return colors[0].styles[0]
return None
def get_style_dict(self, clname, type, tags={}, zoom=0, xscale=1, zscale=.5, olddict={}, filter_by_runtime_conditions=None): def get_style_dict(self, clname, type, tags={}, zoom=0, xscale=1, zscale=.5, olddict={}, filter_by_runtime_conditions=None):
""" """
Kothic styling API Kothic styling API
@ -184,11 +215,13 @@ class MapCSS():
def get_variable(self, m): def get_variable(self, m):
name = m.group()[1:] name = m.group()[1:]
if name in self.unused_variables:
self.unused_variables.remove(name)
if not name in self.variables: if not name in self.variables:
raise Exception("Variable not found: " + str(format(name))) raise Exception("Variable not found: " + str(format(name)))
return self.variables[name] if name in self.variables else m.group() return self.variables[name] if name in self.variables else m.group()
def parse(self, css=None, clamp=True, stretch=1000, filename=None, static_tags=set(), dynamic_tags=set()): def parse(self, css=None, clamp=True, stretch=1000, filename=None, static_tags={}, dynamic_tags=set()):
""" """
Parses MapCSS given as string Parses MapCSS given as string
""" """
@ -196,7 +229,8 @@ class MapCSS():
if filename: if filename:
basepath = os.path.dirname(filename) basepath = os.path.dirname(filename)
if not css: if not css:
css = open(filename).read() with open(filename) as css_file:
css = css_file.read()
if not self.style_loaded: if not self.style_loaded:
self.choosers = [] self.choosers = []
@ -248,6 +282,7 @@ class MapCSS():
elif GROUP.match(css): elif GROUP.match(css):
css = GROUP.sub("", css, 1) css = GROUP.sub("", css, 1)
sc.newGroup() sc.newGroup()
had_main_tag = False
previous = oGROUP previous = oGROUP
# Condition - [highway=primary] or [population>1000] # Condition - [highway=primary] or [population>1000]
@ -255,14 +290,26 @@ class MapCSS():
if (previous == oDECLARATION): if (previous == oDECLARATION):
self.choosers.append(sc) self.choosers.append(sc)
sc = StyleChooser(self.scalepair) sc = StyleChooser(self.scalepair)
had_main_tag = False
if (previous != oOBJECT) and (previous != oZOOM) and (previous != oCONDITION): if (previous != oOBJECT) and (previous != oZOOM) and (previous != oCONDITION):
sc.newObject() sc.newObject()
had_main_tag = False
cond = CONDITION.match(css).groups()[0] cond = CONDITION.match(css).groups()[0]
log.debug("condition found: %s" % (cond))
c = parseCondition(cond) c = parseCondition(cond)
tag = c.extract_tag() tag = c.extract_tag()
if tag == "*" or tag in static_tags: tag_type = static_tags.get(tag, None)
sc.addCondition(c) if tag == "*" or tag_type is not None:
if tag_type and had_main_tag:
if '!' in cond:
condType = 'ne'
cond = cond.replace('!', '')
else:
condType = 'eq'
sc.addRuntimeCondition(Condition(condType, ('extra_tag', cond)))
else:
sc.addCondition(c)
if tag_type:
had_main_tag = True
elif tag in dynamic_tags: elif tag in dynamic_tags:
sc.addRuntimeCondition(c) sc.addRuntimeCondition(c)
else: else:
@ -279,6 +326,7 @@ class MapCSS():
log.debug("object found: %s" % (obj)) log.debug("object found: %s" % (obj))
css = OBJECT.sub("", css, 1) css = OBJECT.sub("", css, 1)
sc.newObject(obj) sc.newObject(obj)
had_main_tag = False
previous = oOBJECT previous = oOBJECT
# Declaration - {...} # Declaration - {...}
@ -302,7 +350,8 @@ class MapCSS():
import_filename = os.path.join(basepath, IMPORT.match(css).groups()[0]) import_filename = os.path.join(basepath, IMPORT.match(css).groups()[0])
try: try:
css = IMPORT.sub("", css, 1) 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[-1][1] = css # store remained part
stck.append([import_filename, import_text, import_text]) stck.append([import_filename, import_text, import_text])
wasBroken = True wasBroken = True
@ -315,6 +364,7 @@ class MapCSS():
name = VARIABLE_SET.match(css).groups()[0] name = VARIABLE_SET.match(css).groups()[0]
log.debug("variable set found: %s" % name) log.debug("variable set found: %s" % name)
self.variables[name] = VARIABLE_SET.match(css).groups()[1] self.variables[name] = VARIABLE_SET.match(css).groups()[1]
self.unused_variables.add( name )
css = VARIABLE_SET.sub("", css, 1) css = VARIABLE_SET.sub("", css, 1)
previous = oVARIABLE_SET previous = oVARIABLE_SET
@ -322,7 +372,7 @@ class MapCSS():
elif UNKNOWN.match(css): elif UNKNOWN.match(css):
raise Exception("Unknown construction: " + UNKNOWN.match(css).group()) raise Exception("Unknown construction: " + UNKNOWN.match(css).group())
# Must be unreacheable # Must be unreachable
else: else:
raise Exception("Unexpected construction: " + css) raise Exception("Unexpected construction: " + css)
@ -340,10 +390,13 @@ class MapCSS():
css_orig = stck[-1][2] # original css_orig = stck[-1][2] # original
css = stck[-1][1] # remained css = stck[-1][1] # remained
line = css_orig[:-len(css)].count("\n") + 1 line = css_orig[:-len(css)].count("\n") + 1
# TODO: Handle filename is None
msg = str(e) + "\nFile: " + filename + "\nLine: " + str(line) msg = str(e) + "\nFile: " + filename + "\nLine: " + str(line)
# TODO: Print stack trace of original exception `e`
raise Exception(msg) raise Exception(msg)
try: try:
# TODO: Drop support of z-index because `clamp` is always False and z-index properties unused in Organic Maps)
if clamp: if clamp:
"clamp z-indexes, so they're tightly following integers" "clamp z-indexes, so they're tightly following integers"
zindex = set() zindex = set()
@ -352,18 +405,19 @@ class MapCSS():
zindex.add(float(stylez.get('z-index', 0))) zindex.add(float(stylez.get('z-index', 0)))
zindex = list(zindex) zindex = list(zindex)
zindex.sort() zindex.sort()
zoffset = len([x for x in zindex if x < 0])
for chooser in self.choosers: for chooser in self.choosers:
for stylez in chooser.styles: for stylez in chooser.styles:
if 'z-index' in stylez: 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: if stretch:
stylez['z-index'] = 1. * res / len(zindex) * stretch stylez['z-index'] = stretch * res / len(zindex)
else: else:
stylez['z-index'] = res stylez['z-index'] = res
except TypeError: except TypeError:
# TODO: Better error handling here
pass pass
# Group MapCSS styles by object type: 'area', 'line', 'way', 'node'
for chooser in self.choosers: for chooser in self.choosers:
for t in chooser.compatible_types: for t in chooser.compatible_types:
if t not in self.choosers_by_type: if t not in self.choosers_by_type:
@ -371,7 +425,11 @@ class MapCSS():
else: else:
self.choosers_by_type[t].append(chooser) 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): def parseCondition(s):
log = logging.getLogger('mapcss.parser.condition') log = logging.getLogger('mapcss.parser.condition')
@ -452,7 +510,7 @@ def parseDeclaration(s):
logging.debug("%s == %s" % (tzz[0], tzz[1])) logging.debug("%s == %s" % (tzz[0], tzz[1]))
else: else:
logging.debug("unknown %s" % (a)) logging.debug("unknown %s" % (a))
return [t] return [t] # TODO: don't wrap `t` dict into a list. Return `t` instead.
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -198,7 +198,7 @@ def _reversedict(d):
dictionary, returns a new dictionary with keys and values swapped. dictionary, returns a new dictionary with keys and values swapped.
""" """
return dict(zip(d.values(), d.keys())) return dict(list(zip(list(d.values()), list(d.keys()))))
HEX_COLOR_RE = re.compile(r'^#([a-fA-F0-9]|[a-fA-F0-9]{3}|[a-fA-F0-9]{6})$') HEX_COLOR_RE = re.compile(r'^#([a-fA-F0-9]|[a-fA-F0-9]{3}|[a-fA-F0-9]{6})$')
@ -454,7 +454,7 @@ def normalize_hex(hex_value):
except AttributeError: except AttributeError:
raise ValueError("'%s' is not a valid hexadecimal color value." % hex_value) raise ValueError("'%s' is not a valid hexadecimal color value." % hex_value)
if len(hex_digits) == 3: if len(hex_digits) == 3:
hex_digits = ''.join(map(lambda s: 2 * s, hex_digits)) hex_digits = ''.join([2 * s for s in hex_digits])
elif len(hex_digits) == 1: elif len(hex_digits) == 1:
hex_digits = hex_digits * 6 hex_digits = hex_digits * 6
return '#%s' % hex_digits.lower() return '#%s' % hex_digits.lower()
@ -648,8 +648,7 @@ def hex_to_rgb(hex_value):
""" """
hex_digits = normalize_hex(hex_value) hex_digits = normalize_hex(hex_value)
return tuple(map(lambda s: int(s, 16), return tuple([int(s, 16) for s in (hex_digits[1:3], hex_digits[3:5], hex_digits[5:7])])
(hex_digits[1:3], hex_digits[3:5], hex_digits[5:7])))
def hex_to_rgb_percent(hex_value): def hex_to_rgb_percent(hex_value):
@ -716,7 +715,7 @@ def rgb_to_hex(rgb_triplet):
'#2138c0' '#2138c0'
""" """
return '#%02x%02x%02x' % rgb_triplet return '#%02x%02x%02x' % (int(rgb_triplet[0]), int(rgb_triplet[1]), int(rgb_triplet[2]))
def rgb_to_rgb_percent(rgb_triplet): def rgb_to_rgb_percent(rgb_triplet):
@ -750,8 +749,7 @@ def rgb_to_rgb_percent(rgb_triplet):
# from 0 through 4, as well as 0 itself. # from 0 through 4, as well as 0 itself.
specials = {255: '100%', 128: '50%', 64: '25%', specials = {255: '100%', 128: '50%', 64: '25%',
32: '12.5%', 16: '6.25%', 0: '0%'} 32: '12.5%', 16: '6.25%', 0: '0%'}
return tuple(map(lambda d: specials.get(d, '%.02f%%' % ((d / 255.0) * 100)), return tuple([specials.get(d, '%.02f%%' % ((d / 255.0) * 100)) for d in rgb_triplet])
rgb_triplet))
###################################################################### ######################################################################

0
tests/__init__.py Normal file
View file

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,136 @@
/* ~~~~ CONTENT OF ROADS ~~~~~
1.Z-INDEX ROADS
2.WORLD LEVEL ROAD 4-9 ZOOM
3.TRUNK & MOTORWAY 6-22 ZOOM
3.1 Trunk & Motorway 6-22 ZOOM
3.2 Trunk & Motorway tunnel 12-22 ZOOM
3.3 Trunk & Motorway bridge 13-22 ZOOM
4.PRIMARY 8-22 ZOOM
4.1 Primary 8-22 ZOOM
4.2 Primary tunnel 14-22 ZOOM
4.3 Primary bridge 14-22 ZOOM
5.SECONDARY 10-22 ZOOM
5.1 Secondary 10-22 ZOOM
5.2 Secondary tunnel 16-22 ZOOM
5.3 Secondary bridge 14-22 ZOOM
6.TERTIARY & UNCLASSIFIED 11-22 ZOOM
6.1 Tertiary & Unclassified 11-22 ZOOM
6.2 Tertiary & Unclassified tunnel 16-22 ZOOM
6.3 Tertiary & Unclassified bridge 14-22 ZOOM
7.RESIDENTAL, ROAD, STREETS & SERVICE 12-22 ZOOM
7.1 Residential, Road, Street 12-22 ZOOM
7.2 Residential, Road, Street tunnel 16-22 ZOOM
7.3 Residential, Road, Street bridge 14-22 ZOOM
7.4 Service 15-22 ZOOM
8.OTHERS ROADS 13-22 ZOOM
8.1 Pedestrian & ford 13-22 ZOOM
8.2 Pedestrian & ford tunnel 16-22 ZOOM
8.3 Pedestrian & other brige 13-22 ZOOM
8.4 Cycleway 13-22 ZOOM
8.5 Construction 13-22 ZOOM
8.6 Track & Path 14-22 ZOOM
8.7 Footway 15-22 ZOOM
8.8 Steps 15-22 ZOOM
8.9 Bridleway 14-22 ZOOM
8.11 Runway 12-22 ZOOM
9.RAIL 11-22 ZOOM
9.1 RAIL 11-22 ZOOM
9.2 Rail tunnel 14-22 ZOOM
9.3 Rail bridge 14-22 ZOOM
9.4 Monorail 14-22 ZOOM
9.5 Tram line 13-22 ZOOM
9.6 Funicular 12-22 ZOOM
10.PISTE 12-22 ZOOM
10.1 Lift 12-22 ZOOM
10.2 Aerialway 12-22 ZOOM
10.3 Piste & Route 14-22 ZOOM
11.FERRY
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
/* 2.WORLD LEVEL ROAD 4-9 ZOOM */
line|z6-9[highway=world_towns_level],
line|z4-9[highway=world_level],
{color: @trunk1;opacity: 1;}
line|z4[highway=world_level]
{width: 0.5;}
line|z5-6[highway=world_level],
{width: 0.7;}
line|z6[highway=world_towns_level],
{width: 0.9;}
line|z7[highway=world_towns_level],
line|z7[highway=world_level]
{width: 0.7;}
line|z8[highway=world_towns_level],
line|z8[highway=world_level]
{width: 0.9;}
line|z9[highway=world_towns_level],
line|z9[highway=world_level]
{width: 0.8;}
/* 3.TRUNK & MOTORWAY 6-22 ZOOM */
line|z6[highway=trunk],
line|z6[highway=motorway],
{color: @trunk0; opacity: 0.3;}
line|z7-9[highway=trunk],
line|z7-9[highway=motorway],
{color: @trunk0; opacity: 0.7;}
line|z10-[highway=trunk],
line|z10-[highway=motorway],
{color: @trunk1; opacity: 0.7;}
line|z10-[highway=motorway_link],
line|z10-[highway=trunk_link],
{color: @primary0; opacity: 0.7;}
/* 3.1 Trunk & Motorway 6-22 ZOOM */
line|z6[highway=trunk],
line|z6[highway=motorway],
{width: 0.8;}
line|z7[highway=trunk],
line|z7[highway=motorway]
{width: 0.9;}
line|z8[highway=trunk],
line|z8[highway=motorway]
{width: 1.1;}
line|z9[highway=trunk],
line|z9[highway=motorway]
{width: 1.2;}
line|z10[highway=trunk],
line|z10[highway=motorway]
{width: 1.5;}
line|z10[highway=motorway_link],
line|z10[highway=trunk_link]
{width: 0.8;}
/* 4.PRIMARY 8-22 ZOOM */
line|z8-10[highway=primary],
{color: @primary0; opacity: 0.7;}
/* 4.1 Primary 8-22 ZOOM */
line|z8[highway=primary],
{width: 0.7;}
line|z9[highway=primary],
{width: 0.8;}
line|z10[highway=primary],
{width: 1.2;}
/* 5.SECONDARY 10-22 ZOOM */
line|z10[highway=secondary],
{color: @secondary0; opacity: 0.8;}
/* 5.1 Secondary 10-22 ZOOM */
line|z10[highway=secondary],
{width: 1.2;}

View file

@ -0,0 +1,75 @@
/* ~~~~ CONTENT OF ROADS ~~~~~
1.Z-INDEX ROADS
2.SHIELD 10-22 ZOOM
3.TRUNK & MOTORWAY 10-22 ZOOM
4.PRIMARY 10-22 ZOOM
5.SECONDARY 10-22 ZOOM
6.RESIDENTAL & TERTIARY 12-22 ZOOM
7.ROAD, STREETS, UNCLASSIFIED & SERVICE 15-22 ZOOM
8.OTHERS ROADS 15-22 ZOOM
9.RAIL 15-22 ZOOM ????
9.1 Monorail 14-22 ZOOM
9.2 Tram line 13-22 ZOOM
9.3 Funicular 12-22 ZOOM
10.PISTE 12-22 ZOOM ????
10.1 Lift 12-22 ZOOM
10.2 Aerialway 12-22 ZOOM
10.3 Piste & Route 14-22 ZOOM
11.FERRY 10-22 ZOOM
12.ONEWAY ARROWS 15-22 ZOOM
13.JUNCTION 15-22 ZOOM
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*/
line[highway]
{text-position: line;}
/* 2.SHIELD 10-22 ZOOM */
line|z10-[highway=motorway]::shield,
line|z10-[highway=trunk]::shield,
line|z10-[highway=motorway_link]::shield,
line|z10-[highway=trunk_link]::shield,
line|z10-[highway=primary]::shield,
{shield-font-size: 9;shield-text-color: @shield_text;shield-text-halo-radius: 0;shield-text-halo-color: @shield_text_halo;shield-color: @shield;shield-outline-radius: 1;shield-outline-color: @shield_outline;}
line|z10[highway=motorway]::shield,
line|z10[highway=trunk]::shield,
line|z10[highway=motorway_link]::shield,
line|z10[highway=trunk_link]::shield,
line|z10[highway=primary]::shield,
{shield-min-distance: 85;}
/* 3.TRUNK & MOTORWAY 10-22 ZOOM */
line|z10-[highway=trunk],
line|z10-[highway=motorway],
line|z10-[highway=motorway_link],
line|z10-[highway=trunk_link],
{text: name; text-halo-radius: 1; text-halo-color: @label_halo_medium;}
line|z10-[highway=motorway],
line|z10-[highway=trunk],
{font-size: 11; text-color: @label_medium; text-halo-opacity: 0.9;}
line|z10-[highway=motorway_link],
line|z10-[highway=trunk_link],
{font-size: 10; text-color: @label_medium; text-halo-opacity: 0.7;}
/* 4.PRIMARY 10-22 ZOOM */
line|z10-[highway=primary],
{text: name; text-halo-radius: 1; text-halo-color: @label_halo_medium;}
line|z10-[highway=primary],
{font-size: 10; text-color: @label_medium; text-halo-opacity: 0.7;}
/* 5.SECONDARY 10-22 ZOOM */
line|z10-[highway=secondary],
{text: name; text-halo-radius: 1; text-halo-color: @label_halo_medium;}
line|z10-[highway=secondary],
{font-size: 10; text-color: @label_light; text-halo-opacity: 0.7;}

View file

@ -0,0 +1,16 @@
/* 5.1 All roads */
@trunk0: #FF7326;
@trunk1: #FF7A26;
@primary0: #FF8726;
@secondary0: #FFB226;
/* 6.1 Main labels */
@label_medium: #333333;
@label_light: #444444;
@label_halo_medium: #EDEBDB;
/* 6.4 Road labels */
@shield_text: #000000;
@shield_text_halo: #000000;
@shield: #FFFFFF;
@shield_outline: #000000;

View file

@ -0,0 +1,16 @@
# This file is automatically re-formatted and re-sorted in priorities descending order
# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)
# are generated automatically for information only. Custom formatting and comments are not preserved.
#
# BG-by-size geometry: background areas rendered below BG-top and everything else.
# Smaller areas are rendered above larger ones (area's size is estimated as the size of its' bounding box).
# So effectively priority values of BG-by-size areas are not used at the moment.
# But we might use them later for some special cases, e.g. to determine a main area type of a multi-type feature.
# Keep them in a logical importance order please.
#
# Priorities ranges' rendering order overview:
# - overlays (icons, captions...)
# - FG: foreground areas and lines
# - BG-top: water (linear and areal)
# - BG-by-size: landcover areas sorted by their size

View file

@ -0,0 +1,18 @@
# This file is automatically re-formatted and re-sorted in priorities descending order
# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)
# are generated automatically for information only. Custom formatting and comments are not preserved.
#
# BG-top geometry: background lines and areas that should be always below foreground ones
# (including e.g. layer=-10 underwater tunnels), but above background areas sorted by size (BG-by-size),
# because ordering by size doesn't always work with e.g. water mapped over a forest,
# so water should be on top of other landcover always, but linear waterways should be hidden beneath it.
# Still, e.g. a layer=-1 BG-top feature will be rendered under a layer=0 BG-by-size feature
# (so areal water tunnels are hidden beneath other landcover area) and a layer=1 landcover areas
# are displayed above layer=0 BG-top.
#
# Priorities ranges' rendering order overview:
# - overlays (icons, captions...)
# - FG: foreground areas and lines
# - BG-top: water (linear and areal)
# - BG-by-size: landcover areas sorted by their size

View file

@ -0,0 +1,41 @@
# This file is automatically re-formatted and re-sorted in priorities descending order
# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)
# are generated automatically for information only. Custom formatting and comments are not preserved.
#
# FG geometry: foreground lines and areas (e.g. buildings) are rendered always below overlays
# and always on top of background geometry (BG-top & BG-by-size) even if a foreground feature
# is layer=-10 (as tunnels should be visibile over landcover and water).
#
# Priorities ranges' rendering order overview:
# - overlays (icons, captions...)
# - FG: foreground areas and lines
# - BG-top: water (linear and areal)
# - BG-by-size: landcover areas sorted by their size
highway-motorway # line z6- (also has pathtext z10-, shield::shield z10-)
highway-motorway-bridge # line z6- (also has pathtext z10-, shield::shield z10-)
highway-motorway-tunnel # line z6- (also has pathtext z10-, shield::shield z10-)
highway-trunk # line z6- (also has pathtext z10-, shield::shield z10-)
highway-trunk-bridge # line z6- (also has pathtext z10-, shield::shield z10-)
highway-trunk-tunnel # line z6- (also has pathtext z10-, shield::shield z10-)
highway-world_level # line z4-9
highway-world_towns_level # line z6-9
=== 310
highway-primary # line z8- (also has pathtext z10-, shield::shield z10-)
highway-primary-bridge # line z8- (also has pathtext z10-, shield::shield z10-)
highway-primary-tunnel # line z8- (also has pathtext z10-, shield::shield z10-)
=== 290
highway-secondary # line z10- (also has pathtext z10-)
highway-secondary-bridge # line z10- (also has pathtext z10-)
highway-secondary-tunnel # line z10- (also has pathtext z10-)
=== 270
highway-motorway_link # line z10- (also has pathtext z10-, shield::shield z10-)
highway-motorway_link-bridge # line z10- (also has pathtext z10-, shield::shield z10-)
highway-motorway_link-tunnel # line z10- (also has pathtext z10-, shield::shield z10-)
highway-trunk_link # line z10- (also has pathtext z10-, shield::shield z10-)
highway-trunk_link-bridge # line z10- (also has pathtext z10-, shield::shield z10-)
highway-trunk_link-tunnel # line z10- (also has pathtext z10-, shield::shield z10-)
=== 228

View file

@ -0,0 +1,61 @@
# This file is automatically re-formatted and re-sorted in priorities descending order
# when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)
# are generated automatically for information only. Custom formatting and comments are not preserved.
#
# Overlays (icons, captions, path texts and shields) are rendered on top of all the geometry (lines, areas).
# Overlays don't overlap each other, instead the ones with higher priority displace the less important ones.
# Optional captions (which have an icon) are usually displayed only if there are no other overlays in their way
# (technically, max overlays priority value (10000) is subtracted from their priorities automatically).
#
# Priorities ranges' rendering order overview:
# - overlays (icons, captions...)
# - FG: foreground areas and lines
# - BG-top: water (linear and areal)
# - BG-by-size: landcover areas sorted by their size
highway-motorway # pathtext z10- (also has shield::shield z10-, line z6-)
highway-motorway-bridge # pathtext z10- (also has shield::shield z10-, line z6-)
highway-motorway-tunnel # pathtext z10- (also has shield::shield z10-, line z6-)
highway-trunk # pathtext z10- (also has shield::shield z10-, line z6-)
highway-trunk-bridge # pathtext z10- (also has shield::shield z10-, line z6-)
highway-trunk-tunnel # pathtext z10- (also has shield::shield z10-, line z6-)
=== 6750
highway-motorway::shield # shield::shield z10- (also has pathtext z10-, line z6-)
highway-motorway-bridge::shield # shield::shield z10- (also has pathtext z10-, line z6-)
highway-motorway-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z6-)
highway-trunk::shield # shield::shield z10- (also has pathtext z10-, line z6-)
highway-trunk-bridge::shield # shield::shield z10- (also has pathtext z10-, line z6-)
highway-trunk-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z6-)
=== 6740
highway-primary # pathtext z10- (also has shield::shield z10-, line z8-)
highway-primary-bridge # pathtext z10- (also has shield::shield z10-, line z8-)
highway-primary-tunnel # pathtext z10- (also has shield::shield z10-, line z8-)
=== 6200
highway-motorway_link # pathtext z10- (also has shield::shield z10-, line z10-)
highway-motorway_link-bridge # pathtext z10- (also has shield::shield z10-, line z10-)
highway-motorway_link-tunnel # pathtext z10- (also has shield::shield z10-, line z10-)
highway-trunk_link # pathtext z10- (also has shield::shield z10-, line z10-)
highway-trunk_link-bridge # pathtext z10- (also has shield::shield z10-, line z10-)
highway-trunk_link-tunnel # pathtext z10- (also has shield::shield z10-, line z10-)
=== 6150
highway-motorway_link::shield # shield::shield z10- (also has pathtext z10-, line z10-)
highway-motorway_link-bridge::shield # shield::shield z10- (also has pathtext z10-, line z10-)
highway-motorway_link-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z10-)
highway-trunk_link::shield # shield::shield z10- (also has pathtext z10-, line z10-)
highway-trunk_link-bridge::shield # shield::shield z10- (also has pathtext z10-, line z10-)
highway-trunk_link-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z10-)
=== 6140
highway-secondary # pathtext z10- (also has line z10-)
highway-secondary-bridge # pathtext z10- (also has line z10-)
highway-secondary-tunnel # pathtext z10- (also has line z10-)
=== 5600
highway-primary::shield # shield::shield z10- (also has pathtext z10-, line z8-)
highway-primary-bridge::shield # shield::shield z10- (also has pathtext z10-, line z8-)
highway-primary-tunnel::shield # shield::shield z10- (also has pathtext z10-, line z8-)
=== 2975

View file

@ -0,0 +1,133 @@
*::int_name
{
text-offset: 1;
}
@import("include/colors.mapcss");
@import("include/Roads.mapcss");
@import("include/Roads_label.mapcss");
colors
{
GuiText-color: #4D4D4D;
GuiText-opacity: 0.86;
MyPositionAccuracy-color: #000000;
MyPositionAccuracy-opacity: 0.08;
Selection-color: #1E96F0;
Selection-opacity: 0.64;
Route-color: #0087FF;
RouteOutline-color: #055FCD;
RouteTrafficG0-color: #9B2300;
RouteTrafficG1-color: #E82705;
RouteTrafficG2-color: #E82705;
RouteTrafficG3-color: #FFE500;
RouteTrafficG3-opacity: 0.0;
RoutePedestrian-color: #1D339E;
RoutePedestrian-opacity: 0.8;
RouteBicycle-color: #9C27B0;
RouteBicycle-opacity: 0.8;
RouteRuler-color: #66347F;
RouteRuler-opacity: 0.9;
RoutePreview-color: #000000;
RoutePreview-opacity: 0.3;
RouteMaskCar-color: #000000;
RouteMaskCar-opacity: 0.3;
RouteFirstSegmentArrowsMaskCar-color: #033B80;
RouteFirstSegmentArrowsMaskCar-opacity: 0.0;
RouteArrowsMaskCar-color: #033B80;
RouteArrowsMaskCar-opacity: 0.2;
RouteMaskBicycle-color: #000000;
RouteMaskBicycle-opacity: 0.5;
RouteFirstSegmentArrowsMaskBicycle-color: #9C27B0;
RouteFirstSegmentArrowsMaskBicycle-opacity: 0.0;
RouteArrowsMaskBicycle-color: #9C27B0;
RouteArrowsMaskBicycle-opacity: 0.2;
RouteMaskPedestrian-color: #000000;
RouteMaskPedestrian-opacity: 0.5;
RouteFake-color: #A8A8A8;
RouteFakeOutline-color: #717171;
Arrow3D-color: #50AAFF;
Arrow3DObsolete-color: #82AAC8;
Arrow3DObsolete-opacity: 0.72;
Arrow3DShadow-color: #3C3C3C;
Arrow3DShadow-opacity: 0.24;
Arrow3DOutline-color: #FFFFFF;
TrackHumanSpeed-color: #1D339E;
TrackCarSpeed-color: #7C8EDE;
TrackPlaneSpeed-color: #A8B7ED;
TrackUnknownDistance-color: #616161;
TrafficG0-color: #7E1712;
TrafficG1-color: #E42300;
TrafficG2-color: #E42300;
TrafficG3-color: #FCDE00;
TrafficG3-opacity: 0.0;
TrafficG4-color: #39962E;
TrafficG5-color: #39962E;
TrafficTempBlock-color: #525252;
TrafficUnknown-color: #000000;
TrafficArrowLight-color: #FFFFFF;
TrafficArrowDark-color: #473635;
TrafficOutline-color: #E8E6DC;
RoadShieldBlackText-color: #000000;
RoadShieldWhiteText-color: #FFFFFF;
RoadShieldUKYellowText-color: #FFD400;
RoadShieldBlueBackground-color: #1A5EC1;
RoadShieldGreenBackground-color: #309302;
RoadShieldRedBackground-color: #E63534;
RoadShieldOrangeBackground-color: #FFBE00;
PoiHotelTextOutline-color: #FFFFFF;
PoiHotelTextOutline-opacity: 0.6;
PoiDeletedMask-color: #FFFFFF;
PoiDeletedMask-opacity: 0.3;
PoiVisitedMask-color: #FFFFFF;
PoiVisitedMask-opacity: 0.7;
DefaultTrackColor-color: #1E96F0;
RouteMarkPrimaryText-color: #000000;
RouteMarkPrimaryTextOutline-color: #FFFFFF;
RouteMarkSecondaryText-color: #000000;
RouteMarkSecondaryTextOutline-color: #FFFFFF;
TransitMarkPrimaryText-color: #000000;
TransitMarkPrimaryTextOutline-color: #FFFFFF;
TransitMarkSecondaryText-color: #000000;
TransitMarkSecondaryTextOutline-color: #FFFFFF;
TransitTransferOuterMarker-color: #000000;
TransitTransferInnerMarker-color: #FFFFFF;
TransitStopInnerMarker-color: #FFFFFF;
LocalAdsPrimaryText-color: #000000;
LocalAdsPrimaryTextOutline-color: #FFFFFF;
LocalAdsSecondaryText-color: #000000;
LocalAdsSecondaryTextOutline-color: #FFFFFF;
TransitBackground-color: #FFFFFF;
TransitBackground-opacity: 0.4;
BookmarkRed-color: #E51B23;
BookmarkPink-color: #FF4182;
BookmarkPurple-color: #9B24B2;
BookmarkDeepPurple-color: #6639BF;
BookmarkBlue-color: #0066CC;
BookmarkLightBlue-color: #249CF2;
BookmarkCyan-color: #14BECD;
BookmarkTeal-color: #00A58C;
BookmarkGreen-color: #3C8C3C;
BookmarkLime-color: #93BF39;
BookmarkYellow-color: #FFC800;
BookmarkOrange-color: #FF9600;
BookmarkDeepOrange-color: #F06432;
BookmarkBrown-color: #804633;
BookmarkGray-color: #737373;
BookmarkBlueGray-color: #597380;
SearchmarkPreparing-color: #597380;
SearchmarkNotAvailable-color: #597380;
SearchmarkSelectedNotAvailable-color: #F06432;
RatingBad-color: #F06432;
RatingGood-color: #3C8C3C;
RatingNone-color: #249CF2;
SearchmarkDefault-color: #249CF2;
RatingText-color: #FFFFFF;
UGCRatingText-color: #000000;
SpeedCameraMarkText-color: #FFFFFF;
SpeedCameraMarkBg-color: #F51E30;
SpeedCameraMarkOutline-color: #FFFFFF;
GuideCityMarkText-color: #6639BF;
GuideOutdoorMarkText-color: #3C8C3C;
HotelPriceText-color: #000000;
}

View file

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

View file

@ -0,0 +1,148 @@
highway|residential;2;
highway|service;3;
highway|unclassified;5;
highway|footway;7;
highway|track;8;
highway|tertiary;9;
highway|secondary;13;
highway|path;16;
highway|bus_stop;17;
highway|footway|sidewalk;[highway=footway][footway=sidewalk];;name;int_name;18;
highway|primary;27;
highway|service|parking_aisle;[highway=service][service=parking_aisle];;name;int_name;29;
moved:highway|road:05.2024;31;highway|road
deprecated:highway|track|grade2:04.2024;[highway=track][tracktype=grade2];x;name;int_name;32;highway|track
deprecated:highway|track|grade3:04.4024;[highway=track][tracktype=grade3];x;name;int_name;34;highway|track
highway|cycleway;37;
deprecated:highway|track|grade1:04.2024;[highway=track][tracktype=grade1];x;name;int_name;40;highway|track
highway|service|driveway;[highway=service][service=driveway];;name;int_name;42;
highway|motorway_link;44;
deprecated:highway|track|grade4:04.2024;[highway=track][tracktype=grade4];x;name;int_name;46;highway|track
highway|footway|crossing;[highway=footway][footway=crossing];;name;int_name;51;
highway|path|bicycle;[highway=path][bicycle=designated];;;;53;
highway|living_street;55;
highway|motorway;58;
highway|steps;59;
deprecated:highway|track|grade5:04.2024;[highway=track][tracktype=grade5];x;name;int_name;63;highway|track
highway|trunk;66;
highway|pedestrian;70;
highway|motorway|bridge;[highway=motorway][bridge?];;name;int_name;72;
highway|residential|bridge;[highway=residential][bridge?];;name;int_name;81;
highway|secondary|bridge;[highway=secondary][bridge?];;name;int_name;85;
highway|tertiary|bridge;[highway=tertiary][bridge?];;name;int_name;86;
highway|trunk_link;91;
highway|unclassified|bridge;[highway=unclassified][bridge?];;name;int_name;92;
highway|primary|bridge;[highway=primary][bridge?];;name;int_name;95;
highway|primary_link;96;
highway|footway|bridge;[highway=footway][bridge?];;name;int_name;98;
deprecated:highway|path|hiking:04.2024;[highway=path][route=hiking],[highway=path][sac_scale=hiking];x;name;int_name;113;highway|path
highway|trunk|bridge;[highway=trunk][bridge?];;name;int_name;116;
highway|motorway_junction;121;
highway|footway|bicycle;[highway=footway][bicycle=designated];;;;141;
highway|motorway_link|bridge;[highway=motorway_link][bridge?];;name;int_name;143;
deprecated:highway|footway|permissive:12.2023;[highway=footway][access=permissive],[highway=footway][foot=permissive];x;name;int_name;153;highway|footway
highway|pedestrian|area;[highway=pedestrian][area?];;name;int_name;158;
highway|construction;163;
highway|cycleway|bridge;[highway=cycleway][bridge?];;name;int_name;164;
deprecated:highway|path|mountain_hiking:04.2024;[highway=path][sac_scale=mountain_hiking];x;name;int_name;166;highway|path
highway|bridleway;168;
highway|secondary_link;177;
highway|footway|tunnel;[highway=footway][tunnel?],[highway=footway][location=underground];;name;int_name;183;
highway|track|bridge;[highway=track][bridge?];;name;int_name;193;
highway|path|bridge;[highway=path][bridge?];;name;int_name;194;
highway|service|bridge;[highway=service][bridge?];;name;int_name;203;
highway|service|area;[highway=service][area?];;name;int_name;226;
highway|residential|area;[highway=residential][area?];;name;int_name;227;
deprecated:highway|track|permissive:12.2023;[highway=track][access=permissive];x;name;int_name;229;highway|track
highway|cycleway|tunnel;[highway=cycleway][tunnel?];;name;int_name;232;
highway|unclassified|tunnel;[highway=unclassified][tunnel?];;name;int_name;235;
highway|residential|tunnel;[highway=residential][tunnel?];;name;int_name;238;
deprecated:highway|path|permissive:12.2023;[highway=path][access=permissive];x;name;int_name;240;highway|path
highway|trunk_link|bridge;[highway=trunk_link][bridge?];;name;int_name;261;
highway|service|tunnel;[highway=service][tunnel?];;name;int_name;263;
highway|tertiary|tunnel;[highway=tertiary][tunnel?];;name;int_name;269;
highway|tertiary_link;273;
highway|footway|area;[highway=footway][area?];;name;int_name;276;
highway|road|bridge;[highway=road][bridge?];;name;int_name;280;
highway|secondary|tunnel;[highway=secondary][tunnel?];;name;int_name;297;
deprecated:highway|path|demanding_mountain_hiking:04.2024;[highway=path][sac_scale=demanding_mountain_hiking];x;name;int_name;300;highway|path|difficult
highway|pedestrian|bridge;[highway=pedestrian][bridge?];;name;int_name;304;
highway|raceway;308;
highway|primary|tunnel;[highway=primary][tunnel?];;name;int_name;309;
highway|primary_link|bridge;[highway=primary_link][bridge?];;name;int_name;310;
deprecated:highway|footway|hiking:04.2024;[highway=footway][sac_scale=hiking];x;name;int_name;314;highway|path
highway|path|horse;[highway=path][horse?];;name;int_name;317;
highway|trunk|tunnel;[highway=trunk][tunnel?];;name;int_name;326;
highway|steps|tunnel;[highway=steps][tunnel?],[highway=steps][location=underground];;name;int_name;327;
highway|steps|bridge;[highway=steps][bridge?];;name;int_name;330;
highway|pedestrian|tunnel;[highway=pedestrian][tunnel?],[highway=pedestrian][location=underground];;name;int_name;332;
highway|path|tunnel;[highway=path][tunnel?],[highway=path][location=underground];;name;int_name;336;
deprecated:highway|path|alpine_hiking:04.2024;[highway=path][sac_scale=alpine_hiking];x;name;int_name;350;highway|path|expert
deprecated:highway|cycleway|permissive:12.2023;[highway=cycleway][access=permissive];x;name;int_name;353;highway|cycleway
highway|unclassified|area;[highway=unclassified][area?];;name;int_name;354;
deprecated:highway|footway|mountain_hiking:04.2024;[highway=footway][sac_scale=mountain_hiking];x;name;int_name;361;highway|path
deprecated:highway|service|driveway|bridge:01.2020;[highway=service][service=driveway][bridge?];x;name;int_name;362;highway|service|driveway
deprecated:highway|bridleway|permissive:12.2023;[highway=bridleway][access=permissive];x;name;int_name;370;highway|bridleway
highway|bridleway|bridge;[highway=bridleway][bridge?];;name;int_name;378;
deprecated:highway|service|driveway|tunnel:01.2020;[highway=service][service=driveway][tunnel?];x;name;int_name;379;highway|service|driveway
deprecated:highway|service|driveway|area:01.2020;[highway=service][service=driveway][area?];x;name;int_name;386;highway|service|driveway
deprecated:highway|path|demanding_alpine_hiking:04.2024;[highway=path][sac_scale=demanding_alpine_hiking];x;name;int_name;395;highway|path|expert
highway|secondary_link|bridge;[highway=secondary_link][bridge?];;name;int_name;397;
area:highway|living_street;401;
highway|living_street|bridge;[highway=living_street][bridge?];;name;int_name;407;
highway|road;411;
highway|motorway|tunnel;[highway=motorway][tunnel?];;name;int_name;416;
area:highway|service;418;
highway|road|tunnel;[highway=road][tunnel?];;name;int_name;423;
highway|ford;427;
area:highway|path;428;
highway|track|area;[highway=track][area?];;name;int_name;430;
deprecated:highway|path|difficult_alpine_hiking:04.2024;[highway=path][sac_scale=difficult_alpine_hiking];x;name;int_name;444;highway|path|expert
deprecated:highway|footway|demanding_mountain_hiking:04.2024;[highway=footway][sac_scale=demanding_mountain_hiking];x;name;int_name;452;highway|path|difficult
highway|living_street|tunnel;[highway=living_street][tunnel?];;name;int_name;457;
highway|path|difficult;[highway=path][_path_grade=difficult];;name;int_name;464;
highway|path|expert;[highway=path][_path_grade=expert];;name;int_name;465;
area:highway|steps;470;
highway|bridleway|tunnel;[highway=bridleway][tunnel?];;name;int_name;488;
highway|motorway_link|tunnel;[highway=motorway_link][tunnel?];;name;int_name;489;
highway|tertiary_link|bridge;[highway=tertiary_link][bridge?];;name;int_name;493;
highway|trunk_link|tunnel;[highway=trunk_link][tunnel?];;name;int_name;503;
highway|primary_link|tunnel;[highway=primary_link][tunnel?];;name;int_name;528;
deprecated:highway|footway|alpine_hiking:04.2024;[highway=footway][sac_scale=alpine_hiking];x;name;int_name;529;highway|path|expert
deprecated:amenity|speed_trap:10.2021;542;highway|speed_camera
area:highway|track;543;
area:highway|primary;544;
deprecated:highway|footway|demanding_alpine_hiking:04.2024;[highway=footway][sac_scale=demanding_alpine_hiking];x;name;int_name;555;highway|path|expert
highway|secondary_link|tunnel;[highway=secondary_link][tunnel?];;name;int_name;578;
highway|track|grade3|permissive;[highway=track][tracktype=grade3][access=permissive];x;name;int_name;591;highway|track
deprecated:highway|footway|difficult_alpine_hiking:04.2024;[highway=footway][sac_scale=difficult_alpine_hiking];x;name;int_name;627;highway|path|expert
highway|track|grade5|permissive;[highway=track][tracktype=grade5][access=permissive];x;name;int_name;631;highway|track
highway|tertiary_link|tunnel;[highway=tertiary_link][tunnel?];;name;int_name;634;
highway|track|grade4|permissive;[highway=track][tracktype=grade4][access=permissive];x;name;int_name;675;highway|track
highway|track|grade3|no-access;[highway=track][tracktype=grade3][access=no];x;name;int_name;821;highway|track
highway|track|grade4|no-access;[highway=track][tracktype=grade4][access=no];x;name;int_name;822;highway|track
highway|track|grade5|no-access;[highway=track][tracktype=grade5][access=no];x;name;int_name;823;highway|track
highway|track|no-access;[highway=track][access=no];;name;int_name;824;
deprecated:highway|service|busway:10.2023;[highway=service][service=busway];x;name;int_name;857;highway|busway
highway|busway;[highway=busway],[highway=service][service=busway],[highway=service][service=bus];;name;int_name;858;
highway|busway|bridge;[highway=busway][bridge?];;name;int_name;859;
highway|busway|tunnel;[highway=busway][tunnel?];;name;int_name;860;
area:highway|footway;866;
area:highway|residential;868;
area:highway|secondary;869;
area:highway|tertiary;870;
area:highway|pedestrian;873;
area:highway|unclassified;874;
area:highway|cycleway;877;
area:highway|motorway;879;
area:highway|trunk;880;
highway|speed_camera;991;
highway|world_level;1052;
highway|world_towns_level;1053;
highway|elevator;1059;
highway|rest_area;1080;
highway|traffic_signals;1081;
hwtag|nobicycle;1114;
hwtag|yesbicycle;1115;
hwtag|bidir_bicycle;1116;
highway|services;1173;
Can't render this file because it has a wrong number of fields in line 38.

View file

@ -0,0 +1,4 @@
Files for testLibkomwm.test_generate_drules_mini() method.
These styles contain only zooms 0-10 and only highway=* rules.
So we can verify generated files content.

View file

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

300
tests/testCondition.py Normal file
View file

@ -0,0 +1,300 @@
import re
import unittest
import sys
from pathlib import Path
# Add `src` directory to the import paths
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
from mapcss import parseCondition
from mapcss.Condition import Condition
class ConditionTest(unittest.TestCase):
def test_parser_eq(self):
cond:Condition = parseCondition("natural=coastline")
self.assertEqual(cond.type, "eq")
self.assertEqual(cond.params, ("natural", "coastline"))
self.assertTrue(cond.test({'natural': 'coastline'}))
self.assertFalse(cond.test({'Natural': 'Coastline'}))
cond = parseCondition(" highway\t=\tprimary")
self.assertEqual(cond.type, "eq")
self.assertEqual(cond.params, ("highway", "primary"))
self.assertTrue(cond.test({'highway': 'primary'}))
self.assertFalse(cond.test({'highway': 'secondary'}))
cond = parseCondition(" admin_level = 3")
self.assertEqual(cond.type, "eq")
self.assertEqual(cond.params, ("admin_level", "3"))
self.assertTrue(cond.test({'admin_level': '3'}))
self.assertFalse(cond.test({'admin_level': '32'}))
cond = Condition('eq', ("::class", "::*"))
self.assertEqual(cond.type, "eq")
self.assertEqual(cond.params, ("::class", "::*"))
self.assertEqual(cond.extract_tag(), "*")
self.assertEqual(cond.test({'any_key': 'any_value'}), "::*")
self.assertTrue(cond.test({'any_key': 'any_value'}))
cond = Condition('eq', ("::class", "::int_name"))
self.assertEqual(cond.type, "eq")
self.assertEqual(cond.params, ("::class", "::int_name"))
self.assertEqual(cond.extract_tag(), "*")
self.assertEqual(cond.test({'any_key': 'any_value'}), "::int_name")
self.assertTrue(cond.test({'any_key': 'any_value'}))
def test_parser_regex(self):
""" Test conditions in format natural =~/water.+/
Note that such conditions are not used by Organic Maps styles.
"""
cond:Condition = parseCondition("natural =~/water.+/")
self.assertEqual(cond.type, "regex")
self.assertEqual(cond.params, ("natural", "water.+"))
self.assertEqual(type(cond.regex), re.Pattern)
self.assertTrue(cond.test({"natural": "waterway"}))
self.assertTrue(cond.test({"natural": "water123"}))
self.assertFalse(cond.test({"natural": "water"}))
self.assertFalse(cond.test({"natural": " waterway "}))
def test_parser_ge(self):
cond:Condition = parseCondition("population>=0")
self.assertEqual(cond.type, ">=")
self.assertEqual(cond.params, ("population", "0"))
self.assertTrue(cond.test({"population": "0"}))
self.assertTrue(cond.test({"population": "100000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"population": "-1"}))
cond:Condition = parseCondition("population >= 150000")
self.assertEqual(cond.type, ">=")
self.assertEqual(cond.params, ("population", "150000"))
self.assertTrue(cond.test({"population": "150000"}))
self.assertTrue(cond.test({"population": "250000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"population": "10000"}))
cond:Condition = parseCondition("\tbbox_area >= 4000000")
self.assertEqual(cond.type, ">=")
self.assertEqual(cond.params, ("bbox_area", "4000000"))
self.assertTrue(cond.test({"bbox_area": "4000000"}))
self.assertTrue(cond.test({"bbox_area": "8000000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"bbox_area": "999"}))
def test_parser_gt(self):
""" Test conditions in format population > 100000
Note that such conditions are not used by Organic Maps styles.
"""
cond:Condition = parseCondition("population>0")
self.assertEqual(cond.type, ">")
self.assertEqual(cond.params, ("population", "0"))
self.assertTrue(cond.test({"population": "100"}))
self.assertFalse(cond.test({"population": "000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"population": "-1"}))
cond:Condition = parseCondition("population > 150000")
self.assertEqual(cond.type, ">")
self.assertEqual(cond.params, ("population", "150000"))
self.assertTrue(cond.test({"population": "250000"}))
self.assertFalse(cond.test({"population": "150000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"population": "10000"}))
cond:Condition = parseCondition("\tbbox_area > 4000000 ")
self.assertEqual(cond.type, ">")
self.assertEqual(cond.params, ("bbox_area", "4000000 ")) # TODO fix parser to exclude trailing space
self.assertTrue(cond.test({"bbox_area": "8000000"}))
self.assertFalse(cond.test({"bbox_area": "4000000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"bbox_area": "999"}))
def test_parser_lt(self):
cond:Condition = parseCondition("population<40000")
self.assertEqual(cond.type, "<")
self.assertEqual(cond.params, ("population", "40000"))
self.assertTrue(cond.test({"population": "100"}))
self.assertTrue(cond.test({"population": "-1"}))
self.assertFalse(cond.test({"population": "40000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"population": "500000"}))
cond:Condition = parseCondition("\tbbox_area < 4000000\n")
self.assertEqual(cond.type, "<")
self.assertEqual(cond.params, ("bbox_area", "4000000\n")) # TODO fix parser to exclude trailing \n
self.assertTrue(cond.test({"bbox_area": "100"}))
self.assertTrue(cond.test({"bbox_area": "-1"}))
self.assertTrue(cond.test({"bbox_area": "000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"bbox_area": "4000000"}))
self.assertFalse(cond.test({"bbox_area": "8000000"}))
def test_parser_le(self):
""" Test conditions in format population <= 100000
Note that such conditions are not used by Organic Maps styles.
"""
cond:Condition = parseCondition("population<=40000")
self.assertEqual(cond.type, "<=")
self.assertEqual(cond.params, ("population", "40000"))
self.assertTrue(cond.test({"population": "100"}))
self.assertTrue(cond.test({"population": "-1"}))
self.assertTrue(cond.test({"population": "40000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"population": "500000"}))
cond:Condition = parseCondition("\tbbox_area <= 4000000\n")
self.assertEqual(cond.type, "<=")
self.assertEqual(cond.params, ("bbox_area", "4000000\n")) # TODO fix parser to exclude trailing \n
self.assertTrue(cond.test({"bbox_area": "100"}))
self.assertTrue(cond.test({"bbox_area": "-1"}))
self.assertTrue(cond.test({"bbox_area": "000"}))
self.assertTrue(cond.test({"bbox_area": "4000000"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"bbox_area": "8000000"}))
def test_parser_ne(self):
cond:Condition = parseCondition("capital!=2")
self.assertEqual(cond.type, "ne")
self.assertEqual(cond.params, ("capital", "2"))
self.assertTrue(cond.test({"capital": "1"}))
self.assertTrue(cond.test({"capital": "22"}))
self.assertTrue(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"capital": "2"}))
cond:Condition = parseCondition("\tcapital != 2")
self.assertEqual(cond.type, "ne")
self.assertEqual(cond.params, ("capital", "2"))
self.assertTrue(cond.test({"capital": "1"}))
self.assertTrue(cond.test({"capital": "22"}))
self.assertTrue(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"capital": "2"}))
cond:Condition = parseCondition("garden:type != residential")
self.assertEqual(cond.type, "ne")
self.assertEqual(cond.params, ("garden:type", "residential"))
self.assertTrue(cond.test({"garden:type": "public"}))
self.assertTrue(cond.test({"garden:type": "res"}))
self.assertTrue(cond.test({"garden:type": "residential_plus"}))
self.assertTrue(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"garden:type": "residential"}))
def test_parser_set(self):
cond:Condition = parseCondition("tunnel")
self.assertEqual(cond.type, "set")
self.assertEqual(cond.params, ("tunnel", ))
self.assertTrue(cond.test({"tunnel": "yes"}))
self.assertTrue(cond.test({"tunnel": "maybe"}))
self.assertTrue(cond.test({"tunnel": "+1"}))
self.assertFalse(cond.test({"highway": "secondary"}))
cond:Condition = parseCondition("building\t")
self.assertEqual(cond.type, "set")
self.assertEqual(cond.params, ("building", ))
self.assertTrue(cond.test({"building": "yes"}))
self.assertTrue(cond.test({"building": "apartment"}))
self.assertTrue(cond.test({"building": "1"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"building:part": "yes"}))
cond:Condition = parseCondition(" addr:housenumber ")
self.assertEqual(cond.type, "set")
self.assertEqual(cond.params, ("addr:housenumber", ))
self.assertTrue(cond.test({"addr:housenumber": "1"}))
self.assertTrue(cond.test({"addr:housenumber": "yes"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"addr:street": "Baker st"}))
cond:Condition = parseCondition(" some-tag ")
self.assertEqual(cond.type, "set")
self.assertEqual(cond.params, ("some-tag", ))
self.assertTrue(cond.test({"some-tag": "1"}))
self.assertTrue(cond.test({"some-tag": "yes"}))
self.assertFalse(cond.test({"highway": "secondary"}))
self.assertFalse(cond.test({"some": "tag"}))
def test_parser_unset(self):
cond:Condition = parseCondition("!tunnel")
self.assertEqual(cond.type, "unset")
self.assertEqual(cond.params, ("tunnel", ))
self.assertTrue(cond.test({"capital": "1"}))
self.assertFalse(cond.test({"tunnel": "yes"}))
self.assertFalse(cond.test({"tunnel": "no"}))
cond:Condition = parseCondition("\t!name ")
self.assertEqual(cond.type, "unset")
self.assertEqual(cond.params, ("name", ))
self.assertTrue(cond.test({"capital": "1"}))
self.assertTrue(cond.test({"int_name": "1"}))
self.assertFalse(cond.test({"name": "London"}))
def test_parser_false(self):
""" Test conditions in format some_tag = no
Note that such conditions are not used by Organic Maps styles.
"""
cond:Condition = parseCondition("access=no")
self.assertEqual(cond.type, "false")
self.assertEqual(cond.params, ("access", ))
#self.assertTrue(cond.test({"access": "no"})) # test is not implemented for `false` condition
#self.assertTrue(cond.test({"access": "private"})) # test is not implemented for `false` condition
self.assertFalse(cond.test({"tunnel": "yes"}))
def test_parser_invTrue(self):
""" Test conditions in format [!some_tag?] It works the same way as [some_tag != yes]
Note that such conditions are not used by Organic Maps styles.
"""
cond:Condition = parseCondition("!oneway?")
self.assertEqual(cond.type, "ne")
self.assertEqual(cond.params, ("oneway", "yes"))
self.assertTrue(cond.test({"oneway": "no"}))
self.assertTrue(cond.test({"oneway": "nobody_knows"}))
self.assertTrue(cond.test({"access": "private"}))
self.assertFalse(cond.test({"oneway": "yes"}))
cond:Condition = parseCondition("\t! intermittent ?\n")
self.assertEqual(cond.type, "ne")
self.assertEqual(cond.params, ("intermittent", "yes"))
self.assertTrue(cond.test({"intermittent": "no"}))
self.assertTrue(cond.test({"intermittent": "maybe"}))
self.assertTrue(cond.test({"access": "private"}))
self.assertFalse(cond.test({"intermittent": "yes"}))
def test_parser_true(self):
""" Test conditions in format [some_tag?] It works the same way as [some_tag = yes] """
cond:Condition = parseCondition("area?")
self.assertEqual(cond.type, "true")
self.assertEqual(cond.params, ("area", ))
self.assertTrue(cond.test({"area": "yes"}))
self.assertFalse(cond.test({"area": "no"}))
self.assertFalse(cond.test({"access": "private"}))
self.assertFalse(cond.test({"oneway": "nobody_knows"}))
cond:Condition = parseCondition("\tbridge ? ")
self.assertEqual(cond.type, "true")
self.assertEqual(cond.params, ("bridge", ))
self.assertTrue(cond.test({"bridge": "yes"}))
self.assertFalse(cond.test({"bridge": "no"}))
self.assertFalse(cond.test({"access": "private"}))
self.assertFalse(cond.test({"bridge": "maybe"}))
def test_untrue(self):
""" parseCondition(...) doesn't support this type of condition.
Not sure if it's ever used.
"""
cond:Condition = Condition("untrue", "access")
self.assertEqual(cond.type, "untrue")
self.assertEqual(cond.params, ("access", ))
self.assertTrue(cond.test({"access": "no"}))
self.assertFalse(cond.test({"access": "private"}))
self.assertFalse(cond.test({"oneway": "yes"}))
def test_parser_errors(self):
with self.assertRaises(Exception):
parseCondition("! tunnel")
with self.assertRaises(Exception):
""" Symbol '-' is only supported in simple 'set' rule. E.g. [key-with-dash]
But not in 'unset' rule [!key-with-dash] """
parseCondition("key-with-dash?")
if __name__ == '__main__':
unittest.main()

124
tests/testEval.py Normal file
View file

@ -0,0 +1,124 @@
import unittest
import sys
from pathlib import Path
# Add `src` directory to the import paths
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
from mapcss.Eval import Eval
class EvalTest(unittest.TestCase):
""" Test eval(...) feature for CSS properties.
NOTE: eval() is not used in Organic Maps styles. We can drop it completely.
"""
def test_eval_tag(self):
a = Eval("""eval( tag("lanes") )""")
self.assertEqual(a.compute({"lanes": "4"}), "4")
self.assertEqual(a.compute({"natural": "trees"}), "")
self.assertSetEqual(a.extract_tags(), {"lanes"})
def test_eval_prop(self):
a = Eval("""eval( prop("dpi") / 2 )""")
self.assertEqual(a.compute({"lanes": "4"}, {"dpi": 144}), "72")
self.assertEqual(a.compute({"lanes": "4"}, {"orientation": "vertical"}), "")
self.assertSetEqual(a.extract_tags(), set())
def test_eval_num(self):
a = Eval("""eval( num(tag("lanes")) + 2 )""")
self.assertEqual(a.compute({"lanes": "4"}), "6")
self.assertEqual(a.compute({"lanes": "many"}), "2")
self.assertSetEqual(a.extract_tags(), {"lanes"})
def test_eval_metric(self):
a = Eval("""eval( metric(tag("height")) )""")
self.assertEqual(a.compute({"height": "512"}), "512")
self.assertEqual(a.compute({"height": "10m"}), "10")
self.assertEqual(a.compute({"height": " 10m"}), "10")
self.assertEqual(a.compute({"height": "500cm"}), "5")
self.assertEqual(a.compute({"height": "500 cm"}), "5")
self.assertEqual(a.compute({"height": "250CM"}), "2.5")
self.assertEqual(a.compute({"height": "250 CM"}), "2.5")
self.assertEqual(a.compute({"height": "30см"}), "0.3")
self.assertEqual(a.compute({"height": " 30 см"}), "0.3")
self.assertEqual(a.compute({"height": "1200 mm"}), "1.2")
self.assertEqual(a.compute({"height": "2400MM"}), "2.4")
self.assertEqual(a.compute({"height": "2800 мм"}), "2.8")
self.assertSetEqual(a.extract_tags(), {"height"})
def test_eval_metric_with_scale(self):
a = Eval("""eval( metric(tag("height")) )""")
self.assertEqual(a.compute({"height": "512"}, xscale=4), "2048")
self.assertEqual(a.compute({"height": "512"}, zscale=4), "512")
self.assertEqual(a.compute({"height": "10m"}, xscale=4), "40")
self.assertEqual(a.compute({"height": " 10m"}, xscale=4), "40")
self.assertEqual(a.compute({"height": "500cm"}, xscale=4), "20")
self.assertEqual(a.compute({"height": "500 cm"}, xscale=4), "20")
self.assertEqual(a.compute({"height": "250CM"}, xscale=4), "10")
self.assertEqual(a.compute({"height": "250 CM"}, xscale=4), "10")
self.assertEqual(a.compute({"height": "30см"}, xscale=4), "1.2")
self.assertEqual(a.compute({"height": " 30 см"}, xscale=4), "1.2")
self.assertEqual(a.compute({"height": "1200 mm"}, xscale=4), "4.8")
self.assertEqual(a.compute({"height": "2400MM"}, xscale=4), "9.6")
self.assertEqual(a.compute({"height": "2800 мм"}, xscale=4), "11.2")
self.assertSetEqual(a.extract_tags(), {"height"})
def test_eval_zmetric(self):
a = Eval("""eval( zmetric(tag("depth")) )""")
self.assertEqual(a.compute({"depth": "512"}), "256")
self.assertEqual(a.compute({"depth": "10m"}), "5")
self.assertEqual(a.compute({"depth": " 10m"}), "5")
self.assertEqual(a.compute({"depth": "500cm"}), "2.5")
self.assertEqual(a.compute({"depth": "500 cm"}), "2.5")
self.assertEqual(a.compute({"depth": "250CM"}), "1.25")
self.assertEqual(a.compute({"depth": "250 CM"}), "1.25")
self.assertEqual(a.compute({"depth": "30см"}), "0.15")
self.assertEqual(a.compute({"depth": " 30 см"}), "0.15")
self.assertEqual(a.compute({"depth": "1200 mm"}), "0.6")
self.assertEqual(a.compute({"depth": "2400MM"}), "1.2")
self.assertEqual(a.compute({"depth": "2800 мм"}), "1.4")
self.assertSetEqual(a.extract_tags(), {"depth"})
def test_eval_str(self):
a = Eval("""eval( str( num(tag("width")) - 200 ) )""")
self.assertEqual(a.compute({"width": "400"}), "200.0")
self.assertSetEqual(a.extract_tags(), {"width"})
def test_eval_any(self):
a = Eval("""eval( any(tag("building"), tag("building:part"), "no") )""")
self.assertEqual(a.compute({"building": "apartment"}), "apartment")
self.assertEqual(a.compute({"building:part": "roof"}), "roof")
self.assertEqual(a.compute({"junction": "roundabout"}), "no")
self.assertSetEqual(a.extract_tags(), {"building", "building:part"})
def test_eval_min(self):
a = Eval("""eval( min( num(tag("building:levels")) * 3, 50) )""")
self.assertEqual(a.compute({"natural": "wood"}), "0")
self.assertEqual(a.compute({"building:levels": "0"}), "0")
self.assertEqual(a.compute({"building:levels": "10"}), "30")
self.assertEqual(a.compute({"building:levels": "30"}), "50")
self.assertSetEqual(a.extract_tags(), {"building:levels"})
def test_eval_max(self):
a = Eval("""eval( max( tag("speed:limit"), 60) )""")
self.assertEqual(a.compute({"natural": "wood"}), "60")
self.assertEqual(a.compute({"speed:limit": "30"}), "60")
self.assertEqual(a.compute({"speed:limit": "60"}), "60")
self.assertEqual(a.compute({"speed:limit": "90"}), "90")
self.assertSetEqual(a.extract_tags(), {"speed:limit"})
def test_eval_cond(self):
a = Eval("""eval( cond( boolean(tag("oneway")), 200, 100) )""")
self.assertEqual(a.compute({"natural": "wood"}), "100")
self.assertEqual(a.compute({"oneway": "yes"}), "200")
self.assertEqual(a.compute({"oneway": "no"}), "100")
self.assertEqual(a.compute({"oneway": "true"}), "200")
self.assertEqual(a.compute({"oneway": "probably no"}), "200")
self.assertSetEqual(a.extract_tags(), {"oneway"})
def test_complex_eval(self):
a = Eval(""" eval( any( metric(tag("height")), metric ( num(tag("building:levels")) * 3), metric("1m"))) """)
self.assertEqual(a.compute({"building:levels": "3"}), "9")
self.assertSetEqual(a.extract_tags(), {"height", "building:levels"})
if __name__ == '__main__':
unittest.main()

68
tests/testLibkomwm.py Normal file
View file

@ -0,0 +1,68 @@
import unittest
import sys
from pathlib import Path
from copy import deepcopy
# Add `src` directory to the import paths
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
import libkomwm
from libkomwm import komap_mapswithme
class LibKomwmTest(unittest.TestCase):
def test_generate_drules_mini(self):
assets_dir = Path(__file__).parent / 'assets' / 'case-2-generate-drules-mini'
class Options(object):
pass
options = Options()
options.data = None
options.minzoom = 0
options.maxzoom = 10
options.txt = True
options.filename = str( assets_dir / "main.mapcss" )
options.outfile = str( assets_dir / "style_output" )
options.priorities_path = str( assets_dir / "include" )
try:
# Save state
libkomwm.MULTIPROCESSING = False
prio_ranges_orig = deepcopy(libkomwm.prio_ranges)
libkomwm.visibilities = {}
# Run style generation
komap_mapswithme(options)
# Restore state
libkomwm.prio_ranges = prio_ranges_orig
libkomwm.MULTIPROCESSING = True
libkomwm.visibilities = {}
# Check that types.txt contains 1173 lines
with open(assets_dir / "types.txt", "rt") as typesFile:
lines = [l.strip() for l in typesFile]
self.assertEqual(len(lines), 1173, "Generated types.txt file should contain 1173 lines")
self.assertEqual(len([l for l in lines if l!="mapswithme"]), 148, "Actual types count should be 148 as in mapcss-mapping.csv")
# Check that style_output.bin has 20 styles
with open(assets_dir / "style_output.bin", "rb") as protobuf_file:
protobuf_data = protobuf_file.read()
drules = libkomwm.ContainerProto()
drules.ParseFromString(protobuf_data)
self.assertEqual(len(drules.cont), 20, "Generated style_output.bin should contain 20 styles")
finally:
# Clean up generated files
files2delete = ["classificator.txt", "colors.txt", "patterns.txt", "style_output.bin",
"style_output.txt", "types.txt", "visibility.txt"]
for filename in files2delete:
(assets_dir / filename).unlink(missing_ok=True)
def test_generate_drules_validation_errors(self):
assets_dir = Path(__file__).parent / 'assets' / 'case-3-styles-validation'
# TODO: needs refactoring of libkomwm.validation_errors_count to have a list
# of validation errors.
self.assertTrue(True)

364
tests/testMapCSS.py Normal file
View file

@ -0,0 +1,364 @@
import unittest
import sys
from pathlib import Path
# Add `src` directory to the import paths
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
from mapcss import parseDeclaration, MapCSS
class MapCSSTest(unittest.TestCase):
def test_declarations(self):
decl = parseDeclaration(""" linejoin: round; """)
self.assertEqual(len(decl), 1)
self.assertEqual(decl[0], {"linejoin": "round"})
decl = parseDeclaration("""\tlinejoin :\nround ; """)
self.assertEqual(len(decl), 1)
self.assertEqual(decl[0], {"linejoin": "round"})
decl = parseDeclaration(""" icon-image: parking_private-s.svg; text: "name"; """)
self.assertEqual(len(decl), 1)
self.assertEqual(decl[0], {
"icon-image": "parking_private-s.svg",
"text": "name"
})
decl = parseDeclaration("""
pattern-offset: 90\t;
pattern-image:\tarrow-m.svg ;
pattern-spacing: @trunk0 ;""")
self.assertEqual(len(decl), 1)
self.assertEqual(decl[0], {
"pattern-offset": "90",
"pattern-image": "arrow-m.svg",
"pattern-spacing": "@trunk0",
})
def test_parse_variables(self):
parser = MapCSS()
parser.parse("""
@city_label: #999999;
@country_label: #444444;
@wave_length: 25;
""")
self.assertEqual(parser.variables, {
"city_label": "#999999",
"country_label": "#444444",
"wave_length": "25"
})
def test_parse_colors(self):
parser = MapCSS()
parser.parse("""
@city_label : #999999;
@country_label: #444444 ;
@wave_length: 25;
""")
self.assertEqual(parser.variables, {
"city_label": "#999999",
"country_label": "#444444",
"wave_length": "25"
})
def test_parse_import(self):
parser = MapCSS()
mapcssFile = Path(__file__).parent / 'assets' / 'case-1-import' / 'main.mapcss'
parser.parse(filename=str(mapcssFile))
colors = parser.get_colors()
self.assertEqual(colors, {
"GuiText-color": (1.0, 1.0, 1.0),
"GuiText-opacity": 0.7,
"Route-color": (0.0, 0.0, 1.0),
"Route-opacity": 0.5,
})
def test_parse_basic_chooser(self):
parser = MapCSS()
static_tags = {"tourism": True, "office": True,
"craft": True, "amenity": True}
parser.parse("""
node|z17-[tourism],
area|z17-[tourism],
node|z18-[office],
area|z18-[office],
node|z18-[craft],
area|z18-[craft],
node|z19-[amenity],
area|z19-[amenity],
{text: name; text-color: #000030; text-offset: 1;}
""", static_tags=static_tags)
self.assertEqual(len(parser.choosers), 1)
self.assertEqual(len(parser.choosers[0].ruleChains), 8)
def test_parse_basic_chooser_2(self):
parser = MapCSS()
static_tags = {"highway": True}
parser.parse("""
@trunk0: #FF7326;
line|z6[highway=trunk],
line|z6[highway=motorway],
{color: @trunk0; opacity: 0.3;}
line|z7-9[highway=trunk],
line|z7-9[highway=motorway],
{color: @trunk0; opacity: 0.7;}
""", static_tags=static_tags)
self.assertEqual(len(parser.choosers), 2)
self.assertEqual(len(parser.choosers[0].ruleChains), 2)
self.assertEqual(parser.choosers[0].ruleChains[0].subject, 'line')
self.assertEqual(parser.choosers[0].selzooms, [6, 6])
self.assertEqual(parser.choosers[1].selzooms, [7, 9])
rule, object_id = parser.choosers[0].testChains({"highway": "trunk"})
self.assertEqual(object_id, "::default")
def test_parse_basic_chooser_3(self):
parser = MapCSS()
static_tags = {"addr:housenumber": True, "addr:street": False}
parser.parse("""
/* Some Comment Here */
/*
This sample is borrowed from Organic Maps Basemap_label.mapcss file
*/
node|z18-[addr:housenumber][addr:street]::int_name
{text: int_name; text-color: #65655E; text-position: center;}
""", static_tags=static_tags)
building_tags = {"building": "yes", "addr:housenumber": "12", "addr:street": "Baker street"}
# Check that mapcss parsed correctly
self.assertEqual(len(parser.choosers), 1)
styleChooser = parser.choosers[0]
self.assertEqual(len(styleChooser.ruleChains), 1)
self.assertEqual(styleChooser.selzooms, [18, 19])
rule, object_id = styleChooser.testChains(building_tags)
self.assertEqual(object_id, "::int_name")
rule = styleChooser.ruleChains[0]
self.assertEqual(rule.subject, 'node')
self.assertEqual(rule.extract_tags(), {'addr:housenumber', 'addr:street'})
def test_parse_basic_chooser_class(self):
parser = MapCSS()
parser.parse("""
way|z-13::*
{
linejoin: round;
}
""")
# Check that mapcss parsed correctly
self.assertEqual(len(parser.choosers), 1)
styleChooser = parser.choosers[0]
self.assertEqual(len(styleChooser.ruleChains), 1)
self.assertEqual(styleChooser.selzooms, [0, 13])
rule, object_id = styleChooser.testChains({})
self.assertEqual(object_id, "::*")
rule = styleChooser.ruleChains[0]
self.assertEqual(rule.subject, 'way')
self.assertEqual(rule.extract_tags(), {'*'})
def test_parse_basic_chooser_class_2(self):
parser = MapCSS()
parser.parse("""
way|z10-::*
{
linejoin: round;
}
""")
# Check that mapcss parsed correctly
self.assertEqual(len(parser.choosers), 1)
styleChooser = parser.choosers[0]
self.assertEqual(len(styleChooser.ruleChains), 1)
self.assertEqual(styleChooser.selzooms, [10, 19])
rule, object_id = styleChooser.testChains({})
self.assertEqual(object_id, "::*")
rule = styleChooser.ruleChains[0]
self.assertEqual(rule.subject, 'way')
self.assertEqual(rule.extract_tags(), {'*'})
def test_parse_basic_chooser_colors(self):
parser = MapCSS()
parser.parse("""
way|z-6::*
{
linejoin: round;
}
colors {
GuiText-color: #FFFFFF;
GuiText-opacity: 0.7;
MyPositionAccuracy-color: #FFFFFF;
MyPositionAccuracy-opacity: 0.06;
Selection-color: #FFFFFF;
Selection-opacity: 0.64;
Route-color: #0000FF;
RouteOutline-color: #00FFFF;
}
""")
# Check that colors from mapcss parsed correctly
colors = parser.get_colors()
self.assertEqual(colors, {
"GuiText-color": (1.0, 1.0, 1.0),
"GuiText-opacity": 0.7,
"MyPositionAccuracy-color": (1.0, 1.0, 1.0),
"MyPositionAccuracy-opacity": 0.06,
"Selection-color": (1.0, 1.0, 1.0),
"Selection-opacity": 0.64,
"Route-color": (0.0, 0.0, 1.0),
"RouteOutline-color": (0.0, 1.0, 1.0)
})
def test_parser_choosers_tree(self):
parser = MapCSS()
static_tags = {"tourism": True, "office": True,
"craft": True, "amenity": True}
parser.parse("""
node|z17-[office=lawyer],
area|z17-[office=lawyer],
{text: name;text-color: #444444;text-offset: 1;font-size: 10;}
node|z17-[tourism],
area|z17-[tourism],
node|z18-[office],
area|z18-[office],
node|z18-[craft],
area|z18-[craft],
node|z19-[amenity],
area|z19-[amenity],
{text: name; text-color: #000030; text-offset: 1;}
node|z18-[office],
area|z18-[office],
node|z18-[craft],
area|z18-[craft],
{font-size: 11;}
node|z17-[office=lawyer],
area|z17-[office=lawyer]
{icon-image: lawyer-m.svg;}
""", static_tags=static_tags)
for obj_type in ["line", "area", "node"]:
parser.build_choosers_tree("tourism", obj_type, "tourism")
parser.build_choosers_tree("office", obj_type, "office")
parser.build_choosers_tree("craft", obj_type, "craft")
parser.build_choosers_tree("amenity", obj_type, "amenity")
parser.finalize_choosers_tree()
# Pick style for zoom = 17
styles18 = parser.get_style("office", "node", {"office": "lawyer"},
zoom=18, xscale=1, zscale=1, filter_by_runtime_conditions=False)
self.assertEqual(len(styles18), 1),
self.assertEqual(styles18[0], {'object-id': '::default',
'font-size': '11',
'text': 'name',
'text-color': (0, 0, 16*3/255),
'text-offset': 1.0,
'icon-image': 'lawyer-m.svg'})
# Pick style for zoom = 17
styles17 = parser.get_style("office", "node", {"office": "lawyer"},
zoom=17, xscale=1, zscale=1, filter_by_runtime_conditions=False)
self.assertEqual(len(styles17), 1),
self.assertEqual(styles17[0], {'object-id': '::default',
'font-size': '10',
'text': 'name',
'text-color': (68/255, 68/255, 68/255),
'text-offset': 1.0,
'icon-image': 'lawyer-m.svg'})
# Pick style for zoom = 15
styles15 = parser.get_style("office", "node", {"office": "lawyer"},
zoom=15, xscale=1, zscale=1, filter_by_runtime_conditions=False)
self.assertEqual(styles15, []),
def test_parser_choosers_tree_with_classes(self):
parser = MapCSS()
static_tags = {"highway": True}
parser.parse("""
line|z10-[highway=motorway]::shield,
line|z10-[highway=trunk]::shield,
line|z10-[highway=motorway_link]::shield,
line|z10-[highway=trunk_link]::shield,
line|z10-[highway=primary]::shield,
line|z11-[highway=primary_link]::shield,
line|z12-[highway=secondary]::shield,
line|z13-[highway=tertiary]::shield,
line|z15-[highway=residential]::shield,
{
shield-font-size: 9;
shield-text-color: #000000;
shield-text-halo-radius: 0;
shield-color: #FFFFFF;
shield-outline-radius: 1;
}
line|z12-[highway=residential],
line|z12-[highway=tertiary],
line|z18-[highway=tertiary_link]
{
text: name;
text-color: #333333;
text-halo-opacity: 0.8;
text-halo-radius: 1;
}
line|z12-13[highway=residential],
line|z12-13[highway=tertiary]
{
font-size: 12;
text-color: #444444;
}
""", static_tags=static_tags)
parser.build_choosers_tree("highway", "line", "highway")
parser.finalize_choosers_tree()
# Pick style for zoom = 10
styles10 = parser.get_style("highway", "line", {"highway": "primary"},
zoom=10, xscale=1, zscale=1, filter_by_runtime_conditions=False)
self.assertEqual(len(styles10), 1),
self.assertEqual(styles10[0], {'object-id': '::shield',
'shield-font-size': '9',
'shield-text-color': (0.0, 0.0, 0.0),
'shield-text-halo-radius': 0.0,
'shield-color': (1.0, 1.0, 1.0),
'shield-outline-radius': 1.0})
# Pick style for zoom = 15. Expecting two `object-id` values: '::shield' and '::default'
styles15 = parser.get_style("highway", "line", {"highway": "tertiary"},
zoom=15, xscale=1, zscale=1, filter_by_runtime_conditions=False)
self.assertEqual(len(styles15), 2),
self.assertEqual(styles15[0], {'object-id': '::shield',
'shield-font-size': '9',
'shield-text-color': (0.0, 0.0, 0.0),
'shield-text-halo-radius': 0.0,
'shield-color': (1.0, 1.0, 1.0),
'shield-outline-radius': 1.0})
self.assertEqual(styles15[1], {'object-id': '::default',
'text': 'name',
'text-color': (51/255, 51/255, 51/255),
'text-halo-opacity': 0.8,
'text-halo-radius': 1.0})
if __name__ == '__main__':
unittest.main()

114
tests/testRule.py Normal file
View file

@ -0,0 +1,114 @@
import unittest
import sys
from pathlib import Path
# Add `src` directory to the import paths
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
from mapcss.Rule import Rule
from mapcss.Condition import Condition
from mapcss import parseCondition
class RuleTest(unittest.TestCase):
def test_rule_subject(self):
self.assertEqual(Rule().subject, "")
self.assertEqual(Rule("*").subject, "")
self.assertEqual(Rule("way").subject, "way")
self.assertEqual(Rule("area").subject, "area")
self.assertEqual(Rule("node").subject, "node")
self.assertEqual(Rule("planet").subject, "planet")
def test_rule_type_matches(self):
self.assertCountEqual(Rule().type_matches, ('area', 'line', 'way', 'node'))
self.assertCountEqual(Rule("*").type_matches, ('area', 'line', 'way', 'node'))
self.assertCountEqual(Rule("way").type_matches, ('area', 'line', 'way'))
self.assertCountEqual(Rule("area").type_matches, ('area', 'way'))
self.assertCountEqual(Rule("node").type_matches, ('node', ))
self.assertCountEqual(Rule("planet").type_matches, set())
def test_rule_with_conditions(self):
rule = Rule()
rule.conditions = [
parseCondition("aeroway=aerodrome"),
parseCondition("aerodrome=international")
]
tt = rule.test({
"aeroway": "aerodrome",
"aerodrome": "international",
"name": "JFK"
})
self.assertTrue(tt)
self.assertEqual(tt, "::default")
self.assertCountEqual(rule.extract_tags(), ["aeroway", "aerodrome"])
# Negative test cases
self.assertFalse(rule.test({
"aeroway": "aerodrome",
"name": "JFK"
}))
def test_rule_with_class(self):
rule = Rule()
rule.conditions = [
parseCondition("highway=unclassified"),
parseCondition("bridge?"),
Condition("eq", ("::class", "::bridgeblack"))
]
tt = rule.test({
"highway": "unclassified",
"bridge": "yes",
"layer": "1"
})
self.assertTrue(tt)
self.assertEqual(tt, "::bridgeblack")
self.assertCountEqual(rule.extract_tags(), ["highway", "bridge"])
# Negative test cases
self.assertFalse(rule.test({
"highway": "unclassified",
"bridge": "no",
"layer": "1"
}))
self.assertFalse(rule.test({
"highway": "unclassified",
"tunnel": "yes",
"layer": "-1"
}))
def test_tags_from_rule_with_class(self):
# Class condition doesn't add new tags
rule = Rule()
rule.conditions = [
parseCondition("highway=unclassified"),
parseCondition("bridge?"),
Condition("eq", ("::class", "::bridgeblack")),
]
self.assertCountEqual(rule.extract_tags(), ["highway", "bridge"])
# Class condition doesn't add new tags
rule = Rule()
rule.conditions = [
parseCondition("highway=unclassified"),
Condition("eq", ("::class", "::*")),
parseCondition("bridge?"),
]
self.assertCountEqual(rule.extract_tags(), ["highway", "bridge"])
# BUT having class as a first item overrides all the others
rule = Rule()
rule.conditions = [
Condition("eq", ("::class", "::int_name")),
parseCondition("highway=unclassified"),
parseCondition("bridge?"),
]
self.assertCountEqual(rule.extract_tags(), ["*"])
if __name__ == '__main__':
unittest.main()

297
tests/testStyleChooser.py Normal file
View file

@ -0,0 +1,297 @@
import unittest
import sys
from pathlib import Path
from mapcss.Rule import Rule
# Add `src` directory to the import paths
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
from mapcss import parseCondition, Condition
from mapcss.Eval import Eval
from mapcss.StyleChooser import StyleChooser, make_nice_style
class StyleChooserTest(unittest.TestCase):
def test_rules_chain(self):
sc = StyleChooser((0, 16))
sc.newObject()
sc.addCondition(parseCondition("highway=footway"))
sc.addCondition(parseCondition("footway=sidewalk"))
sc.newObject()
sc.addCondition(parseCondition("highway=footway"))
sc.addCondition(parseCondition("footway=crossing"))
sc.addCondition(Condition("eq", ("::class", "::*")))
self.assertTrue( sc.testChains({ "highway": "footway", "footway": "sidewalk" }) )
self.assertTrue( sc.testChains({ "highway": "footway", "footway": "crossing" }) )
self.assertFalse( sc.testChains({ "highway": "footway"}) )
self.assertFalse( sc.testChains({ "highway": "residential", "footway": "crossing" }) )
rule1, tt = sc.testChains({ "highway": "footway", "footway": "sidewalk" })
self.assertEqual(tt, "::default")
rule2, tt = sc.testChains({ "highway": "footway", "footway": "crossing" })
self.assertEqual(tt, "::*")
self.assertNotEqual(rule1, rule2)
def test_zoom(self):
sc = StyleChooser((0, 16))
sc.newObject()
sc.addZoom( (10, 19) )
sc.addCondition(parseCondition("railway=station"))
sc.addCondition(parseCondition("transport=subway"))
sc.addCondition(parseCondition("city=yerevan"))
sc.newObject()
sc.addZoom( (4, 15) )
sc.addCondition(parseCondition("railway=station"))
sc.addCondition(parseCondition("transport=subway"))
sc.addCondition(parseCondition("city=yokohama"))
rule1, tt = sc.testChains({ "railway": "station", "transport": "subway", "city": "yerevan" })
self.assertEqual(rule1.minZoom, 10)
self.assertEqual(rule1.maxZoom, 19)
rule2, tt = sc.testChains({ "railway": "station", "transport": "subway", "city": "yokohama" })
self.assertEqual(rule2.minZoom, 4)
self.assertEqual(rule2.maxZoom, 15)
def test_extract_tags(self):
sc = StyleChooser((0, 16))
sc.newObject()
sc.addCondition(parseCondition("aerialway=rope_tow"))
sc.newObject()
sc.addCondition(parseCondition("piste:type=downhill"))
self.assertSetEqual(sc.extract_tags(), {"aerialway", "piste:type"})
sc = StyleChooser((0, 16))
sc.newObject()
sc.addCondition(parseCondition("aeroway=terminal"))
sc.addCondition(parseCondition("building"))
sc.newObject()
sc.addCondition(parseCondition("waterway=dam"))
sc.addCondition(parseCondition("building:part"))
self.assertSetEqual(sc.extract_tags(), {"waterway", "building:part", "building", "aeroway"})
def test_make_nice_style(self):
style = make_nice_style({
"outline-color": "none",
"bg-color": "red",
"dash-color": "#ffff00",
"front-color": "rgb(0, 255, 255)",
"line-width": Eval("""eval(min(tag("line_width"), 10))"""),
"outline-width": "2.5",
"arrow-opacity": "0.5",
"offset-2": "20",
"border-radius": "4",
"line-extrude": "16",
"dashes": "3,3,1.5,3",
"wrong-dashes": "yes, yes, yes, no",
"make-nice": True,
"additional-len": 44.5
})
expectedStyle = {
"outline-color": "none",
"bg-color": (1.0, 0.0, 0.0),
"dash-color": (1.0, 1.0, 0.0),
"front-color": (0.0, 1.0, 1.0),
"line-width": Eval("""eval(min(tag("line_width"), 10))"""),
"outline-width": 2.5,
"arrow-opacity": 0.5,
"offset-2": 20.0,
"border-radius": 4.0,
"line-extrude": 16.0,
"dashes": [3.0, 3.0, 1.5, 3.0],
"wrong-dashes": [],
"make-nice": True,
"additional-len": 44.5
}
self.assertEqual(style, expectedStyle)
def test_add_styles(self):
sc = StyleChooser((15, 19))
sc.newObject()
sc.addStyles([{
"width": "1.3",
"opacity": "0.6",
"bg-color": "blue"
}])
sc.addStyles([{
"color": "#FFFFFF",
"casing-width": "+10"
}])
self.assertEqual(len(sc.styles), 2)
self.assertEqual(sc.styles[0], {
"width": 1.3,
"opacity": 0.6,
"bg-color": (0.0, 0.0, 1.0)
})
self.assertEqual(sc.styles[1], {
"color": (1.0, 1.0, 1.0),
"casing-width": 5.0
})
def test_update_styles(self):
styles = [{"primary_color": (1.0, 1.0, 1.0)}]
sc = StyleChooser((15, 19))
sc.newObject()
sc.addStyles([{
"width": "1.3",
"opacity": "0.6",
"bg-color": """eval( prop("primary_color") )""", # Check that property from `styles` is applied
"text-offset": """eval( cond( boolean(tag("oneway")), 10, 5) )""" # Check that tags are applied
}])
object_tags = {"highway": "service",
"oneway": "yes"}
new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False)
expected_new_styles = {
"width": 1.3,
"opacity": 0.6,
"bg-color": (1.0, 1.0, 1.0),
"text-offset": 10.0,
"object-id": "::default"
}
self.assertEqual(len(new_styles), 2)
self.assertEqual(new_styles[-1], expected_new_styles)
def test_update_styles_2(self):
styles = []
sc = StyleChooser((15, 19))
sc.newObject()
sc.addCondition(Condition("eq", ("::class", "::int_name") )) # Class should be added to the style
sc.addCondition(parseCondition("oneway?"))
sc.addStyles([{
"width": "1.3",
"bg-color": "black"
}])
object_tags = {"highway": "service", "oneway": "yes"}
new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False)
expected_new_styles = {
"width": 1.3,
"bg-color": (0.0, 0.0, 0.0),
"object-id": "::int_name" # Check that class from sc.ruleChains is added to the style
}
self.assertEqual(len(new_styles), 1)
self.assertEqual(new_styles[-1], expected_new_styles)
def test_update_styles_by_class(self):
# Predefined styles
styles = [{
"some-width": 2.5,
"object-id": "::flats"
},
{
"some-width": 3.5,
"object-id": "::bridgeblack"
},
{
"some-width": 4.5,
"object-id": "::default"
}]
sc = StyleChooser((15, 19))
sc.newObject()
sc.addCondition(Condition("eq", ("::class", "::flats") )) # `sc` styles should apply only to `::flats` class
sc.addCondition(parseCondition("oneway?"))
sc.newObject()
sc.addCondition(Condition("eq", ("::class", "::bridgeblack") )) # This class is ignored by StyleChooser
sc.addCondition(parseCondition("oneway?"))
sc.addStyles([{
"some-width": "1.5",
"other-offset": "4"
}])
object_tags = {"highway": "service", "oneway": "yes"}
# Apply new style to predefined styles with filter by class
new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False)
expected_new_styles = [{ # The first style changes
"some-width": 1.5,
"other-offset": 4.0,
"object-id": "::flats"
},
{ # Style not changed (class is not `::flats`)
"some-width": 3.5,
"object-id": "::bridgeblack"
},
{ # Style not changed (class is not `::flats`)
"some-width": 4.5,
"object-id": "::default"
}]
self.assertEqual(len(new_styles), 3)
self.assertEqual(new_styles, expected_new_styles)
def test_update_styles_by_class_all(self):
# Predefined styles
styles = [{ # This is applied to StyleChooser styles
"some-width": 2.5,
"corner-radius": 2.5,
"object-id": "::*"
},
{
"some-width": 3.5,
"object-id": "::bridgeblack"
}]
sc = StyleChooser((15, 19))
sc.newObject()
sc.addCondition(parseCondition("tunnel"))
sc.addStyles([{
"some-width": "1.5",
"other-offset": "4"
}])
object_tags = {"highway": "service", "tunnel": "yes"}
# Apply new style to predefined styles with filter by class
new_styles = sc.updateStyles(styles, object_tags, 1.0, 1.0, False)
# Check that new style with new `object-id` is added.
# This style is built from `styles[0]` and styles from `sc`
expected_new_style = {
"some-width": 1.5,
"corner-radius": 2.5,
"other-offset": 4.0,
"object-id": "::default" # New class, never listed in `styles`
}
self.assertEqual(len(new_styles), 3)
self.assertEqual(new_styles[-1], expected_new_style)
def test_runtime_conditions(self):
# TODO: Create test with sc.addRuntimeCondition(Condition(condType, ('extra_tag', cond)))
pass
if __name__ == '__main__':
unittest.main()