maps4fs 0.7.8__py3-none-any.whl → 0.9.8__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/generator/dem.py CHANGED
@@ -13,13 +13,14 @@ import requests
13
13
  from maps4fs.generator.component import Component
14
14
 
15
15
  SRTM = "https://elevation-tiles-prod.s3.amazonaws.com/skadi/{latitude_band}/{tile_name}.hgt.gz"
16
- DEFAULT_MULTIPLIER = 1
17
- DEFAULT_BLUR_RADIUS = 21
16
+ DEFAULT_MULTIPLIER = 1.0
17
+ DEFAULT_BLUR_RADIUS = 35
18
+ DEFAULT_PLATEAU = 0
18
19
 
19
20
 
20
- # pylint: disable=R0903
21
+ # pylint: disable=R0903, R0902
21
22
  class DEM(Component):
22
- """Component for map settings and configuration.
23
+ """Component for processing Digital Elevation Model data.
23
24
 
24
25
  Args:
25
26
  coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
@@ -40,19 +41,34 @@ class DEM(Component):
40
41
 
41
42
  self.multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
42
43
  blur_radius = self.kwargs.get("blur_radius", DEFAULT_BLUR_RADIUS)
43
- if blur_radius % 2 == 0:
44
+ if blur_radius is None or blur_radius <= 0:
45
+ # We'll disable blur if the radius is 0 or negative.
46
+ blur_radius = 0
47
+ elif blur_radius % 2 == 0:
44
48
  blur_radius += 1
45
49
  self.blur_radius = blur_radius
46
50
  self.logger.debug(
47
51
  "DEM value multiplier is %s, blur radius is %s.", self.multiplier, self.blur_radius
48
52
  )
49
53
 
50
- # pylint: disable=no-member
51
- def process(self) -> None:
52
- """Reads SRTM file, crops it to map size, normalizes and blurs it,
53
- saves to map directory."""
54
- north, south, east, west = self.bbox
54
+ self.auto_process = self.kwargs.get("auto_process", False)
55
+ self.plateau = self.kwargs.get("plateau", False)
56
+
57
+ @property
58
+ def dem_path(self) -> str:
59
+ """Returns path to the DEM file.
60
+
61
+ Returns:
62
+ str: Path to the DEM file.
63
+ """
64
+ return self._dem_path
55
65
 
66
+ def get_output_resolution(self) -> tuple[int, int]:
67
+ """Get output resolution for DEM data.
68
+
69
+ Returns:
70
+ tuple[int, int]: Output resolution for DEM data.
71
+ """
56
72
  dem_height = int((self.map_height / 2) * self.game.dem_multipliyer + 1)
57
73
  dem_width = int((self.map_width / 2) * self.game.dem_multipliyer + 1)
58
74
  self.logger.debug(
@@ -61,7 +77,15 @@ class DEM(Component):
61
77
  dem_height,
62
78
  dem_width,
63
79
  )
64
- dem_output_resolution = (dem_width, dem_height)
80
+ return dem_width, dem_height
81
+
82
+ # pylint: disable=no-member
83
+ def process(self) -> None:
84
+ """Reads SRTM file, crops it to map size, normalizes and blurs it,
85
+ saves to map directory."""
86
+ north, south, east, west = self.bbox
87
+
88
+ dem_output_resolution = self.get_output_resolution()
65
89
  self.logger.debug("DEM output resolution: %s.", dem_output_resolution)
66
90
 
67
91
  tile_path = self._srtm_tile()
@@ -105,15 +129,22 @@ class DEM(Component):
105
129
  resampled_data.min(),
106
130
  )
107
131
 
108
- resampled_data = resampled_data * self.multiplier
109
- self.logger.debug(
110
- "DEM data multiplied by %s. Shape: %s, dtype: %s. Min: %s, max: %s.",
111
- self.multiplier,
112
- resampled_data.shape,
113
- resampled_data.dtype,
114
- resampled_data.min(),
115
- resampled_data.max(),
116
- )
132
+ if self.auto_process:
133
+ self.logger.debug("Auto processing is enabled, will normalize DEM data.")
134
+ resampled_data = self._normalize_dem(resampled_data)
135
+ else:
136
+ self.logger.debug("Auto processing is disabled, DEM data will not be normalized.")
137
+ resampled_data = resampled_data * self.multiplier
138
+ # Clip values to 16-bit unsigned integer range.
139
+ resampled_data = np.clip(resampled_data, 0, 65535)
140
+ resampled_data = resampled_data.astype("uint16")
141
+ self.logger.debug(
142
+ "DEM data was multiplied by %s and clipped to 16-bit unsigned integer range. "
143
+ "Min: %s, max: %s.",
144
+ self.multiplier,
145
+ resampled_data.min(),
146
+ resampled_data.max(),
147
+ )
117
148
 
