Pārlūkot izejas kodu

Edited DB Model, Added API Endpoints with location

Lukas Cerny 1 gadu atpakaļ
vecāks
revīzija
3b2198481f

+ 2 - 2
docker-compose.yaml

@@ -21,8 +21,8 @@ services:
     ports:
       - "8080:8080"
       - "5005:5005"
-#    depends_on:
-#      - telemetry-db
+    depends_on:
+      - telemetry-db
 
   telemetry-test:
     container_name: senslog_telemetry_test

+ 3 - 3
sql/init.sql

@@ -69,7 +69,7 @@ CREATE TABLE maplog.obs_telemetry (
     unit_id BIGINT NOT NULL,
     observed_values jsonb NOT NULL,
     the_geom geometry,
---     speed INTEGER NOT NULL,
+    speed INTEGER NOT NULL,
     time_received TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
 );
 
@@ -123,7 +123,7 @@ CREATE TABLE maplog.sensor (
     sensor_id BIGINT NOT NULL PRIMARY KEY,
     sensor_name CHARACTER VARYING(100) UNIQUE,
     sensor_type TEXT,
-    sensor_type_id INTEGER,
+    io_id INTEGER,
     min_range TEXT,
     max_range TEXT,
     phenomenon_id INTEGER NOT NULL
@@ -148,7 +148,7 @@ ALTER TABLE maplog.system_user OWNER TO senslog;
 
 CREATE TABLE maplog.unit (
     unit_id BIGINT NOT NULL PRIMARY KEY,
-    imei CHARACTER VARYING(20) NOT NULL,
+    imei CHARACTER VARYING(20) NOT NULL UNIQUE,
     description TEXT,
     is_mobile boolean DEFAULT true NOT NULL,
     unit_type_id CHARACTER VARYING(2) DEFAULT 'X'::CHARACTER VARYING NOT NULL

+ 1 - 1
sql/mock-data.sql

@@ -6,7 +6,7 @@ INSERT INTO maplog.unit(unit_id, imei, unit_type_id, description) VALUES (1000,
 
 INSERT INTO maplog.unit_to_campaign(camp_id, unit_id, from_time, to_time) SELECT campaign_id, 1000, from_time, to_time FROM maplog.campaign;
 
-INSERT INTO maplog.phenomenon(phenomenon_name, uom) VALUES ('Default phenomenon', 'no UOM');
+INSERT INTO maplog.phenomenon(phenomenon_id, phenomenon_name, uom) VALUES (1, 'DEFAULT', 'NONE');
 
 INSERT INTO maplog.sensor(sensor_id, sensor_name, sensor_type_id, phenomenon_id) VALUES (360200000, 'IO Property 66', 66, 1);
 INSERT INTO maplog.sensor(sensor_id, sensor_name, sensor_type_id, phenomenon_id) VALUES (360300000, 'IO Property 239', 239, 1);

+ 10 - 0
src/main/java/cz/senslog/telemetry/database/SortType.java

@@ -0,0 +1,10 @@
+package cz.senslog.telemetry.database;
+
+public enum SortType {
+
+    DESC, ASC
+    ;
+    public static SortType of(String type) {
+        return valueOf(type.toUpperCase());
+    }
+}

+ 32 - 0
src/main/java/cz/senslog/telemetry/database/domain/UnitLocation.java

@@ -0,0 +1,32 @@
+package cz.senslog.telemetry.database.domain;
+
+import java.time.OffsetDateTime;
+
+public class UnitLocation {
+
+    private final long unitId;
+    private final OffsetDateTime timestamp;
+    private final float [] location;
+
+    public static UnitLocation of(long unitId, OffsetDateTime timestamp, float[] location) {
+        return new UnitLocation(unitId, timestamp, location);
+    }
+
+    private UnitLocation(long unitId, OffsetDateTime timestamp, float[] location) {
+        this.unitId = unitId;
+        this.timestamp = timestamp;
+        this.location = location;
+    }
+
+    public long getUnitId() {
+        return unitId;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+
+    public float[] getLocation() {
+        return location;
+    }
+}

+ 129 - 15
src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java

@@ -2,6 +2,7 @@ package cz.senslog.telemetry.database.repository;
 
 import cz.senslog.telemetry.database.DataNotFoundException;
 import cz.senslog.telemetry.database.PagingRetrieve;
+import cz.senslog.telemetry.database.SortType;
 import cz.senslog.telemetry.database.domain.*;
 import io.vertx.core.Future;
 import io.vertx.core.json.JsonObject;
@@ -17,6 +18,7 @@ import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -59,8 +61,8 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<Integer> saveTelemetry(ObsTelemetry data) {
-        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(time_stamp, unit_id, observed_values, the_geom) " +
-                        "VALUES ($1, $2, $3::json, ST_SetSRID(ST_MakePoint($4, $5, $6, $7), 4326)) RETURNING (obs_id)")
+        return client.preparedQuery("INSERT INTO maplog.obs_telemetry(time_stamp, unit_id, observed_values, the_geom, speed) " +
+                        "VALUES ($1, $2, $3::json, ST_SetSRID(ST_MakePoint($4, $5, $6), 4326), $7) RETURNING (obs_id)")
                 .execute(Tuple.of(
                         data.getTimestamp(),
                         data.getUnitId(),
@@ -93,12 +95,21 @@ public class MapLogRepository implements SensLogRepository {
                 d.getSpeed()
         )).collect(toList());
         return client.preparedQuery("INSERT INTO maplog.obs_telemetry(time_stamp, unit_id, observed_values, the_geom, speed) " +
-                        "VALUES ($1, $2,  $3::json, ST_SetSRID(ST_MakePoint($4, $5, $6, $7), 4326))")
+                        "VALUES ($1, $2,  $3::json, ST_SetSRID(ST_MakePoint($4, $5, $6), 4326), $7)")
                 .executeBatch(tuples)
                 .map(SqlResult::rowCount);
     }
 
     @Override
+    public Future<Boolean> createSensor(Sensor sensor, long unitId) {
+        return client.preparedQuery("WITH rows AS " +
+                        "(INSERT INTO maplog.sensor(sensor_id, io_id, sensor_name, phenomenon_id) VALUES ($1, $2, $3, $4) RETURNING sensor_id) " +
+                        "INSERT INTO maplog.unit_to_sensor(sensor_id, unit_id) SELECT sensor_id, $5 FROM rows RETURNING sensor_id")
+                .execute(Tuple.of(sensor.getSensorId(), sensor.getIoID(), sensor.getName(), 1, unitId))
+                .map(r -> r.iterator().next().getInteger(0) > 0);
+    }
+
+    @Override
     public Future<List<Unit>> loadAllUnits() {
         return client.query("SELECT unit_id FROM maplog.unit")
                 .execute()
@@ -124,14 +135,14 @@ public class MapLogRepository implements SensLogRepository {
     private static final Function<Row, Sensor> ROW_TO_SENSOR = (row) -> Sensor.of(
             row.getLong("sensor_id"),
             row.getString("sensor_name"),
-            row.getInteger("sensor_type_id")
+            row.getInteger("io_id")
     );
 
     @Override
     public Future<Sensor> findSensorByIOAndUnitId(int ioID, long unitId) {
-        return client.preparedQuery("SELECT s.sensor_id, s.sensor_name, s.sensor_type_id FROM maplog.sensor AS s " +
+        return client.preparedQuery("SELECT s.sensor_id, s.sensor_name, s.io_id FROM maplog.sensor AS s " +
                         "JOIN maplog.unit_to_sensor uts ON s.sensor_id = uts.sensor_id " +
-                        "WHERE s.sensor_type_id = $1 AND uts.unit_id = $2")
+                        "WHERE s.io_id = $1 AND uts.unit_id = $2")
                 .execute(Tuple.of(ioID, unitId))
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_SENSOR.apply(iterator.next()) : null);
@@ -139,7 +150,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<Sensor>> findSensorsByUnitId(long unitId) {
-        return client.preparedQuery("SELECT s.sensor_id, s.sensor_name, s.sensor_type_id FROM maplog.unit_to_sensor AS uts " +
+        return client.preparedQuery("SELECT s.sensor_id, s.sensor_name, s.io_id FROM maplog.unit_to_sensor AS uts " +
                         "JOIN maplog.sensor s on s.sensor_id = uts.sensor_id " +
                         "WHERE UTS.unit_id = $1")
                 .execute(Tuple.of(unitId))
@@ -216,11 +227,10 @@ public class MapLogRepository implements SensLogRepository {
     @Override
     public Future<List<ObsTelemetry>> findObservationsByCampaignId(long campaignId) {
         return client.preparedQuery(
-                    "SELECT obs.obs_id, obs.time_stamp, obs.unit_id, obs.observed_values, " +
+                    "SELECT obs.obs_id, obs.time_stamp, obs.unit_id, obs.observed_values, obs.speed " +
                             "ST_X (ST_Transform (obs.the_geom, 4326)) AS long, " +
                             "ST_Y (ST_Transform (obs.the_geom, 4326)) AS lat " +
                             "ST_Z (ST_Transform (obs.the_geom, 4326)) AS alt " +
-                            "ST_M (obs.the_geom) AS speed " +
                         "FROM maplog.obs_telemetry AS obs " +
                         "JOIN maplog.unit AS u ON u.unit_id = obs.unit_id " +
                         "JOIN maplog.unit_to_campaign AS u_cam ON u_cam.unit_id = u.unit_id " +
@@ -247,9 +257,8 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<List<ObsTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId,
-                                                                            OffsetDateTime from, OffsetDateTime to, ZoneId zone,
-                                                                            int offset, int limit
+    public Future<List<ObsTelemetry>> findObservationsByCampaignIdAndUnitId(
+            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
     ) {
         String whereTimestampClause;
         Tuple tupleParams;
@@ -267,12 +276,11 @@ public class MapLogRepository implements SensLogRepository {
             tupleParams = Tuple.of(campaignId, unitId, offset, limit, zone.getId());
         }
 
-        String sql = "SELECT tel.obs_id, tel.unit_id, tel.observed_values::json, " +
+        String sql = "SELECT tel.obs_id, tel.unit_id, tel.observed_values::json, tel.speed " +
                     "tel.time_stamp, $5 AS zone_id, " + // ::timestamp with time zone at time zone $5 AS time_stamp
                     "ST_X (ST_Transform (tel.the_geom, 4326)) AS long, " +
                     "ST_Y (ST_Transform (tel.the_geom, 4326)) AS lat, " +
                     "ST_Z (ST_Transform (tel.the_geom, 4326)) AS alt " +
-                    "ST_M (tel.the_geom) AS speed " +
                 "FROM maplog.obs_telemetry AS tel " +
                 "JOIN maplog.unit u on u.unit_id = tel.unit_id " +
                 "JOIN maplog.unit_to_campaign utc on tel.unit_id = utc.unit_id " +
@@ -297,7 +305,9 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<PagingRetrieve<List<ObsTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<PagingRetrieve<List<ObsTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(
+            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+    ) {
         return findObservationsByCampaignIdAndUnitId(campaignId, unitId, from, to, zone, offset, limit+1)
                 .map(data -> {
                     boolean hasNext = data.size() > limit;
@@ -307,4 +317,108 @@ public class MapLogRepository implements SensLogRepository {
                    return new PagingRetrieve<>(hasNext, data.size(), data);
                 });
     }
+
+    @Override
+    public Future<List<UnitLocation>> findLocationsByCampaignIdAndUnitId(
+            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+    ) {
+        String whereTimestampClause;
+        Tuple tupleParams;
+        if (from != null && to != null) {
+            whereTimestampClause = "tel.time_stamp <= (CASE WHEN $6 > utc.to_time THEN utc.to_time ELSE $6 END) AND tel.time_stamp >= (CASE WHEN $7 > utc.from_time THEN utc.from_time ELSE $7 END)";
+            tupleParams = Tuple.of(campaignId, unitId, offset, limit, zone.getId(), from, to);
+        } else if (from != null) {
+            whereTimestampClause = "tel.time_stamp >= (CASE WHEN $6 > utc.from_time THEN utc.from_time ELSE $6 END) AND tel.time_stamp <= utc.to_time";
+            tupleParams = Tuple.of(campaignId, unitId, offset, limit, zone.getId(), from);
+        } else if (to != null) {
+            whereTimestampClause = "tel.time_stamp >= utc.from_time AND tel.time_stamp <= (CASE WHEN $6 > utc.to_time THEN utc.to_time ELSE $6 END)";
+            tupleParams = Tuple.of(campaignId, unitId, offset, limit, zone.getId(), to);
+        } else {
+            whereTimestampClause = "tel.time_stamp >= utc.from_time AND tel.time_stamp <= utc.to_time";
+            tupleParams = Tuple.of(campaignId, unitId, offset, limit, zone.getId());
+        }
+
+        String sql = "SELECT tel.unit_id, tel.time_stamp, $5 AS zone_id, " + // ::timestamp with time zone at time zone $5 AS time_stamp
+                "ST_X (ST_Transform (tel.the_geom, 4326)) AS long, " +
+                "ST_Y (ST_Transform (tel.the_geom, 4326)) AS lat, " +
+                "ST_Z (ST_Transform (tel.the_geom, 4326)) AS alt " +
+                "FROM maplog.obs_telemetry AS tel " +
+                "JOIN maplog.unit u on u.unit_id = tel.unit_id " +
+                "JOIN maplog.unit_to_campaign utc on tel.unit_id = utc.unit_id " +
+                "WHERE utc.camp_id = $1 AND utc.unit_id = $2 AND " + whereTimestampClause + " " +
+                "ORDER BY tel.time_stamp OFFSET $3 LIMIT $4;";
+
+        return client.preparedQuery(sql)
+                .execute(tupleParams)
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(r -> UnitLocation.of(
+                                r.getLong("unit_id"),
+                                r.getOffsetDateTime("time_stamp"),
+                                new float[] {
+                                        r.getFloat("long"),
+                                        r.getFloat("lat"),
+                                        r.getFloat("alt")
+                                })
+                        )
+                        .collect(Collectors.toList()))
+                .onFailure(logger::catching);
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<UnitLocation>>> findLocationsByCampaignIdAndUnitIdWithPaging(
+            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+    ) {
+        return findLocationsByCampaignIdAndUnitId(campaignId, unitId, from, to, zone, offset, limit+1)
+                .map(data -> {
+                    boolean hasNext = data.size() > limit;
+                    if (hasNext) {
+                        data.remove(data.size() - 1);
+                    }
+                    return new PagingRetrieve<>(hasNext, data.size(), data);
+                });
+    }
+
+    @Override
+    public Future<List<UnitLocation>> findUnitsLocationsByCampaignId(
+            long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort
+    ) {
+        String whereTimestampClause;
+        Tuple tupleParams;
+        if (from != null && to != null) {
+            whereTimestampClause = "WHERE utc.camp_id = $1 AND time_stamp >= $4 AND time_stamp < $5";
+            tupleParams = Tuple.of(campaignId, limitPerUnit, zone.getId(), from, to);
+        } else if (from != null) {
+            whereTimestampClause = "WHERE utc.camp_id = $1 AND time_stamp >= $4";
+            tupleParams = Tuple.of(campaignId, limitPerUnit, zone.getId(), from);
+        } else if (to != null) {
+            whereTimestampClause = "WHERE utc.camp_id = $1 AND time_stamp < $4";
+            tupleParams = Tuple.of(campaignId, limitPerUnit, zone.getId(), to);
+        } else {
+            whereTimestampClause = "WHERE utc.camp_id = $1";
+            tupleParams = Tuple.of(campaignId, limitPerUnit, zone.getId());
+        }
+
+        String sql = "SELECT unit_id, time_stamp, $3 AS zone_id, " + // ::timestamp with time zone at time zone $5 AS time_stamp
+                    "ST_X (ST_Transform (the_geom, 4326)) AS long, " +
+                    "ST_Y (ST_Transform (the_geom, 4326)) AS lat, " +
+                    "ST_Z (ST_Transform (the_geom, 4326)) AS alt " +
+                "FROM (SELECT *, row_number() OVER (PARTITION BY unit_id ORDER BY time_stamp "+ sort.name() +" ) AS rn " +
+                "FROM (SELECT obs.* FROM maplog.obs_telemetry obs JOIN maplog.unit_to_campaign utc ON obs.unit_id = utc.unit_id "+whereTimestampClause+") AS data) AS g " +
+                "WHERE rn <= $2";
+
+        return client.preparedQuery(sql)
+                .execute(tupleParams)
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(r -> UnitLocation.of(
+                                r.getLong("unit_id"),
+                                r.getOffsetDateTime("time_stamp"),
+                                new float[] {
+                                        r.getFloat("long"),
+                                        r.getFloat("lat"),
+                                        r.getFloat("alt")
+                                }
+                        ))
+                        .collect(toList()))
+                .onFailure(logger::catching);
+    }
 }

+ 21 - 0
src/main/java/cz/senslog/telemetry/database/repository/MockMapLogRepository.java

@@ -1,6 +1,7 @@
 package cz.senslog.telemetry.database.repository;
 
 import cz.senslog.telemetry.database.PagingRetrieve;
+import cz.senslog.telemetry.database.SortType;
 import cz.senslog.telemetry.database.domain.*;
 import io.vertx.core.Future;
 import org.apache.logging.log4j.LogManager;
@@ -32,6 +33,11 @@ public class MockMapLogRepository implements SensLogRepository {
         return Future.succeededFuture(data.size());
     }
 
+    @Override
+    public Future<Boolean> createSensor(Sensor sensor, long unitId) {
+        return Future.succeededFuture(true);
+    }
+
     public Future<List<Unit>> loadAllUnits() {
         return Future.succeededFuture(Collections.emptyList());
     }
@@ -75,6 +81,16 @@ public class MockMapLogRepository implements SensLogRepository {
     }
 
     @Override
+    public Future<List<UnitLocation>> findLocationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+        return Future.succeededFuture(Collections.emptyList());
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<UnitLocation>>> findLocationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+        return Future.succeededFuture(new PagingRetrieve<>(false, 0, Collections.emptyList()));
+    }
+
+    @Override
     public Future<Map<Long, Sensor>> findSensorsByUnitIdGroupById(long unitId) {
         return Future.succeededFuture(Collections.emptyMap());
     }
@@ -88,4 +104,9 @@ public class MockMapLogRepository implements SensLogRepository {
     public Future<List<CampaignUnit>> findUnitsByCampaignId(long campaignId) {
         return Future.succeededFuture(Collections.emptyList());
     }
+
+    @Override
+    public Future<List<UnitLocation>> findUnitsLocationsByCampaignId(long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort) {
+        return Future.succeededFuture(Collections.emptyList());
+    }
 }

+ 10 - 0
src/main/java/cz/senslog/telemetry/database/repository/SensLogRepository.java

@@ -1,6 +1,7 @@
 package cz.senslog.telemetry.database.repository;
 
 import cz.senslog.telemetry.database.PagingRetrieve;
+import cz.senslog.telemetry.database.SortType;
 import cz.senslog.telemetry.database.domain.*;
 import io.vertx.core.Future;
 
@@ -14,7 +15,13 @@ public interface SensLogRepository {
     Future<Integer> updateDriverAction(DriverAction data);
     Future<Integer> saveTelemetry(ObsTelemetry data);
     Future<Integer> saveAllTelemetry(List<ObsTelemetry> data);
+
+    Future<Boolean> createSensor(Sensor sensor, long unitId);
+
+
     Future<List<Unit>> loadAllUnits();
+
+
     Future<Unit> findUnitByIMEI(String imei);
     Future<Sensor> findSensorByIOAndUnitId(int ioID, long unitId);
     Future<List<Campaign>> allCampaigns();
@@ -23,7 +30,10 @@ public interface SensLogRepository {
     Future<List<ObsTelemetry>> findObservationsByCampaignId(long campaignId);
     Future<List<ObsTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
     Future<PagingRetrieve<List<ObsTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+    Future<List<UnitLocation>> findLocationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+    Future<PagingRetrieve<List<UnitLocation>>> findLocationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
     Future<Map<Long, Sensor>> findSensorsByUnitIdGroupById(long unitId);
     Future<List<Sensor>> findSensorsByUnitId(long unitId);
     Future<List<CampaignUnit>> findUnitsByCampaignId(long campaignId);
+    Future<List<UnitLocation>> findUnitsLocationsByCampaignId(long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort);
 }

+ 1 - 1
src/main/java/cz/senslog/telemetry/server/HttpVertxServer.java

@@ -61,7 +61,7 @@ public final class HttpVertxServer extends AbstractVerticle {
                     openAPIRouterBuilder.operation("campaignIdUnitsGET").handler(apiHandler::campaignIdUnitsGET);
                     openAPIRouterBuilder.operation("campaignIdUnitIdObservationsGET").handler(apiHandler::campaignIdUnitIdObservationsGET);
                     openAPIRouterBuilder.operation("campaignIdUnitsObservationsLocationGET").handler(apiHandler::campaignIdUnitsObservationsLocationGET);
-
+                    openAPIRouterBuilder.operation("campaignIdUnitIdLocationsGET").handler(apiHandler::campaignIdUnitIdLocationsGET);
 
                     Router mainRouter = openAPIRouterBuilder.createRouter();
 //                    mainRouter.route().handler(LoggerHandler.create());

+ 106 - 4
src/main/java/cz/senslog/telemetry/server/OpenAPIHandler.java

@@ -1,7 +1,10 @@
 package cz.senslog.telemetry.server;
 
 import cz.senslog.telemetry.app.Application;
+import cz.senslog.telemetry.database.SortType;
+import cz.senslog.telemetry.database.domain.UnitLocation;
 import cz.senslog.telemetry.database.repository.SensLogRepository;
+import cz.senslog.telemetry.utils.JsonConvertor;
 import cz.senslog.telemetry.utils.TernaryCondition;
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.json.JsonArray;
@@ -15,10 +18,12 @@ import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import static cz.senslog.telemetry.utils.JsonConvertor.floatArrToJson;
 import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
 import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
 import static java.util.stream.Collectors.toList;
@@ -155,7 +160,6 @@ public class OpenAPIHandler {
 
         Function<Long, String> createNextNavLink = dataSize -> {
             String urlParams = paramsJson.stream()
-
                     .filter(e -> !e.getKey().equals("offset"))
                     .map(e -> String.format("%s=%s", e.getKey(), e.getValue()))
                     .collect(Collectors.joining("&"));
@@ -225,9 +229,57 @@ public class OpenAPIHandler {
         });
 
         List<String> paramSort = rc.queryParam("sort");
-        String sort = TernaryCondition.<String>ternaryIf(paramSort::isEmpty, "asc", () -> {
+        SortType sortType = TernaryCondition.<SortType>ternaryIf(paramSort::isEmpty, SortType.ASC, () -> {
            paramsJson.put("sort", paramLimitPerUnit.get(0));
-           return paramSort.get(0);
+           return SortType.of(paramSort.get(0));
+        });
+
+        List<String> paramZone = rc.queryParam("zone");
+        ZoneId zone = TernaryCondition.<ZoneId>ternaryIf(paramZone::isEmpty, ZoneId.of("UTC"), () -> {
+            paramsJson.put("zone", paramZone.get(0));
+            return ZoneId.of(paramZone.get(0));
+        });
+
+        List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
+            paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
+            return Boolean.parseBoolean(paramNavigationLinks.get(0));
+        });
+
+        JsonObject navLinks = navigationLinks ? JsonObject.of(
+                "Campaign@NavigationLink", String.format("%s/campaigns/%d",host, campaignId)
+        ) : JsonObject.of();
+
+        repo.findUnitsLocationsByCampaignId(campaignId, limitPerUnit, from, to, zone, sortType)
+                .onSuccess(locations -> rc.response().end(navLinks.mergeIn(JsonObject.of(
+                        "params", paramsJson,
+                        "size", locations.size(),
+                        "data", new JsonArray(locations.stream().map(l -> JsonObject.of(
+                            "unitId", l.getUnitId(),
+                            "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                            "location", floatArrToJson(l.getLocation())
+                        )).collect(toList()))
+                )).encode()))
+                .onFailure(th -> rc.fail(400, th));
+    }
+
+    public void campaignIdUnitIdLocationsGET(RoutingContext rc) {
+        String host =  hostURLFull(rc.request());
+        JsonObject paramsJson = new JsonObject();
+
+        long campaignId = Long.parseLong(rc.pathParam("campaignId"));
+        long unitId = Long.parseLong(rc.pathParam("unitId"));
+
+        List<String> paramFrom = rc.queryParam("from");
+        OffsetDateTime from = TernaryCondition.<OffsetDateTime>ternaryIf(paramFrom::isEmpty, () -> null, () -> {
+            paramsJson.put("from", paramFrom.get(0));
+            return OffsetDateTime.parse(paramFrom.get(0));
+        });
+
+        List<String> paramTo = rc.queryParam("to");
+        OffsetDateTime to = TernaryCondition.<OffsetDateTime>ternaryIf(paramTo::isEmpty, () -> null,() -> {
+            paramsJson.put("to", paramTo.get(0));
+            return OffsetDateTime.parse(paramTo.get(0));
         });
 
         List<String> paramZone = rc.queryParam("zone");
@@ -236,12 +288,62 @@ public class OpenAPIHandler {
             return ZoneId.of(paramZone.get(0));
         });
 
+        List<String> paramOffset = rc.queryParam("offset");
+        int offset = TernaryCondition.<Integer>ternaryIf(paramOffset::isEmpty, 0, () -> {
+            paramsJson.put("offset", paramOffset.get(0));
+            return Integer.parseInt(paramOffset.get(0));
+        });
+
+        List<String> paramLimit = rc.queryParam("limit");
+        int limit = TernaryCondition.<Integer>ternaryIf(paramLimit::isEmpty, DEFAULT_MAX_DATA_LIMIT, () -> {
+            paramsJson.put("limit", paramLimit.get(0));
+            return Integer.parseInt(paramLimit.get(0));
+        });
+
+        List<String> filter = rc.queryParam("filter");
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
             return Boolean.parseBoolean(paramNavigationLinks.get(0));
         });
 
-        // TODO create repository method and then...
+        JsonObject navLinks = navigationLinks ? JsonObject.of(
+                "Campaign@NavigationLink", String.format("%s/campaigns/%d",host, campaignId),
+                "Unit@NavigationLink", String.format("%s/campaigns/%d/units/%d", host, campaignId, unitId)
+        ) : JsonObject.of();
+
+        Function<Long, String> createNextNavLink = dataSize -> {
+            String urlParams = paramsJson.stream()
+                    .filter(e -> !e.getKey().equals("offset"))
+                    .map(e -> String.format("%s=%s", e.getKey(), e.getValue()))
+                    .collect(Collectors.joining("&"));
+            long newOffset = offset + dataSize;
+            return String.format("%s/campaigns/%d/units/%d/observations?offset=%d&%s", host, campaignId, unitId, newOffset, urlParams);
+        };
+
+        if (filter.isEmpty()) {
+            repo.findLocationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
+                    .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
+                            "next@NavigationLink", createNextNavLink.apply(paging.size())
+                    ) : JsonObject.of()).mergeIn(JsonObject.of(
+                            "params", paramsJson,
+                            "size", paging.size(),
+                            "offset", offset,
+                            "hasNext", paging.hasNext(),
+                            "data", new JsonArray(
+                                    paging.data().stream().map(l -> JsonObject.of(
+                                            "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                            "location", floatArrToJson(l.getLocation())
+                                    )).collect(toList()))
+                    )).encode()))
+                    .onFailure(th -> rc.fail(400, th));
+        } else {
+            // TODO implement filter
+            /*
+            	filtr=sensorId(10003)gt10
+            	filtr=driverId(34245)eq3
+             */
+        }
     }
 }

+ 56 - 0
src/main/java/cz/senslog/telemetry/utils/JsonConvertor.java

@@ -0,0 +1,56 @@
+package cz.senslog.telemetry.utils;
+
+import io.vertx.core.json.JsonArray;
+
+import java.util.ArrayList;
+
+public final class JsonConvertor {
+
+    public static JsonArray byteArrToJson(byte[] arr) {
+        JsonArray j = new JsonArray(new ArrayList(arr.length));
+        for (byte v : arr) {
+            j.add(v);
+        }
+        return j;
+    }
+
+    public static JsonArray shortArrToJson(short[] arr) {
+        JsonArray j = new JsonArray(new ArrayList(arr.length));
+        for (short v : arr) {
+            j.add(v);
+        }
+        return j;
+    }
+
+    public static JsonArray intArrToJson(int[] arr) {
+        JsonArray j = new JsonArray(new ArrayList(arr.length));
+        for (int v : arr) {
+            j.add(v);
+        }
+        return j;
+    }
+
+    public static JsonArray floatArrToJson(float[] arr) {
+        JsonArray j = new JsonArray(new ArrayList(arr.length));
+        for (float v : arr) {
+            j.add(v);
+        }
+        return j;
+    }
+
+    public static JsonArray doubleArrToJson(double[] arr) {
+        JsonArray j = new JsonArray(new ArrayList(arr.length));
+        for (double v : arr) {
+            j.add(v);
+        }
+        return j;
+    }
+
+    public static JsonArray longArrToJson(long[] arr) {
+        JsonArray j = new JsonArray(new ArrayList(arr.length));
+        for (long v : arr) {
+            j.add(v);
+        }
+        return j;
+    }
+}

+ 59 - 7
src/main/resources/openAPISpec.yaml

@@ -176,14 +176,17 @@ paths:
             type: string
             format: date-time
           required: false
+          example: 2017-07-21T17:32:28Z
         - in: query
           name: to
           schema:
             type: string
             format: date-time
           required: false
+          example: 2017-07-21T17:32:28Z
         - in: query
           name: limitPerUnit
+          required: true
           schema:
             type: integer
             default: 1
@@ -195,6 +198,7 @@ paths:
           name: sort
           schema:
             type: string
+            enum: [asc, desc]
             default: asc
           examples:
             Descending Order:
@@ -205,6 +209,7 @@ paths:
           name: zone
           schema:
             type: string
+            default: UTC
           required: false
         - in: query
           name: navigationLinks
@@ -213,12 +218,12 @@ paths:
             default: true
           description: Option to disable @NavigationLinks in a response
       responses:
-#        200:
-#          description: JSON containing stream of telemetry data
-#          content:
-#            application/json:
-#              schema:
-#                $ref: '#/components/schemas/CampaignObservation'
+        200:
+          description: JSON containing stream of telemetry data
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/CampaignUnitsLocations'
         default:
           description: unexpected error
           content:
@@ -352,13 +357,14 @@ paths:
             type: string
             format: date-time
           required: false
-          example: 2023-01-25 15:35:32Z
+          example: 2017-07-21T17:32:28Z
         - in: query
           name: to
           schema:
             type: string
             format: date-time
           required: false
+          example: 2017-07-21T17:32:28Z
         - in: query
           name: zone
           schema:
@@ -1177,6 +1183,52 @@ components:
           - timestamp: "2023-01-25 15:35:32Z"
             location: [49.7384, 13.3736, 350.3]
 
+    CampaignUnitsLocations:
+      type: object
+      required:
+        - size
+        - data
+      properties:
+        Campaign@NavigationLink:
+          type: string
+          format: uri
+        params:
+          type: object
+          description: Used params in URL
+        size:
+          type: integer
+        data:
+          type: array
+          items:
+            type: object
+            required:
+              - unitId
+              - timestamp
+              - location
+            properties:
+              unitId:
+                type: integer
+                format: int64
+              timestamp:
+                type: string
+                format: date-time
+              location:
+                description: Array in a format [longitude, latitude, altitude]
+                type: array
+                items:
+                  type: integer
+      example:
+        Campaign@NavigationLink: "<domain>/campaigns/1"
+        params:
+          from: "2023-01-25 15:35:32Z"
+          to: "2023-01-25 15:35:32Z"
+          navigationLinks: true
+        size: 8
+        data:
+          - unitId: 25
+            timestamp: "2023-01-25 15:35:32Z"
+            location: [ 49.7384, 13.3736, 350.3 ]
+
     Location:
       type: object
       properties: