maps4fs 2.8.9__py3-none-any.whl → 2.9.37__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/component/background.py +118 -35
- maps4fs/generator/component/base/component_mesh.py +39 -13
- maps4fs/generator/component/building.py +706 -0
- maps4fs/generator/component/config.py +5 -0
- maps4fs/generator/component/dem.py +7 -0
- maps4fs/generator/component/grle.py +66 -0
- maps4fs/generator/component/i3d.py +63 -5
- maps4fs/generator/component/layer.py +19 -0
- maps4fs/generator/component/road.py +648 -0
- maps4fs/generator/component/satellite.py +3 -0
- maps4fs/generator/component/texture.py +89 -50
- maps4fs/generator/config.py +79 -11
- maps4fs/generator/game.py +64 -3
- maps4fs/generator/map.py +73 -39
- maps4fs/generator/monitor.py +118 -0
- maps4fs/generator/settings.py +18 -1
- maps4fs/generator/statistics.py +10 -0
- maps4fs/generator/utils.py +26 -1
- maps4fs/logger.py +2 -1
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.37.dist-info}/METADATA +6 -4
- maps4fs-2.9.37.dist-info/RECORD +32 -0
- maps4fs-2.9.37.dist-info/licenses/LICENSE.md +416 -0
- maps4fs-2.8.9.dist-info/RECORD +0 -29
- maps4fs-2.8.9.dist-info/licenses/LICENSE.md +0 -651
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.37.dist-info}/WHEEL +0 -0
- {maps4fs-2.8.9.dist-info → maps4fs-2.9.37.dist-info}/top_level.txt +0 -0
|
@@ -10,6 +10,7 @@ import numpy as np
|
|
|
10
10
|
import maps4fs.generator.utils as mfsutils
|
|
11
11
|
from maps4fs.generator.component.base.component_image import ImageComponent
|
|
12
12
|
from maps4fs.generator.component.base.component_xml import XMLComponent
|
|
13
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
13
14
|
from maps4fs.generator.settings import Parameters
|
|
14
15
|
|
|
15
16
|
# Defines coordinates for country block on the license plate texture.
|
|
@@ -120,6 +121,7 @@ class Config(XMLComponent, ImageComponent):
|
|
|
120
121
|
|
|
121
122
|
self.create_qgis_scripts(layers)
|
|
122
123
|
|
|
124
|
+
@monitor_performance
|
|
123
125
|
def _adjust_fog(self) -> None:
|
|
124
126
|
"""Adjusts the fog settings in the environment XML file based on the DEM and height scale."""
|
|
125
127
|
self.logger.debug("Adjusting fog settings based on DEM and height scale...")
|
|
@@ -231,6 +233,7 @@ class Config(XMLComponent, ImageComponent):
|
|
|
231
233
|
|
|
232
234
|
return dem_maximum_meter, dem_minimum_meter
|
|
233
235
|
|
|
236
|
+
@monitor_performance
|
|
234
237
|
def _set_overview(self) -> None:
|
|
235
238
|
"""Generates and sets the overview image for the map."""
|
|
236
239
|
try:
|
|
@@ -347,6 +350,7 @@ class Config(XMLComponent, ImageComponent):
|
|
|
347
350
|
"HR",
|
|
348
351
|
}
|
|
349
352
|
|
|
353
|
+
@monitor_performance
|
|
350
354
|
def update_license_plates(self):
|
|
351
355
|
"""Updates license plates for the specified country."""
|
|
352
356
|
try:
|
|
@@ -565,6 +569,7 @@ class Config(XMLComponent, ImageComponent):
|
|
|
565
569
|
self.save_tree(tree, xml_path=i3d_path)
|
|
566
570
|
self.logger.debug("Updated licensePlatesPL.i3d texture reference to: %s", filename)
|
|
567
571
|
|
|
572
|
+
@monitor_performance
|
|
568
573
|
def _generate_license_plate_texture(
|
|
569
574
|
self,
|
|
570
575
|
license_plates_directory: str,
|
|
@@ -9,6 +9,7 @@ from pydtmdl import DTMProvider
|
|
|
9
9
|
|
|
10
10
|
import maps4fs.generator.config as mfscfg
|
|
11
11
|
from maps4fs.generator.component.base.component_image import ImageComponent
|
|
12
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
# pylint: disable=R0903, R0902
|
|
@@ -115,6 +116,7 @@ class DEM(ImageComponent):
|
|
|
115
116
|
except Exception as e:
|
|
116
117
|
self.logger.warning("Failed to update DEM info: %s.", e)
|
|
117
118
|
|
|
119
|
+
@monitor_performance
|
|
118
120
|
def process(self) -> None:
|
|
119
121
|
"""Reads DTM file, crops it to map size, normalizes and blurs it,
|
|
120
122
|
saves to map directory."""
|
|
@@ -190,6 +192,7 @@ class DEM(ImageComponent):
|
|
|
190
192
|
if self.rotation:
|
|
191
193
|
self.rotate_dem()
|
|
192
194
|
|
|
195
|
+
@monitor_performance
|
|
193
196
|
def normalize_data(self, data: np.ndarray, height_scale_value: int) -> np.ndarray:
|
|
194
197
|
"""Normalize DEM data to 16-bit unsigned integer range (0 to 65535).
|
|
195
198
|
|
|
@@ -209,6 +212,7 @@ class DEM(ImageComponent):
|
|
|
209
212
|
)
|
|
210
213
|
return normalized_data
|
|
211
214
|
|
|
215
|
+
@monitor_performance
|
|
212
216
|
def determine_height_scale(self, data: np.ndarray) -> int:
|
|
213
217
|
"""Determine height scale value using ceiling.
|
|
214
218
|
|
|
@@ -261,6 +265,7 @@ class DEM(ImageComponent):
|
|
|
261
265
|
)
|
|
262
266
|
return data
|
|
263
267
|
|
|
268
|
+
@monitor_performance
|
|
264
269
|
def apply_multiplier(self, data: np.ndarray) -> np.ndarray:
|
|
265
270
|
"""Apply multiplier to DEM data.
|
|
266
271
|
|
|
@@ -283,6 +288,7 @@ class DEM(ImageComponent):
|
|
|
283
288
|
)
|
|
284
289
|
return multiplied_data
|
|
285
290
|
|
|
291
|
+
@monitor_performance
|
|
286
292
|
def resize_to_output(self, data: np.ndarray) -> np.ndarray:
|
|
287
293
|
"""Resize DEM data to the output resolution.
|
|
288
294
|
|
|
@@ -296,6 +302,7 @@ class DEM(ImageComponent):
|
|
|
296
302
|
|
|
297
303
|
return resampled_data
|
|
298
304
|
|
|
305
|
+
@monitor_performance
|
|
299
306
|
def rotate_dem(self) -> None:
|
|
300
307
|
"""Rotate DEM image."""
|
|
301
308
|
self.logger.debug("Rotating DEM image by %s degrees.", self.rotation)
|
|
@@ -12,6 +12,7 @@ from tqdm import tqdm
|
|
|
12
12
|
from maps4fs.generator.component.base.component_image import ImageComponent
|
|
13
13
|
from maps4fs.generator.component.base.component_xml import XMLComponent
|
|
14
14
|
from maps4fs.generator.component.layer import Layer
|
|
15
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
15
16
|
from maps4fs.generator.settings import Parameters
|
|
16
17
|
|
|
17
18
|
# This value sums up the pixel value of the basic area type to convert it from "No Water" to "Near Water".
|
|
@@ -98,6 +99,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
98
99
|
|
|
99
100
|
return grle_schema
|
|
100
101
|
|
|
102
|
+
@monitor_performance
|
|
101
103
|
def process(self) -> None:
|
|
102
104
|
"""Generates InfoLayer PNG files based on the GRLE schema."""
|
|
103
105
|
grle_schema = self._read_grle_schema()
|
|
@@ -134,6 +136,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
134
136
|
self._process_environment()
|
|
135
137
|
self._process_indoor()
|
|
136
138
|
|
|
139
|
+
@monitor_performance
|
|
137
140
|
def previews(self) -> list[str]:
|
|
138
141
|
"""Returns a list of paths to the preview images (empty list).
|
|
139
142
|
The component does not generate any preview images so it returns an empty list.
|
|
@@ -173,6 +176,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
173
176
|
|
|
174
177
|
return preview_paths
|
|
175
178
|
|
|
179
|
+
@monitor_performance
|
|
176
180
|
def overlay_fields(self, farmlands_np: np.ndarray) -> np.ndarray | None:
|
|
177
181
|
"""Overlay fields on the farmlands preview image.
|
|
178
182
|
|
|
@@ -200,6 +204,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
200
204
|
# use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
|
|
201
205
|
return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
|
|
202
206
|
|
|
207
|
+
@monitor_performance
|
|
203
208
|
def _add_farmlands(self) -> None:
|
|
204
209
|
"""Adds farmlands to the InfoLayer PNG file."""
|
|
205
210
|
farmlands = []
|
|
@@ -297,6 +302,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
297
302
|
|
|
298
303
|
self.preview_paths["farmlands"] = info_layer_farmlands_path
|
|
299
304
|
|
|
305
|
+
@monitor_performance
|
|
300
306
|
def _add_plants(self) -> None:
|
|
301
307
|
"""Adds plants to the InfoLayer PNG file."""
|
|
302
308
|
grass_layer = self.map.get_texture_layer(by_usage="grass")
|
|
@@ -399,6 +405,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
399
405
|
|
|
400
406
|
self.logger.debug("Updated density map for fruits saved in %s.", density_map_fruit_path)
|
|
401
407
|
|
|
408
|
+
@monitor_performance
|
|
402
409
|
def create_island_of_plants(self, image: np.ndarray, count: int) -> np.ndarray:
|
|
403
410
|
"""Create an island of plants in the image.
|
|
404
411
|
|
|
@@ -495,6 +502,7 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
495
502
|
image_np[:, -1] = 0 # Right side
|
|
496
503
|
return image_np
|
|
497
504
|
|
|
505
|
+
@monitor_performance
|
|
498
506
|
def _process_environment(self) -> None:
|
|
499
507
|
info_layer_environment_path = self.game.get_environment_path(self.map_directory)
|
|
500
508
|
if not info_layer_environment_path or not os.path.isfile(info_layer_environment_path):
|
|
@@ -534,6 +542,17 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
534
542
|
|
|
535
543
|
for layer in texture_component.get_area_type_layers():
|
|
536
544
|
pixel_value = area_type_to_pixel_value(layer.area_type) # type: ignore
|
|
545
|
+
# * Not enabled for now.
|
|
546
|
+
# * If the layer is invisible, we need to draw the mask from the info layer.
|
|
547
|
+
# if layer.invisible:
|
|
548
|
+
# self.logger.debug("Processing invisible area type layer: %s.", layer.name)
|
|
549
|
+
# if layer.info_layer:
|
|
550
|
+
# self.logger.debug("Info layer available: %s.", layer.info_layer)
|
|
551
|
+
# weight_image = self.draw_invisible_layer_mask(layer, environment_size)
|
|
552
|
+
# else:
|
|
553
|
+
# self.logger.debug("No info layer available for layer: %s.", layer.name)
|
|
554
|
+
# continue
|
|
555
|
+
# else:
|
|
537
556
|
weight_image = self.get_resized_weight(layer, environment_size) # type: ignore
|
|
538
557
|
if weight_image is None:
|
|
539
558
|
self.logger.warning("Weight image for area type layer not found in %s.", layer.name)
|
|
@@ -554,6 +573,53 @@ class GRLE(ImageComponent, XMLComponent):
|
|
|
554
573
|
self.logger.debug("Environment InfoLayer PNG file saved: %s.", info_layer_environment_path)
|
|
555
574
|
self.preview_paths["environment"] = info_layer_environment_path
|
|
556
575
|
|
|
576
|
+
# def draw_invisible_layer_mask(self, layer: Layer, resize_to: int) -> np.ndarray:
|
|
577
|
+
# """Draw the mask for the invisible layer.
|
|
578
|
+
|
|
579
|
+
# Arguments:
|
|
580
|
+
# layer (Layer): The layer for which to draw the mask.
|
|
581
|
+
# resize_to (int): The size to which the mask should be resized.
|
|
582
|
+
|
|
583
|
+
# Returns:
|
|
584
|
+
# np.ndarray: The resized mask.
|
|
585
|
+
# """
|
|
586
|
+
# mask = np.zeros((self.map.size, self.map.size), dtype=np.uint8)
|
|
587
|
+
# polygons = self.get_infolayer_data(Parameters.TEXTURES, layer.info_layer)
|
|
588
|
+
# self.logger.debug("Found %d polygons in info layer %s.", len(polygons), layer.info_layer)
|
|
589
|
+
|
|
590
|
+
# for polygon in polygons:
|
|
591
|
+
# try:
|
|
592
|
+
# fitted_polygon = self.fit_object_into_bounds(
|
|
593
|
+
# polygon_points=polygon,
|
|
594
|
+
# # margin=self.map.grle_settings.farmland_margin,
|
|
595
|
+
# angle=self.rotation,
|
|
596
|
+
# )
|
|
597
|
+
# except ValueError as e:
|
|
598
|
+
# self.logger.debug(
|
|
599
|
+
# "Polygon could not be fitted into the map bounds with error: %s",
|
|
600
|
+
# e,
|
|
601
|
+
# )
|
|
602
|
+
# continue
|
|
603
|
+
# polygon_np = self.polygon_points_to_np(fitted_polygon)
|
|
604
|
+
|
|
605
|
+
# try:
|
|
606
|
+
# cv2.fillPoly(mask, [polygon_np], (float(255),)) # type: ignore
|
|
607
|
+
# except Exception as e:
|
|
608
|
+
# self.logger.debug(
|
|
609
|
+
# "Polygon could not be added to the mask with error: %s",
|
|
610
|
+
# e,
|
|
611
|
+
# )
|
|
612
|
+
# continue
|
|
613
|
+
|
|
614
|
+
# resized_mask = cv2.resize(
|
|
615
|
+
# mask,
|
|
616
|
+
# (resize_to, resize_to),
|
|
617
|
+
# interpolation=cv2.INTER_NEAREST,
|
|
618
|
+
# )
|
|
619
|
+
|
|
620
|
+
# return resized_mask
|
|
621
|
+
|
|
622
|
+
@monitor_performance
|
|
557
623
|
def get_resized_weight(
|
|
558
624
|
self, layer: Layer, resize_to: int, dilations: int = 3
|
|
559
625
|
) -> np.ndarray | None:
|
|
@@ -12,12 +12,14 @@ import cv2
|
|
|
12
12
|
import numpy as np
|
|
13
13
|
from tqdm import tqdm
|
|
14
14
|
|
|
15
|
+
from maps4fs.generator.component.base.component_image import ImageComponent
|
|
15
16
|
from maps4fs.generator.component.base.component_xml import XMLComponent
|
|
17
|
+
from maps4fs.generator.monitor import monitor_performance
|
|
16
18
|
from maps4fs.generator.settings import Parameters
|
|
17
19
|
|
|
18
20
|
NODE_ID_STARTING_VALUE = 2000
|
|
19
21
|
SPLINES_NODE_ID_STARTING_VALUE = 5000
|
|
20
|
-
TREE_NODE_ID_STARTING_VALUE =
|
|
22
|
+
TREE_NODE_ID_STARTING_VALUE = 30000
|
|
21
23
|
|
|
22
24
|
FIELDS_ATTRIBUTES = [
|
|
23
25
|
("angle", "integer", "0"),
|
|
@@ -29,7 +31,7 @@ FIELDS_ATTRIBUTES = [
|
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
|
|
32
|
-
class I3d(XMLComponent):
|
|
34
|
+
class I3d(XMLComponent, ImageComponent):
|
|
33
35
|
"""Component for map i3d file settings and configuration.
|
|
34
36
|
|
|
35
37
|
Arguments:
|
|
@@ -112,6 +114,7 @@ class I3d(XMLComponent):
|
|
|
112
114
|
|
|
113
115
|
self.save_tree(tree)
|
|
114
116
|
|
|
117
|
+
@monitor_performance
|
|
115
118
|
def _add_splines(self) -> None:
|
|
116
119
|
"""Adds splines to the map I3D file."""
|
|
117
120
|
splines_i3d_path = self.game.splines_file_path(self.map_directory)
|
|
@@ -240,6 +243,7 @@ class I3d(XMLComponent):
|
|
|
240
243
|
|
|
241
244
|
self.assets.splines = splines_i3d_path
|
|
242
245
|
|
|
246
|
+
@monitor_performance
|
|
243
247
|
def _add_fields(self) -> None:
|
|
244
248
|
"""Adds fields to the map I3D file."""
|
|
245
249
|
tree = self.get_tree()
|
|
@@ -498,6 +502,7 @@ class I3d(XMLComponent):
|
|
|
498
502
|
|
|
499
503
|
return choice(trees_by_leaf_type)
|
|
500
504
|
|
|
505
|
+
@monitor_performance
|
|
501
506
|
def _add_forests(self) -> None:
|
|
502
507
|
"""Adds forests to the map I3D file."""
|
|
503
508
|
tree_schema = self._read_tree_schema()
|
|
@@ -686,9 +691,13 @@ class I3d(XMLComponent):
|
|
|
686
691
|
|
|
687
692
|
return recommended_step if not current_step else max(recommended_step, current_step)
|
|
688
693
|
|
|
689
|
-
def get_not_resized_dem(self) -> np.ndarray | None:
|
|
694
|
+
def get_not_resized_dem(self, with_foundations: bool = False) -> np.ndarray | None:
|
|
690
695
|
"""Reads the not resized DEM image from the background component.
|
|
691
696
|
|
|
697
|
+
Arguments:
|
|
698
|
+
with_foundations (bool, optional): Whether to get the DEM with foundations.
|
|
699
|
+
Defaults to False.
|
|
700
|
+
|
|
692
701
|
Returns:
|
|
693
702
|
np.ndarray | None: The not resized DEM image or None if the image could not be read.
|
|
694
703
|
"""
|
|
@@ -697,11 +706,60 @@ class I3d(XMLComponent):
|
|
|
697
706
|
self.logger.warning("Background component not found.")
|
|
698
707
|
return None
|
|
699
708
|
|
|
700
|
-
|
|
709
|
+
dem_path = (
|
|
710
|
+
background_component.not_resized_with_foundations_path
|
|
711
|
+
if with_foundations
|
|
712
|
+
else background_component.not_resized_path
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
if not dem_path or not os.path.isfile(dem_path):
|
|
701
716
|
self.logger.warning("Not resized DEM path not found.")
|
|
702
717
|
return None
|
|
703
718
|
|
|
704
|
-
not_resized_dem = cv2.imread(
|
|
719
|
+
not_resized_dem = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED)
|
|
720
|
+
|
|
721
|
+
return not_resized_dem
|
|
722
|
+
|
|
723
|
+
def get_not_resized_dem_with_foundations(
|
|
724
|
+
self, allow_fallback: bool = False
|
|
725
|
+
) -> np.ndarray | None:
|
|
726
|
+
"""Gets the not resized DEM with foundations. If the DEM with foundations is not found
|
|
727
|
+
and allow_fallback is True, the method returns the not resized DEM without foundations.
|
|
728
|
+
|
|
729
|
+
Arguments:
|
|
730
|
+
allow_fallback (bool, optional): Whether to allow fallback to DEM without
|
|
731
|
+
foundations. Defaults to False.
|
|
732
|
+
|
|
733
|
+
Returns:
|
|
734
|
+
np.ndarray | None: The not resized DEM image or None if the image could not be read.
|
|
735
|
+
"""
|
|
736
|
+
dem_with_foundations = self.get_not_resized_dem(with_foundations=True)
|
|
737
|
+
|
|
738
|
+
if dem_with_foundations is not None:
|
|
739
|
+
return dem_with_foundations
|
|
740
|
+
self.logger.warning("Not resized DEM with foundations not found.")
|
|
741
|
+
if allow_fallback:
|
|
742
|
+
return self.get_not_resized_dem(with_foundations=False)
|
|
743
|
+
return None
|
|
744
|
+
|
|
745
|
+
def get_not_resized_dem_with_flattened_roads(self) -> np.ndarray | None:
|
|
746
|
+
"""Gets the not resized DEM with flattened roads.
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
np.ndarray | None: The not resized DEM image or None if the image could not be read.
|
|
750
|
+
"""
|
|
751
|
+
background_component = self.map.get_background_component()
|
|
752
|
+
if not background_component:
|
|
753
|
+
self.logger.warning("Background component not found.")
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
dem_path = background_component.not_resized_with_flattened_roads_path
|
|
757
|
+
|
|
758
|
+
if not dem_path or not os.path.isfile(dem_path):
|
|
759
|
+
self.logger.warning("Not resized DEM with flattened roads path not found.")
|
|
760
|
+
return None
|
|
761
|
+
|
|
762
|
+
not_resized_dem = cv2.imread(dem_path, cv2.IMREAD_UNCHANGED)
|
|
705
763
|
|
|
706
764
|
return not_resized_dem
|
|
707
765
|
|
|
@@ -21,6 +21,16 @@ class Layer:
|
|
|
21
21
|
usage (str | None): Usage of the layer.
|
|
22
22
|
background (bool): Flag to determine if the layer is a background.
|
|
23
23
|
invisible (bool): Flag to determine if the layer is invisible.
|
|
24
|
+
procedural (list[str] | None): List of procedural textures to apply.
|
|
25
|
+
border (int | None): Border size in pixels.
|
|
26
|
+
precise_tags (dict[str, str | list[str]] | None): Dictionary of precise tags to search for.
|
|
27
|
+
precise_usage (str | None): Precise usage of the layer.
|
|
28
|
+
area_type (str | None): Type of the area (e.g., residential, commercial).
|
|
29
|
+
area_water (bool): Flag to determine if the area is water.
|
|
30
|
+
indoor (bool): Flag to determine if the layer is indoor.
|
|
31
|
+
merge_into (str | None): Name of the layer to merge into.
|
|
32
|
+
building_category (str | None): Category of the building.
|
|
33
|
+
external (bool): External layers not being used by the game directly.
|
|
24
34
|
|
|
25
35
|
Attributes:
|
|
26
36
|
name (str): Name of the layer.
|
|
@@ -50,6 +60,9 @@ class Layer:
|
|
|
50
60
|
area_water: bool = False,
|
|
51
61
|
indoor: bool = False,
|
|
52
62
|
merge_into: str | None = None,
|
|
63
|
+
building_category: str | None = None,
|
|
64
|
+
external: bool = False,
|
|
65
|
+
road_texture: str | None = None,
|
|
53
66
|
):
|
|
54
67
|
self.name = name
|
|
55
68
|
self.count = count
|
|
@@ -70,6 +83,9 @@ class Layer:
|
|
|
70
83
|
self.area_water = area_water
|
|
71
84
|
self.indoor = indoor
|
|
72
85
|
self.merge_into = merge_into
|
|
86
|
+
self.building_category = building_category
|
|
87
|
+
self.external = external
|
|
88
|
+
self.road_texture = road_texture
|
|
73
89
|
|
|
74
90
|
def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
|
|
75
91
|
"""Returns dictionary with layer data.
|
|
@@ -96,6 +112,9 @@ class Layer:
|
|
|
96
112
|
"area_water": self.area_water,
|
|
97
113
|
"indoor": self.indoor,
|
|
98
114
|
"merge_into": self.merge_into,
|
|
115
|
+
"building_category": self.building_category,
|
|
116
|
+
"external": self.external,
|
|
117
|
+
"road_texture": self.road_texture,
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
data = {k: v for k, v in data.items() if v is not None}
|