Skip to content

Commit 1aff431

Browse files
author
Allison Zheng
authored
estimate-area command (#104)
* added estimate-area command * added input tests * tested out some feature input scenarios * added 1cm flag * edited some error messages for more clarity * update Changelog and init for version 1.5.0 * moved over some tests and removed an exception caught * fixed formatting for precision-testing.ldgeosjon * updated min version for mercantile + supermercado * edit 1cm error msg
1 parent 362ae5e commit 1aff431

File tree

10 files changed

+676
-1
lines changed

10 files changed

+676
-1
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
22

3+
# 1.5.0 (2020-10-16)
4+
- Create estimate-area command
5+
36
# 1.4.3 (2020-10-08)
47
- Update Click version to 7.1.2 and fix assertions to pass all tests
58

mapbox_tilesets/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""mapbox_tilesets package"""
22

3-
__version__ = "1.4.3"
3+
__version__ = "1.5.0"

mapbox_tilesets/scripts/cli.py

100644100755
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import mapbox_tilesets
1111
from mapbox_tilesets import utils, errors
12+
from supermercado.super_utils import filter_features
13+
import builtins
1214

1315

1416
@click.version_option(version=mapbox_tilesets.__version__, message="%(version)s")
@@ -703,3 +705,70 @@ def list_sources(username, token=None):
703705
click.echo(source["id"])
704706
else:
705707
raise errors.TilesetsError(r.text)
708+
709+
710+
@cli.command("estimate-area")
711+
@cligj.features_in_arg
712+
@click.option(
713+
"--precision",
714+
"-p",
715+
required=True,
716+
type=click.Choice(["10m", "1m", "30cm", "1cm"]),
717+
help="Precision level",
718+
)
719+
@click.option(
720+
"--no-validation",
721+
required=False,
722+
is_flag=True,
723+
help="Bypass source file validation",
724+
)
725+
@click.option(
726+
"--force-1cm",
727+
required=False,
728+
is_flag=True,
729+
help="Enables 1cm precision",
730+
)
731+
def estimate_area(features, precision, no_validation=False, force_1cm=False):
732+
"""Estimate area of features with a precision level.
733+
734+
tilesets estimate-area <features> <precision>
735+
736+
features must be a list of paths to local files containing GeoJSON feature collections or feature sequences from argument or stdin, or a list of string-encoded coordinate pairs of the form "[lng, lat]", or "lng, lat", or "lng lat".
737+
"""
738+
area = 0
739+
if precision == "1cm" and not force_1cm:
740+
raise errors.TilesetsError(
741+
"The --force-1cm flag must be present to enable 1cm precision area calculation and may take longer for large feature inputs or data with global extents. 1cm precision for tileset processing is only available upon request after contacting Mapbox support."
742+
)
743+
if precision != "1cm" and force_1cm:
744+
raise errors.TilesetsError(
745+
"The --force-1cm flag is enabled but the precision is not 1cm."
746+
)
747+
748+
# builtins.list because there is a list command in the cli & will thrown an error
749+
try:
750+
features = builtins.list(filter_features(features))
751+
except (ValueError, json.decoder.JSONDecodeError):
752+
raise errors.TilesetsError(
753+
"Error with feature parsing. Ensure that feature inputs are valid and formatted correctly. Try 'tilesets estimate-area --help' for help."
754+
)
755+
except Exception:
756+
raise errors.TilesetsError("Error with feature filtering.")
757+
758+
# expect users to bypass source validation when users rerun command and their features passed validation previously
759+
if not no_validation:
760+
for feature in features:
761+
utils.validate_geojson(feature)
762+
763+
area = utils.calculate_tiles_area(features, precision)
764+
area = str(round(area))
765+
766+
click.echo(
767+
json.dumps(
768+
{
769+
"km2": area,
770+
"precision": precision,
771+
"pricing_docs": "For more information, visit https://www.mapbox.com/pricing/#tilesets",
772+
}
773+
)
774+
)

mapbox_tilesets/utils.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import os
22
import re
33

4+
import numpy as np
5+
46
from jsonschema import validate
57
from requests import Session
8+
from supermercado.burntiles import burn
69

710
import mapbox_tilesets
811

@@ -103,3 +106,106 @@ def validate_geojson(feature):
103106
}
104107

105108
return validate(instance=feature, schema=schema)
109+
110+
111+
def _convert_precision_to_zoom(precision):
112+
"""Converts precision to zoom level based on the minimum zoom
113+
114+
Parameters
115+
----------
116+
precision: string
117+
precision level
118+
119+
Returns
120+
-------
121+
zoom level
122+
123+
"""
124+
if precision == "10m":
125+
return 6
126+
elif precision == "1m":
127+
return 11
128+
elif precision == "30cm":
129+
return 14
130+
else:
131+
return 17
132+
133+
134+
def _tile2lng(tile_x, zoom):
135+
"""Returns tile longitude
136+
137+
Parameters
138+
----------
139+
tile_x: int
140+
x coordinate
141+
zoom: int
142+
zoom level
143+
144+
Returns
145+
-------
146+
longitude
147+
"""
148+
return ((tile_x / 2 ** zoom) * 360.0) - 180.0
149+
150+
151+
def _tile2lat(tile_y, zoom):
152+
"""Returns tile latitude
153+
154+
Parameters
155+
----------
156+
tile_y: int
157+
y coordinate
158+
zoom: int
159+
zoom level
160+
161+
Returns
162+
-------
163+
latitude
164+
"""
165+
n = np.pi - 2 * np.pi * tile_y / 2 ** zoom
166+
return (180.0 / np.pi) * np.arctan(0.5 * (np.exp(n) - np.exp(-n)))
167+
168+
169+
def _calculate_tile_area(tile):
170+
"""Returns tile area in square kilometers
171+
172+
Parameters
173+
----------
174+
tile: list
175+
tile in format [x,y,z]
176+
177+
Returns
178+
-------
179+
area of tile
180+
181+
"""
182+
EARTH_RADIUS = 6371.0088
183+
left = np.deg2rad(_tile2lng(tile[:, 0], tile[:, 2]))
184+
top = np.deg2rad(_tile2lat(tile[:, 1], tile[:, 2]))
185+
right = np.deg2rad(_tile2lng(tile[:, 0] + 1, tile[:, 2]))
186+
bottom = np.deg2rad(_tile2lat(tile[:, 1] + 1, tile[:, 2]))
187+
return (
188+
(np.pi / np.deg2rad(180))
189+
* EARTH_RADIUS ** 2
190+
* np.abs(np.sin(top) - np.sin(bottom))
191+
* np.abs(left - right)
192+
)
193+
194+
195+
def calculate_tiles_area(features, precision):
196+
"""Calculates the area of tiles
197+
198+
Parameters
199+
----------
200+
features: list
201+
features from GeoJSON sources and coordinates
202+
precision: string
203+
precision level
204+
205+
Returns
206+
-------
207+
total area of all tiles in square kilometers
208+
"""
209+
zoom = _convert_precision_to_zoom(precision)
210+
tiles = burn(features, zoom)
211+
return np.sum(_calculate_tile_area(tiles))

package-lock.json

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ requests==2.21.0
55
requests-toolbelt==0.9.1
66
jsonschema==3.0.1
77
jsonseq==1.0.0
8+
mercantile==1.1.6
9+
supermercado==0.2.0

setup.py

100644100755
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def read(fname):
3434
"requests-toolbelt",
3535
"jsonschema~=3.0",
3636
"jsonseq~=1.0",
37+
"mercantile~=1.1.6",
38+
"supermercado~=0.2.0",
3739
],
3840
include_package_data=True,
3941
zip_safe=False,

0 commit comments

Comments
 (0)