maps4fs 1.7.5__py3-none-any.whl → 1.7.7__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
@@ -3,6 +3,7 @@ from maps4fs.generator.dtm.dtm import DTMProvider
3
3
  from maps4fs.generator.dtm.srtm import SRTM30Provider, SRTM30ProviderSettings
4
4
  from maps4fs.generator.dtm.usgs import USGSProvider, USGSProviderSettings
5
5
  from maps4fs.generator.dtm.nrw import NRWProvider, NRWProviderSettings
6
+ from maps4fs.generator.dtm.bavaria import BavariaProvider, BavariaProviderSettings
6
7
  from maps4fs.generator.game import Game
7
8
  from maps4fs.generator.map import Map
8
9
  from maps4fs.generator.settings import (
@@ -0,0 +1,115 @@
1
+ """This module contains provider of Bavaria data."""
2
+
3
+ import os
4
+
5
+ from xml.etree import ElementTree as ET
6
+ import hashlib
7
+ import numpy as np
8
+ import requests
9
+ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
10
+
11
+ class BavariaProviderSettings(DTMProviderSettings):
12
+ """Settings for the Bavaria provider."""
13
+
14
+
15
+ class BavariaProvider(DTMProvider):
16
+ """Provider of Bavaria Digital terrain model (DTM) 1m data.
17
+ Data is provided by the 'Bayerische Vermessungsverwaltung' and available
18
+ at https://geodaten.bayern.de/opengeodata/OpenDataDetail.html?pn=dgm1 under CC BY 4.0 license.
19
+ """
20
+
21
+ _code = "bavaria"
22
+ _name = "Bavaria DGM1"
23
+ _region = "DE"
24
+ _icon = "🇩🇪󠁥󠁢󠁹󠁿"
25
+ _resolution = 1
26
+ _data: np.ndarray | None = None
27
+ _settings = BavariaProviderSettings
28
+ _author = "[H4rdB4se](https://github.com/H4rdB4se)"
29
+ _is_community = True
30
+ _instructions = None
31
+
32
+ def __init__(self, *args, **kwargs):
33
+ super().__init__(*args, **kwargs)
34
+ self.tiff_path = os.path.join(self._tile_directory, "tiffs")
35
+ os.makedirs(self.tiff_path, exist_ok=True)
36
+ self.meta4_path = os.path.join(self._tile_directory, "meta4")
37
+ os.makedirs(self.meta4_path, exist_ok=True)
38
+
39
+ def download_tiles(self) -> list[str]:
40
+ download_urls = self.get_meta_file_from_coords()
41
+ all_tif_files = self.download_tif_files(download_urls, self.tiff_path)
42
+ return all_tif_files
43
+
44
+ @staticmethod
45
+ def get_meta_file_name(north: float, south: float, east: float, west: float) -> str:
46
+ """Generate a hashed file name for the .meta4 file.
47
+
48
+ Arguments:
49
+ north (float): Northern latitude.
50
+ south (float): Southern latitude.
51
+ east (float): Eastern longitude.
52
+ west (float): Western longitude.
53
+
54
+ Returns:
55
+ str: Hashed file name.
56
+ """
57
+ coordinates = f"{north}_{south}_{east}_{west}"
58
+ hash_object = hashlib.md5(coordinates.encode())
59
+ hashed_file_name = "download_" + hash_object.hexdigest() + ".meta4"
60
+ return hashed_file_name
61
+
62
+ def get_meta_file_from_coords(self) -> list[str]:
63
+ """Download .meta4 (xml format) file
64
+
65
+ Returns:
66
+ list: List of download URLs.
67
+ """
68
+ (north, south, east, west) = self.get_bbox()
69
+ file_path = os.path.join(self.meta4_path, self.get_meta_file_name(north, south, east, west))
70
+ if not os.path.exists(file_path):
71
+ try:
72
+ # Make the GET request
73
+ response = requests.post(
74
+ "https://geoservices.bayern.de/services/poly2metalink/metalink/dgm1",
75
+ (f"SRID=4326;POLYGON(({west} {south},{east} {south},"
76
+ f"{east} {north},{west} {north},{west} {south}))"),
77
+ stream=True,
78
+ timeout=60
79
+ )
80
+
81
+ # Check if the request was successful (HTTP status code 200)
82
+ if response.status_code == 200:
83
+ # Write the content of the response to the file
84
+ with open(file_path, "wb") as meta_file:
85
+ for chunk in response.iter_content(chunk_size=8192): # Download in chunks
86
+ meta_file.write(chunk)
87
+ self.logger.info("File downloaded successfully: %s", file_path)
88
+ else:
89
+ self.logger.error("Download error. HTTP Status Code: %s", response.status_code)
90
+ except requests.exceptions.RequestException as e:
91
+ self.logger.error("Failed to get data. Error: %s", e)
92
+ else:
93
+ self.logger.debug("File already exists: %s", file_path)
94
+ return self.extract_urls_from_xml(file_path)
95
+
96
+ def extract_urls_from_xml(self, file_path: str) -> list[str]:
97
+ """Extract URLs from the XML file.
98
+
99
+ Arguments:
100
+ file_path (str): Path to the XML file.
101
+
102
+ Returns:
103
+ list: List of URLs.
104
+ """
105
+ urls: list[str] = []
106
+ root = ET.parse(file_path).getroot()
107
+ namespace = {'ml': 'urn:ietf:params:xml:ns:metalink'}
108
+
109
+ for file in root.findall('.//ml:file', namespace):
110
+ url = file.find('ml:url', namespace)
111
+ if url is not None:
112
+ urls.append(str(url.text))
113
+
114
+ self.logger.debug("Received %s urls", len(urls))
115
+ return urls
@@ -7,10 +7,12 @@ from __future__ import annotations
7
7
  from abc import ABC, abstractmethod
8
8
  import os
9
9
  from typing import TYPE_CHECKING, Type
10
+ from zipfile import ZipFile
10
11
 
11
12
  import numpy as np
12
13
  import osmnx as ox # type: ignore
13
14
  import rasterio # type: ignore
15
+ import requests
14
16
  from rasterio.warp import calculate_default_transform, reproject
15
17
  from rasterio.enums import Resampling
16
18
  from rasterio.merge import merge
@@ -300,7 +302,7 @@ class DTMProvider(ABC):
300
302
  self.data_info = {}
301
303
  self.add_numpy_params(data, "original")
302
304
 
303
- data = self.signed_to_unsigned(data)
305
+ data = self.ground_height_data(data)
304
306
  self.add_numpy_params(data, "grounded")
305
307
 
306
308
  original_deviation = int(self.data_info["original_deviation"])
@@ -339,7 +341,7 @@ class DTMProvider(ABC):
339
341
  "Failed to normalize DEM data. Error: %s. Using original data.", e
340
342
  )
