Ver código fonte

Added Advanced Filter for Observations and Location endpoints

Lukas Cerny 1 ano atrás
pai
commit
1bb2ec2ce5

+ 47 - 0
src/main/java/cz/senslog/telemetry/database/domain/Filter.java

@@ -0,0 +1,47 @@
+package cz.senslog.telemetry.database.domain;
+
+
+public class Filter {
+
+    private final FilterType type;
+    private final FilterAttribute attribute;
+    private final String attributeValue;
+    private final FilterOperation operation;
+    private final float operationValue;
+
+    public static Filter of(FilterType type, FilterAttribute attribute, String attributeValue, FilterOperation operation, float operationValue) {
+        return new Filter(type, attribute, attributeValue, operation, operationValue);
+    }
+
+    private Filter(FilterType type, FilterAttribute attribute, String attributeValue, FilterOperation operation, float operationValue) {
+        this.type = type;
+        this.attribute = attribute;
+        this.attributeValue = attributeValue;
+        this.operation = operation;
+        this.operationValue = operationValue;
+    }
+
+    public FilterType getType() {
+        return type;
+    }
+
+    public FilterAttribute getAttribute() {
+        return attribute;
+    }
+
+    public FilterOperation getOperation() {
+        return operation;
+    }
+
+    public float getOperationValue() {
+        return operationValue;
+    }
+
+    public String getAttributeValueAsString() {
+        return attributeValue;
+    }
+
+    public long getAttributeValueAsLong() {
+        return Long.parseLong(attributeValue);
+    }
+}

+ 33 - 0
src/main/java/cz/senslog/telemetry/database/domain/FilterAttribute.java

@@ -0,0 +1,33 @@
+package cz.senslog.telemetry.database.domain;
+
+public enum FilterAttribute {
+
+    ID  ("id:[0-9]+"),
+    SPEED,
+    LONGITUDE,
+    LATITUDE,
+    ALTITUDE
+    ;
+    private final String regex;
+
+    FilterAttribute(String regex) {
+        this.regex = regex;
+    }
+    FilterAttribute() {
+        this.regex = this.name().toLowerCase();
+    }
+
+    public String regex() {
+        return this.regex;
+    }
+
+    public static FilterAttribute of(String strAttribute) {
+        String attr = strAttribute.toUpperCase();
+        for (FilterAttribute value : values()) {
+            if (attr.startsWith(value.name())) {
+                return value;
+            }
+        }
+        return null;
+    }
+}

+ 27 - 0
src/main/java/cz/senslog/telemetry/database/domain/FilterOperation.java

@@ -0,0 +1,27 @@
+package cz.senslog.telemetry.database.domain;
+
+public enum FilterOperation {
+    LT ("<"),
+    LE ("<="),
+    EQ ("="),
+    NE ("!="),
+    GE (">="),
+    GT (">");
+
+    private final String dbOperand;
+    FilterOperation(String dbOperand) {
+        this.dbOperand = dbOperand;
+    }
+
+    public String getDbOperand() {
+        return dbOperand;
+    }
+
+    public static FilterOperation of(String operation) {
+        return valueOf(operation.toUpperCase());
+    }
+
+    public String regex() {
+        return this.name().toLowerCase();
+    }
+}

+ 15 - 0
src/main/java/cz/senslog/telemetry/database/domain/FilterType.java

@@ -0,0 +1,15 @@
+package cz.senslog.telemetry.database.domain;
+
+import static cz.senslog.telemetry.database.domain.FilterAttribute.*;
+
+public enum FilterType {
+    SENSOR, UNIT
+    ;
+    public static FilterType of(String type) {
+        return valueOf(type.toUpperCase());
+    }
+
+    public String regex() {
+        return name().toLowerCase();
+    }
+}

+ 222 - 58
src/main/java/cz/senslog/telemetry/database/repository/MapLogRepository.java

@@ -4,6 +4,7 @@ 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 cz.senslog.telemetry.database.domain.Filter;
 import io.vertx.core.Future;
 import io.vertx.pgclient.PgPool;
 import io.vertx.sqlclient.Row;
@@ -22,6 +23,8 @@ import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.StreamSupport;
 
+import static java.lang.String.format;
+import static java.util.Optional.of;
 import static java.util.stream.Collectors.toList;
 
 public class MapLogRepository implements SensLogRepository {
@@ -120,7 +123,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getString("name"),
                                 row.getString("description")
                         ))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
@@ -136,7 +139,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_SIMPLE_UNIT.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Unit IMEI '%s' not found.", imei))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Unit IMEI '%s' not found.", imei))));
     }
 
     private static final Function<Row, Sensor> ROW_TO_SENSOR = (row) -> Sensor.of(
@@ -161,7 +164,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_SENSOR.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Sensor ID '%d' not found.", sensorId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Sensor ID '%d' not found.", sensorId))));
     }
 
     private static final Function<Row, Sensor> ROW_TO_BASIC_SENSOR = (row) -> Sensor.of(
@@ -183,7 +186,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_BASIC_SENSOR.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Sensor for Unit(%d) and IO(%d) not found.", unitId, ioID))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Sensor for Unit(%d) and IO(%d) not found.", unitId, ioID))));
     }
 
     @Override
@@ -198,7 +201,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getInteger("io_id"),
                                 Phenomenon.of(row.getLong("phenomenon_id"), null),
                                 row.getString("description")
-                        )).collect(Collectors.toList())
+                        )).collect(toList())
                 );
     }
 
@@ -214,7 +217,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getLong("sensor_id"),
                                 row.getString("name"),
                                 row.getString("type")
-                        )).collect(Collectors.toList())
+                        )).collect(toList())
                 );
     }
 
@@ -227,7 +230,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getLong("sensor_id"),
                                 row.getString("name"),
                                 row.getString("type")
-                        )).collect(Collectors.toList())
+                        )).collect(toList())
                 );
     }
 
@@ -244,7 +247,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getLong("sensor_id"),
                                 row.getString("name"),
                                 row.getString("type")
-                        )).collect(Collectors.toList())
+                        )).collect(toList())
                 );
     }
 
@@ -260,7 +263,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_SENSOR.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Sensor ID '%d' not found in Campaign(%d) and Unit(%d).", sensorId, campaignId, unitId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Sensor ID '%d' not found in Campaign(%d) and Unit(%d).", sensorId, campaignId, unitId))));
     }
 
     @Override
@@ -271,7 +274,7 @@ public class MapLogRepository implements SensLogRepository {
                         .map(row -> Phenomenon.of(
                                 row.getLong("id"),
                                 row.getString("name")
-                        )).collect(Collectors.toList())
+                        )).collect(toList())
                 );
     }
 
@@ -289,7 +292,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_PHENOMENON.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Phenomenon ID '%d' not found.", phenomenonId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Phenomenon ID '%d' not found.", phenomenonId))));
     }
 
     @Override
@@ -305,7 +308,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getString("description"),
                                 row.getOffsetDateTime("from_time"),
                                 row.getOffsetDateTime("to_time")
-                        )).collect(Collectors.toList())
+                        )).collect(toList())
                 );
     }
 
@@ -333,7 +336,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_DRIVER.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Driver ID '%d' not found.", driverId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Driver ID '%d' not found.", driverId))));
     }
 
     @Override
@@ -392,7 +395,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_ACTION.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Action ID '%d' not found for Driver(%d).", actionId, driverId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Action ID '%d' not found for Driver(%d).", actionId, driverId))));
     }
 
     @Override
@@ -404,7 +407,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_ACTION.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Action ID '%d' not found for Driver(%d) and Unit(%d).", actionId, driverId, unitId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Action ID '%d' not found for Driver(%d) and Unit(%d).", actionId, driverId, unitId))));
     }
 
     @Override
@@ -440,7 +443,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_EVENT.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Event ID '%d' not found.", eventId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Event ID '%d' not found.", eventId))));
     }
 
     @Override
@@ -490,7 +493,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getOffsetDateTime("from_time"),
                                 row.getOffsetDateTime("to_time")
                         ))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
@@ -515,7 +518,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_CAMPAIGN_UNIT.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Unit ID '%d' not found for Campaign(%d).", unitId, campaignId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Unit ID '%d' not found for Campaign(%d).", unitId, campaignId))));
     }
 
     @Override
@@ -528,7 +531,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_UNIT.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Unit ID '%d' not found for Driver(%d).", unitId, driverId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Unit ID '%d' not found for Driver(%d).", unitId, driverId))));
     }
 
     @Override
