Ver Fonte

Added API 'campaignIdUnitIdSensorIdObservationsGET'

Lukas Cerny há 1 ano atrás
pai
commit
7a41152f44

+ 44 - 0
src/main/java/cz/senslog/telemetry/database/domain/SensorTelemetry.java

@@ -0,0 +1,44 @@
+package cz.senslog.telemetry.database.domain;
+
+import java.time.OffsetDateTime;
+
+public class SensorTelemetry {
+
+    private final long id;
+    private final Long value;
+    private final OffsetDateTime timestamp;
+    private final Location location;
+    private final float speed;
+
+    public static SensorTelemetry of(long id, Long value, OffsetDateTime timestamp, Location location, float speed) {
+        return new SensorTelemetry(id, value, timestamp, location, speed);
+    }
+
+    public SensorTelemetry(long id, Long value, OffsetDateTime timestamp, Location location, float speed) {
+        this.id = id;
+        this.value = value;
+        this.timestamp = timestamp;
+        this.location = location;
+        this.speed = speed;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public OffsetDateTime getTimestamp() {
+        return timestamp;
+    }
+
+    public Location getLocation() {
+        return location;
+    }
+
+    public float getSpeed() {
+        return speed;
+    }
+
+    public Long getValue() {
+        return value;
+    }
+}

+ 69 - 7
src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java

@@ -61,7 +61,7 @@ public class MapLogRepository implements SensLogRepository {
     @Override
     public Future<Integer> saveTelemetry(UnitTelemetry data) {
         return client.preparedQuery("INSERT INTO maplog.obs_telemetry(unit_id, time_stamp, the_geom, speed, observed_values) " +
-                        "VALUES ($1, $2, ST_SetSRID(ST_MakePoint($3, $4, $5, $6), 4326), $7, $8::json) RETURNING (obs_id)")
+                        "VALUES ($1, $2, ST_SetSRID(ST_MakePoint($3, $4, $5, $6), 4326), $7, $8::jsonb) RETURNING (obs_id)")
                 .execute(Tuple.of(
                         data.getUnitId(),
                         data.getTimestamp(),
@@ -70,7 +70,7 @@ public class MapLogRepository implements SensLogRepository {
                         data.getLocation().getAltitude(),
                         data.getLocation().getAngle(),
                         data.getSpeed(),
-                        data.getObservedValues().toString()
+                        data.getObservedValues()
                 ))
                 .onComplete(res -> {
                     if (res.succeeded()) {
@@ -93,10 +93,10 @@ public class MapLogRepository implements SensLogRepository {
                 d.getLocation().getAltitude(),
                 d.getLocation().getAngle(),
                 d.getSpeed(),
-                d.getObservedValues().toString()
+                d.getObservedValues()
         )).collect(toList());
         return client.preparedQuery("INSERT INTO maplog.obs_telemetry(unit_id, time_stamp, the_geom, speed, observed_values) " +
-                        "VALUES ($1, $2, ST_SetSRID(ST_MakePoint($3, $4, $5, $6), 4326), $7, $8::json)")
+                        "VALUES ($1, $2, ST_SetSRID(ST_MakePoint($3, $4, $5, $6), 4326), $7, $8::jsonb)")
                 .executeBatch(tuples)
                 .map(SqlResult::rowCount);
     }
@@ -466,7 +466,7 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt"),
                                         r.getFloat("angle")),
                                 r.getFloat("speed"),
-                                new JsonObject(r.getString("observed_values"))
+                                r.getJsonObject("observed_values")
                         ))
                         .collect(Collectors.toList()))
                 .onFailure(logger::catching);
@@ -513,7 +513,6 @@ public class MapLogRepository implements SensLogRepository {
                     "ST_Z (ST_Transform (tel.the_geom, 4326)) AS alt, " +
                     "ST_M (tel.the_geom) AS angle " +
                 "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;";
@@ -531,7 +530,7 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt"),
                                         r.getFloat("angle")),
                                 r.getFloat("speed"),
-                                new JsonObject(r.getString("observed_values"))))
+                                r.getJsonObject("observed_values")))
                         .collect(Collectors.toList()))
                 .onFailure(logger::catching);
     }
@@ -551,6 +550,69 @@ public class MapLogRepository implements SensLogRepository {
     }
 
     @Override
