-- SPATIAL DATABASE FOR GPS WILDLIFE TRACKING DATA, F. Urbano and F. Cagnacci (eds.)
-- DOI: 10.1007/978-3-319-03743-1_4, Springer International Publishing Switzerland 2014

-- Code presented in Chapter 09
-- Authors: Ferdinando Urbano, Mathieu Basille, Pierre Racine
-- Version 1.0

-- The code in this book is free. You can copy, modify and distribute non-trivial part of the code 
-- with no restrictions, according to the terms of the Creative Commons CC0 1.0 licence
-- (https://creativecommons.org/publicdomain/zero/1.0/). 
-- Nevertheless, the acknowledgement of the authorship is appreciated.

-- Note: to run this code you need the database developed in the previous chapters (2,3,4,5,6,7,8).

-- The test data set is available in the Extra Material page of the book (http://extras.springer.com/)


-- You extract some statistics
SELECT 
  animals_id, 
  min(altitude_srtm)::integer AS min_alt, 
  max(altitude_srtm)::integer AS max_alt,
  avg(altitude_srtm)::integer AS avg_alt,
  stddev(altitude_srtm)::integer AS alt_stddev, 
  count(*) AS num_loc
FROM main.gps_data_animals 
WHERE gps_validity_code = 1 
GROUP BY animals_id 
ORDER BY avg(altitude_srtm);

SELECT 
  animals_id, 
  (count(*)/tot::double precision)::numeric(5,4) AS percentage, 
  label1
FROM 
  main.gps_data_animals, 
  env_data.corine_land_cover_legend, 
  (SELECT 
    animals_id AS x, 
    count(*) AS tot 
  FROM main.gps_data_animals 
  WHERE gps_validity_code = 1 
  GROUP BY animals_id) a 
WHERE 
  gps_validity_code = 1 AND 
  animals_id = x AND 
  corine_land_cover_code = grid_code 
GROUP BY animals_id, label1, tot 
ORDER BY animals_id, label1;

-- You create a locations_set data type
CREATE TYPE tools.locations_set AS (
  animals_id integer,
  acquisition_time timestamp with time zone,
  geom geometry(point, 4326));
  
-- You create a view with location data
CREATE OR REPLACE VIEW main.view_locations_set AS 
  SELECT 
    gps_data_animals.animals_id, 
    gps_data_animals.acquisition_time, 
    CASE
      WHEN gps_data_animals.gps_validity_code = 1 THEN 
        gps_data_animals.geom
      ELSE NULL::geometry
    END AS geom
  FROM main.gps_data_animals
  WHERE gps_data_animals.gps_validity_code != 21
  ORDER BY gps_data_animals.animals_id, gps_data_animals.acquisition_time;
COMMENT ON VIEW main.view_locations_set
IS 'View that stores the core information of the set of GPS positions (id of the animal, the acquisition time and the geometry), where non valid records are represented with empty geometry.';


-- You create a table to store trajectories
CREATE TABLE analysis.trajectories (
  trajectories_id serial NOT NULL,
  animals_id integer NOT NULL,
  start_time timestamp with time zone NOT NULL,
  end_time timestamp with time zone NOT NULL,
  description character varying,
  ref_user character varying,
  num_locations integer,
  length_2d integer,
  insert_timestamp timestamp with time zone DEFAULT now(),
  original_data_set character varying,
  geom geometry(linestring, 4326),
  CONSTRAINT trajectories_pk 
    PRIMARY KEY (trajectories_id ),
  CONSTRAINT trajectories_animals_fk 
    FOREIGN KEY (animals_id)
    REFERENCES main.animals (animals_id) MATCH SIMPLE
    ON UPDATE NO ACTION ON DELETE NO ACTION
);
COMMENT ON TABLE analysis.trajectories
IS 'Table that stores the trajectories derived from a set of selected locations. Each trajectory is related to a single animal. This table is populated by the function tools.make_traj. Each element is described by a number of attributes: the starting date and the ending date of the location set, a general description (that can be used to "tag" each record with specific identifiers), the user who did the analysis, the number of locations (or vertex of the lines) that produced the analysis, the length of the line, and the SQL that generated the dataset.';

-- You add a function to create trajectories
CREATE OR REPLACE FUNCTION tools.make_traj (
  locations_set_query character varying DEFAULT 'main.view_locations_set'::character varying, 
  description character varying DEFAULT 'Standard trajectory'::character varying)
RETURNS integer AS
$BODY$
DECLARE
  locations_set_query_string character varying;
BEGIN
  locations_set_query_string = (SELECT replace(locations_set_query, '''',''''''));
  EXECUTE
    'INSERT INTO analysis.trajectories (animals_id, start_time, end_time, description, ref_user, num_locations, length_2d, original_data_set, geom) 
      SELECT sel_subquery.animals_id, min(acquisition_time), max(acquisition_time), ''' ||description|| ''', current_user, count(*), ST_length2d_spheroid(ST_MakeLine(sel_subquery.geom), ''SPHEROID("WGS84",6378137,298.257223563)''::spheroid), '''|| locations_set_query_string ||''', ST_MakeLine(sel_subquery.geom) AS geom
      FROM 
        (SELECT * 
        FROM ('||locations_set_query||') a 
        WHERE a.geom IS NOT NULL
        ORDER BY a.animals_id, a.acquisition_time) sel_subquery
        GROUP BY sel_subquery.animals_id;';
  raise notice 'Operation correctly performed. Record inserted into analysis.trajectories';
  RETURN 1; 
END;
$BODY$
LANGUAGE plpgsql;

COMMENT ON FUNCTION tools.make_traj( character varying, character varying) IS 'This function produces a trajectory from a locations_set object (animals_id, acquisition_time, geom) into the table analysis.trajectories. Two parameters are accepted: the first is the SQL code that generates the locations_set object, the second is a string that is used to comment the trajectory. A trajectory will be created for each animal in the data set. If you need to include a single quote ('') in the SQL that select the locations (for example, when you want to define a timestamp), you have to use two single quotes to escape the special character: ''''. An example: SELECT tools.make_traj(''SELECT * FROM main.view_locations_set WHERE acquisition_time > ''''2006-01-01''''::timestamp and animals_id in (3,4)'').';

-- Examples of use
SELECT 
  tools.make_traj(
    'SELECT * FROM main.view_locations_set WHERE acquisition_time > ''2006-01-01''::timestamp AND animals_id = 3', 
    'First test');
SELECT 
  tools.make_traj(
    'SELECT animals_id, acquisition_time, geom FROM main.gps_data_animals WHERE gps_validity_code = 1 AND acquisition_time < ''2006-01-01''::timestamp', 
  'Second test');
SELECT * FROM analysis.trajectories;

-- 2D trajectory
SELECT 
  sel_subquery.animals_id, 
  ST_length(
    ST_MakeLine(sel_subquery.geom)::geography)::integer AS lenght_line2d,
  ST_numpoints(
    ST_MakeLine(sel_subquery.geom)) AS num_locations
FROM
  (SELECT 
    gps_data_animals.animals_id, 
    gps_data_animals.geom, 
    gps_data_animals.acquisition_time
  FROM main.gps_data_animals
  WHERE gps_validity_code = 1
  ORDER BY gps_data_animals.animals_id, gps_data_animals.acquisition_time)   
  sel_subquery
GROUP BY 
  sel_subquery.animals_id;

-- 3D trajectory
SELECT 
  animals_id, 
  ST_3DLength_Spheroid(
    ST_SetSrid(ST_MakeLine(geom), 4326),
   'SPHEROID("WGS84",6378137,298.257223563)'::spheroid)::integer AS lenght_line3d, 
  ST_NumPoints(ST_SetSrid(ST_MakeLine(geom), 4326)) AS num_locations
FROM 
  (SELECT 
    gps_data_animals.animals_id, 
    gps_data_animals.acquisition_time, 
    ST_SetSRID(ST_makepoint(
      ST_X(gps_data_animals.geom), 
      ST_Y(gps_data_animals.geom), 
      gps_data_animals.altitude_srtm::double precision,
      date_part('epoch'::text, gps_data_animals.acquisition_time)), 
      4326) AS geom
  FROM main.gps_data_animals
  WHERE gps_validity_code = 1
  ORDER BY gps_data_animals.animals_id, gps_data_animals.acquisition_time) a 
GROUP BY animals_id;

-- You create a function to regularize trajectories
CREATE OR REPLACE FUNCTION tools.regularize(
  animal integer, 
  time_interval integer DEFAULT 10800, 
  buffer double precision DEFAULT 600, 
  starting_time timestamp with time zone DEFAULT NULL::timestamp with time zone, 
  ending_time timestamp with time zone DEFAULT NULL::timestamp with time zone)
RETURNS SETOF tools.locations_set AS
$BODY$
DECLARE
  location_set tools.locations_set%rowtype;
  cursor_var record;
  interval_length integer;
  check_animal boolean;
BEGIN
-- Error trapping: if the buffer is > 0.5 * time interval, I could take 2 times the same locations, therefore an exception is raised
IF buffer > 0.5 * time_interval THEN
  RAISE EXCEPTION 'With a buffer (%) > 0.5 * time interval (%), you could get twice the same location, please reduce buffer or increase time interval.', buffer, time_interval;
END IF;

-- If the starting date is not set, the minimum, valid timestamp of the data set is taken
IF starting_time IS NULL THEN
  SELECT 
    min(acquisition_time) 
  FROM 
    main.view_locations_set
  WHERE 
    view_locations_set.animals_id = animal
  INTO starting_time;
END IF;

-- If the ending date is not set, the maximum, valid timestamp of the data set is taken
IF ending_time IS NULL THEN
  SELECT max(acquisition_time) 
  FROM main.view_locations_set
  WHERE view_locations_set.animals_id = animal
  INTO ending_time;
END IF;

-- I define the interval time (number of seconds between the starting and ending time)
SELECT extract(epoch FROM (ending_time - starting_time))::integer + buffer
INTO interval_length;

-- I create a "virtual" set of records with regular time interval (from starting_time to ending_time, with a step equal to the interval length, then I go through all the elements of the virtual set and I check if a real record exist in main.view_locations_set which has an acquisition_time closer then the defined buffer. If more then 1 record exists in the buffer range, then I take the "closest".
FOR location_set IN 
  SELECT 
    animal, 
    (starting_time + generate_series (0, interval_length, time_interval) * interval '1 second'), 
    NULL::geometry
LOOP
  SELECT geom, acquisition_time
  FROM main.view_locations_set 
  WHERE 
    animals_id = animal AND 
    (acquisition_time < (location_set.acquisition_time + interval '1 second' * buffer) AND 
    acquisition_time > (location_set.acquisition_time - interval '1 second' * buffer)) 
  ORDER BY 
    abs(extract (epoch FROM (acquisition_time - location_set.acquisition_time))) 
  LIMIT 1 
  INTO cursor_var;

-- If I have a record in main.view_locations_set, i get the values from there, otherwise I keep my "virtual" record
  IF cursor_var.acquisition_time IS NOT NULL THEN
    location_set.acquisition_time = cursor_var.acquisition_time;
    location_set.geom = cursor_var.geom;
  END IF;
  RETURN NEXT location_set;
END LOOP;
RETURN;
END;
$BODY$
LANGUAGE plpgsql;

COMMENT ON FUNCTION tools.regularize(integer, integer, double precision, timestamp with time zone, timestamp with time zone) 
IS 'This function creates a complete, regular time series of locations from main.view_locations_set using an individual id, a time interval (in seconds), a buffer time (in seconds, which corresponds to the  accepted delay of GPS recording), a starting time (if no values is defined, the first record of the animal data set is taken), an ending time (if no values is defined, the last record of the animal data set is taken). The function checks at every time step if exists a "real" record (with or with coordinates) in the main.view_locations_set table (which is the "locations_set" object of the "main.gps_data_animals" table): if any real data exist (inside a defined time interval buffer from the reference timestamp generated by the function) in main.view_locations_set, the real record is used, otherwise a "virtual" record is created (with empty geometry). The output is a table with the structure "location_set" (animals_id integer, acquisition_time timestamp with time zone, geom geometry).';

-- You test the function
SELECT animals_id, acquisition_time, ST_AsText(geom) FROM tools.regularize(6, 60*60*8) LIMIT 15;
SELECT animals_id, acquisition_time, ST_AsText(geom) FROM tools.regularize(6, 60*60*4) LIMIT 15;
SELECT animals_id, acquisition_time, ST_AsText(geom) FROM tools.regularize(6, 60*60*1) LIMIT 15;

-- You create a sequence
CREATE SEQUENCE tools.unique_id_seq;
COMMENT ON SEQUENCE tools.unique_id_seq
IS 'Sequence used to generate unique number for routines that need it (e.g. functions that need to generate temporary tables with unique names).';

-- You create a function to interpolate locations
CREATE OR REPLACE FUNCTION tools.interpolate(
  animal integer, 
  locations_set_name character varying DEFAULT 'main.view_locations_set'::character varying, 
  limit_gap integer DEFAULT 172800)
RETURNS SETOF tools.locations_set AS
$BODY$
DECLARE
  location_set tools.locations_set%rowtype;
  starting_point record;
  ending_point record;
  time_distance_tot integer;
  perc_start double precision;
  x_point double precision;
  y_point double precision;
  var_name character varying;
BEGIN
IF NOT locations_set_name = 'main.view_locations_set' THEN

-- I need a unique name for my temporary table
  SELECT nextval('tools.unique_id_seq') 
  INTO var_name;
  EXECUTE 
    'CREATE TEMPORARY TABLE 
      temp_table_regularize_'|| var_name ||' AS SELECT animals_id,
      acquisition_time, 
      geom 
    FROM 
      ' || locations_set_name || ' 
    WHERE 
      animals_id = '|| animal;
  locations_set_name = 'temp_table_regularize_'|| var_name;
END IF;

-- I loop though all the elements of my data set
FOR location_set IN EXECUTE 
  'SELECT * FROM ' || locations_set_name || ' WHERE animals_id = ' || animal
LOOP

-- If the record has a NULL geometry values, I look for the previous and next valid locations and interpolate the coordinates between them
  IF location_set.geom IS NULL THEN

-- I get the geometry and timestamp of the next valid location
    EXECUTE 
      'SELECT 
        ST_X(geom) AS x_end, 
        ST_Y(geom) AS y_end, 
        extract(epoch FROM acquisition_time) AS ending_time, 
        extract(epoch FROM $$' ||location_set.acquisition_time || '$$::timestamp with time zone) AS ref_time 
      FROM 
        ' || locations_set_name || ' 
      WHERE 
        animals_id = ' || animal || ' AND 
        geom IS NOT NULL AND 
        acquisition_time > timestamp with time zone $$' ||location_set.acquisition_time || '$$ 
      ORDER BY acquisition_time 
      LIMIT 1'
    INTO ending_point;

-- I get the geometry and timestamp of the previous valid location
    EXECUTE
      'SELECT 
        ST_X(geom) AS x_start, 
        ST_Y(geom) AS y_start, 
        extract(epoch FROM acquisition_time) AS starting_time, 
        extract(epoch FROM $$' ||location_set.acquisition_time || '$$::timestamp with time zone) AS ref_time 
      FROM 
        ' || locations_set_name || ' 
      WHERE 
        animals_id = ' || animal || ' AND 
        geom IS NOT NULL AND 
        acquisition_time < timestamp with time zone $$' ||location_set.acquisition_time || '$$ 
      ORDER BY acquisition_time DESC 
      LIMIT 1'
    INTO starting_point;

-- If both previous and next locations exist, I calculate the interpolated point, weighting the two points according to the temporal distance to the location with NULL geometry. The interpolated geometry is calculated considering lat long as a Cartesian reference. if needed, this approach can be improved casting geometry as geography and intersecting the line between previous and next locations with the buffer (from the previous location) at the given distance.
    IF (starting_point.x_start IS NOT NULL AND ending_point.x_end IS NOT NULL) THEN
      time_distance_tot = (ending_point.ending_time - starting_point.starting_time);
      IF time_distance_tot <= limit_gap THEN
        perc_start = (starting_point.ref_time - starting_point.starting_time)/time_distance_tot;
        x_point = starting_point.x_start + (ending_point.x_end - starting_point.x_start) * perc_start;
        y_point = starting_point.y_start + (ending_point.y_end - starting_point.y_start) * perc_start;
        SELECT ST_SetSRID(ST_MakePoint(x_point, y_point),4326) 
        INTO location_set.geom;
      END IF;
    END IF;
  END IF;
RETURN NEXT location_set;
END LOOP;

-- If I created the temporary table, I delete it here
IF NOT locations_set_name = 'main.view_locations_set' THEN
  EXECUTE 'drop table ' || locations_set_name;
END IF;
return;
END;
$BODY$
LANGUAGE plpgsql;

COMMENT ON FUNCTION tools.interpolate(integer, character varying, integer) IS 'This function accepts as input an animals_id and a locations_set (by default, the main.view_locations_set). It checks for all locations with NULL geometry. If these locations have a previous and next valid locations (according to the gps_validity_code) with a gap smaller than the defined threshold (default is 2 days), a new geometry is calculated interpolating their geometry.';

-- You test it
SELECT animals_id, acquisition_time, ST_AsText(geom)
FROM main.view_locations_set 
WHERE animals_id = 1 and acquisition_time > '2006-03-01 04:00:00'
LIMIT 15;

SELECT animals_id, acquisition_time, ST_AsText(geom) 
FROM 
  tools.interpolate(1, '(
    SELECT * 
    FROM main.view_locations_set 
    WHERE acquisition_time > ''2006-03-01 04:00:00'')as a')
LIMIT 15;

SELECT animals_id, acquisition_time, ST_AsText(geom)
FROM 
  tools.interpolate(4, '(
    SELECT * 
    FROM tools.regularize(4, 60*60*4)) a')
LIMIT 15;

-- You create a new data type
CREATE TYPE tools.bursts_report AS (
  animals_id integer,
  starting_time timestamp with time zone,
  ending_time timestamp with time zone,
  num_locations integer,
  num_locations_null integer,
  interval_step integer);
  
-- You create a function to detect bursts
CREATE OR REPLACE FUNCTION tools.detect_bursts(
  animal integer, 
  buffer integer DEFAULT 600)
RETURNS SETOF tools.bursts_report AS
$BODY$
DECLARE
  location_set tools.locations_set%rowtype;
  cursor_var tools.bursts_report%rowtype;
  starting_time timestamp with time zone;
  ending_time timestamp with time zone;
  location_time timestamp with time zone;
  time_prev timestamp with time zone;
  start_burst timestamp with time zone;
  end_burst timestamp with time zone;
  delta_time integer;
  ref_delta_time integer;
  ref_delta_time_round integer;
  n integer;
  n_null integer;
BEGIN

SELECT min(acquisition_time) 
FROM main.view_locations_set
WHERE view_locations_set.animals_id = animal
INTO starting_time;

SELECT max(acquisition_time) 
FROM main.view_locations_set
WHERE view_locations_set.animals_id = animal
INTO ending_time;

time_prev = NULL;
ref_delta_time = NULL;
n = 1;
n_null = 0;

FOR location_set IN EXECUTE 
  'SELECT animals_id, acquisition_time, geom 
  FROM main.view_locations_set 
  WHERE animals_id = '''|| animal ||''' ORDER BY acquisition_time'
LOOP
  location_time = location_set.acquisition_time;
  IF time_prev IS NULL THEN
    time_prev = location_time;
    start_burst = location_time;
  ELSE
    delta_time = (extract( epoch FROM (location_time - time_prev)))::integer;
    IF ref_delta_time IS NULL THEN
      ref_delta_time = delta_time;
      time_prev = location_time;
      end_burst = location_time;
    ELSIF abs(delta_time - ref_delta_time) < (buffer) THEN
      end_burst = location_time;
      time_prev = location_time;
      n = n + 1;
      IF location_set.geom IS NULL then
        n_null = n_null + 1;
      END IF;
    ELSE
      ref_delta_time_round = (ref_delta_time/buffer::double precision)::integer * buffer;
      IF ref_delta_time_round = 0 THEN
        ref_delta_time_round = (((extract( epoch FROM (end_burst - start_burst)))::integer/n)/60.0)::integer * 60;
      END IF;
    RETURN QUERY SELECT animal, start_burst, end_burst, n, n_null, ref_delta_time_round;
    ref_delta_time = delta_time;
    time_prev = location_time;
    start_burst = end_burst;
    end_burst = location_time;
    n = 1;
    n_null = 0;
    END IF;
  END IF;
END LOOP;

ref_delta_time_round = (ref_delta_time/buffer::double precision)::integer * buffer;
IF ref_delta_time_round = 0 THEN
  ref_delta_time_round = ((extract( epoch FROM end_burst - start_burst))::integer/n)::integer;
END IF;
RETURN QUERY SELECT animal, start_burst, end_burst, n , n_null, ref_delta_time_round;
RETURN;
END;
$BODY$
LANGUAGE plpgsql;

COMMENT ON FUNCTION tools.detect_bursts(integer, integer) 
IS 'This function gives the "bursts" for a defined animal. Bursts are groups of consecutive locations with the same frequency (or time interval). It gets an animal id and a buffer (in seconds) as input parameters and returns a table with the (supposed) schedule of location frequencies. The output table has the fields: "animals_id", "starting_time", "ending_time", "num_locations", "num_locations_null", and "interval_step" (in seconds, approximated according to multiples of the buffer value). A relocation is considered to have a different interval step if the time gap is bigger or smaller than the defined buffer (the buffer takes into account the fact that small changes can occur because of the delay in receiving the GPS signal). The default value for the buffer is 600 (10 minutes). The function is directly computed on "main.view_locations_set" (locations_set structure) and on the whole data set of the selected animal.';

-- You test the function
SELECT 
  animals_id AS id, 
  starting_time, 
  ending_time, 
  num_locations AS num, 
  num_locations_null AS nulls, 
  (interval_step/60.0/60)::numeric(5,2) as hours 
FROM 
  tools.detect_bursts(5);

SELECT 
  animals_id AS id, 
  starting_time, 
  ending_time, 
  num_locations AS num, 
  num_locations_null AS nulls, 
  (interval_step/60.0/60)::numeric(5,2) as hours 
FROM 
  tools.detect_bursts(6);  

-- Create a table to store MCP
CREATE TABLE analysis.home_ranges_mcp (
  home_ranges_mcp_id serial NOT NULL,
  animals_id integer NOT NULL,
  start_time timestamp with time zone NOT NULL,
  end_time timestamp with time zone NOT NULL,
  description character varying,
  ref_user character varying,
  num_locations integer,
  area numeric(13,5),
  geom geometry (multipolygon, 4326),
  percentage double precision,
  insert_timestamp timestamp with time zone DEFAULT timezone('UTC'::text, ('now'::text)::timestamp(0) with time zone),
  original_data_set character varying,
  CONSTRAINT home_ranges_mcp_pk 
    PRIMARY KEY (home_ranges_mcp_id ),
  CONSTRAINT home_ranges_mcp_animals_fk 
    FOREIGN KEY (animals_id)
    REFERENCES main.animals (animals_id) 
    MATCH SIMPLE
    ON UPDATE NO ACTION ON DELETE NO ACTION);

COMMENT ON TABLE analysis.home_ranges_mcp
IS 'Table that stores the home range polygons derived from MCP. The area is computed in hectars.';

CREATE INDEX fki_home_ranges_mcp_animals_fk
  ON analysis.home_ranges_mcp
  USING btree (animals_id );
CREATE INDEX gist_home_mcp_ranges_index
  ON analysis.home_ranges_mcp
  USING gist (geom );
  
-- You create a function for the MCP
CREATE OR REPLACE FUNCTION tools.mcp_perc(
  animal integer, 
  perc double precision DEFAULT 1, 
  description character varying DEFAULT 'Standard analysis'::character varying, 
  locations_set_name character varying DEFAULT 'main.view_locations_set'::character varying, 
  starting_time timestamp with time zone DEFAULT NULL::timestamp with time zone, 
  ending_time timestamp with time zone DEFAULT NULL::timestamp with time zone)
RETURNS integer AS
$BODY$
DECLARE
  hr record;
  var_name character varying;
  locations_set_name_input character varying;
BEGIN
locations_set_name_input = locations_set_name;

IF NOT locations_set_name = 'main.view_locations_set' THEN
  SELECT nextval('tools.unique_id_seq') INTO var_name;
  EXECUTE 
    'CREATE TEMPORARY TABLE temp_table_mcp_perc_'|| var_name ||' AS 
      SELECT * 
      FROM ' || locations_set_name || ' 
      WHERE animals_id = '|| animal;
  locations_set_name = 'temp_table_mcp_perc_'|| var_name;
END IF;

IF perc <= 0 OR perc > 1 THEN
  RAISE EXCEPTION 'INVALID PARAMETER: the percentage of the selected (closest to the data set centroid) points must be a value > 0 and <= 1';
END IF;

IF starting_time IS NULL THEN
  EXECUTE 
    'SELECT min(acquisition_time) 
    FROM '|| locations_set_name ||'
    WHERE '|| locations_set_name ||'.animals_id = ' || animal || ' AND '|| locations_set_name ||'.geom IS NOT NULL '
  INTO starting_time;
END IF;

IF ending_time IS NULL THEN
  EXECUTE 
    'SELECT max(acquisition_time) 
    FROM '|| locations_set_name ||'
    WHERE '|| locations_set_name ||'.animals_id = ' || animal || ' AND '|| locations_set_name ||'.geom IS NOT NULL '
  INTO ending_time;
END IF;

EXECUTE
  'SELECT 
    animals_id, 
    min(acquisition_time) AS start_time, 
    max(acquisition_time) AS end_time, 
    count(animals_id) AS num_locations, 
    ST_Area(geography(ST_ConvexHull(ST_Collect(a.geom)))) AS area, 
    (ST_ConvexHull(ST_Collect(a.geom))).ST_Multi AS geom
  FROM 
    (SELECT '|| locations_set_name ||'.animals_id, '|| locations_set_name ||'.geom, acquisition_time, ST_Distance('|| locations_set_name ||'.geom, 
      (SELECT ST_Centroid(ST_collect('|| locations_set_name ||'.geom))
      FROM '|| locations_set_name ||'
      WHERE '|| locations_set_name ||'.animals_id = ' || animal || ' AND '|| locations_set_name ||'.geom IS NOT NULL AND '|| locations_set_name ||'.acquisition_time >= $$' || starting_time ||'$$::timestamp with time zone AND '|| locations_set_name ||'.acquisition_time <= $$' || ending_time || '$$::timestamp with time zone 
      GROUP BY '|| locations_set_name ||'.animals_id)) AS dist
    FROM '|| locations_set_name ||'
    WHERE '|| locations_set_name ||'.animals_id = ' || animal || ' AND '|| locations_set_name ||'.geom IS NOT NULL AND '|| locations_set_name ||'.acquisition_time >= $$' || starting_time ||'$$::timestamp with time zone and '|| locations_set_name ||'.acquisition_time <= $$' || ending_time || '$$::timestamp with time zone 
    ORDER BY 
      ST_Distance('|| locations_set_name ||'.geom, 
     (SELECT ST_Centroid(ST_Collect('|| locations_set_name ||'.geom))
     FROM '|| locations_set_name ||'
     WHERE '|| locations_set_name ||'.animals_id = ' || animal || ' AND '|| locations_set_name ||'.geom IS NOT NULL AND '|| locations_set_name ||'.acquisition_time >= $$' || starting_time ||'$$::timestamp with time zone and '|| locations_set_name ||'.acquisition_time <= $$' || ending_time || '$$::timestamp with time zone 
     GROUP BY '|| locations_set_name ||'.animals_id))
     LIMIT (((SELECT count('|| locations_set_name ||'.animals_id) AS count
  FROM '|| locations_set_name ||'
  WHERE '|| locations_set_name ||'.animals_id = ' || animal || ' AND '|| locations_set_name ||'.geom IS NOT NULL AND '|| locations_set_name ||'.acquisition_time >= $$' || starting_time ||'$$::timestamp with time zone AND '|| locations_set_name ||'.acquisition_time <= $$' || ending_time || '$$::timestamp with time zone 
))::numeric * ' || perc || ')::integer) a
  GROUP BY a.animals_id;'
  INTO hr;
  IF hr.num_locations < 3 or hr.num_locations IS NULL THEN
    RAISE NOTICE 'INVALID SELECTION: less then 3 points or no points at all match the given criteria. The animal % will be skipped.', animal;
RETURN 0;
END IF;
INSERT INTO analysis.home_ranges_mcp (animals_id, start_time, end_time, 
percentage, description, ref_user, num_locations, 
area, geom, original_data_set)
values (animal, starting_time, ending_time , perc , description, current_user, hr.num_locations, hr.area/1000000.00000, hr.geom, locations_set_name_input);
IF NOT locations_set_name = 'main.view_locations_set' THEN
EXECUTE 'drop table ' || locations_set_name;
END IF;
RAISE NOTICE 'Operation correctly performed. Record inserted into analysis.home_ranges % ', animal;
RETURN 1; 
END;
$BODY$
LANGUAGE plpgsql;

COMMENT ON FUNCTION tools.mcp_perc(integer, double precision, character varying, character varying, timestamp with time zone, timestamp with time zone) 
IS 'This function applies the MCP (Minimum Convex Polygon) algorithm (also called convex hull) to a set of locations. The input parameters are the animal id (each analysis is related to a single individual), the percentage of locations considered, a "locations_set" object (the default is "main.view_locations_set"). An additional parameter can be added: a description that will be included in the table home_ranges_mcp, where the result of the analysis is stored. The parameter "percentage" defines how many locations are included in the analysis: if for example 90% is specified (as 0.9), the 10% of locations farthest from the centroid of the data set will be excluded. If no parameters are specified, percentage of 100% and the complete data set (from the first to the last location) are considered. The function, once computed the MCP and stored the result in home_range_mcp, does not return anything. Few errors trapping are added (no points selected, percentage out of range). Note that this function works with a fixed centroid, computed at the beginning, so the distance is calculated on this basis for all the selection process.';

-- You test it
SELECT tools.mcp_perc(1, 0.1, 'test 0.1');
SELECT tools.mcp_perc(1, 0.5, 'test 0.5');
SELECT tools.mcp_perc(1, 0.75, 'test 0.75');
SELECT tools.mcp_perc(1, 1, 'test 1');
SELECT tools.mcp_perc(1, 1, 'test start and end', 'main.view_locations_set', '2006-01-01 00:00:00', '2006-01-10 00:00:00');
SELECT tools.mcp_perc(animals_id, 0.9, 'test all animals at 0.9') FROM main.animals;

-- You create a view with an alternative way to represent an homerange
CREATE VIEW analysis.view_locations_buffer AS
  SELECT 
    animals_id, 
    ST_Union(ST_Buffer(geom, 0.001))::geometry(multipolygon, 4326) AS geom 
  FROM main.gps_data_animals 
  WHERE gps_validity_code = 1
  GROUP BY animals_id
  ORDER BY animals_id;
COMMENT ON VIEW analysis.view_locations_buffer
IS 'GPS locations - Buffer (dissolved) of 0.001 degrees.';

-- You create a new data type
CREATE TYPE tools.geom_parameters AS(
  animals_id integer,
  acquisition_time timestamp with time zone,
  acquisition_time_t_1 timestamp with time zone,
  acquisition_time_t_2 timestamp with time zone,
  deltat_t_1 integer,
  deltat_t_2 integer,
  deltat_start integer,
  dist_t_1 integer,
  dist_start integer,
  speed_mh_t_1 numeric(8,2),
  abs_angle_t_1 numeric(7,5),
  rel_angle_t_2 numeric(7,5));
  
-- A function to calculate the geometric parameters of a trajectory
CREATE OR REPLACE FUNCTION tools.geom_parameters(
  animal integer, 
  time_interval integer DEFAULT 10800, 
  buffer double precision DEFAULT 600, 
  locations_set_name character varying DEFAULT 'main.view_locations_set'::character varying, 
  starting_time timestamp with time zone DEFAULT NULL::timestamp with time zone, 
  ending_time timestamp with time zone DEFAULT NULL::timestamp with time zone)
RETURNS SETOF tools.geom_parameters AS
$BODY$
DECLARE
  cursor_var tools.geom_parameters%rowtype;
  check_animal boolean;
  var_name character varying;
BEGIN
EXECUTE 
  'SELECT ' || animal || ' IN 
    (SELECT animals_id FROM main.animals)' INTO check_animal;

IF NOT check_animal THEN
  RAISE EXCEPTION 'This animal is not in the data set...';
END IF;

IF starting_time IS NULL THEN
  SELECT min(acquisition_time) 
  FROM main.view_locations_set
  WHERE view_locations_set.animals_id = animal
  INTO starting_time;
END IF;

IF ending_time IS NULL THEN
  SELECT max(acquisition_time) 
  FROM main.view_locations_set
  WHERE view_locations_set.animals_id = animal
  INTO ending_time;
END IF;

IF NOT locations_set_name = 'main.view_locations_set' THEN
  SELECT nextval('tools.unique_id_seq') into var_name;
  EXECUTE 
    'CREATE TEMPORARY TABLE temp_table_temp_table_geoparameters_'|| var_name ||' AS 
      SELECT animals_id, acquisition_time, geom 
      FROM ' || locations_set_name || ' 
      WHERE animals_id = '|| animal;
  locations_set_name = 'temp_table_temp_table_geoparameters_'|| var_name;
END IF;

FOR cursor_var IN EXECUTE 
  'SELECT 
    animals_id, 
    acquisition_time, 
    acquisition_time_t_1, 
    acquisition_time_t_2, 
    deltaT_t_1, 
    deltaT_t_2, 
    deltaT_start, 
    dist_t_1, 
    dist_start, 
    speed_Mh_t_1, 
    abs_angle_t_1, 
    CASE WHEN (deltaT_t_2 < ' || time_interval * 2 + buffer || ' and deltaT_t_2 > ' || time_interval * 2 - buffer || ') THEN 
      rel_angle_t_2
    ELSE 
      NULL
    END
  FROM 
    (SELECT 
      animals_id, 
      acquisition_time, 
      lead(acquisition_time,-1) OVER (PARTITION BY animals_id ORDER BY acquisition_time) AS acquisition_time_t_1,
      lead(acquisition_time,-2) OVER (PARTITION BY animals_id ORDER BY acquisition_time) AS acquisition_time_t_2,
      rank() OVER (PARTITION BY animals_id ORDER BY acquisition_time), 
      (extract(epoch FROM acquisition_time) - lead(extract(epoch FROM acquisition_time), -1) OVER (PARTITION BY animals_id ORDER BY acquisition_time))::integer AS deltat_t_1,
      (extract(epoch FROM acquisition_time) - lead(extract(epoch FROM acquisition_time), -2) OVER (PARTITION BY animals_id ORDER BY acquisition_time))::integer AS deltat_t_2,
      (extract(epoch FROM acquisition_time) - first_value(extract(epoch FROM acquisition_time)) OVER (PARTITION BY animals_id ORDER BY acquisition_time))::integer AS deltat_start,
      (ST_Distance_Spheroid(geom, lead(geom, -1) OVER (PARTITION BY animals_id ORDER BY acquisition_time), ''SPHEROID["WGS 84",6378137,298.257223563]''))::integer AS dist_t_1,
      ST_Distance_Spheroid(geom, first_value(geom) OVER (PARTITION BY animals_id ORDER BY acquisition_time), ''SPHEROID["WGS 84",6378137,298.257223563]'')::integer AS dist_start,
      (ST_Distance_Spheroid(geom, lead(geom, -1) OVER (PARTITION BY animals_id ORDER BY acquisition_time), ''SPHEROID["WGS 84",6378137,298.257223563]'')/(extract(epoch FROM acquisition_time) - lead(extract(epoch FROM acquisition_time), -1) OVER (PARTITION BY animals_id ORDER BY acquisition_time))*60*60)::numeric(8,2) AS speed_Mh_t_1,
      ST_Azimuth(geom::geography, (lead(geom, -1) OVER (PARTITION BY animals_id ORDER BY acquisition_time))::geography) AS abs_angle_t_1,
      ST_Azimuth(geom::geography, (lead(geom, -1) OVER (PARTITION BY animals_id ORDER BY acquisition_time))::geography) - ST_Azimuth((lead(geom, -1) OVER (PARTITION BY animals_id ORDER BY acquisition_time))::geography, (lead(geom, -2) OVER (PARTITION BY animals_id ORDER BY acquisition_time))::geography) AS rel_angle_t_2
    FROM 
      '|| locations_set_name ||'
    WHERE 
      animals_id = ' || animal ||' AND 
      geom IS NOT NULL AND 
      acquisition_time >= ''' || starting_time || ''' AND 
      acquisition_time <= ''' || ending_time || ''') a
  WHERE 
    deltaT_t_1 <' || time_interval + buffer || ' AND 
    deltaT_t_1 > '|| time_interval - buffer
LOOP
RETURN NEXT cursor_var;
END LOOP;

IF NOT locations_set_name = 'main.view_locations_set' THEN
  EXECUTE 'drop table ' || locations_set_name;
END IF;
RETURN;
END;
$BODY$
LANGUAGE plpgsql;

COMMENT ON FUNCTION tools.geom_parameters(integer, integer, double precision, character varying, timestamp with time zone, timestamp with time zone) 
IS 'This function returns a table with the geometrical parameters of the data set (reference: previous location): time gap with the previous point, time gap with the previous-previous point, distance to the previous point, speed of the last step, distance from the first point of the data set, absolute angle (previous location), relative angle (previous and previous-previous location). The input parameters are the animal id, the time gap and the buffer. The time gap select just locations that have the previous point at a defined time interval (with a buffer tolerance). All the other points are not taken into consideration. A "locations_set" class is accepted as input table. It is also possible to specify the starting and ending acquisition time of the time series. The output is a table with the structure "geom_parameters".';

-- You test the function
SELECT * FROM tools.geom_parameters(6, 60*60*2, 600);
SELECT * FROM tools.geom_parameters(6, 60*60*4, 600);
SELECT * FROM tools.geom_parameters(6, 60*60*8, 600);

-- You create another representation of the GPS data set
CREATE TYPE tools.grid_element AS (
  cell_id integer,
  geom geometry);

CREATE OR REPLACE FUNCTION tools.create_grid(
  locations_collection geometry, 
  xysize integer)
RETURNS SETOF tools.grid_element AS
$BODY$

WITH spatial_object AS
  (SELECT
    ST_Xmin(ST_Transform($1,tools.srid_utm(ST_X(ST_Centroid($1)), ST_Y(ST_Centroid($1)))))::integer AS xmin,
    ST_Ymin(ST_Transform($1,tools.srid_utm(ST_X(ST_Centroid($1)), ST_Y(ST_Centroid($1)))))::integer AS ymin,
    ST_Xmax(ST_Transform($1,tools.srid_utm(ST_X(ST_Centroid($1)), ST_Y(ST_Centroid($1)))))::integer AS xmax,
    ST_Ymax(ST_Transform($1,tools.srid_utm(ST_X(ST_Centroid($1)), ST_Y(ST_Centroid($1)))))::integer AS ymax,
    tools.srid_utm(ST_X(ST_Centroid($1)), ST_Y(ST_Centroid($1))) AS sridset)
  SELECT 
    (ROW_NUMBER() OVER ())::integer, 
    ST_Translate(cell, i , j)
  FROM 
    generate_series(
      ((((SELECT xmin FROM spatial_object) - $2/2)/100)::integer)*100, 
      (SELECT xmax FROM spatial_object) + $2, $2) AS i,
    generate_series(
      ((((SELECT ymin FROM spatial_object) - $2/2)/100)::integer)*100, 
      (SELECT ymax FROM spatial_object) + $2, $2) AS j, 
    spatial_object,
    (SELECT 
      ST_setsrid(ST_GeomFROMText('POLYGON((0 0, 0 '||$2||', '||$2||' '||$2||', '||$2||' 0,0 0))'),
      (SELECT sridset FROM spatial_object)) AS cell) AS foo;
$BODY$
LANGUAGE sql;

COMMENT ON FUNCTION tools.create_grid(geometry, integer) 
IS 'Function that creates a vector grid with a given resolution that contains a given geometry.';

CREATE OR REPLACE VIEW analysis.view_probability_grid_traj AS 
  WITH 
  setx AS (
    SELECT 
      gps_data_animals.gps_data_animals_id,
      gps_data_animals.animals_id, 
      ST_MakeLine(gps_data_animals.geom, 
      lead(gps_data_animals.geom, (-1)) OVER (PARTITION BY gps_data_animals.animals_id ORDER BY gps_data_animals.acquisition_time)) AS geom, 
      ST_Length(ST_MakeLine(gps_data_animals.geom, lead(gps_data_animals.geom, (-1)) OVER (PARTITION BY gps_data_animals.animals_id ORDER BY gps_data_animals.acquisition_time))::geography) AS line_length, 
      CASE WHEN (date_part('epoch'::text, gps_data_animals.acquisition_time) - date_part('epoch'::text, lead(gps_data_animals.acquisition_time, (-1)) OVER (PARTITION BY gps_data_animals.animals_id ORDER BY gps_data_animals.acquisition_time))) < (60 * 60 * 24)::double precision THEN 
        date_part('epoch'::text, gps_data_animals.acquisition_time) - date_part('epoch'::text, lead(gps_data_animals.acquisition_time, (-1)) OVER (PARTITION BY gps_data_animals.animals_id ORDER BY gps_data_animals.acquisition_time))
      ELSE 
        0::double precision
      END AS time_spent
    FROM 
      main.gps_data_animals
    WHERE 
      gps_data_animals.gps_validity_code = 1 AND 
      (gps_data_animals.animals_id = 1)
    ORDER BY 
      gps_data_animals.acquisition_time),
 
  gridx AS (
    SELECT 
      setx.animals_id, 
      tools.create_grid(ST_Collect(setx.geom), 100) AS cell
    FROM setx
    GROUP BY setx.animals_id)

  SELECT 
    a.animals_id * 10000 + a.cell_id AS id, 
    a.animals_id, 
    a.cell_id, 
    ST_Transform(a.geom, 4326)::geometry(Polygon,4326) AS geom, 
    (sum(a.segment_time_spent) / 60::double precision / 60::double precision)::integer AS hours_spent
  FROM 
    (SELECT 
      gridx.animals_id, 
      (gridx.cell).cell_id AS cell_id, 
      CASE setx.line_length WHEN 0 THEN 
        setx.time_spent
      ELSE 
        setx.time_spent * ST_Length(ST_Intersection(ST_Transform(setx.geom, ST_SRID((SELECT (gridx.cell).geom AS geom FROM gridx LIMIT 1))), (gridx.cell).geom)) / setx.line_length
      END AS segment_time_spent, 
      (gridx.cell).geom AS geom
    FROM gridx, setx
    WHERE ST_Intersects(ST_Transform(setx.geom, ST_SRID((SELECT (gridx.cell).geom AS geom FROM gridx LIMIT 1))), (gridx.cell).geom) AND setx.time_spent > 0::double precision AND setx.animals_id = gridx.animals_id) a
    GROUP BY a.animals_id, a.cell_id, a.geom
    HAVING sum(a.segment_time_spent) > 0::double precision;

COMMENT ON VIEW analysis.view_probability_grid_traj
IS 'This view presents the SQL code to calculate the time spent by an animal on every cell of a grid with a defined resolution, which correspond to a probability surface. Trajectory (segments between locations) is considered. Each segment represents the time spent between the two locations. This view calls the function "tools.reate_grid". This is a view with pure SQL, but this tool can be coded into a function that using temporary tables ad some other optimized approaches, can speed up the processing time. In this case, just animals 1 is returned.';

-- You create a function for the calculation of dynamic age class and test it
CREATE OR REPLACE FUNCTION tools.age_class(
  animal_id integer, 
  acquisition_time timestamp with time zone)
RETURNS integer AS
$BODY$
DECLARE
  animal_age_class_code_capture integer;
  add_year integer;
  animal_date_capture date;
BEGIN
-- Retrieve the age class at first capture
animal_age_class_code_capture = (SELECT age_class_code FROM main.animals WHERE animals_id = animal_id);

-- If the animal is already an adult any location will be adult
IF animal_age_class_code_capture = 3 THEN
  RETURN 3;
END IF;

-- In case the animal at capture was not an adult, the function checks if the capture was before or after April. 
-- In the second case, the age class will increase the April of the next year.
animal_date_capture = (SELECT age_class_code FROM main.animals WHERE animals_id = animal_id);

IF EXTRACT(month FROM animal_date_capture) > 3 THEN
  add_year = 1;
ELSE 
  add_year = 0;
END IF;

-- If the animal was an yearling at capture, I check if it went through an age class increase.
IF animal_age_class_code_capture = 2 THEN
  IF acquisition_time > ((extract(year FROM animal_date_capture) + add_year)|| '/4/1')::date THEN
    RETURN 3;
  ELSE
    RETURN 2;
  END IF;
END IF;

-- If the animal was a fawn at capture, I check if it went through two and then one age class increase.
IF animal_age_class_code_capture = 1 THEN
  IF acquisition_time > ((extract(year FROM animal_date_capture) + add_year + 1)|| '/4/1')::date THEN
    RETURN 3;
  ELSEIF acquisition_time > ((extract(year FROM animal_date_capture) + add_year)|| '/4/1')::date THEN
    RETURN 2;
  ELSE
    RETURN 1;
  END IF;
END IF;

END;
 $BODY$
LANGUAGE plpgsql;

COMMENT ON FUNCTION tools.age_class(integer, timestamp with time zone) 
IS 'This function returns the age class at the acquisition time of a location. It has two input parameters: the id of the animal and the timestamp. According to the age class at first capture, the function increases the class by 1 every time the animal goes through a defined day of the year (1st April).';

SELECT 
  animals_id, 
  acquisition_time, 
  tools.age_class(animals_id, acquisition_time) 
FROM main.gps_data_animals 
ORDER BY animals_id, acquisition_time 
LIMIT 10;

-- You create a function to generate random points and then test it in a view and in a permanent table
CREATE OR REPLACE FUNCTION tools.randompoints(
  geom geometry, 
  num_points integer,
  seed numeric DEFAULT NULL) 
RETURNS SETOF geometry AS 
$$ 
DECLARE 
  pt geometry; 
  xmin float8; 
  xmax float8; 
  ymin float8; 
  ymax float8; 
  xrange float8; 
  yrange float8; 
  srid int; 
  count integer := 0; 
  bcontains boolean := FALSE; 
  gtype text; 
BEGIN 
SELECT ST_GeometryType(geom) 
INTO gtype; 

IF ( gtype != 'ST_Polygon' ) AND ( gtype != 'ST_MultiPolygon' ) THEN 
  RAISE EXCEPTION 'Attempting to get random point in a non polygon geometry'; 
END IF; 

SELECT ST_XMin(geom), ST_XMax(geom), ST_YMin(geom), ST_YMax(geom), ST_SRID(geom) 
INTO xmin, xmax, ymin, ymax, srid; 

SELECT xmax - xmin, ymax - ymin 
INTO xrange, yrange; 

IF seed IS NOT NULL THEN 
  PERFORM setseed(seed); 
END IF; 

WHILE count < num_points LOOP 
  SELECT 
    ST_SetSRID(ST_MakePoint(
      xmin + xrange * random(), 
      ymin + yrange * random()),
      srid) 
  INTO pt; 

  SELECT ST_Contains(geom, pt) 
  INTO bcontains; 

  IF bcontains THEN 
    count := count + 1; 
    RETURN NEXT pt; 
  END IF; 
END LOOP; 
RETURN; 
END; 
$$ 
LANGUAGE 'plpgsql'; 

COMMENT ON FUNCTION tools.randompoints(geometry, integer, numeric) 
IS 'This function generates a set of random points into a given polygon (or multipolygon). The number of points and the polygon must be provided as input. A third optional parameter can define the seed, and thus generate consistent (random) set of points.';

CREATE VIEW analysis.view_test_randompoints AS
  SELECT
    row_number() over() as id,
    geom::geometry(point, 4326)
  FROM 
    (SELECT
      tools.randompoints(
        (select geom geom from env_data.study_area), 
        100)as geom) a;
COMMENT ON VIEW analysis.view_test_randompoints 
IS 'This view is a test that shows 100 random points (generated every time that the view is called) into the boundaries of the first polygon stored in the home_ranges_mcp table.';

CREATE TABLE analysis.test_randompoints AS
  SELECT
    row_number() over() as id,
    geom::geometry(point, 4326)
  FROM 
    (SELECT
      tools.randompoints(
        (select geom from env_data.study_area), 
        100)as geom) a;
ALTER TABLE analysis.test_randompoints
  ADD CONSTRAINT test_randompoints_pk PRIMARY KEY(id);

COMMENT ON TABLE analysis.test_randompoints 
IS 'This table is a test that permanently stores 100 random points into the boundaries of the first polygon stored in the home_ranges_mcp table.';