diff --git a/README.md b/README.md index 018fb69..9b830ce 100644 --- a/README.md +++ b/README.md @@ -112,18 +112,21 @@ The `time` object should contain the following parameters: - "first" (furthest back time value in RDB) Defaults to "now". -The `trajectory` object should contain the following parameters, more descriptions of the file contents are given in [here](#trajectory-queries): +For clarification, the `limit` value supports both positive and negative integers. For reference types of `now` and `latest` it is multiplied by **-1** then **added** to the reference time during the calculation of retrieval times. For references of `first` is is simply **added** to the reference time. + +For example, a value of `24` with a reference of `now` will provide all values generated within the last real-world day. Whereas a value of `24` with a reference of `first` will return all values generated between the first data point and one real-world day afterwards. + +The `trajectory` should be an array of JSON objects, where each of these objects should contain the following parameters: - Required: - `pointIriQuery`: Location of file with SPARQL query used to get point IRIs containing time series (relative to configuration file). - `featureIriQuery`: Location of file with SQL/SPARQL query to obtain the intersected feature IRIs (relative to configuration file). - `metaQuery`: Location of file with SPARQL query to obtain metadata of the intersected features (relative to configuration file). + - `database`: Database containing time series data of the points. -For clarification, the `limit` value supports both positive and negative integers. For reference types of `now` and `latest` it is multiplied by **-1** then **added** to the reference time during the calculation of retrieval times. For references of `first` is is simply **added** to the reference time. - -For example, a value of `24` with a reference of `now` will provide all values generated within the last real-world day. Whereas a value of `24` with a reference of `first` will return all values generated between the first data point and one real-world day afterwards. +More descriptions of the file contents are given in [here](#trajectory-queries). -Within the [samples/fia/fia-config.json](./samples/fia/fia-config.json) file, a mock configuration can be found. +Within the [sample/fia/fia-config.json](./sample/fia/fia-config.json) file, a mock configuration can be found. ### Expected query formats diff --git a/code/src/main/java/com/cmclinnovations/featureinfo/QueryManager.java b/code/src/main/java/com/cmclinnovations/featureinfo/QueryManager.java index 85b643e..8e70d0e 100644 --- a/code/src/main/java/com/cmclinnovations/featureinfo/QueryManager.java +++ b/code/src/main/java/com/cmclinnovations/featureinfo/QueryManager.java @@ -138,7 +138,18 @@ public JSONObject processRequest(Request request, HttpServletResponse response) result.put("time", timedata); } if (trajectoryData != null && !trajectoryData.isEmpty()) { - result.put("meta", trajectoryData); + if (result.has("meta")) { + // append to existing metadata object + trajectoryData.keySet().forEach(k -> { + if (metadata.has(k)) { + LOGGER.warn("Trajectory data and metadata has the same label: {}", k); + } else { + metadata.put(k, trajectoryData.get(k)); + } + }); + } else { + result.put("meta", trajectoryData); + } } return result; @@ -149,7 +160,7 @@ public JSONObject processRequest(Request request, HttpServletResponse response) * and which * configuration entries match said classes. * - * @param iri feature IRI. + * @param iri feature IRI. * * @return Set of matching configuration entries. * diff --git a/code/src/main/java/com/cmclinnovations/featureinfo/config/ConfigEntry.java b/code/src/main/java/com/cmclinnovations/featureinfo/config/ConfigEntry.java index 8a9b57a..b5f7bc9 100644 --- a/code/src/main/java/com/cmclinnovations/featureinfo/config/ConfigEntry.java +++ b/code/src/main/java/com/cmclinnovations/featureinfo/config/ConfigEntry.java @@ -5,10 +5,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.json.JSONArray; import com.google.common.base.Objects; @@ -74,13 +77,7 @@ public class ConfigEntry { */ private String timeDatabase; - private String pointIriQueryFile; - private String pointIriQueryContent; - private String featureIriQueryFile; - private String featureIriQueryContent; - private String trajectoryMetaFile; - private String trajectoryMetaContent; - private String trajectoryDatabase; + private List trajectoryConfigEntryList; /** * Initialise a new ConfigEntry instance. @@ -172,20 +169,8 @@ public String getTimeDatabase() { return this.timeDatabase; } - public String getPointIriQuery() { - return pointIriQueryContent; - } - - public String getFeatureIriQuery() { - return featureIriQueryContent; - } - - public String getTrajectoryMetaQuery() { - return trajectoryMetaContent; - } - - public String getTrajectoryDatabase() { - return trajectoryDatabase; + public List getTrajectoryConfigEntries() { + return trajectoryConfigEntryList; } /** @@ -204,10 +189,6 @@ public int hashCode() { hash = prime * hash + (this.timeLimitValue); hash = prime * hash + (this.timeLimitUnit != null ? this.timeLimitUnit.hashCode() : 0); hash = prime * hash + (this.timeDatabase != null ? this.timeDatabase.hashCode() : 0); - hash = prime * hash + (this.pointIriQueryFile != null ? this.pointIriQueryFile.hashCode() : 0); - hash = prime * hash + (this.featureIriQueryFile != null ? this.featureIriQueryFile.hashCode() : 0); - hash = prime * hash + (this.trajectoryMetaFile != null ? this.trajectoryMetaFile.hashCode() : 0); - hash = prime * hash + (this.trajectoryDatabase != null ? this.trajectoryDatabase.hashCode() : 0); return hash; } @@ -241,14 +222,6 @@ public boolean equals(Object object) { return false; if (!Objects.equal(this.timeDatabase, that.timeDatabase)) return false; - if (!Objects.equal(this.pointIriQueryFile, that.pointIriQueryFile)) - return false; - if (!Objects.equal(this.featureIriQueryFile, that.featureIriQueryFile)) - return false; - if (!Objects.equal(this.trajectoryMetaFile, that.trajectoryMetaFile)) - return false; - if (!Objects.equal(this.trajectoryDatabase, that.trajectoryDatabase)) - return false; return true; } @@ -294,7 +267,7 @@ public ConfigEntry build( String classIRI, String metaQueryFile) throws IllegalArgumentException, IOException { - return build(id, classIRI, metaQueryFile, null, null, 0, null, null); + return build(id, classIRI, metaQueryFile, null, null, 0, null, null, new JSONArray()); } /** @@ -322,7 +295,8 @@ public ConfigEntry build( String timeLimitUnit, String timeDatabase) throws IllegalArgumentException, IOException { - return build(id, classIRI, null, timeQueryFile, timeReference, timeLimitValue, timeLimitUnit, timeDatabase); + return build(id, classIRI, null, timeQueryFile, timeReference, timeLimitValue, timeLimitUnit, timeDatabase, + new JSONArray()); } /** @@ -350,7 +324,8 @@ public ConfigEntry build( String timeReference, int timeLimitValue, String timeLimitUnit, - String timeDatabase) throws IllegalArgumentException, IOException { + String timeDatabase, + JSONArray trajectoryConfig) throws IllegalArgumentException, IOException { // Check for valid parameters if (timeQueryFile != null && !timeQueryFile.isEmpty() && (timeDatabase == null || timeDatabase.isEmpty())) { @@ -397,37 +372,18 @@ public ConfigEntry build( entry.timeDatabase = timeDatabase; - // Populate query contents - readQueryContent(entry); - return entry; - } - - /** - * special case for trajectory query - * - * @param id - * @param classIRI - * @param trajectoryQuery - * @param featureIriQuery - * @param metaQuery - * @return - * @throws IOException - */ - public ConfigEntry build( - String id, - String classIRI, - String pointIriQuery, - String featureIriQuery, - String metaQuery, - String database) throws IOException { - - // Create and return ConfigEntry instance. - ConfigEntry entry = new ConfigEntry(id); - entry.classIRI = classIRI; - entry.pointIriQueryFile = pointIriQuery; - entry.featureIriQueryFile = featureIriQuery; - entry.trajectoryMetaFile = metaQuery; - entry.trajectoryDatabase = database; + // trajectory stuff + entry.trajectoryConfigEntryList = new ArrayList<>(); + for (int i = 0; i < trajectoryConfig.length(); i++) { + TrajectoryConfigEntry trajectoryConfigEntry = new TrajectoryConfigEntry(); + trajectoryConfigEntry.pointIriQueryFile = trajectoryConfig.getJSONObject(i).getString("pointIriQuery"); + trajectoryConfigEntry.featureIriQueryFile = trajectoryConfig.getJSONObject(i) + .getString("featureIriQuery"); + trajectoryConfigEntry.trajectoryMetaFile = trajectoryConfig.getJSONObject(i).getString("metaQuery"); + trajectoryConfigEntry.trajectoryDatabase = trajectoryConfig.getJSONObject(i).getString("database"); + + entry.trajectoryConfigEntryList.add(trajectoryConfigEntry); + } // Populate query contents readQueryContent(entry); @@ -462,20 +418,51 @@ private void readQueryContent(ConfigEntry entry) throws IOException { } // Parse trajectory query - if (entry.pointIriQueryFile != null && !entry.pointIriQueryFile.isEmpty()) { - Path file = this.configDirectory.resolve(Paths.get(entry.pointIriQueryFile)); - entry.pointIriQueryContent = Files.readString(file); - } - if (entry.featureIriQueryFile != null && !entry.featureIriQueryFile.isEmpty()) { - Path file = this.configDirectory.resolve(Paths.get(entry.featureIriQueryFile)); - entry.featureIriQueryContent = Files.readString(file); - } - if (entry.trajectoryMetaFile != null && !entry.trajectoryMetaFile.isEmpty()) { - Path file = this.configDirectory.resolve(Paths.get(entry.trajectoryMetaFile)); - entry.trajectoryMetaContent = Files.readString(file); + for (TrajectoryConfigEntry trajectoryConfigEntry : entry.trajectoryConfigEntryList) { + if (trajectoryConfigEntry.pointIriQueryFile != null + && !trajectoryConfigEntry.pointIriQueryFile.isEmpty()) { + Path file = this.configDirectory.resolve(Paths.get(trajectoryConfigEntry.pointIriQueryFile)); + trajectoryConfigEntry.pointIriQueryContent = Files.readString(file); + } + if (trajectoryConfigEntry.featureIriQueryFile != null + && !trajectoryConfigEntry.featureIriQueryFile.isEmpty()) { + Path file = this.configDirectory.resolve(Paths.get(trajectoryConfigEntry.featureIriQueryFile)); + trajectoryConfigEntry.featureIriQueryContent = Files.readString(file); + } + if (trajectoryConfigEntry.trajectoryMetaFile != null + && !trajectoryConfigEntry.trajectoryMetaFile.isEmpty()) { + Path file = this.configDirectory.resolve(Paths.get(trajectoryConfigEntry.trajectoryMetaFile)); + trajectoryConfigEntry.trajectoryMetaContent = Files.readString(file); + } } } } + public static class TrajectoryConfigEntry { + private String pointIriQueryFile; + private String pointIriQueryContent; + private String featureIriQueryFile; + private String featureIriQueryContent; + private String trajectoryMetaFile; + private String trajectoryMetaContent; + private String trajectoryDatabase; + + public String getPointIriQuery() { + return pointIriQueryContent; + } + + public String getFeatureIriQuery() { + return featureIriQueryContent; + } + + public String getTrajectoryMetaQuery() { + return trajectoryMetaContent; + } + + public String getTrajectoryDatabase() { + return trajectoryDatabase; + } + } + } // End of class. \ No newline at end of file diff --git a/code/src/main/java/com/cmclinnovations/featureinfo/config/ConfigReader.java b/code/src/main/java/com/cmclinnovations/featureinfo/config/ConfigReader.java index f456f7b..c96df09 100644 --- a/code/src/main/java/com/cmclinnovations/featureinfo/config/ConfigReader.java +++ b/code/src/main/java/com/cmclinnovations/featureinfo/config/ConfigReader.java @@ -111,7 +111,7 @@ private ConfigEntry parseEntry(Path configDir, JSONObject jsonEntry) // Initialise a new builder ConfigEntryBuilder builder = new ConfigEntryBuilder(configDir); - if (jsonEntry.has("meta") || jsonEntry.has("time")) { + if (jsonEntry.has("meta") || jsonEntry.has("time") || jsonEntry.has("trajectory")) { // New format entry String id = jsonEntry.getString("id"); String clazz = jsonEntry.getString("class"); @@ -139,22 +139,18 @@ private ConfigEntry parseEntry(Path configDir, JSONObject jsonEntry) timeDatabase = timeEntry.optString("database"); } - // Build - return builder.build(id, clazz, metaFile, timeFile, timeReference, timeLimit, timeUnit, timeDatabase); - } else if (jsonEntry.has("trajectory")) { // special trajectory case - // TODO: in futrue, it would be best to do tragectory calculations all in a - // single sparql query. This requires the time series being kg accessible. - String id = jsonEntry.getString("id"); - String clazz = jsonEntry.getString("class"); - - // trajectory details - JSONObject trajectoryEntry = jsonEntry.getJSONObject("trajectory"); - String pointIriQuery = trajectoryEntry.getString("pointIriQuery"); - String featureIriQuery = trajectoryEntry.getString("featureIriQuery"); - String metaQuery = trajectoryEntry.getString("metaQuery"); - String timeDatabase = trajectoryEntry.optString("database"); + // Trajectory + JSONArray trajectoryConfig; + if (jsonEntry.has("trajectory")) { + LOGGER.info("Detected trajectory config for {}", clazz); + trajectoryConfig = jsonEntry.getJSONArray("trajectory"); + } else { + trajectoryConfig = new JSONArray(); + } - return builder.build(id, clazz, pointIriQuery, featureIriQuery, metaQuery, timeDatabase); + // Build + return builder.build(id, clazz, metaFile, timeFile, timeReference, timeLimit, timeUnit, timeDatabase, + trajectoryConfig); } else { // Assume old format entry String id = "entry-" + (entries.size() + 1); @@ -171,7 +167,8 @@ private ConfigEntry parseEntry(Path configDir, JSONObject jsonEntry) String timeDatabase = jsonEntry.optString("databaseName"); // Build - return builder.build(id, clazz, metaFile, timeFile, timeReference, timeLimit, timeUnit, timeDatabase); + return builder.build(id, clazz, metaFile, timeFile, timeReference, timeLimit, timeUnit, timeDatabase, + new JSONArray()); } } diff --git a/code/src/main/java/com/cmclinnovations/featureinfo/core/trajectory/TrajectoryHandler.java b/code/src/main/java/com/cmclinnovations/featureinfo/core/trajectory/TrajectoryHandler.java index aab1399..5392d32 100644 --- a/code/src/main/java/com/cmclinnovations/featureinfo/core/trajectory/TrajectoryHandler.java +++ b/code/src/main/java/com/cmclinnovations/featureinfo/core/trajectory/TrajectoryHandler.java @@ -81,18 +81,22 @@ public void setClients(RemoteStoreClient remoteStoreClient) { public JSONObject getData(List classMatches) { List rawResults = new ArrayList<>(); - classMatches.stream().filter(c -> c.getFeatureIriQuery() != null).forEach(classMatch -> { - // Construct line using points queried from point time series - List pointIriList = getPointIriList(classMatch.getPointIriQuery()); + classMatches.forEach(classMatch -> { + classMatch.getTrajectoryConfigEntries().forEach(trajectoryConfigEntry -> { + // Construct line using points queried from point time series + List pointIriList = getPointIriList(trajectoryConfigEntry.getPointIriQuery()); - LineString trajectoryLine = makeLine(pointIriList); + LineString trajectoryLine = makeLine(pointIriList); - String featureIriQuery = classMatch.getFeatureIriQuery().replace("[LINE_WKT]", trajectoryLine.toString()); + String featureIriQuery = trajectoryConfigEntry.getFeatureIriQuery().replace("[LINE_WKT]", + trajectoryLine.toString()); - List featureIriList = getFeatures(featureIriQuery, classMatch.getTrajectoryDatabase()); + List featureIriList = getFeatures(featureIriQuery, + trajectoryConfigEntry.getTrajectoryDatabase()); - String trajectoryMetaQueryTemplate = classMatch.getTrajectoryMetaQuery(); - rawResults.add(getMetadata(trajectoryMetaQueryTemplate, featureIriList)); + String trajectoryMetaQueryTemplate = trajectoryConfigEntry.getTrajectoryMetaQuery(); + rawResults.add(getMetadata(trajectoryMetaQueryTemplate, featureIriList)); + }); }); return MetaParser.formatData(rawResults); @@ -137,17 +141,47 @@ private LineString makeLine(List pointIriList) { TimeSeriesClient tsClient = getTimeSeriesClientViaFactory(List.of(pointIri)); tsClient.setRDBClient(tsClient.getRdbUrl(), rdbEndpoint.username(), rdbEndpoint.password()); - TimeSeries timeseries = tsClient.getTimeSeriesWithinBounds(List.of(pointIri), lowerbound, upperbound); - timeList.addAll(timeseries.getTimes()); - pointList.addAll(timeseries.getValuesAsPoint(pointIri)); + TimeSeries latestData = tsClient.getLatestData(pointIri); + + if (!latestData.getTimes().isEmpty()) { + // value from viz framework is always in seconds + if (isMilliSeconds(latestData.getTimes().get(0))) { + Long lowerboundRequest = null; + Long upperboundRequest = null; + if (lowerbound != null) { + lowerboundRequest = lowerbound * 1000; // convert to milliseconds + } + if (upperbound != null) { + upperboundRequest = upperbound * 1000; // convert to milliseconds + } + TimeSeries timeseries = tsClient.getTimeSeriesWithinBounds(List.of(pointIri), + lowerboundRequest, upperboundRequest); + timeList.addAll(timeseries.getTimes()); + pointList.addAll(timeseries.getValuesAsPoint(pointIri)); + } else { + TimeSeries timeseries = tsClient.getTimeSeriesWithinBounds(List.of(pointIri), lowerbound, + upperbound); + timeList.addAll(timeseries.getTimes()); + pointList.addAll(timeseries.getValuesAsPoint(pointIri)); + } + } }); - // sort points according to time sortTwoLists(timeList, pointList); return new LineString(pointList.toArray(new Point[pointList.size()])); } + // assume that data is always historical... + private boolean isMilliSeconds(long epoch) { + // Get the current time in milliseconds + long currentTimeMillis = System.currentTimeMillis(); + + // If the epoch value is greater than current time in milliseconds, + // it's likely too large to be in milliseconds and might be in seconds. + return epoch > currentTimeMillis / 1000; // compare with seconds since epoch + } + private List getFeatures(String queryString, String trajectoryDatabase) { List featureIriList = new ArrayList<>(); if (isSparql(queryString)) { diff --git a/sample/fia/fia-config.json b/sample/fia/fia-config.json index afa3821..d9233e3 100644 --- a/sample/fia/fia-config.json +++ b/sample/fia/fia-config.json @@ -3,7 +3,7 @@ { "id": "Castle", "class": "https://theworldavatar.io/ontology/ontocastle/ontocastle.owl#Castle", - "meta" : { + "meta": { "queryFile": "CastleMeta.sparql" }, "time": { @@ -17,16 +17,34 @@ { "id": "Structure", "class": "https://theworldavatar.io/ontology/ontocastle/ontocastle.owl#Structure", - "meta" : { + "meta": { "queryFile": "StructureMeta.sparql" } }, { "id": "Feature", "class": "http://www.opengis.net/ont/geosparql#Feature", - "meta" : { + "meta": { "queryFile": "FeatureMeta.sparql" } + }, + { + "id": "Line", + "class": "https://www.theworldavatar.io/kg/trajectory/Trajectory", + "trajectory": [ + { + "pointIriQuery": "point_query.sparql", + "featureIriQuery": "feature_query.sparql", + "metaQuery": "trajectory_meta.sparql", + "database": "postgres" + }, + { + "pointIriQuery": "point_query.sparql", + "featureIriQuery": "feature_query.sql", + "metaQuery": "trajectory_meta.sparql", + "database": "postgres" + } + ] } ] } \ No newline at end of file