diff --git a/docker-compose.yml b/docker-compose.yml index 00192689..6752e22a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,7 +55,7 @@ services: # retries: 3 proxy: - image: "docker.io/nginx:1.25.3-bookworm" + image: "docker.io/nginx:1.27.0-bookworm" depends_on: central: condition: service_healthy @@ -68,7 +68,7 @@ services: restart: "unless-stopped" central: - image: "ghcr.io/hotosm/fmtm/odkcentral:v2024.1.0" + image: "ghcr.io/hotosm/fmtm/odkcentral:v2024.2.1" depends_on: central-db: condition: service_healthy @@ -98,7 +98,7 @@ services: restart: "unless-stopped" central-db: - image: "postgis/postgis:14-3.4-alpine" + image: "postgis/postgis:17-3.5-alpine" environment: - POSTGRES_USER=odk - POSTGRES_PASSWORD=odk diff --git a/osm_fieldwork/form_components/mandatory_fields.py b/osm_fieldwork/form_components/mandatory_fields.py index 1fdd6202..8c943a3c 100644 --- a/osm_fieldwork/form_components/mandatory_fields.py +++ b/osm_fieldwork/form_components/mandatory_fields.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -# Copyright (c) 2020, 2021, 2022, 2023, 2024 Humanitarian OpenStreetMap Team +# Copyright (c) Humanitarian OpenStreetMap Team # # This file is part of OSM-Fieldwork. # @@ -40,120 +40,147 @@ """ from datetime import datetime +from enum import Enum import pandas as pd -current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S") -meta_data = [ - {"type": "start", "name": "start"}, - {"type": "end", "name": "end"}, - {"type": "today", "name": "today"}, - {"type": "phonenumber", "name": "phonenumber"}, - {"type": "deviceid", "name": "deviceid"}, - {"type": "username", "name": "username"}, - { - "type": "email", - "name": "email", - }, -] +class DbGeomType(str, Enum): + """Type for new geometries collected.""" -meta_df = pd.DataFrame(meta_data) + POINT = "POINT" + POLYGON = "POLYGON" + LINESTRING = "LINESTRING" -mandatory_data = [ - { - "type": "note", - "name": "instructions", - "label::english(en)": """Welcome ${username}. This survey form was generated - by HOT's FMTM to record ${form_category} map features.""", - "label::nepali(ne)": """स्वागत छ ${username}। ${form_category} नक्सा Data रेकर्ड गर्न HOT को FMTM द्वारा - यो सर्वेक्षण फारम उत्पन्न भएको थियो।""", - }, - {"notes": "Fields essential to FMTM"}, - {"type": "start-geopoint", "name": "warmup", "notes": "collects location on form start"}, - {"type": "select_one_from_file features.csv", "name": "feature", "label::english(en)": "Geometry", "appearance": "map"}, - { - "type": "geopoint", - "name": "new_feature", - "label::english(en)": "Alternatively, take a gps coordinates of a new feature", - "label::nepali(ne)": "वैकल्पिक रूपमा, नयाँ सुविधाको GPS निर्देशांक लिनुहोस्।", - "appearance": "placement-map", - "relevant": "${feature}= ''", - "required": "yes", - }, - { - "type": "calculate", - "name": "form_category", - "label::english(en)": "FMTM form category", - "appearance": "minimal", - "calculation": "once('Unkown')", - }, - { - "type": "calculate", - "name": "xid", - "notes": "e.g. OSM ID", - "label::english(en)": "Feature ID", - "appearance": "minimal", - "calculation": "if(${feature} != '', instance('features')/root/item[name=${feature}]/osm_id, '')", - }, - { - "type": "calculate", - "name": "xlocation", - "notes": "e.g. OSM Geometry", - "label::english(en)": "Feature Geometry", - "appearance": "minimal", - "calculation": "if(${feature} != '', instance('features')/root/item[name=${feature}]/geometry, ${new_feature})", - "save_to": "geometry", - }, - { - "type": "calculate", - "name": "task_id", - "notes": "e.g. FMTM Task ID", - "label::english(en)": "Task ID", - "appearance": "minimal", - "calculation": "if(${feature} != '', instance('features')/root/item[name=${feature}]/task_id, '')", - "save_to": "task_id", - }, - { - "type": "calculate", - "name": "status", - "notes": "Update the Entity 'status' field", - "label::english(en)": "Mapping Status", - "appearance": "minimal", - "calculation": """if(${new_feature} != '', 2, - if(${building_exists} = 'no', 5, - if(${digitisation_correct} = 'no', 6, - ${status})))""", - "default": "2", - "trigger": "${new_feature}", - "save_to": "status", - }, - { - "type": "select_one yes_no", - "name": "building_exists", - "label::english(en)": "Does this feature exist?", - "label::nepali(ne)": "के यो भवन अवस्थित छ?", - "relevant": "${feature} != '' ", - }, - { - "type": "calculate", - "name": "submission_ids", - "notes": "Update the submission ids", - "label::english(en)": "Submission ids", - "appearance": "minimal", - "calculation": """if( - instance('features')/root/item[name=${feature}]/submission_ids = '', - ${instanceID}, - concat(instance('features')/root/item[name=${feature}]/submission_ids, ',', ${instanceID}) - )""", - "save_to": "submission_ids", - }, -] -mandatory_df = pd.DataFrame(mandatory_data) +meta_df = pd.DataFrame( + [ + {"type": "start", "name": "start"}, + {"type": "end", "name": "end"}, + {"type": "today", "name": "today"}, + {"type": "phonenumber", "name": "phonenumber"}, + {"type": "deviceid", "name": "deviceid"}, + {"type": "username", "name": "username"}, + { + "type": "email", + "name": "email", + }, + ] +) + + +def get_mandatory_fields(new_geom_type: DbGeomType): + """Return the mandatory fields data.""" + if new_geom_type == DbGeomType.POINT: + geom_field = "geopoint" + elif new_geom_type == DbGeomType.POLYGON: + geom_field = "geoshape" + elif new_geom_type == DbGeomType.LINESTRING: + geom_field = "geotrace" + else: + raise ValueError(f"Unsupported geometry type: {new_geom_type}") + + return [ + { + "type": "note", + "name": "instructions", + "label::english(en)": """Welcome ${username}. This survey form was generated + by HOT's FMTM to record ${form_category} map features.""", + "label::nepali(ne)": """स्वागत छ ${username}। ${form_category} नक्सा Data रेकर्ड गर्न HOT को FMTM द्वारा + यो सर्वेक्षण फारम उत्पन्न भएको थियो।""", + }, + {"notes": "Fields essential to FMTM"}, + {"type": "start-geopoint", "name": "warmup", "notes": "collects location on form start"}, + {"type": "select_one_from_file features.csv", "name": "feature", "label::english(en)": "Geometry", "appearance": "map"}, + { + "type": geom_field, + "name": "new_feature", + "label::english(en)": "Please draw a new geometry", + "label::nepali(ne)": "कृपया नयाँ ज्यामिति कोर्नुहोस्।", + "appearance": "placement-map", + "relevant": "${feature}= ''", + "required": "yes", + }, + { + "type": "calculate", + "name": "form_category", + "label::english(en)": "FMTM form category", + "appearance": "minimal", + "calculation": "once('Unkown')", + }, + { + "type": "calculate", + "name": "xid", + "notes": "e.g. OSM ID", + "label::english(en)": "Feature ID", + "appearance": "minimal", + "calculation": "if(${feature} != '', instance('features')/root/item[name=${feature}]/osm_id, '')", + }, + { + "type": "calculate", + "name": "xlocation", + "notes": "e.g. OSM Geometry", + "label::english(en)": "Feature Geometry", + "appearance": "minimal", + "calculation": "if(${feature} != '', instance('features')/root/item[name=${feature}]/geometry, ${new_feature})", + "save_to": "geometry", + }, + { + "type": "calculate", + "name": "task_id", + "notes": "e.g. FMTM Task ID", + "label::english(en)": "Task ID", + "appearance": "minimal", + "calculation": "if(${feature} != '', instance('features')/root/item[name=${feature}]/task_id, '')", + "save_to": "task_id", + }, + { + "type": "calculate", + "name": "status", + "notes": "Update the Entity 'status' field", + "label::english(en)": "Mapping Status", + "appearance": "minimal", + "calculation": """if(${new_feature} != '', 2, + if(${building_exists} = 'no', 5, + if(${digitisation_correct} = 'no', 6, + ${status})))""", + "default": "2", + "trigger": "${new_feature}", + "save_to": "status", + }, + { + "type": "select_one yes_no", + "name": "building_exists", + "label::english(en)": "Does this feature exist?", + "label::nepali(ne)": "के यो भवन अवस्थित छ?", + "relevant": "${feature} != '' ", + }, + { + "type": "calculate", + "name": "submission_ids", + "notes": "Update the submission ids", + "label::english(en)": "Submission ids", + "appearance": "minimal", + "calculation": """if( + instance('features')/root/item[name=${feature}]/submission_ids = '', + ${instanceID}, + concat(instance('features')/root/item[name=${feature}]/submission_ids, ',', ${instanceID}) + )""", + "save_to": "submission_ids", + }, + ] + + +def create_survey_df(new_geom_type: DbGeomType) -> pd.DataFrame: + """Create the survey sheet dataframe. + + We do this in a function to allow the geometry type + for new data to be specified. + """ + fields = get_mandatory_fields(new_geom_type) + mandatory_df = pd.DataFrame(fields) + return pd.concat([meta_df, mandatory_df]) -# Define the survey sheet -survey_df = pd.concat([meta_df, mandatory_df]) # Define entities sheet entities_data = [ @@ -174,7 +201,7 @@ settings_data = [ { "form_id": "mandatory_fields", - "version": current_datetime, + "version": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "form_title": "Mandatory Fields Form", "allow_choice_duplicates": "yes", } diff --git a/osm_fieldwork/update_xlsform.py b/osm_fieldwork/update_xlsform.py index 17fa3fb7..f42b86e8 100644 --- a/osm_fieldwork/update_xlsform.py +++ b/osm_fieldwork/update_xlsform.py @@ -1,3 +1,23 @@ +#!/usr/bin/python3 + +# Copyright (c) Humanitarian OpenStreetMap Team +# +# This file is part of OSM-Fieldwork. +# +# This is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OSM-Fieldwork. If not, see . +# + """Update an existing XLSForm with additional fields useful for field mapping.""" import logging @@ -11,7 +31,7 @@ from osm_fieldwork.form_components.choice_fields import choices_df, digitisation_choices_df from osm_fieldwork.form_components.digitisation_fields import digitisation_df -from osm_fieldwork.form_components.mandatory_fields import entities_df, meta_df, settings_df, survey_df +from osm_fieldwork.form_components.mandatory_fields import DbGeomType, create_survey_df, entities_df, meta_df, settings_df log = logging.getLogger(__name__) @@ -254,6 +274,7 @@ async def append_mandatory_fields( form_category: str, additional_entities: list[str] = None, existing_id: str = None, + new_geom_type: DbGeomType = DbGeomType.POINT, ) -> tuple[str, BytesIO]: """Append mandatory fields to the XLSForm for use in FMTM. @@ -265,6 +286,8 @@ async def append_mandatory_fields( The values should be plural, so that 's' will be stripped in the field name. existing_id(str): an existing UUID to use for the form_id, else random uuid4. + new_geom_type (DbGeomType): the type of geometry required when collecting + new geometry data: point, line, polygon. Returns: tuple(str, BytesIO): the xFormId + the update XLSForm wrapped in BytesIO. @@ -280,6 +303,7 @@ async def append_mandatory_fields( custom_sheets = standardize_xlsform_sheets(custom_sheets) log.debug("Merging survey sheet XLSForm data") + survey_df = create_survey_df(new_geom_type) custom_sheets["survey"] = merge_dataframes(survey_df, custom_sheets.get("survey"), digitisation_df) # Hardcode the form_category value for the start instructions if form_category.endswith("s"):