Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions integration-tests/full_drules_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,16 @@

def full_styles_regenerate(options):
log.info("Start generating styles")
libkomwm.MULTIPROCESSING = False
prio_ranges_orig = deepcopy(libkomwm.prio_ranges)
Comment on lines -31 to -32

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the use-case for these options?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MULTIPROCESSING flags enables paraller processing of CSS rules. But on practice enabling it doesn't give signifficant improvements. Generating of drules takes less than 2 second. And adding paralellisation speed up is insignifficant.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 seconds is still too much. There is a plan to fix/revive Map Style Designer in the repo, it should regenerate map style on every edit/change, to immediately see the difference on the map.


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)
libkomwm.main(options)
log.info("Done!")

def main():
Expand Down
167 changes: 167 additions & 0 deletions src/drules_classificator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""
Classificator parsing and management.
"""
import os
import csv
import logging
from typing import Dict, List, Set
from collections import OrderedDict

# Configure module logger
logger = logging.getLogger(__name__)

# Constants
EXPECTED_SHORT_COLUMNS = 3
EXPECTED_FULL_COLUMNS = 7


class ClassificatorManager:
"""Manages classificator types and their parsing."""

def __init__(self) -> None:
"""Initialize classificator manager."""
self.classificator: Dict[str, OrderedDict] = {}
self.class_order: List[str] = []
self.class_tree: Dict[str, str] = {}

def parse_mapcss_mapping(self, data_dir: str) -> Set[str]:
"""
Parse mapcss-mapping.csv file.

Args:
data_dir: Directory containing mapcss-mapping.csv

Returns:
Set of unique types for validation

Raises:
FileNotFoundError: If required files don't exist
ValueError: If file format is invalid
"""
# Validate input directory
if not os.path.isdir(data_dir):
raise FileNotFoundError(f"Data directory not found: {data_dir}")

types_file_path = os.path.join(data_dir, 'types.txt')
mapping_file_path = os.path.join(data_dir, 'mapcss-mapping.csv')

if not os.path.isfile(mapping_file_path):
raise FileNotFoundError(f"Mapping file not found: {mapping_file_path}")

try:
with open(types_file_path, "w") as types_file:
cnt = 1
unique_types_check = set()

with open(mapping_file_path) as mapping_file:
for row_num, row in enumerate(csv.reader(mapping_file, delimiter=';'), start=1):
if len(row) <= 1 or row[0].startswith('#'):
# Allow for empty lines and comment lines starting with '#'.
continue

if len(row) == EXPECTED_SHORT_COLUMNS:
# Short format: type name, type id, x / replacement type name
tag = row[0].replace('|', '=')
obsolete = len(row[2].strip()) > 0
row = (row[0], '[{0}]'.format(tag), 'x' if obsolete else '', 'name', 'int_name', row[1], row[2] if row[2] != 'x' else '')

if len(row) != EXPECTED_FULL_COLUMNS:
raise ValueError(
f'Row {row_num}: Expecting {EXPECTED_SHORT_COLUMNS} or {EXPECTED_FULL_COLUMNS} columns, '
f'got {len(row)}: {";".join(row)}'
)

try:
type_id = int(row[5])
except ValueError:
raise ValueError(f'Row {row_num}: Invalid type id "{row[5]}"')

if type_id < cnt:
raise ValueError(
f'Row {row_num}: Wrong type id {type_id}, expected >= {cnt}: {";".join(row)}'
)

while type_id > cnt:
types_file.write("mapswithme\n")
cnt += 1
cnt += 1

cl = row[0].replace("|", "-")
if cl in unique_types_check and row[2] != 'x':
raise ValueError(f'Row {row_num}: Duplicate type: {row[0]}')

pairs = [i.strip(']').split("=") for i in row[1].split(',')[0].split('[')]
kv = OrderedDict()
for i in pairs:
if len(i) == 1:
if i[0]:
if i[0][0] == "!":
kv[i[0][1:].strip('?')] = "no"
else:
kv[i[0].strip('?')] = "yes"
else:
kv[i[0]] = i[1]

if row[2] != "x":
self.classificator[cl] = kv
self.class_order.append(cl)
unique_types_check.add(cl)
# Mark original type to distinguish it among replacing types.
types_file.write(f"*{row[0]}\n")
else:
# compatibility mode
if row[6]:
types_file.write(f"{row[6]}\n")
else:
types_file.write("mapswithme\n")

self.class_tree[cl] = row[0]

except IOError as e:
logger.error(f"Error reading/writing files: {e}")
raise
except csv.Error as e:
logger.error(f"CSV parsing error: {e}")
raise

self.class_order.sort()
return unique_types_check

def get_mapcss_static_tags(self) -> Dict[str, bool]:
"""
Get all mapcss static tags used in mapcss-mapping.csv.

Returns:
Dictionary with main_tag flags (True = appears first in types)
"""
mapcss_static_tags = {}
for v in list(self.classificator.values()):
for i, t in enumerate(v.keys()):
mapcss_static_tags[t] = mapcss_static_tags.get(t, True) and i == 0
return mapcss_static_tags


def load_mapcss_dynamic_tags(data_dir: str) -> Set[str]:
"""
Load mapcss dynamic tags from mapcss-dynamic.txt.

Args:
data_dir: Directory containing mapcss-dynamic.txt

Returns:
Set of dynamic tag names

