maps4fs 0.9.97__tar.gz → 1.0.1__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.
Files changed (30) hide show
  1. {maps4fs-0.9.97/maps4fs.egg-info → maps4fs-1.0.1}/PKG-INFO +17 -6
  2. maps4fs-0.9.97/PKG-INFO → maps4fs-1.0.1/README.md +15 -29
  3. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/background.py +15 -113
  4. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/component.py +12 -2
  5. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/dem.py +1 -4
  6. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/game.py +2 -1
  7. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/grle.py +0 -9
  8. maps4fs-1.0.1/maps4fs/generator/i3d.py +360 -0
  9. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/texture.py +35 -6
  10. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/tile.py +3 -3
  11. maps4fs-1.0.1/maps4fs/toolbox/background.py +63 -0
  12. maps4fs-0.9.97/README.md → maps4fs-1.0.1/maps4fs.egg-info/PKG-INFO +40 -4
  13. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs.egg-info/SOURCES.txt +1 -0
  14. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs.egg-info/requires.txt +1 -1
  15. {maps4fs-0.9.97 → maps4fs-1.0.1}/pyproject.toml +2 -2
  16. maps4fs-0.9.97/maps4fs/generator/i3d.py +0 -89
  17. {maps4fs-0.9.97 → maps4fs-1.0.1}/LICENSE.md +0 -0
  18. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/__init__.py +0 -0
  19. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/__init__.py +0 -0
  20. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/config.py +0 -0
  21. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/map.py +0 -0
  22. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/path_steps.py +0 -0
  23. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/generator/qgis.py +0 -0
  24. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/logger.py +0 -0
  25. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/toolbox/__init__.py +0 -0
  26. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs/toolbox/dem.py +0 -0
  27. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs.egg-info/dependency_links.txt +0 -0
  28. {maps4fs-0.9.97 → maps4fs-1.0.1}/maps4fs.egg-info/top_level.txt +0 -0
  29. {maps4fs-0.9.97 → maps4fs-1.0.1}/setup.cfg +0 -0
  30. {maps4fs-0.9.97 → maps4fs-1.0.1}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maps4fs
3
- Version: 0.9.97
3
+ Version: 1.0.1
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
@@ -14,7 +14,7 @@ Classifier: Operating System :: OS Independent
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE.md
16
16
  Requires-Dist: opencv-python
17
- Requires-Dist: osmnx<2.0.0
17
+ Requires-Dist: osmnx>=2.0.0
18
18
  Requires-Dist: rasterio
19
19
  Requires-Dist: folium
20
20
  Requires-Dist: geopy
@@ -31,8 +31,10 @@ Requires-Dist: pympler
31
31
  <p align="center">
32
32
  <a href="#Quick-Start">Quick Start</a> •
33
33
  <a href="#Overview">Overview</a> •
34
- <a href="#How-To-Run">How-To-Run</a> •
34
+ <a href="#Step-by-step">Create a map in 10 steps</a> •
35
+ <a href="#How-To-Run">How-To-Run</a><br>
35
36
  <a href="docs/FAQ.md">FAQ</a> •
37
+ <a href="docs/map_structure.md">Map Structure</a> •
36
38
  <a href="#Modder-Toolbox">Modder Toolbox</a><br>
37
39
  <a href="#Supported-objects">Supported objects</a> •
38
40
  <a href="#Generation-info">Generation info</a> •
@@ -68,6 +70,7 @@ Requires-Dist: pympler
68
70
  🔷 Generates *.obj files for background terrain based on the real-world height map<br>