341
343
 
342
- return data
344
+ return data.astype(np.uint16)
343
345
 
344
346
  def info_sequence(self) -> dict[str, int | str | float] | None:
345
347
  """Returns the information sequence for the component. Must be implemented in the child
@@ -364,20 +366,75 @@ class DTMProvider(ABC):
364
366
  bbox = north, south, east, west
365
367
  return bbox
366
368
 
369
+ def download_tif_files(self, urls: list[str], output_path: str) -> list[str]:
370
+ """Download GeoTIFF files from the given URLs.
371
+
372
+ Arguments:
373
+ urls (list): List of URLs to download GeoTIFF files from.
374
+ output_path (str): Path to save the downloaded GeoTIFF files.
375
+
376
+ Returns:
377
+ list: List of paths to the downloaded GeoTIFF files.
378
+ """
379
+ tif_files: list[str] = []
380
+ for url in urls:
381
+ file_name = os.path.basename(url)
382
+ self.logger.debug("Retrieving TIFF: %s", file_name)
383
+ file_path = os.path.join(output_path, file_name)
384
+ if not os.path.exists(file_path):
385
+ try:
386
+ # Send a GET request to the file URL
387
+ response = requests.get(url, stream=True, timeout=60)
388
+ response.raise_for_status() # Raise an error for HTTP status codes 4xx/5xx
389
+
390
+ # Write the content of the response to the file
391
+ with open(file_path, "wb") as file:
392
+ for chunk in response.iter_content(chunk_size=8192): # Download in chunks
393
+ file.write(chunk)
394
+ self.logger.info("File downloaded successfully: %s", file_path)
395
+ except requests.exceptions.RequestException as e:
396
+ self.logger.error("Failed to download file: %s", e)
397
+ else:
398
+ self.logger.debug("File already exists: %s", file_name)
399
+ if file_name.endswith('.zip'):
400
+ file_path = self.unzip_img_from_tif(file_name, output_path)
401
+ tif_files.append(file_path)
402
+ return tif_files
403
+
404
+ def unzip_img_from_tif(self, file_name: str, output_path: str) -> str:
405
+ """Unpacks the .img file from the zip file.
406
+
407
+ Arguments:
408
+ file_name (str): Name of the file to unzip.
409
+ output_path (str): Path to the output directory.
410
+
411
+ Returns:
412
+ str: Path to the unzipped file.
413
+ """
414
+ file_path = os.path.join(output_path, file_name)
415
+ img_file_name = file_name.replace('.zip', '.img')
416
+ img_file_path = os.path.join(output_path, img_file_name)
417
+ if not os.path.exists(img_file_path):
418
+ with ZipFile(file_path, "r") as f_in:
419
+ f_in.extract(img_file_name, output_path)
420
+ self.logger.debug("Unzipped file %s to %s", file_name, img_file_name)
421
+ else:
422
+ self.logger.debug("File already exists: %s", img_file_name)
423
+ return img_file_path
424
+
367
425
  def reproject_geotiff(self, input_tiff: str) -> str:
368
426
  """Reproject a GeoTIFF file to a new coordinate reference system (CRS).
