Skip to content

Commit 5242a78

Browse files
authored
Merge pull request #28 from bengtl/tolerance-function
Tolerance simplification function
2 parents efb232f + 2102282 commit 5242a78

File tree

5 files changed

+124
-11
lines changed

5 files changed

+124
-11
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.4.1
2+
current_version = 0.4.2
33
commit = True
44
tag = False
55
parse = (?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)(\\-(?P<release>[a-z]+)(?P<dev>\\d+))?

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "microjson"
7-
version = "0.4.1"
7+
version = "0.4.2"
88
description = "MicroJSON is a library for validating, parsing, and manipulating MicroJSON data."
99
readme = "README_short.md"
1010
authors = ["Bengt Ljungquist <[email protected]>"]
@@ -28,7 +28,7 @@ Tracker = "https://github.com/polusai/microjson/issues"
2828
ipykernel = "^6.27.1"
2929

3030
[tool.poetry.dependencies]
31-
python = ">=3.9.15,<3.12"
31+
python = ">=3.9.15,<3.14"
3232
pydantic = "^2.3.0"
3333
geojson-pydantic = "^1.2.0"
3434
geojson2vt = "^1.0.1"
@@ -45,7 +45,7 @@ version = "^0.20.0"
4545
optional = true
4646

4747
[tool.poetry.dependencies.bfio]
48-
version = "2.4.5"
48+
version = "2.4.6"
4949
extras = ["all"]
5050
optional = true
5151

@@ -95,7 +95,7 @@ testpaths = ["tests/"]
9595
pythonpath = ["src/"]
9696

9797
[tool.bumpver]
98-
current_version = "0.4.1"
98+
current_version = "0.4.2"
9999
version_pattern = "MAJOR.MINOR.PATCH"
100100
commit_message = "Bump version {old_version} -> {new_version}"
101101
commit = true

src/microjson/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
from .tilemodel import TileJSON # noqa: F401
33
from .microjson2vt.microjson2vt import microjson2vt # noqa: F401
44

5-
__version__ = "0.4.1"
5+
__version__ = "0.4.2"

src/microjson/microjson2vt/microjson2vt.py

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,105 @@
55
# Modifications by PolusAI, 2024
66

77
import logging
8+
import math
89
from .convert import convert
910
from .clip import clip
1011
from .transform import transform_tile
1112
from .tile import create_tile
1213
from .simplify import simplify
1314

1415

16+
def default_tolerance_func(z, options):
17+
"""Calculates the default simplification tolerance based on zoom level."""
18+
# Ensure options exist and have defaults if necessary
19+
tolerance_val = options.get('tolerance', 50) # Use default if not present
20+
extent_val = options.get('extent', 4096) # Use default if not present
21+
denominator = (1 << z) * extent_val
22+
if denominator == 0:
23+
# Avoid division by zero, return a very small tolerance
24+
# Consider if raising an error might be better depending on context
25+
return 1e-12
26+
return (tolerance_val / denominator) ** 2
27+
28+
29+
# --- Alternative Tolerance Functions ---
30+
31+
def linear_tolerance_func(z, options):
32+
"""Linear scaling: tolerance decreases linearly with map scale."""
33+
tolerance_val = options.get('tolerance', 50)
34+
extent_val = options.get('extent', 4096)
35+
denominator = (1 << z) * extent_val
36+
if denominator == 0:
37+
return 1e-12 # Avoid division by zero
38+
# Note: No square here compared to default
39+
return tolerance_val / denominator
40+
41+
def constant_tolerance_func(z, options):
42+
"""Constant tolerance relative to extent (same simplification regardless of zoom)."""
43+
tolerance_val = options.get('tolerance', 50)
44+
extent_val = options.get('extent', 4096)
45+
if extent_val == 0:
46+
return 1e-12 # Avoid division by zero
47+
# Apply the base tolerance scaled by extent, squared like the default, but without zoom factor
48+
return (tolerance_val / extent_val) ** 2
49+
# Alternative: return a fixed value if extent scaling is not desired e.g. options.get('tolerance', 50)
50+
51+
def slow_exponential_tolerance_func(z, options, exponent=1.5):
52+
"""Slower exponential decay (exponent < 2). Tune exponent as needed."""
53+
tolerance_val = options.get('tolerance', 50)
54+
extent_val = options.get('extent', 4096)
55+
denominator = (1 << z) * extent_val
56+
if denominator == 0:
57+
return 1e-12
58+
return (tolerance_val / denominator) ** exponent
59+
60+
def logarithmic_tolerance_func(z, options):
61+
"""Logarithmic scaling: tolerance decreases slowly, especially at high zooms."""
62+
tolerance_val = options.get('tolerance', 50)
63+
extent_val = options.get('extent', 4096)
64+
# Use log(z + 2) to avoid log(0) or log(1) issues at low zooms
65+
log_factor = math.log(z + 2)
66+
if extent_val == 0 or log_factor == 0:
67+
return 1e-12 # Avoid division by zero
68+
# Example scaling - adjust as needed
69+
return tolerance_val / (log_factor * extent_val)
70+
71+
def step_tolerance_func(z, options):
72+
"""Step function: different tolerance levels for different zoom ranges."""
73+
base_tolerance = options.get('tolerance', 50)
74+
extent = options.get('extent', 4096)
75+
index_max_zoom = options.get('indexMaxZoom', 5)
76+
max_zoom = options.get('maxZoom', 8)
77+
78+
# Define zoom thresholds and corresponding multipliers
79+
if z < index_max_zoom - 1: # Low zooms (e.g., < 4 if indexMaxZoom is 5)
80+
effective_tolerance = base_tolerance * 4
81+
elif z < max_zoom - 1: # Mid zooms (e.g., 4-6 if maxZoom is 8)
82+
effective_tolerance = base_tolerance * 1.5
83+
else: # High zooms (e.g., >= 7 if maxZoom is 8)
84+
effective_tolerance = base_tolerance * 0.5
85+
86+
# Apply scaling based on extent and zoom, similar to default
87+
denominator = (1 << z) * extent
88+
if denominator == 0:
89+
return 1e-12
90+
return (effective_tolerance / denominator) ** 2
91+
# Alternative: Return a tolerance based only on the step, scaled by extent
92+
# if extent == 0: return 1e-12
93+
# return (effective_tolerance / extent) ** 2
94+
95+
96+
# --- End Alternative Tolerance Functions ---
97+
98+
AVAILABLE_TOLERANCE_FUNCTIONS = {
99+
"default": default_tolerance_func,
100+
"linear": linear_tolerance_func,
101+
"constant": constant_tolerance_func,
102+
"slow_exponential": slow_exponential_tolerance_func,
103+
"logarithmic": logarithmic_tolerance_func,
104+
"step": step_tolerance_func,
105+
}
106+
15107
def get_default_options():
16108
return {
17109
"maxZoom": 8, # max zoom to preserve detail on
@@ -24,7 +116,8 @@ def get_default_options():
24116
"promoteId": None, # name of a feature property to be promoted
25117
"generateId": False, # whether to generate feature ids.
26118
"projector": None, # which projection to use
27-
"bounds": None # [west, south, east, north]
119+
"bounds": None, # [west, south, east, north]
120+
"tolerance_function": default_tolerance_func # function to calculate tolerance per zoom
28121
}
29122

