diff --git a/moped-database/metadata/tables.yaml b/moped-database/metadata/tables.yaml
index 0a588ccbdf..92f5e640f5 100644
--- a/moped-database/metadata/tables.yaml
+++ b/moped-database/metadata/tables.yaml
@@ -814,6 +814,182 @@
- id
filter: {}
allow_aggregations: true
+- table:
+ name: feature_school_beacons
+ schema: public
+ insert_permissions:
+ - role: moped-admin
+ permission:
+ check: {}
+ set:
+ created_by_user_id: x-hasura-user-db-id
+ updated_by_user_id: x-hasura-user-db-id
+ columns:
+ - beacon_id
+ - school_zone_beacon_id
+ - beacon_name
+ - component_id
+ - geography
+ - is_deleted
+ - knack_id
+ - location_name
+ - zone_name
+ comment: ""
+ - role: moped-editor
+ permission:
+ check: {}
+ set:
+ created_by_user_id: x-hasura-user-db-id
+ updated_by_user_id: x-hasura-user-db-id
+ columns:
+ - beacon_id
+ - school_zone_beacon_id
+ - beacon_name
+ - component_id
+ - geography
+ - is_deleted
+ - knack_id
+ - location_name
+ - zone_name
+ comment: ""
+ select_permissions:
+ - role: moped-admin
+ permission:
+ columns:
+ - is_deleted
+ - component_id
+ - created_by_user_id
+ - id
+ - updated_by_user_id
+ - beacon_id
+ - school_zone_beacon_id
+ - beacon_name
+ - knack_id
+ - location_name
+ - zone_name
+ - created_at
+ - updated_at
+ - geography
+ filter: {}
+ comment: ""
+ - role: moped-editor
+ permission:
+ columns:
+ - is_deleted
+ - component_id
+ - created_by_user_id
+ - id
+ - updated_by_user_id
+ - beacon_id
+ - school_zone_beacon_id
+ - beacon_name
+ - knack_id
+ - location_name
+ - zone_name
+ - created_at
+ - updated_at
+ - geography
+ filter: {}
+ comment: ""
+ - role: moped-viewer
+ permission:
+ columns:
+ - is_deleted
+ - component_id
+ - created_by_user_id
+ - id
+ - updated_by_user_id
+ - beacon_id
+ - school_zone_beacon_id
+ - beacon_name
+ - knack_id
+ - location_name
+ - zone_name
+ - created_at
+ - updated_at
+ - geography
+ filter: {}
+ comment: ""
+ update_permissions:
+ - role: moped-admin
+ permission:
+ columns:
+ - beacon_id
+ - school_zone_beacon_id
+ - beacon_name
+ - component_id
+ - geography
+ - is_deleted
+ - knack_id
+ - location_name
+ - zone_name
+ filter: {}
+ check: {}
+ set:
+ updated_by_user_id: x-hasura-user-db-id
+ comment: ""
+ - role: moped-editor
+ permission:
+ columns:
+ - beacon_id
+ - school_zone_beacon_id
+ - beacon_name
+ - component_id
+ - geography
+ - is_deleted
+ - knack_id
+ - location_name
+ - zone_name
+ filter: {}
+ check: {}
+ set:
+ updated_by_user_id: x-hasura-user-db-id
+ comment: ""
+ event_triggers:
+ - name: activity_log_feature_school_beacons
+ definition:
+ enable_manual: false
+ insert:
+ columns: '*'
+ update:
+ columns: '*'
+ retry_conf:
+ interval_sec: 10
+ num_retries: 0
+ timeout_sec: 60
+ webhook_from_env: HASURA_ENDPOINT
+ headers:
+ - name: x-hasura-admin-secret
+ value_from_env: ACTIVITY_LOG_API_SECRET
+ request_transform:
+ body:
+ action: transform
+ template: |-
+ {
+ "query": "mutation InsertActivity($object: moped_activity_log_insert_input!) { insert_moped_activity_log_one(object: $object) { activity_id } }",
+ "variables": {
+ "object": {
+ "record_id": {{ $body.event.data.new.id }},
+ "record_type": {{ $body.table.name }},
+ "activity_id": {{ $body.id }},
+ "record_data": {"event": {{ $body.event }}},
+ "description": [{"newSchema": "true"}],
+ "operation_type": {{ $body.event.op }},
+ "updated_by_user_id": {{ $session_variables?['x-hasura-user-db-id'] ?? 1}}
+ }
+ }
+ }
+ method: POST
+ query_params: {}
+ template_engine: Kriti
+ version: 2
+ cleanup_config:
+ batch_size: 10000
+ clean_invocation_logs: false
+ clear_older_than: 168
+ paused: true
+ schedule: 0 0 * * *
+ timeout: 60
- table:
name: feature_signals
schema: public
@@ -2070,6 +2246,15 @@
remote_table:
name: feature_intersections
schema: public
+ - name: feature_school_beacons
+ using:
+ manual_configuration:
+ column_mapping:
+ project_component_id: component_id
+ insertion_order: null
+ remote_table:
+ name: feature_school_beacons
+ schema: public
- name: feature_signals
using:
manual_configuration:
diff --git a/moped-database/migrations/1725983667950_add_beacons_asset_table/down.sql b/moped-database/migrations/1725983667950_add_beacons_asset_table/down.sql
new file mode 100644
index 0000000000..0dd6a4a281
--- /dev/null
+++ b/moped-database/migrations/1725983667950_add_beacons_asset_table/down.sql
@@ -0,0 +1,3 @@
+DROP TABLE feature_school_beacons;
+
+DELETE FROM feature_layers WHERE internal_table IN ('feature_school_beacons')
diff --git a/moped-database/migrations/1725983667950_add_beacons_asset_table/up.sql b/moped-database/migrations/1725983667950_add_beacons_asset_table/up.sql
new file mode 100644
index 0000000000..86c83ae923
--- /dev/null
+++ b/moped-database/migrations/1725983667950_add_beacons_asset_table/up.sql
@@ -0,0 +1,30 @@
+CREATE TABLE public.feature_school_beacons (
+ school_zone_beacon_id text NOT NULL,
+ beacon_id text NOT NULL,
+ knack_id text NOT NULL,
+ location_name text NOT NULL,
+ zone_name text NOT NULL,
+ beacon_name text NOT NULL,
+ geography geography('MULTIPOINT') NOT NULL,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ created_by_user_id int4 NULL,
+ updated_by_user_id int4 NULL,
+ updated_at timestamptz DEFAULT now()
+) inherits (features);
+
+
+ALTER TABLE feature_school_beacons
+ADD CONSTRAINT fk_feature_school_beacons_created_by FOREIGN KEY (created_by_user_id) REFERENCES moped_users(user_id),
+ADD CONSTRAINT fk_feature_school_beacons_updated_by FOREIGN KEY (updated_by_user_id) REFERENCES moped_users(user_id);
+
+-- Adding comments for audit fields
+COMMENT ON COLUMN feature_school_beacons.created_at IS 'Timestamp of when the school beacon feature was created';
+COMMENT ON COLUMN feature_school_beacons.created_by_user_id IS 'User ID of the creator of the school beacon feature';
+COMMENT ON COLUMN feature_school_beacons.updated_by_user_id IS 'User ID of the last updater of the school beacon feature';
+COMMENT ON COLUMN feature_school_beacons.updated_at IS 'Timestamp of the last update of the school beacon feature';
+
+-- Adding comments for feature_school_beacons constraints
+COMMENT ON CONSTRAINT fk_feature_school_beacons_created_by ON feature_school_beacons IS 'Foreign key constraint linking created_by_user_id to moped_users table.';
+COMMENT ON CONSTRAINT fk_feature_school_beacons_updated_by ON feature_school_beacons IS 'Foreign key constraint linking updated_by_user_id to moped_users table.';
+
+INSERT INTO feature_layers (id, internal_table, reference_layer_primary_key_column) values (6, 'feature_school_beacons', 'school_zone_beacon_id');
diff --git a/moped-database/migrations/1726048331776_update_school_beacon_asset_feature_layer/down.sql b/moped-database/migrations/1726048331776_update_school_beacon_asset_feature_layer/down.sql
new file mode 100644
index 0000000000..41e1f4c8dc
--- /dev/null
+++ b/moped-database/migrations/1726048331776_update_school_beacon_asset_feature_layer/down.sql
@@ -0,0 +1,6 @@
+-- revert asset layer for school zone beacons
+UPDATE moped_components
+SET
+ asset_feature_layer_id = NULL
+WHERE
+ component_subtype = 'School Zone Beacon';
diff --git a/moped-database/migrations/1726048331776_update_school_beacon_asset_feature_layer/up.sql b/moped-database/migrations/1726048331776_update_school_beacon_asset_feature_layer/up.sql
new file mode 100644
index 0000000000..c7c6641f5b
--- /dev/null
+++ b/moped-database/migrations/1726048331776_update_school_beacon_asset_feature_layer/up.sql
@@ -0,0 +1,6 @@
+-- set asset layer for school beacon to signals
+UPDATE moped_components
+SET
+ asset_feature_layer_id = 6
+WHERE
+ component_subtype = 'School Zone Beacon';
diff --git a/moped-database/migrations/1726852807615_school_beacon_triggers/down.sql b/moped-database/migrations/1726852807615_school_beacon_triggers/down.sql
new file mode 100644
index 0000000000..a485a7c0bc
--- /dev/null
+++ b/moped-database/migrations/1726852807615_school_beacon_triggers/down.sql
@@ -0,0 +1,5 @@
+DROP TRIGGER IF EXISTS update_feature_school_beacons_council_district ON feature_school_beacons;
+
+DROP TRIGGER IF EXISTS feature_school_beacons_parent_audit_log_trigger ON feature_school_beacons;
+
+DROP TRIGGER IF EXISTS set_feature_school_beacons_updated_at ON feature_school_beacons;
diff --git a/moped-database/migrations/1726852807615_school_beacon_triggers/up.sql b/moped-database/migrations/1726852807615_school_beacon_triggers/up.sql
new file mode 100644
index 0000000000..4e8a664d9c
--- /dev/null
+++ b/moped-database/migrations/1726852807615_school_beacon_triggers/up.sql
@@ -0,0 +1,20 @@
+CREATE TRIGGER update_feature_school_beacons_council_district BEFORE INSERT OR UPDATE ON
+feature_school_beacons FOR EACH ROW EXECUTE FUNCTION update_council_district();
+COMMENT ON TRIGGER update_feature_school_beacons_council_district ON feature_school_beacons IS
+'Trigger to insert record in feature_council_district table connecting feature_id with corresponding council district id';
+
+
+-- Trigger for feature_school_beacons table
+CREATE TRIGGER feature_school_beacons_parent_audit_log_trigger
+AFTER INSERT OR UPDATE ON feature_school_beacons
+FOR EACH ROW
+EXECUTE FUNCTION update_audit_fields_with_dynamic_parent_table_name("moped_proj_components", "project_component_id", "component_id");
+COMMENT ON TRIGGER feature_school_beacons_parent_audit_log_trigger ON feature_school_beacons IS 'Trigger to update parent project and component audit fields';
+
+
+CREATE TRIGGER set_feature_school_beacons_updated_at
+BEFORE INSERT OR UPDATE ON feature_school_beacons
+FOR EACH ROW
+EXECUTE FUNCTION public.set_updated_at();
+
+COMMENT ON TRIGGER set_feature_school_beacons_updated_at ON public.feature_school_beacons IS 'Trigger to set updated_at timestamp for each insert or update on feature_school_beacons';
diff --git a/moped-database/migrations/1727229291111_add_beacons_to_unified_features_view/down.sql b/moped-database/migrations/1727229291111_add_beacons_to_unified_features_view/down.sql
new file mode 100644
index 0000000000..54e91b38db
--- /dev/null
+++ b/moped-database/migrations/1727229291111_add_beacons_to_unified_features_view/down.sql
@@ -0,0 +1,127 @@
+DROP view IF EXISTS project_geography;
+DROP view IF EXISTS uniform_features;
+
+CREATE OR REPLACE VIEW uniform_features AS
+ SELECT feature_signals.id,
+ feature_signals.component_id,
+ 'feature_signals'::text AS "table",
+ json_build_object('signal_id', feature_signals.signal_id, 'knack_id', feature_signals.knack_id, 'location_name', feature_signals.location_name, 'signal_type', feature_signals.signal_type) AS attributes,
+ feature_signals.geography,
+ districts.council_districts,
+ NULL::integer AS length_feet,
+ feature_signals.created_at,
+ feature_signals.updated_at,
+ feature_signals.created_by_user_id,
+ feature_signals.updated_by_user_id
+ FROM feature_signals
+ LEFT JOIN ( SELECT d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts d
+ GROUP BY d.feature_id) districts ON districts.feature_id = feature_signals.id
+ WHERE feature_signals.is_deleted = false
+UNION ALL
+ SELECT feature_street_segments.id,
+ feature_street_segments.component_id,
+ 'feature_street_segments'::text AS "table",
+ json_build_object('ctn_segment_id', feature_street_segments.ctn_segment_id, 'from_address_min', feature_street_segments.from_address_min, 'to_address_max', feature_street_segments.to_address_max, 'full_street_name', feature_street_segments.full_street_name, 'line_type', feature_street_segments.line_type, 'symbol', feature_street_segments.symbol, 'source_layer', feature_street_segments.source_layer) AS attributes,
+ feature_street_segments.geography,
+ districts.council_districts,
+ feature_street_segments.length_feet,
+ feature_street_segments.created_at,
+ feature_street_segments.updated_at,
+ feature_street_segments.created_by_user_id,
+ feature_street_segments.updated_by_user_id
+ FROM feature_street_segments
+ LEFT JOIN ( SELECT d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts d
+ GROUP BY d.feature_id) districts ON districts.feature_id = feature_street_segments.id
+ WHERE feature_street_segments.is_deleted = false
+UNION ALL
+ SELECT feature_intersections.id,
+ feature_intersections.component_id,
+ 'feature_intersections'::text AS "table",
+ json_build_object('intersection_id', feature_intersections.intersection_id, 'source_layer', feature_intersections.source_layer) AS attributes,
+ feature_intersections.geography,
+ districts.council_districts,
+ NULL::integer AS length_feet,
+ feature_intersections.created_at,
+ feature_intersections.updated_at,
+ feature_intersections.created_by_user_id,
+ feature_intersections.updated_by_user_id
+ FROM feature_intersections
+ LEFT JOIN ( SELECT d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts d
+ GROUP BY d.feature_id) districts ON districts.feature_id = feature_intersections.id
+ WHERE feature_intersections.is_deleted = false
+UNION ALL
+ SELECT feature_drawn_points.id,
+ feature_drawn_points.component_id,
+ 'feature_drawn_points'::text AS "table",
+ NULL::json AS attributes,
+ feature_drawn_points.geography,
+ districts.council_districts,
+ NULL::integer AS length_feet,
+ feature_drawn_points.created_at,
+ feature_drawn_points.updated_at,
+ feature_drawn_points.created_by_user_id,
+ feature_drawn_points.updated_by_user_id
+ FROM feature_drawn_points
+ LEFT JOIN ( SELECT d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts d
+ GROUP BY d.feature_id) districts ON districts.feature_id = feature_drawn_points.id
+ WHERE feature_drawn_points.is_deleted = false
+UNION ALL
+ SELECT feature_drawn_lines.id,
+ feature_drawn_lines.component_id,
+ 'feature_drawn_lines'::text AS "table",
+ NULL::json AS attributes,
+ feature_drawn_lines.geography,
+ districts.council_districts,
+ feature_drawn_lines.length_feet,
+ feature_drawn_lines.created_at,
+ feature_drawn_lines.updated_at,
+ feature_drawn_lines.created_by_user_id,
+ feature_drawn_lines.updated_by_user_id
+ FROM feature_drawn_lines
+ LEFT JOIN ( SELECT d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts d
+ GROUP BY d.feature_id) districts ON districts.feature_id = feature_drawn_lines.id
+ WHERE feature_drawn_lines.is_deleted = false;
+
+COMMENT ON VIEW uniform_features IS 'This view unifies various geographical feature data from multiple tables such as signals, street segments, intersections, drawn points, and lines. It provides a view of these features along with their attributes, geographic details, and council district associations.';
+
+CREATE OR REPLACE VIEW project_geography AS (
+ SELECT
+ moped_project.project_id,
+ uniform_features.id AS feature_id,
+ moped_components.component_id AS component_archtype_id,
+ moped_components.line_representation AS line_representation,
+ moped_proj_components.project_component_id AS component_id,
+ moped_proj_components.is_deleted,
+ moped_project.project_name,
+ feature_layers.internal_table AS "table",
+ feature_layers.reference_layer_primary_key_column AS original_fk,
+ moped_components.component_name,
+ uniform_features.attributes,
+ uniform_features.geography,
+ uniform_features.council_districts,
+ uniform_features.length_feet,
+ uniform_features.created_at AS feature_created_at,
+ uniform_features.updated_at AS feature_updated_at,
+ uniform_features.created_by_user_id AS feature_created_by_user_id,
+ uniform_features.updated_by_user_id AS feature_updated_by_user_id
+ FROM
+ moped_project
+ INNER JOIN moped_proj_components ON moped_project.project_id = moped_proj_components.project_id
+ INNER JOIN moped_components ON moped_proj_components.component_id = moped_components.component_id
+ INNER JOIN feature_layers ON moped_components.feature_layer_id = feature_layers.id
+ INNER JOIN uniform_features ON moped_proj_components.project_component_id = uniform_features.component_id
+ WHERE
+ moped_proj_components.is_deleted IS FALSE
+);
+
+COMMENT ON VIEW public.project_geography IS 'The project_geography view merges project-specific data with the unified geographical features from the uniform_features view. It links projects with their respective geographical components, including type, attributes, and location.';
diff --git a/moped-database/migrations/1727229291111_add_beacons_to_unified_features_view/up.sql b/moped-database/migrations/1727229291111_add_beacons_to_unified_features_view/up.sql
new file mode 100644
index 0000000000..267788aa0c
--- /dev/null
+++ b/moped-database/migrations/1727229291111_add_beacons_to_unified_features_view/up.sql
@@ -0,0 +1,170 @@
+DROP VIEW IF EXISTS project_geography;
+DROP VIEW IF EXISTS uniform_features;
+
+CREATE OR REPLACE VIEW uniform_features
+AS SELECT
+ feature_signals.id,
+ feature_signals.component_id,
+ 'feature_signals'::text AS "table",
+ json_build_object('signal_id', feature_signals.signal_id, 'knack_id', feature_signals.knack_id, 'location_name', feature_signals.location_name, 'signal_type', feature_signals.signal_type) AS attributes,
+ feature_signals.geography,
+ districts.council_districts,
+ NULL::integer AS length_feet,
+ feature_signals.created_at,
+ feature_signals.updated_at,
+ feature_signals.created_by_user_id,
+ feature_signals.updated_by_user_id
+FROM feature_signals LEFT JOIN
+ (
+ SELECT
+ d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts AS d
+ GROUP BY d.feature_id
+ ) AS districts ON feature_signals.id = districts.feature_id
+WHERE feature_signals.is_deleted = FALSE
+UNION ALL
+SELECT
+ feature_street_segments.id,
+ feature_street_segments.component_id,
+ 'feature_street_segments'::text AS "table",
+ json_build_object('ctn_segment_id', feature_street_segments.ctn_segment_id, 'from_address_min', feature_street_segments.from_address_min, 'to_address_max', feature_street_segments.to_address_max, 'full_street_name', feature_street_segments.full_street_name, 'line_type', feature_street_segments.line_type, 'symbol', feature_street_segments.symbol, 'source_layer', feature_street_segments.source_layer) AS attributes,
+ feature_street_segments.geography,
+ districts.council_districts,
+ feature_street_segments.length_feet,
+ feature_street_segments.created_at,
+ feature_street_segments.updated_at,
+ feature_street_segments.created_by_user_id,
+ feature_street_segments.updated_by_user_id
+FROM feature_street_segments
+LEFT JOIN (
+ SELECT
+ d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts AS d
+ GROUP BY d.feature_id
+) AS districts ON feature_street_segments.id = districts.feature_id
+WHERE feature_street_segments.is_deleted = FALSE
+UNION ALL
+SELECT
+ feature_intersections.id,
+ feature_intersections.component_id,
+ 'feature_intersections'::text AS "table",
+ json_build_object('intersection_id', feature_intersections.intersection_id, 'source_layer', feature_intersections.source_layer) AS attributes,
+ feature_intersections.geography,
+ districts.council_districts,
+ NULL::integer AS length_feet,
+ feature_intersections.created_at,
+ feature_intersections.updated_at,
+ feature_intersections.created_by_user_id,
+ feature_intersections.updated_by_user_id
+FROM feature_intersections
+LEFT JOIN (
+ SELECT
+ d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts AS d
+ GROUP BY d.feature_id
+) AS districts ON feature_intersections.id = districts.feature_id
+WHERE feature_intersections.is_deleted = FALSE
+UNION ALL
+SELECT
+ feature_drawn_points.id,
+ feature_drawn_points.component_id,
+ 'feature_drawn_points'::text AS "table",
+ NULL::json AS attributes,
+ feature_drawn_points.geography,
+ districts.council_districts,
+ NULL::integer AS length_feet,
+ feature_drawn_points.created_at,
+ feature_drawn_points.updated_at,
+ feature_drawn_points.created_by_user_id,
+ feature_drawn_points.updated_by_user_id
+FROM feature_drawn_points
+LEFT JOIN (
+ SELECT
+ d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts AS d
+ GROUP BY d.feature_id
+) AS districts ON feature_drawn_points.id = districts.feature_id
+WHERE feature_drawn_points.is_deleted = FALSE
+UNION ALL
+SELECT
+ feature_drawn_lines.id,
+ feature_drawn_lines.component_id,
+ 'feature_drawn_lines'::text AS "table",
+ NULL::json AS attributes,
+ feature_drawn_lines.geography,
+ districts.council_districts,
+ feature_drawn_lines.length_feet,
+ feature_drawn_lines.created_at,
+ feature_drawn_lines.updated_at,
+ feature_drawn_lines.created_by_user_id,
+ feature_drawn_lines.updated_by_user_id
+FROM feature_drawn_lines
+LEFT JOIN (
+ SELECT
+ d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts AS d
+ GROUP BY d.feature_id
+) AS districts ON feature_drawn_lines.id = districts.feature_id
+WHERE feature_drawn_lines.is_deleted = FALSE
+UNION ALL
+SELECT
+ feature_school_beacons.id,
+ feature_school_beacons.component_id,
+ 'feature_school_beacons'::text AS "table",
+ json_build_object('school_zone_beacon_id', feature_school_beacons.school_zone_beacon_id, 'knack_id', feature_school_beacons.knack_id, 'location_name', feature_school_beacons.location_name, 'zone_name', feature_school_beacons.zone_name, 'beacon_name', feature_school_beacons.beacon_name) AS attributes,
+ feature_school_beacons.geography,
+ districts.council_districts,
+ NULL::integer AS length_feet,
+ feature_school_beacons.created_at,
+ feature_school_beacons.updated_at,
+ feature_school_beacons.created_by_user_id,
+ feature_school_beacons.updated_by_user_id
+FROM feature_school_beacons LEFT JOIN
+ (
+ SELECT
+ d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts AS d
+ GROUP BY d.feature_id
+ ) AS districts ON feature_school_beacons.id = districts.feature_id
+WHERE feature_school_beacons.is_deleted = FALSE;
+
+COMMENT ON VIEW uniform_features IS 'This view unifies various geographical feature data from multiple tables such as signals, street segments, intersections, drawn points, and lines. It provides a view of these features along with their attributes, geographic details, and council district associations.';
+
+CREATE OR REPLACE VIEW project_geography AS (
+ SELECT
+ moped_project.project_id,
+ uniform_features.id AS feature_id,
+ moped_components.component_id AS component_archtype_id,
+ moped_components.line_representation AS line_representation,
+ moped_proj_components.project_component_id AS component_id,
+ moped_proj_components.is_deleted,
+ moped_project.project_name,
+ feature_layers.internal_table AS "table",
+ feature_layers.reference_layer_primary_key_column AS original_fk,
+ moped_components.component_name,
+ uniform_features.attributes,
+ uniform_features.geography,
+ uniform_features.council_districts,
+ uniform_features.length_feet,
+ uniform_features.created_at AS feature_created_at,
+ uniform_features.updated_at AS feature_updated_at,
+ uniform_features.created_by_user_id AS feature_created_by_user_id,
+ uniform_features.updated_by_user_id AS feature_updated_by_user_id
+ FROM
+ moped_project
+ INNER JOIN moped_proj_components ON moped_project.project_id = moped_proj_components.project_id
+ INNER JOIN moped_components ON moped_proj_components.component_id = moped_components.component_id
+ INNER JOIN feature_layers ON moped_components.feature_layer_id = feature_layers.id
+ INNER JOIN uniform_features ON moped_proj_components.project_component_id = uniform_features.component_id
+ WHERE
+ moped_proj_components.is_deleted IS FALSE
+);
+
+
+COMMENT ON VIEW public.project_geography IS 'The project_geography view merges project-specific data with the unified geographical features from the uniform_features view. It links projects with their respective geographical components, including type, attributes, and location.';
diff --git a/moped-database/migrations/1727279529178_update_component_agol_view_school_beacons/down.sql b/moped-database/migrations/1727279529178_update_component_agol_view_school_beacons/down.sql
new file mode 100644
index 0000000000..5d7125a94f
--- /dev/null
+++ b/moped-database/migrations/1727279529178_update_component_agol_view_school_beacons/down.sql
@@ -0,0 +1,245 @@
+DROP VIEW IF EXISTS exploded_component_arcgis_online_view;
+DROP VIEW IF EXISTS component_arcgis_online_view;
+
+-- Update arguments of get_project_development_status() and get_project_development_status_date() to consider component-level phase name simple
+CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH work_types AS (
+ SELECT
+ mpcwt.project_component_id,
+ string_agg(mwt.name, ', '::text) AS work_types
+ FROM moped_proj_component_work_types AS mpcwt
+ LEFT JOIN moped_work_types AS mwt ON mpcwt.work_type_id = mwt.id
+ WHERE mpcwt.is_deleted = false
+ GROUP BY mpcwt.project_component_id
+),
+
+council_districts AS (
+ SELECT
+ features.component_id AS project_component_id,
+ string_agg(DISTINCT features_council_districts.council_district_id::text, ', '::text) AS council_districts,
+ string_agg(DISTINCT lpad(features_council_districts.council_district_id::text, 2, '0'::text), ', '::text) AS council_districts_searchable
+ FROM features_council_districts
+ LEFT JOIN features ON features_council_districts.feature_id = features.id
+ WHERE features.is_deleted = false
+ GROUP BY features.component_id
+),
+
+comp_geography AS (
+ SELECT
+ feature_union.component_id AS project_component_id,
+ string_agg(DISTINCT feature_union.id::text, ', '::text) AS feature_ids,
+ st_asgeojson(st_union(array_agg(feature_union.geography)))::json AS geometry,
+ st_asgeojson(st_union(array_agg(feature_union.line_geography)))::json AS line_geometry,
+ string_agg(DISTINCT feature_union.signal_id::text, ', '::text) AS signal_ids,
+ sum(feature_union.length_feet) AS length_feet_total
+ FROM (
+ SELECT
+ feature_signals.id,
+ feature_signals.component_id,
+ feature_signals.geography::geometry AS geography,
+ st_exteriorring(st_buffer(feature_signals.geography, 7::double precision)::geometry) AS line_geography,
+ feature_signals.signal_id,
+ null::integer AS length_feet
+ FROM feature_signals
+ WHERE feature_signals.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_street_segments.id,
+ feature_street_segments.component_id,
+ feature_street_segments.geography::geometry AS geography,
+ feature_street_segments.geography::geometry AS line_geography,
+ null::integer AS signal_id,
+ feature_street_segments.length_feet
+ FROM feature_street_segments
+ WHERE feature_street_segments.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_intersections.id,
+ feature_intersections.component_id,
+ feature_intersections.geography::geometry AS geography,
+ st_exteriorring(st_buffer(feature_intersections.geography, 7::double precision)::geometry) AS line_geography,
+ null::integer AS signal_id,
+ null::integer AS length_feet
+ FROM feature_intersections
+ WHERE feature_intersections.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_drawn_points.id,
+ feature_drawn_points.component_id,
+ feature_drawn_points.geography::geometry AS geography,
+ st_exteriorring(st_buffer(feature_drawn_points.geography, 7::double precision)::geometry) AS line_geography,
+ null::integer AS signal_id,
+ null::integer AS length_feet
+ FROM feature_drawn_points
+ WHERE feature_drawn_points.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_drawn_lines.id,
+ feature_drawn_lines.component_id,
+ feature_drawn_lines.geography::geometry AS geography,
+ feature_drawn_lines.geography::geometry AS line_geography,
+ null::integer AS signal_id,
+ feature_drawn_lines.length_feet
+ FROM feature_drawn_lines
+ WHERE feature_drawn_lines.is_deleted = false
+ ) AS feature_union
+ GROUP BY feature_union.component_id
+),
+
+subcomponents AS (
+ SELECT
+ mpcs.project_component_id,
+ string_agg(ms.subcomponent_name, ', '::text) AS subcomponents
+ FROM moped_proj_components_subcomponents AS mpcs
+ LEFT JOIN moped_subcomponents AS ms ON mpcs.subcomponent_id = ms.subcomponent_id
+ WHERE mpcs.is_deleted = false
+ GROUP BY mpcs.project_component_id
+),
+
+component_tags AS (
+ SELECT
+ mpct.project_component_id,
+ string_agg((mct.type || ' - '::text) || mct.name, ', '::text) AS component_tags
+ FROM moped_proj_component_tags AS mpct
+ LEFT JOIN moped_component_tags AS mct ON mpct.component_tag_id = mct.id
+ WHERE mpct.is_deleted = false
+ GROUP BY mpct.project_component_id
+),
+
+related_projects AS (
+ SELECT
+ pmp.project_id,
+ concat_ws(', '::text, pmp.project_id, string_agg(cmp.project_id::text, ', '::text)) AS related_project_ids_with_self,
+ concat_ws(', '::text, lpad(pmp.project_id::text, 5, '0'::text), string_agg(lpad(cmp.project_id::text, 5, '0'::text), ', '::text)) AS related_project_ids_searchable_with_self
+ FROM moped_project AS pmp
+ LEFT JOIN moped_project AS cmp ON pmp.project_id = cmp.parent_project_id
+ WHERE cmp.is_deleted = false
+ GROUP BY pmp.project_id
+),
+
+latest_public_meeting_date AS (
+ SELECT
+ mpm.project_id,
+ coalesce(max(mpm.date_actual), max(mpm.date_estimate)) AS latest
+ FROM moped_proj_milestones AS mpm
+ WHERE mpm.milestone_id = 65 AND mpm.is_deleted = false
+ GROUP BY mpm.project_id
+),
+
+earliest_active_or_construction_phase_date AS (
+ SELECT
+ mpp.project_id,
+ min(mpp.phase_start) AS earliest
+ FROM moped_proj_phases AS mpp
+ LEFT JOIN moped_phases AS mp ON mpp.phase_id = mp.phase_id
+ WHERE (mp.phase_name_simple = any(ARRAY['Active'::text, 'Construction'::text])) AND mpp.is_deleted = false
+ GROUP BY mpp.project_id
+)
+
+SELECT
+ mpc.project_id,
+ comp_geography.project_component_id,
+ comp_geography.feature_ids,
+ mpc.component_id,
+ comp_geography.geometry,
+ comp_geography.line_geometry,
+ comp_geography.signal_ids,
+ council_districts.council_districts,
+ council_districts.council_districts_searchable,
+ NOT coalesce(council_districts.council_districts IS null OR council_districts.council_districts = ''::text, false) AS is_within_city_limits,
+ comp_geography.length_feet_total,
+ round(comp_geography.length_feet_total::numeric / 5280::numeric, 2) AS length_miles_total,
+ mc.component_name,
+ mc.component_subtype,
+ mc.component_name_full,
+ 'placeholder text'::text AS component_categories,
+ subcomponents.subcomponents AS component_subcomponents,
+ work_types.work_types AS component_work_types,
+ component_tags.component_tags,
+ mpc.description AS component_description,
+ mpc.interim_project_component_id,
+ coalesce(mpc.completion_date, plv.substantial_completion_date) AS substantial_completion_date,
+ plv.substantial_completion_date_estimated,
+ mpc.srts_id,
+ mpc.location_description AS component_location_description,
+ plv.project_name,
+ plv.project_name_secondary,
+ plv.project_name_full,
+ plv.project_description,
+ plv.ecapris_subproject_id,
+ plv.project_website,
+ plv.updated_at AS project_updated_at,
+ mpc.phase_id AS component_phase_id,
+ mph.phase_name AS component_phase_name,
+ mph.phase_name_simple AS component_phase_name_simple,
+ current_phase.phase_id AS project_phase_id,
+ current_phase.phase_name AS project_phase_name,
+ current_phase.phase_name_simple AS project_phase_name_simple,
+ coalesce(mph.phase_name, current_phase.phase_name) AS current_phase_name,
+ coalesce(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple,
+ plv.project_team_members,
+ plv.project_sponsor,
+ plv.project_lead,
+ plv.public_process_status,
+ plv.interim_project_id,
+ plv.project_partners,
+ plv.task_order_names,
+ plv.funding_source_name,
+ plv.funding_source_and_program_names AS funding_sources,
+ plv.type_name,
+ plv.project_status_update,
+ plv.project_status_update_date_created,
+ to_char(plv.construction_start_date AT TIME ZONE 'US/Central', 'YYYY-MM-DD'::text) AS construction_start_date,
+ plv.project_inspector,
+ plv.project_designer,
+ plv.project_tags,
+ plv.workgroup_contractors,
+ plv.contract_numbers,
+ plv.parent_project_id,
+ plv.parent_project_name,
+ plv.parent_project_url,
+ plv.parent_project_name AS parent_project_name_full,
+ rp.related_project_ids_with_self AS related_project_ids,
+ rp.related_project_ids_searchable_with_self AS related_project_ids_searchable,
+ plv.knack_project_id AS knack_data_tracker_project_record_id,
+ plv.project_url,
+ (plv.project_url || '?tab=map&project_component_id='::text) || mpc.project_component_id::text AS component_url,
+ get_project_development_status(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple)) AS project_development_status,
+ project_development_status_date.result AS project_development_status_date,
+ to_char(project_development_status_date.result, 'YYYY')::integer AS project_development_status_date_calendar_year, -- noqa
+ to_char(project_development_status_date.result, 'FMMonth YYYY') AS project_development_status_date_calendar_year_month, -- noqa
+ to_char(project_development_status_date.result, 'YYYY-MM') AS project_development_status_date_calendar_year_month_numeric, -- noqa
+ extract(QUARTER FROM project_development_status_date.result)::text AS project_development_status_date_calendar_year_quarter, -- noqa
+ CASE WHEN extract(QUARTER FROM project_development_status_date.result) = 4 THEN (to_char(project_development_status_date.result, 'YYYY')::integer + 1)::text ELSE to_char(project_development_status_date.result, 'YYYY') END AS project_development_status_date_fiscal_year, -- noqa
+ CASE WHEN extract(QUARTER FROM project_development_status_date.result) = 4 THEN 1 ELSE extract(QUARTER FROM project_development_status_date.result) + 1 END::text AS project_development_status_date_fiscal_year_quarter, -- noqa
+ plv.added_by AS project_added_by
+FROM moped_proj_components AS mpc
+LEFT JOIN comp_geography ON mpc.project_component_id = comp_geography.project_component_id
+LEFT JOIN council_districts ON mpc.project_component_id = council_districts.project_component_id
+LEFT JOIN subcomponents ON mpc.project_component_id = subcomponents.project_component_id
+LEFT JOIN work_types ON mpc.project_component_id = work_types.project_component_id
+LEFT JOIN component_tags ON mpc.project_component_id = component_tags.project_component_id
+LEFT JOIN project_list_view AS plv ON mpc.project_id = plv.project_id
+LEFT JOIN current_phase_view AS current_phase ON mpc.project_id = current_phase.project_id
+LEFT JOIN moped_phases AS mph ON mpc.phase_id = mph.phase_id
+LEFT JOIN moped_components AS mc ON mpc.component_id = mc.component_id
+LEFT JOIN related_projects AS rp ON mpc.project_id = rp.project_id
+LEFT JOIN latest_public_meeting_date AS lpmd ON mpc.project_id = lpmd.project_id
+LEFT JOIN earliest_active_or_construction_phase_date AS eaocpd ON mpc.project_id = eaocpd.project_id
+LEFT JOIN LATERAL (SELECT get_project_development_status_date(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple)) AT TIME ZONE 'US/Central' AS result) AS project_development_status_date ON true -- noqa
+WHERE mpc.is_deleted = false AND plv.is_deleted = false;
+
+-- create exploded view
+CREATE VIEW exploded_component_arcgis_online_view AS
+SELECT
+ component_arcgis_online_view.project_id,
+ component_arcgis_online_view.project_component_id,
+ ST_GEOMETRYTYPE(dump.geom) AS geometry_type,
+ dump.path[1] AS point_index, -- ordinal value of the point in the MultiPoint geometry
+ component_arcgis_online_view.geometry AS original_geometry,
+ ST_ASGEOJSON(dump.geom) AS exploded_geometry, -- noqa: RF04
+ component_arcgis_online_view.project_updated_at
+FROM
+ component_arcgis_online_view,
+ LATERAL ST_DUMP(ST_GEOMFROMGEOJSON(component_arcgis_online_view.geometry)) AS dump -- noqa: RF04
+WHERE
+ ST_GEOMETRYTYPE(ST_GEOMFROMGEOJSON(component_arcgis_online_view.geometry)) = 'ST_MultiPoint';
diff --git a/moped-database/migrations/1727279529178_update_component_agol_view_school_beacons/up.sql b/moped-database/migrations/1727279529178_update_component_agol_view_school_beacons/up.sql
new file mode 100644
index 0000000000..a996eff7fe
--- /dev/null
+++ b/moped-database/migrations/1727279529178_update_component_agol_view_school_beacons/up.sql
@@ -0,0 +1,261 @@
+DROP VIEW IF EXISTS exploded_component_arcgis_online_view;
+DROP VIEW IF EXISTS component_arcgis_online_view;
+
+CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH
+work_types AS (
+ SELECT
+ mpcwt.project_component_id,
+ string_agg(mwt.name, ', '::text) AS work_types
+ FROM moped_proj_component_work_types AS mpcwt
+ LEFT JOIN moped_work_types AS mwt ON mpcwt.work_type_id = mwt.id
+ WHERE mpcwt.is_deleted = false
+ GROUP BY mpcwt.project_component_id
+),
+
+council_districts AS (
+ SELECT
+ features.component_id AS project_component_id,
+ string_agg(DISTINCT features_council_districts.council_district_id::text, ', '::text) AS council_districts,
+ string_agg(DISTINCT lpad(features_council_districts.council_district_id::text, 2, '0'::text), ', '::text) AS council_districts_searchable
+ FROM features_council_districts
+ LEFT JOIN features ON features_council_districts.feature_id = features.id
+ WHERE features.is_deleted = false
+ GROUP BY features.component_id
+),
+
+comp_geography AS (
+ SELECT
+ feature_union.component_id AS project_component_id,
+ string_agg(DISTINCT feature_union.id::text, ', '::text) AS feature_ids,
+ st_asgeojson(st_union(array_agg(feature_union.geography)))::json AS geometry,
+ st_asgeojson(st_union(array_agg(feature_union.line_geography)))::json AS line_geometry,
+ string_agg(DISTINCT feature_union.signal_id::text, ', '::text) AS signal_ids,
+ sum(feature_union.length_feet) AS length_feet_total
+ FROM (
+ SELECT
+ feature_signals.id,
+ feature_signals.component_id,
+ feature_signals.geography::geometry AS geography,
+ st_exteriorring(st_buffer(feature_signals.geography, 7::double precision)::geometry) AS line_geography,
+ feature_signals.signal_id,
+ null::integer AS length_feet
+ FROM feature_signals
+ WHERE feature_signals.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_street_segments.id,
+ feature_street_segments.component_id,
+ feature_street_segments.geography::geometry AS geography,
+ feature_street_segments.geography::geometry AS line_geography,
+ null::integer AS signal_id,
+ feature_street_segments.length_feet
+ FROM feature_street_segments
+ WHERE feature_street_segments.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_intersections.id,
+ feature_intersections.component_id,
+ feature_intersections.geography::geometry AS geography,
+ st_exteriorring(st_buffer(feature_intersections.geography, 7::double precision)::geometry) AS line_geography,
+ null::integer AS signal_id,
+ null::integer AS length_feet
+ FROM feature_intersections
+ WHERE feature_intersections.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_drawn_points.id,
+ feature_drawn_points.component_id,
+ feature_drawn_points.geography::geometry AS geography,
+ st_exteriorring(st_buffer(feature_drawn_points.geography, 7::double precision)::geometry) AS line_geography,
+ null::integer AS signal_id,
+ null::integer AS length_feet
+ FROM feature_drawn_points
+ WHERE feature_drawn_points.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_drawn_lines.id,
+ feature_drawn_lines.component_id,
+ feature_drawn_lines.geography::geometry AS geography,
+ feature_drawn_lines.geography::geometry AS line_geography,
+ null::integer AS signal_id,
+ feature_drawn_lines.length_feet
+ FROM feature_drawn_lines
+ WHERE feature_drawn_lines.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_school_beacons.id,
+ feature_school_beacons.component_id,
+ feature_school_beacons.geography::geometry AS geography,
+ st_exteriorring(st_buffer(feature_school_beacons.geography, 7::double precision)::geometry) AS line_geography,
+ null::integer AS signal_id,
+ null::integer AS length_feet
+ FROM feature_school_beacons
+ WHERE feature_school_beacons.is_deleted = false
+ ) feature_union
+ GROUP BY feature_union.component_id
+),
+
+subcomponents AS (
+ SELECT
+ mpcs.project_component_id,
+ string_agg(ms.subcomponent_name, ', '::text) AS subcomponents
+ FROM moped_proj_components_subcomponents mpcs
+ LEFT JOIN moped_subcomponents ms ON mpcs.subcomponent_id = ms.subcomponent_id
+ WHERE mpcs.is_deleted = false
+ GROUP BY mpcs.project_component_id
+),
+
+component_tags AS (
+ SELECT
+ mpct.project_component_id,
+ string_agg((mct.type || ' - '::text) || mct.name, ', '::text) AS component_tags
+ FROM moped_proj_component_tags mpct
+ LEFT JOIN moped_component_tags mct ON mpct.component_tag_id = mct.id
+ WHERE mpct.is_deleted = false
+ GROUP BY mpct.project_component_id
+),
+
+related_projects AS (
+ SELECT
+ pmp.project_id,
+ concat_ws(', '::text, pmp.project_id, string_agg(cmp.project_id::text, ', '::text)) AS related_project_ids_with_self,
+ concat_ws(', '::text, lpad(pmp.project_id::text, 5, '0'::text), string_agg(lpad(cmp.project_id::text, 5, '0'::text), ', '::text)) AS related_project_ids_searchable_with_self
+ FROM moped_project pmp
+ LEFT JOIN moped_project cmp ON pmp.project_id = cmp.parent_project_id
+ WHERE cmp.is_deleted = false
+ GROUP BY pmp.project_id
+),
+
+latest_public_meeting_date AS (
+ SELECT
+ mpm.project_id,
+ coalesce(max(mpm.date_actual), max(mpm.date_estimate)) AS latest
+ FROM moped_proj_milestones mpm
+ WHERE mpm.milestone_id = 65 AND mpm.is_deleted = false
+ GROUP BY mpm.project_id
+),
+
+earliest_active_or_construction_phase_date AS (
+ SELECT
+ mpp.project_id,
+ min(mpp.phase_start) AS earliest
+ FROM moped_proj_phases mpp
+ LEFT JOIN moped_phases mp ON mpp.phase_id = mp.phase_id
+ WHERE (mp.phase_name_simple = any(ARRAY['Active'::text, 'Construction'::text])) AND mpp.is_deleted = false
+ GROUP BY mpp.project_id
+)
+
+SELECT
+ mpc.project_id,
+ comp_geography.project_component_id,
+ comp_geography.feature_ids,
+ mpc.component_id,
+ comp_geography.geometry,
+ comp_geography.line_geometry,
+ comp_geography.signal_ids,
+ council_districts.council_districts,
+ council_districts.council_districts_searchable,
+ NOT coalesce(council_districts.council_districts IS null OR council_districts.council_districts = ''::text, false) AS is_within_city_limits,
+ comp_geography.length_feet_total,
+ round(comp_geography.length_feet_total::numeric / 5280::numeric, 2) AS length_miles_total,
+ mc.component_name,
+ mc.component_subtype,
+ mc.component_name_full,
+ 'placeholder text'::text AS component_categories,
+ subcomponents.subcomponents AS component_subcomponents,
+ work_types.work_types AS component_work_types,
+ component_tags.component_tags,
+ mpc.description AS component_description,
+ mpc.interim_project_component_id,
+ coalesce(mpc.completion_date, plv.substantial_completion_date) AS substantial_completion_date,
+ plv.substantial_completion_date_estimated,
+ mpc.srts_id,
+ mpc.location_description AS component_location_description,
+ plv.project_name,
+ plv.project_name_secondary,
+ plv.project_name_full,
+ plv.project_description,
+ plv.ecapris_subproject_id,
+ plv.project_website,
+ plv.updated_at AS project_updated_at,
+ mpc.phase_id AS component_phase_id,
+ mph.phase_name AS component_phase_name,
+ mph.phase_name_simple AS component_phase_name_simple,
+ current_phase.phase_id AS project_phase_id,
+ current_phase.phase_name AS project_phase_name,
+ current_phase.phase_name_simple AS project_phase_name_simple,
+ coalesce(mph.phase_name, current_phase.phase_name) AS current_phase_name,
+ coalesce(mph.phase_name_simple, current_phase.phase_name_simple) AS current_phase_name_simple,
+ plv.project_team_members,
+ plv.project_sponsor,
+ plv.project_lead,
+ plv.public_process_status,
+ plv.interim_project_id,
+ plv.project_partners,
+ plv.task_order_names,
+ plv.funding_source_name,
+ plv.funding_source_and_program_names AS funding_sources,
+ plv.type_name,
+ plv.project_status_update,
+ plv.project_status_update_date_created,
+ to_char(timezone('US/Central'::text, plv.construction_start_date), 'YYYY-MM-DD'::text) AS construction_start_date,
+ plv.project_inspector,
+ plv.project_designer,
+ plv.project_tags,
+ plv.workgroup_contractors,
+ plv.contract_numbers,
+ plv.parent_project_id,
+ plv.parent_project_name,
+ plv.parent_project_url,
+ plv.parent_project_name AS parent_project_name_full,
+ rp.related_project_ids_with_self AS related_project_ids,
+ rp.related_project_ids_searchable_with_self AS related_project_ids_searchable,
+ plv.knack_project_id AS knack_data_tracker_project_record_id,
+ plv.project_url,
+ (plv.project_url || '?tab=map&project_component_id='::text) || mpc.project_component_id::text AS component_url,
+ get_project_development_status(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple)) AS project_development_status,
+ project_development_status_date.result AS project_development_status_date,
+ to_char(project_development_status_date.result, 'YYYY'::text)::integer AS project_development_status_date_calendar_year,
+ to_char(project_development_status_date.result, 'FMMonth YYYY'::text) AS project_development_status_date_calendar_year_month,
+ to_char(project_development_status_date.result, 'YYYY-MM'::text) AS project_development_status_date_calendar_year_month_numeric,
+ date_part('quarter'::text, project_development_status_date.result)::text AS project_development_status_date_calendar_year_quarter,
+ CASE
+ WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN (to_char(project_development_status_date.result, 'YYYY'::text)::integer + 1)::text
+ ELSE to_char(project_development_status_date.result, 'YYYY'::text)
+ END AS project_development_status_date_fiscal_year,
+ CASE
+ WHEN date_part('quarter'::text, project_development_status_date.result) = 4::double precision THEN 1::double precision
+ ELSE date_part('quarter'::text, project_development_status_date.result) + 1::double precision
+ END::text AS project_development_status_date_fiscal_year_quarter,
+ plv.added_by AS project_added_by
+FROM moped_proj_components mpc
+LEFT JOIN comp_geography ON mpc.project_component_id = comp_geography.project_component_id
+LEFT JOIN council_districts ON mpc.project_component_id = council_districts.project_component_id
+LEFT JOIN subcomponents ON mpc.project_component_id = subcomponents.project_component_id
+LEFT JOIN work_types ON mpc.project_component_id = work_types.project_component_id
+LEFT JOIN component_tags ON mpc.project_component_id = component_tags.project_component_id
+LEFT JOIN project_list_view plv ON mpc.project_id = plv.project_id
+LEFT JOIN current_phase_view current_phase ON mpc.project_id = current_phase.project_id
+LEFT JOIN moped_phases mph ON mpc.phase_id = mph.phase_id
+LEFT JOIN moped_components mc ON mpc.component_id = mc.component_id
+LEFT JOIN related_projects rp ON mpc.project_id = rp.project_id
+LEFT JOIN latest_public_meeting_date lpmd ON mpc.project_id = lpmd.project_id
+LEFT JOIN earliest_active_or_construction_phase_date eaocpd ON mpc.project_id = eaocpd.project_id
+LEFT JOIN LATERAL (SELECT timezone('US/Central'::text, get_project_development_status_date(lpmd.latest::timestamp with time zone, eaocpd.earliest, coalesce(mpc.completion_date, plv.substantial_completion_date), plv.substantial_completion_date_estimated, coalesce(mph.phase_name_simple, current_phase.phase_name_simple))) AS result) project_development_status_date ON true
+WHERE mpc.is_deleted = false AND plv.is_deleted = false;
+
+
+CREATE VIEW exploded_component_arcgis_online_view AS
+SELECT
+ component_arcgis_online_view.project_id,
+ component_arcgis_online_view.project_component_id,
+ ST_GEOMETRYTYPE(dump.geom) AS geometry_type,
+ dump.path[1] AS point_index, -- ordinal value of the point in the MultiPoint geometry
+ component_arcgis_online_view.geometry AS original_geometry,
+ ST_ASGEOJSON(dump.geom) AS exploded_geometry, -- noqa: RF04
+ component_arcgis_online_view.project_updated_at
+FROM
+ component_arcgis_online_view,
+ LATERAL ST_DUMP(ST_GEOMFROMGEOJSON(component_arcgis_online_view.geometry)) AS dump -- noqa: RF04
+WHERE
+ ST_GEOMETRYTYPE(ST_GEOMFROMGEOJSON(component_arcgis_online_view.geometry)) = 'ST_MultiPoint';
diff --git a/moped-database/views/component_arcgis_online_view.sql b/moped-database/views/component_arcgis_online_view.sql
index 1b2e793a1f..3eec77c4b8 100644
--- a/moped-database/views/component_arcgis_online_view.sql
+++ b/moped-database/views/component_arcgis_online_view.sql
@@ -1,4 +1,4 @@
--- Most recent migration: moped-database/migrations/1725556123250_add_component_level_phase_simple/up.sql
+-- Most recent migration: moped-database/migrations/1727279529178_update_component_agol_view_school_beacons/up.sql
CREATE OR REPLACE VIEW component_arcgis_online_view AS WITH work_types AS (
SELECT
@@ -79,6 +79,16 @@ comp_geography AS (
feature_drawn_lines.length_feet
FROM feature_drawn_lines
WHERE feature_drawn_lines.is_deleted = false
+ UNION ALL
+ SELECT
+ feature_school_beacons.id,
+ feature_school_beacons.component_id,
+ feature_school_beacons.geography::geometry AS geography,
+ st_exteriorring(st_buffer(feature_school_beacons.geography, 7::double precision)::geometry) AS line_geography,
+ null::integer AS signal_id,
+ null::integer AS length_feet
+ FROM feature_school_beacons
+ WHERE feature_school_beacons.is_deleted = false
) feature_union
GROUP BY feature_union.component_id
),
diff --git a/moped-database/views/exploded_component_arcgis_online_view.sql b/moped-database/views/exploded_component_arcgis_online_view.sql
index cddba03fdb..b221558269 100644
--- a/moped-database/views/exploded_component_arcgis_online_view.sql
+++ b/moped-database/views/exploded_component_arcgis_online_view.sql
@@ -1,4 +1,4 @@
--- Most recent migration: moped-database/migrations/1725649291445_add_exploded_moped_geometry_view_for_agol/up.sql
+-- Most recent migration: moped-database/migrations/1727279529178_update_component_agol_view_school_beacons/up.sql
CREATE OR REPLACE VIEW exploded_component_arcgis_online_view AS SELECT
component_arcgis_online_view.project_id,
diff --git a/moped-database/views/project_geography.sql b/moped-database/views/project_geography.sql
index e565a85e49..d7ef2b05cd 100644
--- a/moped-database/views/project_geography.sql
+++ b/moped-database/views/project_geography.sql
@@ -1,4 +1,4 @@
--- Most recent migration: moped-database/migrations/1717191608944_add_line_rep_to_proj_geo_view/up.sql
+-- Most recent migration: moped-database/migrations/1727229291111_add_beacons_to_unified_features_view/up.sql
CREATE OR REPLACE VIEW project_geography AS SELECT
moped_project.project_id,
diff --git a/moped-database/views/uniform_features.sql b/moped-database/views/uniform_features.sql
index bb1694a8fd..4fb5193376 100644
--- a/moped-database/views/uniform_features.sql
+++ b/moped-database/views/uniform_features.sql
@@ -1,4 +1,4 @@
--- Most recent migration: moped-database/migrations/1700515731002_add_audit_fields_to_unified_features_view/up.sql
+-- Most recent migration: moped-database/migrations/1727229291111_add_beacons_to_unified_features_view/up.sql
CREATE OR REPLACE VIEW uniform_features AS SELECT
feature_signals.id,
@@ -19,7 +19,7 @@ LEFT JOIN (
array_agg(d.council_district_id) AS council_districts
FROM features_council_districts d
GROUP BY d.feature_id
-) districts ON districts.feature_id = feature_signals.id
+) districts ON feature_signals.id = districts.feature_id
WHERE feature_signals.is_deleted = FALSE
UNION ALL
SELECT
@@ -41,7 +41,7 @@ LEFT JOIN (
array_agg(d.council_district_id) AS council_districts
FROM features_council_districts d
GROUP BY d.feature_id
-) districts ON districts.feature_id = feature_street_segments.id
+) districts ON feature_street_segments.id = districts.feature_id
WHERE feature_street_segments.is_deleted = FALSE
UNION ALL
SELECT
@@ -63,7 +63,7 @@ LEFT JOIN (
array_agg(d.council_district_id) AS council_districts
FROM features_council_districts d
GROUP BY d.feature_id
-) districts ON districts.feature_id = feature_intersections.id
+) districts ON feature_intersections.id = districts.feature_id
WHERE feature_intersections.is_deleted = FALSE
UNION ALL
SELECT
@@ -85,7 +85,7 @@ LEFT JOIN (
array_agg(d.council_district_id) AS council_districts
FROM features_council_districts d
GROUP BY d.feature_id
-) districts ON districts.feature_id = feature_drawn_points.id
+) districts ON feature_drawn_points.id = districts.feature_id
WHERE feature_drawn_points.is_deleted = FALSE
UNION ALL
SELECT
@@ -107,5 +107,27 @@ LEFT JOIN (
array_agg(d.council_district_id) AS council_districts
FROM features_council_districts d
GROUP BY d.feature_id
-) districts ON districts.feature_id = feature_drawn_lines.id
-WHERE feature_drawn_lines.is_deleted = FALSE;
+) districts ON feature_drawn_lines.id = districts.feature_id
+WHERE feature_drawn_lines.is_deleted = FALSE
+UNION ALL
+SELECT
+ feature_school_beacons.id,
+ feature_school_beacons.component_id,
+ 'feature_school_beacons'::text AS "table",
+ json_build_object('school_zone_beacon_id', feature_school_beacons.school_zone_beacon_id, 'knack_id', feature_school_beacons.knack_id, 'location_name', feature_school_beacons.location_name, 'zone_name', feature_school_beacons.zone_name, 'beacon_name', feature_school_beacons.beacon_name) AS attributes,
+ feature_school_beacons.geography,
+ districts.council_districts,
+ NULL::integer AS length_feet,
+ feature_school_beacons.created_at,
+ feature_school_beacons.updated_at,
+ feature_school_beacons.created_by_user_id,
+ feature_school_beacons.updated_by_user_id
+FROM feature_school_beacons
+LEFT JOIN (
+ SELECT
+ d.feature_id,
+ array_agg(d.council_district_id) AS council_districts
+ FROM features_council_districts d
+ GROUP BY d.feature_id
+) districts ON feature_school_beacons.id = districts.feature_id
+WHERE feature_school_beacons.is_deleted = FALSE;
diff --git a/moped-editor/src/queries/components.js b/moped-editor/src/queries/components.js
index 9a2f690481..8f8051de1d 100644
--- a/moped-editor/src/queries/components.js
+++ b/moped-editor/src/queries/components.js
@@ -150,6 +150,17 @@ export const PROJECT_COMPONENT_FIELDS = gql`
source_layer
component_id
}
+ feature_school_beacons(where: { is_deleted: { _eq: false } }) {
+ id
+ geometry: geography
+ component_id
+ knack_id
+ location_name
+ beacon_id
+ school_zone_beacon_id
+ zone_name
+ beacon_name
+ }
}
`;
@@ -216,6 +227,7 @@ export const UPDATE_COMPONENT_ATTRIBUTES = gql`
$subcomponents: [moped_proj_components_subcomponents_insert_input!]!
$workTypes: [moped_proj_component_work_types_insert_input!]!
$signalsToCreate: [feature_signals_insert_input!]!
+ $schoolBeaconsToCreate: [feature_school_beacons_insert_input!]!
$featureIdsToDelete: [Int!]!
$phaseId: Int
$subphaseId: Int
@@ -286,6 +298,9 @@ export const UPDATE_COMPONENT_ATTRIBUTES = gql`
) {
affected_rows
}
+ insert_feature_school_beacons(objects: $schoolBeaconsToCreate) {
+ affected_rows
+ }
update_features(
where: { id: { _in: $featureIdsToDelete } }
_set: { is_deleted: true }
@@ -305,6 +320,7 @@ export const UPDATE_COMPONENT_FEATURES = gql`
$drawnPoints: [feature_drawn_points_insert_input!]!
$drawnLinesDragUpdates: [feature_drawn_lines_updates!]!
$drawnPointsDragUpdates: [feature_drawn_points_updates!]!
+ $schoolBeacons: [feature_school_beacons_insert_input!]!
) {
insert_feature_street_segments(objects: $streetSegments) {
affected_rows
@@ -330,6 +346,9 @@ export const UPDATE_COMPONENT_FEATURES = gql`
update_feature_drawn_points_many(updates: $drawnPointsDragUpdates) {
affected_rows
}
+ insert_feature_school_beacons(objects: $schoolBeacons) {
+ affected_rows
+ }
}
`;
diff --git a/moped-editor/src/utils/signalComponentHelpers.js b/moped-editor/src/utils/signalComponentHelpers.js
index 622db35716..4c36d38825 100644
--- a/moped-editor/src/utils/signalComponentHelpers.js
+++ b/moped-editor/src/utils/signalComponentHelpers.js
@@ -2,11 +2,17 @@ import React, { useEffect } from "react";
import { TextField } from "@mui/material";
/*
- * Socrata Endpoint
+ * Socrata Endpoint for Signals and PHBs
*/
export const SOCRATA_ENDPOINT =
"https://data.austintexas.gov/resource/p53x-x73x.geojson?$select=signal_id,location_name,location,signal_type,id&$order=signal_id asc&$limit=9999";
+/*
+ * Socrata endpoint for school beacons
+ */
+export const SOCRATA_ENDPOINT_SCHOOL_BEACONS =
+ "https://data.austintexas.gov/resource/mzsm-hucz.geojson?$select=school_zone_beacon_id,beacon_name,beacon_id,zone_name,location,id,location_name&$order=school_zone_beacon_id asc&$limit=9999";
+
/**
* An array to use as the default value for
* moped_proj_component_work_type: 7 = "New"
@@ -58,9 +64,9 @@ export const knackSignalRecordToFeatureSignalsRecord = (signal) => {
};
/**
- * Format a feature_signals table record to the format of options in the SignalComponentAutocomplete
+ * Format a feature_signals table record to the format of options in the KnackComponentAutocomplete
* @param {Object} featureSignalsRecord - A feature_signals table record
- * @return {Object} A record in the format of options in the SignalComponentAutocomplete
+ * @return {Object} A record in the format of options in the KnackComponentAutocomplete
*/
export const featureSignalsRecordToKnackSignalRecord = (
featureSignalsRecord
@@ -100,7 +106,7 @@ export const renderSignalInput = (
};
/**
- * Get's the correct COMPONENT_DEFIINITION property based on the presence of a signal feature
+ * Gets the correct COMPONENT_DEFINITION property based on the presence of a signal feature
* @param {Boolean} fromSignalAsset - if signal autocomplete switch is active
* @param {Object} signalRecord - The signal record to be inserted into a project and its component
* @param {Object[]} componentData - Array of moped_components from DB
@@ -189,3 +195,69 @@ export const generateProjectComponent = (
},
};
};
+
+/**
+ * MUI autocomplete getOptionSelected function matches input school beacon value to
+ * select options.
+ */
+export const getSchoolZoneBeaconOptionSelected = (option, value) => {
+ const optionId = option?.properties?.id;
+ const valueId = value?.properties?.id;
+ return optionId === valueId;
+};
+
+/**
+ * MUI autocomplete getOptionLabel function to which formats the value rendered in
+ * the select option menu
+ */
+export const getSchoolZoneBeaconOptionLabel = (option) =>
+ `${option.properties.zone_name}: ${option.properties.beacon_name} ${option.properties.beacon_id}`;
+
+/**
+ * Sets required fields so that a Knack school zone beacon record can be inserted into the feature_school_beacons table
+ * @param {Object} schoolBeacon- A GeoJSON feature or a falsey object (e.g. "" from empty input)
+ * @return {Object} A geojson feature collection with the beacon feature or 0 features
+ */
+export const knackSchoolBeaconRecordToFeatureSchoolBeaconRecord = (
+ schoolBeacon
+) => {
+ if (schoolBeacon && schoolBeacon?.properties && schoolBeacon?.geometry) {
+ const featureSchoolBeaconsRecord = {
+ // MultiPoint coordinates are an array of arrays, so we wrap the coordinates
+ geography: {
+ ...schoolBeacon.geometry,
+ type: "MultiPoint",
+ coordinates: [schoolBeacon.geometry.coordinates],
+ },
+ knack_id: schoolBeacon.properties.id,
+ location_name: schoolBeacon.properties.location_name,
+ beacon_id: schoolBeacon.properties.beacon_id,
+ school_zone_beacon_id: schoolBeacon.properties.school_zone_beacon_id,
+ zone_name: schoolBeacon.properties.zone_name,
+ beacon_name: schoolBeacon.properties.beacon_name,
+ };
+
+ return featureSchoolBeaconsRecord;
+ }
+};
+
+/**
+ * Format a feature_school_beacon table record to the format of options in the KnackComponentAutocomplete
+ * @param {Object} featureSignalsRecord - A feature_signals table record
+ * @return {Object} A record in the format of options in the KnackComponentAutocomplete
+ */
+export const featureSchoolBeaconRecordToKnackSchoolBeaconRecord = (
+ featureSchoolBeaconRecord
+) => {
+ const { geometry, ...restOfFeatureSchoolBeaconRecord } =
+ featureSchoolBeaconRecord;
+ const { knack_id } = restOfFeatureSchoolBeaconRecord;
+
+ const knackFormatSchoolBeaconOption = {
+ type: "Feature",
+ geometry: { ...geometry, coordinates: geometry.coordinates.flat() },
+ properties: { ...restOfFeatureSchoolBeaconRecord, id: knack_id },
+ };
+
+ return knackFormatSchoolBeaconOption;
+};
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/ComponentForm.js b/moped-editor/src/views/projects/projectView/ProjectComponents/ComponentForm.js
index e6dabe8deb..82b3fa43d4 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/ComponentForm.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/ComponentForm.js
@@ -12,7 +12,7 @@ import {
import DateFieldEditComponent from "../DateFieldEditComponent";
import { CheckCircle } from "@mui/icons-material";
import { GET_COMPONENTS_FORM_OPTIONS } from "src/queries/components";
-import SignalComponentAutocomplete from "./SignalComponentAutocomplete";
+import KnackComponentAutocomplete from "./KnackComponentAutocomplete";
import {
ComponentOptionWithIcon,
DEFAULT_COMPONENT_WORK_TYPE_OPTION,
@@ -31,7 +31,14 @@ import {
} from "./utils/form";
import ControlledAutocomplete from "../../../../components/forms/ControlledAutocomplete";
import ControlledTextInput from "src/components/forms/ControlledTextInput";
-import { getSignalOptionLabel } from "src/utils/signalComponentHelpers";
+import {
+ getSignalOptionLabel,
+ getSignalOptionSelected,
+ getSchoolZoneBeaconOptionLabel,
+ SOCRATA_ENDPOINT,
+ getSchoolZoneBeaconOptionSelected,
+ SOCRATA_ENDPOINT_SCHOOL_BEACONS,
+} from "src/utils/signalComponentHelpers";
import ComponentProperties from "./ComponentProperties";
import * as yup from "yup";
@@ -47,6 +54,7 @@ const defaultFormValues = {
description: null,
work_types: [DEFAULT_COMPONENT_WORK_TYPE_OPTION],
signal: null,
+ schoolBeacon: null,
srtsId: null,
};
@@ -72,6 +80,7 @@ const validationSchema = yup.object().shape({
work_types: yup.array().of(yup.object()).min(1).required(),
// Signal field is required if the selected component inserts into the feature_signals table
signal: yup.object().nullable(),
+ schoolBeacon: yup.object().nullable(),
srtsId: yup.string().nullable().optional(),
locationDescription: yup.string().nullable().optional(),
});
@@ -118,12 +127,20 @@ const ComponentForm = ({
);
const phaseOptions = usePhaseOptions(optionsData);
- const [component, phase, completionDate, subcomponents, signal] = watch([
+ const [
+ component,
+ phase,
+ completionDate,
+ subcomponents,
+ signal,
+ schoolBeacon,
+ ] = watch([
"component",
"phase",
"completionDate",
"subcomponents",
"signal",
+ "schoolBeacon",
]);
const isPhaseNameSimpleComplete = isPhaseOptionSimpleComplete(phase);
@@ -131,7 +148,10 @@ const ComponentForm = ({
const assetFeatureTable =
component?.data?.asset_feature_layer?.internal_table;
const isSignalComponent = assetFeatureTable === "feature_signals";
+ const isSchoolZoneBeacon = assetFeatureTable === "feature_school_beacons";
const componentTagsOptions = useComponentTagsOptions(optionsData);
+ const hasGeometry =
+ (isSignalComponent && signal) || (isSchoolZoneBeacon && schoolBeacon);
const workTypeOptions = useWorkTypeOptions(
component?.value,
@@ -194,9 +214,16 @@ const ComponentForm = ({
parentValue: watch("signal"),
dependentFieldName: "locationDescription",
comparisonVariable: "properties.id",
- valueToSet: signal
- ? // if the signal exists and the locationDescription is empty, set to option label
- getSignalOptionLabel(signal)
+ valueToSet: signal ? getSignalOptionLabel(signal) : "",
+ setValue,
+ });
+
+ useResetDependentFieldOnParentFieldChange({
+ parentValue: watch("schoolBeacon"),
+ dependentFieldName: "locationDescription",
+ comparisonVariable: "properties.id",
+ valueToSet: schoolBeacon
+ ? getSchoolZoneBeaconOptionLabel(schoolBeacon)
: "",
setValue,
});
@@ -238,9 +265,33 @@ const ComponentForm = ({
control={control}
shouldUnregister={true}
render={({ field }) => (
-
+ )}
+ />
+
+ )}
+ {isSchoolZoneBeacon && (
+
+ (
+
)}
/>
@@ -406,7 +457,7 @@ const ComponentForm = ({
type="submit"
disabled={!isDirty || areFormErrors}
>
- {isSignalComponent && signal ? "Save" : formButtonText}
+ {hasGeometry ? "Save" : formButtonText}
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/CreateComponentModal.js b/moped-editor/src/views/projects/projectView/ProjectComponents/CreateComponentModal.js
index ea16976ca7..f9aa4d8433 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/CreateComponentModal.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/CreateComponentModal.js
@@ -10,7 +10,7 @@ const CreateComponentModal = ({
onSaveDraftSignalComponent,
}) => {
const onSave = (formData) => {
- const isSavingSignalFeature = Boolean(formData.signal);
+ const isSavingKnackFeature = Boolean(formData.signal || formData.schoolBeacon);
let {
component: {
@@ -33,7 +33,7 @@ const CreateComponentModal = ({
locationDescription,
} = formData;
- if (isSavingSignalFeature) {
+ if (isSavingKnackFeature) {
// disgusting hacky override to set the internal table to the asset table
// when an asset has been selected in the form
internal_table =
@@ -61,12 +61,12 @@ const CreateComponentModal = ({
const linkMode = newComponent.line_representation ? "lines" : "points";
- // Signal components get their geometry from the Knack signal dataset so we save them
+ // Signal components and school beacon components get their geometry from Knack datasets so we save them
// immediately. All other components are saved after the user selects or draws their geometry.
- if (isSavingSignalFeature) {
+ if (isSavingKnackFeature) {
const newComponentWithSignalFeature = {
...newComponent,
- features: [formData.signal],
+ features: [formData.signal || formData.schoolBeacon],
};
onSaveDraftSignalComponent(newComponentWithSignalFeature);
} else {
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/EditAttributesModal.js b/moped-editor/src/views/projects/projectView/ProjectComponents/EditAttributesModal.js
index f981e9879e..d01600d694 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/EditAttributesModal.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/EditAttributesModal.js
@@ -15,6 +15,7 @@ import {
makeSubphaseFormFieldValue,
makeTagFormFieldValues,
makeWorkTypesFormFieldValues,
+ makeSchoolBeaconFormFieldValue,
} from "./utils/form";
const EditAttributesModal = ({
@@ -77,8 +78,13 @@ const EditAttributesModal = ({
: [];
const signalFromForm = formData.signal;
- const { signalsToCreate, featureIdsToDelete } =
- getFeatureChangesFromComponentForm(signalFromForm, clickedComponent);
+ const schoolBeaconFromForm = formData.schoolBeacon;
+ const { signalsToCreate, schoolBeaconsToCreate, featureIdsToDelete } =
+ getFeatureChangesFromComponentForm(
+ signalFromForm,
+ schoolBeaconFromForm,
+ clickedComponent
+ );
updateComponentAttributes({
variables: {
@@ -88,6 +94,7 @@ const EditAttributesModal = ({
subcomponents: subcomponentsArray,
workTypes: workTypesArray,
signalsToCreate: signalsToCreate,
+ schoolBeaconsToCreate: schoolBeaconsToCreate,
featureIdsToDelete: featureIdsToDelete,
phaseId: phase?.value,
subphaseId: subphase?.value,
@@ -106,6 +113,13 @@ const EditAttributesModal = ({
{ type: "FeatureCollection", features: [signalFromForm] },
fitBoundsOptions.zoomToClickedComponent
);
+ // Or zoom to new beacon location
+ schoolBeaconsToCreate.length > 0 &&
+ zoomMapToFeatureCollection(
+ mapRef,
+ { type: "FeatureCollection", features: [schoolBeaconFromForm] },
+ fitBoundsOptions.zoomToClickedComponent
+ );
})
.catch((error) => {
console.error(error);
@@ -113,8 +127,7 @@ const EditAttributesModal = ({
};
const onClose = (event, reason) => {
- if (reason && reason === "backdropClick")
- return;
+ if (reason && reason === "backdropClick") return;
editDispatch({ type: "cancel_attributes_edit" });
};
@@ -151,6 +164,7 @@ const EditAttributesModal = ({
: "-",
projectComponentId: clickedComponent.project_component_id,
componentLength: clickedComponent.component_length,
+ schoolBeacon: makeSchoolBeaconFormFieldValue(clickedComponent),
}
: null;
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/SignalComponentAutocomplete.js b/moped-editor/src/views/projects/projectView/ProjectComponents/KnackComponentAutocomplete.js
similarity index 63%
rename from moped-editor/src/views/projects/projectView/ProjectComponents/SignalComponentAutocomplete.js
rename to moped-editor/src/views/projects/projectView/ProjectComponents/KnackComponentAutocomplete.js
index 351170c930..33d8d50546 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/SignalComponentAutocomplete.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/KnackComponentAutocomplete.js
@@ -2,11 +2,6 @@ import React, { useEffect, useMemo } from "react";
import { CircularProgress, TextField } from "@mui/material";
import { Autocomplete, Alert } from "@mui/material";
import { useSocrataGeojson } from "src/utils/socrataHelpers";
-import {
- getSignalOptionLabel,
- getSignalOptionSelected,
- SOCRATA_ENDPOINT,
-} from "src/utils/signalComponentHelpers";
import { filterOptions } from "src/utils/autocompleteHelpers";
/**
@@ -16,21 +11,41 @@ import { filterOptions } from "src/utils/autocompleteHelpers";
* @param {Function} onChange - callback function to run when the signal is changed for React Hook Form
* @param {Object} value - the signal feature to set as the value of the autocomplete from React Hook Form
* @param {Function} onOptionsLoaded - callback function to run when the options are loaded
- * @param {String} signalType - either PHB or TRAFFIC
+ * @param {String} signalType - either PHB, TRAFFIC or null
+ * @param {String} socrataEndpoint -
+ * @param {Function} isOptionEqualToValue -
+ * @param {Function} getOptionLabel -
+ * @param {String} componentLabel -
* @return {JSX.Element}
*/
-const SignalComponentAutocomplete = React.forwardRef(
- ({ classes, onChange, value, onOptionsLoaded, signalType }, ref) => {
- const { features, loading, error } = useSocrataGeojson(SOCRATA_ENDPOINT);
+const KnackComponentAutocomplete = React.forwardRef(
+ (
+ {
+ classes,
+ onChange,
+ value,
+ onOptionsLoaded,
+ signalType,
+ socrataEndpoint,
+ isOptionEqualToValue,
+ getOptionLabel,
+ componentLabel,
+ },
+ ref
+ ) => {
+ const { features, loading, error } = useSocrataGeojson(socrataEndpoint);
// Filter returned results to the signal type chosen - PHB or TRAFFIC
+ // unless school beacons, then return all features
const featuresFilteredByType = useMemo(
() =>
- features?.filter(
- (feature) =>
- feature.properties.signal_type.toLowerCase() ===
- signalType.toLowerCase()
- ),
+ signalType
+ ? features?.filter(
+ (feature) =>
+ feature.properties.signal_type.toLowerCase() ===
+ signalType.toLowerCase()
+ )
+ : features,
[features, signalType]
);
@@ -45,18 +60,18 @@ const SignalComponentAutocomplete = React.forwardRef(
return ;
} else if (error) {
return (
- {`Unable to load signal list: ${error}`}
+ {`Unable to load ${componentLabel} list: ${error}`}
);
}
return (
onChange(option)}
loading={loading}
options={featuresFilteredByType}
@@ -66,7 +81,7 @@ const SignalComponentAutocomplete = React.forwardRef(
inputRef={ref}
error={error}
InputLabelProps={{ required: false }}
- label="Signal"
+ label={componentLabel}
variant="outlined"
size="small"
/>
@@ -77,4 +92,4 @@ const SignalComponentAutocomplete = React.forwardRef(
}
);
-export default SignalComponentAutocomplete;
+export default KnackComponentAutocomplete;
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/ProjectComponentsListItem.js b/moped-editor/src/views/projects/projectView/ProjectComponents/ProjectComponentsListItem.js
index c62b9031da..ad08786610 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/ProjectComponentsListItem.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/ProjectComponentsListItem.js
@@ -79,6 +79,7 @@ const ProjectComponentsListItem = ({
const lineRepresentation = component?.moped_components?.line_representation;
const isSignal = isSignalComponent(component);
+ const isSchoolBeacon = component?.feature_school_beacons.length > 0;
const isComponentMapped = getIsComponentMapped(component);
return (
@@ -116,8 +117,8 @@ const ProjectComponentsListItem = ({
@@ -127,7 +128,7 @@ const ProjectComponentsListItem = ({
color="primary"
aria-label="map"
onClick={onEditMap}
- disabled={isSignal}
+ disabled={isSignal || isSchoolBeacon}
>
component.feature_drawn_lines?.length > 0 ||
component.feature_intersections?.length > 0 ||
component.feature_signals?.length > 0 ||
- component.feature_street_segments?.length > 0;
+ component.feature_street_segments?.length > 0 ||
+ component.feature_school_beacons?.length > 0;
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/form.js b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/form.js
index 0df44ff5de..c9ebeac88e 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/form.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/form.js
@@ -1,7 +1,7 @@
import { useMemo, useEffect, useState } from "react";
import { Icon } from "@mui/material";
import makeStyles from "@mui/styles/makeStyles";
-import { featureSignalsRecordToKnackSignalRecord } from "src/utils/signalComponentHelpers";
+import { featureSchoolBeaconRecordToKnackSchoolBeaconRecord, featureSignalsRecordToKnackSignalRecord } from "src/utils/signalComponentHelpers";
import { isSignalComponent } from "./componentList";
import {
RoomOutlined as RoomOutlinedIcon,
@@ -275,6 +275,21 @@ export const makePhaseFormFieldValue = (phase) => {
};
};
+/**
+ * Create the value for the school beacon autocomplete if the component is a school beacon component
+ * @param {Object} component - The component record
+ * @returns {Object} the field value
+ */
+export const makeSchoolBeaconFormFieldValue = (component) => {
+ if (!(component.feature_school_beacons?.length > 0)) return null;
+
+ const componentSchoolBeacon = component?.feature_school_beacons?.[0];
+ const knackFormatSchoolBeaconOption =
+ featureSchoolBeaconRecordToKnackSchoolBeaconRecord(componentSchoolBeacon);
+
+ return knackFormatSchoolBeaconOption;
+};
+
/**
* Create the value for the subphase autocomplete
* @param {Object} subphase - The component subphase record
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/makeComponentData.js b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/makeComponentData.js
index 061d7c68a6..2443eaa090 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/makeComponentData.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/makeComponentData.js
@@ -5,7 +5,10 @@ import {
makePointFeatureInsertionData,
} from "./makeFeatures";
import { isDrawnDraftFeature } from "./features";
-import { knackSignalRecordToFeatureSignalsRecord } from "src/utils/signalComponentHelpers";
+import {
+ knackSignalRecordToFeatureSignalsRecord,
+ knackSchoolBeaconRecordToFeatureSchoolBeaconRecord,
+} from "src/utils/signalComponentHelpers";
/**
* Take a component object and return an object that can be used to insert a component record
@@ -53,6 +56,7 @@ export const makeComponentInsertData = (projectId, component) => {
const signalFeaturesToInsert = [];
const drawnLinesToInsert = [];
const drawnPointsToInsert = [];
+ const schoolBeaconFeaturesToInsert = [];
const drawnFeatures = features.filter((feature) =>
isDrawnDraftFeature(feature)
@@ -80,6 +84,12 @@ export const makeComponentInsertData = (projectId, component) => {
const signalRecord = knackSignalRecordToFeatureSignalsRecord(feature);
signalFeaturesToInsert.push(signalRecord);
});
+ } else if (featureTable === "feature_school_beacons") {
+ features.forEach((feature) => {
+ const signalRecord =
+ knackSchoolBeaconRecordToFeatureSchoolBeaconRecord(feature);
+ schoolBeaconFeaturesToInsert.push(signalRecord);
+ });
}
return {
@@ -106,20 +116,21 @@ export const makeComponentInsertData = (projectId, component) => {
feature_drawn_lines: { data: drawnLinesToInsert },
feature_drawn_points: { data: drawnPointsToInsert },
feature_signals: { data: signalFeaturesToInsert },
+ feature_school_beacons: { data: schoolBeaconFeaturesToInsert },
};
};
/**
- * Assembles feature data based on I/O from the component attribute form.
- * It handles when a signal component changes to a non-signal component,
+ * Assembles feature data based on I/O from the component attribute form.
+ * It handles when a signal component changes to a non-signal component,
* when a selected signal asset is cleared from the from the form input,
* or when the selected signal asset is changed to a different signal
* asset.
- * @param {Object} signalFromForm - signal objected as returned by the signal
- * autoomplete form option (which is essentially a signalr ecord from
+ * @param {Object} signalFromForm - signal object as returned by the signal
+ * autocomplete form option (which is essentially a signal record from
* socrata)
* @param {Object} clickedComponent - the moped_project_component record that is
- * currently being edited, including it's related feature data
+ * currently being edited, including its related feature data
* @returns {Object[]} signalsToCreate - an array of length 1 or 0 which optionally
* contains the signal feature record to be inserted
* @returns {Number[]} featureIdsToDelete - array of 0 or more feature record IDs
@@ -127,17 +138,21 @@ export const makeComponentInsertData = (projectId, component) => {
*/
export const getFeatureChangesFromComponentForm = (
signalFromForm,
+ schoolBeaconFromForm,
clickedComponent
) => {
let signalToCreate = null;
+ let schoolBeaconToCreate = null;
const featureIdsToDelete = [];
const newSignalId = parseInt(signalFromForm?.properties?.signal_id);
const previousSignal = clickedComponent.feature_signals?.[0];
+ const newSchoolBeaconKnackId = schoolBeaconFromForm?.properties.id;
+ const previousSchoolBeacon = clickedComponent.feature_school_beacons?.[0];
const previousIntersectionFeatures = clickedComponent.feature_intersections;
const previousDrawnPointFeatures = clickedComponent.feature_drawn_points;
+ // Was a Signal (PHB / Traffic) selected in the edit attribute form?
if (newSignalId) {
- // signal is selected in form
if (previousSignal && newSignalId !== previousSignal?.signal_id) {
// signal selection changed
signalToCreate = knackSignalRecordToFeatureSignalsRecord(signalFromForm);
@@ -147,6 +162,41 @@ export const getFeatureChangesFromComponentForm = (
// signal was previously blank
signalToCreate = knackSignalRecordToFeatureSignalsRecord(signalFromForm);
signalToCreate.component_id = clickedComponent.project_component_id;
+ // if there was a beacon that was switched to signal, we need to clear that beacon
+ if (previousSchoolBeacon) {
+ featureIdsToDelete.push(previousSchoolBeacon.id);
+ }
+ }
+ if (previousIntersectionFeatures) {
+ // delete all intersection features
+ featureIdsToDelete.push(...previousIntersectionFeatures.map((f) => f.id));
+ }
+ if (previousDrawnPointFeatures) {
+ // delete all drawn point features
+ featureIdsToDelete.push(...previousDrawnPointFeatures.map((f) => f.id));
+ }
+ } else if (newSchoolBeaconKnackId) {
+ if (
+ previousSchoolBeacon &&
+ newSchoolBeaconKnackId !== previousSchoolBeacon.knack_id
+ ) {
+ // changed which Beacon was chosen
+ schoolBeaconToCreate =
+ knackSchoolBeaconRecordToFeatureSchoolBeaconRecord(
+ schoolBeaconFromForm
+ );
+ schoolBeaconToCreate.component_id = clickedComponent.project_component_id;
+ featureIdsToDelete.push(previousSchoolBeacon.id);
+ } else if (!previousSchoolBeacon) {
+ schoolBeaconToCreate =
+ knackSchoolBeaconRecordToFeatureSchoolBeaconRecord(
+ schoolBeaconFromForm
+ );
+ schoolBeaconToCreate.component_id = clickedComponent.project_component_id;
+ // if there was a signal that switched to a beacon, delete the old signal
+ if (previousSignal) {
+ featureIdsToDelete.push(previousSignal.id);
+ }
}
if (previousIntersectionFeatures) {
// delete all intersection features
@@ -159,10 +209,17 @@ export const getFeatureChangesFromComponentForm = (
} else if (previousSignal) {
// signal selection was cleared
featureIdsToDelete.push(previousSignal.id);
+ } else if (previousSchoolBeacon) {
+ // beacon selection was cleared
+ featureIdsToDelete.push(previousSchoolBeacon.id);
}
- // wrap signal in array to match hasura type
+
+ // wrap signal & school beacon in array to match hasura type
// we do this because it's allowed to insert an empty array, but
// but not a null object
const signalsToCreate = signalToCreate ? [signalToCreate] : [];
- return { signalsToCreate, featureIdsToDelete };
+ const schoolBeaconsToCreate = schoolBeaconToCreate
+ ? [schoolBeaconToCreate]
+ : [];
+ return { signalsToCreate, schoolBeaconsToCreate, featureIdsToDelete };
};
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/makeFeatures.js b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/makeFeatures.js
index 04997f6da8..f989560cae 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/makeFeatures.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/makeFeatures.js
@@ -25,6 +25,9 @@ export const featureTableFieldMap = {
feature_drawn_points: {
// no columns to map
},
+ feature_school_beacons: {
+ // Transform of signal records happens in knackSignalRecordToFeatureSignalsRecord
+ },
};
/**
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/useCreateComponent.js b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/useCreateComponent.js
index 7dc271c29b..712eb11f28 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/useCreateComponent.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/useCreateComponent.js
@@ -172,7 +172,7 @@ export const useCreateComponent = ({
};
/**
- * Prepare signal component and feature data for component creation and call mutation/reset state
+ * Prepare signal component / school beacon component and feature data for component creation and call mutation/reset state
*/
const onSaveDraftSignalComponent = (signalComponent) => {
const newComponentData = makeComponentInsertData(
diff --git a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/useUpdateComponent.js b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/useUpdateComponent.js
index 929dfb44b9..7d344da05f 100644
--- a/moped-editor/src/views/projects/projectView/ProjectComponents/utils/useUpdateComponent.js
+++ b/moped-editor/src/views/projects/projectView/ProjectComponents/utils/useUpdateComponent.js
@@ -279,6 +279,7 @@ export const useUpdateComponent = ({
const signals = [];
const drawnLinesToInsert = [];
const drawnPointsToInsert = [];
+ const schoolBeacons = [];
if (featureTable === "feature_street_segments") {
makeLineStringFeatureInsertionData(
@@ -296,6 +297,8 @@ export const useUpdateComponent = ({
makeDrawnPointsInsertionData(newDrawnPoints, drawnPointsToInsert);
} else if (featureTable === "feature_signals") {
makePointFeatureInsertionData(featureTable, newFeaturesToInsert, signals);
+ } else if (featureTable === "feature_school_beacons") {
+ makePointFeatureInsertionData(featureTable, newFeaturesToInsert, schoolBeacons)
}
// Find the features to delete
@@ -385,6 +388,7 @@ export const useUpdateComponent = ({
drawnPointsToInsert,
editedComponentId
);
+ const schoolBeaconInserts = addComponentIdForUpdate(schoolBeacons, editedComponentId)
updateComponentFeatures({
variables: {
@@ -396,6 +400,7 @@ export const useUpdateComponent = ({
drawnPoints: drawnPointInserts,
drawnLinesDragUpdates,
drawnPointsDragUpdates,
+ schoolBeacons: schoolBeaconInserts
},
})
.then(() => {
@@ -413,7 +418,7 @@ export const useUpdateComponent = ({
});
})
.catch((error) => {
- console.log(error);
+ console.error(error);
});
};