118
149
  self.logger.debug(
119
150
  "DEM data was resampled. Shape: %s, dtype: %s. Min: %s, max: %s.",
@@ -123,11 +154,15 @@ class DEM(Component):
123
154
  resampled_data.max(),
124
155
  )
125
156
 
126
- resampled_data = cv2.GaussianBlur(resampled_data, (self.blur_radius, self.blur_radius), 0)
127
- self.logger.debug(
128
- "Gaussion blur applied to DEM data with kernel size %s.",
129
- self.blur_radius,
130
- )
157
+ if self.blur_radius > 0:
158
+ resampled_data = cv2.GaussianBlur(
159
+ resampled_data, (self.blur_radius, self.blur_radius), sigmaX=40, sigmaY=40
160
+ )
161
+ self.logger.debug(
162
+ "Gaussion blur applied to DEM data with kernel size %s.",
163
+ self.blur_radius,
164
+ )
165
+
131
166
  self.logger.debug(
132
167
  "DEM data was blurred. Shape: %s, dtype: %s. Min: %s, max: %s.",
133
168
  resampled_data.shape,
@@ -136,14 +171,40 @@ class DEM(Component):
136
171
  resampled_data.max(),
137
172
  )
138
173
 
174
+ if self.plateau:
175
+ # Plateau is a flat area with a constant height.
176
+ # So we just add this value to each pixel of the DEM.
177
+ # And also need to ensure that there will be no values with height greater than
178
+ # it's allowed in 16-bit unsigned integer.
179
+
180
+ resampled_data += self.plateau
181
+ resampled_data = np.clip(resampled_data, 0, 65535)
182
+
183
+ self.logger.debug(
184
+ "Plateau with height %s was added to DEM data. Min: %s, max: %s.",
185
+ self.plateau,
186
+ resampled_data.min(),
187
+ resampled_data.max(),
188
+ )
189
+
139
190
  cv2.imwrite(self._dem_path, resampled_data)
140
191
  self.logger.debug("DEM data was saved to %s.", self._dem_path)
141
192
 
142
193
  if self.game.additional_dem_name is not None:
143
- dem_directory = os.path.dirname(self._dem_path)
144
- additional_dem_path = os.path.join(dem_directory, self.game.additional_dem_name)
145
- shutil.copyfile(self._dem_path, additional_dem_path)
146
- self.logger.debug("Additional DEM data was copied to %s.", additional_dem_path)
194
+ self.make_copy(self.game.additional_dem_name)
195
+
196
+ def make_copy(self, dem_name: str) -> None:
197
+ """Copies DEM data to additional DEM file.
198
+
199
+ Args:
200
+ dem_name (str): Name of the additional DEM file.
201
+ """
202
+ dem_directory = os.path.dirname(self._dem_path)
203
+
204
+ additional_dem_path = os.path.join(dem_directory, dem_name)
205
+
206
+ shutil.copyfile(self._dem_path, additional_dem_path)
207
+ self.logger.debug("Additional DEM data was copied to %s.", additional_dem_path)
147
208
 
148
209
  def _tile_info(self, lat: float, lon: float) -> tuple[str, str]:
149
210
  """Returns latitude band and tile name for SRTM tile from coordinates.
@@ -233,14 +294,15 @@ class DEM(Component):
233
294
  Returns:
234
295
  str: Path to the preview image.
235
296
  """
236
- rgb_dem_path = self._dem_path.replace(".png", "_grayscale.png")
297
+ # rgb_dem_path = self._dem_path.replace(".png", "_grayscale.png")
298
+ grayscale_dem_path = os.path.join(self.previews_directory, "dem_grayscale.png")
237
299
 
238
- self.logger.debug("Creating grayscale preview of DEM data in %s.", rgb_dem_path)
300
+ self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
239
301
 
240
302
  dem_data = cv2.imread(self._dem_path, cv2.IMREAD_GRAYSCALE)
