Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .png
Binary file not shown.
2 changes: 1 addition & 1 deletion openAPI/examples/collections.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"crs": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
}
},
"itemType": "feature"
"itemType": "indoorfeature"
},
{
"id": "aist_waterfront_lab",
Expand Down
37 changes: 33 additions & 4 deletions pygeoapi/api/indoorgml.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ def manage_collection(api: API, request: APIRequest, action: str, dataset: str =
headers = request.get_response_headers(SYSTEM_LOCALE)
pidb_provider = PostgresIndoorDB()

# --- Action: CREATE ---
if action == 'create':
if action in ['create', 'update']:
if not request.data:
msg = 'No data found'
LOGGER.error(msg)
Expand All @@ -59,12 +58,24 @@ def manage_collection(api: API, request: APIRequest, action: str, dataset: str =
return api.get_exception(
HTTPStatus.BAD_REQUEST,
headers, request.format, 'InvalidParameterValue', msg)


# --- Action: CREATE ---
if action == 'create':
# 2. Call Provider to Create
try:
pidb_provider.connect()
c_id = data.get('id')
title = data.get('title')
item_type = data.get('itemType', 'indoorfeature')
if not c_id or not title:
return api.get_exception(
HTTPStatus.BAD_REQUEST,
headers, request.format, "Missing required parameter 'id' and 'title'.", msg)
elif item_type != 'indoorfeature':
return api.get_exception(
HTTPStatus.BAD_REQUEST,
headers, request.format, "Invalid 'itemType' value. Expected 'indoorfeature'.", msg)
new_id = pidb_provider.post_collection(data)

if not new_id:
return api.get_exception(
HTTPStatus.CONFLICT, headers, request.format,
Expand Down Expand Up @@ -97,6 +108,24 @@ def manage_collection(api: API, request: APIRequest, action: str, dataset: str =
return api.get_exception(HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, 'ServerError', str(e))
finally:
pidb_provider.disconnect()

elif action == 'update':
try:
pidb_provider.connect()
collection_id = str(dataset)

success = pidb_provider.patch_collection(collection_id, data)
if not success:
return api.get_exception(
HTTPStatus.NOT_FOUND, headers, request.format,
'NotFound', f'Collection {collection_id} not found')

return headers, HTTPStatus.NO_CONTENT, 'Updated successfully.'

except Exception as e:
return api.get_exception(HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format, 'ServerError', str(e))
finally:
pidb_provider.disconnect()

return headers, HTTPStatus.METHOD_NOT_ALLOWED, ''

Expand Down
4 changes: 3 additions & 1 deletion pygeoapi/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def get_tilematrix_sets():


@BLUEPRINT.route('/collections', methods=['GET', 'POST'])
@BLUEPRINT.route('/collections/<path:collection_id>', methods=['GET', 'DELETE'])
@BLUEPRINT.route('/collections/<path:collection_id>', methods=['GET', 'PATCH','DELETE'])
def collections(collection_id: str | None = None):
"""
OGC API collections endpoint
Expand Down Expand Up @@ -269,6 +269,8 @@ def collections(collection_id: str | None = None):
elif request.method == 'DELETE':
# Delete from DB
return execute_from_flask(indoorgml.manage_collection, request, 'delete', collection_id)
elif request.method == 'PATCH':
return execute_from_flask(indoorgml.manage_collection, request, 'update', collection_id)

# Fallback: Standard OGC API (YAML-based GeoJSON/CSV features)
else:
Expand Down
112 changes: 43 additions & 69 deletions pygeoapi/provider/postgresql_indoordb.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def get_collections_list(self):

clean_list.append({
'id': c_id,
# Default to the ID if title is missing
'title': props.get('title', c_id),
'itemType': props.get('itemType', 'indoorfeature')
})
Expand Down Expand Up @@ -163,7 +162,6 @@ def post_collection(self, collection):
}
with self.connection.cursor() as cur:
try:
# 2. Insert (Let Postgres handle the 'id' column automatically)
insert_query = """
INSERT INTO collection (id_str, collection_property)
VALUES (%s, %s)
Expand Down Expand Up @@ -228,8 +226,47 @@ def delete_collection(self, collection_id:str):
self.connection.rollback()
LOGGER.error(f"Error creating collection: {e}")
raise e


def patch_collection(self, collection_id:str, data):
with self.connection.cursor() as cur:
try:
cur.execute("SELECT id FROM collection WHERE id_str = %s", (collection_id,))
row = cur.fetchone()

if not row:
return False # Collection not found

coll_pk = row[0]

sql = "SELECT collection_property FROM collection WHERE id = %s"
cur.execute(sql, (coll_pk,))

collection_json = cur.fetchone()

fields = []
values = []

if 'title' in data:
collection_json['title'] = data['title']

if 'description' in data:
collection_json['description'] = data['description']

update_sql = "UPDATE collection SET collection_property = %s WHERE id = %s"
cur.execute(update_sql, (json.dumps(collection_json),coll_pk,))

self.connection.commit()
return True

except Exception as e:
self.connection.rollback()
LOGGER.error(f"Error updating collection: {e}")
raise e

# endregion


# region IndoorFeatures
def is_indoor_collection(self, collection_id:str):
"""
Expand Down Expand Up @@ -260,7 +297,6 @@ def get_collection_items(
"""
Retrieve the indoor feature collection /collections/{collectionId}/items
Optimized to fetch data and total count in a single query.
/collections/{collectionId}/items
"""
try:
# 1. Prepare Filter Strings
Expand Down Expand Up @@ -363,7 +399,6 @@ def get_feature(self, collection_id: str, feature_id:str, level:str=None, bbox:l
properties = props or {}
thematic_layers = []
interlayer_connections = []
# Initialize Skeleton
result_feature = {
"type": "Feature",
"id": feature_id_str,
Expand Down Expand Up @@ -512,7 +547,6 @@ def delete_indoorfeature(self, collection_id:str, feature_id:str):
res = cur.fetchone()

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

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

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

def get_layer(self, collection_id:str, feature_id:str, layer_id:str, level:str=None, bbox:list=None):
"""
Retrieves a single Thematic Layer.
Retrieves a single Thematic Layer with String ID.
If not filtered, just the meta data is given.
- PrimalSpace: Filtered by 'level' if provided.
- DualSpace: Returns the ENTIRE network (unfiltered) for connectivity.
Expand Down Expand Up @@ -817,7 +850,7 @@ def get_layer(self, collection_id:str, feature_id:str, layer_id:str, level:str=N
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):
"""
Helper to build PrimalSpaceLayer.
Supports optional filtering by 'level'.
Supports optional filtering by 'level' and 'bbox'.
"""
primal_space = {
"id": primalspace_id,
Expand Down Expand Up @@ -1053,7 +1086,7 @@ def _post_thematic_layer(self, collection_pk:int, feature_pk:int, layer_data):

layer_new = cur.fetchone()

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

# Insert Dual Members (Nodes/Edges)
Expand Down Expand Up @@ -1168,65 +1201,6 @@ def _post_primal_members(self, collection_pk:int, feature_pk:int, layer_pk:int,
# If there is no 2D geometry but 3D, project 3D to 2D geometry
LOGGER.debug("Project geometry 3D to 2D ")
sql_project_shell = """
WITH faces AS (
SELECT
c.id,
s.shell_idx,
f.face
FROM cell_space_n_boundary c
CROSS JOIN LATERAL jsonb_array_elements(c."3D_geometry"->'coordinates')
WITH ORDINALITY AS s(shell, shell_idx)
CROSS JOIN LATERAL jsonb_array_elements(s.shell) AS f(face)
WHERE c."3D_geometry" IS NOT NULL
AND c.type = 'space'
AND c."2D_geometry" IS NULL
AND c.thematiclayer_id = %s
AND (
s.shell_idx = 1
OR jsonb_array_length(c."3D_geometry"->'coordinates') > 1
)
),
proj AS (
SELECT
id,
shell_idx,
ST_SetSRID(
ST_Force2D(
ST_GeomFromGeoJSON(
jsonb_build_object(
'type', 'Polygon',
'coordinates',
CASE
-- if face is already [ring,...], keep it; else wrap to [ring]
WHEN jsonb_typeof(face->0->0) = 'array' THEN face
ELSE jsonb_build_array(face)
END
)::text
)
),
0
) AS g2d
FROM faces
),
u AS (
SELECT
id,
ST_UnaryUnion( ST_Collect(g2d) FILTER (WHERE shell_idx = 1) ) AS ext2d,
ST_UnaryUnion( ST_Collect(g2d) FILTER (WHERE shell_idx > 1) ) AS int2d
FROM proj
GROUP BY id
)
UPDATE cell_space_n_boundary c
SET "2D_geometry" =
CASE
WHEN u.int2d IS NULL THEN u.ext2d
ELSE ST_Difference(u.ext2d, u.int2d)
END
FROM u
WHERE c.id = u.id
AND c.thematiclayer_id = %s;
"""
sql_project_shell = """
WITH targets AS (
SELECT id, "3D_geometry"
FROM cell_space_n_boundary
Expand Down
54 changes: 0 additions & 54 deletions requirements-indoor.txt

This file was deleted.

Loading