Skip to content

Commit 176bf96

Browse files
authored
Merge pull request #121 from STEMLab/col_patch
07/21 add collection_patch
2 parents 332c806 + 1d081d1 commit 176bf96

6 files changed

Lines changed: 80 additions & 129 deletions

File tree

.png

-25.4 KB
Binary file not shown.

openAPI/examples/collections.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
6767
}
6868
},
69-
"itemType": "feature"
69+
"itemType": "indoorfeature"
7070
},
7171
{
7272
"id": "aist_waterfront_lab",

pygeoapi/api/indoorgml.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ def manage_collection(api: API, request: APIRequest, action: str, dataset: str =
3535
headers = request.get_response_headers(SYSTEM_LOCALE)
3636
pidb_provider = PostgresIndoorDB()
3737

38-
# --- Action: CREATE ---
39-
if action == 'create':
38+
if action in ['create', 'update']:
4039
if not request.data:
4140
msg = 'No data found'
4241
LOGGER.error(msg)
@@ -59,12 +58,24 @@ def manage_collection(api: API, request: APIRequest, action: str, dataset: str =
5958
return api.get_exception(
6059
HTTPStatus.BAD_REQUEST,
6160
headers, request.format, 'InvalidParameterValue', msg)
62-
61+
62+
# --- Action: CREATE ---
63+
if action == 'create':
6364
# 2. Call Provider to Create
6465
try:
6566
pidb_provider.connect()
67+
c_id = data.get('id')
68+
title = data.get('title')
69+
item_type = data.get('itemType', 'indoorfeature')
70+
if not c_id or not title:
71+
return api.get_exception(
72+
HTTPStatus.BAD_REQUEST,
73+
headers, request.format, "Missing required parameter 'id' and 'title'.", msg)
74+
elif item_type != 'indoorfeature':
75+
return api.get_exception(
76+
HTTPStatus.BAD_REQUEST,
77+
headers, request.format, "Invalid 'itemType' value. Expected 'indoorfeature'.", msg)
6678
new_id = pidb_provider.post_collection(data)
67-
6879
if not new_id:
6980
return api.get_exception(
7081
HTTPStatus.CONFLICT, headers, request.format,
@@ -97,6 +108,24 @@ def manage_collection(api: API, request: APIRequest, action: str, dataset: str =
97108
return api.get_exception(HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, 'ServerError', str(e))
98109
finally:
99110
pidb_provider.disconnect()
111+
112+
elif action == 'update':
113+
try:
114+
pidb_provider.connect()
115+
collection_id = str(dataset)
116+
117+
success = pidb_provider.patch_collection(collection_id, data)
118+
if not success:
119+
return api.get_exception(
120+
HTTPStatus.NOT_FOUND, headers, request.format,
121+
'NotFound', f'Collection {collection_id} not found')
122+
123+
return headers, HTTPStatus.NO_CONTENT, 'Updated successfully.'
124+
125+
except Exception as e:
126+
return api.get_exception(HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, 'ServerError', str(e))
127+
finally:
128+
pidb_provider.disconnect()
100129

101130
return headers, HTTPStatus.METHOD_NOT_ALLOWED, ''
102131

pygeoapi/flask_app.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def get_tilematrix_sets():
232232

233233

234234
@BLUEPRINT.route('/collections', methods=['GET', 'POST'])
235-
@BLUEPRINT.route('/collections/<path:collection_id>', methods=['GET', 'DELETE'])
235+
@BLUEPRINT.route('/collections/<path:collection_id>', methods=['GET', 'PATCH','DELETE'])
236236
def collections(collection_id: str | None = None):
237237
"""
238238
OGC API collections endpoint
@@ -269,6 +269,8 @@ def collections(collection_id: str | None = None):
269269
elif request.method == 'DELETE':
270270
# Delete from DB
271271
return execute_from_flask(indoorgml.manage_collection, request, 'delete', collection_id)
272+
elif request.method == 'PATCH':
273+
return execute_from_flask(indoorgml.manage_collection, request, 'update', collection_id)
272274

273275
# Fallback: Standard OGC API (YAML-based GeoJSON/CSV features)
274276
else:

pygeoapi/provider/postgresql_indoordb.py

Lines changed: 43 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ def get_collections_list(self):
8989

9090
clean_list.append({
9191
'id': c_id,
92-
# Default to the ID if title is missing
9392
'title': props.get('title', c_id),
9493
'itemType': props.get('itemType', 'indoorfeature')
9594
})
@@ -163,7 +162,6 @@ def post_collection(self, collection):
163162
}
164163
with self.connection.cursor() as cur:
165164
try:
166-
# 2. Insert (Let Postgres handle the 'id' column automatically)
167165
insert_query = """
168166
INSERT INTO collection (id_str, collection_property)
169167
VALUES (%s, %s)
@@ -228,8 +226,47 @@ def delete_collection(self, collection_id:str):
228226
self.connection.rollback()
229227
LOGGER.error(f"Error creating collection: {e}")
230228
raise e
229+
230+
231+
def patch_collection(self, collection_id:str, data):
232+
with self.connection.cursor() as cur:
233+
try:
234+
cur.execute("SELECT id FROM collection WHERE id_str = %s", (collection_id,))
235+
row = cur.fetchone()
236+
237+
if not row:
238+
return False # Collection not found
239+
240+
coll_pk = row[0]
241+
242+
sql = "SELECT collection_property FROM collection WHERE id = %s"
243+
cur.execute(sql, (coll_pk,))
244+
245+
collection_json = cur.fetchone()
246+
247+
fields = []
248+
values = []
249+
250+
if 'title' in data:
251+
collection_json['title'] = data['title']
252+
253+
if 'description' in data:
254+
collection_json['description'] = data['description']
255+
256+
update_sql = "UPDATE collection SET collection_property = %s WHERE id = %s"
257+
cur.execute(update_sql, (json.dumps(collection_json),coll_pk,))
258+
259+
self.connection.commit()
260+
return True
261+
262+
except Exception as e:
263+
self.connection.rollback()
264+
LOGGER.error(f"Error updating collection: {e}")
265+
raise e
266+
231267
# endregion
232268

269+
233270
# region IndoorFeatures
234271
def is_indoor_collection(self, collection_id:str):
235272
"""
@@ -260,7 +297,6 @@ def get_collection_items(
260297
"""
261298
Retrieve the indoor feature collection /collections/{collectionId}/items
262299
Optimized to fetch data and total count in a single query.
263-
/collections/{collectionId}/items
264300
"""
265301
try:
266302
# 1. Prepare Filter Strings
@@ -363,7 +399,6 @@ def get_feature(self, collection_id: str, feature_id:str, level:str=None, bbox:l
363399
properties = props or {}
364400
thematic_layers = []
365401
interlayer_connections = []
366-
# Initialize Skeleton
367402
result_feature = {
368403
"type": "Feature",
369404
"id": feature_id_str,
@@ -512,7 +547,6 @@ def delete_indoorfeature(self, collection_id:str, feature_id:str):
512547
res = cur.fetchone()
513548

514549
if not res:
515-
# Item not found, usually returns 404 in API, but here we can just return
516550
msg = f"Feature {feature_id} not found."
517551
LOGGER.warning(msg)
518552
raise ValueError(msg)
@@ -543,7 +577,6 @@ def delete_indoorfeature(self, collection_id:str, feature_id:str):
543577
# 3. DELETE PARENT (The IndoorFeature itself)
544578
cur.execute("DELETE FROM indoorfeature WHERE id = %s", (feature_pk,))
545579

546-
# Commit is handled by the context manager
547580
self.connection.commit()
548581
return True
549582
except Exception as e:
@@ -660,7 +693,7 @@ def get_layers(self, collection_id:str, feature_id:str, theme: str = None, level
660693

661694
def _get_layer(self, layer_pk:int, level:str=None, bbox:list=None):
662695
"""
663-
Retrieves a single Thematic Layer.
696+
Retrieves a single Thematic Layer with Integer ID.
664697
If not filtered, just the meta data is given.
665698
- PrimalSpace: Filtered by 'level' if provided.
666699
- DualSpace: Returns the ENTIRE network (unfiltered) for connectivity.
@@ -703,7 +736,7 @@ def _get_layer(self, layer_pk:int, level:str=None, bbox:list=None):
703736

704737
def get_layer(self, collection_id:str, feature_id:str, layer_id:str, level:str=None, bbox:list=None):
705738
"""
706-
Retrieves a single Thematic Layer.
739+
Retrieves a single Thematic Layer with String ID.
707740
If not filtered, just the meta data is given.
708741
- PrimalSpace: Filtered by 'level' if provided.
709742
- DualSpace: Returns the ENTIRE network (unfiltered) for connectivity.
@@ -817,7 +850,7 @@ def get_layer(self, collection_id:str, feature_id:str, layer_id:str, level:str=N
817850
def _get_primal_space(self, layer_pk:int, primalspace_id:str, p_create:str = None, p_termination:str=None, level:str=None, bbox:list=None):
818851
"""
819852
Helper to build PrimalSpaceLayer.
820-
Supports optional filtering by 'level'.
853+
Supports optional filtering by 'level' and 'bbox'.
821854
"""
822855
primal_space = {
823856
"id": primalspace_id,
@@ -1053,7 +1086,7 @@ def _post_thematic_layer(self, collection_pk:int, feature_pk:int, layer_data):
10531086

10541087
layer_new = cur.fetchone()
10551088

1056-
# Insert Primal Members (Cells/Boundaries) - returns duality dict
1089+
# Insert Primal Members (Cells/Boundaries) - returns duality dictionary
10571090
d_c, d_b = self._post_primal_members(collection_pk, feature_pk, layer_new[0], primal)
10581091

10591092
# Insert Dual Members (Nodes/Edges)
@@ -1168,65 +1201,6 @@ def _post_primal_members(self, collection_pk:int, feature_pk:int, layer_pk:int,
11681201
# If there is no 2D geometry but 3D, project 3D to 2D geometry
11691202
LOGGER.debug("Project geometry 3D to 2D ")
11701203
sql_project_shell = """
1171-
WITH faces AS (
1172-
SELECT
1173-
c.id,
1174-
s.shell_idx,
1175-
f.face
1176-
FROM cell_space_n_boundary c
1177-
CROSS JOIN LATERAL jsonb_array_elements(c."3D_geometry"->'coordinates')
1178-
WITH ORDINALITY AS s(shell, shell_idx)
1179-
CROSS JOIN LATERAL jsonb_array_elements(s.shell) AS f(face)
1180-
WHERE c."3D_geometry" IS NOT NULL
1181-
AND c.type = 'space'
1182-
AND c."2D_geometry" IS NULL
1183-
AND c.thematiclayer_id = %s
1184-
AND (
1185-
s.shell_idx = 1
1186-
OR jsonb_array_length(c."3D_geometry"->'coordinates') > 1
1187-
)
1188-
),
1189-
proj AS (
1190-
SELECT
1191-
id,
1192-
shell_idx,
1193-
ST_SetSRID(
1194-
ST_Force2D(
1195-
ST_GeomFromGeoJSON(
1196-
jsonb_build_object(
1197-
'type', 'Polygon',
1198-
'coordinates',
1199-
CASE
1200-
-- if face is already [ring,...], keep it; else wrap to [ring]
1201-
WHEN jsonb_typeof(face->0->0) = 'array' THEN face
1202-
ELSE jsonb_build_array(face)
1203-
END
1204-
)::text
1205-
)
1206-
),
1207-
0
1208-
) AS g2d
1209-
FROM faces
1210-
),
1211-
u AS (
1212-
SELECT
1213-
id,
1214-
ST_UnaryUnion( ST_Collect(g2d) FILTER (WHERE shell_idx = 1) ) AS ext2d,
1215-
ST_UnaryUnion( ST_Collect(g2d) FILTER (WHERE shell_idx > 1) ) AS int2d
1216-
FROM proj
1217-
GROUP BY id
1218-
)
1219-
UPDATE cell_space_n_boundary c
1220-
SET "2D_geometry" =
1221-
CASE
1222-
WHEN u.int2d IS NULL THEN u.ext2d
1223-
ELSE ST_Difference(u.ext2d, u.int2d)
1224-
END
1225-
FROM u
1226-
WHERE c.id = u.id
1227-
AND c.thematiclayer_id = %s;
1228-
"""
1229-
sql_project_shell = """
12301204
WITH targets AS (
12311205
SELECT id, "3D_geometry"
12321206
FROM cell_space_n_boundary

requirements-indoor.txt

Lines changed: 0 additions & 54 deletions
This file was deleted.

0 commit comments

Comments
 (0)