369
427
 
370
428
  Arguments:
371
429
  input_tiff (str): Path to the input GeoTIFF file.
372
- target_crs (str): Target CRS (e.g., EPSG:4326 for CRS:84).
373
430
 
374
431
  Returns:
375
432
  str: Path to the reprojected GeoTIFF file.
376
433
  """
377
- output_tiff = os.path.join(self._tile_directory, "merged.tif")
434
+ output_tiff = os.path.join(self._tile_directory, "reprojected.tif")
378
435
 
379
436
  # Open the source GeoTIFF
380
- self.logger.debug("Reprojecting GeoTIFF to %s CRS...", "EPSG:4326")
437
+ self.logger.debug("Reprojecting GeoTIFF to EPSG:4326 CRS...")
381
438
  with rasterio.open(input_tiff) as src:
382
439
  # Get the transform, width, and height of the target CRS
383
440
  transform, width, height = calculate_default_transform(
@@ -411,7 +468,6 @@ class DTMProvider(ABC):
411
468
 
412
469
  Arguments:
413
470
  input_files (list): List of input GeoTIFF files to merge.
414
- output_file (str): Path to save the merged GeoTIFF file.
415
471
  """
416
472
  output_file = os.path.join(self._tile_directory, "merged.tif")
417
473
  # Open all input GeoTIFF files as datasets
@@ -516,11 +572,15 @@ class DTMProvider(ABC):
516
572
  )
517
573
  return normalized_data
518
574
 
519
- def signed_to_unsigned(self, data: np.ndarray, add_one: bool = True) -> np.ndarray:
520
- """Convert signed 16-bit integer to unsigned 16-bit integer.
575
+ @staticmethod
576
+ def ground_height_data(data: np.ndarray, add_one: bool = True) -> np.ndarray:
577
+ """Shift the data to ground level (0 meter).
578
+ Optionally add one meter to the data to leave some room
579
+ for the water level and pit modifications.
521
580
 
522
581
  Arguments:
523
- data (np.ndarray): DEM data from SRTM file after cropping.
582
+ data (np.ndarray): DEM data after cropping.
583
+ add_one (bool): Add one meter to the data
524
584
 
525
585
  Returns:
526
586
  np.ndarray: Unsigned DEM data.
@@ -528,7 +588,7 @@ class DTMProvider(ABC):
528
588
  data = data - data.min()
529
589
  if add_one:
530
590
  data = data + 1
531
- return data.astype(np.uint16)
591
+ return data
532
592
 