241
303
  dem_data_rgb = cv2.cvtColor(dem_data, cv2.COLOR_GRAY2RGB)
242
- cv2.imwrite(rgb_dem_path, dem_data_rgb)
243
- return rgb_dem_path
304
+ cv2.imwrite(grayscale_dem_path, dem_data_rgb)
305
+ return grayscale_dem_path
244
306
 
245
307
  def colored_preview(self) -> str:
246
308
  """Converts DEM image to colored RGB image and saves it to the map directory.
@@ -250,11 +312,12 @@ class DEM(Component):
250
312
  list[str]: List with a single path to the DEM file
251
313
  """
252
314
 
253
- colored_dem_path = self._dem_path.replace(".png", "_colored.png")
315
+ # colored_dem_path = self._dem_path.replace(".png", "_colored.png")
316
+ colored_dem_path = os.path.join(self.previews_directory, "dem_colored.png")
254
317
 
255
318
  self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
256
319
 
257
- dem_data = cv2.imread(self._dem_path, cv2.IMREAD_UNCHANGED)
320
+ dem_data = cv2.imread(self._dem_path, cv2.IMREAD_GRAYSCALE)
258
321
 
259
322
  self.logger.debug(
260
323
  "DEM data before normalization. Shape: %s, dtype: %s. Min: %s, max: %s.",
@@ -289,3 +352,51 @@ class DEM(Component):
289
352
  """
290
353
  self.logger.debug("Starting DEM previews generation.")
291
354
  return [self.grayscale_preview(), self.colored_preview()]
355
+
356
+ def _get_scaling_factor(self, maximum_deviation: int) -> float:
357
+ """Calculate scaling factor for DEM data normalization.
358
+ NOTE: Needs reconsideration for the implementation.
359
+
360
+ Args:
361
+ maximum_deviation (int): Maximum deviation in DEM data.
362
+
363
+ Returns:
364
+ float: Scaling factor for DEM data normalization.
365
+ """
366
+ ESTIMATED_MAXIMUM_DEVIATION = 1000 # pylint: disable=C0103
367
+ scaling_factor = maximum_deviation / ESTIMATED_MAXIMUM_DEVIATION
368
+ return scaling_factor if scaling_factor < 1 else 1
369
+
370
+ def _normalize_dem(self, data: np.ndarray) -> np.ndarray:
371
+ """Normalize DEM data to 16-bit unsigned integer using max height from settings.
372
+ Args:
373
+ data (np.ndarray): DEM data from SRTM file after cropping.
374
+ Returns:
375
+ np.ndarray: Normalized DEM data.
376
+ """
377
+ self.logger.debug("Starting DEM data normalization.")
378
+ # Calculate the difference between the maximum and minimum values in the DEM data.
379
+
380
+ max_height = data.max() # 1800
381
+ min_height = data.min() # 1700
382
+ max_dev = max_height - min_height # 100
383
+ self.logger.debug(
384
+ "Maximum deviation: %s with maximum at %s and minimum at %s.",
385
+ max_dev,
386
+ max_height,
387
+ min_height,
388
+ )
389
+
390
+ scaling_factor = self._get_scaling_factor(max_dev)
391
+ adjusted_max_height = int(65535 * scaling_factor)
392
+ self.logger.debug(
393
+ f"Maximum deviation: {max_dev}. Scaling factor: {scaling_factor}. "
394
+ f"Adjusted max height: {adjusted_max_height}."
395
+ )
396
+ normalized_data = (
397
+ (data - data.min()) / (data.max() - data.min()) * adjusted_max_height
398
+ ).astype("uint16")
399
+ self.logger.debug(
400
+ f"DEM data was normalized to {normalized_data.min()} - {normalized_data.max()}."
401
+ )
402
+ return normalized_data
maps4fs/generator/game.py CHANGED
@@ -6,8 +6,10 @@ from __future__ import annotations
6
6
 
7
7
  import os
8
8
 
9
+ from maps4fs.generator.background import Background
9
10
  from maps4fs.generator.config import Config
10
11
  from maps4fs.generator.dem import DEM
12
+ from maps4fs.generator.i3d import I3d
11
13
  from maps4fs.generator.texture import Texture
12
14
 
13
15
  working_directory = os.getcwd()
@@ -34,7 +36,7 @@ class Game:
34
36
  _map_template_path: str | None = None
35
37
  _texture_schema: str | None = None
36
38
 
37
- components = [Config, Texture, DEM]
39
+ components = [Config, Texture, DEM, I3d, Background]
38
40
 
39
41
  def __init__(self, map_template_path: str | None = None):
40
42
  if map_template_path:
@@ -113,6 +115,16 @@ class Game:
113
115
  str: The path to the weights directory."""
114
116
  raise NotImplementedError
115
117
 
118
+ def i3d_file_path(self, map_directory: str) -> str:
119
+ """Returns the path to the i3d file.
120
+
121
+ Arguments:
122
+ map_directory (str): The path to the map directory.
123
+
124
+ Returns:
125
+ str: The path to the i3d file."""
126
+ raise NotImplementedError
127
+
116
128
  @property
117
129
  def additional_dem_name(self) -> str | None:
118
130
  """Returns the name of the additional DEM file.
@@ -122,6 +134,7 @@ class Game:
122
134
  return self._additional_dem_name
123
135
 
124
136
 
137
+ # pylint: disable=W0223
125
138
  class FS22(Game):
126
139
  """Class used to define the game version FS22."""
127
140
 
@@ -167,7 +180,7 @@ class FS25(Game):
167
180
 
168
181
  Returns:
169
182
  str: The path to the DEM file."""
170
- return os.path.join(map_directory, "mapUS", "data", "dem.png")
183
+ return os.path.join(map_directory, "map", "data", "dem.png")
171
184
 
172
185
  def map_xml_path(self, map_directory: str) -> str:
173
186
  """Returns the path to the map.xml file.
@@ -178,7 +191,7 @@ class FS25(Game):
178
191
  Returns:
179
192
  str: The path to the map.xml file.
180
193
  """
181
- return os.path.join(map_directory, "mapUS", "mapUS.xml")
194
+ return os.path.join(map_directory, "map", "map.xml")
182
195
 
183
196
  def weights_dir_path(self, map_directory: str) -> str:
184
197
  """Returns the path to the weights directory.
@@ -188,4 +201,14 @@ class FS25(Game):
188
201
 
189
202
  Returns:
190
203
  str: The path to the weights directory."""
191
- return os.path.join(map_directory, "mapUS", "data")
204
+ return os.path.join(map_directory, "map", "data")
205
+
206
+ def i3d_file_path(self, map_directory: str) -> str:
207
+ """Returns the path to the i3d file.
208
+
209
+ Arguments:
210
+ map_directory (str): The path to the map directory.
211
+
212
+ Returns:
213
+ str: The path to the i3d file."""
214
+ return os.path.join(map_directory, "map", "map.i3d")
@@ -0,0 +1,89 @@
1
+ """This module contains the Config class for map settings and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from xml.etree import ElementTree as ET
7
+
8
+ from maps4fs.generator.component import Component
9
+
10
+ DEFAULT_HEIGHT_SCALE = 2000
11
+ DEFAULT_MAX_LOD_DISTANCE = 10000
12
+ DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
13
+
14
+
15
+ # pylint: disable=R0903
16
+ class I3d(Component):
17
+ """Component for map i3d file settings and configuration.
18
+
19
+ Args:
20
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
21
+ map_height (int): The height of the map in pixels.
22
+ map_width (int): The width of the map in pixels.
23
+ map_directory (str): The directory where the map files are stored.
24
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
25
+ info, warning. If not provided, default logging will be used.
26
+ """
27
+
28
+ _map_i3d_path: str | None = None
29
+
30
+ def preprocess(self) -> None:
31
+ """Gets the path to the map I3D file from the game instance and saves it to the instance
32
+ attribute. If the game does not support I3D files, the attribute is set to None."""
33
+ try:
34
+ self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
35
+ self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
36
+ except NotImplementedError:
37
+ self.logger.info("I3D file processing is not implemented for this game.")
38
+ self._map_i3d_path = None
39
+
40
+ def process(self) -> None:
41
+ """Updates the map I3D file with the default settings."""
42
+ self._update_i3d_file()
43
+
44
+ def _update_i3d_file(self) -> None:
45
+ """Updates the map I3D file with the default settings."""
46
+ if not self._map_i3d_path:
47
+ self.logger.info("I3D is not obtained, skipping the update.")
48
+ return
49
+ if not os.path.isfile(self._map_i3d_path):
50
+ self.logger.warning("I3D file not found: %s.", self._map_i3d_path)
51
+ return
52
+
53
+ tree = ET.parse(self._map_i3d_path)
54
+
55
+ self.logger.debug("Map I3D file loaded from: %s.", self._map_i3d_path)
56
+
57
+ root = tree.getroot()
58
+ for map_elem in root.iter("Scene"):
59
+ for terrain_elem in map_elem.iter("TerrainTransformGroup"):
60
+ terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
61
+ self.logger.debug(
62
+ "heightScale attribute set to %s in TerrainTransformGroup element.",
63
+ DEFAULT_HEIGHT_SCALE,
64
+ )
65
+ terrain_elem.set("maxLODDistance", str(DEFAULT_MAX_LOD_DISTANCE))
66
+ self.logger.debug(
67
+ "maxLODDistance attribute set to %s in TerrainTransformGroup element.",
68
+ DEFAULT_MAX_LOD_DISTANCE,
69
+ )
70
+
71
+ terrain_elem.set("occMaxLODDistance", str(DEFAULT_MAX_LOD_OCCLUDER_DISTANCE))
72
+ self.logger.debug(
73
+ "occMaxLODDistance attribute set to %s in TerrainTransformGroup element.",
74
+ DEFAULT_MAX_LOD_OCCLUDER_DISTANCE,
75
+ )
76
+
77
+ self.logger.debug("TerrainTransformGroup element updated in I3D file.")
78
+
79
+ tree.write(self._map_i3d_path)
80
+ self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
81
+
82
+ def previews(self) -> list[str]:
83
+ """Returns a list of paths to the preview images (empty list).
84
+ The component does not generate any preview images so it returns an empty list.
85
+
86
+ Returns:
87
+ list[str]: An empty list.
88
+ """
89
+ return []
maps4fs/generator/map.py CHANGED
@@ -1,10 +1,10 @@
1
1
  """This module contains Map class, which is used to generate map using all components."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import os
4
6
  import shutil
5
- from typing import Any
6
-
7
- from tqdm import tqdm
7
+ from typing import Any, Generator
8
8
 
9
9
  from maps4fs.generator.component import Component
10
10
  from maps4fs.generator.game import Game
@@ -42,11 +42,12 @@ class Map:
42
42
  self.map_directory = map_directory
43
43
 
44
44
  if not logger:
45
- logger = Logger(__name__, to_stdout=True, to_file=False)
45
+ logger = Logger(to_stdout=True, to_file=False)
46
46
  self.logger = logger
47
47
  self.logger.debug("Game was set to %s", game.code)
48
48
 
49
49
  self.kwargs = kwargs
50
+ self.logger.debug("Additional arguments: %s", kwargs)
50
51
 
51
52
  os.makedirs(self.map_directory, exist_ok=True)
52
53
  self.logger.debug("Map directory created: %s", self.map_directory)
@@ -57,31 +58,45 @@ class Map:
57
58
  except Exception as e:
58
59
  raise RuntimeError(f"Can not unpack map template due to error: {e}") from e
59
60
 
60
- def generate(self) -> None:
61
- """Launch map generation using all components."""
62
- with tqdm(total=len(self.game.components), desc="Generating map...") as pbar:
63
- for game_component in self.game.components:
64
- component = game_component(
65
- self.game,
66
- self.coordinates,
67
- self.height,
68
- self.width,
69
- self.map_directory,
70
- self.logger,
71
- **self.kwargs,
61
+ def generate(self) -> Generator[str, None, None]:
62
+ """Launch map generation using all components. Yield component names during the process.
63
+
64
+ Yields:
65
+ Generator[str, None, None]: Component names.
66
+ """
67
+ for game_component in self.game.components:
68
+ component = game_component(
69
+ self.game,
70
+ self.coordinates,
71
+ self.height,
72
+ self.width,
73
+ self.map_directory,
74
+ self.logger,
75
+ **self.kwargs,
76
+ )
77
+
78
+ yield component.__class__.__name__
79
+
80
+ try:
81
+ component.process()
82
+ except Exception as e: # pylint: disable=W0718
83
+ self.logger.error(
84
+ "Error processing component %s: %s",
85
+ component.__class__.__name__,
86
+ e,
87
+ )
88
+ raise e
89
+
90
+ try:
91
+ component.commit_generation_info()
92
+ except Exception as e: # pylint: disable=W0718
93
+ self.logger.error(
94
+ "Error committing generation info for component %s: %s",
95
+ component.__class__.__name__,
96
+ e,
72
97
  )
73
- try:
74
- component.process()
75
- except Exception as e: # pylint: disable=W0718
76
- self.logger.error(
77
- "Error processing component %s: %s",
78
- component.__class__.__name__,
79
- e,
80
- )
81
- raise e
82
- self.components.append(component)
83
-
84
- pbar.update(1)
98
+ raise e
99
+ self.components.append(component)
85
100
 
86
101
  def previews(self) -> list[str]:
87
102
  """Get list of preview images.
@@ -0,0 +1,83 @@
1
+ """This module contains functions and clas for generating path steps."""
2
+
3
+ from typing import NamedTuple
4
+
5
+ from geopy.distance import distance # type: ignore
6
+
7
+ DEFAULT_DISTANCE = 2048
8
+ PATH_FULL_NAME = "FULL"
9
+
10
+
11
+ class PathStep(NamedTuple):
12
+ """Represents parameters of one step in the path.
13
+
14
+ Attributes:
15
+ code {str} -- Tile code (N, NE, E, SE, S, SW, W, NW).
16
+ angle {int} -- Angle in degrees (for example 0 for North, 90 for East).
17
+ If None, the step is a full map with a center at the same coordinates as the
18
+ map itself.
19
+ distance {int} -- Distance in meters from previous step.
20
+ If None, the step is a full map with a center at the same coordinates as the
21
+ map itself.
22
+ size {tuple[int, int]} -- Size of the tile in pixels (width, height).
23
+ """
24
+
25
+ code: str
26
+ angle: int | None
27
+ distance: int | None
28
+ size: tuple[int, int]
29
+
30
+ def get_destination(self, origin: tuple[float, float]) -> tuple[float, float]:
31
+ """Calculate destination coordinates based on origin and step parameters.
32
+
33
+ Arguments:
34
+ origin {tuple[float, float]} -- Origin coordinates (latitude, longitude)
35
+
36
+ Returns:
37
+ tuple[float, float] -- Destination coordinates (latitude, longitude)
38
+ """
39
+ destination = distance(meters=self.distance).destination(origin, self.angle)
40
+ return destination.latitude, destination.longitude
41
+
42
+
43
+ def get_steps(map_height: int, map_width: int) -> list[PathStep]:
44
+ """Return a list of PathStep objects for each tile, which represent a step in the path.
45
+ Moving from the center of the map to North, then clockwise.
46
+
47
+ Arguments:
48
+ map_height {int} -- Height of the map in pixels
49
+ map_width {int} -- Width of the map in pixels
50
+
51
+ Returns:
52
+ list[PathStep] -- List of PathStep objects
53
+ """
54
+ # Move clockwise from N and calculate coordinates and sizes for each tile.
55
+ half_width = int(map_width / 2)
56
+ half_height = int(map_height / 2)
57
+
58
+ half_default_distance = int(DEFAULT_DISTANCE / 2)
59
+
60
+ return [
61
+ PathStep("N", 0, half_height + half_default_distance, (map_width, DEFAULT_DISTANCE)),
62
+ PathStep(
63
+ "NE", 90, half_width + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
64
+ ),
65
+ PathStep("E", 180, half_height + half_default_distance, (DEFAULT_DISTANCE, map_height)),
66
+ PathStep(
67
+ "SE", 180, half_height + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
68
+ ),
69
+ PathStep("S", 270, half_width + half_default_distance, (map_width, DEFAULT_DISTANCE)),
70
+ PathStep(
71
+ "SW", 270, half_width + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
72
+ ),
73
+ PathStep("W", 0, half_height + half_default_distance, (DEFAULT_DISTANCE, map_height)),
74
+ PathStep(
75
+ "NW", 0, half_height + half_default_distance, (DEFAULT_DISTANCE, DEFAULT_DISTANCE)
76
+ ),
77
+ PathStep(
78
+ PATH_FULL_NAME,
79
+ None,
80
+ None,
81
+ (map_width + DEFAULT_DISTANCE * 2, map_height + DEFAULT_DISTANCE * 2),
82
+ ),
83
+ ]