30123

@@ -46,6 +139,22 @@ def __init__(self, data, options, log_level=logging.INFO):
46139
level=log_level, format='%(asctime)s %(levelname)s %(message)s')
47140
options = self.options = extend(get_default_options(), options)
48141

142+
# Validate and resolve tolerance_function
143+
tolerance_setting = options.get('tolerance_function')
144+
if isinstance(tolerance_setting, str):
145+
if tolerance_setting in AVAILABLE_TOLERANCE_FUNCTIONS:
146+
options['tolerance_function'] = AVAILABLE_TOLERANCE_FUNCTIONS[tolerance_setting]
147+
else:
148+
raise ValueError(
149+
f"Invalid tolerance function key: '{tolerance_setting}'. "
150+
f"Available keys: {list(AVAILABLE_TOLERANCE_FUNCTIONS.keys())}"
151+
)
152+
elif not callable(tolerance_setting):
153+
raise TypeError(
154+
"Option 'tolerance_function' must be a callable function or a valid string key."
155+
)
156+
# If it's already callable, we use it directly.
157+
49158
logging.debug('preprocess data start')
50159

51160
if options.get('maxZoom') < 0 or options.get('maxZoom') > 24:
@@ -66,10 +175,12 @@ def __init__(self, data, options, log_level=logging.INFO):
66175
for feature in features:
67176
feature[f'geometry_z{z}'] = feature['geometry'].copy()
68177

178+
tolerance_func = options['tolerance_function'] # Resolved above
179+
69180
# Simplify features for each zoom level
70181
for z in range(options.get('maxZoom') + 1):
71-
tolerance = (options.get('tolerance') / ((1 << z) * options.get(
72-
'extent'))) ** 2
182+
# Calculate tolerance using the provided or default function
183+
tolerance = tolerance_func(z, options)
73184
for feature in features:
74185
geometry_key = f'geometry_z{z}'
75186
# check feature type only simplify Polygon

src/microjson/tilewriter.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ class TileWriter (TileHandler):
129129

130130
def microjson2tiles(self,
131131
microjson_data_path: Union[str, Path],
132-
validate: bool = False
132+
validate: bool = False,
133+
tolerance_key: str = "default"
133134
) -> List[str]:
134135
"""
135136
Generate tiles in form of JSON or PBF files from MicroJSON data.
@@ -238,7 +239,8 @@ def convert_id_to_int(data) -> int | dict | list:
238239
'maxZoom': maxzoom, # max zoom in the final tileset
239240
'indexMaxZoom': self.tile_json.maxzoom, # tile index max zoom
240241
'indexMaxPoints': 0, # max number of points per tile, 0 if none
241-
'bounds': self.tile_json.bounds
242+
'bounds': self.tile_json.bounds,
243+
'tolerance_function': tolerance_key # Pass the string key
242244
}
243245

244246
# Convert GeoJSON to intermediate vector tiles

0 commit comments

Comments
 (0)