maps4fs 1.5.3__py3-none-any.whl → 1.5.5__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.
maps4fs/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  # pylint: disable=missing-module-docstring
2
+ from maps4fs.generator.dtm import DTMProvider
2
3
  from maps4fs.generator.game import Game
3
4
  from maps4fs.generator.map import (
4
5
  BackgroundSettings,
maps4fs/generator/dem.py CHANGED
@@ -1,19 +1,15 @@
1
1
  """This module contains DEM class for processing Digital Elevation Model data."""
2
2
 
3
- import gzip
4
- import math
5
3
  import os
6
- import shutil
7
4
 
8
5
  import cv2
9
6
  import numpy as np
10
- import rasterio # type: ignore
11
- import requests
7
+
8
+ # import rasterio # type: ignore
12
9
  from pympler import asizeof # type: ignore
13
10
 
14
11
  from maps4fs.generator.component import Component
15
-
16
- SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
12
+ from maps4fs.generator.dtm import DTMProvider
17
13
 
18
14
 
19
15
  # pylint: disable=R0903, R0902
@@ -63,6 +59,13 @@ class DEM(Component):
63
59
 
64
60
  self.auto_process = self.map.dem_settings.auto_process
65
61
 
62
+ self.dtm_provider: DTMProvider = self.map.dtm_provider( # type: ignore
63
+ coordinates=self.coordinates,
64
+ size=self.map_rotated_size,
65
+ directory=self.temp_dir,
66
+ logger=self.logger,
67
+ )
68
+
66
69
  @property
67
70
  def dem_path(self) -> str:
68
71
  """Returns path to the DEM file.
@@ -132,36 +135,29 @@ class DEM(Component):
132
135
  def process(self) -> None:
133
136
  """Reads SRTM file, crops it to map size, normalizes and blurs it,
134
137
  saves to map directory."""
135
- north, south, east, west = self.bbox
136
138
 
137
139
  dem_output_resolution = self.output_resolution
138
140
  self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
139
141
 
140
- tile_path = self._srtm_tile()
141
- if not tile_path:
142
- self.logger.warning("Tile was not downloaded, DEM file will be filled with zeros.")
142
+ try:
143
+ data = self.dtm_provider.get_numpy()
144
+ except Exception as e: # pylint: disable=W0718
145
+ self.logger.error("Failed to get DEM data from SRTM: %s.", e)
143
146
  self._save_empty_dem(dem_output_resolution)
144
147
  return
145
148
 
146
- with rasterio.open(tile_path) as src:
147
- self.logger.debug("Opened tile, shape: %s, dtype: %s.", src.shape, src.dtypes[0])
148
- window = rasterio.windows.from_bounds(west, south, east, north, src.transform)
149
- self.logger.debug(
150
- "Window parameters. Column offset: %s, row offset: %s, width: %s, height: %s.",
151
- window.col_off,
152
- window.row_off,
153
- window.width,
154
- window.height,
155
- )
156
- data = src.read(1, window=window)
149
+ if len(data.shape) != 2:
150
+ self.logger.error("DTM provider returned incorrect data: more than 1 channel.")
151
+ self._save_empty_dem(dem_output_resolution)
152
+ return
157
153
 
158
- if not data.size > 0:
159
- self.logger.warning("DEM data is empty, DEM file will be filled with zeros.")
154
+ if data.dtype not in ["int16", "uint16"]:
155
+ self.logger.error("DTM provider returned incorrect data type: %s.", data.dtype)
160
156
  self._save_empty_dem(dem_output_resolution)
161
157
  return
162
158
 
163
159
  self.logger.debug(
164
- "DEM data was read from SRTM file. Shape: %s, dtype: %s. Min: %s, max: %s.",
160
+ "DEM data was retrieved from DTM provider. Shape: %s, dtype: %s. Min: %s, max: %s.",
165
161
  data.shape,
166
162
  data.dtype,
167
163
  data.min(),
@@ -276,81 +272,6 @@ class DEM(Component):
276
272
  output_width=output_width,
277
273
  )
278
274
 
279
- def _tile_info(self, lat: float, lon: float) -> tuple[str, str]:
280
- """Returns latitude band and tile name for SRTM tile from coordinates.
281
-
282
- Arguments:
283
- lat (float): Latitude.
284
- lon (float): Longitude.
285
-
286
- Returns:
287
- tuple[str, str]: Latitude band and tile name.
288
- """
289
- tile_latitude = math.floor(lat)
290
- tile_longitude = math.floor(lon)
291
-
292
- latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
293
- if lon < 0:
294
- tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
295
- else:
296
- tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
297
-
298
- self.logger.debug(
299
- "Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
300
- )
301
- return latitude_band, tile_name
302
-
303
- def _download_tile(self) -> str | None:
304
- """Downloads SRTM tile from Amazon S3 using coordinates.
305
-
306
- Returns:
307
- str: Path to compressed tile or None if download failed.
308
- """
309
- latitude_band, tile_name = self._tile_info(*self.coordinates)
310
- compressed_file_path = os.path.join(self.gz_dir, f"{tile_name}.hgt.gz")
311
- url = SRTM.format(latitude_band=latitude_band, tile_name=tile_name)
312
- self.logger.debug("Trying to get response from %s...", url)
313
- response = requests.get(url, stream=True, timeout=10)
314
-
315
- if response.status_code == 200:
316
- self.logger.debug("Response received. Saving to %s...", compressed_file_path)
317
- with open(compressed_file_path, "wb") as f:
318
- for chunk in response.iter_content(chunk_size=8192):
319
- f.write(chunk)
320
- self.logger.debug("Compressed tile successfully downloaded.")
321
- else:
322
- self.logger.error("Response was failed with status code %s.", response.status_code)
323
- return None
324
-
325
- return compressed_file_path
326
-
327
- def _srtm_tile(self) -> str | None:
328
- """Determines SRTM tile name from coordinates downloads it if necessary, and decompresses.
329
-
330
- Returns:
331
- str: Path to decompressed tile or None if download failed.
332
- """
333
- latitude_band, tile_name = self._tile_info(*self.coordinates)
334
- self.logger.debug("SRTM tile name %s from latitude band %s.", tile_name, latitude_band)
335
-
336
- decompressed_file_path = os.path.join(self.hgt_dir, f"{tile_name}.hgt")
337
- if os.path.isfile(decompressed_file_path):
338
- self.logger.debug(
339
- "Decompressed tile already exists: %s, skipping download.",
340
- decompressed_file_path,
341
- )
342
- return decompressed_file_path
343
-
344
- compressed_file_path = self._download_tile()
345
- if not compressed_file_path:
346
- self.logger.error("Download from SRTM failed, DEM file will be filled with zeros.")
347
- return None
348
- with gzip.open(compressed_file_path, "rb") as f_in:
349
- with open(decompressed_file_path, "wb") as f_out:
350
- shutil.copyfileobj(f_in, f_out)
351
- self.logger.debug("Tile decompressed to %s.", decompressed_file_path)
352
- return decompressed_file_path
353
-
354
275
  def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
355
276
  """Saves empty DEM file filled with zeros."""
356
277
  dem_data = np.zeros(dem_output_resolution, dtype="uint16")
@@ -0,0 +1,266 @@
1
+ """This module contains the DTMProvider class and its subclasses. DTMProvider class is used to
2
+ define different providers of digital terrain models (DTM) data. Each provider has its own URL
3
+ and specific settings for downloading and processing the data."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import gzip
8
+ import math
9
+ import os
10
+ import shutil
11
+ from typing import Type
12
+
13
+ import numpy as np
14
+ import osmnx as ox # type: ignore
15
+ import rasterio # type: ignore
16
+ import requests
17
+
18
+ from maps4fs.logger import Logger
19
+
20
+
21
+ class DTMProvider:
22
+ """Base class for DTM providers."""
23
+
24
+ _code: str | None = None
25
+ _name: str | None = None
26
+ _region: str | None = None
27
+ _icon: str | None = None
28
+ _resolution: float | None = None
29
+
30
+ _url: str | None = None
31
+
32
+ def __init__(self, coordinates: tuple[float, float], size: int, directory: str, logger: Logger):
33
+ self._coordinates = coordinates
34
+ self._size = size
35
+
36
+ if not self._code:
37
+ raise ValueError("Provider code must be defined.")
38
+ self._tile_directory = os.path.join(directory, self._code)
39
+ os.makedirs(self._tile_directory, exist_ok=True)
40
+
41
+ self.logger = logger
42
+
43
+ @property
44
+ def coordinates(self) -> tuple[float, float]:
45
+ """Coordinates of the center point of the DTM data.
46
+
47
+ Returns:
48
+ tuple: Latitude and longitude of the center point.
49
+ """
50
+ return self._coordinates
51
+
52
+ @property
53
+ def size(self) -> int:
54
+ """Size of the DTM data in meters.
55
+
56
+ Returns:
57
+ int: Size of the DTM data.
58
+ """
59
+ return self._size
60
+
61
+ @property
62
+ def url(self) -> str | None:
63
+ """URL of the provider."""
64
+ return self._url
65
+
66
+ def formatted_url(self, **kwargs) -> str:
67
+ """Formatted URL of the provider."""
68
+ if not self.url:
69
+ raise ValueError("URL must be defined.")
70
+ return self.url.format(**kwargs)
71
+
72
+ @classmethod
73
+ def description(cls) -> str:
74
+ """Description of the provider.
75
+
76
+ Returns:
77
+ str: Provider description.
78
+ """
79
+ return f"{cls._icon} {cls._region} [{cls._resolution} m/px] {cls._name}"
80
+
81
+ @classmethod
82
+ def get_provider_by_code(cls, code: str) -> Type[DTMProvider] | None:
83
+ """Get a provider by its code.
84
+
85
+ Arguments:
86
+ code (str): Provider code.
87
+
88
+ Returns:
89
+ DTMProvider: Provider class or None if not found.
90
+ """
91
+ for provider in cls.__subclasses__():
92
+ if provider._code == code: # pylint: disable=W0212
93
+ return provider
94
+ return None
95
+
96
+ @classmethod
97
+ def get_provider_descriptions(cls) -> dict[str, str]:
98
+ """Get descriptions of all providers, where keys are provider codes and
99
+ values are provider descriptions.
100
+
101
+ Returns:
102
+ dict: Provider descriptions.
103
+ """
104
+ providers = {}
105
+ for provider in cls.__subclasses__():
106
+ providers[provider._code] = provider.description() # pylint: disable=W0212
107
+ return providers # type: ignore
108
+
109
+ def download_tile(self, output_path: str, **kwargs) -> bool:
110
+ """Download a tile from the provider.
111
+
112
+ Arguments:
113
+ output_path (str): Path to save the downloaded tile.
114
+
115
+ Returns:
116
+ bool: True if the tile was downloaded successfully, False otherwise.
117
+ """
118
+ url = self.formatted_url(**kwargs)
119
+ response = requests.get(url, stream=True, timeout=10)
120
+ if response.status_code == 200:
121
+ with open(output_path, "wb") as file:
122
+ for chunk in response.iter_content(chunk_size=1024):
123
+ file.write(chunk)
124
+ return True
125
+ return False
126
+
127
+ def get_or_download_tile(self, output_path: str, **kwargs) -> str | None:
128
+ """Get or download a tile from the provider.
129
+
130
+ Arguments:
131
+ output_path (str): Path to save the downloaded tile.
132
+
133
+ Returns:
134
+ str: Path to the downloaded tile or None if the tile not exists and was
135
+ not downloaded.
136
+ """
137
+ if not os.path.exists(output_path):
138
+ if not self.download_tile(output_path, **kwargs):
139
+ return None
140
+ return output_path
141
+
142
+ def get_tile_parameters(self, *args, **kwargs) -> dict:
143
+ """Get parameters for the tile, that will be used to format the URL.
144
+ Must be implemented in subclasses.
145
+
146
+ Returns:
147
+ dict: Tile parameters to format the URL.
148
+ """
149
+ raise NotImplementedError
150
+
151
+ def get_numpy(self) -> np.ndarray:
152
+ """Get numpy array of the tile.
153
+ Resulting array must be 16 bit (signed or unsigned) integer and it should be already
154
+ windowed to the bounding box of ROI. It also must have only one channel.
155
+
156
+ Returns:
157
+ np.ndarray: Numpy array of the tile.
158
+ """
159
+ raise NotImplementedError
160
+
161
+ def get_bbox(self) -> tuple[float, float, float, float]:
162
+ """Get bounding box of the tile based on the center point and size.
163
+
164
+ Returns:
165
+ tuple: Bounding box of the tile (north, south, east, west).
166
+ """
167
+ west, south, east, north = ox.utils_geo.bbox_from_point( # type: ignore
168
+ self.coordinates, dist=self.size // 2, project_utm=False
169
+ )
170
+ bbox = north, south, east, west
171
+ return bbox
172
+
173
+ def extract_roi(self, tile_path: str) -> np.ndarray:
174
+ """Extract region of interest (ROI) from the GeoTIFF file.
175
+
176
+ Arguments:
177
+ tile_path (str): Path to the GeoTIFF file.
178
+
179
+ Raises:
180
+ ValueError: If the tile does not contain any data.
181
+
182
+ Returns:
183
+ np.ndarray: Numpy array of the ROI.
184
+ """
185
+ north, south, east, west = self.get_bbox()
186
+ with rasterio.open(tile_path) as src:
187
+ self.logger.debug("Opened tile, shape: %s, dtype: %s.", src.shape, src.dtypes[0])
188
+ window = rasterio.windows.from_bounds(west, south, east, north, src.transform)
189
+ self.logger.debug(
190
+ "Window parameters. Column offset: %s, row offset: %s, width: %s, height: %s.",
191
+ window.col_off,
192
+ window.row_off,
193
+ window.width,
194
+ window.height,
195
+ )
196
+ data = src.read(1, window=window)
197
+ if not data.size > 0:
198
+ raise ValueError("No data in the tile.")
199
+
200
+ return data
201
+
202
+
203
+ class SRTM30Provider(DTMProvider):
204
+ """Provider of Shuttle Radar Topography Mission (SRTM) 30m data."""
205
+
206
+ _code = "srtm30"
207
+ _name = "SRTM 30 m"
208
+ _region = "Global"
209
+ _icon = "🌎"
210
+ _resolution = 30.0
211
+
212
+ _url = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
213
+
214
+ def __init__(self, *args, **kwargs):
215
+ super().__init__(*args, **kwargs)
216
+ self.hgt_directory = os.path.join(self._tile_directory, "hgt")
217
+ self.gz_directory = os.path.join(self._tile_directory, "gz")
218
+ os.makedirs(self.hgt_directory, exist_ok=True)
219
+ os.makedirs(self.gz_directory, exist_ok=True)
220
+
221
+ def get_tile_parameters(self, *args, **kwargs) -> dict[str, str]:
222
+ """Returns latitude band and tile name for SRTM tile from coordinates.
223
+
224
+ Arguments:
225
+ lat (float): Latitude.
226
+ lon (float): Longitude.
227
+
228
+ Returns:
229
+ dict: Tile parameters.
230
+ """
231
+ lat, lon = args
232
+
233
+ tile_latitude = math.floor(lat)
234
+ tile_longitude = math.floor(lon)
235
+
236
+ latitude_band = f"N{abs(tile_latitude):02d}" if lat >= 0 else f"S{abs(tile_latitude):02d}"
237
+ if lon < 0:
238
+ tile_name = f"{latitude_band}W{abs(tile_longitude):03d}"
239
+ else:
240
+ tile_name = f"{latitude_band}E{abs(tile_longitude):03d}"
241
+
242
+ self.logger.debug(
243
+ "Detected tile name: %s for coordinates: lat %s, lon %s.", tile_name, lat, lon
244
+ )
245
+ return {"latitude_band": latitude_band, "tile_name": tile_name}
246
+
247
+ def get_numpy(self) -> np.ndarray:
248
+ """Get numpy array of the tile.
249
+
250
+ Returns:
251
+ np.ndarray: Numpy array of the tile.
252
+ """
253
+ tile_parameters = self.get_tile_parameters(*self.coordinates)
254
+ tile_name = tile_parameters["tile_name"]
255
+ decompressed_tile_path = os.path.join(self.hgt_directory, f"{tile_name}.hgt")
256
+
257
+ if not os.path.isfile(decompressed_tile_path):
258
+ compressed_tile_path = os.path.join(self.gz_directory, f"{tile_name}.hgt.gz")
259
+ if not self.get_or_download_tile(compressed_tile_path, **tile_parameters):
260
+ raise FileNotFoundError(f"Tile {tile_name} not found.")
261
+
262
+ with gzip.open(compressed_tile_path, "rb") as f_in:
263
+ with open(decompressed_tile_path, "wb") as f_out:
264
+ shutil.copyfileobj(f_in, f_out)
265
+
266
+ return self.extract_roi(decompressed_tile_path)
maps4fs/generator/map.py CHANGED
@@ -10,6 +10,7 @@ from typing import Any, Generator
10
10
  from pydantic import BaseModel
11
11
 
12
12
  from maps4fs.generator.component import Component
13
+ from maps4fs.generator.dtm import DTMProvider
13
14
  from maps4fs.generator.game import Game
14
15
  from maps4fs.logger import Logger
15
16
 
@@ -91,8 +92,8 @@ class BackgroundSettings(SettingsModel):
91
92
  It will be used as 1 / resize_factor of the original size.
92
93
  """
93
94
 
94
- generate_background: bool = True
95
- generate_water: bool = True
95
+ generate_background: bool = False
96
+ generate_water: bool = False
96
97
  resize_factor: int = 8
97
98
 
98
99
 
@@ -174,6 +175,7 @@ class Map:
174
175
  def __init__( # pylint: disable=R0917, R0915
175
176
  self,
176
177
  game: Game,
178
+ dtm_provider: DTMProvider,
177
179
  coordinates: tuple[float, float],
178
180
  size: int,
179
181
  rotation: int,
@@ -203,6 +205,7 @@ class Map:
203
205
  self.rotated_size = int(size * rotation_multiplier)
204
206
 
205
207
  self.game = game
208
+ self.dtm_provider = dtm_provider
206
209
  self.components: list[Component] = []
207
210
  self.coordinates = coordinates
208
211
  self.map_directory = map_directory
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maps4fs
3
- Version: 1.5.3
3
+ Version: 1.5.5
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: MIT License
@@ -76,6 +76,7 @@ Requires-Dist: pydantic
76
76
  🌊 Automatically generates water planes 🆕<br>
77
77
  📈 Automatically generates splines 🆕<br>
78
78
  🛰️ Automatically downloads high resolution satellite images 🆕<br>
79
+ 🏔️ Allows to use multiple DTM providers for elevation models 🆕<br>
79
80
  🌍 Based on real-world data from OpenStreetMap<br>
80
81
  🗺️ Supports [custom OSM maps](/docs/custom_osm.md)<br>
81
82
  🏞️ Generates height map using SRTM dataset<br>
@@ -1,22 +1,23 @@
1
- maps4fs/__init__.py,sha256=LMzzORK3Q3OjXmmRJ03CpS2SMP6zTwKNnUUei3P7s40,300
1
+ maps4fs/__init__.py,sha256=wXI0wsgNpS2Pr7GKPw4akJyChQzehMXNuwjF7vG81tA,346
2
2
  maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
3
3
  maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
4
  maps4fs/generator/background.py,sha256=ySABP9HLji8R0aXi1BwjUQtP2uDqZPkrlmugowa9Gkk,22836
5
5
  maps4fs/generator/component.py,sha256=RtXruvT4Fxfr7_xo9Bi-i3IIWcPd5QQOSpYJ_cNC49o,20408
6
6
  maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
7
- maps4fs/generator/dem.py,sha256=MZf3ZjawJ977TxqB1q9nNpvPZUNwfmm2EaJDtVU-eCU,15939
7
+ maps4fs/generator/dem.py,sha256=hW9zNKX-MSUI-Cw7-x8tNMGF8NyzdVDLNjsCx4wHfuQ,12563
8
+ maps4fs/generator/dtm.py,sha256=DsQsV4t0124-2xdr8L29JX-csD0XNOdCF0MFm1cZQF0,8872
8
9
  maps4fs/generator/game.py,sha256=QHgVnyGYvEnfwGZ84-u-dpbCRr3UeVVqBbrwr5WG8dE,7992
9
10
  maps4fs/generator/grle.py,sha256=u8ZwSs313PIOkH_0B_O2tVTaZ-eYNkc30eKGtBxWzTM,17846
10
11
  maps4fs/generator/i3d.py,sha256=qeZYqfuhbhRPlSAuQHXaq6RmIO7314oMN68Ivebp1YQ,24786
11
- maps4fs/generator/map.py,sha256=-3oqAKXVD_lMkJgDrZEun9kRnvOpjoTPcd82kRajmls,13296
12
+ maps4fs/generator/map.py,sha256=wLu8kexBgvHTO_RGnUupWoD-LMsoPXjNuPivgVH3zTw,13420
12
13
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
13
14
  maps4fs/generator/satellite.py,sha256=Qnb6XxmXKnHdHKVMb9mJ3vDGtGkDHCOv_81hrrXdx3k,3660
14
15
  maps4fs/generator/texture.py,sha256=sErusfv1AqQfP-veMrZ921Tz8DnGEhfB4ucggMmKrD4,31231
15
16
  maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
16
17
  maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
17
18
  maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
18
- maps4fs-1.5.3.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
19
- maps4fs-1.5.3.dist-info/METADATA,sha256=AQDfDzRNJVcvRq68ntu5n3XuB6gWZWV2O9-AuSzntBQ,35937
20
- maps4fs-1.5.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
21
- maps4fs-1.5.3.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
22
- maps4fs-1.5.3.dist-info/RECORD,,
19
+ maps4fs-1.5.5.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
20
+ maps4fs-1.5.5.dist-info/METADATA,sha256=VyaFhbaeS2YlGLWWGeMmLK8Q9saq3p0FWYRtsgcq6TY,36012
21
+ maps4fs-1.5.5.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
22
+ maps4fs-1.5.5.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
23
+ maps4fs-1.5.5.dist-info/RECORD,,