533
593
  def add_numpy_params(
534
594
  self,
@@ -1,4 +1,4 @@
1
- """This module contains provider of USGS data."""
1
+ """This module contains provider of NRW data."""
2
2
 
3
3
  import os
4
4
 
@@ -11,7 +11,7 @@ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
11
11
 
12
12
 
13
13
  class NRWProviderSettings(DTMProviderSettings):
14
- """Settings for the USGS provider."""
14
+ """Settings for the NRW provider."""
15
15
 
16
16
 
17
17
  # pylint: disable=too-many-locals
@@ -2,7 +2,6 @@
2
2
 
3
3
  import os
4
4
  from datetime import datetime
5
- from zipfile import ZipFile
6
5
 
7
6
  import numpy as np
8
7
  import requests
@@ -46,7 +45,7 @@ class USGSProvider(DTMProvider):
46
45
 
47
46
  def download_tiles(self):
48
47
  download_urls = self.get_download_urls()
49
- all_tif_files = self.download_tif_files(download_urls)
48
+ all_tif_files = self.download_tif_files(download_urls, self.shared_tiff_path)
50
49
  return all_tif_files
51
50
 
52
51
  def __init__(self, *args, **kwargs):
@@ -88,48 +87,3 @@ class USGSProvider(DTMProvider):
88
87
  self.logger.error("Failed to get data. Error: %s", e)
89
88
  self.logger.debug("Received %s urls", len(urls))
90
89
  return urls
91
-
92
- def download_tif_files(self, urls: list[str]) -> list[str]:
93
- """Download GeoTIFF files from the given URLs.
94
-
95
- Arguments:
96
- urls (list): List of URLs to download GeoTIFF files from.
97
-
98
- Returns:
99
- list: List of paths to the downloaded GeoTIFF files.
100
- """
101
- tif_files = []
102
- for url in urls:
103
- file_name = os.path.basename(url)
104
- self.logger.debug("Retrieving TIFF: %s", file_name)
105
- file_path = os.path.join(self.shared_tiff_path, file_name)
106
- if not os.path.exists(file_path):
107
- try:
108
- # Send a GET request to the file URL
109
- response = requests.get(url, stream=True) # pylint: disable=W3101
110
- response.raise_for_status() # Raise an error for HTTP status codes 4xx/5xx
111
-
112
- # Write the content of the response to the file
113
- with open(file_path, "wb") as file:
114
- for chunk in response.iter_content(chunk_size=8192): # Download in chunks
115
- file.write(chunk)
116
- self.logger.info("File downloaded successfully: %s", file_path)
117
- if file_name.endswith('.zip'):
118
- with ZipFile(file_path, "r") as f_in:
119
- f_in.extract(file_name.replace('.zip', '.img'), self.shared_tiff_path)
120
- tif_files.append(file_path.replace('.zip', '.img'))
121
- else:
122
- tif_files.append(file_path)
123
- except requests.exceptions.RequestException as e:
124
- self.logger.error("Failed to download file: %s", e)
125
- else:
126
- self.logger.debug("File already exists: %s", file_name)
127
- if file_name.endswith('.zip'):
128
- if not os.path.exists(file_path.replace('.zip', '.img')):
129
- with ZipFile(file_path, "r") as f_in:
130
- f_in.extract(file_name.replace('.zip', '.img'), self.shared_tiff_path)
131
- tif_files.append(file_path.replace('.zip', '.img'))
132
- else:
133
- tif_files.append(file_path)
134
-
135
- return tif_files
maps4fs/generator/game.py CHANGED
@@ -58,18 +58,19 @@ class Game:
58
58
  return os.path.join(map_directory, "maps", "map", "map.xml")
59
59
 
60
60
  @classmethod
61
- def from_code(cls, code: str) -> Game:
61
+ def from_code(cls, code: str, map_template_path: str | None = None) -> Game:
62
62
  """Returns the game instance based on the game code.
63
63
 
64
64
  Arguments:
65
65
  code (str): The code of the game.
66
+ map_template_path (str, optional): Path to the map template file. Defaults to None.
66
67
 
67
68
  Returns:
68
69
  Game: The game instance.
69
70
  """
70
71
  for game in cls.__subclasses__():
71
72
  if game.code and game.code.lower() == code.lower():
72
- return game()
73
+ return game(map_template_path)
73
74
  raise ValueError(f"Game with code {code} not found.")
74
75
 
75
76
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: maps4fs
3
- Version: 1.7.5
3
+ Version: 1.7.7
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
@@ -1,11 +1,11 @@
1
- maps4fs/__init__.py,sha256=TXqX7Ks_Kqt2fiXRt5zdVSLUHxP4cT_p7jgutYFdbo8,630
1
+ maps4fs/__init__.py,sha256=I-WDbZBb1l6us3QxDajPGzjVJe9PK0QrE5DqZwSsd_w,713
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=tV4UXvtkNN-OSvv6ujp4jFWRU1xGBgEvSakVGZ1H4nc,24877
5
5
  maps4fs/generator/component.py,sha256=pbpGaWy5C0UzxpcJ72HPY2gMol98snDr-bvNZSX4yY0,20823
6
6
  maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
7
7
  maps4fs/generator/dem.py,sha256=20gx0dzX0LyO6ipvDitst-BwGfcKogFqgQf9Q2qMH5U,10933
8
- maps4fs/generator/game.py,sha256=Nf5r2ubV4YVAVHGzJyhbF2GnOC0qV3HlHYIZBCWciHs,7992
8
+ maps4fs/generator/game.py,sha256=GmCl_KQ9D-UwKao4HFEb0PRAm829ThtSZfkgzK3Oh2g,8143
9
9
  maps4fs/generator/grle.py,sha256=hcbVBJ4j_Zr2QvEVo2cYNh2jARVXp_X3onifBtp9Zxs,20922
10
10
  maps4fs/generator/i3d.py,sha256=pUyHKWKcw43xVCf3Y8iabtbQba05LYxMHi8vziGksIA,24843
11
11
  maps4fs/generator/map.py,sha256=a50KQEr1XZKjS_WKXywGwh4OC3gyjY6M8FTc0eNcxpg,10183
@@ -14,16 +14,17 @@ maps4fs/generator/satellite.py,sha256=_7RcuNmR1mjxEJWMDsjnzKUIqWxnGUn50XtjB7HmSP
14
14
  maps4fs/generator/settings.py,sha256=9vbXISQrE-aDY7ATpvZ7LVJMqjfwa3-gNl-huI8XLO0,5666
15
15
  maps4fs/generator/texture.py,sha256=L_j5GTTJXbp7OCT4-TWGFcY_zyAI_fNzDFLvXYiyKPI,33921
16
16
  maps4fs/generator/dtm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- maps4fs/generator/dtm/dtm.py,sha256=T2h7eP5kQEWTGllI8ZxcGCDW4czZSoPBgOUS8a7_Ym8,19105
18
- maps4fs/generator/dtm/nrw.py,sha256=lJYZBZB4n5egUlX2iA7AhLmMIRKB7i_LVxCTohWsSmw,4612
17
+ maps4fs/generator/dtm/bavaria.py,sha256=r6-3DcKY4JeFQ-8jbf0xxtB5SPbbZc7KSGYxllqOCo4,4412
18
+ maps4fs/generator/dtm/dtm.py,sha256=DXRxpRuzy5l00yGVMkxamJE2zRygwpi6xcEe29y7LsA,21714
19
+ maps4fs/generator/dtm/nrw.py,sha256=8h33743_FvfScnABSFWgV7zPE5L1i6vL2uwpIMZey6g,4610
19
20
  maps4fs/generator/dtm/srtm.py,sha256=RsvVa7ErajPwXoetG7mO_rldji9GR97HFaazH-PkdHw,4399
20
- maps4fs/generator/dtm/usgs.py,sha256=fWFR_kO_uLVjAJAL43YFvBaHwsXWI-00jMlp23Ue7Wo,5450
21
+ maps4fs/generator/dtm/usgs.py,sha256=dyy2NT0USlRkYL2qfXQzFT_q3VfkVZUSmKBzPkDNvV4,3202
21
22
  maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
22
23
  maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
23
24
  maps4fs/toolbox/custom_osm.py,sha256=X6ZlPqiOhNjkmdD_qVroIfdOl9Rb90cDwVSLDVYgx80,1892
24
25
  maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
25
- maps4fs-1.7.5.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
26
- maps4fs-1.7.5.dist-info/METADATA,sha256=ZccKMQWPuC1-c3PWO7MZUG52LwxsrnP9_cOZ4jeTza0,40436
27
- maps4fs-1.7.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
28
- maps4fs-1.7.5.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
29
- maps4fs-1.7.5.dist-info/RECORD,,
26
+ maps4fs-1.7.7.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
27
+ maps4fs-1.7.7.dist-info/METADATA,sha256=rf4b6SCuVflM7i2t3N2FrGKvgJJetsnKIhMroi5kq0k,40436
28
+ maps4fs-1.7.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
29
+ maps4fs-1.7.7.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
30
+ maps4fs-1.7.7.dist-info/RECORD,,