Skip to content

Commit

Permalink
H3 filter tool (#833)
Browse files Browse the repository at this point in the history
Adds a new sketch class to support filtering planning units for the FWC Coral Reef Decision Support Tool. Requires a separate API server implementation to work, so is only a visible option for superusers to setup.
  • Loading branch information
underbluewaters authored Nov 13, 2024
1 parent 626f2d5 commit 472cd4f
Show file tree
Hide file tree
Showing 41 changed files with 93,792 additions and 90,842 deletions.
6 changes: 6 additions & 0 deletions packages/api/generated-schema-clean.gql
Original file line number Diff line number Diff line change
Expand Up @@ -13060,6 +13060,7 @@ type Sketch implements Node {
"""
copyOf: Int
createdAt: Datetime!
filterMvtUrl: String

"""
Parent folder. Both regular sketches and collections may be nested within folders for organization purposes.
Expand Down Expand Up @@ -13162,6 +13163,8 @@ type SketchClass implements Node {
sketch classes can only be digitized by admins.
"""
canDigitize: Boolean
filterApiServerLocation: String
filterApiVersion: Int!

"""Reads a single `Form` that is related to this `SketchClass`."""
form: Form
Expand Down Expand Up @@ -13304,6 +13307,8 @@ input SketchClassPatch {
"""
allowMulti: Boolean
filterApiServerLocation: String
filterApiVersion: Int

"""
Geometry type users digitize. COLLECTION types act as a feature collection and have no drawn geometry.
Expand Down Expand Up @@ -13439,6 +13444,7 @@ enum SketchFoldersOrderBy {
enum SketchGeometryType {
CHOOSE_FEATURE
COLLECTION
FILTERED_PLANNING_UNITS
LINESTRING
POINT
POLYGON
Expand Down
6 changes: 6 additions & 0 deletions packages/api/generated-schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -13060,6 +13060,7 @@ type Sketch implements Node {
"""
copyOf: Int
createdAt: Datetime!
filterMvtUrl: String

"""
Parent folder. Both regular sketches and collections may be nested within folders for organization purposes.
Expand Down Expand Up @@ -13162,6 +13163,8 @@ type SketchClass implements Node {
sketch classes can only be digitized by admins.
"""
canDigitize: Boolean
filterApiServerLocation: String
filterApiVersion: Int!

"""Reads a single `Form` that is related to this `SketchClass`."""
form: Form
Expand Down Expand Up @@ -13304,6 +13307,8 @@ input SketchClassPatch {
"""
allowMulti: Boolean
filterApiServerLocation: String
filterApiVersion: Int

"""
Geometry type users digitize. COLLECTION types act as a feature collection and have no drawn geometry.
Expand Down Expand Up @@ -13439,6 +13444,7 @@ enum SketchFoldersOrderBy {
enum SketchGeometryType {
CHOOSE_FEATURE
COLLECTION
FILTERED_PLANNING_UNITS
LINESTRING
POINT
POLYGON
Expand Down
8 changes: 8 additions & 0 deletions packages/api/migrations/committed/000339.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
--! Previous: sha1:1683d85c546802a54a712150f5991ca9f6b7655a
--! Hash: sha1:9bc19cbc712fb81e17c7a84ccee62a3facdf5ea5

-- Enter migration here
alter table sketch_classes add column if not exists filter_api_version int not null default 1;
alter table sketch_classes add column if not exists filter_api_server_location text;

alter type sketch_geometry_type add value if not exists 'FILTERED_PLANNING_UNITS';
275 changes: 275 additions & 0 deletions packages/api/migrations/committed/000340.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
--! Previous: sha1:9bc19cbc712fb81e17c7a84ccee62a3facdf5ea5
--! Hash: sha1:a98e9d208313bdb12a74b6a18f984d1d203092e1

-- Enter migration here
set role seasketch_superuser;
delete from sketch_classes where project_id = (select id from projects where slug = 'superuser') and name = 'Filtered Planning Units';

insert into sketch_classes(
project_id,
name,
geometry_type,
mapbox_gl_style,
is_template,
template_description
) values (
(select id from projects where slug = 'superuser'),
'Filtered Planning Units',
'FILTERED_PLANNING_UNITS'::sketch_geometry_type,
'{}'::jsonb,
true,
'Filter polygons by criteria. Requires an API server.'
) on conflict do nothing;

select initialize_sketch_class_form_from_template((select id from sketch_classes where name = 'Filtered Planning Units' and is_template = true), (select id from forms where is_template = true and template_type = 'SKETCHES' and template_name = 'Basic Template'));
set role postgres;

GRANT update (filter_api_server_location) on sketch_classes to seasketch_user;
GRANT update (filter_api_version) on sketch_classes to seasketch_user;

delete from form_element_types where component_name = 'FilterInput';
insert into form_element_types (
component_name,
label,
is_input,
is_surveys_only
) values (
'FilterInput',
'Filter Input',
true,
false
);

CREATE OR REPLACE FUNCTION public.before_sketch_insert_or_update() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
AS $$
declare
class_geometry_type sketch_geometry_type;
allow_multi boolean;
incoming_geometry_type text;
new_geometry_type text;
parent_collection_id int;
begin
select
geometry_type,
sketch_classes.allow_multi
into
class_geometry_type,
allow_multi,
incoming_geometry_type
from
sketch_classes
where
id = NEW.sketch_class_id;
if NEW.folder_id is not null and NEW.collection_id is not null then
raise exception 'Parent cannot be to both folder and collection';
end if;
if class_geometry_type = 'COLLECTION' then
-- Also check for parent collection of parent folder (recursively)
if NEW.collection_id is not null then
if (select get_parent_collection_id('sketch', NEW.collection_id)) is not null then
raise exception 'Nested collections are not allowed';
end if;
elsif NEW.folder_id is not null then
if (select get_parent_collection_id('sketch_folder', NEW.folder_id)) is not null then
raise exception 'Nested collections are not allowed';
end if;
end if;
-- geom must be present unless a collection
if NEW.geom is not null or NEW.user_geom is not null then
raise exception 'Collections should not have geometry';
else
-- no nested collections
if NEW.collection_id is not null then
raise exception 'Nested collections are not allowed';
else
return NEW;
end if;
end if;
elsif class_geometry_type = 'FILTERED_PLANNING_UNITS' then
-- Also check for parent collection of parent folder (recursively)
if NEW.collection_id is not null then
raise exception 'Filtered planning units cannot be part of a collection';
elsif NEW.folder_id is not null then
if (select get_parent_collection_id('sketch_folder', NEW.folder_id)) is not null then
raise exception 'Filtered planning units cannot be part of a collection';
end if;
end if;
-- geom must be present unless a collection
if NEW.geom is not null or NEW.user_geom is not null then
raise exception 'Filtered planning units should not have geometry';
else
-- no nested collections
if NEW.collection_id is not null then
raise exception 'Filtered planning units cannot be part of a collection';
else
return NEW;
end if;
end if;
else
select geometrytype(NEW.user_geom) into new_geometry_type;
-- geometry type must match sketch_class.geometry_type and sketch_class.allow_multi
if (new_geometry_type = class_geometry_type::text) or (allow_multi and new_geometry_type like '%' || class_geometry_type::text) then
-- if specifying a collection_id, must be in it's valid_children
if NEW.collection_id is null or not exists(select 1 from sketch_classes_valid_children where parent_id in (select sketch_class_id from sketches where id = NEW.collection_id)) or exists(select 1 from sketch_classes_valid_children where parent_id in (select sketch_class_id from sketches where id = NEW.collection_id) and child_id = NEW.sketch_class_id) then
return NEW;
else
raise exception 'Sketch is not a valid child of collection';
end if;
else
raise exception 'Geometry type does not match sketch class';
end if;
end if;
end;
$$;


CREATE OR REPLACE FUNCTION url_encode(input text)
RETURNS text AS $$
DECLARE
cleaned_input text;
output text := '';
ch text;
ch_code int;
BEGIN
-- Remove all extraneous whitespace from the input JSON text
cleaned_input := regexp_replace(input, '\s+', '', 'g');

-- Perform URL encoding on the cleaned input
FOR i IN 1..length(cleaned_input) LOOP
ch := substr(cleaned_input, i, 1);
ch_code := ascii(ch);
-- Allow only URL-safe characters (alphanumeric and unreserved characters)
IF ch ~ '[a-zA-Z0-9_.~-]' THEN
output := output || ch;
ELSE
-- Use lpad and upper to ensure two-character hexadecimal representation
output := output || '%' || lpad(upper(to_hex(ch_code)), 2, '0');
END IF;
END LOOP;

RETURN output;
END;
$$ LANGUAGE plpgsql;

grant execute on function url_encode to anon;
comment on function url_encode is '@omit';

drop function if exists filter_state_to_search_string(jsonb, int);
drop function if exists filter_state_to_search_string(jsonb);

CREATE OR REPLACE FUNCTION filter_state_to_search_string(filters jsonb, sketch_class_id int)
RETURNS text AS $$
DECLARE
state jsonb := '{}';
filter jsonb;
attr text;
result text;
attribute_name text;
filter_server_location text;
filter_version int;
final_url text;
BEGIN
-- Retrieve the filter_api_server_location and filter_api_version from sketch_classes table
SELECT sketch_classes.filter_api_server_location, sketch_classes.filter_api_version
INTO filter_server_location, filter_version
FROM sketch_classes
WHERE id = sketch_class_id;

-- If filter_api_server_location is NULL, return NULL
IF filter_server_location IS NULL THEN
RETURN NULL;
END IF;

-- Loop through each attribute in the input JSONB object
FOR attr, filter IN
SELECT key, value FROM jsonb_each(filters)
LOOP
-- Only process if "selected" is true
IF filter->>'selected' = 'true' THEN
if filter->>'attribute' is not null then
attribute_name := filter->>'attribute';
else
-- Look up the attribute name from form_elements for the current attr ID
SELECT component_settings->>'attribute'
INTO attribute_name
FROM form_elements
WHERE id = attr::int; -- Cast attr to integer to match form_elements ID
end if;


-- Default to the original key if no match is found
IF attribute_name IS NULL THEN
attribute_name := attr;
END IF;

-- Check for numberState
IF filter ? 'numberState' THEN
state := state || jsonb_build_object(attribute_name, jsonb_build_object(
'min', filter->'numberState'->'min',
'max', filter->'numberState'->'max'
));

-- Check for stringState
ELSIF filter ? 'stringState' THEN
state := state || jsonb_build_object(attribute_name, jsonb_build_object(
'choices', filter->'stringState'
));

-- Check for booleanState
ELSIF filter ? 'booleanState' THEN
state := state || jsonb_build_object(attribute_name, jsonb_build_object(
'bool', COALESCE(filter->'booleanState', 'false')::boolean
));
END IF;
END IF;
END LOOP;

-- If no keys were added, return an empty string; otherwise, encode the JSON
IF state = '{}'::jsonb THEN
RETURN filter_server_location || '/v' || filter_version || '/mvt/{z}/{x}/{y}.pbf';
ELSE
-- Convert the JSONB object to a text string without formatting
result := state::text;

-- URL encode the resulting JSON text using the updated url_encode function
result := url_encode(result);

-- Construct the final URL
final_url := filter_server_location || '/v' || filter_version || '/mvt/{z}/{x}/{y}.pbf?filter=' || result;
RETURN final_url;
END IF;
END;
$$ LANGUAGE plpgsql;

grant execute on function filter_state_to_search_string to anon;
comment on function filter_state_to_search_string is '@omit';

alter table sketches drop column if exists filter_mvt_url;
create or replace function sketches_filter_mvt_url(s sketches)
returns text
language sql
security definer
stable
as $$
select filter_state_to_search_string(s.properties, s.sketch_class_id);
$$;


grant execute on function sketches_filter_mvt_url to anon;
-- alter table sketches add column if not exists filter_mvt_url text generated always as (filter_state_to_search_string(properties, sketch_class_id)) stored;

-- grant select(filter_mvt_url) on sketches to anon;

delete from form_element_types where component_name = 'CollapsibleGroup';
insert into form_element_types (
component_name,
label,
is_input,
is_surveys_only
) values (
'CollapsibleGroup',
'Collapsible Group',
false,
false
);
2 changes: 1 addition & 1 deletion packages/api/migrations/current.sql
Original file line number Diff line number Diff line change
@@ -1 +1 @@
-- Enter migration here
-- Enter migration here
Loading

0 comments on commit 472cd4f

Please sign in to comment.