+    public Future<List<SensorTelemetry>> findObservationsByCampaignIdAndUnitIdAndSensorId(
+            long campaignId, long unitId, long sensorId, 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 $7 > utc.to_time THEN utc.to_time ELSE $7 END) AND tel.time_stamp >= (CASE WHEN $8 > utc.from_time THEN utc.from_time ELSE $8 END)";
+            tupleParams = Tuple.of(campaignId, unitId, sensorId, offset, limit, zone.getId(), from, to);
+        } else if (from != null) {
+            whereTimestampClause = "tel.time_stamp >= (CASE WHEN $7 > utc.from_time THEN utc.from_time ELSE $7 END) AND tel.time_stamp <= utc.to_time";
+            tupleParams = Tuple.of(campaignId, unitId, sensorId, offset, limit, zone.getId(), from);
+        } else if (to != null) {
+            whereTimestampClause = "tel.time_stamp >= utc.from_time AND tel.time_stamp <= (CASE WHEN $7 > utc.to_time THEN utc.to_time ELSE $7 END)";
+            tupleParams = Tuple.of(campaignId, unitId, sensorId, 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, sensorId, offset, limit, zone.getId());
+        }
+
+        String sql = "SELECT tel.obs_id, (tel.observed_values::jsonb ->> $3::bigint::text::varchar)::integer AS value, tel.speed, " +
+                        "tel.time_stamp, $6 AS zone_id, " + // ::timestamp with time zone at time zone $6 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 angle " +
+                "FROM maplog.obs_telemetry AS tel " +
+                "JOIN maplog.unit_to_sensor uts on tel.unit_id = uts.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 uts.sensor_id = $3 AND observed_values::jsonb -> $3::bigint::text::varchar IS NOT NULL " +
+                "AND " + whereTimestampClause + " ORDER BY tel.time_stamp OFFSET $4 LIMIT $5";
+
+        return client.preparedQuery(sql)
+                .execute(tupleParams)
+                .map(rs -> StreamSupport.stream(rs.spliterator(), false)
+                        .map(r -> SensorTelemetry.of(
+                                r.getLong("obs_id"),
+                                r.getLong("value"),
+                                r.getOffsetDateTime("time_stamp"),
+                                Location.of(
+                                        r.getFloat("long"),
+                                        r.getFloat("lat"),
+                                        r.getFloat("alt"),
+                                        r.getFloat("angle")),
+                                r.getFloat("speed")))
+                        .collect(Collectors.toList()))
+                .onFailure(logger::catching);
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<SensorTelemetry>>> findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(
+            long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+    ) {
+        return findObservationsByCampaignIdAndUnitIdAndSensorId(campaignId, unitId, sensorId, 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>> findLocationsByCampaignIdAndUnitId(
             long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
     ) {

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

@@ -106,6 +106,16 @@ public class MockMapLogRepository implements SensLogRepository {
     }
 
     @Override
+    public Future<List<SensorTelemetry>> findObservationsByCampaignIdAndUnitIdAndSensorId(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+        return Future.succeededFuture(Collections.emptyList());
+    }
+
+    @Override
+    public Future<PagingRetrieve<List<SensorTelemetry>>> findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+        return Future.succeededFuture(new PagingRetrieve<>(false, 0, Collections.emptyList()));
+    }
+
+    @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());
     }

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

@@ -16,6 +16,7 @@ public interface SensLogRepository {
     Future<Integer> saveTelemetry(UnitTelemetry data);
     Future<Integer> saveAllTelemetry(List<UnitTelemetry> data);
 
+
     Future<Boolean> createSensor(Sensor sensor, long unitId);
 
 
@@ -50,6 +51,9 @@ public interface SensLogRepository {
     Future<List<UnitTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
     Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
 
+    Future<List<SensorTelemetry>> findObservationsByCampaignIdAndUnitIdAndSensorId(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+    Future<PagingRetrieve<List<SensorTelemetry>>> findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(long campaignId, long unitId, long sensorId, 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);
 

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

@@ -63,6 +63,7 @@ public final class HttpVertxServer extends AbstractVerticle {
                     openAPIRouterBuilder.operation("campaignIdUnitIdGET").handler(apiHandler::campaignIdUnitIdGET);
                     openAPIRouterBuilder.operation("campaignIdUnitIdSensorsGET").handler(apiHandler::campaignIdUnitIdSensorsGET);
                     openAPIRouterBuilder.operation("campaignIdUnitIdSensorIdGET").handler(apiHandler::campaignIdUnitIdSensorIdGET);
+                    openAPIRouterBuilder.operation("campaignIdUnitIdSensorIdObservationsGET").handler(apiHandler::campaignIdUnitIdSensorIdObservationsGET);
 
                     openAPIRouterBuilder.operation("unitsGET").handler(apiHandler::unitsGET);
                     openAPIRouterBuilder.operation("unitIdGET").handler(apiHandler::unitIdGET);

+ 79 - 0
src/main/java/cz/senslog/telemetry/server/OpenAPIHandler.java

@@ -725,4 +725,83 @@ public class OpenAPIHandler {
                         )).encode())
                         .onFailure(th -> rc.fail(400, th)));
     }
+
+    public void campaignIdUnitIdSensorIdObservationsGET(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"));
+        long sensorId = Long.parseLong(rc.pathParam("sensorId"));
+
+        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");
+        ZoneId zone = TernaryCondition.<ZoneId>ternaryIf(paramZone::isEmpty, ZoneId.of("UTC"), () -> {
+            paramsJson.put("zone", paramZone.get(0));
+            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> paramNavigationLinks = rc.queryParam("navigationLinks");
+        boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
+            paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
+            return parseBoolean(paramNavigationLinks.get(0));
+        });
+
+        JsonObject navLinks = navigationLinks ? JsonObject.of(
+                "CampaignSensor@NavigationLink", String.format("%s/campaigns/%d/units/%d/sensors/%d", host, campaignId, unitId, sensorId)
+        ) : 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/sensors/%d/observations?offset=%d%s", host, campaignId, unitId, sensorId, newOffset, (urlParams.isEmpty() ? "" : "&"+urlParams));
+        };
+
+        repo.findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(campaignId, unitId, sensorId, 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(o -> JsonObject.of(
+                                        "value", o.getValue(),
+                                        "timestamp", OffsetDateTime.ofInstant(o.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                        "speed", o.getSpeed(),
+                                        "location", JsonObject.of(
+                                                "longitude", o.getLocation().getLongitude(),
+                                                "latitude", o.getLocation().getLatitude(),
+                                                "altitude", o.getLocation().getAltitude())
+                                )).collect(toList()))
+                )).encode()))
+                .onFailure(th -> rc.fail(400, th));
+    }
 }

+ 67 - 13
src/main/resources/openAPISpec.yaml

@@ -178,7 +178,13 @@ paths:
           name: zone
           schema:
             type: string
+            default: UTC
           required: false
+          examples:
+            UTC:
+              value: UTC
+            GMT:
+              value: GMT
         - in: query
           name: offset
           schema:
@@ -252,6 +258,11 @@ paths:
             type: string
             default: UTC
           required: false
+          examples:
+            UTC:
+              value: UTC
+            GMT:
+              value: GMT
         - in: query
           name: sort
           schema:
@@ -366,7 +377,13 @@ paths:
           name: zone
           schema:
             type: string
+            default: UTC
           required: false
+          examples:
+            UTC:
+              value: UTC
+            GMT:
+              value: GMT
         - in: query
           name: offset
           schema:
@@ -438,6 +455,7 @@ paths:
           name: zone
           schema:
             type: string
+            default: UTC
           required: false
           examples:
             UTC:
@@ -577,7 +595,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units/{unitId}/sensors/{sensorId}/observations:
-    get:
+    get: # done
       operationId: campaignIdUnitIdSensorIdObservationsGET
       summary: Publish info about all data of the unit within the campaign
       parameters:
@@ -602,11 +620,27 @@ paths:
           required: true
           description: Numeric ID of the sensor to get
         - in: query
+          name: zone
+          schema:
+            type: string
+            default: UTC
+          required: false
+          examples:
+            UTC:
+              value: UTC
+            GMT:
+              value: GMT
+        - in: query
           name: offset
           schema:
             type: integer
             format: int64
-          description: Offset for the data
+        - in: query
+          name: limit
+          schema:
+            type: integer
+            format: int64
+          required: false
         - in: query
           name: navigationLinks
           schema:
@@ -732,6 +766,11 @@ paths:
             type: string
             default: UTC
           required: false
+          examples:
+            UTC:
+              value: UTC
+            GMT:
+              value: GMT
         - in: query
           name: navigationLinks
           schema:
@@ -1260,31 +1299,34 @@ components:
 
     CampaignUnitSensorObservation:
       type: object
+      required:
+        - params
+        - size
+        - offset
+        - hasNext
+        - data
       properties:
-        Campaign@NavigationLink:
-          type: string
-          format: uri
-        Unit@NavigationLink:
-          type: string
-          format: uri
-        Sensor@NavigationLink:
+        CampaignSensor@NavigationLink:
           type: string
           format: uri
         next@NavigationLink:
           type: string
           format: uri
+        params:
+          type: object
+          description: Used params in URL
         size:
           type: integer
         offset:
           type: integer
+        hasNext:
+          type: boolean
         data:
           type: array
           items:
             $ref: '#/components/schemas/SensorDataObservation'
       example:
-        Campaign@NavigationLink: "<domain>/campaigns/1"
-        Unit@NavigationLink: "<domain>/campaigns/1/units/25"
-        Sensor@NavigationLink: "<domain>/sensors/105"
+        CampaignSensor@NavigationLink: "<domain>/campaigns/1/units/25/sensors/105"
         next@NavigationLink: "<domain>/campaigns/1/units/25/sensors/105/observations?offset=500"
         size: 500
         offset: 0
@@ -1356,6 +1398,8 @@ components:
       required:
         - timestamp
         - value
+        - speed
+        - location
       properties:
         timestamp:
           type: string
@@ -1363,9 +1407,19 @@ components:
         value:
           type: integer
           format: int64
+        speed:
+          type: integer
+          format: int64
+        location:
+          $ref: '#/components/schemas/Location'
       example:
         timestamp: "2023-01-25 15:35:32Z"
-        value: 1435
+        value: 1434
+        speed: 34
+        location:
+          longitude: 49.7384
+          latitude: 13.3736
+          altitude: 350.3
 
     CampaignUnitLocation:
       type: object