gemf-map 0.3.0__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.
gemf/gemf_dump.py ADDED
@@ -0,0 +1,806 @@
1
+ """
2
+ This submodule defines the core functionality of the package through the `GEMF` class.
3
+ """
4
+
5
+ from collections.abc import Callable
6
+ import os
7
+ import shutil
8
+ import json
9
+ import warnings
10
+
11
+ import numpy as np
12
+
13
+ from .utils import FORMAT_PATTERNS, get_image_format, kwargify, listdirs, to_json
14
+ from .tiles import AbstractRange, BufferTile, NamedTileBase, Tile, TileCollection, TileRange
15
+
16
+
17
+ ENCODING = "ascii"
18
+ ENCODING_ERROR = "ignore"
19
+
20
+ BYTES_DATA = 4
21
+ BYTES_OFFSET = 8
22
+
23
+
24
+ # auxiliary methods, used to read encoded binary data
25
+ def _read_value(f, N):
26
+ """Read N bytes from a binary file and return as int."""
27
+ value_bytes = f.read(N)
28
+ return int.from_bytes(value_bytes, "big")
29
+
30
+ def _read_int(f):
31
+ """Read int as encoded in GEMF."""
32
+ return _read_value(f, BYTES_DATA)
33
+
34
+ def _read_offset(f):
35
+ """Read offset as encoded in GEMF."""
36
+ return _read_value(f, BYTES_OFFSET)
37
+
38
+ def _read_string(f, N):
39
+ """Read string as encoded in GEMF."""
40
+ str_bytes = f.read(N)
41
+ return str_bytes.decode(ENCODING)
42
+
43
+
44
+ # encoding utilities, used to write binary gemf file
45
+ def _encode_data(value):
46
+ """Encode a data integer, i.e. as 4 bytes."""
47
+ return (value).to_bytes(BYTES_DATA, "big")
48
+
49
+ def _encode_offset(value):
50
+ """Encode an offset integer, i.e. as 8 bytes."""
51
+ return (value).to_bytes(BYTES_OFFSET, "big")
52
+
53
+ def _encode_string(value):
54
+ """Encode a string, with variable byte length."""
55
+ return value.encode(ENCODING, ENCODING_ERROR)
56
+
57
+
58
+
59
+ # GEMF classes
60
+ class GEMFValueBase:
61
+ """
62
+ Base class defining basic GEMF type behavior. Python primitives are wrapped in
63
+ custom GEMF classes for easier integration in the GEMF workflow.
64
+ """
65
+ def __str__(self): return super().__str__()
66
+ def __repr__(self): return f"{type(self).__name__}: {super().__str__()}"
67
+
68
+ def __len__(self):
69
+ """Return the encoded size."""
70
+ return len(self._encode(self))
71
+
72
+ def write(self, f, *args, **kwargs):
73
+ """Write the encoded value to a binary gemf file."""
74
+ return f.write(self._encode(self), *args, **kwargs)
75
+
76
+
77
+ class GEMFIntBase(int, GEMFValueBase):
78
+ """Base class for int-like GEMF types."""
79
+ def __new__(cls, value: int, encoding_method: Callable[[int], None]):
80
+ assert isinstance(value, int) or np.can_cast(value, int), f"Can't cast {type(value)} to int."
81
+ obj = int.__new__(cls, value)
82
+ obj._encode = encoding_method
83
+ return obj
84
+
85
+
86
+ class GEMFInt(GEMFIntBase):
87
+ """Class representing a 4 byte integer."""
88
+ def __new__(cls, value: int) -> None:
89
+ obj = GEMFIntBase.__new__(cls, value, _encode_data)
90
+ return obj
91
+
92
+
93
+ class GEMFOffset(GEMFIntBase):
94
+ """Class representing a 8 byte integer, aka 'offset'."""
95
+ def __new__(cls, value: int) -> None:
96
+ obj = GEMFIntBase.__new__(cls, value, _encode_offset)
97
+ return obj
98
+
99
+
100
+ class GEMFString(GEMFValueBase, str):
101
+ """Class representing a variable size string."""
102
+ def __new__(cls, value: str, encoding: str = ENCODING, error: str = ENCODING_ERROR) -> None:
103
+ obj = str.__new__(cls, value)
104
+ obj.encoding = encoding
105
+ obj.error = error
106
+ obj._encode = _encode_string
107
+ return obj
108
+
109
+
110
+ class GEMFList:
111
+ """An iterable of GEMF objects."""
112
+ def __init__(self, items: list) -> None:
113
+ self.items = items
114
+
115
+ def __len__(self):
116
+ """Sum of byte-length of member items."""
117
+ return sum([len(item) for item in self.items])
118
+
119
+ def __iter__(self): return iter(self.items)
120
+ def __getitem__(self, idx): return self.items[idx]
121
+
122
+ def to_dict(self, *args, **kwargs):
123
+ """Return a list of dict-representation of the member items."""
124
+ list_dict = []
125
+ for item in self.items:
126
+ if hasattr(item, "to_dict"):
127
+ list_dict.append(item.to_dict())
128
+ else:
129
+ list_dict.append(to_json(item))
130
+ return list_dict
131
+
132
+ def __str__(self) -> str: return str(self.to_dict())
133
+
134
+ def append(self, item):
135
+ if len(self.items): assert isinstance(item, type(self[0])), "Items must be of equal type."
136
+ return self.items.append(item)
137
+
138
+ def extend(self, items):
139
+ if len(self.items): assert all([isinstance(item, type(self[0])) for item in items]), "Items must be of equal type."
140
+ return self.items.extend(items)
141
+
142
+ def write(self, f, *args, **kwargs):
143
+ """Sequentially write binary representation of member items."""
144
+ for item in self.items:
145
+ item.write(f, *args, **kwargs)
146
+
147
+
148
+ class GEMFSectionBase:
149
+ """
150
+ Base class representing a GEMF section.
151
+
152
+ **Properties**
153
+ - the length of a section equals the length of its binary representation.
154
+ - the `write()` method is responsible for ultimately serializing the map as a binary GEMF file
155
+ """
156
+
157
+ def __len__(self):
158
+ """
159
+ The length of a section equals the length of its binary representation.
160
+ It is the sum of the binary length of its children elements.
161
+ """
162
+ return sum([len(value) for key, value in self.__dict__.items() if not key.startswith("_")])
163
+
164
+ def __str__(self) -> str:
165
+ return json.dumps(self.to_dict(), indent=2)
166
+
167
+ def to_dict(self, ignore_private: bool = True) -> dict:
168
+ """
169
+ Recursively build a dictionary representation of the GEMF structure.
170
+ Attributes starting with an underscore are supressed by default.
171
+ """
172
+ out_dict = {}
173
+
174
+ for key, el in self.__dict__.items():
175
+ if ignore_private and key.startswith("_"): continue
176
+
177
+ if hasattr(el, "to_dict"):
178
+ out_dict[key] = el.to_dict(ignore_private)
179
+ else:
180
+ out_dict[key] = to_json(el)
181
+
182
+ return out_dict
183
+
184
+ def write(self, f, *args, **kwargs):
185
+ """
186
+ Recursively write the binary representation of the object and its child objects.
187
+ Attributes starting with an underscore are not included in the serialization.
188
+ """
189
+ for key, val in self.__dict__.items():
190
+ if key.startswith("_"): continue
191
+ val.write(f, *args, **kwargs)
192
+
193
+
194
+ class ObjectDescriptor:
195
+ """Utility class with the purpose of renaming general fieldnames of `GEMFList` to subclass-specific names."""
196
+ def __init__(self, name) -> None:
197
+ self.name = name
198
+
199
+ def __get__(self, obj, objtype=None):
200
+ value = getattr(obj, self.name)
201
+ return value
202
+
203
+ def __set__(self, obj, value):
204
+ setattr(obj, self.name, value)
205
+
206
+
207
+ class GEMFListSectionBase(GEMFSectionBase):
208
+ """
209
+ Base class for GEMF sections which are comprised of two attributes:
210
+ - a count of child elements
211
+ - a list of child elements
212
+ """
213
+ _items = ObjectDescriptor("items")
214
+ _num_items = ObjectDescriptor("num_items")
215
+
216
+ def __init__(self, itemlist: GEMFList) -> None:
217
+ self._num_items = GEMFInt(len(itemlist.items))
218
+ self._items = itemlist
219
+
220
+ def __getitem__(self, idx): return self._items[idx]
221
+ def __iter__(self): return iter(self._items)
222
+
223
+
224
+ class HeaderInfo(GEMFSectionBase):
225
+ """
226
+ GEMF meta-information as specified in section '3.1 Overall Header' of the format specification.
227
+
228
+ # Parameters
229
+ - `GEMF version`: file format version
230
+ - `tile_size`: size of contained tiles
231
+ """
232
+ def __init__(self, version: int, tile_size: int) -> None:
233
+ self.version = GEMFInt(version)
234
+ self.tile_size = GEMFInt(tile_size)
235
+
236
+
237
+ class GEMFSource(GEMFSectionBase):
238
+ """
239
+ A single GEMF source, aka tile provider (see section '3.2 Source Data')
240
+
241
+ # Parameters
242
+ - `index`: source index, 0-indexed
243
+ - `name_length`: length of encoded source name
244
+ - `name`: encoded source name
245
+ """
246
+ def __init__(self, index: int, name: str) -> None:
247
+ self.index = GEMFInt(index)
248
+ self.name_length = GEMFInt(len(name.encode(ENCODING, ENCODING_ERROR)))
249
+ self.name = GEMFString(name)
250
+
251
+
252
+ class SourceList(GEMFList):
253
+ """Wrap a list of `GEMFSource`s."""
254
+ def __init__(self, sources: list[GEMFSource]) -> None:
255
+ super().__init__(sources)
256
+
257
+
258
+ class SourceData(GEMFListSectionBase):
259
+ """
260
+ Information on the contained sources as specified in section '3.2 Source Data' of the format specification.
261
+
262
+ # Parameters
263
+ - `num_sources`: number of contained sources
264
+ - `sources`: list of sources
265
+ """
266
+ # renaming the attributes of `GEMFListSectionBase` to adhere with the specification naming convention
267
+ _num_items = ObjectDescriptor("num_sources")
268
+ _items = ObjectDescriptor("sources")
269
+
270
+ def __init__(self, sourcelist: SourceList) -> None:
271
+ super().__init__(sourcelist)
272
+
273
+ @classmethod
274
+ def from_root(cls, root_dir: str):
275
+ """Create a `SourceData` object from a root directory of tiles."""
276
+ gemfsources = []
277
+ for i, source_name in enumerate(listdirs(root_dir)):
278
+ gemfsources.append(GEMFSource(i, source_name))
279
+
280
+ return SourceData(SourceList(gemfsources))
281
+
282
+
283
+ class GEMFRange(GEMFSectionBase):
284
+ """
285
+ A single GEMF range, aka rectangular collection of tiles (see section '3.3 Range Data')
286
+
287
+ # Parameters
288
+ - `z`: the range's zoom level
289
+ - `xmin`: the range's minimum x coordinate
290
+ - `xmax`: the range's maximum x coordinate
291
+ - `ymin`: the range's minimum y coordinate
292
+ - `ymax`: the range's maximum y coordinate
293
+ - `src_index`: the index of the corresponding source
294
+ - `offset`: the start byte of the corresponding 'Range Detail' in the binary file
295
+ """
296
+ def __init__(self, z: int, xmin: int, xmax: int, ymin: int, ymax: int, src_index: int, details_offset: int) -> None:
297
+ self.z = GEMFInt(z)
298
+ self.xmin = GEMFInt(xmin)
299
+ self.xmax = GEMFInt(xmax)
300
+ self.ymin = GEMFInt(ymin)
301
+ self.ymax = GEMFInt(ymax)
302
+ self.src_index = GEMFInt(src_index)
303
+ self.offset = GEMFOffset(details_offset)
304
+
305
+ def get_zxy(self, index: int):
306
+ """Get the n-th tile of the range by index. Ranges are traversed in column-major order."""
307
+ tile = AbstractRange(self.z, self.xmin, self.xmax, self.ymin, self.ymax, major="col")[index]
308
+ return tile.z, tile.x, tile.y
309
+
310
+ @property
311
+ def num_tiles(self):
312
+ return len(AbstractRange(self.z, self.xmin, self.xmax, self.ymin, self.ymax, major="col"))
313
+
314
+
315
+ class RangeList(GEMFList):
316
+ """Wrap a list of `GEMFRange`s."""
317
+ def __init__(self, ranges: list[GEMFRange]) -> None:
318
+ super().__init__(ranges)
319
+
320
+
321
+ class RangeData(GEMFListSectionBase):
322
+ """
323
+ Information on the contained ranges as specified in section '3.3 Range Data' of the format specification.
324
+
325
+ # Parameters
326
+ - `num_ranges`: number of contained ranges
327
+ - `ranges`: list of ranges
328
+ """
329
+ # renaming the attributes of `GEMFListSectionBase` to adhere with the specification naming convention
330
+ _num_items = ObjectDescriptor("num_ranges")
331
+ _items = ObjectDescriptor("ranges")
332
+
333
+ @classmethod
334
+ def from_root(cls, root_dir: str, headerinfo: HeaderInfo, sourcedata: SourceData, mode: str = "split", **kwargs):
335
+ """
336
+ Create a `RangeData` object from the previously loaded GEMF objects.
337
+ Additionally, return a list of `TileRange` objects, which are required for further GEMF creation steps.
338
+ """
339
+ range_list = RangeList([])
340
+ tileranges = []
341
+
342
+ # traverse all source dirs
343
+ for i_source, source in enumerate(sourcedata.sources):
344
+
345
+ # collect tile ranges (aka rectangular collections of tiles)
346
+ tc = TileCollection.from_tiledir(os.path.join(root_dir, source.name)) # first, collect all tiles in source dir
347
+ tileranges_ = tc.to_tileranges(mode=mode, **kwargs) # split collection into valid tileranges
348
+ num_ranges = len(tileranges_)
349
+
350
+ OFFSET = len(headerinfo) + len(sourcedata) + len(RangeData.dummy(num_ranges)) + len(range_list) # initial offset for details based on previous sections and length of RangeData
351
+
352
+ # iterate over all found tile ranges
353
+ for tr_ in tileranges_:
354
+ # create corresponding GEMF object and collect objects
355
+ gemf_range = GEMFRange(tr_.z, tr_.xmin, tr_.xmax, tr_.ymin, tr_.ymax, i_source, OFFSET)
356
+ tileranges.append(tr_)
357
+ range_list.append(gemf_range)
358
+
359
+ # advance offset to block of range details, corresponding to the next range
360
+ num_tiles = len(tr_)
361
+ OFFSET += num_tiles * len(RangeDetail.dummy())
362
+
363
+ rangedata = cls(range_list)
364
+ return rangedata, tileranges
365
+
366
+ @property
367
+ def num_tiles(self):
368
+ """Number of tiles contained in `RangeData`."""
369
+ return sum([range.num_tiles for range in self.ranges])
370
+
371
+ @classmethod
372
+ def dummy(cls, num_ranges: int):
373
+ """Return a dummy instance of `RangeData` for easy computation of expected size."""
374
+ return cls(GEMFList([GEMFRange(0, 0, 0, 0, 0, 0, 0) for _ in range(num_ranges)]))
375
+
376
+ # def write(self, f):
377
+ # range_: GEMFRange
378
+
379
+ # for range_ in self:
380
+ # f.seek(range_.offset)
381
+ # for tile in range_:
382
+ # tile.write(f)
383
+
384
+
385
+ class RangeDetail(GEMFSectionBase):
386
+ """
387
+ A single GEMF range detail, aka tile info (see section '3.4 Range Details')
388
+
389
+ **Note**: This implementation does not fully follow the paradigm described in the format specification. Since the concept of a range detail
390
+ and the corresponding data is so closely related, we unify the two in the class `RangeDetail`. It both serves as the direct representation
391
+ of a GEMF range detail through the parameters `address` and `length`, and as such inherits from `GEMFSectionBase` for automatic serialization.
392
+ Additionally, derived classes shall provide the functionality to ultimately load and write binary tile data from associated tile objects.
393
+
394
+ # Parameters
395
+ - `address`: the start byte of the corresponding data in the binary file
396
+ - `length`: the length of the encoded data corresponding to this `RangeDetail`
397
+ """
398
+ def __init__(self, address: int, length: int, **kwargs) -> None:
399
+ self.address = GEMFOffset(address)
400
+ self.length = GEMFInt(length)
401
+
402
+ super().__init__(**kwargs)
403
+
404
+ def __len__(self): return len(self.address) + len(self.length)
405
+
406
+ @classmethod
407
+ def dummy(cls):
408
+ """Return a dummy instance of `RangeDetail` for easy computation of expected size."""
409
+ return cls(0, 0)
410
+
411
+ def to_dict(self):
412
+ return {"address": self.address, "length": self.length}
413
+
414
+ def write(self, f, *args, **kwargs):
415
+ for val in [self.address, self.length]:
416
+ val.write(f, *args, **kwargs)
417
+
418
+ def to_dict_data(self):
419
+ """Representation of subclasses with additional attributes for `GEMFDataSection`."""
420
+ return f"{type(self).__name__}"
421
+
422
+ def write_data(self, f):
423
+ data_bytes = self.load_bytes()
424
+ f.write(data_bytes)
425
+
426
+
427
+ class RangeDetails(GEMFList):
428
+ """
429
+ Information on the contained tiles as specified in section '3.4 Range Details' of the format specification.
430
+ """
431
+ def __init__(self, range_details: list[RangeDetail]) -> None:
432
+ super().__init__(range_details)
433
+
434
+ @classmethod
435
+ def from_root(cls, headerinfo: HeaderInfo, sourcedata: SourceData, rangedata: RangeData, tileranges: list[TileRange]):
436
+ """Create a `RangeDetails` object from the previously loaded GEMF objects and a set of `TileRange` objects."""
437
+ rangedetails = RangeDetails([])
438
+ # data = GEMFDataSection([])
439
+
440
+ # determine initial byte position after header, source data, range data and range details
441
+ OFFSET = len(headerinfo) + len(sourcedata) + len(rangedata) + len(RangeDetails.dummy(rangedata.num_tiles))
442
+
443
+ # iterate through all tileranges and create a RangeDetail from it
444
+ for i_range, tr_ in enumerate(tileranges):
445
+ for i_tile, tile in enumerate(tr_):
446
+ len_image_bytes = os.stat(tile.get_filepath()).st_size
447
+ # image_bytes = tile.load_bytes()
448
+ # len_image_bytes = len(image_bytes)
449
+ # data.append(GEMFTile.from_tile(tile, i_range, i_tile))
450
+
451
+ rangedetail = GEMFTile.from_tile(OFFSET, tile)
452
+ rangedetail._range_idx = i_range
453
+ rangedetail._tile_idx = i_tile
454
+
455
+ # rangedetail = RangeDetail(OFFSET, len_image_bytes)
456
+ # rangedetail._z = tile.z
457
+ # rangedetail._x = tile.x
458
+ # rangedetail._y = tile.y
459
+ rangedetails.append(rangedetail)
460
+
461
+ OFFSET += len_image_bytes
462
+
463
+ return rangedetails
464
+ # return rangedetails, data
465
+
466
+ @classmethod
467
+ def dummy(cls, num_tiles: int):
468
+ """Return a dummy instance of `RangeDetails` for easy computation of expected size."""
469
+ return cls([RangeDetail.dummy() for _ in range(num_tiles)])
470
+ # return cls([RangeDetail(0, 0) for _ in range(num_tiles)])
471
+
472
+ # def write(self, f):
473
+ # """Serialize the GEMF data section by loading the associated tile's data and writing to file."""
474
+ # print("writing RangeDetails")
475
+ # # iterate over range details
476
+ # for range_detail in self:
477
+ # data_bytes = range_detail.load_bytes()
478
+ # f.write(data_bytes)
479
+
480
+ # @staticmethod
481
+ # def write(f, rangedata: RangeData):
482
+ # range_: GEMFRange
483
+
484
+ # for range_ in rangedata:
485
+ # f.seek(range_.offset)
486
+ # for tile in range_:
487
+ # tile.write(f)
488
+
489
+
490
+ class GEMFHeaderSection(GEMFSectionBase):
491
+ """
492
+ All meta-information of the GEMF map file as specified in section '3. Header Area' of the format specification.
493
+ """
494
+ def __init__(self, header_info: HeaderInfo,
495
+ source_data: SourceData,
496
+ range_data: RangeData,
497
+ range_details: RangeDetails
498
+ ) -> None:
499
+ self.header_info = header_info
500
+ self.source_data = source_data
501
+ self.range_data = range_data
502
+ self.range_details = range_details
503
+
504
+
505
+ class GEMFReference(RangeDetail, NamedTileBase):
506
+ """
507
+ Direct successor of the `RangeDetail` class, holding a reference to an existing range detail section in a `.gemf` file.
508
+ """
509
+ # TODO: enable format passing
510
+ def __init__(self, address: int, length: int, file: str, z: int, x: int, y: int) -> None:
511
+ super().__init__(address=address, length=length, z=z, x=x, y=y, name="tile", format=None)
512
+ self._file = file
513
+
514
+
515
+ def load_bytes(self, f_src=None):
516
+ """Load the tile data from file. Optionally specify `f_src` if the associated source file is already open (useful for repeated access to a source file)."""
517
+
518
+ # define logic for reuse below
519
+ def _load(f_src):
520
+ f_src.seek(self.address)
521
+ data = f_src.read(self.length)
522
+ return data
523
+
524
+ # either open associated source file or use open file
525
+ if f_src is None:
526
+ with open(self._file, "rb") as f_in:
527
+ return _load(f_in)
528
+ else:
529
+ return _load(f_src)
530
+
531
+
532
+ class GEMFBufferTile(RangeDetail, BufferTile):
533
+ """
534
+ GEMF version of a buffered tile, i.e. a tile without a corresponding file on disk.
535
+ Used to hold binary data when greedily loading a `.gemf` file.
536
+
537
+ **Note**: not suitable for large `.gemf` files, as the whole image data may overwhelm memory capacity.
538
+ Instead, use `GEMFReference` for existing `.gemf` files or `GEMFTile` for tiles on disc.
539
+ """
540
+ def __init__(self, address: int, length: int, z: int, x: int, y: int, data: bytes, **kwargs) -> None:
541
+ # def __init__(self, address: int, length: int, z: int, x: int, y: int, data: bytes, range_idx: int, tile_idx: int, **kwargs) -> None:
542
+ super().__init__(
543
+ address=address, length=length, # params for init of RangeDetail
544
+ z=z, x=x, y=y, data=data, **kwargs # params and additional kwargs for init of BufferTile
545
+ )
546
+ # super(RangeDetail, self).__init__(address, length)
547
+ # RangeDetail.__init__(self, address, length)
548
+ # super(BufferTile, self).__init__(z, x, y, data, **kwargs)
549
+ # BufferTile.__init__(self, z, x, y, data, **kwargs)
550
+
551
+ # def __init__(self, z: int, x: int, y: int, range_idx: int, tile_idx: int, data: bytes) -> None:
552
+ # GEMFTileBase.__init__(self)
553
+ # BufferTile.__init__(self, z, x, y, data, allow_invalid=False)
554
+
555
+ # self._range_idx = range_idx
556
+ # self._tile_idx = tile_idx
557
+
558
+
559
+ class GEMFTile(RangeDetail, Tile):
560
+ """
561
+ GEMF version of a regular tile, i.e. a tile with a corresponding file on disk.
562
+ Used to load binary data when creating a `.gemf` file from tiles.
563
+ """
564
+ def __init__(self, address: int, z: int, x: int, y: int, tiledir: str, name: str, format: str) -> None:
565
+ # def __init__(self, address: int, z: int, x: int, y: int, range_idx: int, tile_idx: int, tiledir: str, name: str, format: str) -> None:
566
+ filepath = Tile.compose_filepath(z, x, y, tiledir, name, format)
567
+ length = os.stat(filepath).st_size
568
+ super().__init__(length=length, **kwargify(locals()))
569
+ # def __init__(self, address: int, tile_file: str, range_idx: int, tile_idx: int) -> None:
570
+
571
+
572
+ @classmethod
573
+ def from_file(cls, tile_file: str, address: int) -> None:
574
+ return cls(address=address, **Tile.parse_filepath(tile_file))
575
+ # super().__init__(self, address=address, length=length, **Tile.parse_filepath(tile_file))
576
+
577
+
578
+ # def __init__(self, z: int, x: int, y: int, range_idx: int, tile_idx: int, tiledir: str, name: str, format: str) -> None:
579
+ # GEMFTileBase.__init__(self)
580
+ # Tile.__init__(self, z, x, y, tiledir, name, format, allow_invalid=False)
581
+ # self._range_idx = range_idx
582
+ # self._tile_idx = tile_idx
583
+
584
+ @classmethod
585
+ def from_tile(cls, address: int, tile: Tile):
586
+ # def from_tile(cls, address: int, length: int, tile: Tile, range_idx: int, tile_idx: int):
587
+ """Utility constructor to instantiate a `GEMFTile` from a regular `Tile`."""
588
+ return cls(address, tile.z, tile.x, tile.y, tile.tiledir, tile.name, tile.format)
589
+ # return cls(address, length, tile.z, tile.x, tile.y, range_idx, tile_idx, tile.tiledir, tile.name, tile.format)
590
+
591
+
592
+ class GEMFDataSection(RangeDetails):
593
+ """
594
+ Sequential aggregate of encoded tile data as specified in section '4. Data Area' of the format specification.
595
+
596
+ **Note**: This class is lazy, i.e. it does not load the tile data into memory. It only stores the necessary information to retrieve the binary tile data when requested.
597
+ Hence, in this implementation, the `GEMFDataSection` only differs from `RangeDetails` in the serialization behavior, but holds the same detail data internally.
598
+ """
599
+ def __init__(self, range_details: list[RangeDetail]) -> None:
600
+ super().__init__(range_details)
601
+
602
+ def __len__(self):
603
+ """Sum of byte-length of member data."""
604
+ return sum([detail.length for detail in self])
605
+
606
+ def to_dict(self, *args, **kwargs):
607
+ return [item.to_dict_data() for item in self]
608
+ # return [item.str_data() for item in self]
609
+
610
+ # override the default writing behavior of writing the instance's attributes sequentially
611
+ def write(self, f):
612
+ """Serialize the GEMF data section by loading the associated tile's data and writing to file."""
613
+ print("writing DataSection")
614
+ # iterate over range details
615
+ for range_detail in self:
616
+ data_bytes = range_detail.load_bytes()
617
+ f.write(data_bytes)
618
+
619
+
620
+ class GEMF(GEMFSectionBase):
621
+ """
622
+ Core class to read and write map files of the GEMF format. For a detailed description of the file format,
623
+ see https://www.cgtk.co.uk/gemf.
624
+
625
+ The `GEMF` class supports...
626
+ - reading `.gemf` map files via the `from_file()` classmethod
627
+ - creating a GEMF object from PNG or JPG tiles via the `from_tiles()` classmethod
628
+ - writing the newly created GEMF object to file via the `write()` method
629
+
630
+ Further features are...
631
+ - extracting tiles (PNG or JPG) from binary `.gemf` files via the `save_tiles()` method
632
+ - adding tiles to an existing `.gemf` file (TODO)
633
+ """
634
+ # def __init__(self, header_info: HeaderInfo, source_data: SourceData, range_data: RangeData, range_details: RangeDetails) -> None:
635
+ def __init__(self, gemf_header: GEMFHeaderSection, gemf_data: GEMFDataSection) -> None:
636
+ self.header = gemf_header
637
+ self.data = gemf_data
638
+
639
+ # self.header_info = header_info
640
+ # self.source_data = source_data
641
+ # self.range_data = range_data
642
+ # self.range_details = range_details
643
+
644
+ @classmethod
645
+ def from_file(cls, gemf_file: str):
646
+ """Read a `.gemf` file from file."""
647
+ gemf = cls(None, None) # instantiate empty `GEMF` object
648
+ # gemf = cls(None, None, None, None)
649
+ gemf._src = gemf_file
650
+
651
+ with open(gemf_file, "rb") as f: # populate object
652
+ gemf._read_gemf(f)
653
+
654
+ return gemf
655
+
656
+ @classmethod
657
+ def from_tiles(cls, root_dir: str, mode: str = "split", version: int = 4, tile_size: int = 256):
658
+ """Create a `GEMF` object from tiles."""
659
+ gemf = cls(None, None)
660
+
661
+ # TODO: auto-tilesize?
662
+
663
+ header_info = HeaderInfo(version, tile_size)
664
+ source_data = SourceData.from_root(root_dir)
665
+ range_data, tileranges = RangeData.from_root(root_dir, header_info, source_data, mode=mode)
666
+
667
+ range_details = RangeDetails.from_root(header_info, source_data, range_data, tileranges)
668
+ # rangedetails, data = RangeDetails.from_root(headerinfo, sourcedata, rangedata, tileranges)
669
+ # header = GEMFHeaderSection(header_info, source_data, range_data, range_details)
670
+
671
+ gemf.header = GEMFHeaderSection(header_info, source_data, range_data, range_details)
672
+ gemf.data = GEMFDataSection(range_details.items)
673
+
674
+ gemf._src = root_dir
675
+
676
+ return gemf
677
+
678
+ def write(self, gemf_file: str):
679
+ """Serialize the `GEMF` object to file."""
680
+ os.makedirs(os.path.dirname(gemf_file), exist_ok=True)
681
+
682
+ with open(gemf_file, "wb") as f:
683
+ super().write(f)
684
+
685
+
686
+ # TODO: editing methods
687
+ def add_source(self): pass
688
+ def add_range(self): pass
689
+
690
+ def save_tiles(self, tiledir_root: str, save_empty: bool = False):
691
+ """Save the `GEMF` object's tiles to file."""
692
+ # TODO: leave .gemf file open if data is just References?
693
+
694
+ if os.path.exists(tiledir_root):
695
+ shutil.rmtree(tiledir_root)
696
+ os.makedirs(tiledir_root)
697
+
698
+ # create all source subdirectories
699
+ for source in self.header.source_data:
700
+ os.makedirs(os.path.join(tiledir_root, source.name))
701
+
702
+ # write tiles
703
+ for data in self.data:
704
+ src = self.header.source_data[self.header.range_data[data._range_idx].src_index]
705
+
706
+ if data.length > 0 or save_empty:
707
+ data.save(os.path.join(tiledir_root, src.name))
708
+
709
+
710
+ def _read_gemf(self, f):
711
+ # def _read_gemf(self, f, greedy: bool = False):
712
+ # self.header_info = self._read_header(f)
713
+ # self.source_data = self._read_source_data(f, self.header_info)
714
+ # self.range_data = self._read_range_data(f, self.header_info, self.source_data)
715
+ # self.range_details = self._read_range_details(f, self.header_info, self.source_data, self.range_data)
716
+
717
+ header_info = self._read_header(f)
718
+ source_data = self._read_source_data(f, header_info)
719
+ range_data = self._read_range_data(f, header_info, source_data)
720
+ range_details = self._read_range_details(f, header_info, source_data, range_data)
721
+
722
+ self.header = GEMFHeaderSection(header_info, source_data, range_data, range_details)
723
+ self.data = GEMFDataSection(range_details.items)
724
+
725
+ def _read_header(self, f):
726
+ f.seek(0)
727
+ gemf_version = _read_int(f)
728
+ tile_size = _read_int(f)
729
+ headerinfo = HeaderInfo(gemf_version, tile_size)
730
+ return headerinfo
731
+
732
+ def _read_source_data(self, f, headerinfo: HeaderInfo):
733
+ f.seek(len(headerinfo))
734
+ num_sources = _read_int(f)
735
+ sourcelist = SourceList([])
736
+
737
+ for i_src in range(num_sources):
738
+ src_idx = _read_int(f)
739
+ assert i_src == src_idx
740
+ name_length = _read_int(f)
741
+ name = _read_string(f, name_length)
742
+
743
+ sourcelist.append(GEMFSource(i_src, name))
744
+
745
+ sourcedata = SourceData(sourcelist)
746
+ return sourcedata
747
+
748
+ def _read_range_data(self, f, headerinfo: HeaderInfo, sourcedata: SourceData):
749
+ f.seek(len(headerinfo) + len(sourcedata))
750
+ num_ranges = _read_int(f)
751
+
752
+ rangelist = RangeList([])
753
+ for _ in range(num_ranges):
754
+ z = _read_int(f)
755
+ xmin = _read_int(f)
756
+ xmax = _read_int(f)
757
+ ymin = _read_int(f)
758
+ ymax = _read_int(f)
759
+ src_idx = _read_int(f)
760
+ offset_details = _read_offset(f)
761
+
762
+ rangelist.append(GEMFRange(z, xmin, xmax, ymin, ymax, src_idx, offset_details))
763
+
764
+ return RangeData(rangelist)
765
+
766
+ def _read_range_details(self, f, headerinfo: HeaderInfo, sourcedata: SourceData, rangedata: RangeData):
767
+ f.seek(len(headerinfo) + len(sourcedata) + len(rangedata))
768
+
769
+ rangedetails = RangeDetails([])
770
+ for i_range, range_ in enumerate(rangedata):
771
+ for i_tile in range(range_.num_tiles):
772
+ address = _read_offset(f)
773
+ len_data = _read_int(f)
774
+
775
+ range_: RangeData
776
+
777
+ range_ = rangedata[i_range]
778
+ z, x, y = range_.get_zxy(i_tile)
779
+
780
+ rangedetail = GEMFReference(address, len_data, f.name, z, x, y)
781
+ # rangedetail = RangeDetail(address, len_data)
782
+ rangedetail._range_idx = i_range
783
+ rangedetail._tile_idx = i_tile
784
+
785
+ rangedetails.append(rangedetail)
786
+ return rangedetails
787
+
788
+ def _read_datasection(self, f, headerinfo: HeaderInfo, sourcedata: SourceData, rangedata: RangeData, rangedetails: RangeDetails):
789
+ ADDRESS = len(headerinfo) + len(sourcedata) + len(rangedata)
790
+ f.seek(ADDRESS)
791
+
792
+ data = GEMFDataSection([])
793
+ for rangedetail in rangedetails:
794
+ f.seek(rangedetail.address)
795
+ tile_bytes = f.read(rangedetail.length)
796
+
797
+ range_idx = rangedetail._range_idx
798
+ tile_idx = rangedetail._tile_idx
799
+
800
+ range_ = rangedata[range_idx]
801
+
802
+ z, x, y = range_.get_zxy(tile_idx)
803
+ tile = GEMFBufferTile(z, x, y, range_idx, tile_idx, tile_bytes)
804
+ data.append(tile)
805
+
806
+ return data