gpkglite 0.1.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.
- gpkglite/__init__.py +79 -0
- gpkglite/__main__.py +7 -0
- gpkglite/_sql.py +187 -0
- gpkglite/binary.py +219 -0
- gpkglite/cli.py +210 -0
- gpkglite/convert.py +659 -0
- gpkglite/crs.py +155 -0
- gpkglite/errors.py +39 -0
- gpkglite/geopackage.py +1604 -0
- gpkglite/py.typed +0 -0
- gpkglite/validate.py +673 -0
- gpkglite/wkb.py +437 -0
- gpkglite/wkt.py +58 -0
- gpkglite-0.1.0.dist-info/METADATA +251 -0
- gpkglite-0.1.0.dist-info/RECORD +19 -0
- gpkglite-0.1.0.dist-info/WHEEL +5 -0
- gpkglite-0.1.0.dist-info/entry_points.txt +2 -0
- gpkglite-0.1.0.dist-info/licenses/LICENSE +21 -0
- gpkglite-0.1.0.dist-info/top_level.txt +1 -0
gpkglite/__init__.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""gpkglite: read and write OGC GeoPackage files with the Python standard library.
|
|
2
|
+
|
|
3
|
+
No GDAL, no compiled dependencies: only :mod:`sqlite3`, :mod:`struct` and
|
|
4
|
+
:mod:`json`. Geometries are GeoJSON-like dictionaries.
|
|
5
|
+
|
|
6
|
+
Quick start::
|
|
7
|
+
|
|
8
|
+
import gpkglite
|
|
9
|
+
|
|
10
|
+
with gpkglite.create("cities.gpkg") as gpkg:
|
|
11
|
+
layer = gpkg.create_layer("cities", "POINT", 4326, {"name": "TEXT", "pop": "INTEGER"})
|
|
12
|
+
layer.insert({"type": "Point", "coordinates": [28.97, 41.01]}, {"name": "Istanbul", "pop": 15_600_000})
|
|
13
|
+
|
|
14
|
+
with gpkglite.open("cities.gpkg") as gpkg:
|
|
15
|
+
for feature in gpkg["cities"].features(where="pop > ?", params=[1_000_000]):
|
|
16
|
+
print(feature["properties"]["name"], feature["geometry"]["coordinates"])
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
from . import binary, wkb, wkt
|
|
24
|
+
from .binary import GeoPackageGeometry
|
|
25
|
+
from .binary import decode as decode_geometry
|
|
26
|
+
from .binary import encode as encode_geometry
|
|
27
|
+
from .convert import (
|
|
28
|
+
from_csv,
|
|
29
|
+
from_geodataframe,
|
|
30
|
+
from_geojson,
|
|
31
|
+
infer_fields,
|
|
32
|
+
infer_geometry_type,
|
|
33
|
+
to_csv,
|
|
34
|
+
to_geodataframe,
|
|
35
|
+
to_geojson,
|
|
36
|
+
)
|
|
37
|
+
from .crs import SpatialRefSys
|
|
38
|
+
from .errors import (
|
|
39
|
+
GeometryError,
|
|
40
|
+
GeoPackageError,
|
|
41
|
+
GpkgliteError,
|
|
42
|
+
LayerNotFoundError,
|
|
43
|
+
UnsupportedGeometryError,
|
|
44
|
+
)
|
|
45
|
+
from .geopackage import Column, GeoPackage, Layer, LayerInfo, create, open
|
|
46
|
+
from .validate import Issue, ValidationReport, validate
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"__version__",
|
|
50
|
+
"Column",
|
|
51
|
+
"GeoPackage",
|
|
52
|
+
"GeoPackageError",
|
|
53
|
+
"GeoPackageGeometry",
|
|
54
|
+
"GeometryError",
|
|
55
|
+
"GpkgliteError",
|
|
56
|
+
"Issue",
|
|
57
|
+
"Layer",
|
|
58
|
+
"LayerInfo",
|
|
59
|
+
"LayerNotFoundError",
|
|
60
|
+
"SpatialRefSys",
|
|
61
|
+
"UnsupportedGeometryError",
|
|
62
|
+
"ValidationReport",
|
|
63
|
+
"binary",
|
|
64
|
+
"create",
|
|
65
|
+
"decode_geometry",
|
|
66
|
+
"encode_geometry",
|
|
67
|
+
"from_csv",
|
|
68
|
+
"from_geodataframe",
|
|
69
|
+
"from_geojson",
|
|
70
|
+
"infer_fields",
|
|
71
|
+
"infer_geometry_type",
|
|
72
|
+
"open",
|
|
73
|
+
"to_csv",
|
|
74
|
+
"to_geodataframe",
|
|
75
|
+
"to_geojson",
|
|
76
|
+
"validate",
|
|
77
|
+
"wkb",
|
|
78
|
+
"wkt",
|
|
79
|
+
]
|
gpkglite/__main__.py
ADDED
gpkglite/_sql.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""SQL definitions from OGC 12-128r18 (Annex C) and the RTree extension (Annex F.3)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
#: ``PRAGMA application_id`` value: the ASCII bytes "GPKG".
|
|
8
|
+
APPLICATION_ID = 0x47504B47
|
|
9
|
+
#: ``PRAGMA user_version`` written by gpkglite: GeoPackage 1.3.1 (12-128r18).
|
|
10
|
+
USER_VERSION = 10301
|
|
11
|
+
|
|
12
|
+
TIMESTAMP_DEFAULT = "strftime('%Y-%m-%dT%H:%M:%fZ','now')"
|
|
13
|
+
|
|
14
|
+
RTREE_EXTENSION = "http://www.geopackage.org/spec120/#extension_rtree"
|
|
15
|
+
METADATA_EXTENSION = "http://www.geopackage.org/spec120/#extension_metadata"
|
|
16
|
+
CRS_WKT_EXTENSION = "http://www.geopackage.org/spec120/#extension_crs_wkt"
|
|
17
|
+
|
|
18
|
+
SPATIAL_REF_SYS = """
|
|
19
|
+
CREATE TABLE gpkg_spatial_ref_sys (
|
|
20
|
+
srs_name TEXT NOT NULL,
|
|
21
|
+
srs_id INTEGER NOT NULL PRIMARY KEY,
|
|
22
|
+
organization TEXT NOT NULL,
|
|
23
|
+
organization_coordsys_id INTEGER NOT NULL,
|
|
24
|
+
definition TEXT NOT NULL,
|
|
25
|
+
description TEXT{extra}
|
|
26
|
+
)"""
|
|
27
|
+
|
|
28
|
+
CONTENTS = f"""
|
|
29
|
+
CREATE TABLE gpkg_contents (
|
|
30
|
+
table_name TEXT NOT NULL PRIMARY KEY,
|
|
31
|
+
data_type TEXT NOT NULL,
|
|
32
|
+
identifier TEXT UNIQUE,
|
|
33
|
+
description TEXT DEFAULT '',
|
|
34
|
+
last_change DATETIME NOT NULL DEFAULT ({TIMESTAMP_DEFAULT}),
|
|
35
|
+
min_x DOUBLE,
|
|
36
|
+
min_y DOUBLE,
|
|
37
|
+
max_x DOUBLE,
|
|
38
|
+
max_y DOUBLE,
|
|
39
|
+
srs_id INTEGER,
|
|
40
|
+
CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
|
|
41
|
+
)"""
|
|
42
|
+
|
|
43
|
+
GEOMETRY_COLUMNS = """
|
|
44
|
+
CREATE TABLE gpkg_geometry_columns (
|
|
45
|
+
table_name TEXT NOT NULL,
|
|
46
|
+
column_name TEXT NOT NULL,
|
|
47
|
+
geometry_type_name TEXT NOT NULL,
|
|
48
|
+
srs_id INTEGER NOT NULL,
|
|
49
|
+
z TINYINT NOT NULL,
|
|
50
|
+
m TINYINT NOT NULL,
|
|
51
|
+
CONSTRAINT pk_geom_cols PRIMARY KEY (table_name, column_name),
|
|
52
|
+
CONSTRAINT uk_gc_table_name UNIQUE (table_name),
|
|
53
|
+
CONSTRAINT fk_gc_tn FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name),
|
|
54
|
+
CONSTRAINT fk_gc_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys (srs_id)
|
|
55
|
+
)"""
|
|
56
|
+
|
|
57
|
+
EXTENSIONS = """
|
|
58
|
+
CREATE TABLE gpkg_extensions (
|
|
59
|
+
table_name TEXT,
|
|
60
|
+
column_name TEXT,
|
|
61
|
+
extension_name TEXT NOT NULL,
|
|
62
|
+
definition TEXT NOT NULL,
|
|
63
|
+
scope TEXT NOT NULL,
|
|
64
|
+
CONSTRAINT ge_tce UNIQUE (table_name, column_name, extension_name)
|
|
65
|
+
)"""
|
|
66
|
+
|
|
67
|
+
METADATA = """
|
|
68
|
+
CREATE TABLE gpkg_metadata (
|
|
69
|
+
id INTEGER CONSTRAINT m_pk PRIMARY KEY ASC NOT NULL,
|
|
70
|
+
md_scope TEXT NOT NULL DEFAULT 'dataset',
|
|
71
|
+
md_standard_uri TEXT NOT NULL,
|
|
72
|
+
mime_type TEXT NOT NULL DEFAULT 'text/xml',
|
|
73
|
+
metadata TEXT NOT NULL DEFAULT ''
|
|
74
|
+
)"""
|
|
75
|
+
|
|
76
|
+
METADATA_REFERENCE = f"""
|
|
77
|
+
CREATE TABLE gpkg_metadata_reference (
|
|
78
|
+
reference_scope TEXT NOT NULL,
|
|
79
|
+
table_name TEXT,
|
|
80
|
+
column_name TEXT,
|
|
81
|
+
row_id_value INTEGER,
|
|
82
|
+
timestamp DATETIME NOT NULL DEFAULT ({TIMESTAMP_DEFAULT}),
|
|
83
|
+
md_file_id INTEGER NOT NULL,
|
|
84
|
+
md_parent_id INTEGER,
|
|
85
|
+
CONSTRAINT crmr_mfi_fk FOREIGN KEY (md_file_id) REFERENCES gpkg_metadata(id),
|
|
86
|
+
CONSTRAINT crmr_mpi_fk FOREIGN KEY (md_parent_id) REFERENCES gpkg_metadata(id)
|
|
87
|
+
)"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def quote_ident(name: str) -> str:
|
|
91
|
+
"""Quote an SQL identifier (table or column name)."""
|
|
92
|
+
return '"' + str(name).replace('"', '""') + '"'
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def rtree_name(table: str, column: str) -> str:
|
|
96
|
+
return f"rtree_{table}_{column}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _bbox_values(c: str) -> str:
|
|
100
|
+
return f"ST_MinX(NEW.{c}), ST_MaxX(NEW.{c}), ST_MinY(NEW.{c}), ST_MaxY(NEW.{c})"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def rtree_triggers(table: str, column: str, fid: str, version: int = USER_VERSION) -> List[str]:
|
|
104
|
+
"""``CREATE TRIGGER`` statements maintaining ``rtree_<table>_<column>``.
|
|
105
|
+
|
|
106
|
+
GeoPackage 1.4 (``user_version >= 10400``) replaced the ``update1`` trigger
|
|
107
|
+
with ``update6``/``update7`` and renamed ``update3`` to ``update5``; earlier
|
|
108
|
+
versions use ``insert``, ``update1``-``update4`` and ``delete``.
|
|
109
|
+
"""
|
|
110
|
+
base = rtree_name(table, column)
|
|
111
|
+
rt = quote_ident(base)
|
|
112
|
+
t = quote_ident(table)
|
|
113
|
+
c = quote_ident(column)
|
|
114
|
+
i = quote_ident(fid)
|
|
115
|
+
vals = _bbox_values(c)
|
|
116
|
+
|
|
117
|
+
def name(suffix: str) -> str:
|
|
118
|
+
return quote_ident(f"{base}_{suffix}")
|
|
119
|
+
|
|
120
|
+
present = f"(NEW.{c} NOTNULL AND NOT ST_IsEmpty(NEW.{c}))"
|
|
121
|
+
absent = f"(NEW.{c} ISNULL OR ST_IsEmpty(NEW.{c}))"
|
|
122
|
+
insert = (
|
|
123
|
+
f"CREATE TRIGGER {name('insert')} AFTER INSERT ON {t} "
|
|
124
|
+
f"WHEN (new.{c} NOT NULL AND NOT ST_IsEmpty(NEW.{c})) "
|
|
125
|
+
f"BEGIN INSERT OR REPLACE INTO {rt} VALUES (NEW.{i}, {vals}); END"
|
|
126
|
+
)
|
|
127
|
+
same_id_absent = (
|
|
128
|
+
f"CREATE TRIGGER {name('update2')} AFTER UPDATE OF {c} ON {t} "
|
|
129
|
+
f"WHEN OLD.{i} = NEW.{i} AND {absent} "
|
|
130
|
+
f"BEGIN DELETE FROM {rt} WHERE id = OLD.{i}; END"
|
|
131
|
+
)
|
|
132
|
+
new_id_absent = (
|
|
133
|
+
f"CREATE TRIGGER {name('update4')} AFTER UPDATE ON {t} "
|
|
134
|
+
f"WHEN OLD.{i} != NEW.{i} AND {absent} "
|
|
135
|
+
f"BEGIN DELETE FROM {rt} WHERE id IN (OLD.{i}, NEW.{i}); END"
|
|
136
|
+
)
|
|
137
|
+
delete = (
|
|
138
|
+
f"CREATE TRIGGER {name('delete')} AFTER DELETE ON {t} "
|
|
139
|
+
f"WHEN old.{c} NOT NULL "
|
|
140
|
+
f"BEGIN DELETE FROM {rt} WHERE id = OLD.{i}; END"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def new_id_present(suffix: str) -> str:
|
|
144
|
+
return (
|
|
145
|
+
f"CREATE TRIGGER {name(suffix)} AFTER UPDATE ON {t} "
|
|
146
|
+
f"WHEN OLD.{i} != NEW.{i} AND {present} "
|
|
147
|
+
f"BEGIN DELETE FROM {rt} WHERE id = OLD.{i}; "
|
|
148
|
+
f"INSERT OR REPLACE INTO {rt} VALUES (NEW.{i}, {vals}); END"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if version < 10400:
|
|
152
|
+
update1 = (
|
|
153
|
+
f"CREATE TRIGGER {name('update1')} AFTER UPDATE OF {c} ON {t} "
|
|
154
|
+
f"WHEN OLD.{i} = NEW.{i} AND {present} "
|
|
155
|
+
f"BEGIN INSERT OR REPLACE INTO {rt} VALUES (NEW.{i}, {vals}); END"
|
|
156
|
+
)
|
|
157
|
+
return [insert, update1, same_id_absent, new_id_present("update3"), new_id_absent, delete]
|
|
158
|
+
|
|
159
|
+
old_present = f"(OLD.{c} NOTNULL AND NOT ST_IsEmpty(OLD.{c}))"
|
|
160
|
+
old_absent = f"(OLD.{c} ISNULL OR ST_IsEmpty(OLD.{c}))"
|
|
161
|
+
update6 = (
|
|
162
|
+
f"CREATE TRIGGER {name('update6')} AFTER UPDATE OF {c} ON {t} "
|
|
163
|
+
f"WHEN OLD.{i} = NEW.{i} AND {present} AND {old_present} "
|
|
164
|
+
f"BEGIN UPDATE {rt} SET minx = ST_MinX(NEW.{c}), maxx = ST_MaxX(NEW.{c}), "
|
|
165
|
+
f"miny = ST_MinY(NEW.{c}), maxy = ST_MaxY(NEW.{c}) WHERE id = NEW.{i}; END"
|
|
166
|
+
)
|
|
167
|
+
update7 = (
|
|
168
|
+
f"CREATE TRIGGER {name('update7')} AFTER UPDATE OF {c} ON {t} "
|
|
169
|
+
f"WHEN OLD.{i} = NEW.{i} AND {present} AND {old_absent} "
|
|
170
|
+
f"BEGIN INSERT INTO {rt} VALUES (NEW.{i}, {vals}); END"
|
|
171
|
+
)
|
|
172
|
+
return [
|
|
173
|
+
insert,
|
|
174
|
+
same_id_absent,
|
|
175
|
+
new_id_absent,
|
|
176
|
+
new_id_present("update5"),
|
|
177
|
+
update6,
|
|
178
|
+
update7,
|
|
179
|
+
delete,
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
#: Trigger name suffixes expected for a given GeoPackage version.
|
|
184
|
+
def rtree_trigger_suffixes(version: int) -> List[str]:
|
|
185
|
+
if version < 10400:
|
|
186
|
+
return ["insert", "update1", "update2", "update3", "update4", "delete"]
|
|
187
|
+
return ["insert", "update2", "update4", "update5", "update6", "update7", "delete"]
|
gpkglite/binary.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""StandardGeoPackageBinary encoding and decoding (OGC 12-128r18, clause 2.1.3).
|
|
2
|
+
|
|
3
|
+
Layout of a geometry blob::
|
|
4
|
+
|
|
5
|
+
magic 2 bytes "GP"
|
|
6
|
+
version 1 byte 0 (version 1)
|
|
7
|
+
flags 1 byte bit 0: header byte order (1 = little endian)
|
|
8
|
+
bits 1-3: envelope contents indicator (0-4)
|
|
9
|
+
bit 4: empty geometry flag
|
|
10
|
+
bit 5: 0 = StandardGeoPackageBinary, 1 = Extended
|
|
11
|
+
srs_id int32
|
|
12
|
+
envelope 0/32/48/48/64 bytes of doubles
|
|
13
|
+
geometry ISO WKB
|
|
14
|
+
|
|
15
|
+
Envelope indicator codes: 0 = none, 1 = [minx, maxx, miny, maxy],
|
|
16
|
+
2 = + [minz, maxz], 3 = + [minm, maxm], 4 = + [minz, maxz, minm, maxm].
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import math
|
|
22
|
+
import struct
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
25
|
+
|
|
26
|
+
from . import wkb as _wkb
|
|
27
|
+
from .errors import GeometryError, UnsupportedGeometryError
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"ENVELOPE_SIZES",
|
|
31
|
+
"ENVELOPE_NAMES",
|
|
32
|
+
"GeoPackageGeometry",
|
|
33
|
+
"Header",
|
|
34
|
+
"decode",
|
|
35
|
+
"decode_header",
|
|
36
|
+
"encode",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
#: Number of doubles stored for each envelope indicator code.
|
|
40
|
+
ENVELOPE_SIZES: Dict[int, int] = {0: 0, 1: 4, 2: 6, 3: 6, 4: 8}
|
|
41
|
+
#: Accepted names for the ``envelope`` argument of :func:`encode`.
|
|
42
|
+
ENVELOPE_NAMES: Dict[str, int] = {"none": 0, "xy": 1, "xyz": 2, "xym": 3, "xyzm": 4}
|
|
43
|
+
|
|
44
|
+
MAGIC = b"GP"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class Header:
|
|
49
|
+
"""Parsed GeoPackage binary header."""
|
|
50
|
+
|
|
51
|
+
version: int
|
|
52
|
+
flags: int
|
|
53
|
+
byte_order: str # "<" or ">"
|
|
54
|
+
envelope_type: int
|
|
55
|
+
empty: bool
|
|
56
|
+
extended: bool
|
|
57
|
+
srs_id: int
|
|
58
|
+
envelope: Tuple[float, ...]
|
|
59
|
+
size: int # header length in bytes, i.e. offset of the WKB payload
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def envelope_dict(self) -> Dict[str, float]:
|
|
63
|
+
"""The envelope as ``{"minx": ..., "maxx": ..., ...}``."""
|
|
64
|
+
keys = {
|
|
65
|
+
0: (),
|
|
66
|
+
1: ("minx", "maxx", "miny", "maxy"),
|
|
67
|
+
2: ("minx", "maxx", "miny", "maxy", "minz", "maxz"),
|
|
68
|
+
3: ("minx", "maxx", "miny", "maxy", "minm", "maxm"),
|
|
69
|
+
4: ("minx", "maxx", "miny", "maxy", "minz", "maxz", "minm", "maxm"),
|
|
70
|
+
}[self.envelope_type]
|
|
71
|
+
return dict(zip(keys, self.envelope))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class GeoPackageGeometry:
|
|
76
|
+
"""A decoded GeoPackage geometry blob."""
|
|
77
|
+
|
|
78
|
+
header: Header
|
|
79
|
+
wkb: bytes = field(repr=False)
|
|
80
|
+
geometry: Optional[Dict[str, Any]] # None only when decode(..., parse_wkb=False)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def srs_id(self) -> int:
|
|
84
|
+
return self.header.srs_id
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def empty(self) -> bool:
|
|
88
|
+
return self.header.empty
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def bounds(self) -> Optional[Tuple[float, float, float, float]]:
|
|
92
|
+
"""2D bounds ``(minx, miny, maxx, maxy)`` from the header envelope or WKB."""
|
|
93
|
+
env = self.header.envelope
|
|
94
|
+
if self.header.envelope_type and not any(math.isnan(v) for v in env[:4]):
|
|
95
|
+
return (env[0], env[2], env[1], env[3])
|
|
96
|
+
if self.geometry is None:
|
|
97
|
+
return _wkb.bounds(_wkb.loads(self.wkb))
|
|
98
|
+
return _wkb.bounds(self.geometry)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def decode_header(blob: Union[bytes, bytearray, memoryview]) -> Header:
|
|
102
|
+
"""Parse only the header of a GeoPackage geometry blob."""
|
|
103
|
+
data = bytes(blob)
|
|
104
|
+
if len(data) < 8:
|
|
105
|
+
raise GeometryError(f"GeoPackage geometry blob too short ({len(data)} bytes)")
|
|
106
|
+
if data[:2] != MAGIC:
|
|
107
|
+
raise GeometryError(f"bad GeoPackage geometry magic {data[:2]!r}, expected b'GP'")
|
|
108
|
+
version = data[2]
|
|
109
|
+
if version != 0:
|
|
110
|
+
raise GeometryError(f"unsupported GeoPackage binary version {version}")
|
|
111
|
+
flags = data[3]
|
|
112
|
+
if flags & 0xC0:
|
|
113
|
+
raise GeometryError(f"reserved flag bits set in GeoPackage header flags {flags:#04x}")
|
|
114
|
+
bo = "<" if flags & 1 else ">"
|
|
115
|
+
env_type = (flags >> 1) & 0x07
|
|
116
|
+
if env_type not in ENVELOPE_SIZES:
|
|
117
|
+
raise GeometryError(f"invalid envelope contents indicator code {env_type}")
|
|
118
|
+
empty = bool(flags & 0x10)
|
|
119
|
+
extended = bool(flags & 0x20)
|
|
120
|
+
(srs_id,) = struct.unpack_from(bo + "i", data, 4)
|
|
121
|
+
n = ENVELOPE_SIZES[env_type]
|
|
122
|
+
size = 8 + 8 * n
|
|
123
|
+
if len(data) < size:
|
|
124
|
+
raise GeometryError("GeoPackage geometry blob truncated inside the envelope")
|
|
125
|
+
env = struct.unpack_from(f"{bo}{n}d", data, 8) if n else ()
|
|
126
|
+
return Header(version, flags, bo, env_type, empty, extended, int(srs_id), env, size)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def decode(blob: Union[bytes, bytearray, memoryview], parse_wkb: bool = True) -> GeoPackageGeometry:
|
|
130
|
+
"""Decode a GeoPackage geometry blob.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
blob: the BLOB stored in a feature table geometry column.
|
|
134
|
+
parse_wkb: when ``False`` only the header is parsed and
|
|
135
|
+
:attr:`GeoPackageGeometry.geometry` is ``None``.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
GeometryError: malformed header or WKB.
|
|
139
|
+
UnsupportedGeometryError: ExtendedGeoPackageBinary or curve geometry.
|
|
140
|
+
"""
|
|
141
|
+
data = bytes(blob)
|
|
142
|
+
header = decode_header(data)
|
|
143
|
+
if header.extended:
|
|
144
|
+
raise UnsupportedGeometryError(
|
|
145
|
+
"ExtendedGeoPackageBinary geometries (flag bit 5) carry extension-specific "
|
|
146
|
+
"encodings that gpkglite does not decode"
|
|
147
|
+
)
|
|
148
|
+
payload = data[header.size :]
|
|
149
|
+
geometry = _wkb.loads(payload) if parse_wkb else None
|
|
150
|
+
return GeoPackageGeometry(header=header, wkb=payload, geometry=geometry)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def encode(
|
|
154
|
+
geom: Any,
|
|
155
|
+
srs_id: int = 0,
|
|
156
|
+
envelope: Union[str, int] = "auto",
|
|
157
|
+
byte_order: str = "<",
|
|
158
|
+
dims: Optional[str] = None,
|
|
159
|
+
) -> bytes:
|
|
160
|
+
"""Encode a geometry as a StandardGeoPackageBinary blob.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
geom: geometry dictionary, ``__geo_interface__`` object, or raw WKB
|
|
164
|
+
``bytes`` (re-wrapped without re-encoding).
|
|
165
|
+
srs_id: value of the header ``srs_id`` field.
|
|
166
|
+
envelope: ``"auto"`` (no envelope for points, which the spec
|
|
167
|
+
recommends, XY envelope otherwise), ``"none"``, ``"xy"``, ``"xyz"``,
|
|
168
|
+
``"xym"``, ``"xyzm"`` or an indicator code 0-4. Empty geometries are
|
|
169
|
+
always written without an envelope and with the empty flag set.
|
|
170
|
+
byte_order: byte order of header and WKB, ``"<"`` or ``">"``.
|
|
171
|
+
dims: force the coordinate dimension of the WKB.
|
|
172
|
+
"""
|
|
173
|
+
if byte_order not in ("<", ">"):
|
|
174
|
+
raise ValueError("byte_order must be '<' or '>'")
|
|
175
|
+
if isinstance(geom, (bytes, bytearray, memoryview)):
|
|
176
|
+
payload = bytes(geom)
|
|
177
|
+
g = _wkb.loads(payload)
|
|
178
|
+
else:
|
|
179
|
+
g = _wkb.normalize(geom)
|
|
180
|
+
if dims is not None:
|
|
181
|
+
g["dims"] = dims.upper()
|
|
182
|
+
payload = _wkb.dumps(g, byte_order=byte_order)
|
|
183
|
+
empty = _wkb.is_empty(g)
|
|
184
|
+
env_type = _envelope_code(envelope, g, empty)
|
|
185
|
+
env_values: Tuple[float, ...] = ()
|
|
186
|
+
if env_type:
|
|
187
|
+
full = _wkb.envelope(g)
|
|
188
|
+
assert full is not None # non-empty
|
|
189
|
+
has_z, has_m = _wkb.geometry_dims(g)
|
|
190
|
+
if env_type in (2, 4) and not has_z:
|
|
191
|
+
raise GeometryError("a Z envelope was requested for a geometry without Z")
|
|
192
|
+
if env_type in (3, 4) and not has_m:
|
|
193
|
+
raise GeometryError("an M envelope was requested for a geometry without M")
|
|
194
|
+
xy = full[:4]
|
|
195
|
+
z = full[4:6] if has_z else ()
|
|
196
|
+
m = full[-2:] if has_m else ()
|
|
197
|
+
env_values = xy + {1: (), 2: z, 3: m, 4: z + m}[env_type]
|
|
198
|
+
flags = (1 if byte_order == "<" else 0) | (env_type << 1) | (0x10 if empty else 0)
|
|
199
|
+
head = MAGIC + struct.pack(byte_order + "BBi", 0, flags, int(srs_id))
|
|
200
|
+
if env_values:
|
|
201
|
+
head += struct.pack(f"{byte_order}{len(env_values)}d", *env_values)
|
|
202
|
+
return head + payload
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _envelope_code(envelope: Union[str, int], g: Dict[str, Any], empty: bool) -> int:
|
|
206
|
+
if empty:
|
|
207
|
+
return 0
|
|
208
|
+
if isinstance(envelope, int) and not isinstance(envelope, bool):
|
|
209
|
+
if envelope not in ENVELOPE_SIZES:
|
|
210
|
+
raise ValueError(f"invalid envelope indicator code {envelope}")
|
|
211
|
+
return envelope
|
|
212
|
+
key = str(envelope).lower()
|
|
213
|
+
if key == "auto":
|
|
214
|
+
return 0 if g["type"] == "Point" else 1
|
|
215
|
+
if key not in ENVELOPE_NAMES:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"invalid envelope {envelope!r}; expected auto, {', '.join(ENVELOPE_NAMES)}"
|
|
218
|
+
)
|
|
219
|
+
return ENVELOPE_NAMES[key]
|