69
71
  📄 Generates scripts to download high-resolution satellite images from [QGIS](https://qgis.org/download/) in one click<br>
70
72
  🧰 Modder Toolbox to help you with various of tasks 🆕<br>
73
+ 🌾 Automatically generates fields 🆕<br>
71
74
 
72
75
  <p align="center">
73
76
  <img src="https://github.com/user-attachments/assets/cf8f5752-9c69-4018-bead-290f59ba6976"><br>
@@ -76,6 +79,8 @@ Requires-Dist: pympler
76
79
  🛰️ Realistic background terrain objects with satellite images.<br><br>
77
80
  <img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
78
81
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
82
+ <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
83
+ 🌾 Field generation with one click.<br><br>
79
84
  <img src="https://github.com/user-attachments/assets/cce45575-c917-4a1b-bdc0-6368e32ccdff"><br>
80
85
  📏 Almost any possible map sizes.
81
86
  </p>
@@ -126,6 +131,9 @@ Parameters:
126
131
  - coordinates: 45.15, 19.71
127
132
  - size: 16 x 16 km
128
133
 
134
+ ## Step by step
135
+ Don't know where to start? Don't worry, just follow this [step-by-step guide](docs/step_by_step.md) to create your first map in 10 simple steps.<br>
136
+
129
137
  ## How-To-Run
130
138
 
131
139
  You'll find detailed instructions on how to run the project below. But if you prefer video tutorials, here's one for you:
@@ -202,12 +210,15 @@ The map will be saved in the `map_directory` directory.
202
210
  ## Modder Toolbox
203
211
  The tool now has a Modder Toolbox, which is a set of tools to help you with various tasks. You can open the toolbox by switching to the `🧰 Modder Toolbox` tab in the StreamLit app.<br>
204
212
 
205
- ![Modder Toolbox](https://github.com/user-attachments/assets/18f169e9-1a5b-474c-b488-6becfffadcea)
213
+ ![Modder Toolbox](https://github.com/user-attachments/assets/dffb252f-f5c0-4021-9d45-31e5bccc0d9b)
206
214
 
207
215
  ### Tool categories
208
216
  Tools are divided into categories, which are listed below.
209
217
  #### Textures and DEM
210
- - **GeoTIFF windowing** - allows you to upload your GeoTIFF file and select the region of interest to extract it from the image.
218
+ - **GeoTIFF windowing** - allows you to upload your GeoTIFF file and select the region of interest to extract it from the image. It's useful when you have high-resolution DEM data and want to create the height map using it.
219
+
220
+ #### Background terrain
221
+ - **Convert image to obj model** - allows you to convert the image to the obj model. You can use this tool to create the background terrain for your map. It can be extremely useful if you have access to the sources of high-resolution DEM data and want to create the background terrain using it.
211
222
 
212
223
  ## Supported objects
213
224
  The project is based on the [OpenStreetMap](https://www.openstreetmap.org/) data. So, refer to [this page](https://wiki.openstreetmap.org/wiki/Map_Features) to understand the list below.
@@ -376,7 +387,7 @@ Let's have a closer look at the fields:
376
387
 
377
388
  ## Background terrain
378
389
  The tool now supports the generation of the background terrain. If you don't know what it is, here's a brief explanation. The background terrain is the world around the map. It's important to create it, because if you don't, the map will look like it's floating in the void. The background terrain is a simple plane which can (and should) be texture to look fine.<br>
379
- So, the tool generates the background terrain in the form of the 8 tiles, which surround the map. The tiles are named as the cardinal points, e.g. "N", "NE", "E" and so on. All those tiles will be saved in the `objects/tiles` directory with corresponding names: `N.obj`, `NE.obj`, `E.obj` and so on.<br>
390
+ So, the tool generates the background terrain in the form of the 8 tiles, which surround the map. The tiles are named as the cardinal points, e.g. "N", "NE", "E" and so on. All those tiles will be saved in the `background` directory with corresponding names: `N.obj`, `NE.obj`, `E.obj` and so on.<br>
380
391
  If you don't want to work with separate tiles, the tool also generates the `FULL.obj` file, which includes everything around the map and the map itself. It may be a convinient approach to work with one file, one texture and then just cut the map from it.<br>
381
392
 
382
393
  ![Complete background terrain in Blender](https://github.com/user-attachments/assets/7266b8f1-bfa2-4c14-a740-1c84b1030a66)
@@ -1,28 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: maps4fs
3
- Version: 0.9.97
4
- Summary: Generate map templates for Farming Simulator from real places.
5
- Author-email: iwatkot <iwatkot@gmail.com>
6
- License: MIT License
7
- Project-URL: Homepage, https://github.com/iwatkot/maps4fs
8
- Project-URL: Repository, https://github.com/iwatkot/maps4fs
9
- Keywords: farmingsimulator,fs,farmingsimulator22,farmingsimulator25,fs22,fs25
10
- Classifier: Programming Language :: Python :: 3.11
11
- Classifier: Programming Language :: Python :: 3.12
12
- Classifier: License :: OSI Approved :: MIT License
13
- Classifier: Operating System :: OS Independent
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE.md
16
- Requires-Dist: opencv-python
17
- Requires-Dist: osmnx<2.0.0
18
- Requires-Dist: rasterio
19
- Requires-Dist: folium
20
- Requires-Dist: geopy
21
- Requires-Dist: trimesh
22
- Requires-Dist: imageio
23
- Requires-Dist: tifffile
24
- Requires-Dist: pympler
25
-
26
1
  <div align="center" markdown>
27
2
  <a href="https://discord.gg/Sj5QKKyE42">
28
3
  <img src="https://github.com/user-attachments/assets/37043333-d6ef-4ca3-9f3c-81323d9d0b71">
@@ -31,8 +6,10 @@ Requires-Dist: pympler
31
6
  <p align="center">
32
7
  <a href="#Quick-Start">Quick Start</a> •
33
8
  <a href="#Overview">Overview</a> •
34
- <a href="#How-To-Run">How-To-Run</a> •
9
+ <a href="#Step-by-step">Create a map in 10 steps</a> •
10
+ <a href="#How-To-Run">How-To-Run</a><br>
35
11
  <a href="docs/FAQ.md">FAQ</a> •
12
+ <a href="docs/map_structure.md">Map Structure</a> •
36
13
  <a href="#Modder-Toolbox">Modder Toolbox</a><br>
37
14
  <a href="#Supported-objects">Supported objects</a> •
38
15
  <a href="#Generation-info">Generation info</a> •
@@ -68,6 +45,7 @@ Requires-Dist: pympler
68
45
  🔷 Generates *.obj files for background terrain based on the real-world height map<br>
69
46
  📄 Generates scripts to download high-resolution satellite images from [QGIS](https://qgis.org/download/) in one click<br>
70
47
  🧰 Modder Toolbox to help you with various of tasks 🆕<br>
48
+ 🌾 Automatically generates fields 🆕<br>
71
49
 
72
50
  <p align="center">
73
51
  <img src="https://github.com/user-attachments/assets/cf8f5752-9c69-4018-bead-290f59ba6976"><br>
@@ -76,6 +54,8 @@ Requires-Dist: pympler
76
54
  🛰️ Realistic background terrain objects with satellite images.<br><br>
77
55
  <img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
78
56
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
57
+ <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
58
+ 🌾 Field generation with one click.<br><br>
79
59
  <img src="https://github.com/user-attachments/assets/cce45575-c917-4a1b-bdc0-6368e32ccdff"><br>
80
60
  📏 Almost any possible map sizes.
81
61
  </p>
@@ -126,6 +106,9 @@ Parameters:
126
106
  - coordinates: 45.15, 19.71
127
107
  - size: 16 x 16 km
128
108
 
109
+ ## Step by step
110
+ Don't know where to start? Don't worry, just follow this [step-by-step guide](docs/step_by_step.md) to create your first map in 10 simple steps.<br>
111
+
129
112
  ## How-To-Run
130
113
 
131
114
  You'll find detailed instructions on how to run the project below. But if you prefer video tutorials, here's one for you:
@@ -202,12 +185,15 @@ The map will be saved in the `map_directory` directory.
202
185
  ## Modder Toolbox
203
186
  The tool now has a Modder Toolbox, which is a set of tools to help you with various tasks. You can open the toolbox by switching to the `🧰 Modder Toolbox` tab in the StreamLit app.<br>
204
187
 
205
- ![Modder Toolbox](https://github.com/user-attachments/assets/18f169e9-1a5b-474c-b488-6becfffadcea)
188
+ ![Modder Toolbox](https://github.com/user-attachments/assets/dffb252f-f5c0-4021-9d45-31e5bccc0d9b)
206
189
 
207
190
  ### Tool categories
208
191
  Tools are divided into categories, which are listed below.
209
192
  #### Textures and DEM
210
- - **GeoTIFF windowing** - allows you to upload your GeoTIFF file and select the region of interest to extract it from the image.
193
+ - **GeoTIFF windowing** - allows you to upload your GeoTIFF file and select the region of interest to extract it from the image. It's useful when you have high-resolution DEM data and want to create the height map using it.
194
+
195
+ #### Background terrain
196
+ - **Convert image to obj model** - allows you to convert the image to the obj model. You can use this tool to create the background terrain for your map. It can be extremely useful if you have access to the sources of high-resolution DEM data and want to create the background terrain using it.
211
197
 
212
198
  ## Supported objects
213
199
  The project is based on the [OpenStreetMap](https://www.openstreetmap.org/) data. So, refer to [this page](https://wiki.openstreetmap.org/wiki/Map_Features) to understand the list below.
@@ -376,7 +362,7 @@ Let's have a closer look at the fields:
376
362
 
377
363
  ## Background terrain
378
364
  The tool now supports the generation of the background terrain. If you don't know what it is, here's a brief explanation. The background terrain is the world around the map. It's important to create it, because if you don't, the map will look like it's floating in the void. The background terrain is a simple plane which can (and should) be texture to look fine.<br>
379
- So, the tool generates the background terrain in the form of the 8 tiles, which surround the map. The tiles are named as the cardinal points, e.g. "N", "NE", "E" and so on. All those tiles will be saved in the `objects/tiles` directory with corresponding names: `N.obj`, `NE.obj`, `E.obj` and so on.<br>
365
+ So, the tool generates the background terrain in the form of the 8 tiles, which surround the map. The tiles are named as the cardinal points, e.g. "N", "NE", "E" and so on. All those tiles will be saved in the `background` directory with corresponding names: `N.obj`, `NE.obj`, `E.obj` and so on.<br>
380
366
  If you don't want to work with separate tiles, the tool also generates the `FULL.obj` file, which includes everything around the map and the map itself. It may be a convinient approach to work with one file, one texture and then just cut the map from it.<br>
381
367
 
382
368
  ![Complete background terrain in Blender](https://github.com/user-attachments/assets/7266b8f1-bfa2-4c14-a740-1c84b1030a66)
@@ -15,7 +15,7 @@ from maps4fs.generator.dem import (
15
15
  DEFAULT_MULTIPLIER,
16
16
  DEFAULT_PLATEAU,
17
17
  )
18
- from maps4fs.generator.path_steps import DEFAULT_DISTANCE, PATH_FULL_NAME, get_steps
18
+ from maps4fs.generator.path_steps import PATH_FULL_NAME, get_steps
19
19
  from maps4fs.generator.tile import Tile
20
20
 
21
21
  RESIZE_FACTOR = 1 / 4
@@ -242,119 +242,21 @@ class Background(Component):
242
242
 
243
243
  self.stl_preview_path = preview_path # pylint: disable=attribute-defined-outside-init
244
244
 
245
+ # pylint: disable=no-member
245
246
  def previews(self) -> list[str]:
246
- """Generates a preview by combining all tiles into one image.
247
- NOTE: The map itself is not included in the preview, so it will be empty.
247
+ """Returns the path to the image of full tile and the path to the STL preview file.
248
248
 
249
249
  Returns:
250
- list[str] -- A list of paths to the preview images."""
251
-
252
- self.logger.info("Generating a preview image for the background DEM")
253
-
254
- image_height = self.map_height + DEFAULT_DISTANCE * 2
255
- image_width = self.map_width + DEFAULT_DISTANCE * 2
256
- self.logger.debug("Full size of the preview image: %s x %s", image_width, image_height)
257
-
258
- image = np.zeros((image_height, image_width), np.uint16) # pylint: disable=no-member
259
- self.logger.debug("Empty image created: %s", image.shape)
260
-
261
- for tile in self.tiles:
262
- # pylint: disable=no-member
263
- if tile.code == PATH_FULL_NAME:
264
- continue
265
- tile_image = cv2.imread(tile.dem_path, cv2.IMREAD_UNCHANGED)
266
-
267
- self.logger.debug(
268
- "Tile %s image shape: %s, dtype: %s, max: %s, min: %s",
269
- tile.code,
270
- tile_image.shape,
271
- tile_image.dtype,
272
- tile_image.max(),
273
- tile_image.min(),
274
- )
275
-
276
- tile_height, tile_width = tile_image.shape
277
- self.logger.debug("Tile %s size: %s x %s", tile.code, tile_width, tile_height)
278
-
279
- # Calculate the position based on the tile code
280
- if tile.code == "N":
281
- x = DEFAULT_DISTANCE
282
- y = 0
283
- elif tile.code == "NE":
284
- x = self.map_width + DEFAULT_DISTANCE
285
- y = 0
286
- elif tile.code == "E":
287
- x = self.map_width + DEFAULT_DISTANCE
288
- y = DEFAULT_DISTANCE
289
- elif tile.code == "SE":
290
- x = self.map_width + DEFAULT_DISTANCE
291
- y = self.map_height + DEFAULT_DISTANCE
292
- elif tile.code == "S":
293
- x = DEFAULT_DISTANCE
294
- y = self.map_height + DEFAULT_DISTANCE
295
- elif tile.code == "SW":
296
- x = 0
297
- y = self.map_height + DEFAULT_DISTANCE
298
- elif tile.code == "W":
299
- x = 0
300
- y = DEFAULT_DISTANCE
301
- elif tile.code == "NW":
302
- x = 0
303
- y = 0
304
-
305
- # pylint: disable=possibly-used-before-assignment
306
- x2 = x + tile_width
307
- y2 = y + tile_height
308
-
309
- self.logger.debug(
310
- "Tile %s position. X from %s to %s, Y from %s to %s", tile.code, x, x2, y, y2
250
+ list[str] -- A list of paths to the previews.
251
+ """
252
+ full_tile = next((tile for tile in self.tiles if tile.code == PATH_FULL_NAME), None)
253
+ if full_tile:
254
+ preview_path = os.path.join(self.previews_directory, "background_dem.png")
255
+ full_tile_image = cv2.imread(full_tile.dem_path, cv2.IMREAD_UNCHANGED)
256
+ full_tile_image = cv2.normalize( # type: ignore
257
+ full_tile_image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U
311
258
  )
312
-
313
- # pylint: disable=possibly-used-before-assignment
314
- image[y:y2, x:x2] = tile_image
315
-
316
- # Save image to the map directory.
317
- preview_path = os.path.join(self.previews_directory, "background_dem.png")
318
-
319
- # pylint: disable=no-member
320
- image = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U) # type: ignore
321
- image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR) # type: ignore
322
- cv2.imwrite(preview_path, image)
323
-
324
- return [preview_path, self.stl_preview_path]
325
-
326
-
327
- # Creates tiles around the map.
328
- # The one on corners 2048x2048, on sides and in the middle map_size x 2048.
329
- # So 2048 is a distance FROM the edge of the map, but the other size depends on the map size.
330
- # But for corner tiles it's always 2048.
331
-
332
- # In the beginning we have coordinates of the central point of the map and it's size.
333
- # We need to calculate the coordinates of central points all 8 tiles around the map.
334
-
335
- # Latitude is a vertical line, Longitude is a horizontal line.
336
-
337
- # 2048
338
- # | |
339
- # ____________________|_________|___
340
- # | | | |
341
- # | NW | N | NE | 2048
342
- # |_________|_________|_________|___
343
- # | | | |
344
- # | W | C | E |
345
- # |_________|_________|_________|
346
- # | | | |
347
- # | SW | S | SE |
348
- # |_________|_________|_________|
349
- #
350
- # N = C map_height / 2 + 1024; N_width = map_width; N_height = 2048
351
- # NW = N - map_width / 2 - 1024; NW_width = 2048; NW_height = 2048
352
- # and so on...
353
-
354
- # lat, lon = 45.28565000315636, 20.237121355049904
355
- # dst = 1024
356
-
357
- # # N
358
- # destination = distance(meters=dst).destination((lat, lon), 0)
359
- # lat, lon = destination.latitude, destination.longitude
360
- # print(lat, lon)
259
+ full_tile_image = cv2.cvtColor(full_tile_image, cv2.COLOR_GRAY2BGR)
260
+ cv2.imwrite(preview_path, full_tile_image)
261
+ return [preview_path, self.stl_preview_path]
262
+ return [self.stl_preview_path]
@@ -50,6 +50,7 @@ class Component:
50
50
 
51
51
  os.makedirs(self.previews_directory, exist_ok=True)
52
52
  os.makedirs(self.scripts_directory, exist_ok=True)
53
+ os.makedirs(self.info_layers_directory, exist_ok=True)
53
54
 
54
55
  self.save_bbox()
55
56
  self.preprocess()
@@ -87,6 +88,15 @@ class Component:
87
88
  """
88
89
  return os.path.join(self.map_directory, "previews")
89
90
 
91
+ @property
92
+ def info_layers_directory(self) -> str:
93
+ """The directory where the info layers are stored.
94
+
95
+ Returns:
96
+ str: The directory where the info layers are stored.
97
+ """
98
+ return os.path.join(self.map_directory, "info_layers")
99
+
90
100
  @property
91
101
  def scripts_directory(self) -> str:
92
102
  """The directory where the scripts are stored.
@@ -174,10 +184,10 @@ class Component:
174
184
  height_distance = height_distance or int(self.map_height / 2)
175
185
  width_distance = width_distance or int(self.map_width / 2)
176
186
 
177
- north, south, _, _ = ox.utils_geo.bbox_from_point(
187
+ west, south, _, _ = ox.utils_geo.bbox_from_point( # type: ignore
178
188
  coordinates, dist=height_distance, project_utm=project_utm
179
189
  )
180
- _, _, east, west = ox.utils_geo.bbox_from_point(
190
+ _, _, east, north = ox.utils_geo.bbox_from_point( # type: ignore
181
191
  coordinates, dist=width_distance, project_utm=project_utm
182
192
  )
183
193
  bbox = north, south, east, west
@@ -319,7 +319,7 @@ class DEM(Component):
319
319
  def _save_empty_dem(self, dem_output_resolution: tuple[int, int]) -> None:
320
320
  """Saves empty DEM file filled with zeros."""
321
321
  dem_data = np.zeros(dem_output_resolution, dtype="uint16")
322
- cv2.imwrite(self._dem_path, dem_data) # pylint: disable=no-member
322
+ cv2.imwrite(self._dem_path, dem_data)
323
323
  self.logger.warning("DEM data filled with zeros and saved to %s.", self._dem_path)
324
324
 
325
325
  def grayscale_preview(self) -> str:
@@ -329,7 +329,6 @@ class DEM(Component):
329
329
  Returns:
330
330
  str: Path to the preview image.
331
331
  """
332
- # rgb_dem_path = self._dem_path.replace(".png", "_grayscale.png")
333
332
  grayscale_dem_path = os.path.join(self.previews_directory, "dem_grayscale.png")
334
333
 
335
334
  self.logger.debug("Creating grayscale preview of DEM data in %s.", grayscale_dem_path)
@@ -346,8 +345,6 @@ class DEM(Component):
346
345
  Returns:
347
346
  list[str]: List with a single path to the DEM file
348
347
  """
349
-
350
- # colored_dem_path = self._dem_path.replace(".png", "_colored.png")
351
348
  colored_dem_path = os.path.join(self.previews_directory, "dem_colored.png")
352
349
 
353
350
  self.logger.debug("Creating colored preview of DEM data in %s.", colored_dem_path)
@@ -38,7 +38,8 @@ class Game:
38
38
  _texture_schema: str | None = None
39
39
  _grle_schema: str | None = None
40
40
 
41
- components = [Config, Texture, GRLE, DEM, I3d, Background]
41
+ # Order matters! Some components depend on others.
42
+ components = [Texture, I3d, DEM, Config, GRLE, Background]
42
43
 
43
44
  def __init__(self, map_template_path: str | None = None):
44
45
  if map_template_path:
@@ -57,15 +57,6 @@ class GRLE(Component):
57
57
  width = int(self.map_width * info_layer["width_multiplier"])
58
58
  data_type = info_layer["data_type"]
59
59
 
60
- self.logger.debug(
61
- "Creating InfoLayer PNG file %s with dimensions %sx%s, %s channels, "
62
- "and data type %s.",
63
- file_path,
64
- height,
65
- width,
66
- data_type,
67
- )
68
-
69
60
  # Create the InfoLayer PNG file with zeros.
70
61
  info_layer_data = np.zeros((height, width), dtype=data_type)
71
62
  print(info_layer_data.shape)
@@ -0,0 +1,360 @@
1
+ """This module contains the Config class for map settings and configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from xml.etree import ElementTree as ET
8
+
9
+ from shapely.geometry import Polygon, box # type: ignore
10
+
11
+ from maps4fs.generator.component import Component
12
+
13
+ DEFAULT_HEIGHT_SCALE = 2000
14
+ DEFAULT_MAX_LOD_DISTANCE = 10000
15
+ DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
16
+ NODE_ID_STARTING_VALUE = 500
17
+
18
+
19
+ # pylint: disable=R0903
20
+ class I3d(Component):
21
+ """Component for map i3d file settings and configuration.
22
+
23
+ Arguments:
24
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
25
+ map_height (int): The height of the map in pixels.
26
+ map_width (int): The width of the map in pixels.
27
+ map_directory (str): The directory where the map files are stored.
28
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
29
+ info, warning. If not provided, default logging will be used.
30
+ """
31
+
32
+ _map_i3d_path: str | None = None
33
+
34
+ def preprocess(self) -> None:
35
+ """Gets the path to the map I3D file from the game instance and saves it to the instance
36
+ attribute. If the game does not support I3D files, the attribute is set to None."""
37
+ try:
38
+ self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
39
+ self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
40
+ except NotImplementedError:
41
+ self.logger.info("I3D file processing is not implemented for this game.")
42
+ self._map_i3d_path = None
43
+
44
+ def process(self) -> None:
45
+ """Updates the map I3D file with the default settings."""
46
+ self._update_i3d_file()
47
+ self._add_fields()
48
+
49
+ def _get_tree(self) -> ET.ElementTree | None:
50
+ """Returns the ElementTree instance of the map I3D file."""
51
+ if not self._map_i3d_path:
52
+ self.logger.info("I3D is not obtained, skipping the update.")
53
+ return None
54
+ if not os.path.isfile(self._map_i3d_path):
55
+ self.logger.warning("I3D file not found: %s.", self._map_i3d_path)
56
+ return None
57
+
58
+ return ET.parse(self._map_i3d_path)
59
+
60
+ def _update_i3d_file(self) -> None:
61
+ """Updates the map I3D file with the default settings."""
62
+
63
+ tree = self._get_tree()
64
+ if tree is None:
65
+ return
66
+
67
+ self.logger.debug("Map I3D file loaded from: %s.", self._map_i3d_path)
68
+
69
+ root = tree.getroot()
70
+ for map_elem in root.iter("Scene"):
71
+ for terrain_elem in map_elem.iter("TerrainTransformGroup"):
72
+ terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
73
+ self.logger.debug(
74
+ "heightScale attribute set to %s in TerrainTransformGroup element.",
75
+ DEFAULT_HEIGHT_SCALE,
76
+ )
77
+ terrain_elem.set("maxLODDistance", str(DEFAULT_MAX_LOD_DISTANCE))
78
+ self.logger.debug(
79
+ "maxLODDistance attribute set to %s in TerrainTransformGroup element.",
80
+ DEFAULT_MAX_LOD_DISTANCE,
81
+ )
82
+
83
+ terrain_elem.set("occMaxLODDistance", str(DEFAULT_MAX_LOD_OCCLUDER_DISTANCE))
84
+ self.logger.debug(
85
+ "occMaxLODDistance attribute set to %s in TerrainTransformGroup element.",
86
+ DEFAULT_MAX_LOD_OCCLUDER_DISTANCE,
87
+ )
88
+
89
+ self.logger.debug("TerrainTransformGroup element updated in I3D file.")
90
+
91
+ tree.write(self._map_i3d_path) # type: ignore
92
+ self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
93
+
94
+ def previews(self) -> list[str]:
95
+ """Returns a list of paths to the preview images (empty list).
96
+ The component does not generate any preview images so it returns an empty list.
97
+
98
+ Returns:
99
+ list[str]: An empty list.
100
+ """
101
+ return []
102
+
103
+ # pylint: disable=R0914, R0915
104
+ def _add_fields(self) -> None:
105
+ """Adds fields to the map I3D file."""
106
+ tree = self._get_tree()
107
+ if tree is None:
108
+ return
109
+
110
+ textures_info_layer_path = os.path.join(self.info_layers_directory, "textures.json")
111
+ if not os.path.isfile(textures_info_layer_path):
112
+ self.logger.warning("Textures info layer not found: %s.", textures_info_layer_path)
113
+ return
114
+
115
+ with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
116
+ textures_info_layer = json.load(textures_info_layer_file)
117
+
118
+ fields: list[tuple[int, int]] | None = textures_info_layer.get("fields")
119
+ if not fields:
120
+ self.logger.warning("Fields data not found in textures info layer.")
121
+ return
122
+
123
+ self.logger.debug("Found %s fields in textures info layer.", len(fields))
124
+
125
+ root = tree.getroot()
126
+ gameplay_node = root.find(".//TransformGroup[@name='gameplay']")
127
+ if gameplay_node is not None:
128
+ self.logger.debug("Found the gameplay node.")
129
+
130
+ fields_node = gameplay_node.find(".//TransformGroup[@name='fields']")
131
+ user_attributes_node = root.find(".//UserAttributes")
132
+
133
+ if fields_node is not None:
134
+ node_id = NODE_ID_STARTING_VALUE
135
+
136
+ for field_id, field in enumerate(fields, start=1):
137
+ # Convert the top-left coordinates to the center coordinates system.
138
+ try:
139
+ fitted_field = self.fit_polygon_into_bounds(field) # type: ignore
140
+ except ValueError as e:
141
+ self.logger.warning(
142
+ "Field %s could not be fitted into the map bounds.", field_id
143
+ )
144
+ self.logger.debug("Error: %s", e)
145
+ continue
146
+ field_ccs = [
147
+ self.top_left_coordinates_to_center(point) for point in fitted_field
148
+ ]
149
+
150
+ # Creating the main field node.
151
+ field_node = ET.Element("TransformGroup")
152
+ field_node.set("name", f"field{field_id}")
153
+ cx, cy = self.get_polygon_center(field_ccs)
154
+ field_node.set("translation", f"{cx} 0 {cy}")
155
+ field_node.set("nodeId", str(node_id))
156
+
157
+ # Adding UserAttributes to the field node.
158
+ user_attribute_node = self.create_user_attribute_node(node_id)
159
+ user_attributes_node.append(user_attribute_node) # type: ignore
160
+
161
+ node_id += 1
162
+
163
+ # Creating the polygon points node, which contains the points of the field.
164
+ polygon_points_node = ET.Element("TransformGroup")
165
+ polygon_points_node.set("name", "polygonPoints")
166
+ polygon_points_node.set("nodeId", str(node_id))
167
+ node_id += 1
168
+
169
+ for point_id, point in enumerate(field_ccs, start=1):
170
+ rx, ry = self.absolute_to_relative(point, (cx, cy))
171
+
172
+ node_id += 1
173
+ point_node = ET.Element("TransformGroup")
174
+ point_node.set("name", f"point{point_id}")
175
+ point_node.set("translation", f"{rx} 0 {ry}")
176
+ point_node.set("nodeId", str(node_id))
177
+
178
+ polygon_points_node.append(point_node)
179
+
180
+ field_node.append(polygon_points_node)
181
+
182
+ # Adding the name indicator node to the field node.
183
+ name_indicator_node, node_id = self.get_name_indicator_node(node_id, field_id)
184
+ field_node.append(name_indicator_node)
185
+
186
+ # Adding the teleport indicator node to the field node.
187
+ teleport_indicator_node, node_id = self.get_teleport_indicator_node(node_id)
188
+ field_node.append(teleport_indicator_node)
189
+
190
+ # Adding the field node to the fields node.
191
+ fields_node.append(field_node)
192
+ self.logger.debug("Field %s added to the I3D file.", field_id)
193
+
194
+ node_id += 1
195
+
196
+ tree.write(self._map_i3d_path) # type: ignore
197
+ self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
198
+
199
+ def get_name_indicator_node(self, node_id: int, field_id: int) -> tuple[ET.Element, int]:
200
+ """Creates a name indicator node with given node ID and field ID.
201
+
202
+ Arguments:
203
+ node_id (int): The node ID of the name indicator node.
204
+ field_id (int): The ID of the field.
205
+
206
+ Returns:
207
+ tuple[ET.Element, int]: The name indicator node and the updated node ID.
208
+ """
209
+ node_id += 1
210
+ name_indicator_node = ET.Element("TransformGroup")
211
+ name_indicator_node.set("name", "nameIndicator")
212
+ name_indicator_node.set("nodeId", str(node_id))
213
+
214
+ node_id += 1
215
+ note_node = ET.Element("Note")
216
+ note_node.set("name", "Note")
217
+ note_node.set("nodeId", str(node_id))
218
+ note_node.set("text", f"field{field_id}&#xA;0.00 ha")
219
+ note_node.set("color", "4278190080")
220
+ note_node.set("fixedSize", "true")
221
+
222
+ name_indicator_node.append(note_node)
223
+
224
+ return name_indicator_node, node_id
225
+
226
+ def get_teleport_indicator_node(self, node_id: int) -> tuple[ET.Element, int]:
227
+ """Creates a teleport indicator node with given node ID.
228
+
229
+ Arguments:
230
+ node_id (int): The node ID of the teleport indicator node.
231
+
232
+ Returns:
233
+ tuple[ET.Element, int]: The teleport indicator node and the updated node ID.
234
+ """
235
+ node_id += 1
236
+ teleport_indicator_node = ET.Element("TransformGroup")
237
+ teleport_indicator_node.set("name", "teleportIndicator")
238
+ teleport_indicator_node.set("nodeId", str(node_id))
239
+
240
+ return teleport_indicator_node, node_id
241
+
242
+ def get_polygon_center(self, polygon_points: list[tuple[int, int]]) -> tuple[int, int]:
243
+ """Calculates the center of a polygon defined by a list of points.
244
+
245
+ Arguments:
246
+ polygon_points (list[tuple[int, int]]): The points of the polygon.
247
+
248
+ Returns:
249
+ tuple[int, int]: The center of the polygon.
250
+ """
251
+ polygon = Polygon(polygon_points)
252
+ center = polygon.centroid
253
+ return int(center.x), int(center.y)
254
+
255
+ def absolute_to_relative(
256
+ self, point: tuple[int, int], center: tuple[int, int]
257
+ ) -> tuple[int, int]:
258
+ """Converts a pair of absolute coordinates to relative coordinates.
259
+
260
+ Arguments:
261
+ point (tuple[int, int]): The absolute coordinates.
262
+ center (tuple[int, int]): The center coordinates.
263
+
264
+ Returns:
265
+ tuple[int, int]: The relative coordinates.
266
+ """
267
+ cx, cy = center
268
+ x, y = point
269
+ return x - cx, y - cy
270
+
271
+ def top_left_coordinates_to_center(self, top_left: tuple[int, int]) -> tuple[int, int]:
272
+ """Converts a pair of coordinates from the top-left system to the center system.
273
+ In top-left system, the origin (0, 0) is in the top-left corner of the map, while in the
274
+ center system, the origin is in the center of the map.
275
+
276
+ Arguments:
277
+ top_left (tuple[int, int]): The coordinates in the top-left system.
278
+
279
+ Returns:
280
+ tuple[int, int]: The coordinates in the center system.
281
+ """
282
+ x, y = top_left
283
+ cs_x = x - self.map_width // 2
284
+ cs_y = y - self.map_height // 2
285
+
286
+ return cs_x, cs_y
287
+
288
+ def fit_polygon_into_bounds(
289
+ self, polygon_points: list[tuple[int, int]]
290
+ ) -> list[tuple[int, int]]:
291
+ """Fits a polygon into the bounds of the map.
292
+
293
+ Arguments:
294
+ polygon_points (list[tuple[int, int]]): The points of the polygon.
295
+
296
+ Returns:
297
+ list[tuple[int, int]]: The points of the polygon fitted into the map bounds.
298
+ """
299
+ min_x = min_y = 0
300
+ max_x, max_y = self.map_width, self.map_height
301
+
302
+ # Create a polygon from the given points
303
+ polygon = Polygon(polygon_points)
304
+
305
+ # Create a bounding box for the map bounds
306
+ bounds = box(min_x, min_y, max_x, max_y)
307
+
308
+ # Intersect the polygon with the bounds to fit it within the map
309
+ fitted_polygon = polygon.intersection(bounds)
310
+
311
+ if not isinstance(fitted_polygon, Polygon):
312
+ raise ValueError("The fitted polygon is not a valid polygon.")
313
+
314
+ # Return the fitted polygon points
315
+ return list(fitted_polygon.exterior.coords)
316
+
317
+ @staticmethod
318
+ def create_user_attribute_node(node_id: int) -> ET.Element:
319
+ """Creates an XML user attribute node with given node ID.
320
+
321
+ Arguments:
322
+ node_id (int): The node ID of the user attribute node.
323
+
324
+ Returns:
325
+ ET.Element: The created user attribute node.
326
+ """
327
+ user_attribute_node = ET.Element("UserAttribute")
328
+ user_attribute_node.set("nodeId", str(node_id))
329
+
330
+ attributes = [
331
+ ("angle", "integer", "0"),
332
+ ("missionAllowed", "boolean", "true"),
333
+ ("missionOnlyGrass", "boolean", "false"),
334
+ ("nameIndicatorIndex", "string", "1"),
335
+ ("polygonIndex", "string", "0"),
336
+ ("teleportIndicatorIndex", "string", "2"),
337
+ ]
338
+
339
+ for name, attr_type, value in attributes:
340
+ user_attribute_node.append(I3d.create_attribute_node(name, attr_type, value))
341
+
342
+ return user_attribute_node
343
+
344
+ @staticmethod
345
+ def create_attribute_node(name: str, attr_type: str, value: str) -> ET.Element:
346
+ """Creates an XML attribute node with given name, type, and value.
347
+
348
+ Arguments:
349
+ name (str): The name of the attribute.
350
+ attr_type (str): The type of the attribute.
351
+ value (str): The value of the attribute.
352
+
353
+ Returns:
354
+ ET.Element: The created attribute node.
355
+ """
356
+ attribute_node = ET.Element("Attribute")
357
+ attribute_node.set("name", name)
358
+ attribute_node.set("type", attr_type)
359
+ attribute_node.set("value", value)
360
+ return attribute_node
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import os
7
- import warnings
7
+ from collections import defaultdict
8
8
  from typing import Any, Callable, Generator, Optional
9
9
 
10
10
  import cv2
@@ -41,6 +41,9 @@ class Texture(Component):
41
41
  tags (dict[str, str | list[str]]): Dictionary of tags to search for.
42
42
  width (int | None): Width of the polygon in meters (only for LineString).
43
43
  color (tuple[int, int, int]): Color of the layer in BGR format.
44
+ exclude_weight (bool): Flag to exclude weight from the texture.
45
+ priority (int | None): Priority of the layer.
46
+ info_layer (str | None): Name of the corresnponding info layer.
44
47
 
45
48
  Attributes:
46
49
  name (str): Name of the layer.
@@ -58,6 +61,7 @@ class Texture(Component):
58
61
  color: tuple[int, int, int] | list[int] | None = None,
59
62
  exclude_weight: bool = False,
60
63
  priority: int | None = None,
64
+ info_layer: str | None = None,
61
65
  ):
62
66
  self.name = name
63
67
  self.count = count
@@ -66,6 +70,7 @@ class Texture(Component):
66
70
  self.color = color if color else (255, 255, 255)
67
71
  self.exclude_weight = exclude_weight
68
72
  self.priority = priority
73
+ self.info_layer = info_layer
69
74
 
70
75
  def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
71
76
  """Returns dictionary with layer data.
@@ -80,6 +85,7 @@ class Texture(Component):
80
85
  "color": list(self.color),
81
86
  "exclude_weight": self.exclude_weight,
82
87
  "priority": self.priority,
88
+ "info_layer": self.info_layer,
83
89
  }
84
90
 
85
91
  data = {k: v for k, v in data.items() if v is not None}
@@ -178,6 +184,9 @@ class Texture(Component):
178
184
  self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
179
185
  self.logger.debug("Generation info save path: %s.", self.info_save_path)
180
186
 
187
+ self.info_layer_path = os.path.join(self.info_layers_directory, "textures.json")
188
+ self.logger.debug("Info layer path: %s.", self.info_layer_path)
189
+
181
190
  def get_base_layer(self) -> Layer | None:
182
191
  """Returns base layer.
183
192
 
@@ -297,6 +306,10 @@ class Texture(Component):
297
306
 
298
307
  cumulative_image = None
299
308
 
309
+ # Dictionary to store info layer data.
310
+ # Key is a layer.info_layer, value is a list of polygon points as tuples (x, y).
311
+ info_layer_data = defaultdict(list)
312
+
300
313
  for layer in layers:
301
314
  if not layer.tags:
302
315
  self.logger.debug("Layer %s has no tags, there's nothing to draw.", layer.name)
@@ -317,6 +330,8 @@ class Texture(Component):
317
330
  mask = cv2.bitwise_not(cumulative_image)
318
331
 
319
332
  for polygon in self.polygons(layer.tags, layer.width): # type: ignore
333
+ if layer.info_layer:
334
+ info_layer_data[layer.info_layer].append(self.np_to_polygon_points(polygon))
320
335
  cv2.fillPoly(layer_image, [polygon], color=255) # type: ignore
321
336
 
322
337
  output_image = cv2.bitwise_and(layer_image, mask)
@@ -326,6 +341,10 @@ class Texture(Component):
326
341
  cv2.imwrite(layer_path, output_image)
327
342
  self.logger.debug("Texture %s saved.", layer_path)
328
343
 
344
+ # Save info layer data.
345
+ with open(self.info_layer_path, "w", encoding="utf-8") as f:
346
+ json.dump(info_layer_data, f, ensure_ascii=False, indent=4)
347
+
329
348
  if cumulative_image is not None:
330
349
  self.draw_base_layer(cumulative_image)
331
350
 
@@ -428,6 +447,17 @@ class Texture(Component):
428
447
  """
429
448
  return int(self.map_height * (1 - (y - self.minimum_y) / (self.maximum_y - self.minimum_y)))
430
449
 
450
+ def np_to_polygon_points(self, np_array: np.ndarray) -> list[tuple[int, int]]:
451
+ """Converts numpy array of polygon points to list of tuples.
452
+
453
+ Arguments:
454
+ np_array (np.ndarray): Numpy array of polygon points.
455
+
456
+ Returns:
457
+ list[tuple[int, int]]: List of tuples.
458
+ """
459
+ return [(int(x), int(y)) for x, y in np_array.reshape(-1, 2)]
460
+
431
461
  # pylint: disable=W0613
432
462
  def _to_np(self, geometry: shapely.geometry.polygon.Polygon, *args) -> np.ndarray:
433
463
  """Converts Polygon geometry to numpy array of polygon points.
@@ -509,14 +539,12 @@ class Texture(Component):
509
539
  Generator[np.ndarray, None, None]: Numpy array of polygon points.
510
540
  """
511
541
  try:
512
- with warnings.catch_warnings():
513
- warnings.simplefilter("ignore", DeprecationWarning)
514
- objects = ox.features_from_bbox(bbox=self.bbox, tags=tags)
542
+ objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
515
543
  except Exception as e: # pylint: disable=W0718
516
544
  self.logger.warning("Error fetching objects for tags: %s.", tags)
517
545
  self.logger.warning(e)
518
546
  return
519
- objects_utm = ox.project_gdf(objects, to_latlong=False)
547
+ objects_utm = ox.projection.project_gdf(objects, to_latlong=False)
520
548
  self.logger.debug("Fetched %s elements for tags: %s.", len(objects_utm), tags)
521
549
 
522
550
  for _, obj in objects_utm.iterrows():
@@ -579,6 +607,7 @@ class Texture(Component):
579
607
  merged.dtype,
580
608
  )
581
609
  preview_path = os.path.join(self.previews_directory, "textures_osm.png")
582
- cv2.imwrite(preview_path, merged) # pylint: disable=no-member
610
+
611
+ cv2.imwrite(preview_path, merged) # type: ignore
583
612
  self.logger.info("Preview saved to %s.", preview_path)
584
613
  return preview_path
@@ -36,10 +36,10 @@ class Tile(DEM):
36
36
 
37
37
  self.logger.debug("Generating tile for code %s", self.code)
38
38
 
39
- tiles_directory = os.path.join(self.map_directory, "objects", "tiles")
40
- os.makedirs(tiles_directory, exist_ok=True)
39
+ background_directory = os.path.join(self.map_directory, "background")
40
+ os.makedirs(background_directory, exist_ok=True)
41
41
 
42
- self._dem_path = os.path.join(tiles_directory, f"{self.code}.png")
42
+ self._dem_path = os.path.join(background_directory, f"{self.code}.png")
43
43
  self.logger.debug("DEM path for tile %s is %s", self.code, self._dem_path)
44
44
 
45
45
  def get_output_resolution(self) -> tuple[int, int]:
@@ -0,0 +1,63 @@
1
+ """This module contains functions to work with the background terrain of the map."""
2
+
3
+ import cv2
4
+ import numpy as np
5
+ import trimesh # type: ignore
6
+
7
+
8
+ # pylint: disable=R0801, R0914
9
+ def plane_from_np(
10
+ dem_data: np.ndarray,
11
+ resize_factor: float,
12
+ simplify_factor: int,
13
+ save_path: str,
14
+ ) -> None:
15
+ """Generates a 3D obj file based on DEM data.
16
+
17
+ Arguments:
18
+ dem_data (np.ndarray) -- The DEM data as a numpy array.
19
+ resize_factor (float) -- The factor by which the DEM data will be resized. Bigger values
20
+ will result in a bigger mesh.
21
+ simplify_factor (int) -- The factor by which the mesh will be simplified. Bigger values
22
+ will result in a simpler mesh.
23
+ save_path (str) -- The path to save the obj file.
24
+ """
25
+ dem_data = cv2.resize( # pylint: disable=no-member
26
+ dem_data, (0, 0), fx=resize_factor, fy=resize_factor
27
+ )
28
+
29
+ # Invert the height values.
30
+ dem_data = dem_data.max() - dem_data
31
+
32
+ rows, cols = dem_data.shape
33
+ x = np.linspace(0, cols - 1, cols)
34
+ y = np.linspace(0, rows - 1, rows)
35
+ x, y = np.meshgrid(x, y)
36
+ z = dem_data
37
+
38
+ vertices = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
39
+ faces = []
40
+
41
+ for i in range(rows - 1):
42
+ for j in range(cols - 1):
43
+ top_left = i * cols + j
44
+ top_right = top_left + 1
45
+ bottom_left = top_left + cols
46
+ bottom_right = bottom_left + 1
47
+
48
+ faces.append([top_left, bottom_left, bottom_right])
49
+ faces.append([top_left, bottom_right, top_right])
50
+
51
+ faces = np.array(faces) # type: ignore
52
+ mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
53
+
54
+ # Apply rotation: 180 degrees around Y-axis and Z-axis
55
+ rotation_matrix_y = trimesh.transformations.rotation_matrix(np.pi, [0, 1, 0])
56
+ rotation_matrix_z = trimesh.transformations.rotation_matrix(np.pi, [0, 0, 1])
57
+ mesh.apply_transform(rotation_matrix_y)
58
+ mesh.apply_transform(rotation_matrix_z)
59
+
60
+ # Simplify the mesh to reduce the number of faces.
61
+ mesh = mesh.simplify_quadric_decimation(face_count=len(faces) // simplify_factor)
62
+
63
+ mesh.export(save_path)
@@ -1,3 +1,28 @@
1
+ Metadata-Version: 2.1
2
+ Name: maps4fs
3
+ Version: 1.0.1
4
+ Summary: Generate map templates for Farming Simulator from real places.
5
+ Author-email: iwatkot <iwatkot@gmail.com>
6
+ License: MIT License
7
+ Project-URL: Homepage, https://github.com/iwatkot/maps4fs
8
+ Project-URL: Repository, https://github.com/iwatkot/maps4fs
9
+ Keywords: farmingsimulator,fs,farmingsimulator22,farmingsimulator25,fs22,fs25
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE.md
16
+ Requires-Dist: opencv-python
17
+ Requires-Dist: osmnx>=2.0.0
18
+ Requires-Dist: rasterio
19
+ Requires-Dist: folium
20
+ Requires-Dist: geopy
21
+ Requires-Dist: trimesh
22
+ Requires-Dist: imageio
23
+ Requires-Dist: tifffile
24
+ Requires-Dist: pympler
25
+
1
26
  <div align="center" markdown>
2
27
  <a href="https://discord.gg/Sj5QKKyE42">
3
28
  <img src="https://github.com/user-attachments/assets/37043333-d6ef-4ca3-9f3c-81323d9d0b71">
@@ -6,8 +31,10 @@
6
31
  <p align="center">
7
32
  <a href="#Quick-Start">Quick Start</a> •
8
33
  <a href="#Overview">Overview</a> •
9
- <a href="#How-To-Run">How-To-Run</a> •
34
+ <a href="#Step-by-step">Create a map in 10 steps</a> •
35
+ <a href="#How-To-Run">How-To-Run</a><br>
10
36
  <a href="docs/FAQ.md">FAQ</a> •
37
+ <a href="docs/map_structure.md">Map Structure</a> •
11
38
  <a href="#Modder-Toolbox">Modder Toolbox</a><br>
12
39
  <a href="#Supported-objects">Supported objects</a> •
13
40
  <a href="#Generation-info">Generation info</a> •
@@ -43,6 +70,7 @@
43
70
  🔷 Generates *.obj files for background terrain based on the real-world height map<br>
44
71
  📄 Generates scripts to download high-resolution satellite images from [QGIS](https://qgis.org/download/) in one click<br>
45
72
  🧰 Modder Toolbox to help you with various of tasks 🆕<br>
73
+ 🌾 Automatically generates fields 🆕<br>
46
74
 
47
75
  <p align="center">
48
76
  <img src="https://github.com/user-attachments/assets/cf8f5752-9c69-4018-bead-290f59ba6976"><br>
@@ -51,6 +79,8 @@
51
79
  🛰️ Realistic background terrain objects with satellite images.<br><br>
52
80
  <img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
53
81
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
82
+ <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
83
+ 🌾 Field generation with one click.<br><br>
54
84
  <img src="https://github.com/user-attachments/assets/cce45575-c917-4a1b-bdc0-6368e32ccdff"><br>
55
85
  📏 Almost any possible map sizes.
56
86
  </p>
@@ -101,6 +131,9 @@ Parameters:
101
131
  - coordinates: 45.15, 19.71
102
132
  - size: 16 x 16 km
103
133
 
134
+ ## Step by step
135
+ Don't know where to start? Don't worry, just follow this [step-by-step guide](docs/step_by_step.md) to create your first map in 10 simple steps.<br>
136
+
104
137
  ## How-To-Run
105
138
 
106
139
  You'll find detailed instructions on how to run the project below. But if you prefer video tutorials, here's one for you:
@@ -177,12 +210,15 @@ The map will be saved in the `map_directory` directory.
177
210
  ## Modder Toolbox
178
211
  The tool now has a Modder Toolbox, which is a set of tools to help you with various tasks. You can open the toolbox by switching to the `🧰 Modder Toolbox` tab in the StreamLit app.<br>
179
212
 
180
- ![Modder Toolbox](https://github.com/user-attachments/assets/18f169e9-1a5b-474c-b488-6becfffadcea)
213
+ ![Modder Toolbox](https://github.com/user-attachments/assets/dffb252f-f5c0-4021-9d45-31e5bccc0d9b)
181
214
 
182
215
  ### Tool categories
183
216
  Tools are divided into categories, which are listed below.
184
217
  #### Textures and DEM
185
- - **GeoTIFF windowing** - allows you to upload your GeoTIFF file and select the region of interest to extract it from the image.
218
+ - **GeoTIFF windowing** - allows you to upload your GeoTIFF file and select the region of interest to extract it from the image. It's useful when you have high-resolution DEM data and want to create the height map using it.
219
+
220
+ #### Background terrain
221
+ - **Convert image to obj model** - allows you to convert the image to the obj model. You can use this tool to create the background terrain for your map. It can be extremely useful if you have access to the sources of high-resolution DEM data and want to create the background terrain using it.
186
222
 
187
223
  ## Supported objects
188
224
  The project is based on the [OpenStreetMap](https://www.openstreetmap.org/) data. So, refer to [this page](https://wiki.openstreetmap.org/wiki/Map_Features) to understand the list below.
@@ -351,7 +387,7 @@ Let's have a closer look at the fields:
351
387
 
352
388
  ## Background terrain
353
389
  The tool now supports the generation of the background terrain. If you don't know what it is, here's a brief explanation. The background terrain is the world around the map. It's important to create it, because if you don't, the map will look like it's floating in the void. The background terrain is a simple plane which can (and should) be texture to look fine.<br>
354
- So, the tool generates the background terrain in the form of the 8 tiles, which surround the map. The tiles are named as the cardinal points, e.g. "N", "NE", "E" and so on. All those tiles will be saved in the `objects/tiles` directory with corresponding names: `N.obj`, `NE.obj`, `E.obj` and so on.<br>
390
+ So, the tool generates the background terrain in the form of the 8 tiles, which surround the map. The tiles are named as the cardinal points, e.g. "N", "NE", "E" and so on. All those tiles will be saved in the `background` directory with corresponding names: `N.obj`, `NE.obj`, `E.obj` and so on.<br>
355
391
  If you don't want to work with separate tiles, the tool also generates the `FULL.obj` file, which includes everything around the map and the map itself. It may be a convinient approach to work with one file, one texture and then just cut the map from it.<br>
356
392
 
357
393
  ![Complete background terrain in Blender](https://github.com/user-attachments/assets/7266b8f1-bfa2-4c14-a740-1c84b1030a66)
@@ -22,5 +22,6 @@ maps4fs/generator/qgis.py
22
22
  maps4fs/generator/texture.py
23
23
  maps4fs/generator/tile.py
24
24
  maps4fs/toolbox/__init__.py
25
+ maps4fs/toolbox/background.py
25
26
  maps4fs/toolbox/dem.py
26
27
  tests/test_generator.py
@@ -1,5 +1,5 @@
1
1
  opencv-python
2
- osmnx<2.0.0
2
+ osmnx>=2.0.0
3
3
  rasterio
4
4
  folium
5
5
  geopy
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maps4fs"
7
- version = "0.9.97"
7
+ version = "1.0.1"
8
8
  description = "Generate map templates for Farming Simulator from real places."
9
9
  authors = [{name = "iwatkot", email = "iwatkot@gmail.com"}]
10
10
  license = {text = "MIT License"}
@@ -18,7 +18,7 @@ classifiers = [
18
18
  ]
19
19
  dependencies = [
20
20
  "opencv-python",
21
- "osmnx<2.0.0",
21
+ "osmnx>=2.0.0",
22
22
  "rasterio",
23
23
  "folium",
24
24
  "geopy",
@@ -1,89 +0,0 @@
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
- Arguments:
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 []
File without changes
File without changes
File without changes
File without changes
File without changes