mapreader 1.3.5__tar.gz → 1.3.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mapreader-1.3.5 → mapreader-1.3.6}/PKG-INFO +2 -1
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/__init__.py +25 -10
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/_version.py +3 -3
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/annotate/utils.py +4 -1
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/classifier.py +33 -33
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/load_annotations.py +13 -4
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/data_structures.py +14 -6
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/downloader.py +20 -6
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/downloader_utils.py +8 -2
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/sheet_downloader.py +13 -1
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/tile_merging.py +4 -3
- mapreader-1.3.5/mapreader/process/post_process.py → mapreader-1.3.6/mapreader/process/context_post_process.py +1 -1
- mapreader-1.3.6/mapreader/process/occlusion_analysis.py +307 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/spot_text/deepsolo_runner.py +32 -8
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/spot_text/dptext_detr_runner.py +25 -5
- mapreader-1.3.6/mapreader/spot_text/maptext_runner.py +511 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/spot_text/runner_base.py +63 -1
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/PKG-INFO +2 -1
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/SOURCES.txt +4 -5
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/requires.txt +1 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/setup.py +1 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/tests/test_geo_pipeline.py +1 -1
- mapreader-1.3.5/tests/test_annotator.py +0 -276
- mapreader-1.3.5/tests/test_post_processing.py +0 -171
- mapreader-1.3.5/tests/test_sheet_downloader.py +0 -767
- {mapreader-1.3.5 → mapreader-1.3.6}/LICENSE +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/README.md +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/__main__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/annotate/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/annotate/annotator.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/custom_models.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/classify/datasets.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/download/tile_loading.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/load/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/load/geo_utils.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/load/images.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/load/loader.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/process/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/process/process.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/spot_text/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/utils/__init__.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/utils/compute_and_save_stats.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader/utils/slice_parallel.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/dependency_links.txt +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/entry_points.txt +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/not-zip-safe +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/mapreader.egg-info/top_level.txt +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/setup.cfg +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/tests/test_import.py +0 -0
- {mapreader-1.3.5 → mapreader-1.3.6}/versioneer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mapreader
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.6
|
|
4
4
|
Summary: A computer vision pipeline for the semantic exploration of maps/images at scale
|
|
5
5
|
Home-page: https://github.com/Living-with-machines/MapReader
|
|
6
6
|
Download-URL: https://github.com/Living-with-machines/MapReader/archive/refs/heads/main.zip
|
|
@@ -58,6 +58,7 @@ Requires-Dist: geopandas<1.0.0
|
|
|
58
58
|
Requires-Dist: pyogrio>=0.7.2
|
|
59
59
|
Requires-Dist: cartopy>=0.22.0
|
|
60
60
|
Requires-Dist: joblib>=1.4.0
|
|
61
|
+
Requires-Dist: opencv-python<5.0.0.0
|
|
61
62
|
Provides-Extra: dev
|
|
62
63
|
Requires-Dist: pytest<9.0.0; extra == "dev"
|
|
63
64
|
Requires-Dist: pytest-cov<6.0.0,>=4.1.0; extra == "dev"
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
+
# download
|
|
2
|
+
from mapreader.download.sheet_downloader import SheetDownloader
|
|
3
|
+
from mapreader.download.downloader import Downloader
|
|
4
|
+
from mapreader.download.downloader_utils import create_polygon_from_latlons, create_line_from_latlons
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# load
|
|
1
8
|
from mapreader.load.images import MapImages
|
|
2
9
|
from mapreader.load.loader import loader
|
|
3
10
|
from mapreader.load.loader import load_patches
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
from mapreader.
|
|
7
|
-
from mapreader.download.downloader_utils import create_polygon_from_latlons, create_line_from_latlons
|
|
12
|
+
# annotate
|
|
13
|
+
from mapreader.annotate.annotator import Annotator
|
|
8
14
|
|
|
15
|
+
# classify
|
|
9
16
|
from mapreader.classify.load_annotations import AnnotationsLoader
|
|
10
17
|
from mapreader.classify.datasets import PatchDataset
|
|
11
18
|
from mapreader.classify.datasets import PatchContextDataset
|
|
12
19
|
from mapreader.classify.classifier import ClassifierContainer
|
|
13
20
|
from mapreader.classify import custom_models
|
|
14
21
|
|
|
22
|
+
# spot_text
|
|
15
23
|
try:
|
|
16
24
|
from mapreader.spot_text.deepsolo_runner import DeepSoloRunner
|
|
17
25
|
except ImportError:
|
|
@@ -22,19 +30,26 @@ try:
|
|
|
22
30
|
except ImportError:
|
|
23
31
|
pass
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
try:
|
|
34
|
+
from mapreader.spot_text.maptext_runner import MapTextRunner
|
|
35
|
+
except ImportError:
|
|
36
|
+
pass
|
|
28
37
|
|
|
29
|
-
from . import _version
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
# post process
|
|
40
|
+
from mapreader.process.context_post_process import ContextPostProcessor
|
|
41
|
+
from mapreader.process.occlusion_analysis import OcclusionAnalyzer
|
|
32
42
|
|
|
43
|
+
# utils
|
|
33
44
|
from mapreader.load import geo_utils
|
|
34
45
|
|
|
35
|
-
import mapreader
|
|
36
46
|
|
|
47
|
+
# version
|
|
48
|
+
from . import _version
|
|
49
|
+
__version__ = _version.get_versions()["version"]
|
|
50
|
+
|
|
51
|
+
import mapreader
|
|
37
52
|
|
|
38
53
|
def print_version():
|
|
39
54
|
"""Print the current version of mapreader."""
|
|
40
|
-
print(mapreader.__version__)
|
|
55
|
+
print(mapreader.__version__)
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2024-08-
|
|
11
|
+
"date": "2024-08-09T14:10:51+0100",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "1.3.
|
|
14
|
+
"full-revisionid": "5ba6c479346a58dc6d937776c895b30d6c21f833",
|
|
15
|
+
"version": "1.3.6"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -310,7 +310,10 @@ def annotation_interface(
|
|
|
310
310
|
# stream=True so we don't download the whole page, only check if
|
|
311
311
|
# the page exists
|
|
312
312
|
response = requests.get(url, stream=True)
|
|
313
|
-
|
|
313
|
+
if not response.status_code < 400:
|
|
314
|
+
raise RuntimeError(
|
|
315
|
+
f"URL could not get a response: {response.status_code}"
|
|
316
|
+
)
|
|
314
317
|
print()
|
|
315
318
|
print(f"URL: {url}")
|
|
316
319
|
except:
|
|
@@ -86,8 +86,8 @@ class ClassifierContainer:
|
|
|
86
86
|
The optimizer being used for training the model.
|
|
87
87
|
scheduler : None or torch.optim.lr_scheduler._LRScheduler
|
|
88
88
|
The learning rate scheduler being used for training the model.
|
|
89
|
-
|
|
90
|
-
The
|
|
89
|
+
loss_fn : None or nn.modules.loss._Loss
|
|
90
|
+
The loss function to use for training the model.
|
|
91
91
|
metrics : dict
|
|
92
92
|
A dictionary to store the metrics computed during training.
|
|
93
93
|
last_epoch : int
|
|
@@ -159,7 +159,7 @@ class ClassifierContainer:
|
|
|
159
159
|
|
|
160
160
|
self.optimizer = None
|
|
161
161
|
self.scheduler = None
|
|
162
|
-
self.
|
|
162
|
+
self.loss_fn = None
|
|
163
163
|
|
|
164
164
|
self.metrics = {}
|
|
165
165
|
self.last_epoch = 0
|
|
@@ -399,54 +399,54 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
|
|
|
399
399
|
|
|
400
400
|
self.scheduler = scheduler
|
|
401
401
|
|
|
402
|
-
def
|
|
403
|
-
self,
|
|
402
|
+
def add_loss_fn(
|
|
403
|
+
self, loss_fn: str | nn.modules.loss._Loss | None = "cross entropy"
|
|
404
404
|
) -> None:
|
|
405
405
|
"""
|
|
406
|
-
Add a loss
|
|
406
|
+
Add a loss function to the classifier object.
|
|
407
407
|
|
|
408
408
|
Parameters
|
|
409
409
|
----------
|
|
410
|
-
|
|
411
|
-
The loss
|
|
410
|
+
loss_fn : str or torch.nn.modules.loss._Loss
|
|
411
|
+
The loss function to add to the classifier object.
|
|
412
412
|
Accepted string values are "cross entropy" or "ce" (cross-entropy), "bce" (binary cross-entropy) and "mse" (mean squared error).
|
|
413
413
|
|
|
414
414
|
Returns
|
|
415
415
|
-------
|
|
416
416
|
None
|
|
417
|
-
The function only modifies the ``
|
|
417
|
+
The function only modifies the ``loss_fn`` attribute of the
|
|
418
418
|
classifier and does not return anything.
|
|
419
419
|
"""
|
|
420
|
-
if isinstance(
|
|
421
|
-
if
|
|
422
|
-
|
|
423
|
-
elif
|
|
420
|
+
if isinstance(loss_fn, str):
|
|
421
|
+
if loss_fn in ["cross entropy", "ce", "cross_entropy", "cross-entropy"]:
|
|
422
|
+
loss_fn = nn.CrossEntropyLoss()
|
|
423
|
+
elif loss_fn in [
|
|
424
424
|
"bce",
|
|
425
425
|
"binary_cross_entropy",
|
|
426
426
|
"binary cross entropy",
|
|
427
427
|
"binary cross-entropy",
|
|
428
428
|
]:
|
|
429
|
-
|
|
430
|
-
elif
|
|
429
|
+
loss_fn = nn.BCELoss()
|
|
430
|
+
elif loss_fn in [
|
|
431
431
|
"mse",
|
|
432
432
|
"mean_square_error",
|
|
433
433
|
"mean_squared_error",
|
|
434
434
|
"mean squared error",
|
|
435
435
|
]:
|
|
436
|
-
|
|
436
|
+
loss_fn = nn.MSELoss()
|
|
437
437
|
else:
|
|
438
438
|
raise NotImplementedError(
|
|
439
|
-
'[ERROR] At present, if passing ``
|
|
439
|
+
'[ERROR] At present, if passing ``loss_fn`` as a string, the loss function can only be "cross entropy" or "ce" (cross-entropy), "bce" (binary cross-entropy) or "mse" (mean squared error).'
|
|
440
440
|
)
|
|
441
441
|
|
|
442
|
-
print(f'[INFO] Using "{
|
|
442
|
+
print(f'[INFO] Using "{loss_fn}" as loss function.')
|
|
443
443
|
|
|
444
|
-
elif not isinstance(
|
|
444
|
+
elif not isinstance(loss_fn, nn.modules.loss._Loss):
|
|
445
445
|
raise ValueError(
|
|
446
|
-
'[ERROR] Please pass ``
|
|
446
|
+
'[ERROR] Please pass ``loss_fn`` as a string ("cross entropy", "bce" or "mse") or torch.nn loss function (see https://pytorch.org/docs/stable/nn.html).'
|
|
447
447
|
)
|
|
448
448
|
|
|
449
|
-
self.
|
|
449
|
+
self.loss_fn = loss_fn
|
|
450
450
|
|
|
451
451
|
def model_summary(
|
|
452
452
|
self,
|
|
@@ -643,7 +643,7 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
|
|
|
643
643
|
|
|
644
644
|
def train_component_summary(self) -> None:
|
|
645
645
|
"""
|
|
646
|
-
Print a summary of the optimizer,
|
|
646
|
+
Print a summary of the optimizer, loss function, and trainable model
|
|
647
647
|
components.
|
|
648
648
|
|
|
649
649
|
Returns:
|
|
@@ -655,8 +655,8 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
|
|
|
655
655
|
print("* Optimizer:")
|
|
656
656
|
print(str(self.optimizer))
|
|
657
657
|
print(divider)
|
|
658
|
-
print("*
|
|
659
|
-
print(str(self.
|
|
658
|
+
print("* Loss function:")
|
|
659
|
+
print(str(self.loss_fn))
|
|
660
660
|
print(divider)
|
|
661
661
|
print("* Model:")
|
|
662
662
|
self.model_summary(trainable_col=True)
|
|
@@ -783,9 +783,9 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to define one." # noqa
|
|
|
783
783
|
Raises
|
|
784
784
|
------
|
|
785
785
|
ValueError
|
|
786
|
-
If the
|
|
787
|
-
:meth:`~.classify.classifier.ClassifierContainer.
|
|
788
|
-
method to set the
|
|
786
|
+
If the loss function is not set. Use the
|
|
787
|
+
:meth:`~.classify.classifier.ClassifierContainer.add_loss_fn`
|
|
788
|
+
method to set the loss function.
|
|
789
789
|
|
|
790
790
|
If the optimizer is not set and the phase is "train". Use the
|
|
791
791
|
:meth:`~.classify.classifier.ClassifierContainer.initialize_optimizer`
|
|
@@ -894,10 +894,10 @@ Use ``initialize_optimizer`` or ``add_optimizer`` to add one." # noqa
|
|
|
894
894
|
# summing the final output and the auxiliary
|
|
895
895
|
# output but in testing we only consider the
|
|
896
896
|
# final output.
|
|
897
|
-
if self.
|
|
897
|
+
if self.loss_fn is None:
|
|
898
898
|
raise ValueError(
|
|
899
|
-
"[ERROR]
|
|
900
|
-
Use ``
|
|
899
|
+
"[ERROR] Loss function is not yet defined.\n\n\
|
|
900
|
+
Use ``add_loss_fn`` to define one."
|
|
901
901
|
)
|
|
902
902
|
|
|
903
903
|
if self.is_inception and (
|
|
@@ -910,8 +910,8 @@ Use ``add_criterion`` to define one."
|
|
|
910
910
|
if not isinstance(aux_outputs, torch.Tensor):
|
|
911
911
|
aux_outputs = self._get_logits(aux_outputs)
|
|
912
912
|
|
|
913
|
-
loss1 = self.
|
|
914
|
-
loss2 = self.
|
|
913
|
+
loss1 = self.loss_fn(outputs, label_indices)
|
|
914
|
+
loss2 = self.loss_fn(aux_outputs, label_indices)
|
|
915
915
|
# https://discuss.pytorch.org/t/how-to-optimize-inception-model-with-auxiliary-classifiers/7958
|
|
916
916
|
loss = loss1 + 0.4 * loss2
|
|
917
917
|
|
|
@@ -921,7 +921,7 @@ Use ``add_criterion`` to define one."
|
|
|
921
921
|
if not isinstance(outputs, torch.Tensor):
|
|
922
922
|
outputs = self._get_logits(outputs)
|
|
923
923
|
|
|
924
|
-
loss = self.
|
|
924
|
+
loss = self.loss_fn(outputs, label_indices)
|
|
925
925
|
|
|
926
926
|
_, pred_label_indices = torch.max(outputs, dim=1)
|
|
927
927
|
|
|
@@ -451,10 +451,13 @@ Please check your image paths and update them if necessary.'
|
|
|
451
451
|
self.reviewed.loc[input_id, "label_index"] = self._get_label_index(
|
|
452
452
|
input_label
|
|
453
453
|
)
|
|
454
|
-
|
|
454
|
+
if not (
|
|
455
455
|
self.annotations[self.label_col].value_counts().tolist()
|
|
456
456
|
== self.annotations["label_index"].value_counts().tolist()
|
|
457
|
-
)
|
|
457
|
+
):
|
|
458
|
+
raise RuntimeError(
|
|
459
|
+
f"[ERROR] Label indices do not match label counts. Please check the label indices for label '{input_label}'."
|
|
460
|
+
)
|
|
458
461
|
print(
|
|
459
462
|
f'[INFO] Image {input_id} has been relabelled as "{input_label}"'
|
|
460
463
|
)
|
|
@@ -607,12 +610,18 @@ Please check your image paths and update them if necessary.'
|
|
|
607
610
|
test_size=float(relative_frac_test),
|
|
608
611
|
random_state=random_state,
|
|
609
612
|
)
|
|
610
|
-
|
|
613
|
+
if not len(self.annotations) == len(df_train) + len(df_val) + len(df_test):
|
|
614
|
+
raise ValueError(
|
|
615
|
+
"[ERROR] Number of annotations in the split dataframes does not match the number of annotations in the original dataframe."
|
|
616
|
+
)
|
|
611
617
|
|
|
612
618
|
else:
|
|
613
619
|
df_val = df_temp
|
|
614
620
|
df_test = None
|
|
615
|
-
|
|
621
|
+
if not len(self.annotations) == len(df_train) + len(df_val):
|
|
622
|
+
raise ValueError(
|
|
623
|
+
"[ERROR] Number of annotations in the split dataframes does not match the number of annotations in the original dataframe."
|
|
624
|
+
)
|
|
616
625
|
|
|
617
626
|
if context_datasets:
|
|
618
627
|
datasets = self.create_patch_context_datasets(
|
|
@@ -14,8 +14,11 @@ class Coordinate:
|
|
|
14
14
|
lon : float
|
|
15
15
|
longitude value (in range [-180°, 180°] )
|
|
16
16
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if not -90 <= lat <= 90:
|
|
18
|
+
raise ValueError("Latitude must be in range [-90, 90]")
|
|
19
|
+
if not -180 <= lon <= 180:
|
|
20
|
+
raise ValueError("Longitude must be in range [-180, 180]")
|
|
21
|
+
|
|
19
22
|
self.lat = lat
|
|
20
23
|
self.lon = lon
|
|
21
24
|
|
|
@@ -37,9 +40,12 @@ class GridIndex:
|
|
|
37
40
|
z : int
|
|
38
41
|
Zoom level
|
|
39
42
|
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
if not z >= 0:
|
|
44
|
+
raise ValueError("Zoom level must be greater than or equal to 0")
|
|
45
|
+
if not 0 <= x < 2**z:
|
|
46
|
+
raise ValueError(f"X value must be in range [0, {2**z}]")
|
|
47
|
+
if not 0 <= y < 2**z:
|
|
48
|
+
raise ValueError(f"Y value must be in range [0, {2**z}]")
|
|
43
49
|
self.x = x
|
|
44
50
|
self.y = y
|
|
45
51
|
self.z = z
|
|
@@ -61,7 +67,9 @@ class GridBoundingBox:
|
|
|
61
67
|
cell1 : GridIndex
|
|
62
68
|
cell2 : GridIndex
|
|
63
69
|
"""
|
|
64
|
-
|
|
70
|
+
if cell1.z != cell2.z:
|
|
71
|
+
raise NotImplementedError("Can't calculate a grid on different scales yet")
|
|
72
|
+
|
|
65
73
|
start_x = min(cell1.x, cell2.x)
|
|
66
74
|
end_x = max(cell1.x, cell2.x)
|
|
67
75
|
start_y = min(cell1.y, cell2.y)
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
|
+
import time
|
|
5
6
|
import urllib
|
|
6
7
|
import urllib.request
|
|
7
8
|
|
|
@@ -144,12 +145,24 @@ class Downloader:
|
|
|
144
145
|
map_name = self.merger._get_output_name(grid_bb)
|
|
145
146
|
self.downloader.download_tiles(grid_bb, download_in_parallel=False)
|
|
146
147
|
success = self.merger.merge(grid_bb, map_name)
|
|
148
|
+
|
|
147
149
|
if success:
|
|
148
150
|
print(f'[INFO] Downloaded "{map_name}.png"')
|
|
149
151
|
else:
|
|
150
152
|
print(f'[WARNING] Download of "{map_name}.png" was unsuccessful.')
|
|
151
153
|
|
|
152
|
-
|
|
154
|
+
# Try to remove the temporary folder
|
|
155
|
+
try:
|
|
156
|
+
shutil.rmtree(DEFAULT_TEMP_FOLDER)
|
|
157
|
+
except PermissionError:
|
|
158
|
+
# try again
|
|
159
|
+
time.sleep(5)
|
|
160
|
+
shutil.rmtree(DEFAULT_TEMP_FOLDER)
|
|
161
|
+
except OSError:
|
|
162
|
+
# try again
|
|
163
|
+
time.sleep(5)
|
|
164
|
+
shutil.rmtree(DEFAULT_TEMP_FOLDER)
|
|
165
|
+
|
|
153
166
|
return success
|
|
154
167
|
|
|
155
168
|
def download_map_by_polygon(
|
|
@@ -180,11 +193,12 @@ class Downloader:
|
|
|
180
193
|
Additional keyword arguments to pass to the `_download_map` method
|
|
181
194
|
"""
|
|
182
195
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
Please pass polygon as shapely.geometry.Polygon object.\n\
|
|
187
|
-
[HINT] Use ``create_polygon_from_latlons()`` to create polygon."
|
|
196
|
+
if type(polygon) is not Polygon:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
"[ERROR] \
|
|
199
|
+
Please pass polygon as shapely.geometry.Polygon object.\n\
|
|
200
|
+
[HINT] Use ``create_polygon_from_latlons()`` to create polygon."
|
|
201
|
+
)
|
|
188
202
|
|
|
189
203
|
min_x, min_y, max_x, max_y = polygon.bounds
|
|
190
204
|
|
|
@@ -155,6 +155,12 @@ def get_coordinate_from_index(grid_index: GridIndex) -> Coordinate:
|
|
|
155
155
|
return Coordinate(lat, lon)
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
def _check_z(z):
|
|
159
|
+
if not z >= 0:
|
|
160
|
+
raise ValueError("Zoom level must be positive")
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
|
|
158
164
|
def _get_index_from_coordinate(lon: float, lat: float, z: int) -> tuple[(int, int)]:
|
|
159
165
|
"""Generate (x,y) tuple from Coordinate latitudes and longitudes.
|
|
160
166
|
|
|
@@ -163,7 +169,7 @@ def _get_index_from_coordinate(lon: float, lat: float, z: int) -> tuple[(int, in
|
|
|
163
169
|
Tuple
|
|
164
170
|
(x,y) tuple.
|
|
165
171
|
"""
|
|
166
|
-
|
|
172
|
+
_check_z(z)
|
|
167
173
|
n = 2**z
|
|
168
174
|
x = int((lon + 180) / 360 * n)
|
|
169
175
|
lat_rad = math.radians(lat)
|
|
@@ -179,7 +185,7 @@ def _get_coordinate_from_index(x: int, y: int, z: int) -> tuple[(float, float)]:
|
|
|
179
185
|
Tuple
|
|
180
186
|
(lon, lat) tuple representing the upper left corner of the tile.
|
|
181
187
|
"""
|
|
182
|
-
|
|
188
|
+
_check_z(z)
|
|
183
189
|
n = 2**z
|
|
184
190
|
lon = (x / n) * 360 - 180
|
|
185
191
|
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * y / n)))
|
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import shutil
|
|
7
|
+
import time
|
|
7
8
|
import urllib
|
|
8
9
|
import urllib.request
|
|
9
10
|
from functools import reduce
|
|
@@ -622,7 +623,18 @@ class SheetDownloader:
|
|
|
622
623
|
else:
|
|
623
624
|
print(f'[WARNING] Download of "{img_path}" was unsuccessful.')
|
|
624
625
|
|
|
625
|
-
|
|
626
|
+
# Try to remove the temporary folder
|
|
627
|
+
try:
|
|
628
|
+
shutil.rmtree(DEFAULT_TEMP_FOLDER)
|
|
629
|
+
except PermissionError:
|
|
630
|
+
# try again
|
|
631
|
+
time.sleep(5)
|
|
632
|
+
shutil.rmtree(DEFAULT_TEMP_FOLDER)
|
|
633
|
+
except OSError:
|
|
634
|
+
# try again
|
|
635
|
+
time.sleep(5)
|
|
636
|
+
shutil.rmtree(DEFAULT_TEMP_FOLDER)
|
|
637
|
+
|
|
626
638
|
return img_path
|
|
627
639
|
|
|
628
640
|
def _save_metadata(
|
|
@@ -136,9 +136,10 @@ class TileMerger:
|
|
|
136
136
|
)
|
|
137
137
|
|
|
138
138
|
img_size = start_image.size
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
if not (img_size[0] == img_size[1]):
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"[ERROR] Tiles must be square: {img_size[0]}x{img_size[1]}."
|
|
142
|
+
)
|
|
142
143
|
tile_size = img_size[0]
|
|
143
144
|
return tile_size
|
|
144
145
|
|