ancient-map-tiler 0.1.0__py3-none-any.whl

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.
File without changes
@@ -0,0 +1,203 @@
1
+ import argparse
2
+ import sys
3
+ from argparse import Namespace
4
+ from math import ceil, log
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+ from PIL import Image
10
+ from tqdm import tqdm
11
+
12
+
13
+ def pad_image_to_fit_tile_size_and_make_transparent(
14
+ source: npt.NDArray, tile_size: int
15
+ ) -> npt.NDArray:
16
+ """
17
+ Pad an image to fit a tile size with transparent pixels. Make the image transparent if it was not already.
18
+ :param source: the source image
19
+ :param tile_size: the size of the tiles the image will be split into
20
+ :return: An image whose width and height are multiples of the tile size
21
+ """
22
+
23
+ height, width, _ = source.shape
24
+ missing_in_y = -height % tile_size
25
+ missing_in_x = -width % tile_size
26
+
27
+ # Make the image transparent if it was not already
28
+ if source.shape[2] == 3:
29
+ transparency_layer = np.full((height, width, 1), 255, dtype=source.dtype)
30
+ source = np.concatenate([source, transparency_layer], axis=2) # (H, W, 4)
31
+
32
+ # right padding
33
+ right_pad = np.full((height, missing_in_x, 4), [0, 0, 0, 0], dtype=source.dtype)
34
+ source = np.concatenate((source, right_pad), axis=1)
35
+
36
+ # bottom padding
37
+ bottom_pad = np.full(
38
+ (missing_in_y, width + missing_in_x, 4), [0, 0, 0, 0], dtype=source.dtype
39
+ )
40
+ return np.concatenate((source, bottom_pad), axis=0)
41
+
42
+
43
+ def get_tiles(source: npt.NDArray, tile_size: int) -> npt.NDArray:
44
+ """
45
+ Generate square tiles from an image
46
+ If the image width or height is not a multiple of tile_size the right side tiles and bottom tiles will be padded with black pixels
47
+ :param source: The source image
48
+ :param tile_size: The size of the tile in pixels
49
+ :return: An array with shape (nb_tiles_y, nb_tiles_x, tile_size, tile_size [, pixel size (3 if rgb)] )
50
+ """
51
+ source = pad_image_to_fit_tile_size_and_make_transparent(source, tile_size)
52
+ height, width, nb_colors = source.shape
53
+ nb_tiles_x = width // tile_size
54
+ nb_tiles_y = height // tile_size
55
+ return source.reshape(
56
+ (nb_tiles_y, tile_size, nb_tiles_x, tile_size, nb_colors)
57
+ ).swapaxes(1, 2)
58
+
59
+
60
+ def save_to_directory(
61
+ tiled_image: npt.NDArray,
62
+ target_directory: Path,
63
+ image_format: str,
64
+ progress_bar: tqdm | None = None,
65
+ ) -> None:
66
+ """
67
+ Given a tiled map obtained via get_tiles, save it to a directory that can later be used by mapping libraries such as leaflet
68
+ Resulting directory structure:
69
+ └── target_directory
70
+ ├── x0
71
+ │ ├── y0.png
72
+ │ └── y1.png
73
+ └── x1
74
+ ├── y0.png
75
+ └── y1.png
76
+ :param tiled_image: The tiled image to save
77
+ :param target_directory: The target directory where the tiles will be saved. Must not exist.
78
+ :param image_format: The image format of the tiles. Tested for .webp and .png
79
+ :param progress_bar: Optional, if a progress bar is given it will be ticked for each image saved
80
+ """
81
+ target_directory.mkdir()
82
+
83
+ # Tiled_image format is row, col, tile (y,x)
84
+ # Tiling system usually work with col, row, tile (x,y)
85
+ # Let's swap x and y
86
+ tiled_image_x_then_y = tiled_image.swapaxes(0, 1)
87
+ for x_index, column_of_tiles in enumerate(tiled_image_x_then_y):
88
+ subdirectory = target_directory / str(x_index)
89
+ subdirectory.mkdir()
90
+ for y_index, tile in enumerate(column_of_tiles):
91
+ Image.fromarray(tile).save(subdirectory / f"{y_index}{image_format}")
92
+ if progress_bar:
93
+ progress_bar.update(1)
94
+
95
+
96
+ def get_maximum_zoom_level(source_map_shape: tuple, tile_size: int) -> int:
97
+ """
98
+ Compute the maximum zoom level to use with a given map
99
+ Knowing that the maximum zoom level should contain the map in its original size
100
+ :param source_map_shape: the shape of the source map
101
+ :param tile_size: the tile size
102
+ """
103
+ # At zoom level z, the world is split into 2^z columns (x = 0 … 2^z - 1) and 2^z rows (y = 0 … 2^z - 1).
104
+ # If the map (in full size) should be split into only 1 column and 1 row because the tile size is lower than the original then the max zoom level should be 0
105
+ # It it should be split into 2 columns or 2 rows then the max zoom level should be 1
106
+ # And so on
107
+ largest_side_length_in_px = max(source_map_shape)
108
+ nb_tiles_max = ceil(largest_side_length_in_px / tile_size)
109
+ return ceil(log(nb_tiles_max) / log(2))
110
+
111
+
112
+ def resize_image_for_zoom_level(
113
+ source_map: npt.NDArray, zoom_level: int, maximum_zoom_level: int
114
+ ) -> npt.NDArray:
115
+ """
116
+ Resize an image for a zoom level, assuming the maximum zoom level is the zoom at which the image will not be resized
117
+ At maximum_zoom_level - 1, each image side length in pixels will be divided by 2
118
+ At maximum_zoom_level -2, each image side will be divided by 4
119
+ And so on
120
+ :param source_map: the source map image
121
+ :param zoom_level: the zoom level the image must be resized to
122
+ :param maximum_zoom_level: The maximum_zoom_level for the map
123
+ :return: the image resized to match the zoom level
124
+ """
125
+ resize_factor = 2 ** (maximum_zoom_level - zoom_level)
126
+ if resize_factor == 1:
127
+ return source_map
128
+
129
+ original_height, original_width, _ = source_map.shape
130
+ target_height = original_height // resize_factor
131
+ target_width = original_width // resize_factor
132
+ return np.asarray(
133
+ Image.fromarray(source_map).resize((target_width, target_height)), dtype="uint8"
134
+ )
135
+
136
+
137
+ def get_tiles_for_all_zoom_levels(
138
+ source_map: npt.NDArray, tile_size: int
139
+ ) -> list[npt.NDArray]:
140
+ """
141
+ Generate the tiles for a source map for all zoom levels
142
+ :param source_map: The map we want to generate tiles from
143
+ :param tile_size: each tile size
144
+ :return: An array containing all tiles for all required zoom_levels, of shape (nb_zooms, nb_tiles_y, nb_tiles_x, tile_size, tile_size [, pixel_size (3 if RGB)]
145
+ """
146
+ maximum_zoom_level = get_maximum_zoom_level(source_map.shape, tile_size)
147
+ res = []
148
+ for zoom_level in tqdm(
149
+ range(maximum_zoom_level + 1), desc="Generating map for each zoom level"
150
+ ):
151
+ image_resized_for_zoom_level = resize_image_for_zoom_level(
152
+ source_map, zoom_level, maximum_zoom_level
153
+ )
154
+ res.append(get_tiles(image_resized_for_zoom_level, tile_size))
155
+ return res
156
+
157
+
158
+ def parse_args() -> Namespace:
159
+ parser = argparse.ArgumentParser(description="Create map tiles from a map source.")
160
+ parser.add_argument(
161
+ "map_source",
162
+ help="Path or identifier of the map source to generate tiles from",
163
+ )
164
+ parser.add_argument(
165
+ "--tile-size", "-t", help="Size of a tile", default=256, type=int
166
+ )
167
+ parser.add_argument(
168
+ "--output-directory",
169
+ "-o",
170
+ help="Output directory where to store the tiles, must not exist",
171
+ required=True,
172
+ )
173
+ parser.add_argument(
174
+ "--image-format",
175
+ "-f",
176
+ help="Output image format. Tested for .png and .webp",
177
+ default=".webp",
178
+ )
179
+ return parser.parse_args()
180
+
181
+
182
+ def main() -> None:
183
+ args = parse_args()
184
+ map_source: str = args.map_source
185
+ map_image = np.asarray(Image.open(map_source), dtype="uint8")
186
+ tiled_maps_by_zoom_level = get_tiles_for_all_zoom_levels(map_image, args.tile_size)
187
+ Path.mkdir(args.output_directory)
188
+ nb_images_to_save = sum(
189
+ tiled_map.shape[0] * tiled_map.shape[1]
190
+ for tiled_map in tiled_maps_by_zoom_level
191
+ )
192
+ with tqdm(desc="Saving generated tiles", total=nb_images_to_save) as progress_bar:
193
+ for zoom_level, tiled_map in enumerate(tiled_maps_by_zoom_level):
194
+ save_to_directory(
195
+ tiled_map,
196
+ Path(args.output_directory) / str(zoom_level),
197
+ image_format=args.image_format,
198
+ progress_bar=progress_bar,
199
+ )
200
+
201
+
202
+ if __name__ == "__main__":
203
+ main()
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: ancient-map-tiler
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: Noan Cloarec
6
+ Author-email: noan.cloarec@gmail.com
7
+ Requires-Python: >=3.13
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Requires-Dist: numpy (>=2.3.5,<3.0.0)
12
+ Requires-Dist: tqdm (>=4.67.1,<5.0.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Ancient-map-tiler
16
+ This python project aims to provide tiles data for a web view of an ancient map
17
+ The source image of the map may be very large. To be usable in a web view such as Leaflet it must be splitted into tiles at different zooms
18
+ ## Prerequisites
19
+ - python (tested on 3.13)
20
+ - poetry
21
+ ## Usage
22
+ ```shell
23
+ cd ancient-map-tiler
24
+ poetry install
25
+ # Download a large map from wikipedia
26
+ wget https://upload.wikimedia.org/wikipedia/commons/5/50/TabulaPeutingeriana.jpg
27
+ # Generate the tiles into a directory named tiles
28
+ poetry run make_tiles TabulaPeutingeriana.jpg -o tiles
29
+ # Open the preview in your web browser
30
+ open simple_map_preview.html
31
+ ```
32
+
@@ -0,0 +1,6 @@
1
+ ancient_map_tiler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ ancient_map_tiler/tiles.py,sha256=cPpE4PTycdRSmVT-scLilc4g-Xz6XEJat3c7xxM-ah8,7889
3
+ ancient_map_tiler-0.1.0.dist-info/METADATA,sha256=IxFxtnnUuTtwyAPf0kU_B4nTkTWaIjJbJvGoRKlDL1U,1065
4
+ ancient_map_tiler-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
5
+ ancient_map_tiler-0.1.0.dist-info/entry_points.txt,sha256=8g5jcREn9ACPtgJuvUtNSs3U1jviizdx0tBD772Brqc,59
6
+ ancient_map_tiler-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.2.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ make_tiles=ancient_map_tiler.tiles:main
3
+