@@ -541,7 +544,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_UNIT.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Unit ID '%d' not found for Driver(%d) and Action(%d).", unitId, driverId, actionId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Unit ID '%d' not found for Driver(%d) and Action(%d).", unitId, driverId, actionId))));
     }
 
     @Override
@@ -577,7 +580,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getOffsetDateTime("from_time"),
                                 row.getOffsetDateTime("to_time")
                         ))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
@@ -595,7 +598,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(it -> it.hasNext() ? ROW_TO_CAMPAIGN.apply(it.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Campaign ID '%d' not found.", campaignId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Campaign ID '%d' not found.", campaignId))));
     }
 
     public static final Function<Row, Unit> ROW_TO_UNIT = (row) -> Unit.of(
@@ -612,7 +615,7 @@ public class MapLogRepository implements SensLogRepository {
                 .map(RowSet::iterator)
                 .map(iterator -> iterator.hasNext() ? ROW_TO_UNIT.apply(iterator.next()) : null)
                 .map(Optional::ofNullable)
-                .map(p -> p.orElseThrow(() -> new DataNotFoundException(String.format("Unit ID '%d' not found.", unitId))));
+                .map(p -> p.orElseThrow(() -> new DataNotFoundException(format("Unit ID '%d' not found.", unitId))));
     }
 
     @Override
@@ -627,7 +630,7 @@ public class MapLogRepository implements SensLogRepository {
                                 row.getString("name"),
                                 row.getString("imei"),
                                 row.getString("description")
-                        )).collect(Collectors.toList())
+                        )).collect(toList())
                 );
     }
 
@@ -637,13 +640,13 @@ public class MapLogRepository implements SensLogRepository {
                 .execute(Tuple.of(campaignId))
                 .map(rs -> StreamSupport.stream(rs.spliterator(), false)
                         .map(r -> r.getLong("unit_id"))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
     @Override
     public Future<List<UnitTelemetry>> findObservationsByCampaignId(
-            long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
         String whereTimestampClause;
         Tuple tupleParams;
@@ -661,6 +664,29 @@ public class MapLogRepository implements SensLogRepository {
             tupleParams = Tuple.of(campaignId, offset, limit, zone.getId());
         }
 
+        StringBuilder whereFiltersClause = new StringBuilder();
+        for (Filter filter : filters) {
+            String opt = filter.getOperation().getDbOperand();
+            float optValue = filter.getOperationValue();
+            switch (filter.getType()) {
+                case UNIT -> {
+                    switch (filter.getAttribute()) {
+                        case SPEED       -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND tel.speed %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LONGITUDE   -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_X (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LATITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Y (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case ALTITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Z (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                    }
+                }
+                case SENSOR -> {
+                    switch (filter.getAttribute()) {
+                        case ID          -> of(tupleParams.addLong(filter.getAttributeValueAsLong()).addFloat(optValue))
+                                .map(p -> format(" AND (tel.observed_values::jsonb ->> $%d::bigint::text::varchar)::integer %s $%d", p.size()-1, opt, p.size()))
+                                .ifPresent(whereFiltersClause::append);
+                    }
+                }
+            }
+        }
+
         String sql = "SELECT tel.id, tel.unit_id, tel.observed_values::json, tel.speed, " +
                 "tel.time_stamp, $4 AS zone_id, " + // ::timestamp with time zone at time zone $4 AS time_stamp
                 "ST_X (ST_Transform (tel.the_geom, 4326)) AS long, " +
@@ -670,7 +696,7 @@ public class MapLogRepository implements SensLogRepository {
                 "FROM maplog.obs_telemetry AS tel " +
                 "JOIN maplog.unit_to_campaign AS utc ON tel.unit_id = utc.unit_id " +
                 "JOIN maplog.campaign AS c ON c.id = utc.campaign_id " +
-                "WHERE utc.campaign_id = $1 AND " + whereTimestampClause + " " +
+                "WHERE utc.campaign_id = $1 AND " + whereTimestampClause + whereFiltersClause + " " +
                 "ORDER BY tel.time_stamp OFFSET $2 LIMIT $3;";
 
         return client.preparedQuery(sql)
@@ -688,15 +714,15 @@ public class MapLogRepository implements SensLogRepository {
                                 r.getFloat("speed"),
                                 r.getJsonObject("observed_values")
                         ))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
     @Override
     public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdWithPaging(
-            long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
-        return findObservationsByCampaignId(campaignId, from, to, zone, offset, limit+1)
+        return findObservationsByCampaignId(campaignId, from, to, zone, offset, limit+1, filters)
                 .map(data -> {
                     boolean hasNext = data.size() > limit;
                     if (hasNext) {
@@ -708,7 +734,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<UnitTelemetry>> findObservationsByCampaignIdAndUnitId(
-            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
         String whereTimestampClause;
         Tuple tupleParams;
@@ -726,6 +752,29 @@ public class MapLogRepository implements SensLogRepository {
             tupleParams = Tuple.of(campaignId, unitId, offset, limit, zone.getId());
         }
 
+        StringBuilder whereFiltersClause = new StringBuilder();
+        for (Filter filter : filters) {
+            String opt = filter.getOperation().getDbOperand();
+            float optValue = filter.getOperationValue();
+            switch (filter.getType()) {
+                case UNIT -> {
+                    switch (filter.getAttribute()) {
+                        case SPEED       -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND tel.speed %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LONGITUDE   -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_X (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LATITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Y (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case ALTITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Z (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                    }
+                }
+                case SENSOR -> {
+                    switch (filter.getAttribute()) {
+                        case ID          -> of(tupleParams.addLong(filter.getAttributeValueAsLong()).addFloat(optValue))
+                                .map(p -> format(" AND (tel.observed_values::jsonb ->> $%d::bigint::text::varchar)::integer %s $%d", p.size()-1, opt, p.size()))
+                                .ifPresent(whereFiltersClause::append);
+                    }
+                }
+            }
+        }
+
         String sql = "SELECT tel.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, " +
@@ -734,7 +783,7 @@ public class MapLogRepository implements SensLogRepository {
                     "ST_M (tel.the_geom) AS angle " +
                 "FROM maplog.obs_telemetry AS tel " +
                 "JOIN maplog.unit_to_campaign utc on tel.unit_id = utc.unit_id " +
-                "WHERE utc.campaign_id = $1 AND utc.unit_id = $2 AND " + whereTimestampClause + " " +
+                "WHERE utc.campaign_id = $1 AND utc.unit_id = $2 AND " + whereTimestampClause + whereFiltersClause + " " +
                 "ORDER BY tel.time_stamp OFFSET $3 LIMIT $4;";
 
         return client.preparedQuery(sql)
@@ -751,15 +800,15 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("angle")),
                                 r.getFloat("speed"),
                                 r.getJsonObject("observed_values")))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
     @Override
     public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(
-            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
-        return findObservationsByCampaignIdAndUnitId(campaignId, unitId, from, to, zone, offset, limit+1)
+        return findObservationsByCampaignIdAndUnitId(campaignId, unitId, from, to, zone, offset, limit+1, filters)
                 .map(data -> {
                     boolean hasNext = data.size() > limit;
                     if (hasNext) {
@@ -771,7 +820,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<UnitTelemetry>> findObservationsByEventId(
-            long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
         String whereTimestampClause;
         Tuple tupleParams;
@@ -789,6 +838,29 @@ public class MapLogRepository implements SensLogRepository {
             tupleParams = Tuple.of(eventId, offset, limit, zone.getId());
         }
 
+        StringBuilder whereFiltersClause = new StringBuilder();
+        for (Filter filter : filters) {
+            String opt = filter.getOperation().getDbOperand();
+            float optValue = filter.getOperationValue();
+            switch (filter.getType()) {
+                case UNIT -> {
+                    switch (filter.getAttribute()) {
+                        case SPEED       -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND tel.speed %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LONGITUDE   -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_X (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LATITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Y (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case ALTITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Z (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                    }
+                }
+                case SENSOR -> {
+                    switch (filter.getAttribute()) {
+                        case ID          -> of(tupleParams.addLong(filter.getAttributeValueAsLong()).addFloat(optValue))
+                                .map(p -> format(" AND (tel.observed_values::jsonb ->> $%d::bigint::text::varchar)::integer %s $%d", p.size()-1, opt, p.size()))
+                                .ifPresent(whereFiltersClause::append);
+                    }
+                }
+            }
+        }
+
         String sql = "SELECT tel.id, tel.unit_id, tel.observed_values::json, tel.speed, " +
                         "tel.time_stamp, $4 AS zone_id, " + // ::timestamp with time zone at time zone $4 AS time_stamp
                         "ST_X (ST_Transform (tel.the_geom, 4326)) AS long, " +
@@ -797,7 +869,7 @@ public class MapLogRepository implements SensLogRepository {
                         "ST_M (tel.the_geom) AS angle " +
                         "FROM maplog.obs_telemetry AS tel " +
                     "JOIN maplog.driver_to_action dta on tel.unit_id = dta.unit_id " +
-                "WHERE dta.id = $1 AND " + whereTimestampClause + " " +
+                "WHERE dta.id = $1 AND " + whereTimestampClause + whereFiltersClause + " " +
                 "ORDER BY tel.time_stamp OFFSET $2 LIMIT $3";
 
         return client.preparedQuery(sql)
@@ -814,15 +886,15 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("angle")),
                                 r.getFloat("speed"),
                                 r.getJsonObject("observed_values")))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
     @Override
     public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByEventIdWithPaging(
-            long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
-        return findObservationsByEventId(eventId, from, to, zone, offset, limit+1)
+        return findObservationsByEventId(eventId, from, to, zone, offset, limit+1, filters)
                 .map(data -> {
                     boolean hasNext = data.size() > limit;
                     if (hasNext) {
@@ -834,7 +906,7 @@ 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
+            long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
         String whereTimestampClause;
         Tuple tupleParams;
@@ -852,6 +924,29 @@ public class MapLogRepository implements SensLogRepository {
             tupleParams = Tuple.of(campaignId, unitId, sensorId, offset, limit, zone.getId());
         }
 
+        StringBuilder whereFiltersClause = new StringBuilder();
+        for (Filter filter : filters) {
+            String opt = filter.getOperation().getDbOperand();
+            float optValue = filter.getOperationValue();
+            switch (filter.getType()) {
+                case UNIT -> {
+                    switch (filter.getAttribute()) {
+                        case SPEED       -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND tel.speed %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LONGITUDE   -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_X (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LATITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Y (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case ALTITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Z (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                    }
+                }
+                case SENSOR -> {
+                    switch (filter.getAttribute()) {
+                        case ID          -> of(tupleParams.addLong(filter.getAttributeValueAsLong()).addFloat(optValue))
+                                .map(p -> format(" AND (tel.observed_values::jsonb ->> $%d::bigint::text::varchar)::integer %s $%d", p.size()-1, opt, p.size()))
+                                .ifPresent(whereFiltersClause::append);
+                    }
+                }
+            }
+        }
+
         String sql = "SELECT tel.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, " +
@@ -861,8 +956,8 @@ public class MapLogRepository implements SensLogRepository {
                 "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.campaign_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";
+                "WHERE utc.campaign_id = $1 AND utc.unit_id = $2 AND uts.sensor_id = $3 AND observed_values::jsonb -> $3::bigint::text::varchar IS NOT NULL " + whereFiltersClause +
+                " AND " + whereTimestampClause + " ORDER BY tel.time_stamp OFFSET $4 LIMIT $5";
 
         return client.preparedQuery(sql)
                 .execute(tupleParams)
@@ -877,15 +972,15 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt"),
                                         r.getFloat("angle")),
                                 r.getFloat("speed")))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
     @Override
     public Future<PagingRetrieve<List<SensorTelemetry>>> findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(
-            long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
-        return findObservationsByCampaignIdAndUnitIdAndSensorId(campaignId, unitId, sensorId, from, to, zone, offset, limit+1)
+        return findObservationsByCampaignIdAndUnitIdAndSensorId(campaignId, unitId, sensorId, from, to, zone, offset, limit+1, filters)
                 .map(data -> {
                     boolean hasNext = data.size() > limit;
                     if (hasNext) {
@@ -897,7 +992,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<UnitLocation>> findLocationsByCampaignIdAndUnitId(
-            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
         String whereTimestampClause;
         Tuple tupleParams;
@@ -915,6 +1010,29 @@ public class MapLogRepository implements SensLogRepository {
             tupleParams = Tuple.of(campaignId, unitId, offset, limit, zone.getId());
         }
 
+        StringBuilder whereFiltersClause = new StringBuilder();
+        for (Filter filter : filters) {
+            String opt = filter.getOperation().getDbOperand();
+            float optValue = filter.getOperationValue();
+            switch (filter.getType()) {
+                   case UNIT -> {
+                       switch (filter.getAttribute()) {
+                           case SPEED       -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND tel.speed %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                           case LONGITUDE   -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_X (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                           case LATITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Y (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                           case ALTITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Z (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                       }
+                   }
+                case SENSOR -> {
+                       switch (filter.getAttribute()) {
+                            case ID          -> of(tupleParams.addLong(filter.getAttributeValueAsLong()).addFloat(optValue))
+                                    .map(p -> format(" AND (tel.observed_values::jsonb ->> $%d::bigint::text::varchar)::integer %s $%d", p.size()-1, opt, p.size()))
+                                    .ifPresent(whereFiltersClause::append);
+                    }
+                }
+            }
+        }
+
         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, " +
@@ -922,7 +1040,7 @@ public class MapLogRepository implements SensLogRepository {
                     "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.campaign_id = $1 AND utc.unit_id = $2 AND " + whereTimestampClause + " " +
+                    "WHERE utc.campaign_id = $1 AND utc.unit_id = $2 AND " + whereTimestampClause + whereFiltersClause + " " +
                     "ORDER BY tel.time_stamp OFFSET $3 LIMIT $4;";
 
         return client.preparedQuery(sql)
@@ -937,15 +1055,15 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt")
                                 )
                         ))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
     @Override
     public Future<PagingRetrieve<List<UnitLocation>>> findLocationsByCampaignIdAndUnitIdWithPaging(
-            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
-        return findLocationsByCampaignIdAndUnitId(campaignId, unitId, from, to, zone, offset, limit+1)
+        return findLocationsByCampaignIdAndUnitId(campaignId, unitId, from, to, zone, offset, limit+1, filters)
                 .map(data -> {
                     boolean hasNext = data.size() > limit;
                     if (hasNext) {
@@ -957,7 +1075,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<UnitLocation>> findLocationsByEventId(
-            long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
         String whereTimestampClause;
         Tuple tupleParams;
@@ -975,13 +1093,36 @@ public class MapLogRepository implements SensLogRepository {
             tupleParams = Tuple.of(eventId, offset, limit, zone.getId());
         }
 
+        StringBuilder whereFiltersClause = new StringBuilder();
+        for (Filter filter : filters) {
+            String opt = filter.getOperation().getDbOperand();
+            float optValue = filter.getOperationValue();
+            switch (filter.getType()) {
+                case UNIT -> {
+                    switch (filter.getAttribute()) {
+                        case SPEED       -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND tel.speed %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LONGITUDE   -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_X (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LATITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Y (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case ALTITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Z (ST_Transform (tel.the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                    }
+                }
+                case SENSOR -> {
+                    switch (filter.getAttribute()) {
+                        case ID          -> of(tupleParams.addLong(filter.getAttributeValueAsLong()).addFloat(optValue))
+                                .map(p -> format(" AND (tel.observed_values::jsonb ->> $%d::bigint::text::varchar)::integer %s $%d", p.size()-1, opt, p.size()))
+                                .ifPresent(whereFiltersClause::append);
+                    }
+                }
+            }
+        }
+
         String sql = "SELECT tel.unit_id, tel.time_stamp, $4 AS zone_id, " + // ::timestamp with time zone at time zone $4 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.driver_to_action dta on tel.unit_id = dta.unit_id " +
-                    "WHERE dta.id = $1 AND " + whereTimestampClause + " " +
+                    "WHERE dta.id = $1 AND " + whereTimestampClause + whereFiltersClause + " " +
                     "ORDER BY tel.time_stamp OFFSET $2 LIMIT $3";
 
         return client.preparedQuery(sql)
@@ -996,15 +1137,15 @@ public class MapLogRepository implements SensLogRepository {
                                         r.getFloat("alt")
                                 )
                         ))
-                        .collect(Collectors.toList())
+                        .collect(toList())
                 );
     }
 
     @Override
     public Future<PagingRetrieve<List<UnitLocation>>> findLocationsByEventIdWithPaging(
-            long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit
+            long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters
     ) {
-        return findLocationsByEventId(eventId, from, to, zone, offset, limit+1)
+        return findLocationsByEventId(eventId, from, to, zone, offset, limit+1, filters)
                 .map(data -> {
                     boolean hasNext = data.size() > limit;
                     if (hasNext) {
@@ -1016,7 +1157,7 @@ public class MapLogRepository implements SensLogRepository {
 
     @Override
     public Future<List<UnitLocation>> findUnitsLocationsByCampaignId(
-            long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort
+            long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort, List<Filter> filters
     ) {
         String whereTimestampClause;
         Tuple tupleParams;
@@ -1034,6 +1175,29 @@ public class MapLogRepository implements SensLogRepository {
             tupleParams = Tuple.of(campaignId, limitPerUnit, zone.getId());
         }
 
+        StringBuilder whereFiltersClause = new StringBuilder();
+        for (Filter filter : filters) {
+            String opt = filter.getOperation().getDbOperand();
+            float optValue = filter.getOperationValue();
+            switch (filter.getType()) {
+                case UNIT -> {
+                    switch (filter.getAttribute()) {
+                        case SPEED       -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND speed %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LONGITUDE   -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_X (ST_Transform (the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case LATITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Y (ST_Transform (the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                        case ALTITUDE    -> of(tupleParams.addFloat(optValue)).map(p -> format(" AND ST_Z (ST_Transform (the_geom, 4326)) %s $%d", opt, p.size())).ifPresent(whereFiltersClause::append);
+                    }
+                }
+                case SENSOR -> {
+                    switch (filter.getAttribute()) {
+                        case ID          -> of(tupleParams.addLong(filter.getAttributeValueAsLong()).addFloat(optValue))
+                                .map(p -> format(" AND (observed_values::jsonb ->> $%d::bigint::text::varchar)::integer %s $%d", p.size()-1, opt, p.size()))
+                                .ifPresent(whereFiltersClause::append);
+                    }
+                }
+            }
+        }
+
         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, " +
@@ -1041,7 +1205,7 @@ public class MapLogRepository implements SensLogRepository {
                     "ST_M (the_geom) AS angle " +
                 "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";
+                "WHERE rn <= $2" + whereFiltersClause;
 
         return client.preparedQuery(sql)
                 .execute(tupleParams)

+ 14 - 13
src/main/java/cz/senslog/telemetry/database/repository/MockMapLogRepository.java

@@ -3,6 +3,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 cz.senslog.telemetry.database.domain.Filter;
 import io.vertx.core.Future;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -86,62 +87,62 @@ public class MockMapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<List<UnitTelemetry>> findObservationsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<List<UnitTelemetry>> findObservationsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         return Future.succeededFuture(Collections.emptyList());
     }
 
     @Override
-    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdWithPaging(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdWithPaging(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         return Future.succeededFuture(new PagingRetrieve<>(false, 0, Collections.emptyList()));
     }
 
     @Override
-    public Future<List<UnitTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<List<UnitTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         return Future.succeededFuture(Collections.emptyList());
     }
 
     @Override
-    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         return Future.succeededFuture(new PagingRetrieve<>(false, 0, Collections.emptyList()));
     }
 
     @Override
-    public Future<List<UnitTelemetry>> findObservationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<List<UnitTelemetry>> findObservationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         return Future.succeededFuture(Collections.emptyList());
     }
 
     @Override
-    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         return Future.succeededFuture(new PagingRetrieve<>(false, 0, Collections.emptyList()));
     }
 
     @Override
-    public Future<List<SensorTelemetry>> findObservationsByCampaignIdAndUnitIdAndSensorId(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<List<SensorTelemetry>> findObservationsByCampaignIdAndUnitIdAndSensorId(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         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) {
+    public Future<PagingRetrieve<List<SensorTelemetry>>> findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         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) {
+    public Future<List<UnitLocation>> findLocationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         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) {
+    public Future<PagingRetrieve<List<UnitLocation>>> findLocationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         return Future.succeededFuture(new PagingRetrieve<>(false, 0, Collections.emptyList()));
     }
 
     @Override
-    public Future<List<UnitLocation>> findLocationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<List<UnitLocation>> findLocationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         return Future.succeededFuture(Collections.emptyList());
     }
 
     @Override
-    public Future<PagingRetrieve<List<UnitLocation>>> findLocationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit) {
+    public Future<PagingRetrieve<List<UnitLocation>>> findLocationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters) {
         return Future.succeededFuture(new PagingRetrieve<>(false, 0, Collections.emptyList()));
     }
 
@@ -261,7 +262,7 @@ public class MockMapLogRepository implements SensLogRepository {
     }
 
     @Override
-    public Future<List<UnitLocation>> findUnitsLocationsByCampaignId(long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort) {
+    public Future<List<UnitLocation>> findUnitsLocationsByCampaignId(long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort, List<Filter> filters) {
         return Future.succeededFuture(Collections.emptyList());
     }
 }

+ 14 - 13
src/main/java/cz/senslog/telemetry/database/repository/SensLogRepository.java

@@ -3,6 +3,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 cz.senslog.telemetry.database.domain.Filter;
 import io.vertx.core.Future;
 
 import java.time.OffsetDateTime;
@@ -62,23 +63,23 @@ public interface SensLogRepository {
     Future<Event> findEventById(long eventId);
 
     Future<List<Unit>> findUnitsByDriverId(int driverId, OffsetDateTime from, OffsetDateTime to);
-    Future<List<UnitTelemetry>> findObservationsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
-    Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdWithPaging(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+    Future<List<UnitTelemetry>> findObservationsByCampaignId(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
+    Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdWithPaging(long campaignId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
 
-    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<UnitTelemetry>> findObservationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
+    Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
 
-    Future<List<UnitTelemetry>> findObservationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
-    Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+    Future<List<UnitTelemetry>> findObservationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
+    Future<PagingRetrieve<List<UnitTelemetry>>> findObservationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
 
-    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<SensorTelemetry>> findObservationsByCampaignIdAndUnitIdAndSensorId(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
+    Future<PagingRetrieve<List<SensorTelemetry>>> findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(long campaignId, long unitId, long sensorId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
 
-    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<List<UnitLocation>> findLocationsByCampaignIdAndUnitId(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
+    Future<PagingRetrieve<List<UnitLocation>>> findLocationsByCampaignIdAndUnitIdWithPaging(long campaignId, long unitId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
 
-    Future<List<UnitLocation>> findLocationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
-    Future<PagingRetrieve<List<UnitLocation>>> findLocationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit);
+    Future<List<UnitLocation>> findLocationsByEventId(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
+    Future<PagingRetrieve<List<UnitLocation>>> findLocationsByEventIdWithPaging(long eventId, OffsetDateTime from, OffsetDateTime to, ZoneId zone, int offset, int limit, List<Filter> filters);
 
-    Future<List<UnitLocation>> findUnitsLocationsByCampaignId(long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort);
+    Future<List<UnitLocation>> findUnitsLocationsByCampaignId(long campaignId, int limitPerUnit, OffsetDateTime from, OffsetDateTime to, ZoneId zone, SortType sort, List<Filter> filters);
 }

+ 0 - 6
src/main/java/cz/senslog/telemetry/server/HandlerBuilder.java

@@ -1,6 +0,0 @@
-package cz.senslog.telemetry.server;
-
-public final class HandlerBuilder {
-
-
-}

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

@@ -3,14 +3,14 @@ package cz.senslog.telemetry.server;
 import cz.senslog.telemetry.database.ConnectionPool;
 import cz.senslog.telemetry.database.repository.MapLogRepository;
 import cz.senslog.telemetry.database.repository.SensLogRepository;
+import cz.senslog.telemetry.server.ws.ExceptionHandler;
+import cz.senslog.telemetry.server.ws.OpenAPIHandler;
 import cz.senslog.telemetry.utils.ResourcesUtils;
 import io.vertx.core.AbstractVerticle;
 import io.vertx.core.Promise;
 import io.vertx.core.http.HttpMethod;
 import io.vertx.core.json.JsonObject;
-import io.vertx.ext.auth.authentication.AuthenticationProvider;
 import io.vertx.ext.web.Router;
-import io.vertx.ext.web.handler.APIKeyHandler;
 import io.vertx.ext.web.handler.CorsHandler;
 import io.vertx.ext.web.handler.LoggerFormat;
 import io.vertx.ext.web.handler.LoggerHandler;

+ 1 - 1
src/main/java/cz/senslog/telemetry/server/ExceptionHandler.java → src/main/java/cz/senslog/telemetry/server/ws/ExceptionHandler.java

@@ -1,4 +1,4 @@
-package cz.senslog.telemetry.server;
+package cz.senslog.telemetry.server.ws;
 
 import cz.senslog.telemetry.database.DataNotFoundException;
 import io.vertx.core.json.JsonObject;

+ 54 - 0
src/main/java/cz/senslog/telemetry/server/ws/FilterParser.java

@@ -0,0 +1,54 @@
+package cz.senslog.telemetry.server.ws;
+
+import cz.senslog.telemetry.database.domain.Filter;
+import cz.senslog.telemetry.database.domain.FilterAttribute;
+import cz.senslog.telemetry.database.domain.FilterOperation;
+import cz.senslog.telemetry.database.domain.FilterType;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.lang.String.format;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.joining;
+
+public final class FilterParser {
+
+    private static final Pattern PATTERN;
+
+    static {
+        String types = stream(FilterType.values()).map(FilterType::regex).collect(joining("|"));
+        String attributes = stream(FilterAttribute.values()).map(FilterAttribute::regex).collect(joining("|"));
+        String operations = stream(FilterOperation.values()).map(FilterOperation::regex).collect(joining("|"));
+        String regex = format("^(?<type>%s)\\((?<attr>%s)\\)(?<opr>%s)(?<val>[+-]?[0-9]*[.]?[0-9]++)", types, attributes, operations);
+        PATTERN = Pattern.compile(regex, Pattern.MULTILINE | Pattern.CASE_INSENSITIVE);
+    }
+
+    public static List<Filter> parse(List<String> stringFilters) {
+        if (stringFilters == null || stringFilters.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Filter> filters = new ArrayList<>(stringFilters.size());
+        Matcher matcher = PATTERN.matcher(String.join("\n", stringFilters));
+        while (matcher.find()) {
+            FilterType type = FilterType.of(matcher.group("type"));
+            String fullAttribute = matcher.group("attr");
+            FilterAttribute attribute = FilterAttribute.of(fullAttribute);
+            FilterOperation opr = FilterOperation.of(matcher.group("opr"));
+            float val = Float.parseFloat(matcher.group("val"));
+
+            String attributeValue;
+            if (attribute == FilterAttribute.ID) {
+                attributeValue = fullAttribute.split(":")[1];
+            } else {
+                attributeValue = fullAttribute;
+            }
+
+            filters.add(Filter.of(type, attribute, attributeValue, opr, val));
+        }
+        return filters;
+    }
+}

+ 81 - 43
src/main/java/cz/senslog/telemetry/server/OpenAPIHandler.java → src/main/java/cz/senslog/telemetry/server/ws/OpenAPIHandler.java

@@ -1,9 +1,10 @@
-package cz.senslog.telemetry.server;
+package cz.senslog.telemetry.server.ws;
 
 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.database.domain.Filter;
 import cz.senslog.telemetry.utils.TernaryCondition;
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.json.JsonArray;
@@ -13,13 +14,16 @@ import io.vertx.ext.web.RoutingContext;
 
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
+import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.*;
 import java.util.stream.Collectors;
 
 import static cz.senslog.telemetry.utils.FluentInvoke.of;
 import static java.lang.Boolean.parseBoolean;
 import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
+import static java.util.Collections.emptyList;
 import static java.util.Optional.ofNullable;
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.toList;
@@ -183,6 +187,12 @@ public class OpenAPIHandler {
            return ResponseFormat.of(paramFormat.get(0));
         });
 
+        List<String> paramFilters = rc.queryParam("filter");
+        List<Filter> filters = TernaryCondition.<List<Filter>>ternaryIf(paramFilters::isEmpty, emptyList(), () -> {
+            paramsJson.put("filter", new JsonArray(paramFilters));
+            return FilterParser.parse(paramFilters);
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -203,7 +213,7 @@ public class OpenAPIHandler {
         };
 
         switch (format) {
-            case JSON ->  repo.findObservationsByCampaignIdWithPaging(campaignId, from, to, zone, offset, limit)
+            case JSON ->  repo.findObservationsByCampaignIdWithPaging(campaignId, from, to, zone, offset, limit, filters)
                 .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
                         "next@NavigationLink", createNextNavLink.apply(paging.size())
                 ) : JsonObject.of()).mergeIn(JsonObject.of(
@@ -223,7 +233,7 @@ public class OpenAPIHandler {
                                         "observedValues", o.getObservedValues()
                                 )).collect(toList())))).encode()))
                 .onFailure(rc::fail);
-            case GEOJSON -> repo.findObservationsByCampaignIdWithPaging(campaignId, from, to, zone, offset, limit)
+            case GEOJSON -> repo.findObservationsByCampaignIdWithPaging(campaignId, from, to, zone, offset, limit, filters)
                     .onSuccess(paging -> rc.response().end(JsonObject.of(
                             "type", "FeatureCollection",
                             "metadata", JsonObject.of(
@@ -296,6 +306,12 @@ public class OpenAPIHandler {
             return ResponseFormat.of(paramFormat.get(0));
         });
 
+        List<String> paramFilters = rc.queryParam("filter");
+        List<Filter> filters = TernaryCondition.<List<Filter>>ternaryIf(paramFilters::isEmpty, Collections.emptyList(), () -> {
+            paramsJson.put("filter", new JsonArray(paramFilters));
+            return FilterParser.parse(paramFilters);
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -317,7 +333,7 @@ public class OpenAPIHandler {
 
         switch (format) {
             case JSON ->
-                    repo.findObservationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
+                    repo.findObservationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit, filters)
                             .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
                                     "next@NavigationLink", createNextNavLink.apply(paging.size())
                             ) : JsonObject.of()).mergeIn(JsonObject.of(
@@ -337,7 +353,7 @@ public class OpenAPIHandler {
                                             )).collect(toList())))).encode()))
                             .onFailure(rc::fail);
             case GEOJSON ->
-                    repo.findObservationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
+                    repo.findObservationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit, filters)
                             .onSuccess(paging -> rc.response().end(JsonObject.of(
                                     "type", "FeatureCollection",
                                     "metadata", JsonObject.of(
@@ -408,6 +424,12 @@ public class OpenAPIHandler {
             return ResponseFormat.of(paramFormat.get(0));
         });
 
+        List<String> paramFilters = rc.queryParam("filter");
+        List<Filter> filters = TernaryCondition.<List<Filter>>ternaryIf(paramFilters::isEmpty, Collections.emptyList(), () -> {
+            paramsJson.put("filter", new JsonArray(paramFilters));
+            return FilterParser.parse(paramFilters);
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -419,21 +441,20 @@ public class OpenAPIHandler {
         ) : JsonObject.of();
 
         switch (format) {
-            case JSON ->  repo.findUnitsLocationsByCampaignId(campaignId, limitPerUnit, from, to, zone, sortType)
+            case JSON ->  repo.findUnitsLocationsByCampaignId(campaignId, limitPerUnit, from, to, zone, sortType, filters)
                     .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", JsonArray.of(
-                                            l.getLocation().getLongitude(),
-                                            l.getLocation().getLatitude(),
-                                            l.getLocation().getAltitude()
-                                    )
+                                    "location", JsonObject.of(
+                                            "longitude", l.getLocation().getLongitude(),
+                                            "latitude", l.getLocation().getLatitude(),
+                                            "altitude", l.getLocation().getAltitude())
                             )).collect(toList())))).encode()))
                     .onFailure(rc::fail);
-            case GEOJSON -> repo.findUnitsLocationsByCampaignId(campaignId, limitPerUnit, from, to, zone, sortType)
+            case GEOJSON -> repo.findUnitsLocationsByCampaignId(campaignId, limitPerUnit, from, to, zone, sortType, filters)
                     .onSuccess(data -> of(data.stream().collect(groupingBy(UnitLocation::getUnitId))).then(unitLocation -> rc.response().end(JsonObject.of(
                             "type", "FeatureCollection",
                             "metadata", JsonObject.of(
@@ -500,12 +521,11 @@ public class OpenAPIHandler {
             return ResponseFormat.of(paramFormat.get(0));
         });
 
-        List<String> filter = rc.queryParam("filter");
-        // TODO implement filter
-        /*
-            filtr=sensorId(10003)gt10
-            filtr=driverId(34245)eq3
-         */
+        List<String> paramFilters = rc.queryParam("filter");
+        List<Filter> filters = TernaryCondition.<List<Filter>>ternaryIf(paramFilters::isEmpty, Collections.emptyList(), () -> {
+            paramsJson.put("filter", new JsonArray(paramFilters));
+            return FilterParser.parse(paramFilters);
+        });
 
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
@@ -528,7 +548,7 @@ public class OpenAPIHandler {
         };
 
         switch (format) {
-            case JSON ->  repo.findLocationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
+            case JSON ->  repo.findLocationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit, filters)
                     .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
                             "next@NavigationLink", createNextNavLink.apply(paging.size())
                     ) : JsonObject.of()).mergeIn(JsonObject.of(
@@ -539,14 +559,13 @@ public class OpenAPIHandler {
                             "data", new JsonArray(
                                     paging.data().stream().map(l -> JsonObject.of(
                                             "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
-                                            "location", JsonArray.of(
-                                                    l.getLocation().getLongitude(),
-                                                    l.getLocation().getLatitude(),
-                                                    l.getLocation().getAltitude()
-                                            )
+                                            "location", JsonObject.of(
+                                                    "longitude", l.getLocation().getLongitude(),
+                                                    "latitude", l.getLocation().getLatitude(),
+                                                    "altitude", l.getLocation().getAltitude())
                                     )).collect(toList())))).encode()))
                     .onFailure(rc::fail);
-            case GEOJSON -> repo.findLocationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit)
+            case GEOJSON -> repo.findLocationsByCampaignIdAndUnitIdWithPaging(campaignId, unitId, from, to, zone, offset, limit, filters)
                     .onSuccess(paging -> rc.response().end(JsonObject.of(
                                 "type", "Feature",
                                 "metadata", JsonObject.of(
@@ -898,6 +917,12 @@ public class OpenAPIHandler {
             return ResponseFormat.of(paramFormat.get(0));
         });
 
+        List<String> paramFilters = rc.queryParam("filter");
+        List<Filter> filters = TernaryCondition.<List<Filter>>ternaryIf(paramFilters::isEmpty, Collections.emptyList(), () -> {
+            paramsJson.put("filter", new JsonArray(paramFilters));
+            return FilterParser.parse(paramFilters);
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -918,7 +943,7 @@ public class OpenAPIHandler {
         };
 
         switch (format) {
-            case JSON -> repo.findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(campaignId, unitId, sensorId, from, to, zone, offset, limit)
+            case JSON -> repo.findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(campaignId, unitId, sensorId, from, to, zone, offset, limit, filters)
                     .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
                             "next@NavigationLink", createNextNavLink.apply(paging.size())
                     ) : JsonObject.of()).mergeIn(JsonObject.of(
@@ -927,17 +952,17 @@ public class OpenAPIHandler {
                             "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(),
+                                    paging.data().stream().map(l -> JsonObject.of(
+                                            "observed_value", l.getValue(),
+                                            "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
+                                            "speed", l.getSpeed(),
                                             "location", JsonObject.of(
-                                                    "longitude", o.getLocation().getLongitude(),
-                                                    "latitude", o.getLocation().getLatitude(),
-                                                    "altitude", o.getLocation().getAltitude())
+                                                    "longitude", l.getLocation().getLongitude(),
+                                                    "latitude", l.getLocation().getLatitude(),
+                                                    "altitude", l.getLocation().getAltitude())
                                     )).collect(toList())))).encode()))
                     .onFailure(rc::fail);
-            case GEOJSON -> repo.findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(campaignId, unitId, sensorId, from, to, zone, offset, limit)
+            case GEOJSON -> repo.findObservationsByCampaignIdAndUnitIdAndSensorIdWithPaging(campaignId, unitId, sensorId, from, to, zone, offset, limit, filters)
                     .onSuccess(paging -> rc.response().end(JsonObject.of(
                             "type", "FeatureCollection",
                             "metadata", JsonObject.of(
@@ -1080,6 +1105,8 @@ public class OpenAPIHandler {
 
         int driverId = Integer.parseInt(rc.pathParam("driverId"));
 
+        // TODO add from & to
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = paramNavigationLinks.isEmpty() ? DEFAULT_NAVIGATION_LINKS : parseBoolean(paramNavigationLinks.get(0));
 
@@ -1281,6 +1308,12 @@ public class OpenAPIHandler {
             return ResponseFormat.of(paramFormat.get(0));
         });
 
+        List<String> paramFilters = rc.queryParam("filter");
+        List<Filter> filters = TernaryCondition.<List<Filter>>ternaryIf(paramFilters::isEmpty, Collections.emptyList(), () -> {
+            paramsJson.put("filter", new JsonArray(paramFilters));
+            return FilterParser.parse(paramFilters);
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -1301,7 +1334,7 @@ public class OpenAPIHandler {
         };
 
         switch (format) {
-            case JSON -> repo.findObservationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
+            case JSON -> repo.findObservationsByEventIdWithPaging(eventId, from, to, zone, offset, limit, filters)
                     .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
                             "next@NavigationLink", createNextNavLink.apply(paging.size())
                     ) : JsonObject.of()).mergeIn(JsonObject.of(
@@ -1320,7 +1353,7 @@ public class OpenAPIHandler {
                                             "observedValues", o.getObservedValues()
                                     )).collect(toList())))).encode()))
                     .onFailure(rc::fail);
-            case GEOJSON -> repo.findObservationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
+            case GEOJSON -> repo.findObservationsByEventIdWithPaging(eventId, from, to, zone, offset, limit, filters)
                     .onSuccess(paging -> rc.response().end(JsonObject.of(
                             "type", "FeatureCollection",
                             "metadata", JsonObject.of(
@@ -1392,6 +1425,12 @@ public class OpenAPIHandler {
             return ResponseFormat.of(paramFormat.get(0));
         });
 
+        List<String> paramFilters = rc.queryParam("filter");
+        List<Filter> filters = TernaryCondition.<List<Filter>>ternaryIf(paramFilters::isEmpty, emptyList(), () -> {
+            paramsJson.put("filter", new JsonArray(paramFilters));
+           return FilterParser.parse(paramFilters);
+        });
+
         List<String> paramNavigationLinks = rc.queryParam("navigationLinks");
         boolean navigationLinks = TernaryCondition.<Boolean>ternaryIf(paramNavigationLinks::isEmpty, true, () -> {
             paramsJson.put("navigationLinks", paramNavigationLinks.get(0));
@@ -1412,7 +1451,7 @@ public class OpenAPIHandler {
         };
 
         switch (format) {
-            case JSON -> repo.findLocationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
+            case JSON -> repo.findLocationsByEventIdWithPaging(eventId, from, to, zone, offset, limit, filters)
                     .onSuccess(paging -> rc.response().end(navLinks.mergeIn(navigationLinks && paging.hasNext() ? JsonObject.of(
                             "next@NavigationLink", createNextNavLink.apply(paging.size())
                     ) : JsonObject.of()).mergeIn(JsonObject.of(
@@ -1423,14 +1462,13 @@ public class OpenAPIHandler {
                             "data", new JsonArray(
                                     paging.data().stream().map(l -> JsonObject.of(
                                             "timestamp", OffsetDateTime.ofInstant(l.getTimestamp().toInstant(), zone).format(ISO_OFFSET_DATE_TIME),
-                                            "location", JsonArray.of(
-                                                    l.getLocation().getLongitude(),
-                                                    l.getLocation().getLatitude(),
-                                                    l.getLocation().getAltitude()
-                                            )
+                                            "location", JsonObject.of(
+                                                    "longitude", l.getLocation().getLongitude(),
+                                                    "latitude", l.getLocation().getLatitude(),
+                                                    "altitude", l.getLocation().getAltitude())
                                     )).collect(toList())))).encode()))
                     .onFailure(rc::fail);
-            case GEOJSON -> repo.findLocationsByEventIdWithPaging(eventId, from, to, zone, offset, limit)
+            case GEOJSON -> repo.findLocationsByEventIdWithPaging(eventId, from, to, zone, offset, limit, filters)
                     .onSuccess(paging -> rc.response().end(JsonObject.of(
                             "type", "Feature",
                             "metadata", JsonObject.of(

+ 1 - 1
src/main/java/cz/senslog/telemetry/server/ParamParseException.java → src/main/java/cz/senslog/telemetry/server/ws/ParamParseException.java

@@ -1,4 +1,4 @@
-package cz.senslog.telemetry.server;
+package cz.senslog.telemetry.server.ws;
 
 public class ParamParseException extends IllegalArgumentException {
 

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

@@ -7,7 +7,7 @@ servers:
   - url: https://theros.wirelessinfo.cz
 paths:
   /info:
-    get: # done
+    get:
       operationId: infoGET
       summary: Information about running instance
       responses:
@@ -25,7 +25,7 @@ paths:
                 $ref: "#/components/schemas/Error"
 
   /campaigns:
-    get: # done
+    get:
       operationId: campaignsGET
       summary: Publish info about all campaigns
       parameters:
@@ -48,7 +48,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}:
-    get: # done
+    get:
       operationId: campaignIdGET
       summary: Publish info about a campaign
       parameters:
@@ -70,7 +70,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units:
-    get: # done
+    get:
       operationId: campaignIdUnitsGET
       summary: Publish info about the campaign's units
       parameters:
@@ -94,7 +94,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units/observations:
-    get: # done
+    get:
       operationId: campaignIdUnitsObservationsGET
       summary: Publish info about all data of units merged together within the campaign
       parameters:
@@ -104,8 +104,9 @@ paths:
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
-        - $ref: '#/components/parameters/navigationLinksParam'
         - $ref: '#/components/parameters/formatParam'
+        - $ref: '#/components/parameters/filterParam'
+        - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
           description: JSON containing stream of telemetry data
@@ -121,7 +122,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units/observations/locations:
-    get: # done
+    get:
       operationId: campaignIdUnitsObservationsLocationsGET
       summary: Publish info about all data of units merged together within the campaign
       parameters:
@@ -132,6 +133,7 @@ paths:
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/sortParam'
         - $ref: '#/components/parameters/formatParam'
+        - $ref: '#/components/parameters/filterParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -148,7 +150,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units/{unitId}:
-    get: # done
+    get:
       operationId: campaignIdUnitIdGET
       summary: Publish info about the unit within its campaign's scope
       parameters:
@@ -171,7 +173,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units/{unitId}/observations:
-    get: # done
+    get:
       operationId: campaignIdUnitIdObservationsGET
       summary: Publish info about all data of the unit within the campaign
       parameters:
@@ -183,6 +185,7 @@ paths:
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
         - $ref: '#/components/parameters/formatParam'
+        - $ref: '#/components/parameters/filterParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -199,7 +202,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units/{unitId}/observations/locations:
-    get: # done
+    get:
       operationId: campaignIdUnitIdLocationsGET
       summary: Publish locations of the unit within the campaign
       parameters:
@@ -228,7 +231,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units/{unitId}/sensors:
-    get: # done
+    get:
       operationId: campaignIdUnitIdSensorsGET
       summary: Publish info about all sensors of the unit within the campaign
       parameters:
@@ -252,7 +255,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units/{unitId}/sensors/{sensorId}:
-    get: # done
+    get:
       operationId: campaignIdUnitIdSensorIdGET
       summary: Publish info about all sensors asociated with the unit and the campaign
       parameters:
@@ -275,17 +278,20 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /campaigns/{campaignId}/units/{unitId}/sensors/{sensorId}/observations:
-    get: # done // DODO fromParam and toParam are missing?
+    get:
       operationId: campaignIdUnitIdSensorIdObservationsGET
       summary: Publish info about all data of the unit within the campaign
       parameters:
         - $ref: '#/components/parameters/campaignIdParam'
         - $ref: '#/components/parameters/unitIdParam'
         - $ref: '#/components/parameters/sensorIdParam'
+        - $ref: '#/components/parameters/fromParam'
+        - $ref: '#/components/parameters/toParam'
         - $ref: '#/components/parameters/zoneParam'
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
         - $ref: '#/components/parameters/formatParam'
+        - $ref: '#/components/parameters/filterParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -302,7 +308,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /units:
-    get: # done
+    get:
       operationId: unitsGET
       summary: Publish info about all units
       parameters:
@@ -324,7 +330,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /units/{unitId}:
-    get: # done
+    get:
       operationId: unitIdGET
       summary: Publish info about the unit
       parameters:
@@ -345,7 +351,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /units/{unitId}/sensors:
-    get: # done
+    get:
       operationId: unitIdSensorsGET
       summary: Publish info about sensors assigned to the unit
       parameters:
@@ -368,7 +374,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /units/{unitId}/campaigns:
-    get: # done
+    get:
       operationId: unitIdCampaignsGET
       summary: Publish info about campaigns where the unit was/is assigned
       parameters:
@@ -392,7 +398,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /units/{unitId}/drivers:
-    get: # done
+    get:
       operationId: unitIdDriversGET
       summary: Publish basic info about drivers who performed actions upon the unit
       parameters:
@@ -415,7 +421,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /sensors:
-    get: # done
+    get:
       operationId: sensorsGET
       summary: Publish info about all sensors
       parameters:
@@ -437,7 +443,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /sensors/{sensorId}:
-    get: # done
+    get:
       operationId: sensorIdGET
       summary: Publish info about the sensor
       parameters:
@@ -458,7 +464,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /sensors/{sensorId}/units:
-    get: # done
+    get:
       operationId: sensorIdUnitsGET
       summary: Publish info about units to whom the sensor is assigned
       parameters:
@@ -482,7 +488,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /phenomenons:
-    get: # done
+    get:
       operationId: phenomenonsGET
       summary: Publish info about all phenomenons
       parameters:
@@ -504,7 +510,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /phenomenons/{phenomenonId}:
-    get: # done
+    get:
       operationId: phenomenonIdGET
       summary: Publish info about the phenomenon
       parameters:
@@ -525,7 +531,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /phenomenons/{phenomenonId}/sensors:
-    get: # done
+    get:
       operationId: phenomenonIdSensorsGET
       summary: Publish info about sensors of the phenomenon
       parameters:
@@ -548,7 +554,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers:
-    get: # done
+    get:
       operationId: driversGET
       summary: Publish basic info about all drivers
       parameters:
@@ -570,7 +576,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}:
-    get: # done
+    get:
       operationId: driverIdGET
       summary: Publish detailed info about the driver
       parameters:
@@ -591,7 +597,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}/units:
-    get: # done
+    get:
       operationId: driverIdUnitsGET
       summary: Publish basic info about driver's units
       parameters:
@@ -616,7 +622,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}/units/{unitId}:
-    get: # done
+    get:
       operationId: driverIdUnitIdGET
       summary: Publish detailed info about driver's unit
       parameters:
@@ -638,7 +644,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}/units/{unitId}/actions:
-    get: # done
+    get:
       operationId: driverIdUnitIdActionsGET
       summary: Publish basic info actions performed on the unit by the driver
       parameters:
@@ -662,7 +668,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}/actions:
-    get: # done
+    get: # TODO add from & to
       operationId: driverIdActionsGET
       summary: Publish basic info about driver's actions
       parameters:
@@ -685,7 +691,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}/actions/{actionId}:
-    get: # done
+    get:
       operationId: driverIdActionIdGET
       summary: Publish detailed info about the driver's action
       parameters:
@@ -707,7 +713,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}/actions/{actionId}/units:
-    get: # done
+    get:
       operationId: driverIdActionIdUnitsGET
       summary: Publish basic info about units on which the driver performed its action
       parameters:
@@ -731,7 +737,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}/actions/{actionId}/units/{unitId}:
-    get: # done
+    get:
       operationId: driverIdActionIdUnitIdGET
       summary: Publish detail info about the unit on which the driver performed the action
       parameters:
@@ -754,7 +760,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}/units/{unitId}/actions/{actionId}:
-    get: # done
+    get:
       operationId: driverIdUnitIdActionIdGET
       summary: Publish detailed info about the action performed on the unit by the driver
       parameters:
@@ -777,7 +783,7 @@ paths:
                 $ref: '#/components/schemas/Error'
 
   /drivers/{driverId}/units/{unitId}/actions/{actionId}/events:
-    get: # done
+    get:
       operationId: driverIdUnitIdActionIdEventsGET
       summary: Publish basic info about events that where performed on the unit byt the driver with the specific action
       parameters:
@@ -804,7 +810,7 @@ paths:
 
 
   /events/{eventId}:
-    get: # done
+    get:
       operationId: eventIdGET
       summary: Publish basic info about events that where performed on the unit byt the driver with the specific action
       parameters:
@@ -835,6 +841,7 @@ paths:
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
         - $ref: '#/components/parameters/formatParam'
+        - $ref: '#/components/parameters/filterParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -862,6 +869,7 @@ paths:
         - $ref: '#/components/parameters/offsetParam'
         - $ref: '#/components/parameters/limitParam'
         - $ref: '#/components/parameters/formatParam'
+        - $ref: '#/components/parameters/filterParam'
         - $ref: '#/components/parameters/navigationLinksParam'
       responses:
         200:
@@ -1017,16 +1025,21 @@ components:
     filterParam:
       in: query
       name: filter
+      description: Filter for results following the pattern
+        <unit | sensor>(<numeric_float_id | speed | longitude | latitude | altitude>)<lt | le | eq | ne | ge | lt><float_value>, e.g,
       schema:
         type: string
       required: false
       examples:
-        Value of Sensor ID 105 > 10:
-          value: sensorId(105)gt10
-          summary: Filter locations of units having value of sensor id 105 > 10
-        Value:
-          value: driverId(34245)eq3
-          summary: Filter locations of units driven by Driver ID 34245 == Activity 3
+        Units latitude:
+          value: unit(latitude)lt50.1
+          summary: Returns all units with its latitude coordination lower than (LT) 50.1
+        Units speed:
+          value: unit(speed)gt90.0
+          summary: Returns all units with its speed greater than 90.0 Km/h
+        Sensor ID value:
+          value: sensor(105)gt10
+          summary: Returns sensors having its value greater than 10
     formatParam:
       in: query
       name: format
@@ -1611,7 +1624,7 @@ components:
         timestamp:
           type: string
           format: date-time
-        value:
+        observed_value:
           type: integer
           format: int64
         speed:
@@ -1621,7 +1634,7 @@ components:
           $ref: '#/components/schemas/Location'
       example:
         timestamp: "2023-01-25 15:35:32Z"
-        value: 1434
+        observed_value: 1434
         speed: 34
         location:
           longitude: 49.7384
@@ -1678,10 +1691,7 @@ components:
                 type: string
                 format: date-time
               location:
-                description: Array in a format [longitude, latitude, altitude]
-                type: array
-                items:
-                  type: integer
+                $ref: '#/components/schemas/Location'
       example:
         Campaign@NavigationLink: "<domain>/campaigns/1"
         Unit@NavigationLink: "<domain>/campaigns/1/units/25"
@@ -1695,7 +1705,10 @@ components:
         offset: 0
         data:
           - timestamp: "2023-01-25 15:35:32Z"
-            location: [49.7384, 13.3736, 350.3]
+            location:
+              longitude: 49.7384
+              latitude: 13.3736
+              altitude: 350.3
 
     CampaignUnitsLocations:
       type: object
@@ -1732,10 +1745,7 @@ components:
                 type: string
                 format: date-time
               location:
-                description: Array in a format [longitude, latitude, altitude]
-                type: array
-                items:
-                  type: integer
+                $ref: '#/components/schemas/Location'
       example:
         Campaign@NavigationLink: "<domain>/campaigns/1"
         params:
@@ -1746,7 +1756,10 @@ components:
         data:
           - unitId: 25
             timestamp: "2023-01-25 15:35:32Z"
-            location: [ 49.7384, 13.3736, 350.3 ]
+            location:
+              longitude: 49.7384
+              latitude: 13.3736
+              altitude: 350.3
 
     ActionEventLocation:
       type: object
@@ -1790,10 +1803,7 @@ components:
                 type: string
                 format: date-time
               location:
-                description: Array in a format [longitude, latitude, altitude]
-                type: array
-                items:
-                  type: integer
+                $ref: '#/components/schemas/Location'
       example:
         Event@NavigationLink: "<domain>/events/999"
         next@NavigationLink: "<domain>/events/999/observations/locations?offset=500"
@@ -1806,7 +1816,10 @@ components:
         offset: 0
         data:
           - timestamp: "2023-01-25 15:35:32Z"
-            location: [49.7384, 13.3736, 350.3]
+            location:
+              longitude: 49.7384
+              latitude: 13.3736
+              altitude: 350.3
 
 
     Location:

+ 100 - 0
src/test/java/cz/senslog/telemetry/server/ws/FilterParserTest.java

@@ -0,0 +1,100 @@
+package cz.senslog.telemetry.server.ws;
+
+import cz.senslog.telemetry.database.domain.Filter;
+import cz.senslog.telemetry.database.domain.FilterAttribute;
+import cz.senslog.telemetry.database.domain.FilterOperation;
+import cz.senslog.telemetry.database.domain.FilterType;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class FilterParserTest {
+
+    @Test
+    void success_sensor_lowerCase() {
+
+        List<String> strings = List.of(
+                "sensor(id:360200000)gt10.24",
+                "sensor(id:360400000)lt0"
+        );
+
+        List<Filter> filters = FilterParser.parse(strings);
+
+        assertEquals(2, filters.size());
+
+        assertEquals(FilterType.SENSOR, filters.get(0).getType());
+        assertEquals(FilterAttribute.ID, filters.get(0).getAttribute());
+        assertEquals(360200000L, filters.get(0).getAttributeValueAsLong());
+        assertEquals(FilterOperation.GT, filters.get(0).getOperation());
+        assertEquals(10.24f, filters.get(0).getOperationValue());
+
+        assertEquals(FilterType.SENSOR, filters.get(1).getType());
+        assertEquals(FilterAttribute.ID, filters.get(1).getAttribute());
+        assertEquals(360400000L, filters.get(1).getAttributeValueAsLong());
+        assertEquals(FilterOperation.LT, filters.get(1).getOperation());
+        assertEquals(0f, filters.get(1).getOperationValue());
+    }
+
+    @Test
+    void success_sensor_upperCase() {
+
+        List<String> strings = List.of(
+                "sensor(id:360200000)GT10.24",
+                "sensor(id:360400000)LT0"
+        );
+
+        List<Filter> filters = FilterParser.parse(strings);
+
+        assertEquals(2, filters.size());
+
+        assertEquals(FilterType.SENSOR, filters.get(0).getType());
+        assertEquals(FilterAttribute.ID, filters.get(0).getAttribute());
+        assertEquals(360200000L, filters.get(0).getAttributeValueAsLong());
+        assertEquals(FilterOperation.GT, filters.get(0).getOperation());
+        assertEquals(10.24f, filters.get(0).getOperationValue());
+
+        assertEquals(FilterType.SENSOR, filters.get(1).getType());
+        assertEquals(FilterAttribute.ID, filters.get(1).getAttribute());
+        assertEquals(360400000L, filters.get(1).getAttributeValueAsLong());
+        assertEquals(FilterOperation.LT, filters.get(1).getOperation());
+        assertEquals(0f, filters.get(1).getOperationValue());
+    }
+
+    @Test
+    void success_sensor_returnOneOfTwo() {
+
+        List<String> strings = List.of(
+                "sensor(id:360200000)gt10.24",
+                "sensor(id:360400000)XX0"
+        );
+
+        List<Filter> filters = FilterParser.parse(strings);
+
+        assertEquals(1, filters.size());
+
+        assertEquals(FilterType.SENSOR, filters.get(0).getType());
+        assertEquals(FilterAttribute.ID, filters.get(0).getAttribute());
+        assertEquals(360200000L, filters.get(0).getAttributeValueAsLong());
+        assertEquals(FilterOperation.GT, filters.get(0).getOperation());
+        assertEquals(10.24f, filters.get(0).getOperationValue());
+    }
+
+    @Test
+    void success_unit_lowerCase() {
+        List<String> strings = List.of(
+                "unit(speed)gt90"
+        );
+
+        List<Filter> filters = FilterParser.parse(strings);
+
+        assertEquals(1, filters.size());
+
+        assertEquals(FilterType.UNIT, filters.get(0).getType());
+        assertEquals(FilterAttribute.SPEED, filters.get(0).getAttribute());
+        assertEquals(FilterAttribute.SPEED.name().toLowerCase(), filters.get(0).getAttributeValueAsString());
+        assertEquals(FilterOperation.GT, filters.get(0).getOperation());
+        assertEquals(90f, filters.get(0).getOperationValue());
+    }
+}