Raises:
FileNotFoundError: If mapcss-dynamic.txt doesn't exist
"""
dynamic_file_path = os.path.join(data_dir, 'mapcss-dynamic.txt')

if not os.path.isfile(dynamic_file_path):
raise FileNotFoundError(f"Dynamic tags file not found: {dynamic_file_path}")

try:
with open(dynamic_file_path) as dynamic_file:
return {line.rstrip() for line in dynamic_file}
except IOError as e:
logger.error(f"Error reading dynamic tags file: {e}")
raise
143 changes: 143 additions & 0 deletions src/drules_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""
Configuration constants for drules generation.

This module defines priority ranges for drawing rules and their associated metadata.
Priority ranges control the rendering order of map features.
"""
import logging
from typing import Dict, Any, NamedTuple

# Configure module logger
logger = logging.getLogger(__name__)

# =============================================================================
# CONSTANTS
# =============================================================================

# Priority range for area and line drules. Should be same as drule::kLayerPriorityRange.
LAYER_PRIORITY_RANGE = 1000

# Should be same as drule::kOverlaysMaxPriority. The overlays range is [-kOverlaysMaxPriority; kOverlaysMaxPriority),
# negative values are used for optional captions which are below most other overlays.
OVERLAYS_MAX_PRIORITY = 10000

# Priority range identifiers
PRIO_OVERLAYS = 'overlays'
PRIO_FG = 'FG'
PRIO_BG_TOP = 'BG-top'
PRIO_BG_BY_SIZE = 'BG-by-size'

# =============================================================================
# FILE HEADER COMMENTS
# =============================================================================

COMMENT_AUTOFORMAT = (
'This file is automatically re-formatted and re-sorted in priorities descending order\n'
'when generate_drules.sh is run. All comments (automatic priorities of e.g. optional captions, drule types visibilities, etc.)\n'
'are generated automatically for information only. Custom formatting and comments are not preserved.\n'
)

COMMENT_RANGES_OVERVIEW = (
"\nPriorities ranges' rendering order overview:\n"
'- overlays (icons, captions...)\n'
'- FG: foreground areas and lines\n'
'- BG-top: water (linear and areal)\n'
'- BG-by-size: landcover areas sorted by their size\n'
)

# =============================================================================
# PRIORITY RANGE DESCRIPTIONS
# =============================================================================

COMMENT_OVERLAYS = (
'\nOverlays (icons, captions, path texts and shields) are rendered on top of all the geometry (lines, areas).\n'
"Overlays don't overlap each other, instead the ones with higher priority displace the less important ones.\n"
'Optional captions (which have an icon) are usually displayed only if there are no other overlays in their way\n'
f'(technically, max overlays priority value ({OVERLAYS_MAX_PRIORITY}) is subtracted from their priorities automatically).\n'
)

COMMENT_FG = (
'\nFG geometry: foreground lines and areas (e.g. buildings) are rendered always below overlays\n'
'and always on top of background geometry (BG-top & BG-by-size) even if a foreground feature\n'
'is layer=-10 (as tunnels should be visibile over landcover and water).\n'
)

COMMENT_BG_TOP = (
'\nBG-top geometry: background lines and areas that should be always below foreground ones\n'
'(including e.g. layer=-10 underwater tunnels), but above background areas sorted by size (BG-by-size),\n'
"because ordering by size doesn't always work with e.g. water mapped over a forest,\n"
'so water should be on top of other landcover always, but linear waterways should be hidden beneath it.\n'
'Still, e.g. a layer=-1 BG-top feature will be rendered under a layer=0 BG-by-size feature\n'
'(so areal water tunnels are hidden beneath other landcover area) and a layer=1 landcover areas\n'
'are displayed above layer=0 BG-top.\n'
)

COMMENT_BG_BY_SIZE = (
'\nBG-by-size geometry: background areas rendered below BG-top and everything else.\n'
"Smaller areas are rendered above larger ones (area's size is estimated as the size of its' bounding box).\n"
'So effectively priority values of BG-by-size areas are not used at the moment.\n'
'But we might use them later for some special cases, e.g. to determine a main area type of a multi-type feature.\n'
'Keep them in a logical importance order please.\n'
)

# =============================================================================
# PRIORITY RANGE CONFIGURATION
# =============================================================================


class PriorityRangeConfig(NamedTuple):
"""Configuration for a priority range."""
pos: int # Rendering position (higher = rendered later/on top)
base: int # Base priority offset
comment: str # Description text for the priority file


# Priority range configurations - centralized and easy to maintain
PRIORITY_RANGE_CONFIGS = {
PRIO_OVERLAYS: PriorityRangeConfig(
pos=4,
base=0,
comment=COMMENT_OVERLAYS
),
PRIO_FG: PriorityRangeConfig(
pos=3,
base=0,
comment=COMMENT_FG
),
PRIO_BG_TOP: PriorityRangeConfig(
pos=2,
base=-1000,
comment=COMMENT_BG_TOP
),
PRIO_BG_BY_SIZE: PriorityRangeConfig(
pos=1,
base=-2000,
comment=COMMENT_BG_BY_SIZE
),
}


def get_prio_ranges() -> Dict[str, Dict[str, Any]]:
"""
Initialize and return priority ranges with their configurations.

Returns:
Dictionary mapping priority range names to their configuration dicts.
Each configuration contains:
- pos: Rendering position (higher = on top)
- base: Base priority offset
- comment: Description for priority file
- priorities: Dictionary of (type, object_id) -> priority mappings (empty initially)
"""
prio_ranges = {}

for range_name, config in PRIORITY_RANGE_CONFIGS.items():
prio_ranges[range_name] = {
'pos': config.pos,
'base': config.base,
'comment': config.comment,
'priorities': {} # Will be populated when loading priority files
}

return prio_ranges

Loading