diff --git a/README.md b/README.md index 1ec5a81be2..61d8c2fe51 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ As well as other handy tools: * [Higher-level entities](/src/main/java/org/openstreetmap/atlas/geography/atlas/items/complex#complex-entities) * [Saving](/src/main/java/org/openstreetmap/atlas/geography/atlas#saving-an-atlas) / [Loading](/src/main/java/org/openstreetmap/atlas/geography/atlas#using-atlas) * [Command Line Tools](atlas-shell-tools) +* [Atlas Query Language i.e. AQL](/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl#README.md) # Community diff --git a/build.gradle b/build.gradle index 1b439c1390..f770bd8e7f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java' + id 'groovy' id 'maven' id 'maven-publish' id 'idea' @@ -66,6 +67,7 @@ dependencies compile packages.groovy compile packages.checkstyle compile packages.diff_utils + compile packages.groovy_json testCompile packages.checkstyle_tests diff --git a/dependencies.gradle b/dependencies.gradle index 16113edbaa..0e3dfd08e8 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -30,6 +30,7 @@ project.ext.versions = [ groovy: '2.5.4', atlas_checkstyle: '5.6.9', diff_utils: '4.0', + groovy_json: '2.5.4' ] project.ext.packages = [ @@ -77,4 +78,5 @@ project.ext.packages = [ checkstyle_tests: "com.puppycrawl.tools:checkstyle:${versions.checkstyle}:tests", atlas_checkstyle: "org.openstreetmap.atlas:atlas:${versions.atlas_checkstyle}", diff_utils: "io.github.java-diff-utils:java-diff-utils:${versions.diff_utils}", + groovy_json: "org.codehaus.groovy:groovy-json:${versions.groovy_json}" ] diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/README.md b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/README.md new file mode 100644 index 0000000000..dbeb992c7f --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/README.md @@ -0,0 +1,120 @@ +# Atlas Query Language (AQL) + +# Introduction +Atlas Query Language or AQL is a new feature that allows developers to write queries directly against the Atlas files. This will allow rapid development where developers can write select statements against their Atlas or OSM files locally and then port these queries in their applications (spark or otherwise) to run the same queries at scale. + +AQL currently supports using (as in use Atlas/OSM files), select, update, delete, explain (as in explain plan), commit and diff (as in difference between two atlas schemas). + +The AQL statements (select, update, delete) support complex where clauses with geospatial and tag querying among other criterion. AQL also supports nested inner queries.. + +AQL leverages Atlas as the underlying framework and doesn't reinvent the wheels. AQL is efficient in the sense that queries are automatically optimized to benefit from index-ing which can be either atlas id based or geospatial based. + +Atlas allows working against all 6 major entities, node, point, edge, line, relation, area, exposed as "tables" in an atlas schema. + +# Quick Start Guide + +The easiest way to get started is to have an IDE with Groovy support. Ensure that you have Atlas +as a dependency. IntelliJ with Groovy plugin offers excellent support including auto-complete of +queries as demonstrated below. + +- Imports + +Create a groovy file with 2 imports listed below. Notice these are `static` star imports, + +```groovy +/*Optional package statement if applicable.*/ + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getEdge +``` + +- Load Atlas (or OSM) file(s) + +```sql +/* +examples, +a. One Atlas File on disk:- file:///home/me/dir1/dir2//ButterflyPark.atlas +b. One Atlas File omn disk (without explicitly mentioning file: scheme):- /home/me/dir1/dir2//ButterflyPark.atlas +d. All Atlas files in a directory:- /home/dir1/dir2/allAtlasesInsideDirNoRecurse +e. A file from the classpath:- classpath:/data/ButterflyPark/ButterflyPark.osm +f. Multiple files from classpath:- classpath:/atlas1/something1.atlas,something2.atlas;/atlas2/Alcatraz.atlas,Butterfly.atlas +*/ +myAtlas = using "" +``` + +- Write queries against `myAtlas` + +Here are some examples, + +* Select everything from node "table" + +```sql +select node._ from myAtlas.node +``` + +* Select with where clause + +```sql +select node.id, node.osmId, node.tags from myAtlas.node where node.hasId(123000000) or node.hasTag("amenity": "college") +``` + +* Update statement + +```sql +update myAtlas.node set node.addTag(hello: "world") where node.hasIds(123456789, 9087655432) +``` + +* Delete statement + +```sql +delete myAtlas.edge where edge.hasTag(highway: "footway") and not(edge.hasTag(foot: "yes")) +``` + +* An example where we commit to create a new Atlas (schema), + +```sql + +/*Load*/ +atlas = using "classpath:/data/Alcatraz/Alcatraz.osm" + +/*Run some select queries to explore the data.*/ +select edge._ from atlas.edge limit 100 + +/* +Run some update and/or delete statement(s) +*/ +update1 = update atlas.edge set edge.addTag(website: "https://www.nps.gov/alca") where edge.hasTagLike(name: /Pier/) or edge.hasTagLike(name: /Island/) or edge.hasTagLike(name: /Road/) +update2 = update atlas.edge set edge.addTag(wikipedia: "https://en.wikipedia.org/wiki/Alcatraz_Island") where edge.hasTagLike(/foot/) + +/* +Note edge.hasTagLike(name: /Pier/), in groovy you can use forward slashes instead of double +quotes to work with RegEx and that allows you to skip the double escaping that we do in Java +when working with RegExes. +*/ + +/* +Commit the changes +*/ +changedAtlas = commit update1, update2 + +/* +Run Selects against the new atlas schema, notice changedAtlas.edge instead of atlas.edge here, +*/ +select edge._ from changedAtlas.edge where edge.hasTag("website") or edge.hasTag("wikipedia") + +/* +Run some commands like diff and explain plan +*/ +diff atlas, changedAtlas + +explain update1 +``` + +# Supported Tables + +- node +- point +- line +- edge +- relation +- area diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/authentication/Authenticator.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/authentication/Authenticator.groovy new file mode 100644 index 0000000000..5852cb3277 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/authentication/Authenticator.groovy @@ -0,0 +1,13 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.authentication + +/** + * A general purpose message authenticator. See wiki + * for more details on message authentication. + * + * @author Yazad Khambata + */ +interface Authenticator { + String sign(String message) + + void verify(String message, String signature) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/authentication/impl/SHA512HMACAuthenticatorImpl.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/authentication/impl/SHA512HMACAuthenticatorImpl.groovy new file mode 100644 index 0000000000..d9d338e665 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/authentication/impl/SHA512HMACAuthenticatorImpl.groovy @@ -0,0 +1,52 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.authentication.impl + +import org.apache.commons.lang3.Validate +import org.openstreetmap.atlas.geography.atlas.dsl.authentication.Authenticator +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +/** + * This implementation performs message authentication uses HMAC + * with SHA512 + * (read more about MACs on wiki). + * + * HMAC here is implemented utilizing Java Cryptography Architecture or JCA. Read Oracle documentation on JCA + * here. + * + * @author Yazad Khambata + */ +class SHA512HMACAuthenticatorImpl implements Authenticator { + + public static final int MIN_KEY_SIZE = 10 + private String key + + private static final String HMAC_SHA512 = "HmacSHA512" + + SHA512HMACAuthenticatorImpl(final String key) { + super() + Valid.notEmpty key + + Valid.isTrue(key.size() >= MIN_KEY_SIZE, "key should be at least ${MIN_KEY_SIZE}.") + this.key = key + } + + @Override + String sign(final String message) { + final byte[] byteKey = key.getBytes("UTF-8") + final Mac sha512_HMAC = Mac.getInstance(HMAC_SHA512) + final SecretKeySpec keySpec = new SecretKeySpec(byteKey, HMAC_SHA512) + sha512_HMAC.init(keySpec) + + final byte[] mac_data = sha512_HMAC.doFinal(message.getBytes("UTF-8")) + final String result = Base64.encoder.encodeToString(mac_data) + + result + } + + @Override + void verify(final String message, final String signature) { + Validate.isTrue(sign(message) == signature, "Signature Mismatch; message: [${message}]; signature: [${signature}].") + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/ConsoleWriter.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/ConsoleWriter.groovy new file mode 100644 index 0000000000..1473fedfae --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/ConsoleWriter.groovy @@ -0,0 +1,14 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.console + +/** + * Decouple using PrintStream or Logger in the statements and commends. + * + * @author Yazad Khambata + */ +interface ConsoleWriter { + boolean isTurnedOff() + + void echo(String text) + + void echo(Object object) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/impl/BaseConsoleWriter.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/impl/BaseConsoleWriter.groovy new file mode 100644 index 0000000000..13a442ef12 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/impl/BaseConsoleWriter.groovy @@ -0,0 +1,41 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.console.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +import java.util.function.Consumer + +/** + * Base class of console writers. + * + * @author Yazad Khambata + */ +abstract class BaseConsoleWriter implements ConsoleWriter { + private boolean turnedOff + + private Consumer echoer + + protected BaseConsoleWriter(final Consumer echoer) { + this.echoer = echoer + this.turnedOff = (echoer == null) + } + + @Override + boolean isTurnedOff() { + turnedOff + } + + @Override + void echo(final String text) { + if (isTurnedOff()) { + return + } + + echoer.accept(text) + } + + @Override + void echo(final Object object) { + echo(((String)(object?.toString()))) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/impl/QuietConsoleWriter.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/impl/QuietConsoleWriter.groovy new file mode 100644 index 0000000000..8137e6ca55 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/impl/QuietConsoleWriter.groovy @@ -0,0 +1,24 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.console.impl + +/** + * Permanently turned off console writer. + * + * @author Yazad Khambata + */ +final class QuietConsoleWriter extends BaseConsoleWriter { + + private static QuietConsoleWriter instance = new QuietConsoleWriter() + + private QuietConsoleWriter() { + super(null) + } + + @Override + void echo(final String text) { + //NOP + } + + static QuietConsoleWriter getInstance() { + instance + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/impl/StandardOutputConsoleWriter.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/impl/StandardOutputConsoleWriter.groovy new file mode 100644 index 0000000000..7c622041ba --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/impl/StandardOutputConsoleWriter.groovy @@ -0,0 +1,27 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.console.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +/** + * Works against standard output. + * + * @author Yazad Khambata + */ +class StandardOutputConsoleWriter extends BaseConsoleWriter { + + private static final StandardOutputConsoleWriter instance = new StandardOutputConsoleWriter(System.out) + + private StandardOutputConsoleWriter(final PrintStream printStream) { + super(toEchoer(printStream)) + } + + private static Closure toEchoer(final PrintStream printStream) { + Valid.notEmpty printStream + + { text -> printStream.println(text) } + } + + static StandardOutputConsoleWriter getInstance() { + instance + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/QueryExecutor.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/QueryExecutor.groovy new file mode 100644 index 0000000000..9249e4a0b9 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/QueryExecutor.groovy @@ -0,0 +1,17 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine + +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result + +/** + * Used as an integration point between other applications and processes. + * The query is expected as an input String or a Reader. + * + * @author Yazad Khambata + */ +interface QueryExecutor { + + Result exec(Atlas atlas, String queryAsString, final String signature) + + Result exec(Atlas atlas, Reader queryAsReader, final String signature) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/AbstractQueryExecutorImpl.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/AbstractQueryExecutorImpl.groovy new file mode 100644 index 0000000000..0c4604991f --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/AbstractQueryExecutorImpl.groovy @@ -0,0 +1,118 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.impl + +import org.apache.commons.io.IOUtils +import org.apache.commons.lang3.StringUtils +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.customizers.ImportCustomizer +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.console.impl.StandardOutputConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.engine.QueryExecutor +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +/** + * Generalisation of query execution irrespective of security of actual implementation. + * + * @author Yazad Khambata + */ +abstract class AbstractQueryExecutorImpl implements QueryExecutor { + private String key + + public static final String SYSTEM_PARAM_KEY = "aqlKey" + + private ConsoleWriter consoleWriter + + AbstractQueryExecutorImpl() { + this(getKeyFromSystemParam()) + } + + AbstractQueryExecutorImpl(final String key) { + this(key, StandardOutputConsoleWriter.getInstance()) + } + + AbstractQueryExecutorImpl(final ConsoleWriter consoleWriter) { + this(getKeyFromSystemParam(), consoleWriter) + } + + AbstractQueryExecutorImpl(final String key, final ConsoleWriter consoleWriter) { + this.key = key + this.consoleWriter = consoleWriter + } + + protected static final String getKeyFromSystemParam() { + final String key = System.getProperty(SYSTEM_PARAM_KEY) + Valid.notEmpty key, "Have you set the system parameter ${SYSTEM_PARAM_KEY}?" + + key + } + + @Override + final Result exec(final Atlas atlas, final String queryAsString, final String signature) { + validateSignature(queryAsString, signature) + + Valid.notEmpty atlas + + final AtlasSchema atlasSchema = toAtlasSchema(atlas) + + validate(queryAsString) + + evalQueryString(atlasSchema, queryAsString) + } + + @Override + final Result exec(final Atlas atlas, final Reader queryAsReader, final String signature) { + exec(atlas, IOUtils.toString(queryAsReader), signature) + } + + private AtlasSchema toAtlasSchema(Atlas atlas) { + final AtlasMediator atlasMediator = toAtlasMediator(atlas) + + final AtlasSchema atlasSchema = new AtlasSchema(atlasMediator) + atlasSchema + } + + abstract void validateSignature(final String queryAsString, final String signature) + + private AtlasMediator toAtlasMediator(Atlas atlas) { + new AtlasMediator(atlas) + } + + private final void validate(String queryAsString) { + Valid.notEmpty queryAsString, "Query is EMPTY!" + + final List allowedStatements = [Statement.SELECT, Statement.UPDATE, Statement.DELETE] + + Valid.isTrue allowedStatements.stream() + .filter { statement -> queryAsString.startsWith(statement.closureName()) } + .count() == 1, + "Invalid Query input, only ${allowedStatements} supported." + Valid.isTrue Arrays.stream(queryAsString.split(/\r\n|\r|\n/)) + .filter { line -> StringUtils.isNotEmpty(line) } + .count() == 1, + "Only one statement permitted." + } + + private final Result evalQueryString(final AtlasSchema atlasSchema, final String queryAsString) { + final Binding binding = new Binding() + binding.setVariable("atlas", atlasSchema) + + final ImportCustomizer importCustomizer = new ImportCustomizer() + importCustomizer.addStaticStars(QueryBuilderFactory.class.getName(), AtlasDB.class.getName()) + + final CompilerConfiguration compilerConfiguration = new CompilerConfiguration() + compilerConfiguration.addCompilationCustomizers(importCustomizer) + + final GroovyShell groovyShell = new GroovyShell(binding, compilerConfiguration) + + final QueryBuilder queryBuilder = groovyShell.evaluate(queryAsString) + + queryBuilder.buildQuery().execute(consoleWriter) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/InsecureQueryExecutorImpl.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/InsecureQueryExecutorImpl.groovy new file mode 100644 index 0000000000..e5ec841897 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/InsecureQueryExecutorImpl.groovy @@ -0,0 +1,36 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.console.impl.StandardOutputConsoleWriter +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * This class should only be used while experimenting with the integration. Once Integration is completed switch to + * SecureQueryExecutorImpl. + * + * This class skips validating the authenticity of the source of the query. + * + * use SecureQueryExecutorImpl instead. + * + * @author Yazad Khambata + */ +class InsecureQueryExecutorImpl extends AbstractQueryExecutorImpl { + //Note: Deprecation is removed from this class to allow easy integration. + + private static final Logger log = LoggerFactory.getLogger(InsecureQueryExecutorImpl.class) + + InsecureQueryExecutorImpl() { + this(StandardOutputConsoleWriter.getInstance()) + } + + InsecureQueryExecutorImpl(final ConsoleWriter consoleWriter) { + super("NOT_USED", consoleWriter) + } + + @Override + void validateSignature(final String queryAsString, final String signature) { + //NOP + log.warn("Stop using InsecureQueryExecutorImpl and switch to SecureQueryExecutorImpl.") + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/SecureQueryExecutorImpl.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/SecureQueryExecutorImpl.groovy new file mode 100644 index 0000000000..1d768c148c --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/SecureQueryExecutorImpl.groovy @@ -0,0 +1,39 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.authentication.Authenticator +import org.openstreetmap.atlas.geography.atlas.dsl.authentication.impl.SHA512HMACAuthenticatorImpl +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.console.impl.StandardOutputConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +/** + * @author Yazad Khambata + */ +class SecureQueryExecutorImpl extends AbstractQueryExecutorImpl { + private Authenticator authenticator + + SecureQueryExecutorImpl() { + this(StandardOutputConsoleWriter.getInstance()) + } + + SecureQueryExecutorImpl(final ConsoleWriter consoleWriter) { + this(getKeyFromSystemParam(), consoleWriter) + } + + SecureQueryExecutorImpl(final String key) { + this(key, StandardOutputConsoleWriter.newInstance()) + } + + SecureQueryExecutorImpl(final String key, ConsoleWriter consoleWriter) { + super(key, consoleWriter) + + Valid.notEmpty key + + this.authenticator = new SHA512HMACAuthenticatorImpl(key) + } + + @Override + void validateSignature(final String queryAsString, final String signature) { + this.authenticator.verify(queryAsString, signature) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/Linter.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/Linter.groovy new file mode 100644 index 0000000000..6ff0098f53 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/Linter.groovy @@ -0,0 +1,112 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint + +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.Source +import org.openstreetmap.atlas.geography.atlas.dsl.path.PathQueryFilePackCollection +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintException +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintRequest +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintResponse +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.Lintlet +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.stream.Collectors + +/** + * The main entry point for the linter process. + * + * @author Yazad Khambata + */ +@Singleton +class Linter { + private static final Logger log = LoggerFactory.getLogger(Linter) + + void lint(final LintRequest lintRequest, final Class... lintletClasses) { + Valid.notEmpty lintletClasses + + lint(lintRequest, Arrays.stream(lintletClasses) + .map { Class lintletClass -> lintletInstance(lintletClass) } + .collect(Collectors.toList())) + } + + void lint(final LintRequest lintRequest, final List lintlets) { + final Lintlet[] lintletsArray = lintlets.stream().toArray { new Lintlet[lintlets.size()] } + lint(lintRequest, lintletsArray) + } + + void lint(final LintRequest lintRequest, final Lintlet... lintlets) { + lint([lintRequest], lintlets) + } + + void lint(final List lintRequests, final Lintlet... lintlets) { + + + final List issues = + lintRequests.stream() + .map { lintRequest -> + final List issuesForTheRequest = Arrays.stream(lintlets) + .map { lintlet -> + try { + lintlet.lint(lintRequest) + } catch (LintException e) { + log.error("Linting issue.", e) + final List responses = e.getLintResponses() + return Optional.of(responses) + } + + return Optional.> empty() + }.filter { optional -> optional.isPresent() } + .map { optional -> optional.get() } + .flatMap { list -> list.stream() } + .collect(Collectors.toList()) + + issuesForTheRequest + } + .flatMap { issuesForTheRequest -> issuesForTheRequest.stream() } + .collect(Collectors.toList()) + + final LintException lintException = new LintException(issues) + final String message = lintException.getMessage() + + println "---------------------------------------" + println message + println "---------------------------------------" + + if (hasFatalIssues(issues)) { + throw lintException + } + } + + void lint(final Source source, final String root, final List lintlets) { + final PathQueryFilePackCollection classpathQueryFilePackCollection = source.aqlFilesFrom(root) + + final List lintRequests = classpathQueryFilePackCollection.stream() + .map { classpathQueryFilePack -> new LintRequest(classpathQueryFilePack.getFileName(), classpathQueryFilePack.getQuery(), classpathQueryFilePack.getSignature()) } + .collect(Collectors.toList()) + + lint(lintRequests, lintlets as Lintlet[]) + } + + void lint(final Source source, final String root, final Lintlet... lintlets) { + lint(source, root, Arrays.stream(lintlets).collect(Collectors.toList())) + } + + void lint(final Source source, final String root, final Class... lintletClassses) { + lint( + source, + root, + Arrays + .stream(lintletClassses) + .map { Class lintClass -> lintletInstance(lintClass) } + .collect(Collectors.toList()) + ) + } + + private boolean hasFatalIssues(List issues) { + issues.stream().filter { issue -> issue.hasFatalIssues() }.findAny().isPresent() + } + + private L lintletInstance(final Class lintletClass) { + lintletClass.newInstance() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintException.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintException.groovy new file mode 100644 index 0000000000..8cc6c8b6a5 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintException.groovy @@ -0,0 +1,27 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain + +/** + * Issues detected by the linter is wrapped in this exception. + * + * @author Yazad Khambata + */ +class LintException extends RuntimeException { + List lintResponses + + LintException(final List lintResponses, final Throwable cause) { + super(report(lintResponses), (Throwable)cause) + this.lintResponses = lintResponses + } + + LintException(final LintResponse lintResponse, final Throwable cause) { + this([lintResponse], cause) + } + + LintException(final List lintResponses) { + this(lintResponses, null) + } + + static String report(final List lintResponses) { + LintReport.instance.generateReport(lintResponses) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintLogLevel.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintLogLevel.groovy new file mode 100644 index 0000000000..bbf4f81116 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintLogLevel.groovy @@ -0,0 +1,15 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain + +import groovy.transform.TupleConstructor + +/** + * Lint issue severity or level. + * + * @author Yazad Khambata + */ +@TupleConstructor +enum LintLogLevel { + ERROR(true), WARN(false); + + boolean fatal +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintReport.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintReport.groovy new file mode 100644 index 0000000000..8da521c9c1 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintReport.groovy @@ -0,0 +1,44 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain + +import java.util.stream.Collectors + +/** + * Txt based lint report generator for the console. + * + * @author Yazad Khambata + */ +@Singleton +class LintReport { + String generateReport(final List issues) { + final String report = issues.stream().map { LintResponse lintResponse -> + final LintRequest lintRequest = lintResponse.lintRequest + final String queryFilePath = lintRequest.queryFilePath + final String query = lintRequest.queryAsString + final String signature = lintRequest.signature + + final String lintClassName = lintResponse.lintletClass.name + + final String responseMessages = lintResponse.lintResponseItems.stream() + .map { LintResponseItem lintResponseItem -> + final Long id = lintResponseItem.id + final String category = lintResponseItem.category + final String logLevel = lintResponseItem.lintLogLevel?.toString() + final String message = lintResponseItem.message + + "[${logLevel}][$category][${id}][${message}]" + } + .collect(Collectors.joining(";")) + """ +File : [${queryFilePath}]. +Query : [${query}]. +Signature : [${signature}] + -> Lint Class : [${lintClassName}] + => Issues : { + ${responseMessages} + } +""" + }.collect(Collectors.joining("\n")) + + report + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintRequest.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintRequest.groovy new file mode 100644 index 0000000000..6d400f750a --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintRequest.groovy @@ -0,0 +1,28 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain + +import org.apache.commons.io.IOUtils + +/** + * Request to lint a specified AQL file or content. + * + * @author Yazad Khambata + */ +class LintRequest { + String queryFilePath + String queryAsString + String signature + + LintRequest(String queryAsString, final String signature) { + this(null, queryAsString, signature) + } + + LintRequest(final Reader queryAsReader, final String signature) { + this(IOUtils.toString(queryAsReader), signature) + } + + LintRequest(final String queryFilePath, final String queryAsString, final String signature) { + this.queryAsString = queryAsString + this.signature = signature + this.queryFilePath = queryFilePath + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintResponse.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintResponse.groovy new file mode 100644 index 0000000000..9718234117 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintResponse.groovy @@ -0,0 +1,39 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain + +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.Lintlet + +/** + * Response of linting issues if any. + * + * @author Yazad Khambata + */ +class LintResponse { + LintRequest lintRequest + Class lintletClass + List lintResponseItems = [] + + LintResponse(final LintRequest lintRequest, final Class lintletClass) { + this.lintRequest = lintRequest + this.lintletClass = lintletClass + } + + static LintResponse newLintResponse(final LintRequest lintRequest, final Class lintletClass) { + new LintResponse(lintRequest, lintletClass) + } + + static LintResponse newLintResponse(final LintRequest lintRequest, final Class lintletClass, final LintResponseItem...lintResponseItems) { + newLintResponse(lintRequest, lintletClass).addLintResponseItems(lintResponseItems) + } + + LintResponse addLintResponseItems(LintResponseItem...lintResponseItems) { + this.lintResponseItems.addAll(lintResponseItems.toList()) + this + } + + boolean hasFatalIssues() { + lintResponseItems?.stream() + .filter { lintResponseItem -> lintResponseItem.getLintLogLevel().isFatal() } + .findAny() + .isPresent() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintResponseItem.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintResponseItem.groovy new file mode 100644 index 0000000000..81153df4e1 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/LintResponseItem.groovy @@ -0,0 +1,20 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain + +import groovy.transform.builder.Builder + +/** + * A specific lint response item. + * + * @author Yazad Khambata + */ +@Builder +class LintResponseItem { + long id + String category + String message + LintLogLevel lintLogLevel + + long newId() { + System.nanoTime() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/Source.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/Source.groovy new file mode 100644 index 0000000000..6997910e29 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/domain/Source.groovy @@ -0,0 +1,27 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain + +import org.openstreetmap.atlas.geography.atlas.dsl.path.PathQueryFilePackCollection +import org.openstreetmap.atlas.geography.atlas.dsl.path.PathUtil + +/** + * Built in sources supported in AQL (typically used with "using" command) + * + * @author Yazad Khambata + */ +enum Source { + PATH { + @Override + PathQueryFilePackCollection aqlFilesFrom(final String root) { + PathUtil.instance.aqlFilesFromPath(root) + } + }, + + CLASSPATH { + @Override + PathQueryFilePackCollection aqlFilesFrom(final String root) { + PathUtil.instance.aqlFilesFromClasspath(root) + } + }; + + abstract PathQueryFilePackCollection aqlFilesFrom(final String root) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/gradle/LinterGradlePluginHelper.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/gradle/LinterGradlePluginHelper.groovy new file mode 100644 index 0000000000..d4bf0aa338 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/gradle/LinterGradlePluginHelper.groovy @@ -0,0 +1,30 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.gradle + +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.Linter +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.Source +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.Lintlet + +import java.util.stream.Collectors + +/** + * Allows easy integration into gradle. + * + * @author Yazad Khambata + */ +class LinterGradlePluginHelper { + static void main(String[] args) { + final String projectDir = args[0] + + ["main", "test", "integrationTest"].stream().forEach { + final List lintletClasses = Arrays.stream(args, 1, args.length) + .map { arg -> Class.forName(arg) } + .map { lintletClass -> lintletClass.newInstance() } + .collect(Collectors.toList()) + + Linter.instance.lint( + Source.PATH, + "${projectDir}/src/${it}/resources/aql-files", + lintletClasses) + } + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/Lintlet.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/Lintlet.groovy new file mode 100644 index 0000000000..3f4895ad1a --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/Lintlet.groovy @@ -0,0 +1,12 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet + +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintRequest + +/** + * Analogous to a Servlet that runs inside a server, a "Lintlet" runs inside a Linter. + * + * @author Yazad Khambata + */ +interface Lintlet { + void lint(LintRequest lintRequest) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/AbstractLintlet.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/AbstractLintlet.groovy new file mode 100644 index 0000000000..2d28d89c4b --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/AbstractLintlet.groovy @@ -0,0 +1,22 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintException +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintRequest +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.Lintlet +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +/** + * Abstraction of a Lintlet. + * + * @author Yazad Khambata + */ +abstract class AbstractLintlet implements Lintlet { + @Override + void lint(final LintRequest lintRequest) { + Valid.notEmpty lintRequest + + doLint(lintRequest) + } + + abstract void doLint(final LintRequest lintRequest) throws LintException +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/AbstractWellFormednessLintlet.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/AbstractWellFormednessLintlet.groovy new file mode 100644 index 0000000000..32d226d822 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/AbstractWellFormednessLintlet.groovy @@ -0,0 +1,62 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.impl + +import org.openstreetmap.atlas.geography.Location +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.change.exception.EmptyChangeException +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.console.impl.QuietConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.engine.QueryExecutor +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.* +import org.openstreetmap.atlas.geography.atlas.packed.PackedAtlasBuilder + +/** + * Checks if the AQL is well-formed by running the query against an virtually empty atlas. + * + * @author Yazad Khambata + */ +abstract class AbstractWellFormednessLintlet extends AbstractLintlet { + + private QueryExecutor queryExecutor + + protected AbstractWellFormednessLintlet(final QueryExecutor queryExecutor) { + this.queryExecutor = queryExecutor + } + + @Override + final void doLint(final LintRequest lintRequest) throws LintException { + final PackedAtlasBuilder packedAtlasBuilder = new PackedAtlasBuilder() + packedAtlasBuilder.addNode(System.nanoTime(), Location.CENTER, new HashMap<>()) + final Atlas atlas = packedAtlasBuilder.get() + + final String query = lintRequest.getQueryAsString() + final String signature = lintRequest.getSignature() + + try { + queryExecutor.exec(atlas, query, signature) + } catch (EmptyChangeException e) { + //Ignore this - changes WILL be empty for linting. + } catch (Exception e) { + throw toLintException(toLintResponse(lintRequest, query, signature, e), e) + } catch (AssertionError e) { + throw toLintException(toLintResponse(lintRequest, query, signature, e), e) + } + } + + private LintException toLintException(LintResponse lintResponse, Throwable e) { + new LintException(lintResponse, e) + } + + private LintResponse toLintResponse(LintRequest lintRequest, String query, String signature, Throwable e) { + final LintResponse lintResponse = LintResponse.newLintResponse(lintRequest, this.getClass(), LintResponseItem.builder() + .category("Structural Error") + .message("The query is broken query: [${query}]; in [${lintRequest.queryFilePath}] signature: [${signature}]. Reason: [${e.message}].") + .lintLogLevel(LintLogLevel.ERROR) + .build() + ) + lintResponse + } + + static ConsoleWriter consoleWriter() { + QuietConsoleWriter.getInstance() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/InsecureWellFormednessLintlet.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/InsecureWellFormednessLintlet.groovy new file mode 100644 index 0000000000..dbd8465a2b --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/InsecureWellFormednessLintlet.groovy @@ -0,0 +1,16 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.engine.impl.InsecureQueryExecutorImpl + +/** + * Checks if the AQL is well-formed by running the query against an almost empty atlas. + * + * Avoid using this class - use WellFormednessLintlet instead. + * + * @author Yazad Khambata + */ +class InsecureWellFormednessLintlet extends AbstractWellFormednessLintlet { + InsecureWellFormednessLintlet() { + super(new InsecureQueryExecutorImpl(consoleWriter())) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/WellFormednessLintlet.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/WellFormednessLintlet.groovy new file mode 100644 index 0000000000..c7ce07a156 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/WellFormednessLintlet.groovy @@ -0,0 +1,15 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.engine.impl.SecureQueryExecutorImpl + +/** + * Checks if the AQL is well-formed by running the query against an EmptyAtlas. + * + * @author Yazad Khambata + */ +class WellFormednessLintlet extends AbstractWellFormednessLintlet { + + WellFormednessLintlet() { + super(new SecureQueryExecutorImpl(consoleWriter())) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/AbstractField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/AbstractField.groovy new file mode 100644 index 0000000000..7a3353fcb0 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/AbstractField.groovy @@ -0,0 +1,52 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import groovy.transform.PackageScope +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder + +/** + * Abstraction of a Field in a Table. + * + * @author Yazad Khambata + */ +@PackageScope +abstract class AbstractField implements Field { + + private String name + + AbstractField(String name) { + this.name = name + } + + @Override + String getName() { + name + } + + @Override + String getAlias() { + name + } + + @Override + String toString() { + return "${this.getClass().simpleName}(${name})" + } + + @Override + boolean equals(final o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + + final AbstractField that = (AbstractField) o + + if (name != that.name) return false + + return true + } + + @Override + int hashCode() { + return (name != null ? name.hashCode() : 0) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/AbstractMutableField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/AbstractMutableField.groovy new file mode 100644 index 0000000000..ec376e8824 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/AbstractMutableField.groovy @@ -0,0 +1,38 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.EntityUpdateType + +/** + * An abstraction of a mutable field.. + * + * @author Yazad Khambata + */ +class AbstractMutableField extends AbstractField implements MutableProperty { + + final EntityUpdateType entityUpdateType + + AbstractMutableField(final String name, final EntityUpdateType entityUpdateType) { + super(name) + this.entityUpdateType = entityUpdateType + } + + @Override + boolean equals(final o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + if (!super.equals(o)) return false + + final AbstractMutableField that = (AbstractMutableField) o + + if (entityUpdateType != that.entityUpdateType) return false + + return true + } + + @Override + int hashCode() { + int result = super.hashCode() + result = 31 * result + (entityUpdateType != null ? entityUpdateType.hashCode() : 0) + return result + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/CollectionField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/CollectionField.groovy new file mode 100644 index 0000000000..9bcf707e41 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/CollectionField.groovy @@ -0,0 +1,62 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.EntityUpdateType + +import java.util.function.BiConsumer + +/** + * A Collection field that stores more than one value, analogous to a field that is NOT in NF1. + * + * @author Yazad Khambata + */ +@PackageScope +class CollectionField extends AbstractField implements Selectable, Overridable, Elastic, Constrainable { + + @Delegate + Selectable selectable + + @Delegate + Overridable overridable + + @Delegate + Elastic elastic + + @Delegate + Constrainable constrainable + + CollectionField(final String name, final EntityUpdateType entityUpdateType, final BiConsumer overrideEnricher, final BiConsumer addEnricher, final BiConsumer removeEnricher) { + super(name) + selectable = new SelectOnlyField(name) + overridable = new OverridableFiled(name, entityUpdateType, this, overrideEnricher) + elastic = new ElasticField(name, entityUpdateType, this, addEnricher, removeEnricher) + constrainable = new ConstrainableFieldImpl(name) + } + + @Override + boolean equals(final o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + if (!super.equals(o)) return false + + final CollectionField that = (CollectionField) o + + if (constrainable != that.constrainable) return false + if (elastic != that.elastic) return false + if (overridable != that.overridable) return false + if (selectable != that.selectable) return false + + return true + } + + @Override + int hashCode() { + int result = super.hashCode() + result = 31 * result + (selectable != null ? selectable.hashCode() : 0) + result = 31 * result + (overridable != null ? overridable.hashCode() : 0) + result = 31 * result + (elastic != null ? elastic.hashCode() : 0) + result = 31 * result + (constrainable != null ? constrainable.hashCode() : 0) + return result + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ConnectedEdgeIdField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ConnectedEdgeIdField.groovy new file mode 100644 index 0000000000..eb5f07ceec --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ConnectedEdgeIdField.groovy @@ -0,0 +1,63 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.items.ConnectedEdgeType +import org.openstreetmap.atlas.geography.atlas.items.Edge + +import java.util.function.Function +import java.util.stream.Collectors + +/** + * Mapping of in and out edges in a node. + * + * @author Yazad Khambata + */ +class ConnectedEdgeIdField extends StandardMappedField, Set> { + + final String alias + + ConnectedEdgeIdField(final ConnectedEdgeType connectedEdgeType, final String alias) { + super(toName(connectedEdgeType), toId) + this.alias = alias + } + + private static Function, Set> toId = { Set edges -> edges?.stream().map { edge -> edge.identifier }.collect(Collectors.toSet()) } + + private static String toName(final ConnectedEdgeType connectedEdgeType) { + toInfo(connectedEdgeType)["name"] + } + + private static toInfo(ConnectedEdgeType connectedEdgeType) { + connectedEdgeTypeInfoMapping[connectedEdgeType] + } + + private static Map connectedEdgeTypeInfoMapping = [ + (ConnectedEdgeType.IN) : [name: "inEdges", alias: "inEdgeIds"], + (ConnectedEdgeType.OUT): [name: "outEdges", alias: "outEdgeIds"] + ] + + @Override + String getAlias() { + alias + } + + @Override + boolean equals(final o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + if (!super.equals(o)) return false + + final ConnectedEdgeIdField that = (ConnectedEdgeIdField) o + + if (alias != that.alias) return false + + return true + } + + @Override + int hashCode() { + int result = super.hashCode() + result = 31 * result + (alias != null ? alias.hashCode() : 0) + return result + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Constrainable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Constrainable.groovy new file mode 100644 index 0000000000..3762684ff9 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Constrainable.groovy @@ -0,0 +1,30 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * A constrained field is a field that can be used in the where clause of a statement. + * + * @author Yazad Khambata + */ +interface Constrainable extends Readable { + + def Constraint was(Map params, Class atlasEntityClass) + + def Constraint has(Map params, Class atlasEntityClass) + + def Constraint was(Map params, Field delegateField, Class atlasEntityClass) + + def Constraint has(Map params, Field delegateField, Class atlasEntityClass) + + def Constraint was(Map params, ScanType bestCandidateScanStrategy, Class atlasEntityClass) + + def Constraint has(Map params, ScanType bestCandidateScanStrategy, Class atlasEntityClass) + + def Constraint was(Map params, Field delegateField, ScanType bestCandidateScanStrategy, Class atlasEntityClass) + + def Constraint has(Map params, Field delegateField, ScanType bestCandidateScanStrategy, Class atlasEntityClass) + +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ConstrainableFieldImpl.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ConstrainableFieldImpl.groovy new file mode 100644 index 0000000000..98da4d4ad1 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ConstrainableFieldImpl.groovy @@ -0,0 +1,84 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ConstraintGenerator +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * @author Yazad Khambata + */ +@PackageScope +class ConstrainableFieldImpl extends AbstractField implements Constrainable { + + @Delegate + private Readable readableField + + ConstrainableFieldImpl(final String name) { + super(name) + + this.readableField = new SelectOnlyField(name) + } + + @Override + Constraint was(final Map params, final Class atlasEntityClass) { + this.was(params, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final Class atlasEntityClass) { + this.has(params, ScanType.FULL, atlasEntityClass) + } + + + @Override + Constraint was(Map params, Field delegateField, Class atlasEntityClass) { + this.was(params, delegateField, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint has(Map params, Field delegateField, Class atlasEntityClass) { + this.has(params, delegateField, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint was(final Map params, final ScanType bestCandidateScanStrategy, final Class atlasEntityClass) { + was(params, this, bestCandidateScanStrategy, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final ScanType bestCandidateScanStrategy, final Class atlasEntityClass) { + has(params, this, bestCandidateScanStrategy, atlasEntityClass) + } + + @Override + Constraint was(final Map params, final Field delegateField, final ScanType bestCandidateScanStrategy, final Class atlasEntityClass) { + return ConstraintGenerator.instance.was(params, delegateField, bestCandidateScanStrategy, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final Field delegateField, final ScanType bestCandidateScanStrategy, final Class atlasEntityClass) { + return ConstraintGenerator.instance.was(params, delegateField, bestCandidateScanStrategy, atlasEntityClass) + } + + @Override + boolean equals(final o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + if (!super.equals(o)) return false + + final ConstrainableFieldImpl that = (ConstrainableFieldImpl) o + + if (readableField != that.readableField) return false + + return true + } + + @Override + int hashCode() { + int result = super.hashCode() + result = 31 * result + (readableField != null ? readableField.hashCode() : 0) + return result + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Elastic.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Elastic.groovy new file mode 100644 index 0000000000..ab5cf44f90 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Elastic.groovy @@ -0,0 +1,13 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity + +/** + * A field whose contents can grow or shrink. + * + * @author Yazad Khambata + */ +@PackageScope +interface Elastic extends Growable, Shrinkable { +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ElasticField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ElasticField.groovy new file mode 100644 index 0000000000..369ee25bd3 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ElasticField.groovy @@ -0,0 +1,76 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.EntityUpdateOperation +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.EntityUpdateType +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.Mutant + +import java.util.function.BiConsumer + +/** + * An implementation of an Elastic field that can grow or shrink. + * + * @author Yazad Khambata + */ +@PackageScope +class ElasticField extends AbstractMutableField implements Elastic { + + private Field composingField + + private BiConsumer addEnricher + private BiConsumer removeEnricher + + ElasticField(final String name, final EntityUpdateType entityUpdateType, final Field composingField, final BiConsumer addEnricher, final BiConsumer removeEnricher) { + super(name, entityUpdateType) + + this.composingField = composingField + + this.addEnricher = addEnricher + this.removeEnricher = removeEnricher + } + + @Override + Mutant add(final Object value) { + final Mutant.EntityUpdateMetadata entityUpdateMetadata = Mutant.EntityUpdateMetadata.builder() + .field(composingField) + .type(getEntityUpdateType()) + .operation(EntityUpdateOperation.ADD) + .mutationValue(value) + .build() + + Mutant.builder().updatingEntity(entityUpdateMetadata).build() + } + + @Override + void enrichAdd(final C completeEntity, final AV addValue) { + addEnricher.accept(completeEntity, addValue) + } + + @Override + Mutant remove(final Object key) { + final Mutant.EntityUpdateMetadata entityUpdateMetadata = Mutant.EntityUpdateMetadata.builder() + .field(composingField) + .type(getEntityUpdateType()) + .operation(EntityUpdateOperation.DELETENOP) + .mutationValue(key) + .build() + + Mutant.builder().updatingEntity(entityUpdateMetadata).build() + } + + @Override + void enrichRemove(final C completeEntity, final RV removeValue) { + removeEnricher.accept(completeEntity, removeValue) + } + + @Override + boolean equals(final o) { + super.equals(o) + } + + @Override + int hashCode() { + super.hashCode() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Field.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Field.groovy new file mode 100644 index 0000000000..8499adcb2e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Field.groovy @@ -0,0 +1,15 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +/** + * A Field of a table. + * + * @author Yazad Khambata + */ +interface Field { + + String ITSELF = "_" + + String getName() + + String getAlias() +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Growable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Growable.groovy new file mode 100644 index 0000000000..908dba2d17 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Growable.groovy @@ -0,0 +1,15 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.Mutant + +/** + * A field whose value supports "add". + * + * @author Yazad Khambata + */ +interface Growable extends MutableProperty { + Mutant add(value) + + void enrichAdd(final C completeEntity, final AV addValue) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ItselfField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ItselfField.groovy new file mode 100644 index 0000000000..d87391af23 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/ItselfField.groovy @@ -0,0 +1,75 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ConstraintGenerator +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Represents the AtlasEntity itself. It has 2 main purposes. It can either be used as a "select *" (even though * is + * not a valid literal in Groovy). Or it can be used for working with constraints where the + * {@link org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.BinaryOperation} is to be applied on the entire object and + * not ant specific field. + * + * @author Yazad Khambata + */ +class ItselfField extends AbstractField implements Selectable, Constrainable { + ItselfField() { + super(Field.ITSELF) + } + + @Override + def read(final AtlasEntity atlasEntity) { + return atlasEntity + } + + @Override + Constraint was(final Map params, final Class atlasEntityClass) { + was(params, this, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final Class atlasEntityClass) { + has(params, this, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint was(final Map params, final Field delegateField, final Class atlasEntityClass) { + return ConstraintGenerator.instance.was(params, delegateField, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final Field delegateField, final Class atlasEntityClass) { + return ConstraintGenerator.instance.was(params, delegateField, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint was(final Map params, final ScanType scanStrategy, final Class atlasEntityClass) { + was(params, this, scanStrategy, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final ScanType scanStrategy, final Class atlasEntityClass) { + has(params, this, scanStrategy, atlasEntityClass) + } + + @Override + Constraint was(final Map params, final Field delegateField, final ScanType scanStrategy, final Class atlasEntityClass) { + return ConstraintGenerator.instance.was(params, delegateField, scanStrategy, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final Field delegateField, final ScanType scanStrategy, final Class atlasEntityClass) { + return ConstraintGenerator.instance.was(params, delegateField, scanStrategy, atlasEntityClass) + } + + @Override + boolean equals(final Object o) { + return super.equals(o) + } + + @Override + int hashCode() { + return super.hashCode() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/MutableProperty.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/MutableProperty.groovy new file mode 100644 index 0000000000..3835d18572 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/MutableProperty.groovy @@ -0,0 +1,12 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.EntityUpdateType + +/** + * A field that is mutable in any way. + * + * @author Yazad Khambata + */ +interface MutableProperty { + EntityUpdateType getEntityUpdateType() +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Overridable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Overridable.groovy new file mode 100644 index 0000000000..94346edd14 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Overridable.groovy @@ -0,0 +1,15 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.Mutant + +/** + * A Field whose value can be replaced or overridden. + * + * @author Yazad Khambata + */ +interface Overridable extends MutableProperty { + Mutant to(value) + + void enrichOverride(final C completeEntity, final OV overrideValue) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/OverridableFiled.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/OverridableFiled.groovy new file mode 100644 index 0000000000..d9f39203dd --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/OverridableFiled.groovy @@ -0,0 +1,55 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.EntityUpdateOperation +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.EntityUpdateType +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.Mutant + +import java.util.function.BiConsumer + +/** + * An implementation of a general overridable field. + * + * @author Yazad Khambata + */ +@PackageScope +class OverridableFiled extends AbstractMutableField implements Overridable { + + private Field composingField + + private BiConsumer overrideEnricher + + OverridableFiled(final String name, final EntityUpdateType entityUpdateType, final Field composingField, final BiConsumer overrideEnricher) { + super(name, entityUpdateType) + this.composingField = composingField + this.overrideEnricher = overrideEnricher + } + + @Override + Mutant to(final Object value) { + final Mutant.EntityUpdateMetadata entityUpdateMetadata = Mutant.EntityUpdateMetadata.builder() + .field(composingField) + .type(getEntityUpdateType()) + .operation(EntityUpdateOperation.OVERRIDE) + .mutationValue(value) + .build() + + Mutant.builder().updatingEntity(entityUpdateMetadata).build() + } + + @Override + void enrichOverride(final C completeEntity, final OV overrideValue) { + overrideEnricher.accept(completeEntity, overrideValue) + } + + @Override + boolean equals(final o) { + super.equals(o) + } + + @Override + int hashCode() { + super.hashCode() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Readable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Readable.groovy new file mode 100644 index 0000000000..0b89a5142a --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Readable.groovy @@ -0,0 +1,14 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * A field whose value can be read either by the AQL user or the engine. + * + * @author Yazad Khambata + */ +@PackageScope +interface Readable { + def read(final AtlasEntity atlasEntity) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/SelectOnlyField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/SelectOnlyField.groovy new file mode 100644 index 0000000000..510c39438d --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/SelectOnlyField.groovy @@ -0,0 +1,85 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * A field that can only be used in the select list of a query. + * + * @author Yazad Khambata + */ +class SelectOnlyField extends AbstractField implements Selectable { + + SelectOnlyField(final String name) { + super(name) + } + + /** + * Maintaining of a Mapping of MetaMethod(s) by Class since MetaMethod cached on a class like + * say MultiNode cannot be applied to a different class PackedNode. + */ + private Map, MetaMethod> metaMethods = [:] + + private boolean optionalReturnType + + @Override + def read(final AtlasEntity atlasEntity) { + final Class clazz = atlasEntity.class + + MetaMethod metaMethod = null + + try { + // Groovy Meta-classes do not include methods that have default implementations in interfaces, + // hence special handling is needed. + if (name == "osmTags") { + return atlasEntity.getOsmTags() + } + + //Detect Access Style. + final String fieldToLookFor = getName() + + metaMethod = metaMethods[clazz] + + if (metaMethod == null) { + + synchronized (this) { + + if (metaMethod == null) { + metaMethod = atlasEntity.metaClass.methods.stream() + .filter({ m -> m.name == fieldToLookFor || m.name == "get${fieldToLookFor.substring(0, 1).toUpperCase()}${fieldToLookFor.substring(1)}" }) + .filter({ m -> m.getParameterTypes().size() == 0 }) + .findFirst() + .get() + + metaMethods[clazz] = metaMethod + optionalReturnType = metaMethod.getReturnType() == Optional.class + } + } + } + + Valid.notEmpty metaMethod, "metaMethod is NULL for ${getName()}." + + //Access + final Object value = metaMethod.invoke(atlasEntity, null) + + //Optional handling + if (!optionalReturnType) { + return value + } + + return ((Optional) value).orElse(null) + } catch (Exception e) { + throw new IllegalStateException("Couldn't access ${getName()} (metaMethod identified: ${metaMethod}; optionalReturnType identified: ${optionalReturnType}) from ${atlasEntity}", e) + } + } + + @Override + boolean equals(final Object o) { + return super.equals(o) + } + + @Override + int hashCode() { + return super.hashCode() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/SelectOnlyMappedField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/SelectOnlyMappedField.groovy new file mode 100644 index 0000000000..f7eafd222c --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/SelectOnlyMappedField.groovy @@ -0,0 +1,38 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Function + +/** + * Similar to a SelectOnlyField but the value is derived from a mapping function. + * + * @author Yazad Khambata + */ +class SelectOnlyMappedField extends SelectOnlyField { + private Function mapper + + SelectOnlyMappedField(final String name, Function mapper) { + super(name) + this.mapper = mapper + } + + @Override + def read(final AtlasEntity atlasEntity) { + final Object originalValue = super.read(atlasEntity) + + def mappedValue = mapper.apply(originalValue) + + mappedValue + } + + @Override + boolean equals(final Object o) { + return super.equals(o) + } + + @Override + int hashCode() { + return super.hashCode() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Selectable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Selectable.groovy new file mode 100644 index 0000000000..5a4aa64560 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Selectable.groovy @@ -0,0 +1,9 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +/** + * A readable field that can be included in the select list. + * + * @author Yazad Khambata + */ +interface Selectable extends Readable { +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Shrinkable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Shrinkable.groovy new file mode 100644 index 0000000000..b346fd7c36 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/Shrinkable.groovy @@ -0,0 +1,15 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.Mutant + +/** + * A field whose value supports remove. + * + * @author Yazad Khambata + */ +interface Shrinkable extends MutableProperty { + Mutant remove(Object key) + + void enrichRemove(final C completeEntity, final RV removeValue) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/StandardField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/StandardField.groovy new file mode 100644 index 0000000000..e83c816f2c --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/StandardField.groovy @@ -0,0 +1,88 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * A general purpose field implementation that supports most operations. + * + * @author Yazad Khambata + */ +class StandardField extends AbstractField implements Selectable, Constrainable { + + @Delegate + Selectable selectableField + + Constrainable constrainableField + + StandardField(final String name) { + super(name) + + selectableField = new SelectOnlyField(name) + constrainableField = new ConstrainableFieldImpl(name) + } + + @Override + Constraint was(final Map params, final Class atlasEntityClass) { + this.was(params, this, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint was(final Map params, final Field delegateField, final Class atlasEntityClass) { + constrainableField.was(params, delegateField, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final Class atlasEntityClass) { + this.has(params, this, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final Field delegateField, final Class atlasEntityClass) { + constrainableField.has(params, delegateField, ScanType.FULL, atlasEntityClass) + } + + @Override + Constraint was(final Map params, final ScanType bestCandidateScanStrategy, final Class atlasEntityClass) { + this.was(params, this, bestCandidateScanStrategy, atlasEntityClass) + } + + @Override + Constraint was(final Map params, final Field delegateField, final ScanType bestCandidateScanStrategy, final Class atlasEntityClass) { + constrainableField.was(params, delegateField, bestCandidateScanStrategy, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final ScanType bestCandidateScanStrategy, final Class atlasEntityClass) { + this.has(params, this, bestCandidateScanStrategy, atlasEntityClass) + } + + @Override + Constraint has(final Map params, final Field delegateField, final ScanType bestCandidateScanStrategy, final Class atlasEntityClass) { + constrainableField.has(params, delegateField, bestCandidateScanStrategy, atlasEntityClass) + } + + @Override + boolean equals(final o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + if (!super.equals(o)) return false + + final StandardField that = (StandardField) o + + if (constrainableField != that.constrainableField) return false + if (selectableField != that.selectableField) return false + + return true + } + + @Override + int hashCode() { + int result = super.hashCode() + result = 31 * result + (selectableField != null ? selectableField.hashCode() : 0) + result = 31 * result + (constrainableField != null ? constrainableField.hashCode() : 0) + return result + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/StandardMappedField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/StandardMappedField.groovy new file mode 100644 index 0000000000..305ff59427 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/StandardMappedField.groovy @@ -0,0 +1,33 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Function + +/** + * Similar to a standard field but value is derived from a mapping function. + * + * @author Yazad Khambata + */ +class StandardMappedField extends StandardField { + + StandardMappedField(final String name, final Function mapper) { + super(name) + + selectableField = new SelectOnlyMappedField(name, mapper) + constrainableField = new ConstrainableFieldImpl(name) + } + + @Override + boolean equals(final Object o) { + return super.equals(o) + } + + @Override + int hashCode() { + return super.hashCode() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/TagsField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/TagsField.groovy new file mode 100644 index 0000000000..3701c783d0 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/TagsField.groovy @@ -0,0 +1,49 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.apache.commons.lang3.tuple.Pair +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.EntityUpdateType +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +import java.util.function.BiConsumer + +/** + * Represents the tags field in the entity tables. + * + * @author Yazad Khambata + */ +class TagsField extends CollectionField, Map, String> { + + private static final BiConsumer> overrideEnricher = { completeEntity, overrideValue -> + final Pair pair = toPair(overrideValue) + completeEntity.withTags(pair.getKey(), pair.getValue()) + } + + private static final BiConsumer> addEnricher = { completeEntity, addValue -> + final Pair pair = toPair(addValue) + completeEntity.withAddedTag(pair.getKey(), pair.getValue()) + } + + private static final BiConsumer removeEnricher = { completeEntity, removeValue -> completeEntity.withRemovedTag(removeValue) } + + TagsField(final String name) { + super(name, EntityUpdateType.TAGS, overrideEnricher, addEnricher, removeEnricher) + } + + private static Map.Entry toPair(final Map valueAsMap) { + Valid.isTrue valueAsMap.size() == 1 + final Map.Entry entry = valueAsMap.entrySet().iterator().next() + + Pair.of(entry.getKey(), entry.getValue()) + } + + @Override + boolean equals(final Object o) { + return super.equals(o) + } + + @Override + int hashCode() { + return super.hashCode() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/TerminalNodeIdField.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/TerminalNodeIdField.groovy new file mode 100644 index 0000000000..6e3d75472f --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/TerminalNodeIdField.groovy @@ -0,0 +1,61 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.items.ConnectedNodeType +import org.openstreetmap.atlas.geography.atlas.items.Node + +import java.util.function.Function + +/** + * Represents the mapped field for terminal nodes in an Edge table. + * + * @author Yazad Khambata + */ +class TerminalNodeIdField extends StandardMappedField { + final String alias + + TerminalNodeIdField(final ConnectedNodeType connectedEdgeType, final String alias) { + super(toName(connectedEdgeType), toId) + this.alias = alias + } + + private static Function toId = { Node node -> node.identifier } + + private static String toName(final ConnectedNodeType connectedNodeType) { + toInfo(connectedNodeType)["name"] + } + + private static toInfo(ConnectedNodeType connectedNodeType) { + terminalNodeInfoMapping[connectedNodeType] + } + + private static Map terminalNodeInfoMapping = [ + (ConnectedNodeType.START) : [name: "start", alias: "startId"], + (ConnectedNodeType.END): [name: "end", alias: "endId"] + ] + + @Override + String getAlias() { + alias + } + + @Override + boolean equals(final o) { + if (this.is(o)) return true + if (getClass() != o.class) return false + if (!super.equals(o)) return false + + final TerminalNodeIdField that = (TerminalNodeIdField) o + + if (alias != that.alias) return false + + return true + } + + @Override + int hashCode() { + int result = super.hashCode() + result = 31 * result + (alias != null ? alias.hashCode() : 0) + return result + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/EntityUpdateBehaviorsSupported.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/EntityUpdateBehaviorsSupported.groovy new file mode 100644 index 0000000000..28410b23b4 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/EntityUpdateBehaviorsSupported.groovy @@ -0,0 +1,120 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.mutant + +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +import java.util.stream.Collectors + +/** + * A domain that represents the nature of operations supported during a mutation on a Mutable field. + * + * @author Yazad Khambata + */ +class EntityUpdateBehaviorsSupported { + /** + * Add an item inside the values. + */ + private boolean add = false + + /** + * Update one of the values. + */ + private boolean update = false + + /** + * Delete one of the items in the value. + */ + private boolean delete = false + + /** + * Update the complete value. + */ + private boolean override = false + + /** + * Upsert which is a blend + * of update and insert. + * + * See wiktionary. + */ + private boolean upsert = false + + /** + * Delete No Operation (NOP or NOOP) + * means if the value being deleted doesn't exist it completes silently. + */ + private boolean deleteNOP = false + + boolean isEntityUpdateBehavior(final EntityUpdateOperation entityUpdateBehavior) { + this[entityUpdateBehavior.opName] + } + + Set supportedEntityUpdateBehavior() { + Arrays.stream(EntityUpdateOperation.values()).filter { isEntityUpdateBehavior(it) }.collect(Collectors.toSet()) + } + + private static class SupportedUpdateBehaviorBuilder { + + private boolean add = false + private boolean update = false + private boolean delete = false + private boolean override = false + private boolean upsert = false + private boolean deleteNOP = false + + SupportedUpdateBehaviorBuilder add() { + this.add = true + this + } + + SupportedUpdateBehaviorBuilder update() { + this.update = true + this + } + + SupportedUpdateBehaviorBuilder delete() { + this.delete = true + this + } + + SupportedUpdateBehaviorBuilder override() { + this.override = true + this + } + + SupportedUpdateBehaviorBuilder upsert() { + this.upsert = true + + this + } + + SupportedUpdateBehaviorBuilder deleteNOP() { + this.deleteNOP = true + + this + } + + EntityUpdateBehaviorsSupported build() { + final EntityUpdateBehaviorsSupported supportedUpdateBehavior = new EntityUpdateBehaviorsSupported() + supportedUpdateBehavior.add = this.add + supportedUpdateBehavior.update = this.update + supportedUpdateBehavior.delete = this.delete + supportedUpdateBehavior.override = this.override + + if (upsert) { + Valid.isTrue add && update, "Add and Upsert must be supported to support UPSERT." + } + supportedUpdateBehavior.upsert = upsert + + if (deleteNOP) { + Valid.isTrue delete, "Delete must be supported to support DELETE-NOP." + } + supportedUpdateBehavior.deleteNOP = deleteNOP + + supportedUpdateBehavior + } + } + + static SupportedUpdateBehaviorBuilder builder() { + new SupportedUpdateBehaviorBuilder() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/EntityUpdateOperation.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/EntityUpdateOperation.groovy new file mode 100644 index 0000000000..a7e35513e3 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/EntityUpdateOperation.groovy @@ -0,0 +1,78 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.mutant + +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.field.Growable +import org.openstreetmap.atlas.geography.atlas.dsl.field.MutableProperty +import org.openstreetmap.atlas.geography.atlas.dsl.field.Overridable +import org.openstreetmap.atlas.geography.atlas.dsl.field.Shrinkable + +/** + * Enum that keeps track of the actual mutation operation. + * + * @author Yazad Khambata + */ +enum EntityUpdateOperation { + ADD { + @Override + void perform(final MutableProperty mutableProperty, final CompleteEntity completeEntity, final V value) { + ((Growable)mutableProperty).enrichAdd(completeEntity, value) + } + }, + + UPDATE { + @Override + void perform(final MutableProperty mutableProperty, final CompleteEntity completeEntity, final V value) { + performUpdate(mutableProperty, completeEntity, value) + } + }, + + DELETE { + @Override + void perform(final MutableProperty mutableProperty, final CompleteEntity completeEntity, final V value) { + ((Shrinkable)mutableProperty).enrichRemove(completeEntity, value) + } + }, + + OVERRIDE { + @Override + void perform(final MutableProperty mutableProperty, final CompleteEntity completeEntity, final V value) { + ((Overridable)mutableProperty).enrichOverride(completeEntity, value) + } + }, + + UPSERT { + @Override + void perform(final MutableProperty mutableProperty, final CompleteEntity completeEntity, final V value) { + performUpdate(mutableProperty, completeEntity, value) + } + }, + + DELETENOP("deleteNOP") { + @Override + void perform(final MutableProperty mutableProperty, final CompleteEntity completeEntity, final V value) { + ((Shrinkable)mutableProperty).enrichRemove(completeEntity, value) + } + }; + + private String opName + + EntityUpdateOperation() { + this(null) + } + + EntityUpdateOperation(final String opName) { + this.opName = opName + } + + String getOpName() { + opName?:this.name().toLowerCase() + } + + abstract void perform(final MutableProperty mutableProperty, final CompleteEntity completeEntity, final V value) + + + private void performUpdate(final MutableProperty mutableProperty, final CompleteEntity completeEntity, final V value) { + //Update value operation can be improved by creating "Updatable fields" as its own interface. + throw new UnsupportedOperationException() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/EntityUpdateType.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/EntityUpdateType.groovy new file mode 100644 index 0000000000..1e2a1d93f7 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/EntityUpdateType.groovy @@ -0,0 +1,17 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.mutant + +/** + * Currently the only entity update supported but Geometry and relation updates will be supported in the future. + * + * @author Yazad Khambata + */ +enum EntityUpdateType { + + TAGS(EntityUpdateBehaviorsSupported.builder().add().update().delete().override().upsert().deleteNOP().build()); + + final EntityUpdateBehaviorsSupported entityUpdateBehaviorsSupported + + EntityUpdateType(final EntityUpdateBehaviorsSupported entityUpdateBehaviorsSupported) { + this.entityUpdateBehaviorsSupported = entityUpdateBehaviorsSupported + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/Mutant.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/Mutant.groovy new file mode 100644 index 0000000000..e80387d241 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/Mutant.groovy @@ -0,0 +1,83 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.mutant + +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.field.Field + +/** + * Represents a change or mutation. + * + * @param V - the value type. Some examples, for add tag the value will be a Map with one Entry. + * In case of a tag remove it would be One String. In case of an overite it would be a Map of zero + * or more Entries. + * + * @author Yazad Khambata + */ +@ToString +class Mutant { + + MutationType mutationType + + EntityUpdateMetadata entityUpdateMetadata + + @Builder + @ToString + class EntityUpdateMetadata { + //Note: field and entityUpdateType is redundant info at this point. + Field field + + EntityUpdateType type + + EntityUpdateOperation operation + + V mutationValue + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this) + } + } + + static MutantBuilder builder() { + new MutantBuilder() + } + + static class MutantBuilder { + + private MutationType mutationType + + private EntityUpdateMetadata entityUpdateMetadata + + //Delete does not need a mutant just yet. And create needs some design thought. + + MutantBuilder updatingEntity(final EntityUpdateMetadata entityUpdateMetadata) { + this.mutationType = MutationType.UPDATE_ENTITY + this.entityUpdateMetadata = entityUpdateMetadata + this + } + + Mutant build() { + final Mutant mutant = new Mutant() + mutant.mutationType = mutationType + mutant.entityUpdateMetadata = entityUpdateMetadata + mutant + } + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/MutationType.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/MutationType.groovy new file mode 100644 index 0000000000..e2a30f73ba --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/mutant/MutationType.groovy @@ -0,0 +1,14 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.mutant + +/** + * Mutation Type as an enum. + * + * @author Yazad Khambata + */ +enum MutationType { + + //Delete entity doesn't need mutants, and create entity needs some design thought. + + UPDATE_ENTITY; + +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathQueryFilePack.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathQueryFilePack.groovy new file mode 100644 index 0000000000..f1eec5acc9 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathQueryFilePack.groovy @@ -0,0 +1,57 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.path + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +import java.nio.file.Path + +/** + * A domain the represents the query and related information like file name and signature. + * + * @author Yazad Khambata + */ +@Builder +@ToString(includeNames = true, excludes = ["fileName"]) +@EqualsAndHashCode(excludes = ["fileName"]) +class PathQueryFilePack implements Comparable { + String fileName + String query + String signature + + static PathQueryFilePack from(Map.Entry> entry) { + final String key = entry.getKey() + final List values = entry.getValue() + + Valid.isTrue values.size() >= 1 && values.size() <= 2 + + String query = "" + String signature = "" + + for (def path : values) { + if (path.toString() == key) { + query = path.toFile().text + } else { + signature = path.toFile().text + } + } + + PathQueryFilePack.builder() + .fileName(key) + .query(query) + .signature(signature) + .build() + } + + @Override + int compareTo(final PathQueryFilePack other) { + final int queryCompare = Objects.compare(this.query, other.query, Comparator.naturalOrder()) + + if (queryCompare == 0) { + return Objects.compare(this.signature, other.signature, Comparator.naturalOrder()) + } + + return queryCompare + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathQueryFilePackCollection.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathQueryFilePackCollection.groovy new file mode 100644 index 0000000000..ec72db7b5d --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathQueryFilePackCollection.groovy @@ -0,0 +1,30 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.path + +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +import java.util.stream.Stream + +/** + * A collection of one or more PathQueryFilePack(s). + * + * @author Yazad Khambata + */ +@ToString +@EqualsAndHashCode +class PathQueryFilePackCollection implements Iterable { + List classpathQueryFilePackList + + PathQueryFilePackCollection(final List classpathQueryFilePackList) { + this.classpathQueryFilePackList = classpathQueryFilePackList + } + + @Override + Iterator iterator() { + classpathQueryFilePackList.iterator() + } + + Stream stream() { + classpathQueryFilePackList.stream() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathUtil.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathUtil.groovy new file mode 100644 index 0000000000..91677f8ed8 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathUtil.groovy @@ -0,0 +1,79 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.path + +import org.apache.commons.lang3.Validate +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.util.stream.Collectors + +/** + * A util to work with Java NIO Paths. + * + * @author Yazad Khambata + */ +@Singleton +class PathUtil { + private static final Logger log = LoggerFactory.getLogger(PathUtil.class); + + PathQueryFilePackCollection aqlFilesFromClasspath(final String classpathDirectory) { + Valid.notEmpty classpathDirectory + + final URL url = this.getClass().getClassLoader().getResource(classpathDirectory) + + Validate.notNull(url, "url is NULL for ${classpathDirectory}") + + final Path basePath = Paths.get(url.toURI()) + aqlFilesFromBasePath(basePath) + } + + PathQueryFilePackCollection aqlFilesFromPath(String aqlHomePath) { + final Path basePath = Paths.get(aqlHomePath) + aqlFilesFromBasePath(basePath) + } + + void copyFolder(Path src, Path dest) { + Files + .walk(src) + .forEach { + source -> copy(source, dest.resolve(src.relativize(source))) + } + } + + private void copy(Path source, Path dest) { + Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING) + } + + void deleteRecursivelyQuietly(Path pathToDelete) { + try { + Files.walk(pathToDelete).sorted(Comparator.reverseOrder()).forEach { path -> Files.delete(path) } + } catch (Exception e) { + log.warn("delete failed for ${pathToDelete}.", e) + } + } + + private PathQueryFilePackCollection aqlFilesFromBasePath(Path basePath) { + final List paths = Files.walk(basePath) + .filter { path -> !Files.isDirectory(path) } + .collect(Collectors.toList()) + + final Map> groupedFiles = paths.stream() + .collect(Collectors.groupingBy { Path path -> path.toString().replace(".sig", "") }) + + final PathQueryFilePackCollection classpathQueryFilePackCollection = + groupedFiles.entrySet().stream() + .map { entry -> PathQueryFilePack.from(entry) } + .collect( + Collectors.collectingAndThen( + Collectors.toList(), + { list -> new PathQueryFilePackCollection(list) } + ) + ) + + classpathQueryFilePackCollection + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/ConditionalConstruct.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/ConditionalConstruct.groovy new file mode 100644 index 0000000000..d2a29414f5 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/ConditionalConstruct.groovy @@ -0,0 +1,59 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import groovy.transform.builder.Builder +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.Predicatable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.chained.ConditionalConstructPredicate +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Predicate + +/** + * A constraint packed with the clause (where, and, or). + * + * @author Yazad Khambata + */ +@Builder +class ConditionalConstruct implements Predicatable { + Statement.Clause clause + Constraint constraint + + @Override + Predicate toPredicate(final Class entityClass) { + final Predicate predicate = constraint.toPredicate(entityClass) + + ConditionalConstructPredicate + .builder() + .conditionalConstruct(this) + .predicate(predicate) + .build() + } + + @Override + String toString() { + "ConditionalConstruct: [${clause} ${constraint}]" + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this) + } + + ConditionalConstruct deepCopy() { + shallowCopyWithConstraintOverride(constraint.deepCopy()) + } + + ConditionalConstruct shallowCopyWithConstraintOverride(final Constraint constraint) { + ConditionalConstruct.builder() + .clause(this.clause) + .constraint(constraint) //Not deep-copied - deep copy explicitly if needed. + .build() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/ConditionalConstructList.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/ConditionalConstructList.groovy new file mode 100644 index 0000000000..90ca08bc15 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/ConditionalConstructList.groovy @@ -0,0 +1,58 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Collectors + +/** + * A List of Conditional Constructs in the order in which they appear in the query. + * Optimization involves changes to the content and order of the contents of this list. + * + * @author Yazad Khambata + */ +class ConditionalConstructList implements List> { + @Delegate + List> conditionalConstructs + + ConditionalConstructList() { + this([]) + } + + ConditionalConstructList(final List> conditionalConstructs) { + this.conditionalConstructs = conditionalConstructs + } + + Optional> getFirst() { + final ConditionalConstruct conditionalConstruct = conditionalConstructs.size() == 0 ? null : conditionalConstructs.get(0) + Optional.ofNullable(conditionalConstruct) + } + + List> getExcludingFirst() { + final int size = this.size() + if (size <= 1) { + return [] + } + + conditionalConstructs.subList(1, size) + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this) + } + + ConditionalConstructList deepCopy() { + final List> copyOfConditionalConstructs = conditionalConstructs.stream() + .map { conditionalConstruct -> conditionalConstruct.deepCopy() } + .collect(Collectors.toList()) + + new ConditionalConstructList(copyOfConditionalConstructs) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Delete.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Delete.groovy new file mode 100644 index 0000000000..b83a509b70 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Delete.groovy @@ -0,0 +1,115 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.change.Change +import org.openstreetmap.atlas.geography.atlas.change.ChangeBuilder +import org.openstreetmap.atlas.geography.atlas.change.FeatureChange +import org.openstreetmap.atlas.geography.atlas.complete.CompleteItemType +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.query.commit.Commitable +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.MutantResult +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.Selector +import org.openstreetmap.atlas.geography.atlas.dsl.util.StreamUtil +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Collectors +import java.util.stream.StreamSupport + +/** + * Representation of a Delete statement. + * + * @author Yazad Khambata + */ +@Builder +@ToString +class Delete extends Query implements Commitable { + AtlasTable table + + ConditionalConstructList conditionalConstructList + + @Override + Statement type() { + Statement.DELETE + } + + @Override + Result execute(final ConsoleWriter consoleWriter) { + final Selector selector = Selector. builder() + .with(table) + .with(conditionalConstructList) + .with(this.type()) + .build() + final Iterable recordsToDelete = selector.fetchMatchingEntities() + + consoleWriter.echo "----------------------------------------------------" + consoleWriter.echo "Directly Affected Records: " + consoleWriter.echo "----------------------------------------------------" + + recordsToDelete.forEach { consoleWriter.echo it } + + consoleWriter.echo "Note:- Deletes cascade as updates and deletes to other tables. The cascade changes are not shown here. Use the diff command for more details." + + final Atlas atlasContext = table.atlasMediator.atlas + + final Change change = StreamUtil.stream(recordsToDelete) + .map { E entity -> FeatureChange.remove( CompleteItemType.from(entity.getType()).completeEntityFrom(entity), atlasContext) } + .collect(Collectors.collectingAndThen(Collectors.toList(), { listFeatureChanges -> ChangeBuilder.newInstance().addAll(listFeatureChanges).get() })) + + final List relevantIdentifiers = StreamUtil.stream(recordsToDelete) + .map { E entity -> entity.getIdentifier() } + .collect(Collectors.toList()) + + final Result result = MutantResult.builder() + .with(table) + .with(change) + .with(relevantIdentifiers) + .with(table.atlasMediator) + .build() + + result + } + + @Override + Delete shallowCopy() { + this.shallowCopy(false) + } + + @Override + Query shallowCopy(final boolean excludeConditionalConstructList) { + this.shallowCopyWithConditionalConstructList(shallowCopyConditionalConstructList(excludeConditionalConstructList)) + } + + @Override + Query shallowCopyWithConditionalConstructList(final ConditionalConstructList conditionalConstructList) { + Delete.builder() + .table(this.table) + .conditionalConstructList(conditionalConstructList) + .build() + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this) + } + + @Override + String toPrettyString() { + """ +DELETE + ${table} +WHERE + ${conditionalConstructList} + """ + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/InnerSelectWrapper.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/InnerSelectWrapper.groovy new file mode 100644 index 0000000000..aa7b1f523e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/InnerSelectWrapper.groovy @@ -0,0 +1,85 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Collectors + +/** + * Executes immediately and sets the identifiers, so the inner query can be used safely in an update statement's + * where clause without concerns of side-effects. + * + * The inner query ignores the select list and looks for the Ids in + * the {@link org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result}. + * + * @author Yazad Khambata + */ +class InnerSelectWrapper { + Long[] identifiers + + private InnerSelectWrapper(final Select select) { + this(toRelevantIdentifiers(select) as Long[]) + } + + private InnerSelectWrapper(final QueryBuilder selectQueryBuilder) { + this(toSelect(selectQueryBuilder)) + } + + private InnerSelectWrapper(final Long[] identifiers) { + this.identifiers = identifiers + } + + private static Select toSelect(QueryBuilder selectQueryBuilder) { + Valid.notEmpty selectQueryBuilder + Valid.isTrue selectQueryBuilder.getBase() == Statement.SELECT + + (Select) selectQueryBuilder.buildQuery() + } + + private static List toRelevantIdentifiers(Select select) { + Valid.notEmpty select + + select.executeQuietly().relevantIdentifiers + } + + static InnerSelectWrapper from(final QueryBuilder selectQueryBuilder) { + new InnerSelectWrapper<>(selectQueryBuilder) + } + + static InnerSelectWrapper from(final Long[] identifiers) { + new InnerSelectWrapper<>(identifiers) + } + + boolean equals(Object obj) { + if (obj == null) + return false + + if (obj.is(this)) + return true + + if (obj.getClass() != getClass()) + return false + + final InnerSelectWrapper that = (InnerSelectWrapper) obj + + sortedList(this.identifiers) == sortedList(that.identifiers) + } + + @Override + int hashCode() { + new HashCodeBuilder() + .appendSuper(0) //Don't append super. + .append(sortedList(this.identifiers)) + .toHashCode() + } + + private List sortedList(final Long[] identifiers) { + Arrays.stream(identifiers).sorted().collect(Collectors.toList()) + } + + @Override + String toString() { + "" + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Query.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Query.groovy new file mode 100644 index 0000000000..4ca1bf031b --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Query.groovy @@ -0,0 +1,58 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.openstreetmap.atlas.geography.Rectangle +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.console.impl.QuietConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.console.impl.StandardOutputConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Abstraction of a query. + * + * @author Yazad Khambata + */ +abstract class Query { + abstract Statement type() + + Result execute() { + execute(StandardOutputConsoleWriter.getInstance()) + } + + abstract Result execute(final ConsoleWriter consoleWriter) + + Result executeQuietly() { + execute(QuietConsoleWriter.getInstance()) + } + + abstract String toPrettyString() + + abstract ConditionalConstructList getConditionalConstructList() + + abstract AtlasTable getTable() + + Atlas atlas() { + def atlas = getTable().atlasMediator.atlas + + Valid.notEmpty atlas + + atlas + } + + Rectangle bounds() { + atlas().bounds() + } + + abstract Query shallowCopy() + + abstract Query shallowCopy(final boolean excludeConditionalConstructList) + + abstract Query shallowCopyWithConditionalConstructList(final ConditionalConstructList conditionalConstructList) + + protected ConditionalConstructList shallowCopyConditionalConstructList(boolean excludeConditionalConstructList) { + excludeConditionalConstructList == false ? this.conditionalConstructList : null + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryBuilder.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryBuilder.groovy new file mode 100644 index 0000000000..95a494385e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryBuilder.groovy @@ -0,0 +1,206 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import groovy.transform.ToString +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.QueryAnalyzer +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.domain.Analysis +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.impl.QueryAnalyzerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.difference.Difference +import org.openstreetmap.atlas.geography.atlas.dsl.query.difference.DifferenceGenerator +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.RuleBasedOptimizerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.field.Field +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.Mutant +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement.Clause +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.dsl.schema.mutant.MutantAtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.NotConstraint +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * The actual query builder, accessed via the corresponding builder-factory. + * + * @author Yazad Khambata + */ +@ToString +class QueryBuilder { + + Statement base + + /** + * Non-condition clauses + */ + Map clauses = [:] + + List conditionalConstructs = new ConditionalConstructList() + + private newQuerySetup(final Statement base) { + Valid.isTrue this.base == null, "BUG! base is not NULL. ${this.base}, trying to set base to: ${base}." + Valid.isTrue clauses.isEmpty(), "BUG! clauses must be empty. ${clauses}" + Valid.isTrue conditionalConstructs.isEmpty(), "BUG! conditionalConstructs must be empty. ${conditionalConstructs}" + + this.base = base + } + + Closure using = { String uri -> + newQuerySetup(Statement.USING) + + clauses[Clause.__IT__] = uri + + new AtlasSchema(uri) + } + + def select = { Field... fields -> + newQuerySetup(Statement.SELECT) + + this.clauses[Statement.Clause.__IT__] = Arrays.asList(fields) + + this + } + + private onSchema = { Statement statement, Clause clause, AtlasTable table -> + Valid.isTrue statement == base, "Statement mismatch error." + Valid.isTrue statement in [Statement.SELECT, Statement.UPDATE, Statement.DELETE], "${statement} unexpected" + + Valid.isTrue statement.isClauseAllowed(clause), "Unexpected clause ${clause} in ${base} statement." + + Valid.notEmpty table, "Table is NULL! statement: ${statement}; clause: ${clause}" + + clauses[clause] = table + + this + } + + def from = { AtlasTable table -> + Valid.notEmpty this.base, "base statement is NULL." + + onSchema this.base, Clause.FROM, table + } + + private condition = { Clause clause, Constraint constraint -> + Valid.isTrue base in [Statement.SELECT, Statement.UPDATE, Statement.DELETE] + + final ConditionalConstruct conditionalConstruct = + ConditionalConstruct.builder().clause(clause).constraint(constraint).build() + + conditionalConstructs.add(conditionalConstruct) + + this + } + + + def where = { Constraint constraint -> + condition Clause.WHERE, constraint + } + + def and = { Constraint constraint -> + condition Clause.AND, constraint + } + + def or = { Constraint constraint -> + condition Clause.OR, constraint + } + + public static not = { Constraint constraint -> + NotConstraint.from(constraint) + } + + def limit = { long to -> + Valid.isTrue base == Statement.SELECT + + this.clauses[Statement.Clause.LIMIT] = to + + this + } + + def update = { AtlasTable table -> + newQuerySetup(Statement.UPDATE) + + onSchema this.base, Clause.__IT__, table + } + + def set = { Mutant... mutants -> + Valid.isTrue base == Statement.UPDATE, "Set can only be used with ${Statement.UPDATE} statement, not ${base}." + + clauses[Clause.SET] = Arrays.asList(mutants) + + this + } + + def delete = { AtlasTable table -> + newQuerySetup(Statement.DELETE) + + onSchema this.base, Clause.__IT__, table + } + + Closure commit = { final QueryBuilder...queryBuilders -> + newQuerySetup(Statement.COMMIT) + + clauses[Clause.__IT__] = [queryBuilders: queryBuilders] + + new AtlasSchema(new MutantAtlasMediator(queryBuilders)) + } + + Closure explain = { queryOrBuilder -> + newQuerySetup(Statement.EXPLAIN) + + clauses[Clause.__IT__] = [queryOrBuilder: queryOrBuilder] + + //ExplainerImpl.instance.dumpExplanation(queryOrBuilder) + + QueryAnalyzer queryAnalyzer = new QueryAnalyzerImpl(ExplainerImpl.instance, RuleBasedOptimizerImpl.defaultOptimizer()) + + final Analysis analysis = queryAnalyzer.analyze(queryOrBuilder) + analysis + } + + Closure difference = { final AtlasSchema atlas1, final AtlasSchema atlas2 -> + newQuerySetup(Statement.DIFFERENCE) + + clauses[Clause.__IT__] = [atlas1: atlas1, atlas2: atlas2] + + DifferenceGenerator.getInstance().generateAndDumpDifference(atlas1, atlas2) + } + + private Select buildSelectQuery() { + Select.builder() + .fieldsToSelect(clauses[Clause.__IT__]) + .table(clauses[Clause.FROM]) + .conditionalConstructList(conditionalConstructs) + .limit(clauses[Clause.LIMIT]) + .build() + } + + private Update buildUpdateQuery() { + Update.builder() + .table(clauses[Clause.__IT__]) + .mutants(clauses[Clause.SET]) + .conditionalConstructList(conditionalConstructs) + .build() + } + + private Delete buildDeleteQuery() { + Delete.builder() + .table(clauses[Clause.__IT__]) + .conditionalConstructList(conditionalConstructs) + .build() + } + + private Map> statementToQueryBuildMapping = [ + (Statement.SELECT): { return buildSelectQuery() }, + (Statement.UPDATE): { return buildUpdateQuery() }, + (Statement.DELETE): { return buildDeleteQuery() }, + ] + + Query buildQuery() { + statementToQueryBuildMapping[base]() + } + + @Override + String toString() { + return buildQuery().toPrettyString() + } +} \ No newline at end of file diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryBuilderFactory.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryBuilderFactory.groovy new file mode 100644 index 0000000000..dfa20b5017 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryBuilderFactory.groovy @@ -0,0 +1,45 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.openstreetmap.atlas.geography.atlas.dsl.field.Field +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.domain.Analysis +import org.openstreetmap.atlas.geography.atlas.dsl.query.difference.Difference +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * An entry point for AQL that offeres easy access to query objects with static imports. + * + * @author Yazad Khambata + */ +class QueryBuilderFactory { + + static QueryBuilder builder() { + new QueryBuilder() + } + + //Statements + static final Closure select = { final Field... fields -> QueryBuilderFactory.builder().select(fields) } + static final Closure update = { final AtlasTable table -> QueryBuilderFactory.builder().update(table) } + static final Closure delete = { final AtlasTable table -> QueryBuilderFactory.builder().delete(table) } + + //Special case of NOT clause. + static final Closure not = { final Constraint constraint -> QueryBuilder.not(constraint) } + + //Commands returning AtlasSchema + static final Closure using = { final String uri -> QueryBuilderFactory.builder().using(uri) } + static final Closure commit = { final QueryBuilder... queryBuilders -> QueryBuilderFactory.builder().commit(queryBuilders) } + static final Closure analyze = { final queryOrBuilder -> QueryBuilderFactory.builder().explain(queryOrBuilder) } + static final Closure explain = QueryBuilderFactory.analyze + + //Commands returning Result + static final Closure exec = { QueryBuilder qb -> qb.buildQuery().execute() } + static final Closure execute = QueryBuilderFactory.exec + + //Commands returning Result + static final Closure diff = { final AtlasSchema atlas1, final AtlasSchema atlas2 -> QueryBuilderFactory.builder().difference(atlas1, atlas2) } + static final Closure difference = QueryBuilderFactory.diff +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Select.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Select.groovy new file mode 100644 index 0000000000..e40404cd54 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Select.groovy @@ -0,0 +1,130 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.field.Field +import org.openstreetmap.atlas.geography.atlas.dsl.field.Selectable +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.IdempotentResult +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.IdempotentResult.IdempotentResultBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.Selector +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * A representation of a Select query. + * + * @author Yazad Khambata + */ +@Builder +@ToString +class Select extends Query { + List fieldsToSelect + + AtlasTable table + + ConditionalConstructList conditionalConstructList + + Long limit + + def Map read(final E entity) { + final Map map = new LinkedHashMap() + + fieldsToSelect.forEach({ fieldToSelect -> + map.put(fieldAlias(fieldToSelect), ((Selectable) fieldToSelect).read(entity)) + }) + + map + } + + private String fieldAlias(final Field fieldToSelect) { + fieldToSelect.alias + } + + @Override + Statement type() { + Statement.SELECT + } + + @Override + Result execute(final ConsoleWriter consoleWriter) { + final Selector selector = Selector. builder() + .with(table) + .with(conditionalConstructList) + .with(this.type()) + .build() + + final Iterable entities = selector.fetchMatchingEntities() + + int index = 0 + + final boolean checkLimit = limit > 0 + + final IdempotentResultBuilder idempotentResultBuilder = IdempotentResult.builder() + idempotentResultBuilder.setTable(table) + + for (E entity : entities) { + ++index + + if (checkLimit && index > limit) { + break + } + + idempotentResultBuilder.addingRelevantIdentifiers(entity.getIdentifier()) + + if (!consoleWriter.isTurnedOff()) { + consoleWriter.echo "${index}\t :: ${read(entity)}" + } + entity.getIdentifier() + } + + idempotentResultBuilder.build() + } + + @Override + Select shallowCopy() { + shallowCopy(false) + } + + @Override + Query shallowCopy(final boolean excludeConditionalConstructList) { + this.shallowCopyWithConditionalConstructList(shallowCopyConditionalConstructList(excludeConditionalConstructList)) + } + + @Override + Query shallowCopyWithConditionalConstructList(final ConditionalConstructList conditionalConstructList) { + Select.builder() + .fieldsToSelect(this.fieldsToSelect) + .table(this.table) + .conditionalConstructList(conditionalConstructList) + .limit(this.limit) + .build() + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this) + } + + @Override + String toPrettyString() { + """\ +SELECT + ${fieldsToSelect} +FROM + ${table} +WHERE + ${conditionalConstructList} +LIMIT + ${limit} + """ + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Statement.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Statement.groovy new file mode 100644 index 0000000000..eb75c991d2 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Statement.groovy @@ -0,0 +1,67 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +/** + * An enum of statements and commands and the clauses allowed with them. + * + * @author Yazad Khambata + */ +enum Statement { + USING, + + SELECT(Clause.FROM, Clause.WHERE, Clause.AND, Clause.OR, Clause.LIMIT), + + UPDATE(Clause.SET, Clause.WHERE, Clause.AND, Clause.OR), + + DELETE(Clause.WHERE, Clause.AND, Clause.OR), + + COMMIT, + + EXPLAIN, + + DIFFERENCE + ; + + private List allowedClauses + + Statement(Clause...allowedClauses) { + this.allowedClauses = Arrays.asList(allowedClauses) + } + + def from(final String statementAsStr) { + Statement.valueOf(statementAsStr) + } + + def isClauseAllowed(Clause clause) { + clause == Clause.__IT__ || this.allowedClauses.contains(clause) + } + + String closureName() { + this.name().toLowerCase() + } + + static enum Clause { + /** + * To represent the statement itself. + */ + __IT__, + + FROM, + + WHERE, + + AND, + + OR, + + NOT, + + LIMIT, + + SET + ; + + def from(final String clauseAsStr) { + Clause.valueOf(clauseAsStr) + } + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Update.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Update.groovy new file mode 100644 index 0000000000..131c70bf34 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/Update.groovy @@ -0,0 +1,140 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.change.Change +import org.openstreetmap.atlas.geography.atlas.change.ChangeBuilder +import org.openstreetmap.atlas.geography.atlas.change.FeatureChange +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.complete.CompleteItemType +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.Mutant +import org.openstreetmap.atlas.geography.atlas.dsl.query.commit.Commitable +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.MutantResult +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.Selector +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Collectors +import java.util.stream.StreamSupport + +/** + * An update statement representation. + * + * @author Yazad Khambata + */ +@Builder +@ToString +class Update extends Query implements Commitable { + AtlasTable table + + List mutants + + ConditionalConstructList conditionalConstructList + + @Override + Statement type() { + Statement.UPDATE + } + + @Override + Result execute(final ConsoleWriter consoleWriter) { + final Selector selector = Selector. builder() + .with(table) + .with(conditionalConstructList) + .with(this.type()) + .build() + final Iterable recordsToUpdate = selector.fetchMatchingEntities() + + consoleWriter.echo "----------------------------------------------------" + consoleWriter.echo "Affected Records: " + consoleWriter.echo "----------------------------------------------------" + + recordsToUpdate.forEach { consoleWriter.echo it } + + consoleWriter.echo "----------------------------------------------------" + consoleWriter.echo "Mutants: " + consoleWriter.echo "----------------------------------------------------" + + final List mutants = this.getMutants() + + mutants.forEach { consoleWriter.echo it } + + final Atlas atlasContext = table.atlasMediator.atlas + + final MutantResult.MutantResultBuilder mutantResultBuilder = MutantResult.builder() + + final Change change = StreamSupport.stream(recordsToUpdate.spliterator(), false).map { final E atlasEntity -> + mutantResultBuilder.addingRelevantIdentifiers(atlasEntity.getIdentifier()) + + final CompleteItemType completeItemType = CompleteItemType.from(table.getTableSetting().itemType) + + final CompleteEntity completeEntity = completeItemType.completeEntityFrom(atlasEntity) + + mutants.forEach { mutant -> + mutant.entityUpdateMetadata.operation.perform(mutant.entityUpdateMetadata.field, completeEntity, mutant.entityUpdateMetadata.mutationValue) + } + + completeEntity + }.map { completeEntity -> + //For Delete statements we must use remove instead of add. + //FeatureChange.add(completeEntity, atlasContext) // This line should be uncommented after fix from Atlas. + FeatureChange.add(completeEntity, atlasContext) + }.collect(Collectors.collectingAndThen(Collectors.toList(), { listFeatureChanges -> ChangeBuilder.newInstance().addAll(listFeatureChanges).get() })) + + consoleWriter.echo "----------------------------------------------------" + consoleWriter.echo "Change: " + consoleWriter.echo "----------------------------------------------------" + + change.changes().forEach { featureChange -> + consoleWriter.echo "${featureChange.identifier} ${featureChange.changeType} ${featureChange.bounds()} ${featureChange.getAfterView().tags}" + } + + mutantResultBuilder.with(table).with(change).with(table.atlasMediator).build() + } + + @Override + Update shallowCopy() { + shallowCopy(false) + } + + @Override + Query shallowCopy(final boolean excludeConditionalConstructList) { + this.shallowCopyWithConditionalConstructList(shallowCopyConditionalConstructList(excludeConditionalConstructList)) + } + + @Override + Query shallowCopyWithConditionalConstructList(final ConditionalConstructList conditionalConstructList) { + Update.builder() + .table(this.table) + .mutants(this.mutants) + .conditionalConstructList(conditionalConstructList) + .build() + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this) + } + + @Override + String toPrettyString() { + """\ +UPDATE + ${table} +SET + ${mutants} +WHERE + ${conditionalConstructList} +""" + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/analyzer/QueryAnalyzer.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/analyzer/QueryAnalyzer.groovy new file mode 100644 index 0000000000..74d4f3093d --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/analyzer/QueryAnalyzer.groovy @@ -0,0 +1,24 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer + +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.domain.Analysis +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explainer +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.Optimizer +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * A query analyzer is an overarching wrapper over an Explainer and an Optimizer and is responsible to iteratively + * optimizing a query. + * + * @author Yazad Khambata + */ +interface QueryAnalyzer { + Explainer getExplainer() + + Optimizer getOptimizer() + + Analysis analyze(final Query query) + + Analysis analyze(final QueryBuilder queryBuilder) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/analyzer/domain/Analysis.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/analyzer/domain/Analysis.groovy new file mode 100644 index 0000000000..4c55fb3285 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/analyzer/domain/Analysis.groovy @@ -0,0 +1,22 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.domain + +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Domain representing the analysis. + * + * @author Yazad Khambata + */ +@Builder +class Analysis { + Query originalQuery + Query optimizedQuery + Map>, Query> optimizationTrace + + boolean checkIfOptimized() { + !optimizedQuery?.is(originalQuery) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/analyzer/impl/QueryAnalyzerImpl.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/analyzer/impl/QueryAnalyzerImpl.groovy new file mode 100644 index 0000000000..7345073845 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/analyzer/impl/QueryAnalyzerImpl.groovy @@ -0,0 +1,99 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.impl + +import org.apache.commons.lang3.tuple.Pair +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.QueryAnalyzer +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.domain.Analysis +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explainer +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.Optimizer +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationResult +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Collectors + +/** + * A general purpose Query Analyzer implementation. + * + * @author Yazad Khambata + */ +class QueryAnalyzerImpl implements QueryAnalyzer { + + Explainer explainer + + Optimizer optimizer + + QueryAnalyzerImpl(final Explainer explainer, final Optimizer optimizer) { + this.explainer = explainer + this.optimizer = optimizer + } + + @Override + Analysis analyze(final Query originalQuery) { + Query knownMostOptimalQuery = originalQuery + Explanation explanation + + final List optimizationResultList = [] + + for (QueryOptimizationTransformer queryOptimizationTransformer : getOptimizer().getQueryOptimizationTransformers()) { + explanation = getExplainer().explain(knownMostOptimalQuery) + final OptimizationRequest optimizationRequest = QueryAnalyzerImpl.from(explanation) + + final OptimizationResult optimizationResult = getOptimizer().optimizeIfPossible(optimizationRequest, queryOptimizationTransformer) + + if (optimizationResult.checkIfOptimized()) { + knownMostOptimalQuery = optimizationResult.optimizedQuery + optimizationResultList.add(optimizationResult) + } + } + + final Map>, Query> optimizationTrace = QueryAnalyzerImpl.from(optimizationResultList) + + return createAnalysis(originalQuery, knownMostOptimalQuery, optimizationTrace) + } + + @Override + Analysis analyze(final QueryBuilder queryBuilder) { + analyze(queryBuilder.buildQuery()) + } + + private Analysis createAnalysis(Query originalQuery, Query knownMostOptimalQuery, Map>, Query> optimizationTrace) { + Analysis.builder() + .originalQuery(originalQuery) + .optimizedQuery(knownMostOptimalQuery) + .optimizationTrace(optimizationTrace) + .build() + } + + static Map>, Query> from(List optimizationResultList) { + optimizationResultList.stream() + .map { optimizationResult -> + final Class> queryOptimizationTransformerClass = optimizationResult.queryOptimizationTransformer.getClass() + final Query optimizedQuery = optimizationResult.getOptimizedQuery() + + Pair.of(queryOptimizationTransformerClass, optimizedQuery) + } + .collect( + Collectors.toMap( + { final Pair>, Query> pair -> pair.getKey() }, + { final Pair>, Query> pair -> pair.getValue() } + ) + ) + } + + static OptimizationRequest from(final Explanation explanation) { + Valid.notEmpty explanation + + OptimizationRequest.builder() + .query(explanation.query) + .table(explanation.table) + .scanStrategy(explanation.scanStrategy) + .statement(explanation.statement) + .hasUnusedBetterIndexScanOptions(explanation.hasUnusedBetterIndexScanOptions()) + .build() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/commit/Commitable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/commit/Commitable.groovy new file mode 100644 index 0000000000..54123fd1be --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/commit/Commitable.groovy @@ -0,0 +1,9 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.commit + +/** + * Statements that support committing. + * + * @author Yazad Khambata + */ +interface Commitable { +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/difference/Difference.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/difference/Difference.groovy new file mode 100644 index 0000000000..c3962054fa --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/difference/Difference.groovy @@ -0,0 +1,35 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.difference + +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.change.Change +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema + +import java.util.stream.Collectors + +/** + * Domain representing the difference. + * + * @author Yazad Khambata + */ +@Builder +class Difference { + AtlasSchema atlasSchema1 + AtlasSchema atlasSchema2 + + Change change + + @Override + String toString() { + """ +================================================================================================ +Atlas 1 : ${atlasSchema1} +Atlas 2 : ${atlasSchema2} + +Has Difference : ${change != null} + +Differences : +${change.changes().map {featureChange -> "\t${featureChange}" }.collect(Collectors.joining("\n")) } +================================================================================================ +""" + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/difference/DifferenceGenerator.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/difference/DifferenceGenerator.groovy new file mode 100644 index 0000000000..36b599daba --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/difference/DifferenceGenerator.groovy @@ -0,0 +1,40 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.difference + +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.change.Change +import org.openstreetmap.atlas.geography.atlas.change.diff.AtlasDiff +import org.openstreetmap.atlas.geography.atlas.dsl.console.ConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.console.impl.StandardOutputConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema + +/** + * Generates the difference between 2 atlas schemas. + * + * @author Yazad Khambata + */ +@Singleton +class DifferenceGenerator { + + Difference generateAndDumpDifference(final AtlasSchema atlasSchema1, final AtlasSchema atlasSchema2, final ConsoleWriter consoleWriter) { + final Atlas atlas1 = atlasSchema1.atlasMediator.atlas + final Atlas atlas2 = atlasSchema2.atlasMediator.atlas + + final AtlasDiff atlasDiff = new AtlasDiff(atlas1, atlas2) + + final Optional optionalChange = atlasDiff.generateChange() + + final Difference difference = Difference.builder() + .atlasSchema1(atlasSchema1) + .atlasSchema2(atlasSchema2) + .change(optionalChange.orElseGet { -> null }) + .build() + + consoleWriter.echo(difference) + + difference + } + + Difference generateAndDumpDifference(final AtlasSchema atlasSchema1, final AtlasSchema atlasSchema2) { + this.generateAndDumpDifference(atlasSchema1, atlasSchema2, StandardOutputConsoleWriter.getInstance()) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/Explainer.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/Explainer.groovy new file mode 100644 index 0000000000..48f49f6d41 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/Explainer.groovy @@ -0,0 +1,17 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.explain + +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Provides an explanation of why an index is or is not used. Note that the explanation does not suggest + * or perform optimizations directly. + * + * @author Yazad Khambata + */ +interface Explainer { + Explanation explain(final QueryBuilder queryBuilder) + + Explanation explain(Query query) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/ExplainerImpl.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/ExplainerImpl.groovy new file mode 100644 index 0000000000..f99f7cc4af --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/ExplainerImpl.groovy @@ -0,0 +1,34 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.explain + +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.strategy.ScanStrategizer +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.strategy.ScanStrategy +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * @author Yazad Khambata + */ +@Singleton +class ExplainerImpl implements Explainer { + Explanation explain(final QueryBuilder queryBuilder) { + final Query query = queryBuilder.buildQuery() + + explain(query) + } + + Explanation explain(Query query) { + final Statement statement = query.type() + final ScanStrategy scanStrategy = ScanStrategizer. getInstance().strategize(query.conditionalConstructList) + final AtlasTable table = query.table + + Explanation. builder() + .statement(statement) + .scanStrategy(scanStrategy) + .table(table) + .query(query) + .build() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/Explanation.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/Explanation.groovy new file mode 100644 index 0000000000..0d6ac0966f --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/Explanation.groovy @@ -0,0 +1,41 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.explain + +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.IndexSetting +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.strategy.ScanStrategy +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Domain representing the explanation of the explain command. + * + * @author Yazad Khambata + */ +@Builder +class Explanation { + Query query + AtlasTable table + ScanStrategy scanStrategy + Statement statement + + boolean hasUnusedBetterIndexScanOptions() { + final IndexSetting indexSettingForFirstClause = scanStrategy.indexUsageInfo.indexSetting + final int rankOfScanTypeInUse = indexSettingForFirstClause == null?ScanType.FULL.preferntialRank:indexSettingForFirstClause.scanType.preferntialRank + + final int bestOfTheRest = scanStrategy.conditionalConstructListExcludingIndexedConstraintIfAny.stream() + .map { conditionalConstruct -> conditionalConstruct.constraint } + .map { constraint -> constraint.bestCandidateScanType } + .mapToInt { it.preferntialRank } + .min() + .orElse(ScanType.FULL.preferntialRank) + + //return true when ID index is used but the for the second condition we are performing a full scan for another id. + //therefore >= and not just >. + final boolean bestOptionUsed = (rankOfScanTypeInUse < ScanType.FULL.preferntialRank)?rankOfScanTypeInUse >= bestOfTheRest:rankOfScanTypeInUse > bestOfTheRest + + bestOptionUsed + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/IndexNonUseReason.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/IndexNonUseReason.groovy new file mode 100644 index 0000000000..126a29de34 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/explain/IndexNonUseReason.groovy @@ -0,0 +1,16 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.explain + +/** + * Reasons for not benefiting from available indexes. + * + * @author Yazad Khambata + */ +enum IndexNonUseReason { + NO_WHERE_CLAUSE, + + FIRST_WHERE_CLAUSE_NEEDS_FULL_SCAN, + + FIRST_WHERE_CLAUSE_USES_NOT_OPERATOR, //special case of FIRST_WHERE_CLAUSE_NEEDS_FULL_SCAN + + WHERE_HAS_OR_USED +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/Optimizer.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/Optimizer.groovy new file mode 100644 index 0000000000..9fbe8ded39 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/Optimizer.groovy @@ -0,0 +1,18 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer + +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationResult +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * A general interface for an Optimizer - the optimizer performs ∃! optimization + * + * @author Yazad Khambata + */ +interface Optimizer { + QueryOptimizationTransformer[] getQueryOptimizationTransformers() + + OptimizationResult optimizeIfPossible(final OptimizationRequest optimizationRequest, + final QueryOptimizationTransformer queryOptimizationTransformer) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/RuleBasedOptimizerImpl.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/RuleBasedOptimizerImpl.groovy new file mode 100644 index 0000000000..10666bee92 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/RuleBasedOptimizerImpl.groovy @@ -0,0 +1,70 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer + +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationResult +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl.GeometricSurfacesOverlapOptimization +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl.GeometricSurfacesWithinOptimization +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl.IdsInOptimization +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl.ReorderingOptimization +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Analogous to a Rule based optimization in a query engine. + * + * @author Yazad Khambata + */ +class RuleBasedOptimizerImpl implements Optimizer { + QueryOptimizationTransformer[] queryOptimizationTransformers + + RuleBasedOptimizerImpl(final QueryOptimizationTransformer... queryOptimizationTransformers) { + this.queryOptimizationTransformers = queryOptimizationTransformers + } + + /** + * @param optimizationRequest + * @return + */ + OptimizationResult optimizeIfPossible(final OptimizationRequest optimizationRequest, final QueryOptimizationTransformer queryOptimizationTransformer) { + if (queryOptimizationTransformer.isApplicable(optimizationRequest)) { + final Query optimizedQuery = queryOptimizationTransformer.applyTransformation(optimizationRequest) + + return optimized(optimizationRequest, optimizedQuery, queryOptimizationTransformer) + } + + return notOptimized(optimizationRequest, queryOptimizationTransformer) + } + + private static OptimizationResult optimized(OptimizationRequest optimizationRequest, Query optimizedQuery, QueryOptimizationTransformer queryOptimizationTransformer) { + OptimizationResult.builder() + .originalQuery(optimizationRequest.getQuery()) + .optimizedQuery(optimizedQuery) + .queryOptimizationTransformer(queryOptimizationTransformer) + .build() + } + + private static OptimizationResult notOptimized(OptimizationRequest optimizationRequest, QueryOptimizationTransformer queryOptimizationTransformer) { + OptimizationResult.builder() + .originalQuery(optimizationRequest.getQuery()) + .optimizedQuery(optimizationRequest.getQuery()) + .queryOptimizationTransformer(queryOptimizationTransformer) + .build() + } + + /** + * Order is important here, since we allow multiple optimizations to be applied at once. + * When spatial index conditions are involved it is possible that more than 2 are applied + * (within and overlap optimizations) + * + * @return - An Optimizer with configured QueryOptimizationTransformer. + */ + static RuleBasedOptimizerImpl defaultOptimizer() { + new RuleBasedOptimizerImpl( + ReorderingOptimization.instance, + IdsInOptimization.instance, + GeometricSurfacesWithinOptimization.instance, + GeometricSurfacesOverlapOptimization.instance + ) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/domain/OptimizationInfo.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/domain/OptimizationInfo.groovy new file mode 100644 index 0000000000..3708da51e9 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/domain/OptimizationInfo.groovy @@ -0,0 +1,49 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain + +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Domain representing the results of an optimization. + * + * @author Yazad Khambata + */ +final class OptimizationInfo> { + private Query originalQuery + private Query optimizedQuery + + private Map, Query> optimizationTrace + + OptimizationInfo(final Query originalQuery) { + this.originalQuery = this.optimizedQuery = originalQuery + } + + OptimizationInfo(final Query originalQuery, final Query optimizedQuery, final Map, Query> optimizationTrace) { + this.originalQuery = originalQuery + this.optimizedQuery = optimizedQuery + this.optimizationTrace = optimizationTrace + } + + boolean isOptimized() { + !optimizedQuery?.is(originalQuery) + } + + static OptimizationInfo> notOptimized(final Query originalQuery) { + Valid.notEmpty originalQuery + return new OptimizationInfo<>(originalQuery) + } + + static > OptimizationInfo optimized(final Query originalQuery, final Query optimizedQuery, final Map, Query> optimizationTrace) { + Valid.notEmpty originalQuery + Valid.notEmpty optimizedQuery + Valid.notEmpty optimizationTrace + return new OptimizationInfo<>(originalQuery, optimizedQuery, optimizationTrace) + } + + @Override + String toString() { + "isOptimized: ${isOptimized()}; originalQuery: ${originalQuery}; optimizedQuery: ${optimizedQuery}; optimizationTrace: ${optimizationTrace}." + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/domain/OptimizationRequest.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/domain/OptimizationRequest.groovy new file mode 100644 index 0000000000..d956f66d91 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/domain/OptimizationRequest.groovy @@ -0,0 +1,22 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain + +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.strategy.ScanStrategy +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Inputs to the optimization process. + * + * @author Yazad Khambata + */ +@Builder +class OptimizationRequest { + Query query + AtlasTable table + ScanStrategy scanStrategy + Statement statement + boolean hasUnusedBetterIndexScanOptions +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/domain/OptimizationResult.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/domain/OptimizationResult.groovy new file mode 100644 index 0000000000..9e696a46b8 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/domain/OptimizationResult.groovy @@ -0,0 +1,25 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain + +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * @author Yazad Khambata + */ +@Builder +class OptimizationResult { + Query originalQuery + Query optimizedQuery + + QueryOptimizationTransformer queryOptimizationTransformer + + boolean checkIfOptimized() { + Valid.notEmpty originalQuery + Valid.notEmpty optimizedQuery + + !originalQuery.is(optimizedQuery) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/QueryOptimizationTransformer.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/QueryOptimizationTransformer.groovy new file mode 100644 index 0000000000..3533be1844 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/QueryOptimizationTransformer.groovy @@ -0,0 +1,14 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization + +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * @author Yazad Khambata + */ +interface QueryOptimizationTransformer { + boolean isApplicable(final OptimizationRequest optimizationRequest) + + Query applyTransformation(final OptimizationRequest optimizationRequest) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/AbstractQueryOptimizationTransform.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/AbstractQueryOptimizationTransform.groovy new file mode 100644 index 0000000000..d4e2b9d4fd --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/AbstractQueryOptimizationTransform.groovy @@ -0,0 +1,57 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstruct +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.stream.Collectors +import java.util.stream.Stream + +/** + * An abstraction of an optimization transformation process. + * + * @author Yazad Khambata + */ +abstract class AbstractQueryOptimizationTransform implements QueryOptimizationTransformer { + private static final Logger log = LoggerFactory.getLogger(AbstractQueryOptimizationTransform.class) + + @Override + final boolean isApplicable(final OptimizationRequest optimizationRequest) { + if (QueryOptimizationHelper.instance.considerOptimization(optimizationRequest)) { + final boolean additionalChecksMet = areAdditionalChecksMet(optimizationRequest) + + log.info("AbstractQueryOptimizationTransform::isApplicable -> additionalChecksMet: ${additionalChecksMet}.") + + return additionalChecksMet + } + + log.info("AbstractQueryOptimizationTransform::isApplicable -> Basic checks NOT met!") + return false + } + + protected Set andOrClausesInUse(final OptimizationRequest optimizationRequest) { + conditionalConstructListStream(optimizationRequest) + .map { it.clause } + .filter { it != Statement.Clause.WHERE } + .distinct() + .collect(Collectors.toSet()) + } + + private Stream> conditionalConstructListStream(final OptimizationRequest optimizationRequest) { + optimizationRequest.query.conditionalConstructList.stream() + } + + protected Set scanTypeAvailable(final OptimizationRequest optimizationRequest) { + conditionalConstructListStream(optimizationRequest) + .map { conditionalConstruct -> conditionalConstruct.constraint.bestCandidateScanType } + .distinct() + .collect(Collectors.toSet()) + } + + abstract boolean areAdditionalChecksMet(final OptimizationRequest optimizationRequest) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesOverlapOptimization.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesOverlapOptimization.groovy new file mode 100644 index 0000000000..f427ee3185 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesOverlapOptimization.groovy @@ -0,0 +1,72 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.openstreetmap.atlas.geography.GeometricSurface +import org.openstreetmap.atlas.geography.MultiPolygon +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstructList +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.polygon.GeometricSurfaceSupport +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Collectors + +/** + * Also called Optimization-4, which removes the polygons with a within check that are outside the + * bounds of the atlas. + * + * @author Yazad Khambata + */ +@Singleton +class GeometricSurfacesOverlapOptimization extends AbstractQueryOptimizationTransform { + @Override + boolean areAdditionalChecksMet(final OptimizationRequest optimizationRequest) { + optimizationRequest.query.conditionalConstructList.stream() + .filter { conditionalConstruct -> + conditionalConstruct.constraint.bestCandidateScanType == ScanType.SPATIAL_INDEX + } + .findAny() + .isPresent() + } + + @Override + Query applyTransformation(final OptimizationRequest optimizationRequest) { + final Query query = optimizationRequest.getQuery() + final GeometricSurface bounds = query.bounds() + + final ConditionalConstructList optimizedConditionalConstructList = query.conditionalConstructList.stream().map { conditionalConstruct -> + def constraint = conditionalConstruct.constraint + + if (constraint.bestCandidateScanType != ScanType.SPATIAL_INDEX) { + //NOP + return conditionalConstruct + } + + //Check if value needs to be changed and updated it. + final Object valueToCheck = constraint.valueToCheck + + //Value to check must be List of List of List of BigDecimal or a MultiPolygon (GeometricSurface) + Valid.isTrue valueToCheck instanceof List || valueToCheck instanceof MultiPolygon + + final Optional optionalGeometricSurface = GeometricSurfaceSupport.instance.toGeometricSurface(valueToCheck, bounds) + + final Constraint copiedConstraint = optionalGeometricSurface.map { geometricSurface -> + constraint.deepCopyWithNewValueToCheck(geometricSurface) + }.orElseThrow { -> new UnsupportedOperationException("") } + + def copiedConditionalConstruct = conditionalConstruct.shallowCopyWithConstraintOverride(copiedConstraint) + copiedConditionalConstruct + }.collect( + Collectors.collectingAndThen( + Collectors.toList(), + { listOfConditionalConstructs -> new ConditionalConstructList<>(listOfConditionalConstructs) } + ) + ) + + final Query optimizedQuery = query.shallowCopyWithConditionalConstructList(optimizedConditionalConstructList) + + optimizedQuery + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesWithinOptimization.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesWithinOptimization.groovy new file mode 100644 index 0000000000..9529042b4d --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesWithinOptimization.groovy @@ -0,0 +1,75 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstruct +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstructList +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.CommonFields +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.BasicConstraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.BinaryOperations +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.openstreetmap.atlas.geography.atlas.items.Relation + +import java.util.stream.Collectors + +/** + * Also called Optimization-3, combines multiple within checks to one to encourage use of the + * spatial index. + * + * An optimization that can kick in, + * + * IFF, + * 1. All constraints are based on SPATIAL_INDEX - isWithin(GeometricSurface) + * 2. Only or clause is used. + * + * @author Yazad Khambata + */ +@Singleton +class GeometricSurfacesWithinOptimization extends AbstractQueryOptimizationTransform { + @Override + boolean areAdditionalChecksMet(final OptimizationRequest optimizationRequest) { + //All ORs + //Only id as field and clauses allowed =, in, inner query (at least 2) - i.e Scan Type ID only. + onlyOrsUsed(optimizationRequest) && onlySpatialIndexScanTypeAvailable(optimizationRequest) + } + + private boolean onlyOrsUsed(final OptimizationRequest optimizationRequest) { + final Set andOrClausesInUse = andOrClausesInUse(optimizationRequest) + andOrClausesInUse.size() == 1 && andOrClausesInUse.contains(Statement.Clause.OR) + } + + boolean onlySpatialIndexScanTypeAvailable(final OptimizationRequest optimizationRequest) { + final Set scanTypes = scanTypeAvailable(optimizationRequest) + scanTypes.size() == 1 && scanTypes.contains(ScanType.SPATIAL_INDEX) + } + + + @Override + Query applyTransformation(final OptimizationRequest optimizationRequest) { + final Query query = optimizationRequest.getQuery() + final ConditionalConstructList conditionalConstructList = query.conditionalConstructList + + final List> combinedGeometricSurface = (conditionalConstructList.stream().map { + it.constraint + }.map { it.valueToCheck } + .flatMap { List> geometricSurface -> geometricSurface.stream() } + .collect(Collectors.toList())) + + query.shallowCopyWithConditionalConstructList(new ConditionalConstructList([ + ConditionalConstruct.builder() + .constraint( + BasicConstraint.builder() + .field(((CommonFields) query.table)._) + .operation(BinaryOperations.inside) + .valueToCheck(combinedGeometricSurface) + .bestCandidateScanType(ScanType.SPATIAL_INDEX) + .atlasEntityClass(query.table.entityClass) + .build() + ) + .clause(Statement.Clause.WHERE) + .build() + ])) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/IdsInOptimization.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/IdsInOptimization.groovy new file mode 100644 index 0000000000..7922380f1e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/IdsInOptimization.groovy @@ -0,0 +1,88 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.query.* +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.CommonFields +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.BasicConstraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.BinaryOperations +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.openstreetmap.atlas.geography.atlas.items.Relation + +import java.util.stream.Collectors + +/** + * Also called Optimization-2 + * + * An optimization that can kick in, + * + * IFF, + * 1. All constraints are based on ID_UNIQUE_INDEX - hasId(anId), hasIds(id1, id2, id3, ...), hasIds(innerSelect) is used + * 2. Only or clause is used. + * + * @author Yazad Khambata + */ +@Singleton +class IdsInOptimization extends AbstractQueryOptimizationTransform { + @Override + boolean areAdditionalChecksMet(final OptimizationRequest optimizationRequest) { + //All ORs + //Only id as field and clauses allowed =, in, inner query (at least 2) - i.e Scan Type ID only. + onlyOrsUsed(optimizationRequest) && onlyIdUniqueIndexScanTypeAvailable(optimizationRequest) + } + + private boolean onlyOrsUsed(final OptimizationRequest optimizationRequest) { + final Set andOrClausesInUse = andOrClausesInUse(optimizationRequest) + andOrClausesInUse.size() == 1 && andOrClausesInUse.contains(Statement.Clause.OR) + } + + boolean onlyIdUniqueIndexScanTypeAvailable(final OptimizationRequest optimizationRequest) { + final Set scanTypes = scanTypeAvailable(optimizationRequest) + scanTypes.size() == 1 && scanTypes.contains(ScanType.ID_UNIQUE_INDEX) + } + + + @Override + Query applyTransformation(final OptimizationRequest optimizationRequest) { + final Query query = optimizationRequest.getQuery() + final ConditionalConstructList conditionalConstructList = query.conditionalConstructList + + final List ids = (conditionalConstructList.stream().map { it.constraint }.map { it.valueToCheck } + .flatMap { + if (it instanceof Number) { + return [it].stream() + } + + if (it instanceof List) { + return it.stream() + } + + if (it instanceof Long[]) { + return Arrays.stream(it) + } + + if (it instanceof InnerSelectWrapper) { + return (it.identifiers as List).stream() + } + + throw new IllegalArgumentException("${it} : ${it?.class}") + } + .collect(Collectors.toList())) + + query.shallowCopyWithConditionalConstructList(new ConditionalConstructList([ + ConditionalConstruct.builder() + .constraint( + BasicConstraint.builder() + .field(((CommonFields)query.table).id) + .operation(BinaryOperations.inside) + .valueToCheck(ids as Long[]) + .bestCandidateScanType(ScanType.ID_UNIQUE_INDEX) + .atlasEntityClass(query.table.entityClass) + .build() + ) + .clause(Statement.Clause.WHERE) + .build() + ])) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/QueryOptimizationHelper.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/QueryOptimizationHelper.groovy new file mode 100644 index 0000000000..04dd81fe57 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/QueryOptimizationHelper.groovy @@ -0,0 +1,44 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstruct +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * Internal use optimization helper. + * + * @author Yazad Khambata + */ +@Singleton +@PackageScope +class QueryOptimizationHelper { + private static final Logger log = LoggerFactory.getLogger(QueryOptimizationHelper.class); + + boolean considerOptimization(final OptimizationRequest optimizationRequest) { + //For optimizations 1, 2, 3 + final boolean hasUnusedBetterIndexScanOptions = optimizationRequest.hasUnusedBetterIndexScanOptions + //For optimization 4 + final boolean hasSpatialCheck = hasSpatialCheck(optimizationRequest) + + final boolean considerOptimization = hasUnusedBetterIndexScanOptions || hasSpatialCheck + + log.info("QueryOptimizationHelper::considerOptimization -> hasUnusedBetterIndexScanOptions: ${hasUnusedBetterIndexScanOptions}; hasSpatialCheck: ${hasSpatialCheck}; considerOptimization: ${considerOptimization}.") + + considerOptimization + } + + private boolean hasSpatialCheck(final OptimizationRequest optimizationRequest) { + optimizationRequest.query + .conditionalConstructList?.stream() + .filter { ConditionalConstruct cc -> + cc.getConstraint().getBestCandidateScanType() == ScanType.SPATIAL_INDEX + } + .findAny() + .isPresent() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/ReorderingOptimization.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/ReorderingOptimization.groovy new file mode 100644 index 0000000000..ff9a22a5fd --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/ReorderingOptimization.groovy @@ -0,0 +1,68 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstruct +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstructList +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.stream.Collectors + +/** + * Also called Optimization-1 + * + * An optimization that can kick in, + * + * IFF, + * 0. Passes AbstractQueryOptimizationTransform#isApplicable(Explain) + * 1. only "and" clauses are used in the "where" clause (i.e. no "or" clauses in "where"). + * 2. The conditions use different scan types. + * 3. A condition using an inferior scan type is placed before a condition with a superior scan type. + * + * @author Yazad Khambata + */ +@Singleton +class ReorderingOptimization extends AbstractQueryOptimizationTransform { + private static final Logger log = LoggerFactory.getLogger(ReorderingOptimization.class); + + @Override + boolean areAdditionalChecksMet(final OptimizationRequest optimizationRequest) { + final boolean usesDiverseScanTypes = usesDiverseScanTypes(optimizationRequest) + final boolean noOrsUsed = this.noOrsUsed(optimizationRequest) + + log.info("usesDiverseScanTypes: ${usesDiverseScanTypes}; noOrsUsed: ${noOrsUsed}.") + + usesDiverseScanTypes && noOrsUsed + } + + private boolean usesDiverseScanTypes(final OptimizationRequest optimizationRequest) { + final Set scanTypesAvailable = scanTypeAvailable(optimizationRequest) + scanTypesAvailable.size() > 1 + } + + private boolean noOrsUsed(final OptimizationRequest optimizationRequest) { + final Set andOrClausesInUse = andOrClausesInUse(optimizationRequest) + andOrClausesInUse.size() == 1 && andOrClausesInUse.contains(Statement.Clause.AND) + } + + @Override + Query applyTransformation(final OptimizationRequest optimizationRequest) { + final Query query = optimizationRequest.getQuery() + final ConditionalConstructList copyOfConditionalConstructList = query.conditionalConstructList.deepCopy().stream() + .sorted(Comparator.comparing { ConditionalConstruct conditionalConstruct -> conditionalConstruct.constraint.bestCandidateScanType.preferntialRank }) + .map { conditionalConstruct -> conditionalConstruct.clause = Statement.Clause.AND; conditionalConstruct } + .collect(Collectors.toList()) + + if (copyOfConditionalConstructList.size() > 1) { + copyOfConditionalConstructList.get(0).clause = Statement.Clause.WHERE + } + final Query shallowCopyOfQuery = query.shallowCopyWithConditionalConstructList(copyOfConditionalConstructList) + + shallowCopyOfQuery + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/AbstractResult.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/AbstractResult.groovy new file mode 100644 index 0000000000..f0ce7786c1 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/AbstractResult.groovy @@ -0,0 +1,47 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.result + + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Stream + +/** + * Abstraction of a query result. + * + * @author Yazad Khambata + */ +@PackageScope +abstract class AbstractResult implements Result { + + @Override + Stream entityStream() { + final AtlasTable table = getTable() + Valid.notEmpty table + final List identifiers = getRelevantIdentifiers() + + final Stream stream = EntityStreamHelper. stream(table, identifiers) + + stream + } + + @Override + Stream entityStream(final AtlasMediator atlasMediator) { + EntityStreamHelper + . stream(getTable().getTableSetting().getItemType(), atlasMediator, getRelevantIdentifiers()) + } + + @Override + Iterator entityIterator() { + EntityStreamHelper. iterator(getTable(), getRelevantIdentifiers()) + } + + @Override + Iterator entityIterator(final AtlasMediator atlasMediator) { + EntityStreamHelper + . iterator(getTable().getTableSetting().getItemType(), atlasMediator, getRelevantIdentifiers()) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/CommitableResult.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/CommitableResult.groovy new file mode 100644 index 0000000000..8e4b641ad5 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/CommitableResult.groovy @@ -0,0 +1,21 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.result + +import org.openstreetmap.atlas.geography.atlas.change.Change +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Results that can be committed. + * + * @author Yazad Khambata + */ +interface CommitableResult extends Result { + /** + * The original Atlas to commit on. + */ + AtlasMediator getAtlasMediatorToCommitOn() + + Change getChange() + + +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/EntityIterable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/EntityIterable.groovy new file mode 100644 index 0000000000..b74404e398 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/EntityIterable.groovy @@ -0,0 +1,23 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.result + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Stream + +/** + * Contract to iterate over a collection of entities in a result. + * + * @author Yazad Khambata + */ +@PackageScope +interface EntityIterable { + Iterator entityIterator() + + Iterator entityIterator(final AtlasMediator atlasMediator) + + Stream entityStream() + + Stream entityStream(final AtlasMediator atlasMediator) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/EntityStreamHelper.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/EntityStreamHelper.groovy new file mode 100644 index 0000000000..093902c1b8 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/EntityStreamHelper.groovy @@ -0,0 +1,55 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.result + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.openstreetmap.atlas.geography.atlas.items.ItemType + +import java.util.stream.Stream + +/** + * Java's stream helper to work with entities. + * + * @author Yazad Khambata + */ +@PackageScope +final class EntityStreamHelper { + + private EntityStreamHelper() {} + + static Stream stream(final AtlasTable table, List relevantIdentifiers) { + final ItemType itemType = getItemType(table) + final AtlasMediator atlasMediator = getAtlasMediator(table) + + stream(itemType, atlasMediator, relevantIdentifiers) + } + + static Stream stream(final ItemType itemType, final AtlasMediator atlasMediator, List relevantIdentifiers) { + (relevantIdentifiers ?: []).stream().map { id -> itemType.entityForIdentifier(atlasMediator.atlas, id) } + } + + static Iterator iterator(final AtlasTable table, List relevantIdentifiers) { + final ItemType itemType = getItemType(table) + final AtlasMediator atlasMediator = getAtlasMediator(table) + + iterator(itemType, atlasMediator, relevantIdentifiers) + } + + static Iterator iterator(final ItemType itemType, final AtlasMediator atlasMediator, List relevantIdentifiers) { + stream(itemType, atlasMediator, relevantIdentifiers).iterator() + } + +// private static Atlas getAtlas(AtlasTable table) { +// getAtlasMediator(table).atlas +// } + + private static AtlasMediator getAtlasMediator(AtlasTable table) { + table.atlasMediator + } + + private static ItemType getItemType(AtlasTable table) { + table.tableSetting.itemType + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/IdempotentResult.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/IdempotentResult.groovy new file mode 100644 index 0000000000..7cca764bfd --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/IdempotentResult.groovy @@ -0,0 +1,45 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.result + +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Idempotent results - associated with a select statement. + * + * @author Yazad Khambata + */ +class IdempotentResult extends AbstractResult implements Result { + AtlasTable table + List relevantIdentifiers + + static IdempotentResultBuilder builder() { + new IdempotentResultBuilder() + } + + static class IdempotentResultBuilder { + AtlasTable table + List relevantIdentifiers = [] + + IdempotentResultBuilder with(AtlasTable table) { + this.table = table + this + } + + IdempotentResultBuilder with(final List relevantIdentifiers) { + this.relevantIdentifiers = relevantIdentifiers + this + } + + IdempotentResultBuilder addingRelevantIdentifiers(final Long relevantIdentifier) { + this.relevantIdentifiers += relevantIdentifier + this + } + + IdempotentResult build() { + final IdempotentResult idempotentResult = new IdempotentResult<>() + idempotentResult.setTable(table) + idempotentResult.setRelevantIdentifiers(relevantIdentifiers ?: []) + idempotentResult + } + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/MutantResult.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/MutantResult.groovy new file mode 100644 index 0000000000..3c5217fb6e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/MutantResult.groovy @@ -0,0 +1,76 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.result + +import org.openstreetmap.atlas.geography.atlas.change.Change +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Results associated with a mutation like a delete or update statement. + * + * @author Yazad Khambata + */ +class MutantResult extends AbstractResult implements CommitableResult { + AtlasTable table + + Change change + + AtlasMediator atlasMediatorToCommitOn + + List relevantIdentifiers + + static MutantResultBuilder builder() { + new MutantResultBuilder() + } + + static class MutantResultBuilder { + AtlasTable table + + Change change + + AtlasMediator atlasMediatorToCommitOn + + List relevantIdentifiers = [] + + MutantResultBuilder with(final AtlasTable table) { + this.table = table + + this + } + + MutantResultBuilder with(final Change change) { + this.change = change + + this + } + + MutantResultBuilder with(final AtlasMediator atlasMediatorToCommitOn) { + this.atlasMediatorToCommitOn = atlasMediatorToCommitOn + + this + } + + MutantResultBuilder with(final List relevantIdentifiers) { + this.relevantIdentifiers = relevantIdentifiers + + this + } + + MutantResultBuilder addingRelevantIdentifiers(final Long relevantIdentifier) { + this.relevantIdentifiers += relevantIdentifier + + this + } + + MutantResult build() { + final MutantResult mutantResult = new MutantResult<>() + + mutantResult.setTable(table) + mutantResult.setChange(change) + mutantResult.setAtlasMediatorToCommitOn(atlasMediatorToCommitOn) + mutantResult.setRelevantIdentifiers(relevantIdentifiers?:[]) + + mutantResult + } + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/Result.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/Result.groovy new file mode 100644 index 0000000000..00901250c3 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/result/Result.groovy @@ -0,0 +1,15 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.result + +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Contract of a result of a statement. + * + * @author Yazad Khambata + */ +interface Result extends EntityIterable { + AtlasTable getTable() + + List getRelevantIdentifiers() +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasDB.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasDB.groovy new file mode 100644 index 0000000000..3b7c751946 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasDB.groovy @@ -0,0 +1,36 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema + +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.* +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Defines the structure of tables and views inside the schema. + * + * The tables or views are NOT concerned with rendering. + * + * @author Yazad Khambata + */ +class AtlasDB { + + /* + * Table References - used in the select list and where clause. + */ + static NodeTable node = new NodeTable() + static PointTable point = new PointTable() + static LineTable line = new LineTable() + static EdgeTable edge = new EdgeTable() + static RelationTable relation = new RelationTable() + static AreaTable area = new AreaTable() + + static > Map getSupportedTables() { + [ + (TableSetting.NODE): node, + (TableSetting.POINT): point, + (TableSetting.LINE): line, + (TableSetting.EDGE): edge, + (TableSetting.RELATION): relation, + (TableSetting.AREA): area + ] + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasMediator.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasMediator.groovy new file mode 100644 index 0000000000..0e166130eb --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasMediator.groovy @@ -0,0 +1,51 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema + +import groovy.transform.ToString +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.SchemeSupport + +/** + * The AtlasSchema and AtlasTables act as Colleagues. + * + * @author Yazad Khambata + */ +@ToString(includes = ["uri"]) +class AtlasMediator { + + String uri + + Atlas atlas + + private AtlasMediator(final String uri, final Atlas atlas) { + this.uri = uri + this.atlas = atlas + } + + AtlasMediator(String uri) { + this(uri, loadAtlas(uri)) + } + + AtlasMediator(final Atlas atlas) { + this(null, atlas) + } + + AtlasMediator(final AtlasMediator atlasMediator) { + this(atlasMediator.uri, atlasMediator.atlas) + } + + protected static Atlas loadAtlas(String uri) { + SchemeSupport.instance.load(uri) + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that, "uri") + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this, "uri") + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasSchema.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasSchema.groovy new file mode 100644 index 0000000000..d99346542d --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasSchema.groovy @@ -0,0 +1,77 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema + +import groovy.transform.ToString +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.* +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Collectors + +/** + * The table references in this class are used in the from clause of select or update statement of update. + * + * @author Yazad Khambata + */ +@ToString(includes = ["uri"]) +class AtlasSchema { + final NodeTable node + final PointTable point + final LineTable line + final EdgeTable edge + final RelationTable relation + final AreaTable area + + String uri + + AtlasMediator atlasMediator + + AtlasSchema(String uri) { + + this(new AtlasMediator(uri), uri) + } + + AtlasSchema(AtlasMediator atlasMediator) { + this(atlasMediator, null) + } + + AtlasSchema(final AtlasMediator atlasMediator, final String uri) { + this.atlasMediator = atlasMediator + this.uri = uri + + this.node = new NodeTable(atlasMediator) + this.point = new PointTable(atlasMediator) + this.line = new LineTable(atlasMediator) + this.edge = new EdgeTable(atlasMediator) + this.relation = new RelationTable(atlasMediator) + this.area = new AreaTable(atlasMediator) + } + + List getAllTableNames() { + Arrays.stream(TableSetting.values()).map { tableSetting -> tableSetting.name().toLowerCase() }.collect(Collectors.toList()) + } + + private final static String[] EXCLUDE = ["node", "point", "line", "edge", "relation", "area", "uri"] + + def Map> getAllTables() { + [ + (TableSetting.NODE) : node, + (TableSetting.POINT) : point, + (TableSetting.LINE) : line, + (TableSetting.EDGE) : edge, + (TableSetting.RELATION): relation, + (TableSetting.AREA) : area + ] + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that, EXCLUDE) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this, EXCLUDE) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/mutant/MutantAtlasMediator.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/mutant/MutantAtlasMediator.groovy new file mode 100644 index 0000000000..ca644fa7db --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/mutant/MutantAtlasMediator.groovy @@ -0,0 +1,51 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.mutant + +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.change.Change +import org.openstreetmap.atlas.geography.atlas.change.ChangeAtlas +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.commit.Commitable +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.CommitableResult +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.stream.Collectors + +/** + * An AtlasMediator over ChangeAtlas. + * + * @author Yazad Khambata + */ +class MutantAtlasMediator extends AtlasMediator { + private static final Logger log = LoggerFactory.getLogger(MutantAtlasMediator.class); + + MutantAtlasMediator(final QueryBuilder[] queryBuilders) { + super(commit(queryBuilders)) + } + + protected static AtlasMediator commit(final QueryBuilder[] queryBuilders) { + Valid.notEmpty queryBuilders + + Valid.isTrue queryBuilders.size() >= 1 + + final List commitableResults = Arrays.stream(queryBuilders) + .map { queryBuilder -> queryBuilder.buildQuery() } + .map { query -> Valid.isTrue query instanceof Commitable; query.execute() as CommitableResult } + .collect(Collectors.toList()) + + //Check how efficient atlas#hashCode() is. + final long atlasCount = commitableResults.stream().map { it.atlasMediatorToCommitOn.atlas }.distinct().count() + Valid.isTrue atlasCount == 1, "Illegal attempt to commit multiple Atlases in the same statement." + + final Atlas atlasToCommitOn = commitableResults[0].atlasMediatorToCommitOn.atlas + final Change[] allChanges = commitableResults.stream().map { it.change }.toArray() as Change[] + + log.info "Applying changes to atlas..." + final Atlas changeAtlas = new ChangeAtlas(atlasToCommitOn, allChanges) + log.info "Changes applied to atlas!" + + new AtlasMediator(changeAtlas) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/AreaTable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/AreaTable.groovy new file mode 100644 index 0000000000..71830f0dee --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/AreaTable.groovy @@ -0,0 +1,26 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import org.openstreetmap.atlas.geography.atlas.dsl.field.SelectOnlyField +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.Area + +/** + * The Area table. + * + * @author Yazad Khambata + */ +final class AreaTable extends BaseTable { + SelectOnlyField asPolygon = null + SelectOnlyField closedGeometry = null + SelectOnlyField rawGeometry = null + + AreaTable() { + this(null) + } + + AreaTable(final AtlasMediator atlasMediator) { + super(atlasMediator, TableSetting.AREA) + autoSetFields(this) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/AtlasTable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/AtlasTable.groovy new file mode 100644 index 0000000000..31b7ec495e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/AtlasTable.groovy @@ -0,0 +1,30 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import org.openstreetmap.atlas.geography.atlas.dsl.field.Field +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Predicate + +/** + * The Atlas Table contract. + * + * @author Yazad Khambata + */ +interface AtlasTable { + + Iterable getAll() + + Iterable getAllMatching(Predicate predicate) + + E getById(final long id) + + AtlasMediator getAtlasMediator() + + Class getEntityClass() + + TableSetting getTableSetting() + + Map getAllFields() +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/BaseTable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/BaseTable.groovy new file mode 100644 index 0000000000..a1906ae75e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/BaseTable.groovy @@ -0,0 +1,71 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import groovy.transform.EqualsAndHashCode +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.field.Field +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Predicate + +/** + * Abstraction of an AQL table. + * + * @author Yazad Khambata + */ +@EqualsAndHashCode(includeFields = true, includes = ["atlasMediator", "atlasTableSettings"], callSuper = true) +abstract class BaseTable extends CommonFields implements AtlasTable { + /** + * This field is NULLABLE - in case of static references of AtlasTable which doesn't link to an AtlasSchema. + */ + private AtlasMediator atlasMediator + + private TableSetting atlasTableSettings + + BaseTable(final AtlasMediator atlasMediator, final TableSetting tableSetting) { + super(tableSetting.memberClass) + + this.atlasMediator = atlasMediator + this.atlasTableSettings = tableSetting + } + + AtlasMediator getAtlasMediator() { + atlasMediator + } + + protected Atlas getAtlas() { + this.atlasMediator.atlas + } + + @Override + TableSetting getTableSetting() { + atlasTableSettings + } + + @Override + Iterable getAll() { + atlasTableSettings.getAll(this.atlas) + } + + @Override + Iterable getAllMatching(final Predicate predicate) { + atlasTableSettings.getAll(this.atlas, predicate) + } + + @Override + E getById(final long id) { + atlasTableSettings.getById(this.atlas, id) + } + + @Override + Class getEntityClass() { + atlasTableSettings.memberClass + } + + @Override + Map getAllFields() { + final Map fieldMap = super.getAllFields(this) + fieldMap + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/CommonFields.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/CommonFields.groovy new file mode 100644 index 0000000000..b9e538b535 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/CommonFields.groovy @@ -0,0 +1,175 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import groovy.transform.EqualsAndHashCode +import org.apache.commons.lang3.tuple.Pair +import org.openstreetmap.atlas.geography.GeometricSurface +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.dsl.field.Field +import org.openstreetmap.atlas.geography.atlas.dsl.field.ItselfField +import org.openstreetmap.atlas.geography.atlas.dsl.field.SelectOnlyField +import org.openstreetmap.atlas.geography.atlas.dsl.field.StandardField +import org.openstreetmap.atlas.geography.atlas.dsl.field.TagsField +import org.openstreetmap.atlas.geography.atlas.dsl.mutant.Mutant +import org.openstreetmap.atlas.geography.atlas.dsl.query.InnerSelectWrapper +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.stream.Collectors +import java.util.stream.Stream + +/** + * Composition of common fields in ALL Atlas Tables. + * + * @author Yazad Khambata + */ +@EqualsAndHashCode(includeFields = true, includes = ["atlasEntityClass"]) +class CommonFields { + + private Class atlasEntityClass + + CommonFields(final Class atlasEntityClass) { + this.atlasEntityClass = atlasEntityClass + } + + ItselfField _ = new ItselfField() + + private ItselfField itself = _ + + StandardField identifier = new StandardField("identifier") + StandardField id = identifier + StandardField osmIdentifier = new StandardField("osmIdentifier") + StandardField osmId = osmIdentifier + TagsField tags = new TagsField<>("tags") + SelectOnlyField osmTags = null + SelectOnlyField rawGeometry = null + + SelectOnlyField type = null + StandardField lastEdit = null + StandardField lastUserIdentifier = null + + StandardField lastUserName = null + SelectOnlyField relations = null + StandardField bounds = null + + Constraint hasId(final Long id) { + identifier.has(eq: id, ScanType.ID_UNIQUE_INDEX, atlasEntityClass) + } + + Constraint hasOsmId(final Long osmId) { + osmIdentifier.has(eq: osmId, atlasEntityClass) + } + + Constraint hasIds(final Long...ids) { + identifier.has(in: ids, ScanType.ID_UNIQUE_INDEX, atlasEntityClass) + } + + Constraint hasOsmIds(final Long...osmIds) { + osmIdentifier.has(in: osmIds, atlasEntityClass) + } + + def Constraint hasIds(final QueryBuilder selectInnerQuery) { + final InnerSelectWrapper innerSelectWrapper = InnerSelectWrapper.from(selectInnerQuery) + + //Needless to say that the Operation.inner_query will be used only if not indexed. + identifier.has(inner_query: innerSelectWrapper, ScanType.ID_UNIQUE_INDEX, atlasEntityClass) + } + + Constraint hasTag(final Map params) { + return tags.has(tag: params, atlasEntityClass) + } + + Constraint hasTag(final String key) { + tags.has(tag: key, atlasEntityClass) + } + + Constraint hasTagLike(final Map paramsExactKeyAndRegexValue) { + tags.has(tag_like: paramsExactKeyAndRegexValue, atlasEntityClass) + } + + Constraint hasTagLike(final String keyRegex) { + tags.has(tag_like: keyRegex, atlasEntityClass) + } + + Constraint hasLastUserName(final String userName) { + lastUserName.has(eq: userName, atlasEntityClass) + } + + Constraint hasLastUserNameLike(final String userNameRegex) { + lastUserName.has(like: userNameRegex, atlasEntityClass) + } + + /** + * @param geometricSurface - expects [ [ [x, y], [a, b] ] ] + * @return + */ + Constraint isWithin(final List> geometricSurface) { + this.itself.was(within: geometricSurface, ScanType.SPATIAL_INDEX, atlasEntityClass) + } + + Constraint isWithin(final GeometricSurface geometricSurface) { + this.itself.was(within: geometricSurface, ScanType.SPATIAL_INDEX, atlasEntityClass) + } + + Mutant addTag(final Map tagToAdd) { + Valid.isTrue tagToAdd.size() == 1, "add accepts one tag key/value at a time, invoke addTag multiple times to add more tags." + + this.tags.add(tagToAdd) + } + + Mutant deleteTag(final String key) { + this.tags.remove(key) + } + + /** + * Alias for deleting tags. + */ + def removeTag = this.&deleteTag + + /** + * Automatically initialize the fields in the table. + * + * @param atlasTable - the table whose fields need to be auto initialized. + */ + void autoSetFields(final AtlasTable atlasTable) { + final Map> filedNamesToAutoSet = streamFields(atlasTable) + .filter { prop -> prop.getProperty(atlasTable) == null } + .map { prop -> Pair.of(prop.name, prop.getType()) } + .collect(Collectors.toMap({ pair -> pair.key}, { pair -> pair.value})) + + filedNamesToAutoSet.entrySet().stream() + .forEach { pair -> + final String fieldName = pair.key + final Class type = pair.value + + def instance = type.newInstance(fieldName) + atlasTable[fieldName] = instance + } + } + + private Stream streamFields(final AtlasTable atlasTable) { + atlasTable.getMetaClass().getProperties().stream() + .filter { prop -> prop.getName() != "class" } + .filter { prop -> prop.getType() in [StandardField, SelectOnlyField] } + } + + @Override + String toString() { + def fieldsAsStr = this.getMetaClass().getProperties().stream() + .filter({ prop -> Field.isAssignableFrom(prop.type) }) + .map({ prop -> "${prop.name} ${prop.type.simpleName}" }) + .collect(Collectors.joining(", ")) + + "${this.class.simpleName} ( ${fieldsAsStr} )" + } + + Map getAllFields(final AtlasTable atlasTable) { + streamFields(atlasTable) + .map { prop -> prop.getProperty(atlasTable) } + .distinct() //needed to remove alias fields like identifier and osmIdentifier. + .map { prop -> new Tuple2<>(prop.name, prop) } + .collect(Collectors.toMap({ t2 -> t2.first}, { t2 -> t2.second })) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/EdgeTable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/EdgeTable.groovy new file mode 100644 index 0000000000..1847c1cd41 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/EdgeTable.groovy @@ -0,0 +1,43 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import org.openstreetmap.atlas.geography.atlas.dsl.field.SelectOnlyField +import org.openstreetmap.atlas.geography.atlas.dsl.field.TerminalNodeIdField +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.ConnectedNodeType +import org.openstreetmap.atlas.geography.atlas.items.Edge + +/** + * The Edge table. + * + * @author Yazad Khambata + */ +final class EdgeTable extends BaseTable { + SelectOnlyField isWaySectioned = null + SelectOnlyField isMasterEdge = null + SelectOnlyField isClosed = null + SelectOnlyField isZeroLength = null + SelectOnlyField connectedEdges = null + SelectOnlyField connectedNodes = null + SelectOnlyField start = null + SelectOnlyField end = null + + TerminalNodeIdField startId = new TerminalNodeIdField(ConnectedNodeType.START, "startId") + TerminalNodeIdField endId = new TerminalNodeIdField(ConnectedNodeType.END, "endId") + + SelectOnlyField hasReverseEdge = null + SelectOnlyField highwayTag = null + SelectOnlyField inEdges = null + SelectOnlyField outEdges = null + SelectOnlyField reversed = null + SelectOnlyField length = null + + EdgeTable() { + this(null) + } + + EdgeTable(AtlasMediator atlasMediator) { + super(atlasMediator, TableSetting.EDGE) + autoSetFields(this) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/LineTable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/LineTable.groovy new file mode 100644 index 0000000000..0ad45e0214 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/LineTable.groovy @@ -0,0 +1,25 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import org.openstreetmap.atlas.geography.atlas.dsl.field.SelectOnlyField +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.Line + +/** + * The Line table. + * + * @author Yazad Khambata + */ +final class LineTable extends BaseTable { + SelectOnlyField asPolyLine = null + SelectOnlyField numberOfShapePoints = null + + LineTable() { + this(null) + } + + LineTable(AtlasMediator atlasMediator) { + super(atlasMediator, TableSetting.LINE) + autoSetFields(this) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/LocationItemTable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/LocationItemTable.groovy new file mode 100644 index 0000000000..4eafdcf8e4 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/LocationItemTable.groovy @@ -0,0 +1,19 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import org.openstreetmap.atlas.geography.atlas.dsl.field.SelectOnlyField +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.LocationItem + +/** + * Abstraction of Location item tables. + * + * @author Yazad Khambata + */ +abstract class LocationItemTable extends BaseTable { + SelectOnlyField location = null + + LocationItemTable(final AtlasMediator atlasMediator, final TableSetting tableSetting) { + super(atlasMediator, tableSetting) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/NodeTable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/NodeTable.groovy new file mode 100644 index 0000000000..08f97df485 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/NodeTable.groovy @@ -0,0 +1,30 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import org.openstreetmap.atlas.geography.atlas.dsl.field.ConnectedEdgeIdField +import org.openstreetmap.atlas.geography.atlas.dsl.field.StandardField +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.ConnectedEdgeType +import org.openstreetmap.atlas.geography.atlas.items.Node + +/** + * The node table. + * + * @author Yazad Khambata + */ +final class NodeTable extends LocationItemTable { + StandardField inEdges + StandardField outEdges + + ConnectedEdgeIdField inEdgeIds = new ConnectedEdgeIdField(ConnectedEdgeType.IN, "inEdgeIds") + ConnectedEdgeIdField outEdgeIds = new ConnectedEdgeIdField(ConnectedEdgeType.OUT, "outEdgeIds") + + NodeTable() { + this(null) + } + + NodeTable(AtlasMediator atlasMediator) { + super(atlasMediator, TableSetting.NODE) + autoSetFields(this) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/PointTable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/PointTable.groovy new file mode 100644 index 0000000000..08f1185487 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/PointTable.groovy @@ -0,0 +1,21 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.Point + +/** + * The point table. + * + * @author Yazad Khambata + */ +final class PointTable extends LocationItemTable { + PointTable() { + this(null) + } + + PointTable(AtlasMediator atlasMediator) { + super(atlasMediator, TableSetting.POINT) + autoSetFields(this) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/RelationTable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/RelationTable.groovy new file mode 100644 index 0000000000..8658b03a1d --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/RelationTable.groovy @@ -0,0 +1,28 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import org.openstreetmap.atlas.geography.atlas.dsl.field.SelectOnlyField +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.Relation + +/** + * The relation table. + * + * @author Yazad Khambata + */ +final class RelationTable extends BaseTable { + SelectOnlyField allRelationsWithSameOsmIdentifier = null + SelectOnlyField allKnownOsmMembers = null + SelectOnlyField members = null + SelectOnlyField osmRelationIdentifier = null + SelectOnlyField isMultiPolygon = null + + RelationTable() { + this(null) + } + + RelationTable(AtlasMediator atlasMediator) { + super(atlasMediator, TableSetting.RELATION) + autoSetFields(this) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/setting/TableSetting.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/setting/TableSetting.groovy new file mode 100644 index 0000000000..448a35ac49 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/setting/TableSetting.groovy @@ -0,0 +1,75 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting + +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.complete.CompleteEntity +import org.openstreetmap.atlas.geography.atlas.complete.CompleteItemType +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.openstreetmap.atlas.geography.atlas.items.ItemType + +import java.util.function.Predicate + +/** + * The built-in tables supported in AQL. + * + * @author Yazad Khambata + */ +enum TableSetting { + + NODE, + + POINT, + + LINE, + + EDGE, + + RELATION, + + AREA; + + + private String atlasGetAllMethodName() { + "${toLowerCase()}s" + } + + private String atlasGetByIdMethodName() { + toLowerCase() + } + + private String toLowerCase() { + this.name().toLowerCase() + } + + String tableName() { + toLowerCase() + } + + ItemType getItemType() { + this.name() as ItemType + } + + def Class getMemberClass() { + this.itemType.memberClass + } + + CompleteItemType getCompleteItemType() { + CompleteItemType.from(itemType) + } + + def Class getCompleteEntityClass() { + completeItemType.completeEntityClass + } + + Iterable getAll(final Atlas atlas) { + atlas."${atlasGetAllMethodName()}"() + } + + Iterable getAll(final Atlas atlas, final Predicate predicate) { + atlas."${atlasGetAllMethodName()}"(predicate) + } + + AtlasEntity getById(final Atlas atlas, final long id) { + atlas."${atlasGetByIdMethodName()}"(id) + } +} + diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/Scheme.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/Scheme.groovy new file mode 100644 index 0000000000..19a9b0fcdd --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/Scheme.groovy @@ -0,0 +1,24 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.uri + +import org.openstreetmap.atlas.geography.atlas.Atlas + +/** + * A sample URI scheme like file or classpath. + * + * @author Yazad Khambata + */ +interface Scheme { + String name() + + Atlas loadAtlas(final String uriExcludingScheme) + + Atlas loadOsmXml(final String uriExcludingScheme) + + /** + * Allows only one file per call. Expects the file and not the parent directory. + * + * @param uriExcludingScheme + * @return + */ + InputStream loadFile(final String uriExcludingScheme) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/SchemeSupport.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/SchemeSupport.groovy new file mode 100644 index 0000000000..ec9059d439 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/SchemeSupport.groovy @@ -0,0 +1,73 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.uri + +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl.ClasspathScheme +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl.FileScheme +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +/** + * Scheme (example file://, classpath://, factory://, ...) support registry. + * + * @author Yazad Khambata + */ +@Singleton +class SchemeSupport { + + private Map schemeMapping = [ + (FileScheme.SCHEME):FileScheme.instance, + (ClasspathScheme.SCHEME):ClasspathScheme.instance + ] + + static final String DEFAULT = FileScheme.SCHEME + + void register(final String schemeName, final Scheme scheme) { + Valid.isTrue schemeName == scheme.name() + + schemeMapping.put(schemeName, scheme) + } + + protected String toScheme(final String uri) { + Valid.notEmpty uri + + //Default encoding is file: + + final int indexOfColon = uri.indexOf(/:/) + + if (indexOfColon < 0) { + return SchemeSupport.DEFAULT + } + + return uri.substring(0, indexOfColon) + } + + protected Scheme toSchemeHandler(final String uri) { + final String schemeName = toScheme(uri) + + final Scheme schemeHandler = schemeMapping[schemeName] + + Valid.notEmpty schemeHandler, "No handler found for ${uri}." + + schemeHandler + } + + protected String toUriExcludingScheme(String uri) { + uri.replace("${toScheme(uri)}:", "") + } + + Atlas load(final String uri) { + final Scheme schemeHandler = toSchemeHandler(uri) + + schemeHandler.loadAtlas(toUriExcludingScheme(uri)) + } + + Atlas loadOsmXml(final String uri) { + final Scheme schemeHandler = toSchemeHandler(uri) + + schemeHandler.loadOsmXml(toUriExcludingScheme(uri)) + } + + InputStream loadFile(final String uri) { + final Scheme schemeHandler = toSchemeHandler(uri) + schemeHandler.loadFile(toUriExcludingScheme(uri)) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/AbstractScheme.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/AbstractScheme.groovy new file mode 100644 index 0000000000..2221b55095 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/AbstractScheme.groovy @@ -0,0 +1,41 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl + +import org.openstreetmap.atlas.geography.MultiPolygon +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.Scheme +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.multi.MultiAtlas +import org.openstreetmap.atlas.geography.atlas.pbf.AtlasLoadingOption +import org.openstreetmap.atlas.geography.atlas.raw.creation.RawAtlasGenerator +import org.openstreetmap.atlas.geography.atlas.raw.sectioning.WaySectionProcessor +import org.openstreetmap.atlas.streaming.resource.ByteArrayResource +import org.openstreetmap.atlas.streaming.resource.InputStreamResource +import org.openstreetmap.atlas.utilities.testing.OsmFileToPbf + +/** + * An abstraction of a scheme. + * + * @author Yazad Khambata + */ +abstract class AbstractScheme implements Scheme { + protected Atlas osmInputStreamResourceToAtlas(final InputStreamResource inputStreamResource) { + Valid.notEmpty inputStreamResource + + final ByteArrayResource pbfFile = new ByteArrayResource() + new OsmFileToPbf().update(inputStreamResource, pbfFile) + + final AtlasLoadingOption loadingOption = AtlasLoadingOption.withNoFilter() + final Atlas rawAtlas = new RawAtlasGenerator(pbfFile, loadingOption, + MultiPolygon.MAXIMUM).build() + + // Way-section + final Atlas atlas = new WaySectionProcessor(rawAtlas, loadingOption).run() + + atlas + } + + protected Atlas merge(List atlases) { + final Atlas atlas = new MultiAtlas(atlases) + atlas + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/ClasspathScheme.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/ClasspathScheme.groovy new file mode 100644 index 0000000000..98cb99f927 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/ClasspathScheme.groovy @@ -0,0 +1,92 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl + +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.AtlasResourceLoader +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.streaming.resource.InputStreamResource + +import java.util.function.Supplier +import java.util.stream.Collectors + +/** + * Classpath scheme implementation. + * + * @author Yazad Khambata + */ +@Singleton +class ClasspathScheme extends AbstractScheme { + static final String SCHEME = "classpath" + + @Override + String name() { + SCHEME + } + + private List toInputStreamResources(String uriExcludingScheme) { + Valid.notEmpty uriExcludingScheme + + final String[] dirsWithFiles = uriExcludingScheme.split(";") + + final List inputStreamResources = Arrays.stream(dirsWithFiles).flatMap { String dirWithFiles -> + final String[] files = dirWithFiles.split(",") + + final String dir = files[0].substring(0, files[0].lastIndexOf("/") + 1) + + files[0] = files[0].replace(dir, "") + + Arrays.stream(files).map { file -> "${dir}${file}" } + }.map { classpathFilePath -> + toInputResource(classpathFilePath) + }.collect(Collectors.toList()) + inputStreamResources + } + + protected InputStreamResource toInputResource(final String classpathFilePath) { + final Supplier supplierOfInputStream = { + final InputStream inputStream = getResourceAsStreamStrict(classpathFilePath) + + inputStream + } + + def inputStreamResource = new InputStreamResource(supplierOfInputStream) + inputStreamResource + } + + /** + * + * @param uriExcludingScheme - example /data/atlas/XYZ/file1.atlas,file2.atlas;/data/atlas/ABC/file1.atlas,file2.atlas + * @return + */ + @Override + Atlas loadAtlas(final String uriExcludingScheme) { + List inputStreamResources = toInputStreamResources(uriExcludingScheme) + + final Atlas atlas = new AtlasResourceLoader().load(inputStreamResources) + + atlas + } + + @Override + Atlas loadOsmXml(final String uriExcludingScheme) { + final List inputStreamResourceList = toInputStreamResources(uriExcludingScheme) + + final List atlases = inputStreamResourceList.stream() + .map { inputStreamResource -> osmInputStreamResourceToAtlas(inputStreamResource) } + .collect(Collectors.toList()) + + merge(atlases) + } + + @Override + InputStream loadFile(final String uriExcludingScheme) { + getResourceAsStreamStrict(uriExcludingScheme) + } + + private InputStream getResourceAsStreamStrict(String uriExcludingScheme) { + final InputStream inputStream = this.getClass().getResourceAsStream(uriExcludingScheme) + + Valid.isTrue inputStream != null + + inputStream + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/FileScheme.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/FileScheme.groovy new file mode 100644 index 0000000000..494e8f5bb5 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/FileScheme.groovy @@ -0,0 +1,83 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl + +import groovy.io.FileType +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.AtlasResourceLoader +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.streaming.resource.File +import org.openstreetmap.atlas.streaming.resource.InputStreamResource + +import java.util.function.Supplier +import java.util.stream.Collectors + +/** + * File scheme implementation. + * + * @author Yazad Khambata + */ +@Singleton +class FileScheme extends AbstractScheme { + static final String SCHEME = "file" + + @Override + String name() { + SCHEME + } + + /** + * @param uriExcludingScheme - the dir path (not file) and no file: to be prepended. + * @return + */ + @Override + Atlas loadAtlas(final String uriExcludingScheme) { + final List list = toFiles(uriExcludingScheme) + + final Atlas atlas = new AtlasResourceLoader().load(list) + + atlas + } + + /** + * @param uriExcludingScheme - the dir path (not file) and no file: to be prepended. + * @return + */ + @Override + Atlas loadOsmXml(final String uriExcludingScheme) { + final List list = toFiles(uriExcludingScheme) + + final List atlases = list.stream() + .map { file -> + byte[] bytes = file.readBytesAndClose() + + Valid.notEmpty bytes + + final Supplier supplier = { new ByteArrayInputStream(bytes) } + final InputStreamResource inputStreamResource = new InputStreamResource(supplier) + + osmInputStreamResourceToAtlas(inputStreamResource) + } + .collect(Collectors.toList()) + + merge(atlases) + } + + @Override + InputStream loadFile(final String uriExcludingScheme) { + new FileInputStream(new java.io.File(uriExcludingScheme)) + } + + private List toFiles(String uriExcludingScheme) { + Valid.notEmpty uriExcludingScheme + + def dir = new java.io.File(uriExcludingScheme) + + Valid.isTrue dir.isDirectory() && dir.canRead(), "${uriExcludingScheme} needs to be a directory (${dir.isDirectory()}) with read access (${dir.canRead()})." + + final List list = [] + + dir.eachFileRecurse(FileType.FILES) { file -> + list << new File(file.toString()) + } + list + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/Selector.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/Selector.groovy new file mode 100644 index 0000000000..40e8abb597 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/Selector.groovy @@ -0,0 +1,160 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection + + +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstructList +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.IndexScanner +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.IndexSetting +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.strategy.ScanStrategizer +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.strategy.ScanStrategy +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.StatementPredicate +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.chained.BinaryLogicalOperator +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.chained.ChainedConditionalConstruct +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.chained.ConditionalConstructPredicate +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Predicate +import java.util.stream.StreamSupport + +/** + * Deals with selection (σ) in terms of relational algebra and SQL. + * + * @author Yazad Khambata + */ +class Selector { + + final AtlasTable table + + final ScanStrategy scanStrategy + + final Statement statement + + private Selector(final AtlasTable table, final ScanStrategy scanStrategy, final Statement statement) { + this.table = table + this.scanStrategy = scanStrategy + this.statement = statement + } + + static class SelectorBuilder { + AtlasTable table + + ConditionalConstructList conditionalConstructList + + Statement statement + + SelectorBuilder with(final AtlasTable table) { + this.table = table + this + } + + SelectorBuilder with(final ConditionalConstructList conditionalConstructList) { + this.conditionalConstructList = conditionalConstructList + this + } + + SelectorBuilder with(final Statement statement) { + this.statement = statement + this + } + + Selector build() { + final ScanStrategy scanStrategy = ScanStrategizer. getInstance().strategize(conditionalConstructList) + final Selector selector = new Selector<>(this.table, scanStrategy, this.statement) + selector + } + } + + static SelectorBuilder builder() { + new SelectorBuilder<>() + } + + Iterable fetchMatchingEntities() { + if (scanStrategy.canUseIndex()) { + //1. When Index info is present. + final IndexSetting indexSetting = scanStrategy.indexUsageInfo.indexSetting + final Constraint indexConstraint = scanStrategy.indexUsageInfo.constraint + + final IndexScanner indexScanner = indexSetting.indexScanner + + final Object valueToCheck = indexConstraint.valueToCheck + final Iterable iterableOfRecordsFromIndex = indexScanner.fetch(table, valueToCheck) + + if (sizeOfNonIndexedConstraints() >= 1) { + //1.1 Has additional constraints. + final Iterable iterable = { + StreamSupport.stream(iterableOfRecordsFromIndex.spliterator(), false).filter(toPredicate()).iterator() + } + + return iterable + } else { + //1.2 No additional constraints. + return iterableOfRecordsFromIndex + } + } else { + //2. No index + if (sizeOfNonIndexedConstraints() >= 1) { + //2.1 Has constraints + return table.getAllMatching(toPredicate()) + } else { + //2.2 No constraints + return table.getAll() + } + } + } + + + private Predicate toPredicate() { + toPredicate(table.tableSetting.memberClass, statement) + } + + private Predicate toPredicate(final Class entityClass, final Statement statement) { + final ConditionalConstructList conditionalConstructList = scanStrategy.conditionalConstructListExcludingIndexedConstraintIfAny + + Valid.isTrue conditionalConstructList.size() > 0 + + if (!scanStrategy.canUseIndex()) { + Valid.isTrue conditionalConstructList.get(0).clause == Statement.Clause.WHERE + } + + final Predicate aggregatePredicate = + conditionalConstructList.stream() + .map { conditionalConstruct -> new Tuple2<>(conditionalConstruct.clause, conditionalConstruct.toPredicate(entityClass)) } + .reduce { t2A, t2B -> + final Predicate predicateA = t2A.second + + final Statement.Clause clauseB = t2B.first + + final Predicate predicateB = t2B.second + + /* + Conditional Construct Predicate expected for B + (A my not be conditional construct as it is being aggregated). + */ + Valid.isTrue predicateB instanceof ConditionalConstructPredicate + + final BinaryLogicalOperator operator = BinaryLogicalOperator.from(clauseB) + + final ChainedConditionalConstruct chainedConditionalConstruct = ChainedConditionalConstruct + .builder() + .predicateA(predicateA) + .op(operator) + .predicateB(predicateB) + .build() + + final Predicate subAggregate = chainedConditionalConstruct.toChainedConditionalConstructPredicate(entityClass) + + new Tuple2>(null, subAggregate) + } + .get() + .second + + StatementPredicate.builder().statement(statement).predicate(aggregatePredicate).build() + } + + private int sizeOfNonIndexedConstraints() { + (scanStrategy.conditionalConstructListExcludingIndexedConstraintIfAny ?: []).size() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BasicConstraint.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BasicConstraint.groovy new file mode 100644 index 0000000000..c6fa2a57b6 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BasicConstraint.groovy @@ -0,0 +1,71 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import groovy.transform.builder.Builder +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.field.Constrainable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.ConstraintPredicate +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Predicate + +/** + * @author Yazad Khambata + */ +@Builder +class BasicConstraint implements Constraint { + Constrainable field + BinaryOperation operation + def valueToCheck + + ScanType bestCandidateScanType + + Class atlasEntityClass + + /** + * Generates a predicate ignoring the bestCandidateScanType. + * @param entityClass + * @return + */ + @Override + Predicate toPredicate(final Class entityClass) { + final Predicate predicate = { atlasEntity -> + final Object actualValue = field.read(atlasEntity) + + operation.perform(actualValue, valueToCheck, entityClass) + } + + ConstraintPredicate.builder().constraint(this).predicate(predicate).build() + } + + @Override + Constraint deepCopy() { + this.deepCopyWithNewValueToCheck(this.valueToCheck) + } + + @Override + Constraint deepCopyWithNewValueToCheck(final Object valueToCheck) { + BasicConstraint.builder() + .field(this.field) //less risk since not backed by schema here. + .operation(this.operation) + .valueToCheck(valueToCheck) //risky since cloning is not universal in the JVM, and using a reference here. + .bestCandidateScanType(bestCandidateScanType) + .atlasEntityClass(atlasEntityClass) + .build() + } + + @Override + String toString() { + "Constraint(${field} ${operation} ${valueToCheck}; Potential Index: ${bestCandidateScanType})" + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BinaryOperation.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BinaryOperation.groovy new file mode 100644 index 0000000000..08e53be6c3 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BinaryOperation.groovy @@ -0,0 +1,15 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Implementations must NOT contain mutable states. + * + * @author Yazad Khambata + */ +interface BinaryOperation { + + String[] getTokens() + + def boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BinaryOperations.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BinaryOperations.groovy new file mode 100644 index 0000000000..0c94aa9785 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BinaryOperations.groovy @@ -0,0 +1,149 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import org.openstreetmap.atlas.geography.GeometricSurface +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.polygon.GeometricSurfaceSupport +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.regex.RegexSupport +import org.openstreetmap.atlas.geography.atlas.dsl.query.InnerSelectWrapper +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Operations that can be performed in the where clause. + * + * @author Yazad Khambata + */ +enum BinaryOperations implements BinaryOperation { + eq("eq"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + actualValue == valueToCheck + } + }, + lt("lt"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + (actualValue <=> valueToCheck) == -1 + } + }, + gt("gt"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + (actualValue <=> valueToCheck) == 1 + } + }, + le("le"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + (actualValue <=> valueToCheck) <= 0 + } + }, + ge("ge"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + (actualValue <=> valueToCheck) >= 0 + } + }, + + ne("not", "ne"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + !eq.perform(actualValue, valueToCheck, entityClass) + } + }, + + /** + * Within bounds + */ + within("within"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + final AtlasEntity atlasEntity = (AtlasEntity) actualValue + + GeometricSurface geometricSurfaceToCheckIfEntityIsWithin + if (valueToCheck instanceof List) { + final List> geometricSurfaceLocations = (List>) valueToCheck + geometricSurfaceToCheckIfEntityIsWithin = GeometricSurfaceSupport.instance.toGeometricSurface(geometricSurfaceLocations) + } else if (valueToCheck instanceof GeometricSurface) { + geometricSurfaceToCheckIfEntityIsWithin = (GeometricSurface)valueToCheck + } else { + throw new IllegalArgumentException("Unsupported ${valueToCheck?.class} : ${valueToCheck}") + } + + Valid.notEmpty geometricSurfaceToCheckIfEntityIsWithin + + atlasEntity.within(geometricSurfaceToCheckIfEntityIsWithin) + } + }, + + /** + * Typical SQL in clause. Here actualValue is a scalar value like identifier or osmIdentifier. + */ + inside("in"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + final List values = (List) valueToCheck + + actualValue in values + } + }, + + /** + * Inner queries + */ + inner_query("inner_query"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + final InnerSelectWrapper innerSelectWrapper = valueToCheck + + actualValue in innerSelectWrapper.identifiers + } + }, + + tag("tag", "tagged"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + final Map tags = actualValue + + //valueToCheck could be a String (key) or a Map(key, string-value) or a Map(key, list-of-string-values). + TagOperationHelper.instance.has(tags, valueToCheck) + } + }, + + tag_like("tag_like", "tagged_like"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + final Map tags = actualValue + + TagOperationHelper.instance.like(tags, valueToCheck) + } + }, + + like("like"){ + @Override + boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + final String actualValueAsString = actualValue + final String regexValueToCheckAsString = valueToCheck + + RegexSupport.instance.matches(actualValueAsString, regexValueToCheckAsString) + } + }; + + String[] tokens + + BinaryOperations(String[] tokens) { + this.tokens = tokens + } + + def boolean perform(VAL_ACTUAL actualValue, VAL_CHECK valueToCheck, final Class entityClass) { + throw new UnsupportedOperationException("The operation ${this} is not supported currently. actualValue: ${actualValue}; valueToCheck: ${valueToCheck} for entity class: ${entityClass}.") + } + + static BinaryOperation fromToken(final String token) { + final BinaryOperation operation = Arrays.stream(BinaryOperations.values()) + .filter({ operation -> token in operation.tokens }) + .findFirst() + .orElseThrow { new IllegalArgumentException("token: ${token}") } + + operation + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/Constraint.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/Constraint.groovy new file mode 100644 index 0000000000..c786f732f5 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/Constraint.groovy @@ -0,0 +1,24 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import org.openstreetmap.atlas.geography.atlas.dsl.field.Constrainable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.Predicatable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * @author Yazad Khambata + */ +interface Constraint extends Predicatable { + Constrainable getField() + BinaryOperation getOperation() + def getValueToCheck() + + ScanType getBestCandidateScanType() + + /** + * Creates a deep copy of the objects - not values inside the object may still be reference, so use with caution. + * @return + */ + Constraint deepCopy() + + Constraint deepCopyWithNewValueToCheck(valueToCheck) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/ConstraintGenerator.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/ConstraintGenerator.groovy new file mode 100644 index 0000000000..bf73875143 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/ConstraintGenerator.groovy @@ -0,0 +1,61 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import org.openstreetmap.atlas.geography.atlas.dsl.field.Constrainable +import org.openstreetmap.atlas.geography.atlas.dsl.field.Field +import org.openstreetmap.atlas.geography.atlas.dsl.util.SingletonMap +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * @author Yazad Khambata + */ +@Singleton +class ConstraintGenerator { + /** + * + * was(in: [1, 2, 3]) + * was(within: [ [10.5, 11.5], [12.5, 15.5], [16, 17], [5.5, 4.5], [10.5, 11.5] ]) + * was(eq: 10) + * was(not: 10) OR is(ne: 10) + * was(lt: 10) + * was(gt: 10) + * was(le: 10) + * was(ge: 10) + * + * @param params + * @param field + * @param bestCandidateScanStrategy + * @param atlasEntityClass + * @return + */ + def Constraint was(final Map params, final Field field, final ScanType bestCandidateScanStrategy, final Class atlasEntityClass) { + final Map entry = new SingletonMap<>(params) + + final String operationAsStr = entry.getKey() + + final Constraint constraint = BasicConstraint.builder() + .field((Constrainable) field) + .operation(BinaryOperations.fromToken(operationAsStr)) + .valueToCheck(entry.getValue()) + .atlasEntityClass(atlasEntityClass) + .bestCandidateScanType(bestCandidateScanStrategy) + .build() + + constraint + } + + def Constraint was(final Map params, final Field field, final Class atlasEntityClass) { + this.was(params, field, ScanType.FULL, atlasEntityClass) + } + + /** + * + * has(tag: [amenity: "college", "name:en": "Copenhagen Hospitality College"]) + * has(tag: ["amenity"]) + * + * @param params + * @return + */ + Constraint has(final Map params, final Field field) { + was(params, field) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/Constraints.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/Constraints.groovy new file mode 100644 index 0000000000..fa81be1a9b --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/Constraints.groovy @@ -0,0 +1,14 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * @author Yazad Khambata + */ +@Singleton +class Constraints { + def Constraint alwaysTrue(AtlasTable table) { + + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/NotConstraint.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/NotConstraint.groovy new file mode 100644 index 0000000000..c342d4ffa2 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/NotConstraint.groovy @@ -0,0 +1,82 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import org.apache.commons.lang3.builder.EqualsBuilder +import org.apache.commons.lang3.builder.HashCodeBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.field.Constrainable +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Predicate + +/** + * @author Yazad Khambata + */ +class NotConstraint implements Constraint { + Constraint constraint + private final Statement.Clause additionalClause = Statement.Clause.NOT + private final ScanType bestCandidateScanType = ScanType.FULL + + private NotConstraint(final Constraint constraint) { + super() + this.constraint = constraint + } + + static NotConstraint from(Constraint constraint) { + new NotConstraint(constraint) + } + + @Override + Predicate toPredicate(final Class entityClass) { + //Just negate the predicate from the composed Constraint. + return constraint.toPredicate(entityClass).negate() + } + + @Override + Constrainable getField() { + constraint.field + } + + Statement.Clause getAdditionalClause() { + additionalClause + } + + @Override + BinaryOperation getOperation() { + constraint.operation + } + + @Override + def getValueToCheck() { + constraint.valueToCheck + } + + @Override + ScanType getBestCandidateScanType() { + bestCandidateScanType + } + + @Override + Constraint deepCopy() { + this.deepCopyWithNewValueToCheck(this.constraint.valueToCheck) + } + + @Override + Constraint deepCopyWithNewValueToCheck(final Object valueToCheck) { + from(this.constraint.deepCopyWithNewValueToCheck(valueToCheck)) + } + + @Override + String toString() { + "Constraint(${field} ${additionalClause} ${operation} ${valueToCheck}; Potential Index: ${bestCandidateScanType})." + } + + @Override + boolean equals(final Object that) { + EqualsBuilder.reflectionEquals(this, that) + } + + @Override + int hashCode() { + HashCodeBuilder.reflectionHashCode(this) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/ScanType.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/ScanType.groovy new file mode 100644 index 0000000000..5d7fdf456c --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/ScanType.groovy @@ -0,0 +1,28 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import groovy.transform.TupleConstructor + +/** + * @author Yazad Khambata + */ +@TupleConstructor +enum ScanType { + FULL(99), + + ID_UNIQUE_INDEX(1), + + SPATIAL_INDEX(2); + + /** + * Lower number is better, used to indicate if an option is superior to another. + * + * Examples + * 1. ID_UNIQUE_INDEX scan is superior to a SPATIAL_INDEX scan. + * 2. SPATIAL_INDEX scan is superior to FULL. + */ + int preferntialRank + + static ScanType preferredOption(ScanType scanType1, ScanType scanType2) { + scanType1.preferntialRank < scanType2?scanType1:scanType2 + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/TagOperationHelper.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/TagOperationHelper.groovy new file mode 100644 index 0000000000..429c854425 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/TagOperationHelper.groovy @@ -0,0 +1,83 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.regex.RegexSupport +import org.openstreetmap.atlas.geography.atlas.dsl.util.SingletonMap +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +/** + * @author Yazad Khambata + */ +@PackageScope +@Singleton +class TagOperationHelper { + + boolean has(final Map actualTags, final String key) { + actualTags.containsKey(key) + } + + boolean has(final Map actualTags, final String key, String value) { + actualTags[key] == value + } + + /** + * + * @param actualTags + * @param keyValueToLookFor - Supports Map of String, String or Map of String and List Of Strings. + * @return + */ + boolean has(final Map actualTags, final Map keyValueToLookFor) { + final SingletonMap keyValueToLookForAsSingletonMap = keyValueToLookFor as SingletonMap + + def value = keyValueToLookForAsSingletonMap.getValue() + + if (value instanceof String) { + return hasTheValue(actualTags, (SingletonMap)keyValueToLookForAsSingletonMap) + } else if (value instanceof List) { + return hasAnyValue(actualTags, (SingletonMap>)keyValueToLookForAsSingletonMap) + } else { + throw new IllegalArgumentException("Param value of type ${value?.class} is not supported. keyValueToLookFor: ${keyValueToLookFor}.") + } + } + + boolean hasTheValue(final Map actualTags, final SingletonMap keyValueToLookFor) { + final String keyToLookFor = keyValueToLookFor.getKey() + final String valueToLookFor = keyValueToLookFor.getValue() + + has(actualTags, keyToLookFor, valueToLookFor) + } + + private boolean hasAnyValue(final Map actualTags, final SingletonMap> keyValuesToLookFor) { + final String keyToLookFor = keyValuesToLookFor.getKey() + final List valuesToLookFor = keyValuesToLookFor.getValue() + + Valid.isTrue !valuesToLookFor.isEmpty() + + for (String valueToLookFor : valuesToLookFor) { + boolean found = has(actualTags, keyToLookFor, valueToLookFor) + + if (found) { + return true + } + } + + return false + } + + boolean like(final Map actualTags, final String keyRegex) { + !(actualTags.keySet().findAll { it =~ keyRegex }.isEmpty()) + } + + boolean like(final Map actualTags, final String key, String valueRegex) { + RegexSupport.instance.matches(actualTags.get(key), valueRegex) + } + + boolean like(final Map actualTags, final Map keyValueToLookFor) { + //Assumes keyValueToLookFor has ONE entry + final String keyToLookFor = keyValueToLookFor.keySet().iterator().next() + + final String valueRegexToLookFor = keyValueToLookFor[keyToLookFor] + + like(actualTags, keyToLookFor, valueRegexToLookFor) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/polygon/GeometricSurfaceSupport.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/polygon/GeometricSurfaceSupport.groovy new file mode 100644 index 0000000000..6a75210d4f --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/polygon/GeometricSurfaceSupport.groovy @@ -0,0 +1,136 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.polygon + +import groovy.json.JsonSlurper +import org.apache.commons.io.IOUtils +import org.apache.commons.lang3.tuple.Pair +import org.openstreetmap.atlas.geography.* +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.SchemeSupport +import org.openstreetmap.atlas.geography.atlas.dsl.util.StreamUtil +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid + +import java.nio.charset.Charset +import java.util.stream.Collectors +import java.util.stream.Stream + +/** + * @author Yazad Khambata + */ +@Singleton +class GeometricSurfaceSupport { + + Polygon toPolygon(List> polygonAsNextedList) { + final Location[] locations = polygonAsNextedList.stream().map { longLatPair -> + toLocation(longLatPair) + }.toArray { new Location[polygonAsNextedList.size()] } + + new Polygon(locations) + } + + private Location toLocation(BigDecimal latitude, BigDecimal longitude) { + new Location(Latitude.degrees(latitude), Longitude.degrees(longitude)) + } + + private Location toLocation(List longLatPair) { + final BigDecimal longitude = longLatPair.get(0) + final BigDecimal latitude = longLatPair.get(1) + + toLocation(latitude, longitude) + } + + /** + * Converts a set of locations to a Polygon. + * Example of input, + * + * [ + * [ + * [103.7817053, 1.249778], + * [103.8952432, 1.249778], + * [103.8952432, 1.404799], + * [103.7817053, 1.404799], + * [103.7817053, 1.249778] + * ], + * + * [ + * [105.7817053, 3.249778], + * [105.8952432, 3.249778], + * [105.8952432, 3.404799], + * [105.7817053, 3.404799], + * [105.7817053, 3.249778] + * ], + * + * ... + * ] + * + * + * @param surfaceLocations - see example above. + * @param bounds - Rectangle of the bounds used for filtering. Only polygons contained in polygonsAsNextedLists that + * have some overlap with bounds would be considered while generating the GeometricSurface. + * @return - An Optional Atlas MultiPolygon + */ + Optional toGeometricSurface(final List>> polygonsAsNextedLists, final GeometricSurface bounds) { + final Stream polygonStream = toPolygonStream(polygonsAsNextedLists) + + toGeometricSurface(polygonStream, bounds) + } + + Optional toGeometricSurface(final MultiPolygon multiPolygon, final GeometricSurface bounds) { + final Stream polygonStream = StreamUtil.stream(multiPolygon.iterator()) + + toGeometricSurface(polygonStream, bounds) + } + + Optional toGeometricSurface(Stream polygonStream, GeometricSurface bounds) { + final MultiPolygon multiPolygon = polygonStream + .filter { Polygon polygon -> + bounds != null?polygon.overlaps((PolyLine) bounds):true + } + .collect( + Collectors.collectingAndThen( + Collectors.toList(), + { listOfPolygons -> MultiPolygon.forOuters(listOfPolygons) } + ) + ) + + Valid.isTrue multiPolygon != null + + Optional.of(multiPolygon) + } + + private Stream toPolygonStream(List>> polygonsAsNextedLists) { + polygonsAsNextedLists.stream() + .map { List> polygonAsNextedList -> + toPolygon(polygonAsNextedList) + } + } + + GeometricSurface toGeometricSurface(final List>> surfaceLocations) { + toGeometricSurface(surfaceLocations, null).get() + } + + Optional fromJsonlFile(final String uri, final GeometricSurface bounds) { + final InputStream inputStream = SchemeSupport.instance.loadFile(uri) + + Valid.isTrue inputStream != null + + final List lines = IOUtils.readLines(inputStream, Charset.defaultCharset()) + final JsonSlurper jsonSlurper = new JsonSlurper() + final List>> allPolygons = + lines.stream().map { line -> + final List> polygonAsNestedLists = jsonSlurper.parse(line.getBytes(Charset.defaultCharset())) + + polygonAsNestedLists + }.collect(Collectors.toList()) + + toGeometricSurface(allPolygons, bounds) + } + + GeometricSurface fromJsonlFile(final String uri) { + fromJsonlFile(uri, null).get() + } + + private static void isValid(final Pair pair) { + Valid.notEmpty pair + Valid.notEmpty pair.getKey() + Valid.notEmpty pair.getValue() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/regex/RegexSupport.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/regex/RegexSupport.groovy new file mode 100644 index 0000000000..766a41c96e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/regex/RegexSupport.groovy @@ -0,0 +1,11 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.regex +/** + * @author Yazad Khambata + */ +@Singleton +class RegexSupport { + + boolean matches(final String input, final String regex) { + input =~ regex + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/AbstractIndexScanner.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/AbstractIndexScanner.groovy new file mode 100644 index 0000000000..3d9237ced4 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/AbstractIndexScanner.groovy @@ -0,0 +1,28 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core + +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.openstreetmap.atlas.geography.atlas.items.ItemType + +/** + * An index scanner abstraction. + * + * @author Yazad Khambata + */ +abstract class AbstractIndexScanner implements IndexScanner{ + + protected Atlas toAtlas(AtlasTable atlasTable) { + Valid.notEmpty atlasTable + Valid.notEmpty atlasTable.atlasMediator + Valid.notEmpty atlasTable.atlasMediator.atlas + + final Atlas atlas = atlasTable.atlasMediator.atlas + atlas + } + + protected ItemType toItemType(AtlasTable atlasTable) { + atlasTable.tableSetting.itemType + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/IndexScanner.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/IndexScanner.groovy new file mode 100644 index 0000000000..f27b581c68 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/IndexScanner.groovy @@ -0,0 +1,14 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core + +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Represents an index scanner. Index scanners are responsible for going through + * an index and selecting data for a query. + * + * @author Yazad Khambata + */ +interface IndexScanner { + Iterable fetch(final AtlasTable atlasTable, final IV lookupValue) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/IndexSetting.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/IndexSetting.groovy new file mode 100644 index 0000000000..1c12fab90e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/IndexSetting.groovy @@ -0,0 +1,39 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core + +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.id.IdIndexScanner +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.spatial.SpatialIndexScanner + +/** + * Enum managing settings related to the supported indexing strategies. + * + * @author Yazad Khambata + */ +enum IndexSetting { + /** + * The Unique Index here allows fetching an Entity by Atlas Id in O(1) time. + */ + ID_UNIQUE_INDEX(IdIndexScanner.instance, ScanType.ID_UNIQUE_INDEX), + + /** + * The Spatial Index based search. + */ + SPATIAL_INDEX(SpatialIndexScanner.instance, ScanType.SPATIAL_INDEX); + + IndexScanner indexScanner + + ScanType scanType + + IndexSetting(final IndexScanner indexScanner, final ScanType scanType) { + this.indexScanner = indexScanner + this.scanType = scanType + } + + static Optional from(final ScanType scanStrategy) { + final Optional indexSetting = Arrays.stream(IndexSetting.values()) + .filter { indexSetting -> indexSetting.scanType == scanStrategy } + .findFirst() + + indexSetting + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/InnerQueryLookupIndexScanner.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/InnerQueryLookupIndexScanner.groovy new file mode 100644 index 0000000000..ff53ff95c2 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/InnerQueryLookupIndexScanner.groovy @@ -0,0 +1,17 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core + +import org.openstreetmap.atlas.geography.atlas.dsl.query.InnerSelectWrapper +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Index scanning based on inner queries. + * + * @author Yazad Khambata + */ +interface InnerQueryLookupIndexScanner extends IndexScanner { + Iterable fetch(final AtlasTable atlasTable, final QueryBuilder selectInnerQueryBuilder) + + Iterable fetch(final AtlasTable atlasTable, final InnerSelectWrapper innerSelectWrapper) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/MultiLookupIndexScanner.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/MultiLookupIndexScanner.groovy new file mode 100644 index 0000000000..cd335add2d --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/core/MultiLookupIndexScanner.groovy @@ -0,0 +1,16 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core + +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Multi key lookup may not be suitable or desirable for all cases, for example while it makes sense for Id based + * lookups, it is not a good idea for GeoSpatial lookups since there will be an overhead of de-duplication. + * + * @author Yazad Khambata + */ +interface MultiLookupIndexScanner extends IndexScanner { + Iterable fetch(final AtlasTable atlasTable, final IV... lookupValues) + + Iterable fetch(final AtlasTable atlasTable, final List lookupValues) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/id/IdIndexScanner.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/id/IdIndexScanner.groovy new file mode 100644 index 0000000000..f64649719b --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/id/IdIndexScanner.groovy @@ -0,0 +1,60 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.id + +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.query.InnerSelectWrapper +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.AbstractIndexScanner +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.InnerQueryLookupIndexScanner +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.MultiLookupIndexScanner +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.openstreetmap.atlas.geography.atlas.items.ItemType + +/** + * Analogous to clustered index scanner on a primary key of a db table. + * + * @author Yazad Khambata + */ +@Singleton +class IdIndexScanner extends AbstractIndexScanner implements MultiLookupIndexScanner, InnerQueryLookupIndexScanner { + @Override + Iterable fetch(final AtlasTable atlasTable, final Long lookupValue) { + this.fetchInternal(atlasTable, [lookupValue] as Set) + } + + @Override + Iterable fetch(final AtlasTable atlasTable, final Long... lookupValues) { + this.fetchInternal(atlasTable, lookupValues as Set) + } + + @Override + Iterable fetch(final AtlasTable atlasTable, final List lookupValues) { + this.fetchInternal(atlasTable, lookupValues as Set) + } + + @Override + Iterable fetch(final AtlasTable atlasTable, final QueryBuilder selectInnerQueryBuilder) { + final InnerSelectWrapper innerSelectWrapper = new InnerSelectWrapper(selectInnerQueryBuilder) + this.fetch(atlasTable, innerSelectWrapper) + } + + @Override + Iterable fetch(final AtlasTable atlasTable, final InnerSelectWrapper innerSelectWrapper) { + this.fetch(atlasTable, innerSelectWrapper.identifiers) + } + + /** + * Ensures de-duplication is NOT needed on the results. + * + * @param atlasTable + * @param lookupValues + * @return + */ + private Iterable fetchInternal(final AtlasTable atlasTable, final Set lookupValues) { + final Atlas atlas = toAtlas(atlasTable) + + final ItemType itemType = toItemType(atlasTable) + + itemType.entitiesForIdentifiers(atlas, lookupValues as Long[]) + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/spatial/SpatialIndexScanner.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/spatial/SpatialIndexScanner.groovy new file mode 100644 index 0000000000..faf2140a4b --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/spatial/SpatialIndexScanner.groovy @@ -0,0 +1,79 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.spatial + +import org.openstreetmap.atlas.geography.GeometricSurface +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.polygon.GeometricSurfaceSupport +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.AbstractIndexScanner +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.openstreetmap.atlas.geography.atlas.items.ItemType + +/** + * Analogous to a spatial index scanner in a database that supports geo-spatial data. + * + * @author Yazad Khambata + */ +@Singleton +class SpatialIndexScanner extends AbstractIndexScanner { + + /** + * @param atlasTable - The AtlasTable. + * @param lookupValue - expects [ [ [x, y], [a, b] ] ] (This is the same format as bounds within a GeoJson). + * Alternatively GeometricSurface can be directly passed as well. + * @return + */ + @Override + Iterable fetch(final AtlasTable atlasTable, final Object lookupValue) { + final Atlas atlas = toAtlas(atlasTable) + final ItemType itemType = toItemType(atlasTable) + + final SpatialIndexerSetting spatialIndexerSetting = SpatialIndexerSetting.from(itemType) + final GeometricSurface geometricSurfaceToLookup = toGeometricSurface(lookupValue) + spatialIndexerSetting.invokeEntitySpecificIndexAwareMethodName(atlas, geometricSurfaceToLookup) + } + + private GeometricSurface toGeometricSurface(final Object lookupValue) { + if (lookupValue instanceof GeometricSurface) { + return lookupValue + } + + GeometricSurfaceSupport.instance.toGeometricSurface((List>>) lookupValue) + } + + private enum SpatialIndexerSetting { + NODE, + + POINT, + + LINE, + + EDGE, + + RELATION("relationsWithEntitiesWithin"), + + AREA; + + private String entitySpecificIndexAwareMethodName + + SpatialIndexerSetting() { + this(null) + } + + SpatialIndexerSetting(final String entitySpecificIndexAwareMethodName) { + this.entitySpecificIndexAwareMethodName = entitySpecificIndexAwareMethodName + } + + static SpatialIndexerSetting from(final ItemType itemType) { + itemType.name() as SpatialIndexerSetting + } + + String getEntitySpecificIndexAwareMethodName() { + //Example nodesWithin + entitySpecificIndexAwareMethodName ?: "${this.name().toLowerCase()}sWithin" + } + + def Iterable invokeEntitySpecificIndexAwareMethodName(final Atlas atlas, final GeometricSurface geometricSurface) { + atlas."${getEntitySpecificIndexAwareMethodName()}"(geometricSurface) + } + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/strategy/ScanStrategizer.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/strategy/ScanStrategizer.groovy new file mode 100644 index 0000000000..1a13d5022d --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/strategy/ScanStrategizer.groovy @@ -0,0 +1,128 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.strategy + +import groovy.transform.TupleConstructor +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.IndexNonUseReason +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.NotConstraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.IndexSetting +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstruct +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstructList +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.strategy.ScanStrategy.IndexUsageInfo +import org.openstreetmap.atlas.geography.atlas.dsl.util.Valid +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Indexing can be used if and only if, + *
    + *
  • The FIRST constraint can fetch from the optionalIndexInfo
  • + *
  • one of the 2 conditions are met, + *
      + *
    1. There is only ONE constraint.
    2. + *
    3. The constrains are conjugated by AND
    4. + *
    + *
  • + *
+ * + * @author Yazad Khambata + */ +@Singleton +class ScanStrategizer { + + ScanStrategy strategize(final ConditionalConstructList conditionalConstructList) { + /* + * Split the conditionalConstructList - to first vs rest if an optionalIndexInfo can be used. + */ + final ScanInfo scanInfo = strategizeInternal(conditionalConstructList) + final ScanType scanType = scanInfo.scanType + final IndexNonUseReason indexNonUseReason = scanInfo.indexNonUseReason + + final Optional optionalIndexSetting = IndexSetting.from(scanType) + + if (optionalIndexSetting.isPresent()) { + final ScanStrategy scanStrategy = ScanStrategy.builder() + .indexUsageInfo(toIndexUsageInfo(optionalIndexSetting, conditionalConstructList, indexNonUseReason)) + .conditionalConstructListExcludingIndexedConstraintIfAny(new ConditionalConstructList(conditionalConstructList.getExcludingFirst())) + .build() + + return scanStrategy + } + + final ScanStrategy indexingStrategy = ScanStrategy.builder() + .indexUsageInfo(toIndexUsageInfo(optionalIndexSetting, conditionalConstructList, indexNonUseReason)) + .conditionalConstructListExcludingIndexedConstraintIfAny(conditionalConstructList) + .build() + + return indexingStrategy + } + + private IndexUsageInfo toIndexUsageInfo(final Optional optionalIndexSetting, + final ConditionalConstructList conditionalConstructList, + final IndexNonUseReason indexNonUseReason) { + if (optionalIndexSetting.isPresent()) { + final IndexSetting indexSetting = optionalIndexSetting.get() + final Constraint constraint = conditionalConstructList.getFirst().get().constraint + + return new IndexUsageInfo<>(indexSetting, constraint) + } + + return new IndexUsageInfo<>(indexNonUseReason) + } + + private ScanInfo strategizeInternal(final ConditionalConstructList conditionalConstructList) { + final Optional> optionalFirst = conditionalConstructList.getFirst() + + /* + * There are no constraints, hence needs a full scan. + */ + if (!optionalFirst.isPresent()) { + return new ScanInfo(ScanType.FULL, IndexNonUseReason.NO_WHERE_CLAUSE) + } + + final ConditionalConstruct firstConditionalConstruct = optionalFirst.get() + final ScanType firstBestCandidateScanType = firstBestCandidateScanType(firstConditionalConstruct) + + /* + * The firstBestCandidateScanStrategy IS FULL, hence use FULL. + */ + if (firstBestCandidateScanType == ScanType.FULL) { + final IndexNonUseReason indexNonUseReason = firstConditionalConstruct.constraint instanceof NotConstraint ? IndexNonUseReason.FIRST_WHERE_CLAUSE_USES_NOT_OPERATOR : IndexNonUseReason.FIRST_WHERE_CLAUSE_NEEDS_FULL_SCAN + + return new ScanInfo(ScanType.FULL, indexNonUseReason) + } + + final boolean orConditionUsed = conditionalConstructList.stream() + .map { conditionalConstruct -> conditionalConstruct.clause } + .filter { clause -> clause == Statement.Clause.OR } + .findFirst() + .isPresent() + + /* + * No optionalIndexInfo can be used if an OR condition is used, hence use FULL. + */ + if (orConditionUsed) { + return new ScanInfo(ScanType.FULL, IndexNonUseReason.WHERE_HAS_OR_USED) + } + + /* + * Index that can be actually used. + */ + return new ScanInfo(firstBestCandidateScanType, null) + } + + private ScanType firstBestCandidateScanType(ConditionalConstruct firstConditionalConstruct) { + Valid.isTrue firstConditionalConstruct.clause == Statement.Clause.WHERE + + final Constraint firstConstraint = firstConditionalConstruct.constraint + final ScanType firstBestCandidateScanType = firstConstraint.bestCandidateScanType + + firstBestCandidateScanType + } + + @TupleConstructor + private static class ScanInfo { + ScanType scanType + IndexNonUseReason indexNonUseReason + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/strategy/ScanStrategy.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/strategy/ScanStrategy.groovy new file mode 100644 index 0000000000..f04f821507 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/strategy/ScanStrategy.groovy @@ -0,0 +1,48 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.strategy + +import groovy.transform.builder.Builder +import org.apache.commons.lang3.Validate +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstructList +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.IndexNonUseReason +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.IndexSetting +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * Domain representing the strategy to scan data. + * + * @author Yazad Khambata + */ +@Builder +class ScanStrategy { + IndexUsageInfo indexUsageInfo + ConditionalConstructList conditionalConstructListExcludingIndexedConstraintIfAny + + boolean canUseIndex() { + this.indexUsageInfo.isIndexUsed() + } + + static class IndexUsageInfo { + IndexSetting indexSetting + Constraint constraint + + IndexNonUseReason indexNonUseReason + + IndexUsageInfo(IndexSetting indexSetting, Constraint constraint) { + this.indexSetting = indexSetting + this.constraint = constraint + + Validate.notNull(indexSetting) + Validate.notNull(constraint) + } + + IndexUsageInfo(IndexNonUseReason indexNonUseReason) { + this.indexNonUseReason = indexNonUseReason + Validate.notNull(indexNonUseReason) + } + + boolean isIndexUsed() { + indexNonUseReason == null + } + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/package-info.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/package-info.groovy new file mode 100644 index 0000000000..7ee3e28509 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/package-info.groovy @@ -0,0 +1,6 @@ +/** + * Deals with selection (σ) in terms of relational algebra and SQL. + * + * See Selection on Wikipedia + */ +package org.openstreetmap.atlas.geography.atlas.dsl.selection \ No newline at end of file diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/BaseContextualPredicate.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/BaseContextualPredicate.groovy new file mode 100644 index 0000000000..9091bea9da --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/BaseContextualPredicate.groovy @@ -0,0 +1,42 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate + +import java.util.function.Predicate + +/** + * The Java {@link Predicate}s in general do not have any contextual information of the operations being performed on + * them. The BaseContextualPredicate attempts to provide structure to Predicate and allow an introspection of + * operations being performed. + * + * Specific extensions store context as appropriate along with the Predicate. + * + * @author Yazad Khambata + */ +class BaseContextualPredicate implements Predicate { + + Predicate predicate + + @Override + boolean test(final T t) { + predicate.test(t) + } + + @Override + Predicate and(final Predicate other) { + predicate.and(other) + } + + @Override + Predicate negate() { + predicate.negate() + } + + @Override + Predicate or(final Predicate other) { + predicate.or(other) + } + + @Override + String toString() { + predicate.toString() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/ConstraintPredicate.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/ConstraintPredicate.groovy new file mode 100644 index 0000000000..6845b7924a --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/ConstraintPredicate.groovy @@ -0,0 +1,18 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate + +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint + +/** + * Captures the Constraint along side the Predicate. This allows one to introspect the operation being performed + * in the Predicate (via the constraint). + * + * @author Yazad Khambata + */ +@ToString(includeSuperProperties = true, includePackage = false) +@Builder(includeSuperProperties = true) +class ConstraintPredicate extends BaseContextualPredicate { + + Constraint constraint +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/Predicatable.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/Predicatable.groovy new file mode 100644 index 0000000000..adac2786ac --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/Predicatable.groovy @@ -0,0 +1,14 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate + +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Predicate + +/** + * Anything that can be represented as a predicate. + * + * @author Yazad Khambata + */ +interface Predicatable { + Predicate toPredicate(final Class entityClass) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/StatementPredicate.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/StatementPredicate.groovy new file mode 100644 index 0000000000..ae40ae543e --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/StatementPredicate.groovy @@ -0,0 +1,16 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate + +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement + +/** + * Store the statement alongside the Predicate. + * + * @author Yazad Khambata + */ +@ToString(includeSuperProperties = true) +@Builder(includeSuperProperties = true) +class StatementPredicate extends BaseContextualPredicate { + Statement statement +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/BinaryLogicalOperator.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/BinaryLogicalOperator.groovy new file mode 100644 index 0000000000..2de27df278 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/BinaryLogicalOperator.groovy @@ -0,0 +1,41 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.chained + +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement + +import java.util.function.Predicate + +/** + * The Binary Logical Operators that can actually perform logical operations on Predicates. + * + * @author Yazad Khambata + */ +enum BinaryLogicalOperator { + AND(Statement.Clause.AND){ + @Override + Predicate chain(final Predicate predicateA, final ConditionalConstructPredicate predicateB) { + predicateA.and(predicateB) + } + }, + + OR(Statement.Clause.OR){ + @Override + Predicate chain(final Predicate predicateA, final ConditionalConstructPredicate predicateB) { + predicateA.or(predicateB) + } + }; + + Statement.Clause clause + + BinaryLogicalOperator(Statement.Clause clause) { + this.clause = clause + } + + static BinaryLogicalOperator from(Statement.Clause clause) { + Arrays.stream(BinaryLogicalOperator.values()) + .filter { op -> op.clause == clause } + .findFirst() + .orElseThrow { new IllegalArgumentException("clause ${clause} is unsupported.") } + } + + abstract Predicate chain(Predicate predicateA, ConditionalConstructPredicate predicateB) +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/ChainedConditionalConstruct.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/ChainedConditionalConstruct.groovy new file mode 100644 index 0000000000..d229a7a0cd --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/ChainedConditionalConstruct.groovy @@ -0,0 +1,41 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.chained + +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.Predicatable +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import java.util.function.Predicate + +/** + * Chains a Predicate to a ConditionalConstructPredicate. + * + * @author Yazad Khambata + */ +@Builder +@ToString +class ChainedConditionalConstruct implements Predicatable { + + Predicate predicateA + + BinaryLogicalOperator op + + ConditionalConstructPredicate predicateB + + @Override + Predicate toPredicate(final Class entityClass) { + op.chain(predicateA, predicateB) + } + + Predicate toChainedConditionalConstructPredicate(final Class entityClass) { + final ChainedConditionalConstructPredicate chainedConditionalConstructPredicate = + ChainedConditionalConstructPredicate.builder() + .predicateA(predicateA) + .op(op) + .predicateB(predicateB) + .predicate(toPredicate(entityClass)) + .build() + + chainedConditionalConstructPredicate + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/ChainedConditionalConstructPredicate.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/ChainedConditionalConstructPredicate.groovy new file mode 100644 index 0000000000..97406a0fae --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/ChainedConditionalConstructPredicate.groovy @@ -0,0 +1,22 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.chained + +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.BaseContextualPredicate + +import java.util.function.Predicate + +/** + * Constructed by ChainedConditionalConstruct#toChainedConditionalConstructPredicate. + * + * @author Yazad Khambata + */ +@ToString(includeSuperProperties = true, includePackage = false) +@Builder(includeSuperProperties = true) +class ChainedConditionalConstructPredicate extends BaseContextualPredicate { + Predicate predicateA + + BinaryLogicalOperator op + + ConditionalConstructPredicate predicateB +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/ConditionalConstructPredicate.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/ConditionalConstructPredicate.groovy new file mode 100644 index 0000000000..6f90ac15d0 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/predicate/chained/ConditionalConstructPredicate.groovy @@ -0,0 +1,18 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.chained + +import groovy.transform.ToString +import groovy.transform.builder.Builder +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstruct +import org.openstreetmap.atlas.geography.atlas.dsl.selection.predicate.BaseContextualPredicate + +/** + * Store the ConditionalConstruct along with the Predicate. The ConditionalConstruct is generated by + * the Statement's WHERE clause. + * + * @author Yazad Khambata + */ +@ToString(includeSuperProperties = true, includePackage = false) +@Builder(includeSuperProperties = true) +class ConditionalConstructPredicate extends BaseContextualPredicate { + ConditionalConstruct conditionalConstruct +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/SingletonMap.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/SingletonMap.groovy new file mode 100644 index 0000000000..715d5b8eb0 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/SingletonMap.groovy @@ -0,0 +1,57 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.util + +/** + * A map with one and only one entry. + * + * @author Yazad Khambata + */ +class SingletonMap implements Map { + + @Delegate + private Map map + + SingletonMap(K k, V v) { + this(immutableMap(k, v)) + } + + SingletonMap(final Map map) { + super() + + Valid.isTrue map.size() == 1, "Singleton Maps must have one and only one entry." + + this.map = Collections.unmodifiableMap(map) + } + + private static Map immutableMap(final K k, final V v) { + final Map m = new HashMap<>() + m.put(k, v) + + Collections.unmodifiableMap(m) + } + + V put(K key, V value) { + throw new UnsupportedOperationException() + } + + @Override + void clear() { + throw new UnsupportedOperationException() + } + + @Override + void putAll(Map m) { + throw new UnsupportedOperationException() + } + + K getKey() { + map.keySet().iterator().next() + } + + V getValue() { + map.values().iterator().next() + } + + Map.Entry getEntry() { + map.entrySet().iterator().next() + } +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/StreamUtil.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/StreamUtil.groovy new file mode 100644 index 0000000000..2747a4faf6 --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/StreamUtil.groovy @@ -0,0 +1,45 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.util + +import java.util.stream.Stream +import java.util.stream.StreamSupport + +/** + * Util to work with Java's stream API. + * + * @author Yazad Khambata + */ +final class StreamUtil { + + private StreamUtil() {} + + static Stream stream(final Iterable iterable) { + StreamSupport.stream(iterable.spliterator(), false) + } + + static Stream stream(final Iterator iterator) { + final Iterable iterable = { -> iterator } + + stream(iterable) + } + + static Stream stream(Enumeration enumeration) { + /* + inspired by https://stackoverflow.com/a/33243700/1165727 + */ + Valid.notEmpty enumeration + + StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + new Iterator() { + T next() { + enumeration.nextElement() + } + + boolean hasNext() { + enumeration.hasMoreElements() + } + }, + Spliterator.ORDERED), false) + } + +} diff --git a/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/Valid.groovy b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/Valid.groovy new file mode 100644 index 0000000000..e2eefeefce --- /dev/null +++ b/src/main/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/Valid.groovy @@ -0,0 +1,40 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.util + +import org.apache.commons.lang3.Validate + +/** + * A validation util. + * + * @author Yazad Khambata + */ +class Valid { + static void notEmpty(object) { + if (!object) { + throwException("", null) + } + } + + static void notEmpty(object, final String message) { + try { + notEmpty(object) + } catch (e) { + throwException(message, e) + } + } + + static void isTrue(boolean expr) { + Validate.isTrue(expr) + } + + static void isTrue(boolean expr, final String message) { + try { + isTrue(expr) + } catch (e) { + throwException(message, e) + } + } + + private static void throwException(String message, Exception e) { + throw new IllegalStateException(message, e) + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/AbstractAQLTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/AbstractAQLTest.groovy new file mode 100644 index 0000000000..733f18cf84 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/AbstractAQLTest.groovy @@ -0,0 +1,34 @@ +package org.openstreetmap.atlas.geography.atlas.dsl + +import org.junit.BeforeClass +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasMediator +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl.ClasspathScheme + +/** + * @author Yazad Khambata + */ +abstract class AbstractAQLTest { + private static Map cache = [:] + + private static final String BUTTERFLY_PARK = "BUTTERFLY_PARK" + private static final String ALCATRAZ = "ALCATRAZ" + + @BeforeClass + static void setup() { + cache[BUTTERFLY_PARK] = loadOsmXmlFromClasspath(TestConstants.BUTTERFLY_PARK_CLASSPATH) + cache[ALCATRAZ] = loadOsmXmlFromClasspath(TestConstants.ALCATRAZ_CLASSPATH) + } + + private static AtlasSchema loadOsmXmlFromClasspath(final String classpathExcludingScheme) { + new AtlasSchema(new AtlasMediator(ClasspathScheme.instance.loadOsmXml(classpathExcludingScheme))) + } + + protected AtlasSchema usingAlcatraz() { + cache[ALCATRAZ] + } + + protected AtlasSchema usingButterflyPark() { + cache[BUTTERFLY_PARK] + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/TestConstants.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/TestConstants.groovy new file mode 100644 index 0000000000..4a7e08bce0 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/TestConstants.groovy @@ -0,0 +1,40 @@ +package org.openstreetmap.atlas.geography.atlas.dsl + +/** + * @author Yazad Khambata + */ +final class TestConstants { + private TestConstants() {} + + //The classpath: scheme is skipped here since we are directly invoking ClasspathScheme. + public static final String BUTTERFLY_PARK_CLASSPATH = "/data/ButterflyPark/ButterflyPark.osm" + public static final String ALCATRAZ_CLASSPATH = "/data/Alcatraz/Alcatraz.osm" + + static final class Polygons { + private Polygons() {} + + static final List> northernPartOfAlcatraz = [ + [-122.4237920517, 37.8265958156], [-122.4251081994, 37.8270138686], [-122.425258081, 37.8266032148], [-122.4256749392, 37.8271988471], [-122.4259465995, 37.8276908875], [-122.4265742287, 37.8279942492], [-122.4259231805, 37.8283198066], [-122.4240871311, 37.8285232793], [-122.4229396001, 37.8282865111], [-122.4226070504, 37.8278943619], [-122.422499323, 37.8276057981], [-122.4232534147, 37.8270471648], [-122.4237920517, 37.8265958156] + ] + + static final List> centralPartOfAlcatraz =[ + [-122.4222554295,37.8248723624],[-122.4237360089,37.825397785],[-122.4246586888,37.8257452398],[-122.4255813687,37.826448619],[-122.4251307575,37.8271096198],[-122.4240149586,37.8283214392],[-122.4211181729,37.8279824707],[-122.4206353752,37.8266689532],[-122.4216224281,37.825397785],[-122.4222554295,37.8248723624] + ] + + static final List> southernPartOfAlcatraz = [ + [-122.4219013779,37.8242876135],[-122.4228347866,37.8243299868],[-122.4237145512,37.8246520228],[-122.4235858051,37.8252367688],[-122.4231137364,37.8254655812],[-122.4206139176,37.8267028508],[-122.4191977112,37.826940133],[-122.4192513554,37.8256096479],[-122.4194337456,37.8248977862],[-122.4219013779,37.8242876135] + ] + + static final List> goldenGateParkSanFransiscoCalifornia = [ + [-122.51163482666014, 37.76250486498014], [-122.45378494262697, 37.76250486498014], [-122.45378494262697, 37.77654930110635], [-122.51163482666014, 37.77654930110635], [-122.51163482666014, 37.76250486498014] + ] + + static final List> guadalupeRiverParkAndGardensSanJoseCalifornia = [ + [-121.91665649414062, 37.352181090625315], [-121.91566944122313, 37.34627908137618], [-121.89747333526611, 37.32955988981435], [-121.89404010772704, 37.33607740505489], [-121.91082000732422, 37.354500828504115], [-121.91665649414062, 37.352181090625315] + ] + + static final List> provincetownMassachusetts = [ + [-70.24992942810059, 42.013016959237305], [-70.12899398803711, 42.013016959237305], [-70.12899398803711, 42.08420992526112], [-70.24992942810059, 42.08420992526112], [-70.24992942810059, 42.013016959237305] + ] + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/authentication/impl/SHA512HMACAuthenticatorImplTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/authentication/impl/SHA512HMACAuthenticatorImplTest.groovy new file mode 100644 index 0000000000..5c19059af6 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/authentication/impl/SHA512HMACAuthenticatorImplTest.groovy @@ -0,0 +1,27 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.authentication.impl + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.authentication.Authenticator + +/** + * @author Yazad Khambata + */ +class SHA512HMACAuthenticatorImplTest { + @Test + void sanity() { + final String message = "select relation.id, relation.osmId, relation.tags from atlasAIA.relation where relation.hasTagLike(name: /Pond/) or relation.hasTagLike(/natur/)" + final String preSignedMessage = "BnO2lgNrPIXxPEkSPwK5wFBsCwXXx+e+Md0nRbjhG8HXZ0zLTd9gqJ9FKrt9WKxgLrepRkCo2LAwJoKCMlX7KA==" + + final Authenticator authenticator = new SHA512HMACAuthenticatorImpl("DUMMY_SECRET") + + final String signedMessage = authenticator.sign(message) + assert signedMessage == preSignedMessage + + authenticator.verify(message, signedMessage) + + try { + authenticator.verify(message, signedMessage + " ") + assert false + } catch (Exception e) {} + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/ConsoleWriterTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/ConsoleWriterTest.groovy new file mode 100644 index 0000000000..e4e5b82fbb --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/console/ConsoleWriterTest.groovy @@ -0,0 +1,52 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.console + +import org.junit.Assert +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.console.impl.StandardOutputConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.console.impl.QuietConsoleWriter +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.* + +/** + * @author Yazad Khambata + */ +class ConsoleWriterTest extends AbstractAQLTest { + @Test + void testQuietConsoleWriter() { + final ConsoleWriter consoleWriter = QuietConsoleWriter.getInstance() + Assert.assertNotNull(consoleWriter) + Assert.assertTrue(consoleWriter.isTurnedOff()) + consoleWriter.echo("Hello") + } + + @Test + void testBasicConsoleWriter() { + final ConsoleWriter consoleWriter = StandardOutputConsoleWriter.getInstance() + Assert.assertNotNull(consoleWriter) + Assert.assertFalse(consoleWriter.isTurnedOff()) + consoleWriter.echo("Hello") + } + + @Test + void testNullString() { + final ConsoleWriter consoleWriter = StandardOutputConsoleWriter.getInstance() + consoleWriter.echo((String)null) + } + + @Test + void testNullObject() { + final ConsoleWriter consoleWriter = StandardOutputConsoleWriter.getInstance() + consoleWriter.echo(null) + } + + + @Test + void testExplain() { + def atlas = usingAlcatraz() + def select1 = select relation.id from atlas.relation where relation.hasId(1) + ExplainerImpl.instance.explain select1 + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/InsecureQueryExecutorImplTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/InsecureQueryExecutorImplTest.groovy new file mode 100644 index 0000000000..cf8912ce14 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/InsecureQueryExecutorImplTest.groovy @@ -0,0 +1,27 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.impl + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest + +/** + * @author Yazad Khambata + */ +class InsecureQueryExecutorImplTest extends AbstractAQLTest { + private static final String QUERY = """select node.id, node.osmId, node.tags from atlas.node limit 10""" + + @Test + void testExecString() { + new InsecureQueryExecutorImpl().exec(usingButterflyPark().atlasMediator.atlas, QUERY, null) + } + + @Test + void testExecFile() { + def file = new File("/tmp/${UUID.randomUUID()}.aql") + + file << QUERY + + new FileReader(file).withCloseable { fileReader -> + new InsecureQueryExecutorImpl().exec(usingButterflyPark().atlasMediator.atlas, fileReader, null) + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/SecureQueryExecutorImplTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/SecureQueryExecutorImplTest.groovy new file mode 100644 index 0000000000..3a8dae9704 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/impl/SecureQueryExecutorImplTest.groovy @@ -0,0 +1,53 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.impl + +import org.junit.BeforeClass +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.engine.QueryExecutor +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.SchemeSupport + +/** + * @author Yazad Khambata + */ +class SecureQueryExecutorImplTest extends AbstractAQLTest { + private static final String QUERY = """select node.id, node.osmId, node.tags from atlas.node limit 10""" + private static final String QUERY_SIGNATURE = "mCw1ob4SX8a4vMrMvEqI7iGovs/aPKq3hbk/rZ2vUFNasF7pjEJjwFOp496xAwEp32bgiIuW5YxJASVDqQ5oEw==" + + private static QueryExecutor queryExecutor + + @BeforeClass + static void setup() { + AbstractAQLTest.setup() + System.setProperty(AbstractQueryExecutorImpl.SYSTEM_PARAM_KEY, "DUMMY_SECRET") + queryExecutor = new SecureQueryExecutorImpl() + } + + @Test + void testExecString() { + queryExecutor.exec(usingButterflyPark().atlasMediator.atlas, QUERY, QUERY_SIGNATURE) + } + + @Test + void testExecStringBadSignature() { + try { + queryExecutor.exec(usingButterflyPark().atlasMediator.atlas, QUERY, "bad signature") + } catch (IllegalArgumentException e) { + assert e.getMessage().contains("Signature Mismatch") + return + } + + assert false + } + + @Test + void testExecFile() { + def file = new File("/tmp/${UUID.randomUUID()}.aql") + + file << QUERY + + new FileReader(file).withCloseable { fileReader -> + queryExecutor.exec(usingButterflyPark().atlasMediator.atlas, fileReader, QUERY_SIGNATURE) + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/LinterTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/LinterTest.groovy new file mode 100644 index 0000000000..9651f977d7 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/LinterTest.groovy @@ -0,0 +1,76 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.Source +import org.openstreetmap.atlas.geography.atlas.dsl.path.PathUtil +import org.openstreetmap.atlas.geography.atlas.dsl.engine.impl.AbstractQueryExecutorImpl +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintException +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintLogLevel +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintRequest +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.impl.InsecureWellFormednessLintlet +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.impl.WellFormednessLintlet + +import java.nio.file.Paths + +/** + * @author Yazad Khambata + */ +class LinterTest { + + public static final String CLASSPATH = "aql-files/" + + @Test + void sanity() { + Linter.instance.lint(new LintRequest("select node._ from atlas.node limit 10", null), InsecureWellFormednessLintlet) + } + + @Test + void bad() { + try { + Linter.instance.lint(new LintRequest("bad query", null), InsecureWellFormednessLintlet) + } catch (LintException e) { + assert e.lintResponses.size() == 1 + assert e.lintResponses.get(0).getLintResponseItems().size() == 1 + assert e.lintResponses.get(0).getLintResponseItems().get(0).lintLogLevel == LintLogLevel.ERROR + assert e.lintResponses.get(0).getLintResponseItems().get(0).message =~ /The query is broken query: / + } + } + + @Test + void testAll() { + System.setProperty(AbstractQueryExecutorImpl.SYSTEM_PARAM_KEY, UUID.randomUUID().toString()) + try { + Linter.instance.lint(Source.CLASSPATH, CLASSPATH, WellFormednessLintlet) + } catch (LintException e) { + assert e.getLintResponses().size() == PathUtil.instance.aqlFilesFromClasspath("aql-files").size() + e.getLintResponses().stream().forEach { lintResponse -> + assert lintResponse.getLintResponseItems().size() == 1 + assert lintResponse.getLintResponseItems().get(0).getMessage().contains("Signature Mismatch") + } + + return + } + + assert false + } + + @Test + void testAllInsecureClasspath() { + Linter.instance.lint(Source.CLASSPATH, CLASSPATH, InsecureWellFormednessLintlet) + + assert true + } + + @Test + void testAllInsecurePath() { + final String physicalPathAsStr = "/tmp/aql-files" + PathUtil.instance.deleteRecursivelyQuietly(Paths.get(physicalPathAsStr)) + + final URL url = this.getClass().getClassLoader().getResource(CLASSPATH) + PathUtil.instance.copyFolder(Paths.get(url.toURI()), Paths.get(physicalPathAsStr)) + + Linter.instance.lint(Source.PATH, physicalPathAsStr, InsecureWellFormednessLintlet) + + assert true + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/InsecureWellFormednessLintletTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/InsecureWellFormednessLintletTest.groovy new file mode 100644 index 0000000000..8049aecdff --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/InsecureWellFormednessLintletTest.groovy @@ -0,0 +1,86 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.impl + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintException +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintLogLevel +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintRequest +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.Lintlet +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * @author Yazad Khambata + */ +class InsecureWellFormednessLintletTest { + + private static final Logger log = LoggerFactory.getLogger(InsecureWellFormednessLintletTest) + + private String QUERY_1 = """select node.id, node.osmId, node.tags from atlas.node where node.hasLastUserName("granger") limit 10""" + + @Test + void sanity() { + Lintlet lintlet = new InsecureWellFormednessLintlet() + lintlet.doLint(new LintRequest(QUERY_1, null)) + assert true + } + + @Test + void bad1() { + final String query = """select node.BAD_COL_NAME from atlas.node where node.hasLastUserName("lestrange") limit 10""" + verifyQuery(query) + } + + @Test + void bad2() { + final String query = """Not a query""" + verifyQuery(query) + } + + @Test + void bad3() { + final String query = """select bad_table._ from atlas.node""" + verifyQuery(query) + } + + @Test + void bad4() { + final String query = """select node._ from atlas.bad_table1""" + verifyQuery(query) + } + + @Test + void bad5() { + final String query = """select node._ from missingAtlas.node""" + verifyQuery(query) + } + + private void verifyQuery(String query) { + Lintlet lintlet = new InsecureWellFormednessLintlet() + + final LintRequest lintRequest = new LintRequest(query, null) + + try { + lintlet.doLint(lintRequest) + } catch (LintException e) { + validException(e, lintRequest) + return + } + + assert false + } + + private void validException(LintException e, LintRequest lintRequest) { + log.error("", e) + + assert e.lintResponses + assert e.lintResponses.get(0).getLintResponseItems() + assert e.lintResponses.get(0).getLintRequest() == lintRequest + assert e.lintResponses.get(0).getLintletClass() == InsecureWellFormednessLintlet + assert e.lintResponses.get(0).getLintResponseItems().get(0).lintLogLevel == LintLogLevel.ERROR + assert e.lintResponses.get(0).getLintResponseItems().get(0).category == "Structural Error" + final String message = e.lintResponses.get(0).getLintResponseItems().get(0).message + assert message =~ /The query is broken query: / + assert message.contains(e.cause.message) + assert message.contains(lintRequest.queryAsString) + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/WellFormednessLintletTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/WellFormednessLintletTest.groovy new file mode 100644 index 0000000000..be6802e0fb --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/engine/lint/lintlet/impl/WellFormednessLintletTest.groovy @@ -0,0 +1,42 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.impl + +import org.junit.BeforeClass +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.engine.impl.AbstractQueryExecutorImpl +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintException +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.domain.LintRequest +import org.openstreetmap.atlas.geography.atlas.dsl.engine.lint.lintlet.Lintlet + +/** + * @author Yazad Khambata + */ +class WellFormednessLintletTest { + private static final String QUERY = """select node.id, node.osmId, node.tags from atlas.node limit 10""" + private static final String QUERY_SIGNATURE = "mCw1ob4SX8a4vMrMvEqI7iGovs/aPKq3hbk/rZ2vUFNasF7pjEJjwFOp496xAwEp32bgiIuW5YxJASVDqQ5oEw==" + + @BeforeClass + static void setup() { + System.setProperty(AbstractQueryExecutorImpl.SYSTEM_PARAM_KEY, "DUMMY_SECRET") + } + + @Test + void sanity() { + Lintlet lintlet = new WellFormednessLintlet() + + lintlet.doLint(new LintRequest(QUERY, QUERY_SIGNATURE)) + } + + @Test + void badSignature() { + Lintlet lintlet = new WellFormednessLintlet() + + try { + lintlet.doLint(new LintRequest(QUERY, "bad signature")) + } catch (LintException e) { + assert e.getMessage().contains("Signature Mismatch") + return + } + + assert false + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/FieldTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/FieldTest.groovy new file mode 100644 index 0000000000..15a4a71abf --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/field/FieldTest.groovy @@ -0,0 +1,39 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.field + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * @author Yazad Khambata + */ +class FieldTest extends AbstractAQLTest { + + @Test + void testEqualsAndHashCode() { + final AtlasSchema atlasSchema1 = usingAlcatraz() + final AtlasSchema atlasSchema2 = usingButterflyPark() + + final Map> tables1 = atlasSchema1.allTables + final Map> tables2 = atlasSchema2.allTables + + tables1.entrySet().stream().forEach { tableEntry -> + final AtlasTable atlasTable = tableEntry.getValue() + println "${atlasTable.class.simpleName}" + + final Map fieldMap = atlasTable.getAllFields() + + fieldMap.entrySet().stream().forEach { fieldEntry -> + def tableField1 = fieldEntry.value + println "\t${fieldEntry.key} -> ${ tableField1}" + + def tableField2 = tables2[tableEntry.key].getAllFields()[fieldEntry.key] + assert tableField2 == tableField1 + assert tableField2.hashCode() == tableField1.hashCode() + } + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathUtilTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathUtilTest.groovy new file mode 100644 index 0000000000..91410385ba --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/path/PathUtilTest.groovy @@ -0,0 +1,39 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.path + +import org.junit.Test + +import java.nio.file.Paths +import java.util.stream.Collectors + +/** + * @author Yazad Khambata + */ +class PathUtilTest { + + @Test + void testContentsFromClasspath() { + final PathQueryFilePackCollection classpathQueryFilePackCollection = + PathUtil.instance.aqlFilesFromClasspath("aql-files") + + classpathQueryFilePackCollection.stream().forEach { classpathQueryFilePack -> + println classpathQueryFilePack + } + } + + @Test + void testContentsFromPath() { + final String physicalPathAsStr = "/tmp/aql-files" + def classpath = "aql-files" + + def pathToDelete = Paths.get(physicalPathAsStr) + PathUtil.instance.deleteRecursivelyQuietly(pathToDelete) + + final URL url = this.getClass().getClassLoader().getResource(classpath) + PathUtil.instance.copyFolder(Paths.get(url.toURI()), Paths.get(physicalPathAsStr)) + + final PathQueryFilePackCollection pathQueryFilePackCollection1 = PathUtil.instance.aqlFilesFromPath(physicalPathAsStr) + final PathQueryFilePackCollection pathQueryFilePackCollection2 = PathUtil.instance.aqlFilesFromClasspath(classpath) + + assert pathQueryFilePackCollection1.stream().sorted().collect(Collectors.toList()) == pathQueryFilePackCollection2.stream().sorted().collect(Collectors.toList()) + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/CommitTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/CommitTest.groovy new file mode 100644 index 0000000000..a8265e26d2 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/CommitTest.groovy @@ -0,0 +1,87 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.items.Edge +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.stream.Collectors +import java.util.stream.StreamSupport + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.edge + +/** + * @author Yazad Khambata + */ +class CommitTest extends AbstractAQLTest { + + private static final Logger log = LoggerFactory.getLogger(CommitTest.class); + + @Test + void testOneQueryCommit() { + def atlas = usingAlcatraz() + + def update1 = update atlas.edge set edge.addTag(website: "https://www.nps.gov/alca") where edge.hasTagLike(name: /Pier/) or edge.hasTagLike(name: /Island/) or edge.hasTagLike(name: /Road/) + def update2 = update atlas.edge set edge.addTag(wikipedia: "https://en.wikipedia.org/wiki/Alcatraz_Island") where edge.hasTagLike(/foot/) + + def level1ChangesAtlas = commit update1, update2 + + verifyLevel1Updates(level1ChangesAtlas) + + def update3 = update level1ChangesAtlas.edge set edge.addTag(accessibility_website: "https://www.nps.gov/goga/planyourvisit/accessibility.htm") where edge.hasTagLike(/web*/) or edge.hasTagLike(/wiki*/) + + def level2ChangesAtlas = commit update3 + + final int edgesWithAccessibilityWebsite = verifyLevel2Updates(level2ChangesAtlas) + + def wikiSelect = select edge.id, edge.tags from level2ChangesAtlas.edge where edge.hasTag("accessibility_website") + + final Result result = exec wikiSelect + assert result.relevantIdentifiers.size() == edgesWithAccessibilityWebsite + } + + private int verifyLevel2Updates(AtlasSchema level2ChangesAtlas) { + final Atlas atlasAfterLevel2Commit = level2ChangesAtlas.atlasMediator.atlas + final List level2UpdatedEdges = StreamSupport.stream(atlasAfterLevel2Commit.edges { edge -> + return !edge.getTags { tagKey -> tagKey.contains("website") || tagKey.contains("wikipedia") }.isEmpty() + }.spliterator(), false).collect(Collectors.toList()) + + assert level2UpdatedEdges.size() > 1 + + final List edgesWithWebOrWiki = level2UpdatedEdges.stream().collect(Collectors.toList()) + edgesWithWebOrWiki.forEach { relation -> + assert relation.getTag("accessibility_website").get() == "https://www.nps.gov/goga/planyourvisit/accessibility.htm" + } + + return edgesWithWebOrWiki.size() + } + + private void verifyLevel1Updates(AtlasSchema level1ChangesAtlas) { + final Atlas atlasAfterLevel1Commit = level1ChangesAtlas.atlasMediator.atlas + + final List level1UpdatedEdges = StreamSupport.stream(atlasAfterLevel1Commit.edges { edge -> + final Optional tagValue = edge.getTag("name") + + if (!tagValue.isPresent()) { + return false + } + + def value = tagValue.get() + + log.info("name: ${value}") + + return value.contains("Pier") || value.startsWith("Island") || value.contains("Road") + }.spliterator(), false).collect(Collectors.toList()) + + assert level1UpdatedEdges.size() >= 3 + + level1UpdatedEdges.stream().forEach { edge -> + assert edge.getTag("website").isPresent() || edge.getTag("wikipedia").isPresent() + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/DeleteQueryTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/DeleteQueryTest.groovy new file mode 100644 index 0000000000..2904f92f17 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/DeleteQueryTest.groovy @@ -0,0 +1,60 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.change.Change +import org.openstreetmap.atlas.geography.atlas.change.FeatureChange +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.query.difference.Difference +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.items.Edge +import org.openstreetmap.atlas.geography.atlas.items.ItemType + +import java.util.stream.Collectors + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.* + +/** + * @author Yazad Khambata + */ +class DeleteQueryTest extends AbstractAQLTest { + + @Test + void testDelete() { + def atlasSchema = usingAlcatraz() + def selectBeforeDelete = select edge.tags from atlasSchema.edge where edge.hasTag(highway: "footway") and not(edge.hasTag(foot: "yes")) + final Result resultBeforeDelete = exec selectBeforeDelete + + def delete1 = delete atlasSchema.edge where edge.hasTag(highway: "footway") and not(edge.hasTag(foot: "yes")) + + final Result resultFromEdgeDelete = exec delete1 + + assert resultFromEdgeDelete.relevantIdentifiers.sort() == resultBeforeDelete.relevantIdentifiers.sort() + + def afterDelete = commit delete1 + + def selectAfterDelete = select edge._ from afterDelete.edge where edge.hasTag(highway: "footway") and not(edge.hasTag(foot: "yes")) + final Result resultAfterDelete = exec selectAfterDelete + + assert resultAfterDelete.relevantIdentifiers.isEmpty() + + final Difference difference = diff atlasSchema, afterDelete + final Change change = difference.getChange() + + final List featureChanges = change.getFeatureChanges() + final Map> featureChangesByItemType = featureChanges.stream().collect(Collectors.groupingBy { FeatureChange featureChange -> featureChange.getItemType() }) + + + assert featureChangesByItemType.size() == 2 + final int numberOfDeletedEdges = resultFromEdgeDelete.relevantIdentifiers.size() + assert numberOfDeletedEdges > 1 + assert featureChangesByItemType[(ItemType.EDGE)].size() == numberOfDeletedEdges + + final List terminalIds = resultFromEdgeDelete.entityStream(atlasSchema.atlasMediator) + .flatMap { Edge theEdge -> [theEdge.start().getIdentifier(), theEdge.end().getIdentifier()].stream() } + .distinct() + .collect(Collectors.toList()) + + assert featureChangesByItemType[(ItemType.NODE)].size() == terminalIds.size() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/DiffTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/DiffTest.groovy new file mode 100644 index 0000000000..3b82c8c9ee --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/DiffTest.groovy @@ -0,0 +1,34 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.apache.commons.lang3.tuple.Pair +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.change.Change +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.query.difference.Difference +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.MutantResult +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.items.ItemType + +import java.util.stream.Collectors + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.* + +/** + * @author Yazad Khambata + */ +class DiffTest extends AbstractAQLTest { + @Test + void testDiff() { + final AtlasSchema atlasSchema = usingAlcatraz() + def update1 = update atlasSchema.node set node.addTag(test: 'added') where node.hasTagLike(/\*wheelchair*/) or node.hasTagLike("operator") + final MutantResult result = exec update1 + final AtlasSchema afterUpdate = commit update1 + final Difference difference = diff atlasSchema, afterUpdate + assert toComparableList(result.getChange()) == toComparableList(difference.getChange()) + } + + private List> toComparableList(final Change change) { + change.changes().map { featureChange -> Pair.of(featureChange.getItemType(), featureChange.getIdentifier()) }.sorted().collect(Collectors.toList()) + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/ExplainTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/ExplainTest.groovy new file mode 100644 index 0000000000..20b2e1e1b9 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/ExplainTest.groovy @@ -0,0 +1,127 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.IndexSetting + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getRelation +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.node + +/** + * @author Yazad Khambata + */ +class ExplainTest extends AbstractAQLTest { + + def polygon = [ + TestConstants.Polygons.northernPartOfAlcatraz + ] + + def ids = [1641119524000000, 307446838000000, 3202364309000000, 3202364308000000, 1417681468000000] as Long[] + + @Test + void testNoWhereClause() { + def atlasSchema = usingAlcatraz() + + def select1 = select relation._ from atlasSchema.relation + + def explanation = ExplainerImpl.instance.explain(select1) + + assert !explanation.scanStrategy.canUseIndex() + assert !explanation.scanStrategy.indexUsageInfo.isIndexUsed() + assert !explanation.hasUnusedBetterIndexScanOptions() + assert explanation.scanStrategy.conditionalConstructListExcludingIndexedConstraintIfAny.size() == 0 + } + + @Test + void testWhereClauseFullScan() { + def atlasSchema = usingAlcatraz() + + def select1 = select node._ from atlasSchema.node where node.hasLastUserNameLike(/\w/) and node.hasIds(ids) and node.isWithin(polygon) + + def explanationSelect = ExplainerImpl.instance.explain select1 + + assert !explanationSelect.scanStrategy.canUseIndex() + assert !explanationSelect.scanStrategy.indexUsageInfo.isIndexUsed() + assert explanationSelect.hasUnusedBetterIndexScanOptions() + assert explanationSelect.scanStrategy.conditionalConstructListExcludingIndexedConstraintIfAny.size() == 3 + + ensureResultsAreConsistent(select1) + + def update1 = update atlasSchema.node set node.addTag("test": "test") where node.hasLastUserNameLike(/\w/) and node.hasIds(ids) and node.isWithin(polygon) + + validateCorrespondingUpdate(update1, explanationSelect) + } + + + @Test + void testWhereClauseIdUniqueScan() { + def atlasSchema = usingAlcatraz() + + def select1 = select node._ from atlasSchema.node where node.hasIds(ids) and node.hasLastUserNameLike(/\w/) and node.isWithin(polygon) + + def explanationSelect = ExplainerImpl.instance.explain select1 + + assert explanationSelect.scanStrategy.canUseIndex() + assert explanationSelect.scanStrategy.indexUsageInfo.isIndexUsed() + assert explanationSelect.scanStrategy.indexUsageInfo.indexSetting == IndexSetting.ID_UNIQUE_INDEX + assert explanationSelect.scanStrategy.indexUsageInfo.constraint.field == relation.id + assert !explanationSelect.hasUnusedBetterIndexScanOptions() + assert explanationSelect.scanStrategy.conditionalConstructListExcludingIndexedConstraintIfAny.size() == 2 + + ensureResultsAreConsistent(select1) + + def update1 = update atlasSchema.node set node.addTag("test": "test") where node.hasIds(ids) and node.hasLastUserNameLike(/\w/) and node.isWithin(polygon) + + validateCorrespondingUpdate(update1, explanationSelect) + } + + @Test + void testWhereClauseSpatialScan() { + def atlasSchema = usingAlcatraz() + + def select1 = select node._ from atlasSchema.node where node.isWithin(polygon) and node.hasIds(ids) and node.hasLastUserNameLike(/\w/) + + def explanationSelect = ExplainerImpl.instance.explain select1 + + assert explanationSelect.scanStrategy.canUseIndex() + assert explanationSelect.scanStrategy.indexUsageInfo.isIndexUsed() + assert explanationSelect.scanStrategy.indexUsageInfo.indexSetting == IndexSetting.SPATIAL_INDEX + assert explanationSelect.scanStrategy.indexUsageInfo.constraint.field == relation._ + assert explanationSelect.hasUnusedBetterIndexScanOptions() + assert explanationSelect.scanStrategy.conditionalConstructListExcludingIndexedConstraintIfAny.size() == 2 + + ensureResultsAreConsistent(select1) + + def update1 = update atlasSchema.node set node.addTag("test": "test") where node.isWithin(polygon) and node.hasIds(ids) and node.hasLastUserNameLike(/\w/) + + validateCorrespondingUpdate(update1, explanationSelect) + } + + private void ensureResultsAreConsistent(QueryBuilder anyQueryBuilder) { + exec anyQueryBuilder + + final Result result = exec anyQueryBuilder + assert result + assert result.relevantIdentifiers.sort() as Set == ids as Set + } + + private void validateCorrespondingUpdate(QueryBuilder updateQueryBuilder, Explanation explanationSelect) { + validateExplanationForCorrespondingUpdate(updateQueryBuilder, explanationSelect) + + ensureResultsAreConsistent(updateQueryBuilder) + } + + private void validateExplanationForCorrespondingUpdate(QueryBuilder update1, Explanation explanationSelect) { + def explanationUpdate = ExplainerImpl.instance.explain update1 + + assert explanationSelect.scanStrategy.canUseIndex() == explanationUpdate.scanStrategy.canUseIndex() + assert explanationSelect.scanStrategy.indexUsageInfo.isIndexUsed() == explanationUpdate.scanStrategy.indexUsageInfo.isIndexUsed() + assert explanationSelect.scanStrategy.conditionalConstructListExcludingIndexedConstraintIfAny.size() == explanationUpdate.scanStrategy.conditionalConstructListExcludingIndexedConstraintIfAny.size() + assert explanationSelect.hasUnusedBetterIndexScanOptions() == explanationUpdate.hasUnusedBetterIndexScanOptions() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/InnerSelectWrapperTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/InnerSelectWrapperTest.groovy new file mode 100644 index 0000000000..2a409c2b8a --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/InnerSelectWrapperTest.groovy @@ -0,0 +1,34 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getNode + +/** + * @author Yazad Khambata + */ +class InnerSelectWrapperTest extends AbstractAQLTest { + + @Test + void testSelectWithInnerQueryEqualsAndHashCode() { + final AtlasSchema atlasSchema = usingAlcatraz() + + final QueryBuilder innerQueryBuilder1 = select node.id, node.osmId, node.tags from atlasSchema.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + final QueryBuilder innerQueryBuilder2 = select node.id, node.osmId, node.tags from atlasSchema.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + + final Select innerSelect1 = innerQueryBuilder1.buildQuery() + final Select innerSelect2 = innerQueryBuilder2.buildQuery() + + assert innerSelect1 == innerSelect2 + assert innerSelect1.hashCode() == innerSelect2.hashCode() + + final InnerSelectWrapper innerSelectWrapper1 = new InnerSelectWrapper<>(innerSelect1) + final InnerSelectWrapper innerSelectWrapper2 = new InnerSelectWrapper<>(innerSelect2) + + assert innerSelectWrapper1 == innerSelectWrapper2 + assert innerSelectWrapper1.hashCode() == innerSelectWrapper2.hashCode() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/NotQueryTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/NotQueryTest.groovy new file mode 100644 index 0000000000..125ce45f1b --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/NotQueryTest.groovy @@ -0,0 +1,244 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.core.IndexSetting +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.* + +/** + * @author Yazad Khambata + */ +class NotQueryTest extends AbstractAQLTest { + private static final Logger log = LoggerFactory.getLogger(NotQueryTest.class) + + @Test + void testId() { + def atlasSchema = usingAlcatraz() + + final long id = 307459622000000 + + def selectSuperSet = select node.id, node.osmId, node.tags from atlasSchema.node + def selectPositiveSubset = select node.id, node.osmId, node.tags from atlasSchema.node where node.hasId(id) + def selectNegativeSubset = select node.id, node.osmId, node.tags from atlasSchema.node where not(node.hasId(id)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.ID_UNIQUE_INDEX) + } + + @Test + void testIds() { + def atlasSchema = usingAlcatraz() + + final Long[] ids = [307351652000000, 307459464000000, 307446864000000] + + def selectSuperSet = select node.id, node.osmId, node.tags from atlasSchema.node + def selectPositiveSubset = select node.id, node.osmId, node.tags from atlasSchema.node where node.hasIds(ids) + def selectNegativeSubset = select node.id, node.osmId, node.tags from atlasSchema.node where not(node.hasIds(ids)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.ID_UNIQUE_INDEX) + } + + @Test + void testOsmId() { + def atlasSchema = usingAlcatraz() + + final long id = 1417681452 + + def selectSuperSet = select node.id, node.osmId, node.tags from atlasSchema.node + def selectPositiveSubset = select node.id, node.osmId, node.tags from atlasSchema.node where node.hasOsmId(id) + def selectNegativeSubset = select node.id, node.osmId, node.tags from atlasSchema.node where not(node.hasOsmId(id)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.FULL) + } + + @Test + void testOsmIds() { + def atlasSchema = usingAlcatraz() + + final Long[] ids = [307459464, 3202364308, 307446838] + + def selectSuperSet = select node.id, node.osmId, node.tags from atlasSchema.node + def selectPositiveSubset = select node.id, node.osmId, node.tags from atlasSchema.node where node.hasOsmIds(ids) + def selectNegativeSubset = select node.id, node.osmId, node.tags from atlasSchema.node where not(node.hasOsmIds(ids)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.FULL) + } + + + @Test + void testTag() { + def atlasSchema = usingButterflyPark() + + def selectSuperSet = select node.id, node.osmId, node.tags from atlasSchema.node where node.hasTag('highway') + def selectPositiveSubset = select node.id, node.osmId, node.tags from atlasSchema.node where node.hasTag('highway') and node.hasTag(highway: 'bus_stop') + def selectNegativeSubset = select node.id, node.osmId, node.tags from atlasSchema.node where node.hasTag('highway') and not(node.hasTag(highway: 'bus_stop')) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.FULL) + } + + @Test + void testInBounds() { + def atlasSchema = usingAlcatraz() + + def selectSuperSet = select node.id, node.osmId, node.tags from atlasSchema.node + def northAlcatrazPolygon = [TestConstants.Polygons.northernPartOfAlcatraz] + def selectPositiveSubset = select node.id, node.osmId, node.tags from atlasSchema.node where node.isWithin(northAlcatrazPolygon) + def selectNegativeSubset = select node.id, node.osmId, node.tags from atlasSchema.node where not(node.isWithin(northAlcatrazPolygon)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.SPATIAL_INDEX) + } + + @Test + void testTagKeyRegex() { + def atlasSchema = usingButterflyPark() + + def selectSuperSet = select point.id, point.tags from atlasSchema.point where point.hasTagLike(image: /http.*:.*/) + def selectPositiveSubset = select point.id, point.tags from atlasSchema.point where point.hasTagLike(image: /http.*:.*/) and point.hasTagLike(/tour*/) + def selectNegativeSubset = select point.id, point.tags from atlasSchema.point where point.hasTagLike(image: /http.*:.*/) and not(point.hasTagLike(/tour*/)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.FULL) + } + + @Test + void testTagValueRegex() { + def atlasSchema = usingButterflyPark() + + //Note not(point.hasTagLike(highway: /cross*/)) --> will lead to rows where there is no website as well + //besides rows with http websites. hence the first check ensures the website tag is present. + + def selectSuperSet = select point.id, point.tags from atlasSchema.point where point.hasTag('highway') + def selectPositiveSubset = select point.id, point.tags from atlasSchema.point where point.hasTag('highway') and point.hasTagLike(highway: /cross*/) + def selectNegativeSubset = select point.id, point.tags from atlasSchema.point where point.hasTag('highway') and not(point.hasTagLike(highway: /cross*/)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.FULL) + } + + @Test + void testLastUserRegex() { + def atlasSchema = usingButterflyPark() + + def selectSuperSet = select point.id, point.tags from atlasSchema.point + def selectPositiveSubset = select point.id, point.tags from atlasSchema.point where point.hasLastUserNameLike(/^[A-Z]+$/) + def selectNegativeSubset = select point.id, point.tags from atlasSchema.point where not(point.hasLastUserNameLike(/^[A-Z]+$/)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.FULL) + } + + @Test + void testWhereAndLimit() { + def atlasSchema = usingAlcatraz() + + def select1 = select point.id, point.tags from atlasSchema.point where not(point.hasLastUserNameLike(/^[A-Z]+$/)) limit 10 + + def result1 = exec select1 + + assert result1.relevantIdentifiers.size() == 10 + } + + @Test + void testInnerQuery() { + def atlasSchema = usingAlcatraz() + + def select1 = select edge.id, edge.osmId, edge.tags from atlasSchema.edge where edge.hasTagLike(name: /Ferry/) or edge.hasTagLike(/surfac/) + def select2 = select edge.id, edge.osmId, edge.tags from atlasSchema.edge where edge.hasTag("highway") or edge.hasTagLike(/wheelchair/) + + def select3 = select edge.id, edge.tags from atlasSchema.edge where edge.hasIds(select1) or edge.hasIds(select2) + + def selectSuperSet = select edge.id, edge.tags, edge.osmTags from atlasSchema.edge + def selectPositiveSubset = select edge.id, edge.tags, edge.osmTags from atlasSchema.edge where edge.hasIds(select3) + def selectNegativeSubset = select edge.id, edge.tags, edge.osmTags from atlasSchema.edge where not(edge.hasIds(select3)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.ID_UNIQUE_INDEX) + } + + @Test + void testOr() { + def atlasSchema = usingAlcatraz() + + def selectSuperSet = select relation.id from atlasSchema.relation + def selectNegativeSubset = select relation.id from atlasSchema.relation where relation.hasId(3087373000000) or not(relation.hasTagLike(name: /BlahBlahBlah/)) + + def result1 = exec selectSuperSet + def result2 = exec selectNegativeSubset + + assert result1.relevantIdentifiers.sort() == result2.relevantIdentifiers.sort() + } + + @Test + void testUpdateByIds() { + def atlasSchema = usingAlcatraz() + + final Long[] ids = [307351652000000, 307459464000000, 307446864000000] + + def updateSuperSet = update atlasSchema.node set node.addTag(a: 'b') + def updatePositiveSubset = update atlasSchema.node set node.addTag(a: 'b') where node.hasIds(ids) + def updateNegativeSubset = update atlasSchema.node set node.addTag(a: 'b') where not(node.hasIds(ids)) + + verify(updateSuperSet, updatePositiveSubset, updateNegativeSubset, ScanType.ID_UNIQUE_INDEX) + } + + @Test + void testUpdateByBounds() { + def atlasSchema = usingAlcatraz() + + def polygon = [TestConstants.Polygons.northernPartOfAlcatraz] + + def selectSuperSet = update atlasSchema.area set area.addTag(a: 'b') + def selectPositiveSubset = update atlasSchema.area set area.addTag(a: 'b') where area.isWithin(polygon) + def selectNegativeSubset = update atlasSchema.area set area.addTag(a: 'b') where not(area.isWithin(polygon)) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.SPATIAL_INDEX) + } + + @Test + void testUpdateByTag() { + def atlasSchema = usingButterflyPark() + + def selectSuperSet = update atlasSchema.node set node.addTag(a: 'b') where node.hasTag('highway') + def selectPositiveSubset = update atlasSchema.node set node.addTag(a: 'b') where node.hasTag('highway') and node.hasTag(highway: 'crossing') + def selectNegativeSubset = update atlasSchema.node set node.addTag(a: 'b') where node.hasTag('highway') and not(node.hasTag(highway: 'crossing')) + + verify(selectSuperSet, selectPositiveSubset, selectNegativeSubset, ScanType.FULL) + } + + private void verify(querySuperSet, queryPositiveSubset, queryNegativeSubset, final ScanType expectedScanStrategy) { + verifyExplainPlan(querySuperSet, ScanType.FULL) + verifyExplainPlan(queryPositiveSubset, expectedScanStrategy) + verifyExplainPlan(querySuperSet, ScanType.FULL) + + def resultAll = exec querySuperSet + log.info "---" + def resultWithCriterion = exec queryPositiveSubset + log.info "---" + def resultWithCriterionNegated = exec queryNegativeSubset + log.info "---" + + assert resultAll.relevantIdentifiers.size() >= 2 + assert resultWithCriterion.relevantIdentifiers.size() >= 1 + assert resultWithCriterionNegated.relevantIdentifiers.size() >= 1 + + assert resultAll.relevantIdentifiers.sort() == (resultWithCriterion.relevantIdentifiers + resultWithCriterionNegated.relevantIdentifiers).sort() + } + + private void verifyExplainPlan(query, expectedScanStrategy) { + def explanation = ExplainerImpl.instance.explain query + + if (!explanation.scanStrategy.indexUsageInfo.isIndexUsed()) { //No Indexing Info (no where clause) + assert !explanation.scanStrategy.canUseIndex() + assert expectedScanStrategy == ScanType.FULL + } else { + if (expectedScanStrategy != ScanType.FULL) { //Not FULL + assert explanation.scanStrategy.canUseIndex() + assert explanation.scanStrategy.indexUsageInfo.indexSetting == IndexSetting.from(expectedScanStrategy).get() + } else { //FULL + assert !explanation.scanStrategy.canUseIndex() + } + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryEqualsAndHasCodeTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryEqualsAndHasCodeTest.groovy new file mode 100644 index 0000000000..c17930abf1 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryEqualsAndHasCodeTest.groovy @@ -0,0 +1,81 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.* + +/** + * @author Yazad Khambata + */ +class QueryEqualsAndHasCodeTest extends AbstractAQLTest { + @Test + void testSelectEqualsAndHashCode() { + final AtlasSchema atlas = usingAlcatraz() + + final QueryBuilder queryBuilder1 = select node.id, node.osmId, node.tags from atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + final QueryBuilder queryBuilder2 = select node.id, node.osmId, node.tags from atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + + final Select queryA1 = queryBuilder1.buildQuery() + final Select queryB1 = queryBuilder2.buildQuery() + assert queryA1.fieldsToSelect == queryB1.fieldsToSelect + assert queryA1.table == queryB1.table + assert queryA1.limit == queryB1.limit + assert queryA1.conditionalConstructList == queryB1.conditionalConstructList + assert queryA1 == queryB1 + assert queryA1.hashCode() == queryB1.hashCode() + } + + @Test + void testUpdateEqualsAndHashCode() { + final AtlasSchema atlas = usingAlcatraz() + + final QueryBuilder queryBuilder1 = update atlas.node set node.addTag(abc: 'xyz'), node.deleteTag('pqr') where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + final QueryBuilder queryBuilder2 = update atlas.node set node.addTag(abc: 'xyz'), node.deleteTag('pqr') where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + + final Update queryA1 = queryBuilder1.buildQuery() + final Update queryB1 = queryBuilder2.buildQuery() + assert queryA1.table == queryB1.table + assert queryA1.mutants == queryB1.mutants + assert queryA1.conditionalConstructList == queryB1.conditionalConstructList + assert queryA1 == queryB1 + assert queryA1.hashCode() == queryB1.hashCode() + } + + @Test + void testDeleteEqualsAndHashCode() { + final AtlasSchema atlas = usingAlcatraz() + + final QueryBuilder queryBuilder1 = delete atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + final QueryBuilder queryBuilder2 = delete atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + + final Delete queryA1 = queryBuilder1.buildQuery() + final Delete queryB1 = queryBuilder2.buildQuery() + assert queryA1.table == queryB1.table + assert queryA1.conditionalConstructList == queryB1.conditionalConstructList + assert queryA1 == queryB1 + assert queryA1.hashCode() == queryB1.hashCode() + } + + @Test + void testSelectWithInnerQueryEqualsAndHashCode() { + final AtlasSchema atlas = usingAlcatraz() + + final QueryBuilder innerQuery1 = select node.id, node.osmId, node.tags from atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + final QueryBuilder innerQuery2 = select node.id, node.osmId, node.tags from atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + + final QueryBuilder queryBuilder1 = select node.id, node.osmId, node.tags from atlas.node where node.hasIds(innerQuery1) and node.hasTag(pqr: /123/) + final QueryBuilder queryBuilder2 = select node.id, node.osmId, node.tags from atlas.node where node.hasIds(innerQuery2) and node.hasTag(pqr: /123/) + + final Select queryA1 = queryBuilder1.buildQuery() + final Select queryB1 = queryBuilder2.buildQuery() + assert queryA1.fieldsToSelect == queryB1.fieldsToSelect + assert queryA1.table == queryB1.table + assert queryA1.limit == queryB1.limit + assert queryA1.conditionalConstructList == queryB1.conditionalConstructList + assert queryA1 == queryB1 + assert queryA1.hashCode() == queryB1.hashCode() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QuerySanityTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QuerySanityTest.groovy new file mode 100644 index 0000000000..80b459c1cf --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QuerySanityTest.groovy @@ -0,0 +1,111 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.items.Node +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.* + +/** + * @author Yazad Khambata + */ +class QuerySanityTest extends AbstractAQLTest { + @Test + void testSameSelectExecMultipleTimes() { + def atlas = usingButterflyPark() + + def q = select node.id from atlas.node limit 2 + + (1..3).forEach { + final Result result = exec q + assert result.relevantIdentifiers.size() == 2 + } + } + + @Test + void testDifferentSelectSameTableExecMultipleTimes() { + def atlas = usingButterflyPark() + + def q1 = select node.id, node.osmId, node.tags from atlas.node where node.hasTag([foot: "no"]) or node.hasLastUserNameLike(/[A-Za-z]{5,12}/) limit 2 + def q2 = select node.id, node.osmId, node.tags from atlas.node where node.hasTag([foot: "no"]) or node.hasLastUserNameLike(/[A-Za-z]{5,12}/) limit 2 + def q3 = select node.id, node.osmId, node.tags from atlas.node where node.hasTag([foot: "no"]) or node.hasLastUserNameLike(/[A-Za-z]{5,12}/) limit 2 + + final Result result1 = exec q1 + final Result result2 = exec q2 + final Result result3 = exec q3 + + assert result1.relevantIdentifiers == result2.relevantIdentifiers + assert result2.relevantIdentifiers == result3.relevantIdentifiers + } + + @Test + void testDifferentSelectSameTableSameSchemasExecMultipleTimes() { + def atlas1 = usingButterflyPark() + def atlas2 = usingButterflyPark() + + def q1 = select node.id, node.osmId, node.tags from atlas1.node where node.hasTag([foot: "no"]) or node.hasLastUserNameLike(/[A-Za-z]{5,12}/) limit 2 + def q2 = select node.id, node.osmId, node.tags from atlas2.node where node.hasTag([foot: "no"]) or node.hasLastUserNameLike(/[A-Za-z]{5,12}/) limit 2 + def q3 = select node.id, node.osmId, node.tags from atlas1.node where node.hasTag([foot: "no"]) or node.hasLastUserNameLike(/[A-Za-z]{5,12}/) limit 2 + def q4 = select node.id, node.osmId, node.tags from atlas2.node where node.hasTag([foot: "no"]) or node.hasLastUserNameLike(/[A-Za-z]{5,12}/) limit 2 + + final Result result1 = exec q1 + final Result result2 = exec q2 + final Result result3 = exec q3 + final Result result4 = exec q4 + + assert result1.relevantIdentifiers == result2.relevantIdentifiers + assert result2.relevantIdentifiers == result3.relevantIdentifiers + assert result3.relevantIdentifiers == result4.relevantIdentifiers + } + + @Test + void testDifferentSelectSameTableDifferentSchemasExecMultipleTimes() { + def atlas1 = usingButterflyPark() + def atlas2 = usingAlcatraz() + + def q1 = select node.identifier, node.osmIdentifier, node.tags from atlas1.node where node.hasTag([foot: "no"]) or node.hasLastUserNameLike(/[A-Za-z]{5,12}/) limit 2 + def q2 = select node.identifier, node.osmIdentifier, node.tags from atlas2.node where node.hasTag([foot: "no"]) or node.hasLastUserNameLike(/[A-Za-z]{5,12}/) limit 2 + + final Result result1 = exec q1 + final Result result2 = exec q2 + + assert result1.relevantIdentifiers.size() == 2 + assert result2.relevantIdentifiers.size() == 2 + } + + @Test + void testAtlasSanity() { + final AtlasSchema atlasSchema1 = usingButterflyPark() + final AtlasSchema atlasSchema2 = usingAlcatraz() + + final Iterable nodes1 = atlasSchema1.atlasMediator.atlas.nodes() + final Iterable nodes2 = atlasSchema2.atlasMediator.atlas.nodes() + + assert nodes1.first() + assert nodes2.first() + } + + @Test + void testAllTables() { + final String fieldName = "id" + + final long noOfRecs = 5 + + final List atlasSchemas = [usingAlcatraz(), usingButterflyPark()] + + for (AtlasSchema atlasSchema : atlasSchemas) { + (1..2).forEach { dbCtr -> + (1..2).forEach { tableCtr -> + for (String tableName : atlasSchema.allTableNames) { + def query = select atlasSchema[tableName][fieldName] from atlasSchema["node"] limit noOfRecs + + final Result result = exec query + assert result.relevantIdentifiers.size() == noOfRecs + } + } + } + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryShallowCopyTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryShallowCopyTest.groovy new file mode 100644 index 0000000000..5e624705f9 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/QueryShallowCopyTest.groovy @@ -0,0 +1,61 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getNode + +/** + * @author Yazad Khambata + */ +class QueryShallowCopyTest extends AbstractAQLTest { + @Test + void testSelectShallowCopy() { + final AtlasSchema atlas = usingAlcatraz() + + final QueryBuilder queryBuilder1 = select node.id, node.osmId, node.tags from atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + final QueryBuilder queryBuilder2 = select node.id, node.osmId, node.tags from atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + + verifyShallowCopy(queryBuilder1, queryBuilder2) + } + + @Test + void testUpdateShallowCopy() { + final AtlasSchema atlas = usingAlcatraz() + + final QueryBuilder queryBuilder1 = update atlas.node set node.addTag(abc: 'xyz'), node.deleteTag('pqr') where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + final QueryBuilder queryBuilder2 = update atlas.node set node.addTag(abc: 'xyz'), node.deleteTag('pqr') where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + + verifyShallowCopy(queryBuilder1, queryBuilder2) + } + + @Test + void testDeleteShallowCopy() { + final AtlasSchema atlas = usingAlcatraz() + + final QueryBuilder queryBuilder1 = delete atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + final QueryBuilder queryBuilder2 = delete atlas.node where node.hasId(100) and node.hasOsmId(1) and node.hasTag(abc: /xyz/) + + verifyShallowCopy(queryBuilder1, queryBuilder2) + } + + private void verifyShallowCopy(QueryBuilder queryBuilder1, QueryBuilder queryBuilder2) { + final Query query1 = queryBuilder1.buildQuery() + final Query query2 = queryBuilder2.buildQuery() + + final Query query3 = query1.shallowCopy() + final Query query4 = query2.shallowCopy() + + ensureEquals(query1, query4, query3, query2) + } + + void ensureEquals(Query...queries) { + assert queries + assert queries.length > 1 + + assert (queries as Set).size() == 1 + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/SelectQueryMultiPolygonTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/SelectQueryMultiPolygonTest.groovy new file mode 100644 index 0000000000..e005489c21 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/SelectQueryMultiPolygonTest.groovy @@ -0,0 +1,61 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.items.Node + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getExec +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getNode + +/** + * @author Yazad Khambata + */ +class SelectQueryMultiPolygonTest extends AbstractAQLTest { + + @Test + void test() { + def atlas = usingAlcatraz() + + def polygonNorth = [TestConstants.Polygons.northernPartOfAlcatraz] + def polygonCentral = [TestConstants.Polygons.centralPartOfAlcatraz] + def polygonSouth = [TestConstants.Polygons.southernPartOfAlcatraz] + + def allPolygon =[ + TestConstants.Polygons.northernPartOfAlcatraz, + TestConstants.Polygons.centralPartOfAlcatraz, + TestConstants.Polygons.southernPartOfAlcatraz + ] + + final QueryBuilder select1 = select node.id, node.osmId, node.lastUserName, node.bounds, node.tags from atlas.node where node.isWithin(polygonNorth) + final QueryBuilder select2 = select node.id, node.osmId, node.lastUserName, node.bounds, node.tags from atlas.node where node.isWithin(polygonCentral) + final QueryBuilder select3 = select node.id, node.osmId, node.lastUserName, node.bounds, node.tags from atlas.node where node.isWithin(polygonSouth) + + final Result result1 = exec select1 + final Result result2 = exec select2 + final Result result3 = exec select3 + + def count1 = 17 + def count2 = 48 + def count3 = 6 + + assert result1.relevantIdentifiers.size() == count1 + assert result2.relevantIdentifiers.size() == count2 + assert result3.relevantIdentifiers.size() == count3 + + assert result1.relevantIdentifiers.intersect(result2.relevantIdentifiers).size() != 0 + assert result2.relevantIdentifiers.intersect(result3.relevantIdentifiers).size() != 0 + assert result3.relevantIdentifiers.intersect(result1.relevantIdentifiers).size() == 0 + + def select4 = select node.id, node.osmId, node.lastUserName, node.bounds, node.tags from atlas.node where node.isWithin(polygonNorth) or node.isWithin(polygonCentral) or node.isWithin(polygonSouth) + def select4Combined = select node.id, node.osmId, node.lastUserName, node.bounds, node.tags from atlas.node where node.isWithin(allPolygon) + + final Result result4 = exec select4 + final Result result4Combined = exec select4Combined + + assert result4.relevantIdentifiers.size() == result4Combined.relevantIdentifiers.size() + assert result4.relevantIdentifiers.sort() == result4Combined.relevantIdentifiers.sort() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/SelectQueryTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/SelectQueryTest.groovy new file mode 100644 index 0000000000..e7ff7afa54 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/SelectQueryTest.groovy @@ -0,0 +1,216 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.Edge + +import java.util.stream.Collectors + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.* + +/** + * @author Yazad Khambata + */ +class SelectQueryTest extends AbstractAQLTest { + + @Test + void testSelectFunctionFields() { + def atlas = usingAlcatraz() + + def selectEdge = select edge.id, edge.startId, edge.endId, edge.tags, edge.start, edge.end from atlas.edge where edge.hasTag(source: 'yahoo') and edge.hasTag('name') + final Result resultEdge = exec selectEdge + + assert resultEdge.getTable() instanceof AtlasTable + + final List listStartEndNodeIds = resultEdge.entityStream().flatMap { Edge e -> [e.start().getIdentifier(), e.end().getIdentifier()].stream() }.distinct().collect(Collectors.toList()) + final Long[] startEndNodeIds = listStartEndNodeIds.stream().toArray { new Long[listStartEndNodeIds.size()] } + + def selectNode = select node.id, node.inEdgeIds, node.inEdges, node.outEdges, node.outEdgeIds from atlas.node where node.hasIds(startEndNodeIds) + final Result resultNode = exec selectNode + + assert listStartEndNodeIds.sort() == resultNode.relevantIdentifiers.sort() + } + + @Test + void testSelectStar() { + def atlas = usingAlcatraz() + + def select1 = select line._ from atlas.line limit 5 + + final Explanation explanation = ExplainerImpl.instance.explain(select1) + assert !explanation.scanStrategy.indexUsageInfo.isIndexUsed() + + final Result result = exec select1 + assert result.relevantIdentifiers.size() == 5 + } + + @Test + void testExecWithRegex() { + def atlas = usingAlcatraz() + + def polygon = [ + TestConstants.Polygons.northernPartOfAlcatraz, + TestConstants.Polygons.centralPartOfAlcatraz, + TestConstants.Polygons.southernPartOfAlcatraz, + ] + + def select1 = select point.id, point.osmId, point.lastUserName, point.bounds, point.tags from atlas.point where point.hasTagLike(amenity: /e/) and point.hasTagLike(/toilets/) and point.isWithin(polygon) and point.hasLastUserNameLike(/^[a-zA-Z]+$/) + + final Explanation explanation = ExplainerImpl.instance.explain(select1) + assert !explanation.scanStrategy.indexUsageInfo.isIndexUsed() + + final Result result = exec select1 + final List identifiers = result.relevantIdentifiers + + assert identifiers.size() >= 1 + } + + @Test + void testExecWithWhereAndLimit() { + def atlas = usingAlcatraz() + + def polygon = [ + TestConstants.Polygons.northernPartOfAlcatraz, + TestConstants.Polygons.centralPartOfAlcatraz, + TestConstants.Polygons.southernPartOfAlcatraz + ] + + def select1 = select line.id, line.osmId, line.tags, line.osmTags from atlas.line where line.hasTag("barrier") and line.isWithin(polygon) limit 100 + + final Explanation explanation = ExplainerImpl.instance.explain(select1) + assert !explanation.scanStrategy.indexUsageInfo.isIndexUsed() + + final Result result = exec select1 + final List identifiers = result.relevantIdentifiers + assert identifiers.size() >= 1 && identifiers.size() <= 100 + + identifiers.stream().forEach({ identifier -> + final Atlas theAtlas = atlas.atlasMediator.atlas + assert theAtlas.line(identifier).getTag("barrier").isPresent() + }) + } + + @Test + void testExecWithWhere() { + def atlas = usingAlcatraz() + + def polygon = [ + TestConstants.Polygons.northernPartOfAlcatraz, + TestConstants.Polygons.centralPartOfAlcatraz, + TestConstants.Polygons.southernPartOfAlcatraz + ] + + def select1 = select edge.id, edge.osmId, edge.lastUserName, edge.isWaySectioned, edge.start, edge.end, edge.isClosed from atlas.edge where edge.isWithin(polygon) and edge.hasOsmId(27998971L) or edge.hasOsmId(27999864L) or edge.hasId(-629121014000000) + + final Explanation explanation = ExplainerImpl.instance.explain(select1) + assert !explanation.scanStrategy.indexUsageInfo.isIndexUsed() + + final Result result = exec select1 + final List identifiers = result.relevantIdentifiers + assert identifiers.size() >= 3 + } + + @Test + void testExecWithLimit() { + def atlas = usingAlcatraz() + + def select1 = select node.id, node.osmId from atlas.node limit 10 + + final Explanation explanation = ExplainerImpl.instance.explain(select1) + assert !explanation.scanStrategy.indexUsageInfo.isIndexUsed() + + final Result result = exec select1 + final List identifiers = result.relevantIdentifiers + assert identifiers.size() == 10 + } + + @Test + void testExecRelation() { + def atlas = usingAlcatraz() + + def polygon = [ + TestConstants.Polygons.northernPartOfAlcatraz + ] + + def select1 = select relation.id, relation.osmId, relation.allRelationsWithSameOsmIdentifier, relation.allKnownOsmMembers, relation.osmRelationIdentifier, relation.isMultiPolygon, relation.members from atlas.relation where not(relation.isWithin(polygon)) + + final Explanation explanation = ExplainerImpl.instance.explain(select1) + assert !explanation.scanStrategy.indexUsageInfo.isIndexUsed() + + final Result result = exec select1 + assert result.relevantIdentifiers.size() == 1 + } + + @Test + void testExecArea() { + def atlas = usingAlcatraz() + + def polygon = [ + TestConstants.Polygons.northernPartOfAlcatraz + ] + + def select1 = select area.id, area.osmId, area.tags, area.asPolygon, area.closedGeometry, area.rawGeometry from atlas.area where area.isWithin(polygon) and area.hasLastUserNameLike(/M/) limit 10 + + final Explanation explanation = ExplainerImpl.instance.explain(select1) + assert explanation.scanStrategy.indexUsageInfo.isIndexUsed() + assert explanation.scanStrategy.indexUsageInfo.indexSetting.scanType == ScanType.SPATIAL_INDEX + + final Result result = exec select1 + assert result.relevantIdentifiers.size() > 1 + final List identifiers = result.relevantIdentifiers + + identifiers.stream().forEach({ identifier -> + final Atlas theAtlas = atlas.atlasMediator.atlas + assert theAtlas.area(identifier).lastUserName().get().contains("M") + }) + } + + @Test + void testExecWithInClause() { + def atlas = usingAlcatraz() + + def select1 = select point.id, point.osmId from atlas.point where point.hasIds(5784941541000000, 4553243887000000, 307446836000000) or point.hasOsmIds(307446860, 2407548229) + + final Explanation explanation = ExplainerImpl.instance.explain(select1) + assert !explanation.scanStrategy.indexUsageInfo.isIndexUsed() + + final Result result = exec select1 + assert result.relevantIdentifiers.size() == 5 + } + + @Test + void testExecWithInnerQuery() { + def atlas = usingAlcatraz() + + def select1 = select edge.id, edge.osmId, edge.tags from atlas.edge where edge.hasTagLike(highway: /foot/) or edge.hasTagLike(/payment/) + def select2 = select edge.id, edge.osmId, edge.tags from atlas.edge where edge.hasTag("access") or edge.hasTagLike(/surface/) + + def select3 = select edge.id, edge.tags from atlas.edge where edge.hasIds(select1) or edge.hasIds(select2) + + def select4 = select edge.id, edge.tags, edge.osmTags from atlas.edge where edge.hasIds(select3) + + final Explanation explanation1 = ExplainerImpl.instance.explain( select1) + assert !explanation1.scanStrategy.indexUsageInfo.isIndexUsed() + + final Explanation explanation2 = ExplainerImpl.instance.explain(select2) + assert !explanation2.scanStrategy.indexUsageInfo.isIndexUsed() + + final Explanation explanation3 = ExplainerImpl.instance.explain(select3) + assert !explanation3.scanStrategy.indexUsageInfo.isIndexUsed() + + final Explanation explanation4 = ExplainerImpl.instance.explain(select4) + assert explanation4.scanStrategy.indexUsageInfo.isIndexUsed() + assert explanation4.scanStrategy.indexUsageInfo.indexSetting.scanType == ScanType.ID_UNIQUE_INDEX + + final Result result = exec select4 + assert result.relevantIdentifiers.size() > 1 + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/TagConditionTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/TagConditionTest.groovy new file mode 100644 index 0000000000..b63ac742e1 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/TagConditionTest.groovy @@ -0,0 +1,36 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getExec +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.edge + +/** + * @author Yazad Khambata + */ +class TagConditionTest extends AbstractAQLTest { + @Test + void testSelectStar() { + def atlasSchema = usingAlcatraz() + + def selectFootway = select edge.id, edge.tags from atlasSchema.edge where edge.hasTag(highway: "footway") + def selectSteps = select edge.id, edge.tags from atlasSchema.edge where edge.hasTag(highway: "steps") + def selectOr = select edge.id, edge.tags from atlasSchema.edge where edge.hasTag(highway: "footway") or edge.hasTag(highway: "steps") + def selectIn = select edge.id, edge.tags from atlasSchema.edge where edge.hasTag(highway: ["footway", "steps"]) + + final Result result1 = exec selectFootway + final Result result2 = exec selectSteps + final Result result3 = exec selectOr + final Result result4 = exec selectIn + + assert result1.relevantIdentifiers.size() >= 1 + assert result2.relevantIdentifiers.size() >= 1 + assert result3.relevantIdentifiers.size() >= 2 + + assert (result1.relevantIdentifiers + result2.relevantIdentifiers).sort() == result3.relevantIdentifiers.sort() + assert result3.relevantIdentifiers.sort() == result4.relevantIdentifiers.sort() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/UpdateQueryTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/UpdateQueryTest.groovy new file mode 100644 index 0000000000..5b1ed2b24d --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/UpdateQueryTest.groovy @@ -0,0 +1,94 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.change.FeatureChange +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.MutantResult +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType + +import java.util.stream.Collectors + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.* + +/** + * @author Yazad Khambata + */ +class UpdateQueryTest extends AbstractAQLTest { + @Test + void addTag() { + final String key = "accessible" + final String value = "true" + + verifyAddTag(key, value) + } + + /** + * Update use-case. + */ + @Test + void replace() { + final String key = "access" + final String value = "danger" + + verifyAddTag(key, value) + } + + @Test + void delete() { + final String key = "access" + verifyDeleteTag(key) + } + + private void verifyAddTag(String key, String value) { + def atlasSchema = usingAlcatraz() + + def update1 = update atlasSchema.node set node.addTag((key): value) where node.hasIds(307459622000000, 307446836000000) + + final Explanation explanation = ExplainerImpl.instance.explain update1 + assert explanation.scanStrategy.indexUsageInfo.indexSetting.scanType == ScanType.ID_UNIQUE_INDEX + + final MutantResult result = exec update1 + assert result.relevantIdentifiers.size() == 2 + + final List featureChanges = result.change.changes().collect(Collectors.toList()) + + assert featureChanges.size() == 2 + + featureChanges.stream().forEach { featureChange -> + final Atlas theAtlas = atlasSchema.atlasMediator.atlas + assert !featureChange.beforeView.getTag(key).isPresent() || featureChange.beforeView.getTag(key) != value + + assert !theAtlas.node(featureChange.getIdentifier()).getTag(key).isPresent() || theAtlas.node(featureChange.getIdentifier()).getTag(key).get() != value + assert featureChange.getAfterView().getTag(key).isPresent() + assert featureChange.getAfterView().getTag(key).get() == value + } + } + + private void verifyDeleteTag(String key) { + def atlasSchema = usingAlcatraz() + + def update1 = update atlasSchema.node set node.deleteTag(key) where node.hasId(307459622000000) or node.hasId(307446836000000) + + final Explanation explanation = ExplainerImpl.instance.explain update1 + assert !explanation.scanStrategy.indexUsageInfo.isIndexUsed() + + final MutantResult result = execute update1 + assert result.relevantIdentifiers.size() == 2 + + final List featureChanges = result.change.changes().collect(Collectors.toList()) + assert featureChanges.size() == 2 + + featureChanges.stream().forEach { featureChange -> + final Atlas theAtlas = atlasSchema.atlasMediator.atlas + + assert featureChange.beforeView.getTag(key).isPresent() + + assert theAtlas.node(featureChange.getIdentifier()).getTag(key).isPresent() + assert !featureChange.getAfterView().getTag(key).isPresent() + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/RuleBasedOptimizerImplTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/RuleBasedOptimizerImplTest.groovy new file mode 100644 index 0000000000..f4074e6028 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/RuleBasedOptimizerImplTest.groovy @@ -0,0 +1,13 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization + +import org.junit.Test + +/** + * @author Yazad Khambata + */ +class RuleBasedOptimizerImplTest { + @Test + void test() { + + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/AbstractQueryOptimizationTransformTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/AbstractQueryOptimizationTransformTest.groovy new file mode 100644 index 0000000000..b4da12819d --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/AbstractQueryOptimizationTransformTest.groovy @@ -0,0 +1,94 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getRelation + +/** + * @author Yazad Khambata + */ +class AbstractQueryOptimizationTransformTest extends BaseOptimizationTest { + @Test + void testAppliesNoParams() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation + + assertNotApplicable(select1) + } + + @Test + void testApplies1Param() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasId(1) + + assertNotApplicable(select1) + } + + @Test + void testApplies2ParamOr() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasId(1) or relation.hasId(2) + + assertApplicable(select1) + } + + @Test + void testApplies2ParamFullScanAnds() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasLastUserNameLike(/a/) and relation.hasTag(a: "b") + + assertNotApplicable(select1) + } + + @Test + void testApplies2ParamNonFullScanAnds() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.isWithin([ + [ + [-63.171752227, 18.1418553393], [-62.9506523734, 18.1418553393], [-62.9506523734, 18.2749168824], [-63.171752227, 18.2749168824], [-63.171752227, 18.1418553393] + ] + ]) and relation.isWithin([ + [ + [-63.171752227, 18.1418553393], [-62.9506523734, 18.1418553393], [-62.9506523734, 18.2749168824], [-63.171752227, 18.2749168824], [-63.171752227, 18.1418553393] + ] + ]) + + assertApplicable(select1) + } + + @Test + void testApplies3ParamCase1() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasId(1) and relation.isWithin([[[1.567, 2.123], [2.234, 3.567], [3.678, 1.124]]]) and relation.hasLastUserNameLike(/a/) + + assertApplicable(select1) + } + + @Test + void testApplies3ParamCase2() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.isWithin([[[1, 2], [2, 3], [3, 1]]]) and relation.hasId(1) and relation.hasLastUserNameLike(/a/) + + assertApplicable(select1) + } + + @Test + void testApplies3ParamCase3() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasLastUserNameLike(/a/) and relation.isWithin([[[1, 2], [2, 3], [3, 1]]]) and relation.hasId(1) + + assertApplicable(select1) + } + + protected boolean isApplicable(QueryBuilder select1) { + isApplicableAtBaseLevel(select1) + } + + @Override + QueryOptimizationTransformer associatedOptimization() { + throw new UnsupportedOperationException() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/BaseOptimizationTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/BaseOptimizationTest.groovy new file mode 100644 index 0000000000..623bed38d8 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/BaseOptimizationTest.groovy @@ -0,0 +1,55 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.QueryAnalyzer +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.domain.Analysis +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.impl.QueryAnalyzerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.RuleBasedOptimizerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * @author Yazad Khambata + */ +abstract class BaseOptimizationTest extends AbstractAQLTest { + private static final Logger log = LoggerFactory.getLogger(BaseOptimizationTest.class) + + protected void assertNotApplicable(select1) { + assert !isApplicable(select1) + } + + protected void assertApplicable(select1) { + def applicable = isApplicable(select1) + + log.info("applicable: ${applicable}") + + assert applicable + } + + protected boolean isApplicableAtBaseLevel(select1) { + final QueryOptimizationTransformer optimizationTransform = OptimizationTestHelper.instance.abstractQueryOptimizationTransform() + final Explanation explanation = ExplainerImpl.instance.explain(select1) + final OptimizationRequest optimizationRequest = QueryAnalyzerImpl.from(explanation) + + optimizationTransform.isApplicable(optimizationRequest) + } + + protected QueryAnalyzer createQueryAnalyzer(QueryOptimizationTransformer queryOptimizationTransformer) { + final QueryAnalyzer queryAnalyzer = new QueryAnalyzerImpl<>(ExplainerImpl.instance, new RuleBasedOptimizerImpl(queryOptimizationTransformer)) + queryAnalyzer + } + + protected boolean isApplicable(QueryBuilder select1) { + final QueryAnalyzer queryAnalyzer = createQueryAnalyzer(associatedOptimization()) + final Analysis analysis = queryAnalyzer.analyze(select1) + + analysis.checkIfOptimized() + } + + abstract QueryOptimizationTransformer associatedOptimization() +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesOverlapOptimizationTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesOverlapOptimizationTest.groovy new file mode 100644 index 0000000000..498c69c796 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesOverlapOptimizationTest.groovy @@ -0,0 +1,56 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getNode + +/** + * @author Yazad Khambata + */ +class GeometricSurfacesOverlapOptimizationTest extends BaseOptimizationTest { + + @Test + void testIsApplicable1() { + def atlas = usingAlcatraz() + + final QueryBuilder select1 = select node.id from atlas.node where node.isWithin([TestConstants.Polygons.northernPartOfAlcatraz]) + + assertApplicable(select1) + } + + @Test + void testIsApplicable2() { + def atlas = usingAlcatraz() + + final QueryBuilder select1 = select node.id from atlas.node where node.isWithin([TestConstants.Polygons.northernPartOfAlcatraz]) and node.isWithin([TestConstants.Polygons.southernPartOfAlcatraz]) + + assertApplicable(select1) + } + + @Test + void testIsApplicable3() { + def atlas = usingAlcatraz() + + final QueryBuilder select1 = select node.id from atlas.node where node.isWithin([TestConstants.Polygons.northernPartOfAlcatraz]) or node.isWithin([TestConstants.Polygons.centralPartOfAlcatraz]) or node.isWithin([TestConstants.Polygons.southernPartOfAlcatraz]) + + assertApplicable(select1) + } + + @Test + void testIsApplicable4() { + def atlas = usingButterflyPark() + + final QueryBuilder select1 = select node.id from atlas.node where node.isWithin([TestConstants.Polygons.northernPartOfAlcatraz, TestConstants.Polygons.southernPartOfAlcatraz]) + + assertApplicable(select1) + } + + @Override + QueryOptimizationTransformer associatedOptimization() { + GeometricSurfacesOverlapOptimization.instance + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesWithinOptimizationTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesWithinOptimizationTest.groovy new file mode 100644 index 0000000000..b4fb3e9bba --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/GeometricSurfacesWithinOptimizationTest.groovy @@ -0,0 +1,68 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.impl.QueryAnalyzerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.exec +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getNode + +/** + * @author Yazad Khambata + */ +class GeometricSurfacesWithinOptimizationTest extends BaseOptimizationTest { + @Test + void testEligible1() { + def atlas = usingAlcatraz() + + final QueryBuilder select1 = select node.id, node.osmId, node.lastUserName, node.bounds, node.tags from atlas.node where node.isWithin([TestConstants.Polygons.northernPartOfAlcatraz]) or node.isWithin([TestConstants.Polygons.centralPartOfAlcatraz]) or node.isWithin([TestConstants.Polygons.southernPartOfAlcatraz]) + + assertApplicable(select1) + } + + @Test + void testEligible2() { + def atlas = usingAlcatraz() + + final QueryBuilder select1 = select node.id, node.osmId, node.lastUserName, node.bounds, node.tags from atlas.node where node.isWithin([TestConstants.Polygons.northernPartOfAlcatraz]) or node.isWithin([TestConstants.Polygons.centralPartOfAlcatraz]) or node.isWithin([TestConstants.Polygons.southernPartOfAlcatraz]) or node.hasIds(1,2,3) + + OptimizationTestHelper.instance.abstractQueryOptimizationTransform().isApplicable(QueryAnalyzerImpl.from(ExplainerImpl.instance.explain(select1.buildQuery()))) + assertNotApplicable(select1) + } + + @Test + void testEligible3() { + def atlas = usingAlcatraz() + + final QueryBuilder select1 = select node.id, node.osmId, node.lastUserName, node.bounds, node.tags from atlas.node where node.isWithin([TestConstants.Polygons.northernPartOfAlcatraz]) and node.isWithin([TestConstants.Polygons.southernPartOfAlcatraz]) + + OptimizationTestHelper.instance.abstractQueryOptimizationTransform().isApplicable(QueryAnalyzerImpl.from(ExplainerImpl.instance.explain(select1.buildQuery()))) + assertNotApplicable(select1) + } + + @Test + void test() { + def atlas = usingAlcatraz() + + final QueryBuilder select1 = select node.id, node.osmId, node.lastUserName, node.bounds, node.tags from atlas.node where node.isWithin([TestConstants.Polygons.northernPartOfAlcatraz]) or node.isWithin([TestConstants.Polygons.centralPartOfAlcatraz]) or node.isWithin([TestConstants.Polygons.southernPartOfAlcatraz]) + + assertApplicable(select1) + + final Query transformedQuery = GeometricSurfacesWithinOptimization.instance.applyTransformation(QueryAnalyzerImpl.from(ExplainerImpl.instance.explain(select1))) + + def result1 = exec select1 + def result2 = transformedQuery.execute() + + assert result1.relevantIdentifiers.sort() == result2.relevantIdentifiers.sort() + } + + @Override + QueryOptimizationTransformer associatedOptimization() { + GeometricSurfacesWithinOptimization.instance + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/IdsInOptimizationTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/IdsInOptimizationTest.groovy new file mode 100644 index 0000000000..e6c35e95d6 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/IdsInOptimizationTest.groovy @@ -0,0 +1,103 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.apache.commons.math3.util.CombinatoricsUtils +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.impl.QueryAnalyzerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.BinaryOperations +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.ScanType +import org.openstreetmap.atlas.geography.atlas.items.Relation + +import java.util.stream.Collectors + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.exec +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getRelation + +/** + * @author Yazad Khambata + */ +class IdsInOptimizationTest extends BaseOptimizationTest { + + @Test + void testEligibility1() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasId(1) or relation.hasId(2) + + assertApplicable(select1) + } + + @Test + void testEligibility2() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasIds(1, 3) or relation.hasId(2) + + assert isApplicableAtBaseLevel(select1) + assertApplicable(select1) + } + + @Test + void testEligibility3() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasIds(1, 3) or relation.hasLastUserName("whatever") + + OptimizationTestHelper.instance.abstractQueryOptimizationTransform().isApplicable(QueryAnalyzerImpl.from(ExplainerImpl.instance.explain(select1))) + assertNotApplicable(select1) + } + + @Test + void testEligibility() { + def atlas = usingAlcatraz() + + final QueryBuilder inner1 = select relation.id from atlas.relation where relation.hasIds(5369365000000, 5369366000000, 5369367000000) + final QueryBuilder inner2 = select relation.id from atlas.relation where relation.hasIds(7551669000000, 7553474000000, 7625293000000) + + def constraint1 = relation.hasId(2698310000000) + def constraint2 = relation.hasId(2714283000000) + def constraint3 = relation.hasIds(3085693000000, 3087373000000) + def constraint4 = relation.hasIds(6160034000000, 338580000000) + def constraint5 = relation.hasIds(inner1) + def constraint6 = relation.hasIds(inner2) + + final List> constraints = [ + constraint1, constraint2, constraint3, constraint4, constraint5, constraint6 + ] + + //Order unimportant here + final Iterator iterator = CombinatoricsUtils.combinationsIterator(6, 2) + + while (iterator.hasNext()) { + final int[] indexes = iterator.next() + + assert indexes.length == 2 + + final QueryBuilder select1 = select(relation.id).from(atlas.relation).where(constraints[indexes[0]]).or(constraints[indexes[1]]) + + assertApplicable(select1) + + + + final Query optimizedQuery = IdsInOptimization.instance.applyTransformation(QueryAnalyzerImpl.from(ExplainerImpl.instance.explain(select1))) + + assert optimizedQuery.conditionalConstructList.size() == 1 + assert optimizedQuery.conditionalConstructList.get(0).constraint.field == relation.id + assert optimizedQuery.conditionalConstructList.get(0).constraint.bestCandidateScanType == ScanType.ID_UNIQUE_INDEX + assert optimizedQuery.conditionalConstructList.get(0).constraint.operation == BinaryOperations.inside + + final Result result1 = exec select1 + final Result result2 = optimizedQuery.execute() + + assert result1.relevantIdentifiers.stream().sorted().collect(Collectors.toList()) == result2.relevantIdentifiers.stream().sorted().collect(Collectors.toList()) + } + } + + @Override + QueryOptimizationTransformer associatedOptimization() { + IdsInOptimization.instance + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/ManualOptimizationTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/ManualOptimizationTest.groovy new file mode 100644 index 0000000000..d4c2f31636 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/ManualOptimizationTest.groovy @@ -0,0 +1,167 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.apache.commons.math3.util.CombinatoricsUtils +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstruct +import org.openstreetmap.atlas.geography.atlas.dsl.query.ConditionalConstructList +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.Statement +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.items.Area +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.openstreetmap.atlas.geography.atlas.items.Relation + +import java.util.stream.Collectors +import java.util.stream.StreamSupport + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.exec +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getArea + +/** + * @author Yazad Khambata + */ +class ManualOptimizationTest extends AbstractAQLTest { + + def polygonOfNorthernPart = [ + [ + [-122.4237920517,37.8265958156],[-122.4251081994,37.8270138686],[-122.425258081,37.8266032148],[-122.4256749392,37.8271988471],[-122.4259465995,37.8276908875],[-122.4265742287,37.8279942492],[-122.4259231805,37.8283198066],[-122.4240871311,37.8285232793],[-122.4229396001,37.8282865111],[-122.4226070504,37.8278943619],[-122.422499323,37.8276057981],[-122.4232534147,37.8270471648],[-122.4237920517,37.8265958156] + ] + ] + + final long id1 = 28000041000000 + final long id2 = 24433395000000 + final long id3 = 660870452000000 + final long id4 = 396629347000000 + final long id5 = 27996732000000 + + final Long[] ids = [id1, id2, id3, id4, id5] as Long[] + + final Long[] idsGroup1 = [id1, id2, id3] as Long[] + final Long[] idsGroup2 = [id4, id5] as Long[] + + @Test + void testReorderingOptimization() { + def atlasSchema = usingAlcatraz() + + final Constraint constraint1 = area.hasIds(ids) + final Constraint constraint2 = area.isWithin(polygonOfNorthernPart) + final Constraint constraint3 = area.hasLastUserNameLike(/a/) + + final List> preferredOrder = [constraint1, constraint2, constraint3] + + final Iterable>> iterable = { + new PermutationGenerator<>(preferredOrder) + } + + final int sum = StreamSupport.stream(iterable.spliterator(), false) + .mapToInt() { permutation -> + final QueryBuilder qb1 = select(area._).from(atlasSchema.area).where(permutation[0]).and(permutation[1]).and(permutation[2]) + final Result result1 = exec qb1 + assert result1.relevantIdentifiers as Set == expectedIdsInResult() + + final List> constraintsInPreferredOrder = permutation.stream() + .sorted(Comparator.comparing { Constraint constraint -> constraint.bestCandidateScanType.preferntialRank }) + .collect(Collectors.toList()) + + final QueryBuilder qb2 = select(area._).from(atlasSchema.area).where(constraintsInPreferredOrder[0]).and(constraintsInPreferredOrder[1]).and(constraintsInPreferredOrder[2]) + final Result result2 = exec qb2 + assert result2.relevantIdentifiers as Set == expectedIdsInResult() + + 1 + } + .sum() + + assert sum == CombinatoricsUtils.factorial(preferredOrder.size()) + } + + private Set expectedIdsInResult() { + [id1, id2, id3, id4, id5] as Set + } + + @Test + void testInOptimization1() { + def atlasSchema = usingAlcatraz() + + final List> constraints = Arrays.stream(ids) + .map { area.hasId(it) } + .collect(Collectors.toList()) + + assert constraints.size() == 5 + + final QueryBuilder qb1 = select(area._).from(atlasSchema.area) + .where(constraints[0]) + .or(constraints[1]) + .or(constraints[2]) + .or(constraints[3]) + .or(constraints[4]) + + final QueryBuilder qb2 = select(area._).from(atlasSchema.area) + .where(area.hasIds(ids)) + + assertExecEquals(qb1, qb2) + + + final Query query3 = qb1.buildQuery().shallowCopyWithConditionalConstructList(inIdsConditionalConstructList()) + + assert qb2.buildQuery() == query3 + } + + private ConditionalConstructList inIdsConditionalConstructList() { + new ConditionalConstructList<>([ConditionalConstruct.builder().clause(Statement.Clause.WHERE).constraint(area.hasIds(ids)).build()]) + } + + @Test + void testInOptimization2() { + def atlasSchema = usingAlcatraz() + + final QueryBuilder qb1 = select(area._).from(atlasSchema.area) + .where(area.hasIds(idsGroup1)) + .or(area.hasIds(idsGroup2)) + + final QueryBuilder qb2 = select(area._).from(atlasSchema.area) + .where(area.hasIds(ids)) + + assertExecEquals(qb1, qb2) + + final Query query3 = qb1.buildQuery().shallowCopyWithConditionalConstructList(inIdsConditionalConstructList()) + + assert qb2.buildQuery() == query3 + } + + + @Test + void testInOptimization3() { + def atlasSchema = usingAlcatraz() + + final QueryBuilder inner1 = select area._ from atlasSchema.area where area.hasIds(idsGroup1) + final QueryBuilder inner2 = select area._ from atlasSchema.area where area.hasIds(idsGroup2) + final QueryBuilder qb1 = select(area._).from(atlasSchema.area) + .where(area.hasIds(inner1)).or(area.hasIds(inner2)) + + final QueryBuilder qb2 = select(area._).from(atlasSchema.area) + .where(area.hasIds(ids)) + + assertExecEquals(qb1, qb2) + + final Long[] idsExtracted = qb2.buildQuery().conditionalConstructList.get(0).constraint.valueToCheck + + assert Arrays.stream(ids).sorted().collect(Collectors.toList()) == Arrays.stream(idsExtracted).sorted().collect(Collectors.toList()) + } + + private void assertExecEquals(QueryBuilder qb1, QueryBuilder qb2) { + assert !qb1.is(qb2) + + final Result result1 = exec qb1 + final Result result2 = exec qb2 + + assert toIds(result1) == toIds(result2) + } + + private List toIds(Result result) { + result.relevantIdentifiers.stream().sorted().collect(Collectors.toList()) + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/OptimizationTestHelper.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/OptimizationTestHelper.groovy new file mode 100644 index 0000000000..2da391a41f --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/OptimizationTestHelper.groovy @@ -0,0 +1,27 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import groovy.transform.PackageScope +import org.openstreetmap.atlas.geography.atlas.dsl.query.Query +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.Explanation +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest + +/** + * @author Yazad Khambata + */ +@PackageScope +@Singleton +class OptimizationTestHelper { + AbstractQueryOptimizationTransform abstractQueryOptimizationTransform() { + new AbstractQueryOptimizationTransform() { + @Override + boolean areAdditionalChecksMet(final OptimizationRequest optimizationRequest) { + true + } + + @Override + Query applyTransformation(final OptimizationRequest optimizationRequest) { + optimizationRequest.query + } + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/ReorderingOptimizationTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/ReorderingOptimizationTest.groovy new file mode 100644 index 0000000000..d0fb5681fc --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/query/optimizer/optimization/impl/ReorderingOptimizationTest.groovy @@ -0,0 +1,206 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.impl + +import org.apache.commons.math3.util.CombinatoricsUtils +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.query.analyzer.impl.QueryAnalyzerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.explain.ExplainerImpl +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.domain.OptimizationRequest +import org.openstreetmap.atlas.geography.atlas.dsl.query.optimizer.optimization.QueryOptimizationTransformer +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint +import org.openstreetmap.atlas.geography.atlas.items.Relation +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.stream.StreamSupport + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.* +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getRelation +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.node + +/** + * @author Yazad Khambata + */ +class ReorderingOptimizationTest extends BaseOptimizationTest { + private static final Logger log = LoggerFactory.getLogger(ReorderingOptimizationTest.class) + @Test + void testAppliesForOrCase1() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasId(1) or relation.hasId(2) + + assertNotApplicableForReorderingOptimizationOnly(select1) + } + + @Test + void testAppliesForOrCase2() { + def atlas = usingAlcatraz() + + def select1 = select relation._ from atlas.relation where relation.hasId(1) or relation.hasId(2) and relation.hasTag(aaa: "bbb") + + assertNotApplicableForReorderingOptimizationOnly(select1) + } + + @Test + void testAppliesForCase3() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasTag(aaa1: "bbb") and relation.hasTag(aaa: "bbb") + + assertNotApplicableForBoth(select1) + } + + @Test + void testAppliesCase4() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.isWithin([ + [ + [-63.171752227, 18.1418553393], [-62.9506523734, 18.1418553393], [-62.9506523734, 18.2749168824], [-63.171752227, 18.2749168824], [-63.171752227, 18.1418553393] + ] + ]) and relation.isWithin([ + [ + [-63.171752227, 18.1418553393], [-62.9506523734, 18.1418553393], [-62.9506523734, 18.2749168824], [-63.171752227, 18.2749168824], [-63.171752227, 18.1418553393] + ] + ]) + + assertNotApplicableForReorderingOptimizationOnly(select1) + } + + @Test + void testAppliesCase5() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasId(1) and relation.isWithin([ + [ + [-63.171752227, 18.1418553393], [-62.9506523734, 18.1418553393], [-62.9506523734, 18.2749168824], [-63.171752227, 18.2749168824], [-63.171752227, 18.1418553393] + ] + ]) + + assertNotApplicableForReorderingOptimizationOnly(select1) + } + + @Test + void testAppliesCase6() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.isWithin([ + [ + [-63.171752227, 18.1418553393], [-62.9506523734, 18.1418553393], [-62.9506523734, 18.2749168824], [-63.171752227, 18.2749168824], [-63.171752227, 18.1418553393] + ] + ]) and relation.hasId(1) + + assertApplicable(select1) + } + + @Test + void testAppliesCase7() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.isWithin([ + [ + [-63.171752227, 18.1418553393], [-62.9506523734, 18.1418553393], [-62.9506523734, 18.2749168824], [-63.171752227, 18.2749168824], [-63.171752227, 18.1418553393] + ] + ]) and relation.hasId(1) and relation.hasTag(aaa1: "bbb") + + assertApplicable(select1) + } + + @Test + void testAppliesCase8() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasId(1) or relation.hasId(2) and relation.hasTag(aaa: "bbb") + + assertNotApplicableForReorderingOptimizationOnly(select1) + } + + @Test + void testAppliesCase9() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where relation.hasId(1) and not(relation.hasId(1)) + + assertNotApplicableForBoth(select1) + } + + @Test + void testAppliesCase10() { + def atlas = usingAlcatraz() + def select1 = select relation._ from atlas.relation where not(relation.hasId(1)) and relation.hasId(1) + + assertApplicable(select1) + } + + @Test + void testOptimization() { + def atlas = usingAlcatraz() + + def polygon = [ + TestConstants.Polygons.northernPartOfAlcatraz + ] + + def ids = [1641119524000000, 307457176000000, 3202364308000000, 307446836000000, 1417681468000000] as Long[] + + final Constraint constraint1 = node.hasIds(ids) + final Constraint constraint2 = node.isWithin(polygon) + final Constraint constraint3 = node.hasLastUserNameLike(/.+/) //regex for at least one character + + final List> preferredOrder = [constraint1, constraint2, constraint3] + + //Order is important here + final Iterable>> iterable = { + new PermutationGenerator<>(preferredOrder) + } + + final int sum = StreamSupport.stream(iterable.spliterator(), false) + .filter { permutation -> + def passed = permutation != preferredOrder && permutation.get(0) != constraint1 + + passed + } + .mapToInt { permutation -> + log.info("constraint1:: $constraint1") + log.info("constraint2:: $constraint2") + log.info("constraint3:: $constraint3") + + final QueryBuilder optimalQueryBuilder = select(node._).from(atlas.node).where(constraint1).and(constraint2).and(constraint3) + final Result result1 = exec optimalQueryBuilder + assert result1.relevantIdentifiers as Set == ids as Set + + final QueryBuilder potentiallySubOptimalQueryBuilder = select(node._).from(atlas.node).where(permutation[0]).and(permutation[1]).and(permutation[2]) + final Result result2 = exec potentiallySubOptimalQueryBuilder + assert result2.relevantIdentifiers as Set == ids as Set + + def optimalQuery = optimalQueryBuilder.buildQuery() + def potentiallySubOptimalQuery = potentiallySubOptimalQueryBuilder.buildQuery() + + assertApplicable(potentiallySubOptimalQueryBuilder) + + final OptimizationRequest optimizationRequest = QueryAnalyzerImpl.from(ExplainerImpl.instance.explain(potentiallySubOptimalQuery)) + + def queryOptimizedWithReordering = ReorderingOptimization.instance.applyTransformation(optimizationRequest) + assert optimalQuery.conditionalConstructList == queryOptimizedWithReordering.conditionalConstructList + assert optimalQuery == queryOptimizedWithReordering + + final Result result3 = queryOptimizedWithReordering.execute() + assert result3.relevantIdentifiers as Set == ids as Set + + 1 + } + .sum() + + assert sum == CombinatoricsUtils.factorial(preferredOrder.size()) - CombinatoricsUtils.factorial(2) + } + + protected void assertNotApplicableForReorderingOptimizationOnly(select1) { + assert isApplicableAtBaseLevel(select1) + + assert isApplicableAtBaseLevel(select1) + } + + protected void assertNotApplicableForBoth(select1) { + super.assertNotApplicable(select1) + + assert !isApplicableAtBaseLevel(select1) + } + + @Override + QueryOptimizationTransformer associatedOptimization() { + ReorderingOptimization.instance + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/sanity/DataSanity.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/sanity/DataSanity.groovy new file mode 100644 index 0000000000..7dd2b5f620 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/sanity/DataSanity.groovy @@ -0,0 +1,119 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.sanity + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.stream.Collectors + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getExec +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getArea +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getEdge +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getLine +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getNode +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getPoint +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getRelation + +/** + * @author Yazad Khambata + */ +class DataSanity extends AbstractAQLTest { + private static final Logger log = LoggerFactory.getLogger(DataSanity.class); + + @Test + void testSelect1() { + def atlas = usingAlcatraz() + + def select1 + + select1 = select node._ from atlas.node limit 5 + exec select1 + + select1 = select point._ from atlas.point limit 5 + exec select1 + + select1 = select edge._ from atlas.edge limit 5 + exec select1 + + select1 = select line._ from atlas.line limit 5 + exec select1 + + select1 = select relation._ from atlas.relation limit 5 + exec select1 + + select1 = select area._ from atlas.area limit 5 + exec select1 + } + + @Test + void testSelect2() { + def atlas = usingButterflyPark() + + def select1 + + select1 = select node.tags from atlas.node limit 5 + exec select1 + + select1 = select point._ from atlas.point limit 5 + exec select1 + + select1 = select edge._ from atlas.edge limit 5 + exec select1 + + select1 = select line._ from atlas.line limit 5 + exec select1 + + select1 = select relation._ from atlas.relation limit 5 + exec select1 + + select1 = select area._ from atlas.area limit 5 + exec select1 + } + + @Test + void testWithin() { + final AtlasSchema atlas = usingAlcatraz() + + def select1 + + def polygonsNorth = [TestConstants.Polygons.northernPartOfAlcatraz] + def polygonsSouth = [TestConstants.Polygons.southernPartOfAlcatraz] + + [node: node, point: point, edge: edge, line: line, relation: relation, area: area].entrySet().stream().forEach { entry -> + def tableName = entry.getKey() + log.info("${tableName}") + + def table = entry.getValue() + + def list = [north: polygonsNorth, south: polygonsSouth].entrySet().stream().map { polygonEntry -> + def direction = polygonEntry.getKey() + log.info("${tableName}-${direction}") + + def polygon = polygonEntry.getValue() + + select1 = select table._ from atlas[tableName] where table.isWithin(polygon) + final Result result = exec select1 + result.relevantIdentifiers + }.collect(Collectors.toList()) + + assert list.size() == 2 + + final List northIds = list.get(0) + final List southIds = list.get(1) + + log.info("${tableName} northIds:: ${northIds}.") + log.info("${tableName} southIds:: ${southIds}.") + + assert !(northIds.size() == 0 ^ southIds.size() == 0) + assert (northIds as Set).intersect(southIds as Set).size() == 0 + + log.info("---") + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasMediatorTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasMediatorTest.groovy new file mode 100644 index 0000000000..7ceca77a53 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasMediatorTest.groovy @@ -0,0 +1,22 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest + +/** + * @author Yazad Khambata + */ +class AtlasMediatorTest extends AbstractAQLTest { + + @Test + void testEqualsAndHashCode() { + final AtlasSchema schema1 = usingAlcatraz() + final AtlasSchema schema2 = usingAlcatraz() + + assert schema1 == schema2 + assert schema1.hashCode() == schema2.hashCode() + + assert schema1.atlasMediator == schema2.atlasMediator + assert schema1.atlasMediator.hashCode() == schema2.atlasMediator.hashCode() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasSchemaTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasSchemaTest.groovy new file mode 100644 index 0000000000..a99bde3280 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/AtlasSchemaTest.groovy @@ -0,0 +1,76 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema + +import org.apache.commons.io.IOUtils +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl.ClasspathScheme +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl.FileScheme +import org.openstreetmap.atlas.geography.atlas.dsl.util.StreamUtil +import org.openstreetmap.atlas.geography.atlas.items.Node + +/** + * @author Yazad Khambata + */ +class AtlasSchemaTest extends AbstractAQLTest { + + @Test + void testOsmFileLoadFromClasspath() { + def classpath = "/data/Alcatraz/Alcatraz.osm" + def atlas = ClasspathScheme.instance.loadOsmXml(classpath) + assert atlas.numberOfNodes() == 56 + } + + @Test + void testOsmFileLoadFromLocalFileSystem() { + def fileName1 = "Alcatraz.osm" + + def baseClasspath = "/data/Alcatraz" + + def physicalPath = "/tmp/${UUID.randomUUID().toString()}" + + new File(physicalPath).mkdir() + + [fileName1].stream().forEach { fileName -> + final String classpath = "${baseClasspath}/${fileName}" + + final InputStream inputStream = this.getClass().getResourceAsStream(classpath) + + final OutputStream outputStream = new FileOutputStream(new File("${physicalPath}/${fileName}")) + IOUtils.copy(inputStream, outputStream) + + outputStream.close() + } + + final Atlas atlas = FileScheme.instance.loadOsmXml(physicalPath) + assert atlas.numberOfNodes() == 56 + } + + @Test + void testAtlasFileLoad() { + final AtlasSchema atlas1 = usingAlcatraz() + final Iterable all = atlas1.node.getAll() + + assert StreamUtil.stream(all).count() == 56 + } + + + @Test + void testEqualsAndHashCode() { + final AtlasSchema atlas1 = usingAlcatraz() + final AtlasSchema atlas2 = usingAlcatraz() + final AtlasSchema atlas3 = usingButterflyPark() + final AtlasSchema atlas4 = usingButterflyPark() + + assert atlas1 == atlas1 + assert atlas1 == atlas2 + assert atlas1.hashCode() == atlas2.hashCode() + + assert atlas3 == atlas3 + assert atlas3 == atlas4 + assert atlas3.hashCode() == atlas4.hashCode() + + assert atlas1 != atlas3 + assert atlas1.hashCode() != atlas3.hashCode() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/TableSettingTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/TableSettingTest.groovy new file mode 100644 index 0000000000..2bd8592bb7 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/TableSettingTest.groovy @@ -0,0 +1,18 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting + +/** + * @author Yazad Khambata + */ +class TableSettingTest { + + @Test + void sanity() { + Arrays.stream(TableSetting.values()).forEach { tableSetting -> + assert tableSetting.itemType.name() == tableSetting.name() + assert tableSetting.completeItemType.name() == tableSetting.name() + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/TextAtlasTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/TextAtlasTest.groovy new file mode 100644 index 0000000000..1ef421daea --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/TextAtlasTest.groovy @@ -0,0 +1,59 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema + +import org.junit.Assert +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.builder.text.TextAtlasBuilder +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl.ClasspathScheme +import org.openstreetmap.atlas.streaming.resource.File +import org.openstreetmap.atlas.streaming.resource.OutputStreamWritableResource +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * @author Yazad Khambata + */ +class TextAtlasTest { + private static final Logger log = LoggerFactory.getLogger(TextAtlasTest.class) + + @Test + void test1() { + def map = [ + ALCATRAZ : TestConstants.ALCATRAZ_CLASSPATH, + BUTTERFLY: TestConstants.BUTTERFLY_PARK_CLASSPATH + ] + + map.forEach({ key, value -> + final Atlas atlas = ClasspathScheme.instance.loadOsmXml(value) + + final String textAtlasPath = "/tmp/${key}-${UUID.randomUUID()}.atlas.txt" + + atlas.saveAsText(new OutputStreamWritableResource(new FileOutputStream(new java.io.File(textAtlasPath)))) + + final Atlas textAtlas = new TextAtlasBuilder().read(new File(new java.io.File(textAtlasPath))) + + Assert.assertEquals(atlas, textAtlas) + }) + } + + @Test + void test2() { + def map = [ + BUTTERFLY: TestConstants.BUTTERFLY_PARK_CLASSPATH, + ALCATRAZ : TestConstants.ALCATRAZ_CLASSPATH, + ] + + map.forEach({ key, value -> + final Atlas atlas = ClasspathScheme.instance.loadOsmXml(value) + + println "${key} : ${atlas.size()}" + + final String textAtlasPath = "/tmp/${key}-${System.nanoTime()}.atlas.txt" + atlas.saveAsText(new OutputStreamWritableResource(new FileOutputStream(new java.io.File(textAtlasPath)))) + final Atlas textAtlas = new TextAtlasBuilder().read(new File(new java.io.File(textAtlasPath))) + + Assert.assertEquals(atlas, textAtlas) + }) + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/LocationItemTableTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/LocationItemTableTest.groovy new file mode 100644 index 0000000000..c5c6ca7648 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/table/LocationItemTableTest.groovy @@ -0,0 +1,29 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.table + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.query.result.Result +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.items.LocationItem + +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getExec +import static org.openstreetmap.atlas.geography.atlas.dsl.query.QueryBuilderFactory.getSelect + +/** + * @author Yazad Khambata + */ +class LocationItemTableTest extends AbstractAQLTest { + + @Test + void testLocationField() { + final AtlasSchema atlasSchema = usingAlcatraz() + + [atlasSchema.node, atlasSchema.point].forEach { LocationItemTable table -> + def select1 = select table.location from table limit 10 + + final Result locationItemResult = exec select1 + + assert locationItemResult.relevantIdentifiers.size() == 10 + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/SchemeSupportTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/SchemeSupportTest.groovy new file mode 100644 index 0000000000..069a7f16a3 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/SchemeSupportTest.groovy @@ -0,0 +1,37 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.uri + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl.ClasspathScheme +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl.FileScheme + +/** + * @author Yazad Khambata + */ +class SchemeSupportTest { + + @Test + void testToScheme() { + assert SchemeSupport.instance.toScheme("/tmp/data/atlas/XYZ") == SchemeSupport.DEFAULT + assert SchemeSupport.instance.toScheme("file:/tmp/data/atlas/XYZ") == "file" + assert SchemeSupport.instance.toScheme("classpath:/tmp/data/atlas/XYZ") == "classpath" + assert SchemeSupport.instance.toScheme("factory:com.acme.myfactory.AtlasFactory#loadMyAtlas()") == "factory" + } + + @Test + void testToSchemeHandler() { + assert SchemeSupport.instance.toSchemeHandler("/tmp/data/atlas/XYZ") == FileScheme.instance + assert SchemeSupport.instance.toSchemeHandler("file:/tmp/data/atlas/XYZ") == FileScheme.instance + assert SchemeSupport.instance.toSchemeHandler("classpath:/tmp/data/atlas/XYZ") == ClasspathScheme.instance + } + + @Test + void testLoadFile() { + final InputStream inputStream = SchemeSupport.instance.loadFile("classpath:/data/polygon/jsonl/samples.jsonl") + + assert inputStream != null + assert inputStream.available() > 0 + + def count = inputStream.readLines().stream().count() + assert count == 15 + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/ClasspathSchemeTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/ClasspathSchemeTest.groovy new file mode 100644 index 0000000000..7b55b4e84d --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/ClasspathSchemeTest.groovy @@ -0,0 +1,111 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl + +import org.apache.commons.io.IOUtils +import org.junit.Ignore +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.AtlasResourceLoader +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.SchemeSupport +import org.openstreetmap.atlas.geography.atlas.packed.PackedAtlas +import org.openstreetmap.atlas.streaming.resource.File +import org.openstreetmap.atlas.streaming.resource.OutputStreamWritableResource +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.lang.reflect.Method +import java.nio.charset.Charset + +/** + * @author Yazad Khambata + */ +class ClasspathSchemeTest { + private static final Logger log = LoggerFactory.getLogger(ClasspathSchemeTest.class) + + @Test + @Ignore + void testDynamicClasspath() { + def root = "/tmp/${UUID.randomUUID()}" + def fileRoot = new java.io.File(root) + + def cp = "abc/xyz/pqr" + def fileLoc = "${root}/${cp}" + def dir = new java.io.File(fileLoc) + dir.mkdirs() + + def f = new java.io.File("${fileLoc}/HelloWorld.txt") + def text = "The quick brown fox jumps over the lazy dog." + f << text + + log.info("${f}") + + final ClassLoader classLoader = ClassLoader.getSystemClassLoader() + final Class classloaderClass = classLoader.getClass() + final Method method = classloaderClass.getDeclaredMethod("addURL", [URL.class] as Class[]) + method.setAccessible(true) + method.invoke(classLoader, [fileRoot.toURI().toURL()] as Object[]) + + assert text == IOUtils.toString(this.class.getClassLoader().getResourceAsStream("abc/xyz/pqr/HelloWorld.txt"), Charset.defaultCharset()) + } + + @Test + @Ignore + void testClasspathResource() { + final Map atlasMapping = [ + "atlas1/something1.atlas": loadOsmXml("classpath:${TestConstants.ALCATRAZ_CLASSPATH}"), + "atlas1/something2.atlas": loadOsmXml("classpath:${TestConstants.BUTTERFLY_PARK_CLASSPATH}"), + "atlas2/Alcatraz.atlas" : loadOsmXml("classpath:${TestConstants.ALCATRAZ_CLASSPATH}"), + "atlas2/Butterfly.atlas" : loadOsmXml("classpath:${TestConstants.BUTTERFLY_PARK_CLASSPATH}") + ] + + final String classpathRoot = "/tmp/${UUID.randomUUID()}" + + def classpathRootAsFile = new java.io.File(classpathRoot) + classpathRootAsFile.mkdirs() + + new java.io.File("${classpathRoot}/atlas1").mkdirs() + new java.io.File("${classpathRoot}/atlas2").mkdirs() + + atlasMapping.entrySet().stream().forEach { entry -> + final String relativePath = entry.getKey() + final Atlas atlas = PackedAtlas.cloneFrom(entry.getValue()) + + final String path = "$classpathRoot/${relativePath}" + log.info(path) + atlas.save(new OutputStreamWritableResource(new FileOutputStream(new java.io.File(path)))) + + final Atlas reloadedAtlas = new AtlasResourceLoader().load(new File(new java.io.File(path))) + assert reloadedAtlas + } + + addToClasspath(classpathRootAsFile) + + final String uri = "classpath:/atlas1/something1.atlas,something2.atlas;/atlas2/Alcatraz.atlas,Butterfly.atlas" + final Atlas atlas = loadAtlas(uri) + + println atlas + } + + private void addToClasspath(java.io.File classpathRootAsFile) { + final ClassLoader classLoader = ClassLoader.getSystemClassLoader() + final Class classLoaderClass = classLoader.getClass() + final Method method = classLoaderClass.getDeclaredMethod("addURL", [URL.class] as Class[]) + method.setAccessible(true) + method.invoke(classLoader, [classpathRootAsFile.toURI().toURL()] as Object[]) + } + + @Test + void testOsmXmlClasspathResource() { + final String uri = "classpath:/data/Alcatraz/Alcatraz.osm;/data/ButterflyPark/ButterflyPark.osm" + + assert loadOsmXml(uri).size().nodeNumber == 167 + } + + private Atlas loadAtlas(final String uri) { + SchemeSupport.instance.load(uri) + } + + private Atlas loadOsmXml(final String uri) { + SchemeSupport.instance.loadOsmXml(uri) + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/FileSchemeTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/FileSchemeTest.groovy new file mode 100644 index 0000000000..cb0fa1161c --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/schema/uri/impl/FileSchemeTest.groovy @@ -0,0 +1,71 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.impl + +import org.apache.commons.io.IOUtils +import org.junit.Ignore +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.Atlas +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.schema.uri.SchemeSupport +import org.openstreetmap.atlas.streaming.resource.File +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +/** + * @author Yazad Khambata + */ +class FileSchemeTest extends AbstractAQLTest { + private static final Logger log = LoggerFactory.getLogger(FileSchemeTest.class) + + private String saveInTmp(List fileNames, String baseClasspath) { + final String fileBasePath = "/tmp/${UUID.randomUUID()}" + + new File(fileBasePath).mkdirs() + + fileNames.stream() + .map { fileName -> "${baseClasspath}${fileName}" } + .map { classpath -> ClassLoader.getResourceAsStream(classpath) } + .forEach { InputStream inputStream -> + final String physicalFileName = "${fileBasePath}/${UUID.randomUUID()}.atlas" + OutputStream outputStream = new FileOutputStream(physicalFileName) + IOUtils.copy(inputStream, outputStream) + + outputStream.flush() + + outputStream.close() + } + + assert new java.io.File(fileBasePath).list().size() == fileNames.size() + fileBasePath + } + + @Test + void testLoad() { + final String basePath = "/tmp/${UUID.randomUUID()}" + new java.io.File(basePath).mkdirs() + + def atlas1 = usingButterflyPark().atlasMediator.atlas + def atlas2 = usingAlcatraz().atlasMediator.atlas + + atlas1.cloneToPackedAtlas().save(new File(new java.io.File("${basePath}/ButterflyPark.atlas"))) + atlas2.cloneToPackedAtlas().save(new File(new java.io.File("${basePath}/Alcatraz.atlas"))) + + final Atlas reloadedAtlas = SchemeSupport.instance.load("file:${basePath}") + + log.info("reloadedAtlas size:: ${reloadedAtlas.size()}") + + def number = reloadedAtlas.size().nodeNumber + assert number == 167 + } + + @Test + @Ignore + void testLoadOsmXml() { + final String baseClasspath = "/data/" + + final List fileNames = ["Alcatraz/Alcatraz.osm", "ButterflyPark/ButterflyPark.osm"] + + String fileBasePath = saveInTmp(fileNames, baseClasspath) + + assert SchemeSupport.instance.loadOsmXml("file:${fileBasePath}").size().nodeNumber == 167 + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/AbstractConstraintTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/AbstractConstraintTest.groovy new file mode 100644 index 0000000000..3d8ff7a961 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/AbstractConstraintTest.groovy @@ -0,0 +1,13 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection + +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.Constraint + +/** + * @author Yazad Khambata + */ +class AbstractConstraintTest { + protected void assertEquals(Constraint constraint1, Constraint constraint2) { + assert constraint1 == constraint2 + assert constraint1.hashCode() == constraint2.hashCode() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BasicConstraintTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BasicConstraintTest.groovy new file mode 100644 index 0000000000..becd295a17 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BasicConstraintTest.groovy @@ -0,0 +1,62 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.field.Constrainable +import org.openstreetmap.atlas.geography.atlas.dsl.field.Field +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.AtlasTable +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.dsl.selection.AbstractConstraintTest +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity + +/** + * @author Yazad Khambata + */ +class BasicConstraintTest extends AbstractConstraintTest { + @Test + void testEqualsAndHashCode() { + + final Map> supportedTables = AtlasDB.supportedTables + + supportedTables.entrySet().stream().forEach { tableEntry -> + + final TableSetting tableSetting = tableEntry.key + final AtlasTable atlasTable = tableEntry.value + + final Map allFields = atlasTable.allFields + + allFields.entrySet().stream().forEach { fieldEntry -> + final Field field = fieldEntry.value + if (!(field instanceof Constrainable)) { + return + } + + final BinaryOperation operation = BinaryOperations.eq + final Class memberClass = tableSetting.memberClass + final ScanType scanType = ScanType.ID_UNIQUE_INDEX + def valueToCheck = 1L + + final Constraint constraint1 = createBasicConstraint(field, operation, valueToCheck, scanType, memberClass) + final Constraint constraint2 = createBasicConstraint(field, operation, valueToCheck, scanType, memberClass) + + assertEquals(constraint1, constraint2) + + def copy1 = constraint1.deepCopy() + def copy2 = constraint2.deepCopy() + + assertEquals(copy1, copy2) + assertEquals(copy1, constraint2) + } + } + } + + private BasicConstraint createBasicConstraint(Field field, BinaryOperation operation, long valueToCheck, ScanType scanType, Class memberClass) { + BasicConstraint.builder() + .field(field) + .operation(operation) + .valueToCheck(valueToCheck) + .bestCandidateScanType(scanType) + .atlasEntityClass(memberClass) + .build() + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BinaryOperationsTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BinaryOperationsTest.groovy new file mode 100644 index 0000000000..9efd1fa062 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/BinaryOperationsTest.groovy @@ -0,0 +1,182 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import org.junit.Test +import org.openstreetmap.atlas.geography.Latitude +import org.openstreetmap.atlas.geography.Location +import org.openstreetmap.atlas.geography.Longitude +import org.openstreetmap.atlas.geography.atlas.complete.CompleteNode +import org.openstreetmap.atlas.geography.atlas.items.Node + +/** + * @author Yazad Khambata + */ +class BinaryOperationsTest { + + @Test + void testEq() { + final def l1 = 100L + + assert BinaryOperations.eq.perform(l1, l1, Node) == true + assert BinaryOperations.eq.perform(l1, l1 - 1, Node) == false + } + + @Test + void testNe() { + final def l1 = 100L + + assert BinaryOperations.ne.perform(l1, l1 - 1, Node) == true + assert BinaryOperations.ne.perform(l1, l1, Node) == false + } + + @Test + void testLt() { + final def l1 = 99L + + final Long l2 = 100L + + assert BinaryOperations.lt.perform(l1, l2, Node) == true + assert BinaryOperations.lt.perform(l2, l1, Node) == false + assert BinaryOperations.lt.perform(l1, l1, Node) == false + } + + @Test + void testLe() { + final def l1 = 99L + + final Long l2 = 100L + + assert BinaryOperations.le.perform(l1, l2, Node) == true + assert BinaryOperations.le.perform(l2, l1, Node) == false + assert BinaryOperations.le.perform(l1, l1, Node) == true + } + + @Test + void testGt() { + final def l1 = 100L + + final Long l2 = 99L + + assert BinaryOperations.gt.perform(l1, l2, Node) == true + assert BinaryOperations.gt.perform(l2, l1, Node) == false + assert BinaryOperations.gt.perform(l1, l1, Node) == false + } + + @Test + void testGe() { + final def l1 = 100L + + final Long l2 = 99L + + assert BinaryOperations.ge.perform(l1, l2, Node) == true + assert BinaryOperations.ge.perform(l2, l1, Node) == false + assert BinaryOperations.ge.perform(l1, l1, Node) == true + } + + @Test + void testIn() { + final Long l = 10L + final def lst1 = [20L, 10L, 30L] + final def lst2 = [20L, 10L, 30L] - l + + assert BinaryOperations.inside.perform(l, lst1, Node) == true + assert BinaryOperations.inside.perform(l, lst2, Node) == false + } + + @Test + void testWithinForLocationItem() { + final List>> geometricSurfaceLocations = [ + [ + [103.7817055, 1.249780], + [103.8952434, 1.249780], + [103.8952434, 1.404801], + [103.7817055, 1.404801], + [103.7817055, 1.249780] + ] + ] + + final BigDecimal longitude = 1.3272885 + final BigDecimal latitude = 103.8384742 + final Location goodDot = toDot(longitude, latitude) + + final Location badDot = toDot(longitude - 0.2, latitude - 0.2) + + assert BinaryOperations.within.perform(toNode(goodDot), geometricSurfaceLocations, Node) + assert BinaryOperations.within.perform(toNode(badDot), geometricSurfaceLocations, Node) == false + } + + private CompleteNode toNode(Location goodDot) { + new CompleteNode(1, goodDot, new HashMap<>(), null, null, null) + } + + private Location toDot(BigDecimal longitude, BigDecimal latitude) { + new Location(Latitude.degrees(longitude), Longitude.degrees(latitude)) + } + + @Test + void testTag() { + final Map actualTags = [name: "San Jose", state: "California", country: "United States of America"] + final Map actualTagsStateWithQuotes = [name: "San Jose", "state": "California", country: "United States of America"] + + final String keyToCheck1 = "state" + final String keyToCheck2 = "continent" + + assert BinaryOperations.tag.perform(actualTags, keyToCheck1, Node) + assert BinaryOperations.tag.perform(actualTags, keyToCheck2, Node) == false + + assert BinaryOperations.tag.perform(actualTagsStateWithQuotes, keyToCheck1, Node) + + def keyValuePairToCheck1 = [state: "California"] + def keyValuePairToCheck2 = [state: "Washington"] + + assert BinaryOperations.tag.perform(actualTags, keyValuePairToCheck1, Node) + assert BinaryOperations.tag.perform(actualTags, keyValuePairToCheck2, Node) == false + } + + @Test + void testTagValuesTest() { + final Map actualTags1 = [name: "San Jose", state: "California", country: "United States of America"] + final Map actualTags2 = [name: "Seattle", state: "Washington", country: "United States of America"] + + def keyValuePairToCheck1 = [state: "California"] + def keyValuePairToCheck2 = [state: ["California", "Washington"]] + + assert BinaryOperations.tag.perform(actualTags1, keyValuePairToCheck1, Node) + assert BinaryOperations.tag.perform(actualTags2, keyValuePairToCheck1, Node) == false + + assert BinaryOperations.tag.perform(actualTags1, keyValuePairToCheck2, Node) + assert BinaryOperations.tag.perform(actualTags2, keyValuePairToCheck2, Node) + } + + @Test + void testTagLike() { + final Map actualTags = [name: "San Jose", state: "California", country: "United States of America", "name:ko": "산호세"] + + assert BinaryOperations.tag_like.perform(actualTags, [name: /San.*/], Node) + assert BinaryOperations.tag_like.perform(actualTags, [name: /.*BlahBlahBlah.*/], Node) == false + assert BinaryOperations.tag_like.perform(actualTags, /.*tat.*/, Node) + assert BinaryOperations.tag_like.perform(actualTags, /.*BlahBlahBlah.*/, Node) == false + + assert BinaryOperations.tag_like.perform(actualTags, /name:.{2,3}/, Node) + } + + @Test + void testLike() { + String name = "San Jose" + + assert BinaryOperations.like.perform(name, /San.*/, Node) + assert BinaryOperations.like.perform(name, /.*Jose/, Node) + assert BinaryOperations.like.perform(name, /.*a.*/, Node) + assert BinaryOperations.like.perform(name, /.*blah.*/, Node) == false + } + + @Test + void test() { + def list = [0, 1, 2] + assert 1 in list + boolean all = [2, 0, 1].stream().map { it in list }.reduce({ x, y -> x && y }).orElse(false) + boolean any = [1].stream().map { it in list }.reduce({ x, y -> x || y }).orElse(false) + + assert all + assert any + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/NotConstraintTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/NotConstraintTest.groovy new file mode 100644 index 0000000000..fc6832aed3 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/NotConstraintTest.groovy @@ -0,0 +1,46 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints + +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.selection.AbstractConstraintTest +import org.openstreetmap.atlas.geography.atlas.items.Node + +import static org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasDB.getNode + +/** + * @author Yazad Khambata + */ +class NotConstraintTest extends AbstractConstraintTest { + @Test + void testEqualsAndHashCode() { + final Constraint constraint1 = BasicConstraint.builder() + .field(node.identifier) + .operation(BinaryOperations.eq) + .valueToCheck(1L) + .bestCandidateScanType(ScanType.ID_UNIQUE_INDEX) + .atlasEntityClass(Node.class) + .build() + + final Constraint constraint2 = BasicConstraint.builder() + .field(node.identifier) + .operation(BinaryOperations.eq) + .valueToCheck(1L) + .bestCandidateScanType(ScanType.ID_UNIQUE_INDEX) + .atlasEntityClass(Node.class) + .build() + + assertEquals(constraint1, constraint2) + + + final Constraint notConstraint1 = new NotConstraint(constraint1) + final Constraint notConstraint2 = new NotConstraint(constraint2) + + assertEquals(notConstraint1, notConstraint2) + def copy1 = notConstraint1.deepCopy() + def copy2 = notConstraint2.deepCopy() + assertEquals(copy1, copy2) + assertEquals(notConstraint1, copy2) + + assert notConstraint1 != constraint1 + assert notConstraint2 != constraint2 + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/polygon/GeometricSurfaceSupportTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/polygon/GeometricSurfaceSupportTest.groovy new file mode 100644 index 0000000000..4d2fe2a1d3 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/polygon/GeometricSurfaceSupportTest.groovy @@ -0,0 +1,136 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.polygon + +import org.junit.Test +import org.openstreetmap.atlas.geography.GeometricSurface +import org.openstreetmap.atlas.geography.MultiPolygon +import org.openstreetmap.atlas.geography.Polygon +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.util.StreamUtil + +import static org.openstreetmap.atlas.geography.atlas.dsl.TestConstants.Polygons.* + +/** + * @author Yazad Khambata + */ +class GeometricSurfaceSupportTest extends AbstractAQLTest { + + @Test + void testMultiPolygon1() { + + + final List>> listOfLocations = [ + goldenGateParkSanFransiscoCalifornia, + guadalupeRiverParkAndGardensSanJoseCalifornia + ] + + final GeometricSurface geometricSurface = GeometricSurfaceSupport.instance.toGeometricSurface(listOfLocations) + + assert listOfLocations.size() == 2 + assert ((MultiPolygon) geometricSurface).size() == listOfLocations.size() + } + + @Test + void testMultiPolygon2() { + final List>> listOfLocations = [ + goldenGateParkSanFransiscoCalifornia, + guadalupeRiverParkAndGardensSanJoseCalifornia, + northernPartOfAlcatraz + ] + + final GeometricSurface geometricSurface = GeometricSurfaceSupport.instance.toGeometricSurface(listOfLocations) + + assert listOfLocations.size() == 3 + assert ((MultiPolygon) geometricSurface).size() == listOfLocations.size() + } + + @Test + void testMultiPolygon3() { + final List>> listOfLocations = [ + goldenGateParkSanFransiscoCalifornia, + guadalupeRiverParkAndGardensSanJoseCalifornia + ] + + final GeometricSurface geometricSurface = GeometricSurfaceSupport.instance.toGeometricSurface(listOfLocations) + + assert listOfLocations.size() == 2 + assert ((MultiPolygon) geometricSurface).size() == listOfLocations.size() + + final List> californiaAndNeighbouringAreaAsListOfLocations = [ + [-125.39794921875, 42.147114459220994], [-124.69482421875, 40.22082997283287], [-121.11328124999999, 34.27083595165], [-118.98193359375, 32.20350534542368], [-114.3896484375, 32.46342595776104], [-112.84057617187499, 33.660353121928814], [-119.36645507812499, 39.0533181067413], [-119.20166015625, 42.147114459220994], [-125.39794921875, 42.147114459220994] + ] + + final Polygon californiaAndNeighbouringArea = GeometricSurfaceSupport.instance.toPolygon(californiaAndNeighbouringAreaAsListOfLocations) + + for (Polygon polygon : (MultiPolygon) geometricSurface) { + final boolean result = californiaAndNeighbouringArea.overlaps(polygon) + + assert result + } + } + + @Test + void testToGeometricSurface1() { + final List>> list = [ + [ + [10.1, 20.2], + [20.2, 30.3], + [10.1, 20.2], + ], + + [ + [50.5, 60.6], + [70.7, 71.71], + [50.5, 60.6], + ], + + [ + [80.8, 85.85], + [87.87, 89.89], + [80.8, 85.85], + ] + ] + + final GeometricSurface geometricSurface = GeometricSurfaceSupport.instance.toGeometricSurface(list) + + assert geometricSurface != null + assert geometricSurface instanceof MultiPolygon + assert ((MultiPolygon) geometricSurface).size() == 3 + } + + @Test + void testToGeometricSurface2() { + final List>> listOfLocations = [ + southernPartOfAlcatraz, + goldenGateParkSanFransiscoCalifornia, + provincetownMassachusetts, + guadalupeRiverParkAndGardensSanJoseCalifornia, + northernPartOfAlcatraz + ] + + final List> californiaAndNeighbouringAreaAsListOfLocations = [ + [-125.39794921875, 42.147114459220994], [-124.69482421875, 40.22082997283287], [-121.11328124999999, 34.27083595165], [-118.98193359375, 32.20350534542368], [-114.3896484375, 32.46342595776104], [-112.84057617187499, 33.660353121928814], [-119.36645507812499, 39.0533181067413], [-119.20166015625, 42.147114459220994], [-125.39794921875, 42.147114459220994] + ] + + final GeometricSurface californiaAndNeighbouringArea = GeometricSurfaceSupport.instance.toPolygon(californiaAndNeighbouringAreaAsListOfLocations) + + assert ((MultiPolygon) GeometricSurfaceSupport.instance.toGeometricSurface(listOfLocations)).size() == listOfLocations.size() + + final GeometricSurface filteredGeometricSurface = GeometricSurfaceSupport.instance.toGeometricSurface(listOfLocations, californiaAndNeighbouringArea).orElseThrow { + new IllegalStateException() + } + + assert ((MultiPolygon) filteredGeometricSurface).size() == 4 + } + + @Test + void testJsonlFileLoad() { + final GeometricSurface geometricSurface = GeometricSurfaceSupport.instance.fromJsonlFile("classpath:/data/polygon/jsonl/samples.jsonl") + + assert geometricSurface != null + + final MultiPolygon multiPolygon = (MultiPolygon) geometricSurface + + //seems like the MultiPolygon collapses some polygons. + assert StreamUtil.stream((Iterable) multiPolygon).count() == 13 + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/polygon/PolygonSupportTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/polygon/PolygonSupportTest.groovy new file mode 100644 index 0000000000..19729f3f64 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/polygon/PolygonSupportTest.groovy @@ -0,0 +1,40 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.polygon + + +import org.junit.Test +import org.openstreetmap.atlas.geography.* +import org.openstreetmap.atlas.geography.atlas.complete.CompleteNode +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.BinaryOperations +import org.openstreetmap.atlas.geography.atlas.items.Node + +/** + * @author Yazad Khambata + */ +class PolygonSupportTest { + + @Test + void testPolygonConversion() { + final List>> poly = [ + [ + [103.7817053, 1.249778], + [103.8952432, 1.249778], + [103.8952432, 1.404799], + [103.7817053, 1.404799], + [103.7817053, 1.249778] + ] + ] + + final List> locs = poly.get(0) + final Polygon polygonOfLocations = new Polygon( + locs.stream().map { longLatPair -> + final BigDecimal longitude = longLatPair.get(0) + final BigDecimal latitude = longLatPair.get(1) + + return new Location(Latitude.degrees(latitude), Longitude.degrees(longitude)) + }.toArray { new Location[locs.size()] } + ) + final Location center = polygonOfLocations.center() + final Node node = new CompleteNode(1, center, new HashMap<>(), null, null, null) + assert BinaryOperations.within.perform(node, poly, Node) + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/regex/RegexSupportTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/regex/RegexSupportTest.groovy new file mode 100644 index 0000000000..e904ef9190 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/constraints/regex/RegexSupportTest.groovy @@ -0,0 +1,21 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.regex + +import org.junit.Test + +/** + * @author Yazad Khambata + */ +class RegexSupportTest { + + @Test + void testMatchesOneWord() { + assert RegexSupport.instance.matches("facebook", /face.*/) + assert RegexSupport.instance.matches("facebook", /.*book/) + assert RegexSupport.instance.matches("facebook", /.*ace.*/) + } + + @Test + void testMatchesSentences() { + assert RegexSupport.instance.matches("The Course at Cuisinart Golf Resort & Spa", /Resort/) + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/id/IdIndexScannerTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/id/IdIndexScannerTest.groovy new file mode 100644 index 0000000000..70db8f29b3 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/id/IdIndexScannerTest.groovy @@ -0,0 +1,126 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.id + +import org.apache.commons.lang3.time.StopWatch +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.BaseTable +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.NodeTable +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.items.AtlasEntity +import org.openstreetmap.atlas.geography.atlas.items.ItemType +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.concurrent.TimeUnit +import java.util.function.Predicate + +/** + * @author Yazad Khambata + */ +class IdIndexScannerTest extends AbstractAQLTest { + private static final Logger log = LoggerFactory.getLogger(IdIndexScannerTest.class); + + @Test + void testFetchSingle() { + final AtlasSchema anguilaSchema = usingAlcatraz() + + final NodeTable nodeTable = anguilaSchema.node + + assert IdIndexScanner.instance.fetch(nodeTable, 307459622000000).count { true } == 1 + } + + @Test + void testFetchMultiple() { + final AtlasSchema anguilaSchema = usingAlcatraz() + + final NodeTable nodeTable = anguilaSchema.node + + assert IdIndexScanner.instance.fetch(nodeTable, 307459622000000, 307446836000000, 307351652000000).count { + true + } == 3 + } + + @Test + void testFetchMultipleList() { + final AtlasSchema anguilaSchema = usingAlcatraz() + + final NodeTable nodeTable = anguilaSchema.node + + assert IdIndexScanner.instance.fetch(nodeTable, [307459622000000, 307446836000000, 307351652000000]).count { + true + } == 3 + } + + @Test + void testFetchSingleEmpty() { + final AtlasSchema anguilaSchema = usingAlcatraz() + + final NodeTable nodeTable = anguilaSchema.node + + IdIndexScanner.instance.fetch(nodeTable, -1).forEach { assert false } + } + + @Test + void testFetchMultipleEmpty() { + final AtlasSchema anguilaSchema = usingAlcatraz() + + final NodeTable nodeTable = anguilaSchema.node + + IdIndexScanner.instance.fetch(nodeTable, -1, -2, -3).forEach { assert false } + } + + @Test + void testFetchMultipleListEmpty() { + final AtlasSchema anguilaSchema = usingAlcatraz() + + final NodeTable nodeTable = anguilaSchema.node + + IdIndexScanner.instance.fetch(nodeTable, [-1, -2, -3]).forEach { node -> log.info "${node}" } + } + + @Test + void testFetchMultipleAll() { + final AtlasSchema anguilaSchema = usingAlcatraz() + + final Map> idsByEntity = [ + (ItemType.NODE) : [307459622000000, 307446836000000], + (ItemType.POINT) : [4553243887000000, 3054493437000000], + (ItemType.EDGE) : [27989500000000, 27996878000000], + (ItemType.LINE) : [99202295000000, 128245365000000], + (ItemType.AREA) : [24433389000000, 27999864000000], + (ItemType.RELATION): [9451753000000] //only one available. + ] + + (1..5).forEach { + log.info("ROUND-${it}") + Arrays.stream(TableSetting.values()).forEach { tableSetting -> + final BaseTable atlasTable = (BaseTable) anguilaSchema[tableSetting.tableName()] + + final ItemType itemType = tableSetting.itemType + final List ids = idsByEntity[itemType] + + assert ids.size() > 0 + + final Predicate predicate = atlasTable.hasIds(ids as Long[]).toPredicate(itemType.memberClass) + + final StopWatch stopWatchIdx = new StopWatch() + stopWatchIdx.start() + final Long countWithIndex = IdIndexScanner.instance.fetch(atlasTable, ids).count { true } + stopWatchIdx.stop() + + final StopWatch stopWatchFull = new StopWatch() + stopWatchFull.start() + final Long countWithFullScan = tableSetting.getAll(anguilaSchema.atlasMediator.atlas, predicate).count { + true + } + stopWatchFull.stop() + + assert countWithIndex == ids.size() + assert countWithIndex == countWithFullScan, "${itemType} count mismatched." + + log.info "Table: ${itemType} -> Records: ${countWithIndex}; ${stopWatchIdx.getTime(TimeUnit.MILLISECONDS)} millis (Index), ${stopWatchFull.getTime(TimeUnit.MILLISECONDS)} millis (Full)" + } + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/spatial/SpatialIndexScannerTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/spatial/SpatialIndexScannerTest.groovy new file mode 100644 index 0000000000..8a7cacdbb2 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/selection/indexing/scanning/spatial/SpatialIndexScannerTest.groovy @@ -0,0 +1,75 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.selection.indexing.scanning.spatial + +import org.apache.commons.lang3.time.StopWatch +import org.junit.Test +import org.openstreetmap.atlas.geography.atlas.dsl.AbstractAQLTest +import org.openstreetmap.atlas.geography.atlas.dsl.TestConstants +import org.openstreetmap.atlas.geography.atlas.dsl.schema.AtlasSchema +import org.openstreetmap.atlas.geography.atlas.dsl.schema.table.setting.TableSetting +import org.openstreetmap.atlas.geography.atlas.dsl.selection.constraints.polygon.GeometricSurfaceSupport +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.concurrent.TimeUnit + +/** + * @author Yazad Khambata + */ +class SpatialIndexScannerTest extends AbstractAQLTest { + private static final Logger log = LoggerFactory.getLogger(SpatialIndexScannerTest.class); + + def polygon = [ + TestConstants.Polygons.centralPartOfAlcatraz + ] + + @Test + void testFetch1() { + final AtlasSchema atlas = usingAlcatraz() + + assert SpatialIndexScanner.instance.fetch(atlas.node, polygon).count { true } == 48 + } + + @Test + void testFetch2() { + final AtlasSchema atlas = usingAlcatraz() + + assert SpatialIndexScanner.instance.fetch(atlas.edge, polygon).count { true } == 102 + } + + @Test + void testFetchEmpty() { + final AtlasSchema atlas = usingButterflyPark() //no overlap + + SpatialIndexScanner.instance.fetch(atlas.node, polygon).forEach { assert false } + } + + @Test + void testFetchAllTables() { + final AtlasSchema atlasSchema = usingButterflyPark() + + (1..5).forEach { i -> + log.info "ROUND-${i}" + + Arrays.stream(TableSetting.values()).forEach { tableSetting -> + final StopWatch stopWatch1 = new StopWatch() + + stopWatch1.start() + final Long countFetchedFromIndex = SpatialIndexScanner.instance.fetch(atlasSchema[tableSetting.tableName()], polygon).count { + true + } + stopWatch1.stop() + + final StopWatch stopWatch2 = new StopWatch() + stopWatch2.start() + final Long countFromFullScan = tableSetting.getAll(atlasSchema.atlasMediator.atlas, { entity -> entity.within(GeometricSurfaceSupport.instance.toGeometricSurface(polygon)) }).count { + true + } + stopWatch2.stop() + + assert countFetchedFromIndex == countFromFullScan, "${tableSetting} counts mismatched." + + log.info "Table: ${tableSetting} -> Fetched Records: ${countFetchedFromIndex}; in ${stopWatch1.getTime(TimeUnit.MILLISECONDS)} millis (optionalIndexInfo); ${stopWatch2.getTime(TimeUnit.MILLISECONDS)} millis (Full Scan)." + } + } + } +} diff --git a/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/GroovyEmptyTest.groovy b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/GroovyEmptyTest.groovy new file mode 100644 index 0000000000..a6e1fd66d0 --- /dev/null +++ b/src/test/groovy/org/openstreetmap/atlas/geography/atlas/dsl/util/GroovyEmptyTest.groovy @@ -0,0 +1,50 @@ +package org.openstreetmap.atlas.geography.atlas.dsl.util + +import org.apache.commons.lang3.Validate +import org.junit.Test + +/** + * @author Yazad Khambata + */ +class GroovyEmptyTest { + @Test + void testEmpty1() { + def var = null + verifyEmpty(var) + } + + @Test + void testEmpty2() { + def var = "" + verifyEmpty(var) + } + + @Test + void testEmpty3() { + def var = [] + verifyEmpty(var) + } + + @Test + void testEmpty4() { + def var = [:] + verifyEmpty(var) + } + + private void verifyEmpty(var) { + verifyEmptyWithAsserts(var) + verifyEmptyWithValidate(var) + } + + private void verifyEmptyWithValidate(var) { + Validate.isTrue(!var) + } + + private void verifyEmptyWithAsserts(var) { + if (var) { + assert false + } + + assert true + } +} diff --git a/src/test/resources/aql-files/test.aql b/src/test/resources/aql-files/test.aql new file mode 100644 index 0000000000..7c598ec5f9 --- /dev/null +++ b/src/test/resources/aql-files/test.aql @@ -0,0 +1 @@ +select node._ from atlas.node \ No newline at end of file diff --git a/src/test/resources/aql-files/test1/test.aql b/src/test/resources/aql-files/test1/test.aql new file mode 100644 index 0000000000..7c598ec5f9 --- /dev/null +++ b/src/test/resources/aql-files/test1/test.aql @@ -0,0 +1 @@ +select node._ from atlas.node \ No newline at end of file diff --git a/src/test/resources/aql-files/test2/test.aql b/src/test/resources/aql-files/test2/test.aql new file mode 100644 index 0000000000..7c598ec5f9 --- /dev/null +++ b/src/test/resources/aql-files/test2/test.aql @@ -0,0 +1 @@ +select node._ from atlas.node \ No newline at end of file diff --git a/src/test/resources/aql-files/test2/testA/test.aql b/src/test/resources/aql-files/test2/testA/test.aql new file mode 100644 index 0000000000..7c598ec5f9 --- /dev/null +++ b/src/test/resources/aql-files/test2/testA/test.aql @@ -0,0 +1 @@ +select node._ from atlas.node \ No newline at end of file diff --git a/src/test/resources/aql-files/test2/testA/test.aql.sig b/src/test/resources/aql-files/test2/testA/test.aql.sig new file mode 100644 index 0000000000..85df50785d --- /dev/null +++ b/src/test/resources/aql-files/test2/testA/test.aql.sig @@ -0,0 +1 @@ +abcd \ No newline at end of file diff --git a/src/test/resources/aql-files/test2/testB/test.aql b/src/test/resources/aql-files/test2/testB/test.aql new file mode 100644 index 0000000000..7c598ec5f9 --- /dev/null +++ b/src/test/resources/aql-files/test2/testB/test.aql @@ -0,0 +1 @@ +select node._ from atlas.node \ No newline at end of file diff --git a/src/test/resources/aql-files/test2/testB/test.aql.sig b/src/test/resources/aql-files/test2/testB/test.aql.sig new file mode 100644 index 0000000000..85df50785d --- /dev/null +++ b/src/test/resources/aql-files/test2/testB/test.aql.sig @@ -0,0 +1 @@ +abcd \ No newline at end of file diff --git a/src/test/resources/aql-files/test2/testB/test2.aql b/src/test/resources/aql-files/test2/testB/test2.aql new file mode 100644 index 0000000000..7c598ec5f9 --- /dev/null +++ b/src/test/resources/aql-files/test2/testB/test2.aql @@ -0,0 +1 @@ +select node._ from atlas.node \ No newline at end of file diff --git a/src/test/resources/aql-files/test2/testB/test2.aql.sig b/src/test/resources/aql-files/test2/testB/test2.aql.sig new file mode 100644 index 0000000000..85df50785d --- /dev/null +++ b/src/test/resources/aql-files/test2/testB/test2.aql.sig @@ -0,0 +1 @@ +abcd \ No newline at end of file diff --git a/src/test/resources/data/Alcatraz/Alcatraz.atlas.txt b/src/test/resources/data/Alcatraz/Alcatraz.atlas.txt new file mode 100644 index 0000000000..b37ae56944 --- /dev/null +++ b/src/test/resources/data/Alcatraz/Alcatraz.atlas.txt @@ -0,0 +1,261 @@ +# Nodes +307351652000000 && 37.8251293,-122.421207 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755472000 || last_edit_user_id -> 14293 || last_edit_version -> 5 +307429818000000 && 37.8257351,-122.4221372 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664594000 || last_edit_user_id -> 27824 || last_edit_version -> 5 +307459464000000 && 37.8275768,-122.4230619 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958432000 || last_edit_user_id -> 35195 || last_edit_version -> 8 +1417681452000000 && 37.8267031,-122.4210044 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048024 || last_edit_time -> 1546697600000 || last_edit_user_id -> 14293 || last_edit_version -> 2 +307446864000000 && 37.8273227,-122.4225384 && last_edit_user_name -> will l || last_edit_changeset -> 761842 || last_edit_time -> 1226936035000 || last_edit_user_id -> 35195 || last_edit_version -> 11 +3202364308000000 && 37.8270774,-122.4243445 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755473000 || last_edit_user_id -> 14293 || last_edit_version -> 2 +307459624000000 && 37.8276242,-122.4239505 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958476000 || last_edit_user_id -> 35195 || last_edit_version -> 3 +307446872000000 && 37.827285,-122.4227049 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664595000 || last_edit_user_id -> 27824 || last_edit_version -> 7 +307446838000000 && 37.8270347,-122.4244748 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755473000 || last_edit_user_id -> 14293 || last_edit_version -> 2 +5939835722000000 && 37.8263319,-122.4221365 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || last_edit_version -> 1 +1417681468000000 && 37.8280205,-122.4241771 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664589000 || last_edit_user_id -> 27824 || last_edit_version -> 1 +307446867000000 && 37.8271132,-122.42221 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224953895000 || last_edit_user_id -> 35195 || last_edit_version -> 1 || created_by -> JOSM +1526523417000000 && 37.8268181,-122.421603 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || last_edit_time -> 1322753281000 || last_edit_user_id -> 381909 || last_edit_version -> 1 +307447189000000 && 37.8268745,-122.4222271 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958438000 || last_edit_user_id -> 35195 || last_edit_version -> 2 +307447029000000 && 37.8272039,-122.4229806 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958623000 || last_edit_user_id -> 35195 || last_edit_version -> 7 +1417681433000000 && 37.8269443,-122.4217188 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664588000 || last_edit_user_id -> 27824 || last_edit_version -> 1 +307459622000000 && 37.8279428,-122.4243196 && last_edit_user_name -> Walter Schlögl || last_edit_changeset -> 6060751 || access -> private || barrier -> gate || last_edit_time -> 1287257739000 || last_edit_user_id -> 78656 || last_edit_version -> 5 +5939834263000000 && 37.826102,-122.4222069 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050340 || last_edit_time -> 1538261966000 || last_edit_user_id -> 9446 || last_edit_version -> 1 +265561249000000 && 37.8270274,-122.4214142 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048024 || last_edit_time -> 1546697600000 || last_edit_user_id -> 14293 || last_edit_version -> 12 +307447121000000 && 37.8268597,-122.4222351 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224956005000 || last_edit_user_id -> 35195 || last_edit_version -> 2 +307430033000000 && 37.8260568,-122.4221661 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050340 || last_edit_time -> 1538261966000 || last_edit_user_id -> 9446 || last_edit_version -> 6 +307459824000000 && 37.8279582,-122.4246659 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958420000 || last_edit_user_id -> 35195 || last_edit_version -> 3 +307430025000000 && 37.8265962,-122.4236215 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755472000 || last_edit_user_id -> 14293 || last_edit_version -> 9 +3202364307000000 && 37.8270479,-122.4242927 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755472000 || last_edit_user_id -> 14293 || last_edit_version -> 2 +5939835720000000 && 37.8263806,-122.4221839 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || last_edit_version -> 1 +307447150000000 && 37.8268464,-122.4222076 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224956005000 || last_edit_user_id -> 35195 || last_edit_version -> 2 +307459677000000 && 37.8279922,-122.4243622 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958420000 || last_edit_user_id -> 35195 || last_edit_version -> 6 +307446378000000 && 37.8255776,-122.4222696 && last_edit_user_name -> delphiN || last_edit_changeset -> 4749900 || last_edit_time -> 1274289694000 || last_edit_user_id -> 13192 || last_edit_version -> 6 +307430636000000 && 37.8265542,-122.4222529 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958456000 || last_edit_user_id -> 35195 || last_edit_version -> 8 +307456159000000 && 37.8267903,-122.4225171 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958456000 || last_edit_user_id -> 35195 || last_edit_version -> 5 +307446868000000 && 37.8269888,-122.422082 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958426000 || last_edit_user_id -> 35195 || last_edit_version -> 3 +1526523486000000 && 37.8266704,-122.4208218 && last_edit_user_name -> eduaddad || name:pt -> Terminal de Balsa de Alcatraz || last_edit_changeset -> 73895096 || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || amenity -> ferry_terminal || ferry -> yes || name -> Alcatraz Ferry Terminal || public_transport -> stop_position || last_edit_version -> 7 || operator -> Alcatraz Cruises, LLC +307430019000000 && 37.8261152,-122.4234561 && last_edit_user_name -> RAW || last_edit_changeset -> 10745510 || last_edit_time -> 1329774929000 || last_edit_user_id -> 63807 || last_edit_version -> 7 +1417681435000000 && 37.8271206,-122.4239856 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048128 || barrier -> gate || last_edit_time -> 1546697857000 || last_edit_user_id -> 14293 || last_edit_version -> 2 +1641119524000000 && 37.8269543,-122.4241045 && last_edit_user_name -> RAW || last_edit_changeset -> 10745510 || last_edit_time -> 1329774926000 || last_edit_user_id -> 63807 || last_edit_version -> 1 +3202364309000000 && 37.8270878,-122.4243583 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048024 || last_edit_time -> 1546697600000 || last_edit_user_id -> 14293 || entrance -> yes || last_edit_version -> 2 +303176761000000 && 37.8271993,-122.4216941 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755473000 || last_edit_user_id -> 14293 || last_edit_version -> 4 +307459822000000 && 37.8278683,-122.4248946 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224957167000 || last_edit_user_id -> 35195 || last_edit_version -> 1 +307430035000000 && 37.8261526,-122.4219539 && last_edit_user_name -> Nautic || last_edit_changeset -> 17223234 || last_edit_time -> 1375682659000 || last_edit_user_id -> 449569 || last_edit_version -> 9 +4553243991000000 && 37.8258018,-122.4209095 && last_edit_user_name -> FTA || last_edit_changeset -> 44388316 || last_edit_time -> 1481686082000 || last_edit_user_id -> 652301 || last_edit_version -> 1 +307446170000000 && 37.8256105,-122.4207728 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755473000 || last_edit_user_id -> 14293 || last_edit_version -> 5 +307465040000000 && 37.8271677,-122.423325 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664596000 || last_edit_user_id -> 27824 || last_edit_version -> 2 +307429837000000 && 37.8263646,-122.421571 && last_edit_user_name -> delphiN || last_edit_changeset -> 4749900 || last_edit_time -> 1274289689000 || last_edit_user_id -> 13192 || last_edit_version -> 3 +307430638000000 && 37.8262987,-122.4221054 && last_edit_user_name -> Nautic || last_edit_changeset -> 17223234 || last_edit_time -> 1375682660000 || last_edit_user_id -> 449569 || last_edit_version -> 9 +307457176000000 && 37.8270154,-122.4240917 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048128 || last_edit_time -> 1546697857000 || last_edit_user_id -> 14293 || last_edit_version -> 4 +1526523425000000 && 37.8264021,-122.4208428 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || last_edit_version -> 3 +307453263000000 && 37.8261859,-122.4222843 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050340 || last_edit_time -> 1538261966000 || last_edit_user_id -> 9446 || last_edit_version -> 6 +3202359193000000 && 37.8070389,-122.4041405 && wheelchair -> yes || last_edit_changeset -> 69833614 || addr:state -> CA || amenity -> ferry_terminal || addr:postcode -> 94111 || operator -> Alcatraz Cruises, LLC || addr:city -> San Francisco || name:ca -> Ferry Alcatraz || last_edit_user_name -> gpesquero || toilets:wheelchair -> yes || addr:housenumber -> Pier 33 || last_edit_time -> 1556873987000 || last_edit_user_id -> 3584 || name -> Alcatraz Cruises, LLC || name:en -> Alcatraz Cruises, LLC || addr:street -> Alcatraz Landing || last_edit_version -> 7 || name:zh -> 恶魔岛渡轮 +2213794399000000 && 37.8271609,-122.4222329 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048024 || last_edit_time -> 1546697600000 || last_edit_user_id -> 14293 || last_edit_version -> 2 +307446836000000 && 37.827774,-122.4249791 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> private || barrier -> gate || last_edit_time -> 1546755472000 || last_edit_user_id -> 14293 || last_edit_version -> 3 +1417681458000000 && 37.8270905,-122.4219723 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664589000 || last_edit_user_id -> 27824 || last_edit_version -> 1 +1417681432000000 && 37.8266908,-122.4209852 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048024 || last_edit_time -> 1546697600000 || last_edit_user_id -> 14293 || last_edit_version -> 3 +307446865000000 && 37.8271629,-122.4222296 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048024 || last_edit_time -> 1546697601000 || last_edit_user_id -> 14293 || last_edit_version -> 5 +2027966328000000 && 37.8270681,-122.4222388 && last_edit_user_name -> achims311 || last_edit_changeset -> 13998200 || last_edit_time -> 1353674169000 || last_edit_user_id -> 52898 || last_edit_version -> 1 +1417681474000000 && 37.8270442,-122.4220131 && last_edit_user_name -> nithinkamath || last_edit_changeset -> 12663035 || last_edit_time -> 1344465800000 || last_edit_user_id -> 573196 || last_edit_version -> 2 +307446487000000 && 37.8277604,-122.4235646 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224958420000 || last_edit_user_id -> 35195 || last_edit_version -> 10 +# Edges +27989500000000 && 37.8070389,-122.4041405:37.8074686,-122.4039428:37.808076,-122.4035932:37.8084458,-122.4034591:37.8087629,-122.4034225:37.809084,-122.4034509:37.8094213,-122.4035282:37.8097059,-122.4036339:37.8101326,-122.4038737:37.8108196,-122.4043411:37.811474,-122.4048817:37.8121365,-122.4055158:37.8137484,-122.4073261:37.8246229,-122.417873:37.8256667,-122.4188075:37.8262371,-122.4195964:37.8265614,-122.4203732:37.826636,-122.4206135:37.8266723,-122.4207726:37.8266704,-122.4208218 && payment:clipper -> no || last_edit_changeset -> 73085718 || payment:prepaid_ticket -> yes || bicycle -> yes || fee -> yes || payment:cash -> no || operator -> Alcatraz Cruises, LLC || vehicle -> no || duration -> 00:15:00 || last_edit_user_name -> clay_c || route -> ferry || last_edit_time -> 1565127911000 || last_edit_user_id -> 119881 || name -> Alcatraz - Pier 33 Alcatraz Landing || last_edit_version -> 12 || foot -> yes +-27989500000000 && 37.8266704,-122.4208218:37.8266723,-122.4207726:37.826636,-122.4206135:37.8265614,-122.4203732:37.8262371,-122.4195964:37.8256667,-122.4188075:37.8246229,-122.417873:37.8137484,-122.4073261:37.8121365,-122.4055158:37.811474,-122.4048817:37.8108196,-122.4043411:37.8101326,-122.4038737:37.8097059,-122.4036339:37.8094213,-122.4035282:37.809084,-122.4034509:37.8087629,-122.4034225:37.8084458,-122.4034591:37.808076,-122.4035932:37.8074686,-122.4039428:37.8070389,-122.4041405 && payment:clipper -> no || last_edit_changeset -> 73085718 || payment:prepaid_ticket -> yes || bicycle -> yes || fee -> yes || payment:cash -> no || operator -> Alcatraz Cruises, LLC || vehicle -> no || duration -> 00:15:00 || last_edit_user_name -> clay_c || route -> ferry || last_edit_time -> 1565127911000 || last_edit_user_id -> 119881 || name -> Alcatraz - Pier 33 Alcatraz Landing || last_edit_version -> 12 || foot -> yes +27996878000000 && 37.8255776,-122.4222696:37.8256159,-122.4223636:37.8257081,-122.4225136:37.8257651,-122.4226203:37.8258245,-122.4227291:37.8259294,-122.4227869:37.8259939,-122.4229635:37.8259722,-122.4230662:37.82597,-122.4231203:37.8259974,-122.4231939:37.8260251,-122.4232676:37.8260526,-122.4233438:37.8260709,-122.4233933:37.8260901,-122.4234293:37.8261152,-122.4234561 && last_edit_user_name -> RAW || last_edit_changeset -> 10745510 || access -> private || last_edit_time -> 1329774930000 || last_edit_user_id -> 63807 || highway -> footway || last_edit_version -> 5 +-27996878000000 && 37.8261152,-122.4234561:37.8260901,-122.4234293:37.8260709,-122.4233933:37.8260526,-122.4233438:37.8260251,-122.4232676:37.8259974,-122.4231939:37.82597,-122.4231203:37.8259722,-122.4230662:37.8259939,-122.4229635:37.8259294,-122.4227869:37.8258245,-122.4227291:37.8257651,-122.4226203:37.8257081,-122.4225136:37.8256159,-122.4223636:37.8255776,-122.4222696 && last_edit_user_name -> RAW || last_edit_changeset -> 10745510 || access -> private || last_edit_time -> 1329774930000 || last_edit_user_id -> 63807 || highway -> footway || last_edit_version -> 5 +27998996000001 && 37.8265542,-122.4222529:37.8264525,-122.4221499:37.826426,-122.4221443:37.8263254,-122.4220706:37.8262987,-122.4221054 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || source -> survey || highway -> footway || last_edit_version -> 6 +-27998996000001 && 37.8262987,-122.4221054:37.8263254,-122.4220706:37.826426,-122.4221443:37.8264525,-122.4221499:37.8265542,-122.4222529 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || source -> survey || highway -> footway || last_edit_version -> 6 +27998996000002 && 37.8262987,-122.4221054:37.8261526,-122.4219539 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || source -> survey || highway -> footway || last_edit_version -> 6 +-27998996000002 && 37.8261526,-122.4219539:37.8262987,-122.4221054 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || source -> survey || highway -> footway || last_edit_version -> 6 +459479815000000 && 37.8258018,-122.4209095:37.8256778,-122.4207891:37.8256396,-122.4207475:37.8256105,-122.4207728 && last_edit_user_name -> FTA || last_edit_changeset -> 44388316 || last_edit_time -> 1481686082000 || last_edit_user_id -> 652301 || highway -> footway || last_edit_version -> 1 +-459479815000000 && 37.8256105,-122.4207728:37.8256396,-122.4207475:37.8256778,-122.4207891:37.8258018,-122.4209095 && last_edit_user_name -> FTA || last_edit_changeset -> 44388316 || last_edit_time -> 1481686082000 || last_edit_user_id -> 652301 || highway -> footway || last_edit_version -> 1 +27999692000001 && 37.8263806,-122.4221839:37.8262122,-122.4224379:37.8261859,-122.4222843 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || source -> survey || highway -> footway || last_edit_version -> 5 +-27999692000001 && 37.8261859,-122.4222843:37.8262122,-122.4224379:37.8263806,-122.4221839 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || source -> survey || highway -> footway || last_edit_version -> 5 +27999692000002 && 37.8261859,-122.4222843:37.8262987,-122.4221054 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || source -> survey || highway -> footway || last_edit_version -> 5 +-27999692000002 && 37.8262987,-122.4221054:37.8261859,-122.4222843 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || source -> survey || highway -> footway || last_edit_version -> 5 +27999864000001 && 37.8266908,-122.4209852:37.8267423,-122.4209305:37.8266704,-122.4208218 && area -> yes || last_edit_user_name -> clay_c || wheelchair -> yes || last_edit_changeset -> 70368429 || man_made -> pier || last_edit_time -> 1558116233000 || last_edit_user_id -> 119881 || last_edit_version -> 8 +-27999864000001 && 37.8266704,-122.4208218:37.8267423,-122.4209305:37.8266908,-122.4209852 && area -> yes || last_edit_user_name -> clay_c || wheelchair -> yes || last_edit_changeset -> 70368429 || man_made -> pier || last_edit_time -> 1558116233000 || last_edit_user_id -> 119881 || last_edit_version -> 8 +27999864000002 && 37.8266704,-122.4208218:37.8266033,-122.4207207:37.8265198,-122.4208095:37.8266589,-122.4210191:37.8266908,-122.4209852 && area -> yes || last_edit_user_name -> clay_c || wheelchair -> yes || last_edit_changeset -> 70368429 || man_made -> pier || last_edit_time -> 1558116233000 || last_edit_user_id -> 119881 || last_edit_version -> 8 +-27999864000002 && 37.8266908,-122.4209852:37.8266589,-122.4210191:37.8265198,-122.4208095:37.8266033,-122.4207207:37.8266704,-122.4208218 && area -> yes || last_edit_user_name -> clay_c || wheelchair -> yes || last_edit_changeset -> 70368429 || man_made -> pier || last_edit_time -> 1558116233000 || last_edit_user_id -> 119881 || last_edit_version -> 8 +27999973000001 && 37.8268597,-122.4222351:37.8268464,-122.4222076 && last_edit_user_name -> delphiN || last_edit_changeset -> 4749900 || access -> private || last_edit_time -> 1274289692000 || last_edit_user_id -> 13192 || highway -> steps || last_edit_version -> 3 +-27999973000001 && 37.8268464,-122.4222076:37.8268597,-122.4222351 && last_edit_user_name -> delphiN || last_edit_changeset -> 4749900 || access -> private || last_edit_time -> 1274289692000 || last_edit_user_id -> 13192 || highway -> steps || last_edit_version -> 3 +27999973000002 && 37.8268464,-122.4222076:37.8268304,-122.4222147:37.8268702,-122.4223:37.8268474,-122.4223125:37.8268077,-122.4222252:37.8267768,-122.4222596:37.8268717,-122.4224313:37.8267903,-122.4225171 && last_edit_user_name -> delphiN || last_edit_changeset -> 4749900 || access -> private || last_edit_time -> 1274289692000 || last_edit_user_id -> 13192 || highway -> steps || last_edit_version -> 3 +-27999973000002 && 37.8267903,-122.4225171:37.8268717,-122.4224313:37.8267768,-122.4222596:37.8268077,-122.4222252:37.8268474,-122.4223125:37.8268702,-122.4223:37.8268304,-122.4222147:37.8268464,-122.4222076 && last_edit_user_name -> delphiN || last_edit_changeset -> 4749900 || access -> private || last_edit_time -> 1274289692000 || last_edit_user_id -> 13192 || highway -> steps || last_edit_version -> 3 +629120567000000 && 37.826102,-122.4222069:37.8260568,-122.4221661 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050340 || last_edit_time -> 1538261966000 || last_edit_user_id -> 9446 || highway -> footway || last_edit_version -> 1 +-629120567000000 && 37.8260568,-122.4221661:37.826102,-122.4222069 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050340 || last_edit_time -> 1538261966000 || last_edit_user_id -> 9446 || highway -> footway || last_edit_version -> 1 +27999701000001 && 37.8272039,-122.4229806:37.8271429,-122.4230235:37.8270073,-122.4228089:37.8268988,-122.4226544:37.8267903,-122.4225171 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> permissive || last_edit_time -> 1224958456000 || last_edit_user_id -> 35195 || source -> survey || highway -> service || last_edit_version -> 4 || created_by -> Potlatch 0.10e +-27999701000001 && 37.8267903,-122.4225171:37.8268988,-122.4226544:37.8270073,-122.4228089:37.8271429,-122.4230235:37.8272039,-122.4229806 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> permissive || last_edit_time -> 1224958456000 || last_edit_user_id -> 35195 || source -> survey || highway -> service || last_edit_version -> 4 || created_by -> Potlatch 0.10e +27999701000002 && 37.8267903,-122.4225171:37.8267361,-122.4224399:37.8265542,-122.4222529 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> permissive || last_edit_time -> 1224958456000 || last_edit_user_id -> 35195 || source -> survey || highway -> service || last_edit_version -> 4 || created_by -> Potlatch 0.10e +-27999701000002 && 37.8265542,-122.4222529:37.8267361,-122.4224399:37.8267903,-122.4225171 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> permissive || last_edit_time -> 1224958456000 || last_edit_user_id -> 35195 || source -> survey || highway -> service || last_edit_version -> 4 || created_by -> Potlatch 0.10e +28000456000000 && 37.8278683,-122.4248946:37.8279157,-122.4247487:37.8279582,-122.4246659 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> permissive || last_edit_time -> 1224957167000 || last_edit_user_id -> 35195 || highway -> service || last_edit_version -> 1 || created_by -> Potlatch 0.10e +-28000456000000 && 37.8279582,-122.4246659:37.8279157,-122.4247487:37.8278683,-122.4248946 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> permissive || last_edit_time -> 1224957167000 || last_edit_user_id -> 35195 || highway -> service || last_edit_version -> 1 || created_by -> Potlatch 0.10e +27998971000000 && 37.8255776,-122.4222696:37.8254863,-122.4221485:37.8253937,-122.4219604:37.8253699,-122.4219295:37.8253371,-122.421906:37.825312,-122.4218736:37.8252821,-122.4218064:37.8252571,-122.4217389:37.8252466,-122.4217013:37.825241,-122.4216635:37.8252425,-122.4216273:37.8252513,-122.4215468:37.8252669,-122.4214067:37.8252755,-122.4213487:37.825285,-122.4212942:37.8253016,-122.4212326:37.8253232,-122.4211635:37.8253462,-122.4211056:37.8253745,-122.4210509:37.825412,-122.4209931:37.8254547,-122.4209381:37.8254977,-122.4208866:37.8255531,-122.4208283:37.8256105,-122.4207728 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755474000 || last_edit_user_id -> 14293 || highway -> footway || last_edit_version -> 6 +-27998971000000 && 37.8256105,-122.4207728:37.8255531,-122.4208283:37.8254977,-122.4208866:37.8254547,-122.4209381:37.825412,-122.4209931:37.8253745,-122.4210509:37.8253462,-122.4211056:37.8253232,-122.4211635:37.8253016,-122.4212326:37.825285,-122.4212942:37.8252755,-122.4213487:37.8252669,-122.4214067:37.8252513,-122.4215468:37.8252425,-122.4216273:37.825241,-122.4216635:37.8252466,-122.4217013:37.8252571,-122.4217389:37.8252821,-122.4218064:37.825312,-122.4218736:37.8253371,-122.421906:37.8253699,-122.4219295:37.8253937,-122.4219604:37.8254863,-122.4221485:37.8255776,-122.4222696 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755474000 || last_edit_user_id -> 14293 || highway -> footway || last_edit_version -> 6 +27996944000001 && 37.8271677,-122.423325:37.8269103,-122.4229565:37.8269421,-122.4229216:37.8270828,-122.4231364:37.8271874,-122.4232905:37.8272688,-122.4234124:37.8272952,-122.4234433:37.8273118,-122.4234534:37.8273378,-122.4234535:37.8273597,-122.4234474:37.8273791,-122.4234309:37.8273905,-122.4234101:37.8273969,-122.4233843:37.8273955,-122.4233642:37.8273921,-122.4233363:37.8273827,-122.4233086:37.8273607,-122.4232626:37.8272944,-122.4231264:37.8272039,-122.4229806 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 59183284 || access -> permissive || last_edit_time -> 1527002255000 || last_edit_user_id -> 7134350 || source -> survey || highway -> service || last_edit_version -> 7 +-27996944000001 && 37.8272039,-122.4229806:37.8272944,-122.4231264:37.8273607,-122.4232626:37.8273827,-122.4233086:37.8273921,-122.4233363:37.8273955,-122.4233642:37.8273969,-122.4233843:37.8273905,-122.4234101:37.8273791,-122.4234309:37.8273597,-122.4234474:37.8273378,-122.4234535:37.8273118,-122.4234534:37.8272952,-122.4234433:37.8272688,-122.4234124:37.8271874,-122.4232905:37.8270828,-122.4231364:37.8269421,-122.4229216:37.8269103,-122.4229565:37.8271677,-122.423325 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 59183284 || access -> permissive || last_edit_time -> 1527002255000 || last_edit_user_id -> 7134350 || source -> survey || highway -> service || last_edit_version -> 7 +27996944000002 && 37.8272039,-122.4229806:37.8270498,-122.4226474:37.8269409,-122.4223799:37.8268745,-122.4222271 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 59183284 || access -> permissive || last_edit_time -> 1527002255000 || last_edit_user_id -> 7134350 || source -> survey || highway -> service || last_edit_version -> 7 +-27996944000002 && 37.8268745,-122.4222271:37.8269409,-122.4223799:37.8270498,-122.4226474:37.8272039,-122.4229806 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 59183284 || access -> permissive || last_edit_time -> 1527002255000 || last_edit_user_id -> 7134350 || source -> survey || highway -> service || last_edit_version -> 7 +27998974000000 && 37.827774,-122.4249791:37.8275858,-122.4248466:37.8271828,-122.4245778:37.8270347,-122.4244748 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> private || last_edit_time -> 1546755474000 || last_edit_user_id -> 14293 || highway -> footway || last_edit_version -> 2 +-27998974000000 && 37.8270347,-122.4244748:37.8271828,-122.4245778:37.8275858,-122.4248466:37.827774,-122.4249791 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> private || last_edit_time -> 1546755474000 || last_edit_user_id -> 14293 || highway -> footway || last_edit_version -> 2 +28000400000001 && 37.8275768,-122.4230619:37.8274276,-122.4229291:37.8273191,-122.4227832:37.827285,-122.4227049 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || access -> permissive || last_edit_time -> 1314664597000 || last_edit_user_id -> 27824 || source -> survey || highway -> service || last_edit_version -> 4 +-28000400000001 && 37.827285,-122.4227049:37.8273191,-122.4227832:37.8274276,-122.4229291:37.8275768,-122.4230619 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || access -> permissive || last_edit_time -> 1314664597000 || last_edit_user_id -> 27824 || source -> survey || highway -> service || last_edit_version -> 4 +28000400000002 && 37.827285,-122.4227049:37.8272008,-122.422614:37.827145,-122.4225347:37.8270892,-122.4224434:37.826971,-122.4222182:37.8268745,-122.4222271 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || access -> permissive || last_edit_time -> 1314664597000 || last_edit_user_id -> 27824 || source -> survey || highway -> service || last_edit_version -> 4 +-28000400000002 && 37.8268745,-122.4222271:37.826971,-122.4222182:37.8270892,-122.4224434:37.827145,-122.4225347:37.8272008,-122.422614:37.827285,-122.4227049 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || access -> permissive || last_edit_time -> 1314664597000 || last_edit_user_id -> 27824 || source -> survey || highway -> service || last_edit_version -> 4 +27998999000000 && 37.8268745,-122.4222271:37.8268597,-122.4222351 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> private || last_edit_time -> 1224954023000 || last_edit_user_id -> 35195 || highway -> footway || last_edit_version -> 1 || created_by -> JOSM +-27998999000000 && 37.8268597,-122.4222351:37.8268745,-122.4222271 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> private || last_edit_time -> 1224954023000 || last_edit_user_id -> 35195 || highway -> footway || last_edit_version -> 1 || created_by -> JOSM +27999800000001 && 37.8261152,-122.4234561:37.826172,-122.4234946:37.8262353,-122.4235155:37.826368,-122.4235658:37.8264185,-122.4235906:37.8264695,-122.4236169:37.8265259,-122.4236413:37.8265462,-122.4236473:37.8265662,-122.4236481:37.8265794,-122.4236449:37.8265897,-122.4236358:37.8265962,-122.4236215 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || name -> West Road || highway -> service || last_edit_version -> 5 +-27999800000001 && 37.8265962,-122.4236215:37.8265897,-122.4236358:37.8265794,-122.4236449:37.8265662,-122.4236481:37.8265462,-122.4236473:37.8265259,-122.4236413:37.8264695,-122.4236169:37.8264185,-122.4235906:37.826368,-122.4235658:37.8262353,-122.4235155:37.826172,-122.4234946:37.8261152,-122.4234561 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || name -> West Road || highway -> service || last_edit_version -> 5 +27999800000002 && 37.8265962,-122.4236215:37.8265981,-122.423603:37.8265934,-122.4235809:37.8265839,-122.4235623:37.8265732,-122.4235467:37.8263884,-122.4233246:37.8263441,-122.4232657:37.8263238,-122.4232313:37.8263034,-122.4231884:37.8262899,-122.4231439:37.8262707,-122.4230778:37.8262188,-122.4229102:37.8261717,-122.4227524:37.8261517,-122.4226823:37.8261423,-122.422641:37.8261172,-122.4225077:37.8260838,-122.4223215:37.8260651,-122.4222218:37.8260568,-122.4221661 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || name -> West Road || highway -> service || last_edit_version -> 5 +-27999800000002 && 37.8260568,-122.4221661:37.8260651,-122.4222218:37.8260838,-122.4223215:37.8261172,-122.4225077:37.8261423,-122.422641:37.8261517,-122.4226823:37.8261717,-122.4227524:37.8262188,-122.4229102:37.8262707,-122.4230778:37.8262899,-122.4231439:37.8263034,-122.4231884:37.8263238,-122.4232313:37.8263441,-122.4232657:37.8263884,-122.4233246:37.8265732,-122.4235467:37.8265839,-122.4235623:37.8265934,-122.4235809:37.8265981,-122.423603:37.8265962,-122.4236215 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || name -> West Road || highway -> service || last_edit_version -> 5 +27999800000003 && 37.8260568,-122.4221661:37.8260557,-122.4221316:37.826064,-122.4220996:37.8260813,-122.4220557:37.8261526,-122.4219539 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || name -> West Road || highway -> service || last_edit_version -> 5 +-27999800000003 && 37.8261526,-122.4219539:37.8260813,-122.4220557:37.826064,-122.4220996:37.8260557,-122.4221316:37.8260568,-122.4221661 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || name -> West Road || highway -> service || last_edit_version -> 5 +27998983000001 && 37.8269888,-122.422082:37.8270681,-122.4222388 && last_edit_user_name -> achims311 || last_edit_changeset -> 13998200 || access -> permissive || last_edit_time -> 1353674169000 || last_edit_user_id -> 52898 || source -> survey || highway -> service || last_edit_version -> 4 +-27998983000001 && 37.8270681,-122.4222388:37.8269888,-122.422082 && last_edit_user_name -> achims311 || last_edit_changeset -> 13998200 || access -> permissive || last_edit_time -> 1353674169000 || last_edit_user_id -> 52898 || source -> survey || highway -> service || last_edit_version -> 4 +27998983000002 && 37.8270681,-122.4222388:37.8271463,-122.4224117:37.8272161,-122.4225662:37.827285,-122.4227049 && last_edit_user_name -> achims311 || last_edit_changeset -> 13998200 || access -> permissive || last_edit_time -> 1353674169000 || last_edit_user_id -> 52898 || source -> survey || highway -> service || last_edit_version -> 4 +-27998983000002 && 37.827285,-122.4227049:37.8272161,-122.4225662:37.8271463,-122.4224117:37.8270681,-122.4222388 && last_edit_user_name -> achims311 || last_edit_changeset -> 13998200 || access -> permissive || last_edit_time -> 1353674169000 || last_edit_user_id -> 52898 || source -> survey || highway -> service || last_edit_version -> 4 +128245370000000 && 37.8266908,-122.4209852:37.8267031,-122.4210044 && last_edit_user_name -> monena41 || last_edit_changeset -> 58579435 || man_made -> pier || last_edit_time -> 1525167595000 || last_edit_user_id -> 4633898 || amenity -> ferry_terminal || name -> Alcatraz Island Ferry Terminal || cargo -> passengers || last_edit_version -> 2 +-128245370000000 && 37.8267031,-122.4210044:37.8266908,-122.4209852 && last_edit_user_name -> monena41 || last_edit_changeset -> 58579435 || man_made -> pier || last_edit_time -> 1525167595000 || last_edit_user_id -> 4633898 || amenity -> ferry_terminal || name -> Alcatraz Island Ferry Terminal || cargo -> passengers || last_edit_version -> 2 +59621740000000 && 37.8255776,-122.4222696:37.8257351,-122.4221372 && last_edit_user_name -> dpaschich || last_edit_changeset -> 50700405 || last_edit_time -> 1501440870000 || last_edit_user_id -> 621202 || highway -> footway || last_edit_version -> 2 +-59621740000000 && 37.8257351,-122.4221372:37.8255776,-122.4222696 && last_edit_user_name -> dpaschich || last_edit_changeset -> 50700405 || last_edit_time -> 1501440870000 || last_edit_user_id -> 621202 || highway -> footway || last_edit_version -> 2 +128245366000000 && 37.8270905,-122.4219723:37.8269443,-122.4217188 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664590000 || last_edit_user_id -> 27824 || highway -> footway || last_edit_version -> 1 +-128245366000000 && 37.8269443,-122.4217188:37.8270905,-122.4219723 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664590000 || last_edit_user_id -> 27824 || highway -> footway || last_edit_version -> 1 +314192471000000 && 37.8270878,-122.4243583:37.8270774,-122.4243445 && last_edit_user_name -> dchiles || last_edit_changeset -> 26984131 || last_edit_time -> 1416781373000 || last_edit_user_id -> 153669 || highway -> footway || last_edit_version -> 1 +-314192471000000 && 37.8270774,-122.4243445:37.8270878,-122.4243583 && last_edit_user_name -> dchiles || last_edit_changeset -> 26984131 || last_edit_time -> 1416781373000 || last_edit_user_id -> 153669 || highway -> footway || last_edit_version -> 1 +27998977000000 && 37.8271629,-122.4222296:37.827168,-122.422239:37.8272859,-122.4224685:37.8273227,-122.4225384 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753285000 || last_edit_user_id -> 381909 || name -> East Road || highway -> service || last_edit_version -> 3 || layer -> -1 || tunnel -> yes +-27998977000000 && 37.8273227,-122.4225384:37.8272859,-122.4224685:37.827168,-122.422239:37.8271629,-122.4222296 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753285000 || last_edit_user_id -> 381909 || name -> East Road || highway -> service || last_edit_version -> 3 || layer -> -1 || tunnel -> yes +27998998000001 && 37.8268745,-122.4222271:37.8268464,-122.4222076 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 59183284 || access -> permissive || last_edit_time -> 1527002255000 || last_edit_user_id -> 7134350 || source -> survey || highway -> service || last_edit_version -> 4 +-27998998000001 && 37.8268464,-122.4222076:37.8268745,-122.4222271 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 59183284 || access -> permissive || last_edit_time -> 1527002255000 || last_edit_user_id -> 7134350 || source -> survey || highway -> service || last_edit_version -> 4 +27998998000002 && 37.8268464,-122.4222076:37.826809,-122.4221806:37.8266226,-122.4219063:37.8265103,-122.4217541:37.8264333,-122.4216514:37.8263646,-122.421571 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 59183284 || access -> permissive || last_edit_time -> 1527002255000 || last_edit_user_id -> 7134350 || source -> survey || highway -> service || last_edit_version -> 4 +-27998998000002 && 37.8263646,-122.421571:37.8264333,-122.4216514:37.8265103,-122.4217541:37.8266226,-122.4219063:37.826809,-122.4221806:37.8268464,-122.4222076 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 59183284 || access -> permissive || last_edit_time -> 1527002255000 || last_edit_user_id -> 7134350 || source -> survey || highway -> service || last_edit_version -> 4 +629121014000000 && 37.8262987,-122.4221054:37.8263319,-122.4221365 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || highway -> steps || last_edit_version -> 1 +-629121014000000 && 37.8263319,-122.4221365:37.8262987,-122.4221054 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || highway -> steps || last_edit_version -> 1 +27996842000001 && 37.8257351,-122.4221372:37.8257192,-122.4219714:37.8257069,-122.4219113:37.8256076,-122.4217186:37.8256695,-122.4216631:37.8256361,-122.4215908:37.825583,-122.4216015:37.8255055,-122.4214552:37.8254333,-122.4214099:37.8253431,-122.4214434:37.8252912,-122.4215249:37.8253312,-122.4212359:37.8253762,-122.4212779:37.8255871,-122.4213427:37.8256336,-122.4212818:37.8256581,-122.4212001:37.8257184,-122.4211315:37.8257309,-122.4210036:37.8256911,-122.4209166:37.8258018,-122.4209095 && area -> yes || last_edit_user_name -> Azlux || last_edit_changeset -> 47452285 || surface -> concrete || last_edit_time -> 1491332262000 || last_edit_user_id -> 5586237 || name -> Parade Ground || highway -> pedestrian || last_edit_version -> 7 +-27996842000001 && 37.8258018,-122.4209095:37.8256911,-122.4209166:37.8257309,-122.4210036:37.8257184,-122.4211315:37.8256581,-122.4212001:37.8256336,-122.4212818:37.8255871,-122.4213427:37.8253762,-122.4212779:37.8253312,-122.4212359:37.8252912,-122.4215249:37.8253431,-122.4214434:37.8254333,-122.4214099:37.8255055,-122.4214552:37.825583,-122.4216015:37.8256361,-122.4215908:37.8256695,-122.4216631:37.8256076,-122.4217186:37.8257069,-122.4219113:37.8257192,-122.4219714:37.8257351,-122.4221372 && area -> yes || last_edit_user_name -> Azlux || last_edit_changeset -> 47452285 || surface -> concrete || last_edit_time -> 1491332262000 || last_edit_user_id -> 5586237 || name -> Parade Ground || highway -> pedestrian || last_edit_version -> 7 +27996842000002 && 37.8258018,-122.4209095:37.8258258,-122.420908:37.8259144,-122.421014:37.8260075,-122.421124:37.8263461,-122.4213991:37.8263899,-122.4215455:37.8263646,-122.421571 && area -> yes || last_edit_user_name -> Azlux || last_edit_changeset -> 47452285 || surface -> concrete || last_edit_time -> 1491332262000 || last_edit_user_id -> 5586237 || name -> Parade Ground || highway -> pedestrian || last_edit_version -> 7 +-27996842000002 && 37.8263646,-122.421571:37.8263899,-122.4215455:37.8263461,-122.4213991:37.8260075,-122.421124:37.8259144,-122.421014:37.8258258,-122.420908:37.8258018,-122.4209095 && area -> yes || last_edit_user_name -> Azlux || last_edit_changeset -> 47452285 || surface -> concrete || last_edit_time -> 1491332262000 || last_edit_user_id -> 5586237 || name -> Parade Ground || highway -> pedestrian || last_edit_version -> 7 +27996842000003 && 37.8263646,-122.421571:37.8263469,-122.4215926:37.8261538,-122.4217471:37.8261191,-122.4217666:37.8260852,-122.4217843:37.8260352,-122.4218334:37.8260103,-122.4218633:37.82597,-122.4219276:37.8257351,-122.4221372 && area -> yes || last_edit_user_name -> Azlux || last_edit_changeset -> 47452285 || surface -> concrete || last_edit_time -> 1491332262000 || last_edit_user_id -> 5586237 || name -> Parade Ground || highway -> pedestrian || last_edit_version -> 7 +-27996842000003 && 37.8257351,-122.4221372:37.82597,-122.4219276:37.8260103,-122.4218633:37.8260352,-122.4218334:37.8260852,-122.4217843:37.8261191,-122.4217666:37.8261538,-122.4217471:37.8263469,-122.4215926:37.8263646,-122.421571 && area -> yes || last_edit_user_name -> Azlux || last_edit_changeset -> 47452285 || surface -> concrete || last_edit_time -> 1491332262000 || last_edit_user_id -> 5586237 || name -> Parade Ground || highway -> pedestrian || last_edit_version -> 7 +27989502000000 && 37.8251293,-122.421207:37.8251708,-122.4210867:37.8251972,-122.4210324:37.8252111,-122.4210075:37.8252266,-122.4209838:37.8252472,-122.4209575:37.8253152,-122.420884:37.8253488,-122.420853:37.8253689,-122.4208404:37.8253963,-122.4208266:37.8254275,-122.4208081:37.8254607,-122.4207908:37.8255612,-122.4207442:37.8256105,-122.4207728 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755473000 || last_edit_user_id -> 14293 || name -> The Agave Trail || highway -> steps || last_edit_version -> 6 +-27989502000000 && 37.8256105,-122.4207728:37.8255612,-122.4207442:37.8254607,-122.4207908:37.8254275,-122.4208081:37.8253963,-122.4208266:37.8253689,-122.4208404:37.8253488,-122.420853:37.8253152,-122.420884:37.8252472,-122.4209575:37.8252266,-122.4209838:37.8252111,-122.4210075:37.8251972,-122.4210324:37.8251708,-122.4210867:37.8251293,-122.421207 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755473000 || last_edit_user_id -> 14293 || name -> The Agave Trail || highway -> steps || last_edit_version -> 6 +27609572000000 && 37.8270274,-122.4214142:37.8271993,-122.4216941 && last_edit_user_name -> GreyCat || last_edit_changeset -> 172031 || man_made -> pier || last_edit_time -> 1223501888000 || last_edit_user_id -> 45062 || last_edit_version -> 2 || created_by -> Potlatch 0.10d +-27609572000000 && 37.8271993,-122.4216941:37.8270274,-122.4214142 && last_edit_user_name -> GreyCat || last_edit_changeset -> 172031 || man_made -> pier || last_edit_time -> 1223501888000 || last_edit_user_id -> 45062 || last_edit_version -> 2 || created_by -> Potlatch 0.10d +28000429000000 && 37.8279428,-122.4243196:37.8276242,-122.4239505 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> private || last_edit_time -> 1224958477000 || last_edit_user_id -> 35195 || source -> yahoo || highway -> footway || last_edit_version -> 2 || created_by -> Potlatch 0.10e +-28000429000000 && 37.8276242,-122.4239505:37.8279428,-122.4243196 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || access -> private || last_edit_time -> 1224958477000 || last_edit_user_id -> 35195 || source -> yahoo || highway -> footway || last_edit_version -> 2 || created_by -> Potlatch 0.10e +128245371000000 && 37.8270442,-122.4220131:37.8270905,-122.4219723 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664591000 || last_edit_user_id -> 27824 || bridge -> yes || highway -> footway || last_edit_version -> 1 +-128245371000000 && 37.8270905,-122.4219723:37.8270442,-122.4220131 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664591000 || last_edit_user_id -> 27824 || bridge -> yes || highway -> footway || last_edit_version -> 1 +128245363000000 && 37.8280205,-122.4241771:37.8277896,-122.423827:37.8277604,-122.4235646 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || access -> permissive || last_edit_time -> 1314664590000 || last_edit_user_id -> 27824 || source -> survey || highway -> service || last_edit_version -> 1 +-128245363000000 && 37.8277604,-122.4235646:37.8277896,-122.423827:37.8280205,-122.4241771 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || access -> permissive || last_edit_time -> 1314664590000 || last_edit_user_id -> 27824 || source -> survey || highway -> service || last_edit_version -> 1 +28000416000000 && 37.8279922,-122.4243622:37.8279428,-122.4243196 && last_edit_user_name -> dpaschich || last_edit_changeset -> 50700405 || last_edit_time -> 1501440869000 || last_edit_user_id -> 621202 || highway -> footway || last_edit_version -> 4 +-28000416000000 && 37.8279428,-122.4243196:37.8279922,-122.4243622 && last_edit_user_name -> dpaschich || last_edit_changeset -> 50700405 || last_edit_time -> 1501440869000 || last_edit_user_id -> 621202 || highway -> footway || last_edit_version -> 4 +28000072000001 && 37.8270154,-122.4240917:37.8269877,-122.4240829:37.8269669,-122.4240835:37.8269543,-122.4241045 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || highway -> service || last_edit_version -> 6 +-28000072000001 && 37.8269543,-122.4241045:37.8269669,-122.4240835:37.8269877,-122.4240829:37.8270154,-122.4240917 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || highway -> service || last_edit_version -> 6 +28000072000002 && 37.8269543,-122.4241045:37.8269295,-122.4240948:37.8269102,-122.4240852:37.8268929,-122.4240745:37.8268792,-122.4240632:37.8268685,-122.424052:37.8268558,-122.4240366:37.8267221,-122.4238051:37.8266966,-122.4237621:37.8266691,-122.4237203:37.8265962,-122.4236215 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || highway -> service || last_edit_version -> 6 +-28000072000002 && 37.8265962,-122.4236215:37.8266691,-122.4237203:37.8266966,-122.4237621:37.8267221,-122.4238051:37.8268558,-122.4240366:37.8268685,-122.424052:37.8268792,-122.4240632:37.8268929,-122.4240745:37.8269102,-122.4240852:37.8269295,-122.4240948:37.8269543,-122.4241045 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> permissive || last_edit_time -> 1546755475000 || last_edit_user_id -> 14293 || highway -> service || last_edit_version -> 6 +314192472000000 && 37.8270774,-122.4243445:37.8270479,-122.4242927 && last_edit_user_name -> dchiles || last_edit_changeset -> 26984131 || last_edit_time -> 1416781373000 || last_edit_user_id -> 153669 || bridge -> yes || highway -> footway || last_edit_version -> 1 +-314192472000000 && 37.8270479,-122.4242927:37.8270774,-122.4243445 && last_edit_user_name -> dchiles || last_edit_changeset -> 26984131 || last_edit_time -> 1416781373000 || last_edit_user_id -> 153669 || bridge -> yes || highway -> footway || last_edit_version -> 1 +27998972000001 && 37.8268181,-122.421603:37.8268908,-122.4217373:37.8270224,-122.4219712:37.8270442,-122.4220131 && last_edit_user_name -> nithinkamath || last_edit_changeset -> 12663035 || access -> permissive || last_edit_time -> 1344465780000 || last_edit_user_id -> 573196 || name -> East Road || highway -> service || last_edit_version -> 3 +-27998972000001 && 37.8270442,-122.4220131:37.8270224,-122.4219712:37.8268908,-122.4217373:37.8268181,-122.421603 && last_edit_user_name -> nithinkamath || last_edit_changeset -> 12663035 || access -> permissive || last_edit_time -> 1344465780000 || last_edit_user_id -> 573196 || name -> East Road || highway -> service || last_edit_version -> 3 +27998972000002 && 37.8270442,-122.4220131:37.8271046,-122.4221259:37.8271629,-122.4222296 && last_edit_user_name -> nithinkamath || last_edit_changeset -> 12663035 || access -> permissive || last_edit_time -> 1344465780000 || last_edit_user_id -> 573196 || name -> East Road || highway -> service || last_edit_version -> 3 +-27998972000002 && 37.8271629,-122.4222296:37.8271046,-122.4221259:37.8270442,-122.4220131 && last_edit_user_name -> nithinkamath || last_edit_changeset -> 12663035 || access -> permissive || last_edit_time -> 1344465780000 || last_edit_user_id -> 573196 || name -> East Road || highway -> service || last_edit_version -> 3 +629121015000000 && 37.8263319,-122.4221365:37.8263806,-122.4221839 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || highway -> footway || last_edit_version -> 1 +-629121015000000 && 37.8263806,-122.4221839:37.8263319,-122.4221365 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || highway -> footway || last_edit_version -> 1 +27998976000001 && 37.8273227,-122.4225384:37.8274359,-122.4227538:37.827516,-122.4229151:37.8275768,-122.4230619 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +-27998976000001 && 37.8275768,-122.4230619:37.827516,-122.4229151:37.8274359,-122.4227538:37.8273227,-122.4225384 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +27998976000002 && 37.8275768,-122.4230619:37.8275941,-122.4231038:37.8276627,-122.423267:37.8277451,-122.42347:37.8277604,-122.4235646 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +-27998976000002 && 37.8277604,-122.4235646:37.8277451,-122.42347:37.8276627,-122.423267:37.8275941,-122.4231038:37.8275768,-122.4230619 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +27998976000003 && 37.8277604,-122.4235646:37.8277276,-122.4236738:37.82772,-122.4237782:37.8277536,-122.4238891:37.8278635,-122.4240707:37.827942,-122.4242253:37.8279893,-122.424326:37.8279922,-122.4243622 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +-27998976000003 && 37.8279922,-122.4243622:37.8279893,-122.424326:37.827942,-122.4242253:37.8278635,-122.4240707:37.8277536,-122.4238891:37.82772,-122.4237782:37.8277276,-122.4236738:37.8277604,-122.4235646 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +27998976000004 && 37.8279922,-122.4243622:37.8279971,-122.4244226:37.8279971,-122.4245256:37.82797,-122.4246114:37.8279582,-122.4246659 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +-27998976000004 && 37.8279582,-122.4246659:37.82797,-122.4246114:37.8279971,-122.4245256:37.8279971,-122.4244226:37.8279922,-122.4243622 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +27998976000005 && 37.8279582,-122.4246659:37.8279545,-122.4247254:37.8279432,-122.4248882:37.8278803,-122.4249867:37.827774,-122.4249791 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +-27998976000005 && 37.827774,-122.4249791:37.8278803,-122.4249867:37.8279432,-122.4248882:37.8279545,-122.4247254:37.8279582,-122.4246659 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || access -> permissive || last_edit_time -> 1322753284000 || last_edit_user_id -> 381909 || name -> East Road || source -> survey || highway -> service || last_edit_version -> 11 +27998980000000 && 37.8271609,-122.4222329:37.8271132,-122.42221 && last_edit_user_name -> RAW || last_edit_changeset -> 15448995 || last_edit_time -> 1363905028000 || last_edit_user_id -> 63807 || bridge -> yes || highway -> footway || last_edit_version -> 4 || layer -> 2 +-27998980000000 && 37.8271132,-122.42221:37.8271609,-122.4222329 && last_edit_user_name -> RAW || last_edit_changeset -> 15448995 || last_edit_time -> 1363905028000 || last_edit_user_id -> 63807 || bridge -> yes || highway -> footway || last_edit_version -> 4 || layer -> 2 +27989501000000 && 37.8264021,-122.4208428:37.8262588,-122.4206588:37.8261186,-122.4204998:37.8257539,-122.4204073:37.8257395,-122.420403:37.8256169,-122.4204726:37.8255235,-122.4205092:37.8254354,-122.4205599:37.8254083,-122.4205836:37.8252464,-122.4207352:37.8252356,-122.4207497:37.8251927,-122.4208583:37.8251794,-122.4208966:37.8251061,-122.42115:37.8251022,-122.4211649:37.8251293,-122.421207 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755474000 || last_edit_user_id -> 14293 || name -> The Agave Trail (Seasonal) || source -> survey || highway -> footway || last_edit_version -> 7 +-27989501000000 && 37.8251293,-122.421207:37.8251022,-122.4211649:37.8251061,-122.42115:37.8251794,-122.4208966:37.8251927,-122.4208583:37.8252356,-122.4207497:37.8252464,-122.4207352:37.8254083,-122.4205836:37.8254354,-122.4205599:37.8255235,-122.4205092:37.8256169,-122.4204726:37.8257395,-122.420403:37.8257539,-122.4204073:37.8261186,-122.4204998:37.8262588,-122.4206588:37.8264021,-122.4208428 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755474000 || last_edit_user_id -> 14293 || name -> The Agave Trail (Seasonal) || source -> survey || highway -> footway || last_edit_version -> 7 +629120566000000 && 37.8261859,-122.4222843:37.826102,-122.4222069 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050340 || last_edit_time -> 1538261966000 || last_edit_user_id -> 9446 || highway -> steps || last_edit_version -> 1 +-629120566000000 && 37.826102,-122.4222069:37.8261859,-122.4222843 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050340 || last_edit_time -> 1538261966000 || last_edit_user_id -> 9446 || highway -> steps || last_edit_version -> 1 +28000214000001 && 37.8268181,-122.421603:37.826957,-122.421478:37.8270274,-122.4214142 && area -> yes || last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || surface -> concrete || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Dock || highway -> pedestrian || last_edit_version -> 9 +-28000214000001 && 37.8270274,-122.4214142:37.826957,-122.421478:37.8268181,-122.421603 && area -> yes || last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || surface -> concrete || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Dock || highway -> pedestrian || last_edit_version -> 9 +28000214000002 && 37.8270274,-122.4214142:37.8270426,-122.4214009:37.8267594,-122.4209676:37.8267317,-122.420966:37.8267031,-122.4210044 && area -> yes || last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || surface -> concrete || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Dock || highway -> pedestrian || last_edit_version -> 9 +-28000214000002 && 37.8267031,-122.4210044:37.8267317,-122.420966:37.8267594,-122.4209676:37.8270426,-122.4214009:37.8270274,-122.4214142 && area -> yes || last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || surface -> concrete || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Dock || highway -> pedestrian || last_edit_version -> 9 +28000214000003 && 37.8267031,-122.4210044:37.8266576,-122.4210652:37.8265016,-122.420877:37.8264889,-122.4208486:37.8264623,-122.4208766:37.826416,-122.420818:37.8264021,-122.4208428 && area -> yes || last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || surface -> concrete || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Dock || highway -> pedestrian || last_edit_version -> 9 +-28000214000003 && 37.8264021,-122.4208428:37.826416,-122.420818:37.8264623,-122.4208766:37.8264889,-122.4208486:37.8265016,-122.420877:37.8266576,-122.4210652:37.8267031,-122.4210044 && area -> yes || last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || surface -> concrete || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Dock || highway -> pedestrian || last_edit_version -> 9 +28000214000004 && 37.8264021,-122.4208428:37.8263926,-122.4208604:37.8262666,-122.4211063:37.8263971,-122.421303:37.8264185,-122.4213353:37.8266246,-122.4212888:37.8266819,-122.4213756:37.8268112,-122.4216098:37.8268181,-122.421603 && area -> yes || last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || surface -> concrete || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Dock || highway -> pedestrian || last_edit_version -> 9 +-28000214000004 && 37.8268181,-122.421603:37.8268112,-122.4216098:37.8266819,-122.4213756:37.8266246,-122.4212888:37.8264185,-122.4213353:37.8263971,-122.421303:37.8262666,-122.4211063:37.8263926,-122.4208604:37.8264021,-122.4208428 && area -> yes || last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || surface -> concrete || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Dock || highway -> pedestrian || last_edit_version -> 9 +192237295000000 && 37.8271132,-122.42221:37.8270681,-122.4222388 && last_edit_user_name -> achims311 || last_edit_changeset -> 13998200 || last_edit_time -> 1353674169000 || last_edit_user_id -> 52898 || source -> bing || highway -> footway || last_edit_version -> 1 +-192237295000000 && 37.8270681,-122.4222388:37.8271132,-122.42221 && last_edit_user_name -> achims311 || last_edit_changeset -> 13998200 || last_edit_time -> 1353674169000 || last_edit_user_id -> 52898 || source -> bing || highway -> footway || last_edit_version -> 1 +128245364000000 && 37.8271206,-122.4239856:37.8270154,-122.4240917 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664590000 || last_edit_user_id -> 27824 || highway -> steps || last_edit_version -> 1 +-128245364000000 && 37.8270154,-122.4240917:37.8271206,-122.4239856 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664590000 || last_edit_user_id -> 27824 || highway -> steps || last_edit_version -> 1 +314192473000000 && 37.8270479,-122.4242927:37.827025,-122.4242758:37.8269274,-122.4241733:37.8269514,-122.4241288:37.8269543,-122.4241045 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755473000 || last_edit_user_id -> 14293 || highway -> footway || last_edit_version -> 2 +-314192473000000 && 37.8269543,-122.4241045:37.8269514,-122.4241288:37.8269274,-122.4241733:37.827025,-122.4242758:37.8270479,-122.4242927 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755473000 || last_edit_user_id -> 14293 || highway -> footway || last_edit_version -> 2 +# Areas +24433389000000 && 37.8281718,-122.4253998:37.8280361,-122.4255291:37.8279852,-122.4254435:37.8279176,-122.425508:37.8278166,-122.4253382:37.8278507,-122.4253057:37.8277749,-122.425178:37.8278432,-122.4251129:37.82797,-122.4253263:37.828071,-122.4252301 && last_edit_user_name -> Edward || last_edit_changeset -> 61569480 || old_name -> Military Industries and Workshops Building || last_edit_time -> 1534005386000 || last_edit_user_id -> 364 || name -> Model Industries Building || disused -> yes || last_edit_version -> 4 || wikidata -> Q1142654 || building -> yes +660870452000000 && 37.8275819,-122.4238976:37.827596,-122.4238812:37.8276045,-122.4238624:37.8276087,-122.4238396:37.8276074,-122.4238178:37.8276007,-122.4237958:37.8275904,-122.4237791:37.8275771,-122.4237669:37.8275621,-122.4237596:37.8275423,-122.4237581:37.8275248,-122.4237637:37.8275114,-122.4237738:37.8274985,-122.4237911:37.827491,-122.4238102:37.8274881,-122.4238312:37.82749,-122.4238535:37.8274965,-122.423873:37.8275066,-122.4238893:37.8275191,-122.4239009:37.8275327,-122.4239078:37.8275488,-122.4239106:37.827567,-122.4239068 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66062793 || man_made -> water_tower || last_edit_time -> 1546753099000 || last_edit_user_id -> 14293 || name -> Water Tower || disused -> yes || last_edit_version -> 1 || building -> yes +27999864000000 && 37.8266033,-122.4207207:37.8265198,-122.4208095:37.8266589,-122.4210191:37.8266908,-122.4209852:37.8267423,-122.4209305:37.8266704,-122.4208218 && area -> yes || last_edit_user_name -> clay_c || wheelchair -> yes || last_edit_changeset -> 70368429 || man_made -> pier || last_edit_time -> 1558116233000 || last_edit_user_id -> 119881 || last_edit_version -> 8 +128245373000000 && 37.8267327,-122.4231328:37.8266988,-122.4231768:37.8266479,-122.4232435:37.8263371,-122.4228632:37.8264063,-122.4227727:37.8262972,-122.4226392:37.8263178,-122.4226124:37.8265359,-122.422327:37.8268456,-122.4227066:37.8267808,-122.4227913:37.8268908,-122.422926:37.8268581,-122.4229687 && last_edit_user_name -> eduaddad || name:pt -> Prisão principal || last_edit_changeset -> 73895096 || alt_name -> Cellhouse || historic -> yes || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Main Prison || last_edit_version -> 4 || building -> yes +232326056000000 && 37.8276009,-122.4235219:37.8275865,-122.4234812:37.8275419,-122.4235064:37.8275562,-122.4235471 && last_edit_user_name -> Nautic || last_edit_changeset -> 17214523 || last_edit_time -> 1375620456000 || last_edit_user_id -> 449569 || last_edit_version -> 1 || building -> yes +99202294000000 && 37.8262455,-122.4221663:37.826249,-122.4221588:37.8262502,-122.4221506:37.8262499,-122.4221461:37.8262475,-122.4221382:37.8262415,-122.4221304:37.8262357,-122.4221273:37.8262285,-122.4221268:37.8262226,-122.4221292:37.8262178,-122.4221337:37.826214,-122.4221406:37.8262124,-122.4221487:37.826213,-122.4221568:37.8262163,-122.4221652:37.8262213,-122.4221709:37.8262297,-122.4221744:37.8262366,-122.4221735:37.8262429,-122.4221694 && last_edit_user_name -> Edward || last_edit_changeset -> 49588642 || alt_name -> United States Coast Guard Lighthouse || man_made -> lighthouse || last_edit_time -> 1497612241000 || last_edit_user_id -> 364 || name -> Alcatraz Island Lighthouse || last_edit_version -> 5 || wikidata -> Q4712967 || building -> yes || start_date -> 1909 +28000041000000 && 37.8272682,-122.4236113:37.827237,-122.4235831:37.8272679,-122.4235284:37.827299,-122.4235567 && last_edit_user_name -> Manu1400 || last_edit_changeset -> 15874039 || last_edit_time -> 1366994044000 || last_edit_user_id -> 181135 || amenity -> mortuary || name -> Morgue || disused -> yes || last_edit_version -> 4 || building -> yes +27996723000000 && 37.828194,-122.4241906:37.8281081,-122.4242756:37.8279241,-122.4239776:37.82801,-122.4238926 && last_edit_user_name -> eduaddad || name:pt -> Quarto principal || last_edit_changeset -> 73895096 || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Quartermaster || last_edit_version -> 4 || building -> yes +59621863000000 && 37.8256032,-122.4222034:37.8256995,-122.4221046:37.8257007,-122.4219819:37.8256934,-122.4219179:37.8255862,-122.4217157:37.8256532,-122.4216594:37.82563,-122.4216108:37.8255825,-122.4216208:37.8255002,-122.421468:37.825432,-122.421424:37.8253491,-122.4214665:37.8252948,-122.4215568:37.8252887,-122.4217273:37.8253722,-122.4218716:37.8255551,-122.4221903 && last_edit_user_name -> nvk || last_edit_changeset -> 33170040 || historic -> ruins || natural -> scree || last_edit_time -> 1438919870000 || last_edit_user_id -> 131996 || name -> Rubble Pile || last_edit_version -> 2 +151291054000000 && 37.827092,-122.423515:37.8271835,-122.4235957:37.8274396,-122.423822:37.827279,-122.4241222:37.8271206,-122.4239856:37.8266955,-122.4236195:37.8268332,-122.4233348:37.8266988,-122.4231768:37.8267327,-122.4231328:37.8270223,-122.4234875:37.8270451,-122.4234575 && area -> yes || wheelchair -> no || last_edit_changeset -> 73895096 || source -> yahoo || disused -> yes || last_edit_user_name -> eduaddad || name:pt -> Quintal de recreação || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Recreation Yard || last_edit_version -> 4 || leisure -> pitch || sport -> multi +27996759000000 && 37.8271238,-122.4222642:37.8272374,-122.4224816:37.8272245,-122.4224936:37.8272731,-122.4225776:37.8273895,-122.4224703:37.8272872,-122.4222745:37.8272515,-122.4223041:37.8271965,-122.422198:37.8271676,-122.4222218:37.8271629,-122.4222296:37.8271609,-122.4222329 && last_edit_user_name -> eduaddad || name:pt -> Porto de Sally || last_edit_changeset -> 73895096 || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Sally Port || last_edit_version -> 9 || building -> yes || layer -> 1 +27996743000000 && 37.8269854,-122.421387:37.826924,-122.4214379:37.8268678,-122.4213292:37.8269292,-122.4212783 && last_edit_user_name -> wheelmap_visitor || toilets:wheelchair -> yes || wheelchair -> yes || last_edit_changeset -> 36107842 || last_edit_time -> 1450802771000 || last_edit_user_id -> 290680 || amenity -> toilets || name -> Restrooms || last_edit_version -> 5 || building -> yes +59621862000000 && 37.8255826,-122.4213123:37.8256174,-122.4212683:37.8256432,-122.4211879:37.8257016,-122.421124:37.8257145,-122.4210042:37.8256668,-122.4209:37.8258338,-122.4208844:37.8259249,-122.420994:37.8260155,-122.4211065:37.826353,-122.4213814:37.8263971,-122.421303:37.8262666,-122.4211063:37.8263926,-122.4208604:37.8261154,-122.4205159:37.8257483,-122.4204259:37.825604,-122.4204938:37.8255239,-122.4205283:37.8255273,-122.4205376:37.8254534,-122.4205801:37.8254509,-122.4205729:37.8254152,-122.4205947:37.8252491,-122.4207537:37.8252416,-122.4207709:37.8252492,-122.4207754:37.8252237,-122.4208387:37.8252179,-122.4208352:37.8251895,-122.4209076:37.8251206,-122.4211489:37.8251199,-122.4211532:37.8251206,-122.4211573:37.8251234,-122.4211603:37.8251274,-122.421161:37.8251308,-122.4211587:37.825133,-122.4211545:37.8251455,-122.4211147:37.8251654,-122.4210783:37.8251959,-122.4210144:37.8252167,-122.4209844:37.8252405,-122.4209543:37.825309,-122.4208745:37.82534,-122.4208431:37.8254566,-122.4207839:37.8255579,-122.420737:37.8255848,-122.4207253:37.8256559,-122.4207024:37.8256668,-122.4207837:37.8256273,-122.420809:37.8255835,-122.4208507:37.8255278,-122.4209076:37.8254786,-122.4209673:37.8254408,-122.4210174:37.8254061,-122.4210676:37.8253765,-122.4211244:37.825355,-122.4211833:37.8253845,-122.4212585 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || historic -> ruins || natural -> scree || last_edit_time -> 1546755474000 || last_edit_user_id -> 14293 || name -> Rubble Pile || last_edit_version -> 4 +27996789000000 && 37.826337,-122.4220318:37.8262225,-122.4219119:37.8262519,-122.4218669:37.8262392,-122.4218535:37.8262752,-122.4217985:37.8263609,-122.4218884:37.8263689,-122.4218761:37.8264103,-122.4219195 && last_edit_user_name -> Edward || last_edit_changeset -> 49588642 || historic -> ruins || last_edit_time -> 1497612222000 || last_edit_user_id -> 364 || name -> Warden's House || disused -> yes || last_edit_version -> 6 || wikidata -> Q1142648 || building -> yes || start_date -> 1934 +28021912000000 && 37.8270247,-122.4241767:37.8270394,-122.4241437:37.826976,-122.4240988:37.8269614,-122.4241318 && last_edit_user_name -> will l || last_edit_changeset -> 585867 || last_edit_time -> 1225033203000 || last_edit_user_id -> 35195 || last_edit_version -> 3 || created_by -> Potlatch 0.10e || building -> yes +27996792000000 && 37.8264665,-122.4221448:37.8264081,-122.4220876:37.8264253,-122.4220595:37.8264461,-122.4220798:37.8264516,-122.4220708:37.8264892,-122.4221076 && last_edit_user_name -> wheelmap_visitor || toilets:wheelchair -> yes || wheelchair -> yes || last_edit_changeset -> 36107842 || last_edit_time -> 1450802731000 || last_edit_user_id -> 290680 || amenity -> toilets || fee -> no || name -> Restrooms || last_edit_version -> 9 || building -> yes +27996775000000 && 37.8272677,-122.4226159:37.8273186,-122.4225736:37.8274149,-122.4227594:37.8273641,-122.4228016 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224954042000 || last_edit_user_id -> 35195 || name -> Electric Shop || last_edit_version -> 3 || created_by -> Potlatch 0.10e || building -> yes +27996842000000 && 37.8257351,-122.4221372:37.8257192,-122.4219714:37.8257069,-122.4219113:37.8256076,-122.4217186:37.8256695,-122.4216631:37.8256361,-122.4215908:37.825583,-122.4216015:37.8255055,-122.4214552:37.8254333,-122.4214099:37.8253431,-122.4214434:37.8252912,-122.4215249:37.8253312,-122.4212359:37.8253762,-122.4212779:37.8255871,-122.4213427:37.8256336,-122.4212818:37.8256581,-122.4212001:37.8257184,-122.4211315:37.8257309,-122.4210036:37.8256911,-122.4209166:37.8258018,-122.4209095:37.8258258,-122.420908:37.8259144,-122.421014:37.8260075,-122.421124:37.8263461,-122.4213991:37.8263899,-122.4215455:37.8263646,-122.421571:37.8263469,-122.4215926:37.8261538,-122.4217471:37.8261191,-122.4217666:37.8260852,-122.4217843:37.8260352,-122.4218334:37.8260103,-122.4218633:37.82597,-122.4219276 && area -> yes || last_edit_user_name -> Azlux || last_edit_changeset -> 47452285 || surface -> concrete || last_edit_time -> 1491332262000 || last_edit_user_id -> 5586237 || name -> Parade Ground || highway -> pedestrian || last_edit_version -> 7 +128245367000000 && 37.8263178,-122.4226124:37.8262224,-122.4224956:37.8264405,-122.4222102:37.8265359,-122.422327 && last_edit_user_name -> eduaddad || name:pt -> Bloco de Administração || last_edit_changeset -> 73895096 || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Administration Block || last_edit_version -> 4 || building -> yes +27996721000000 && 37.8280874,-122.4246186:37.8280944,-122.4243261:37.8281802,-122.4242915:37.8282267,-122.4242933:37.8282228,-122.4244599:37.828317,-122.4244635:37.828315,-122.4245438:37.8282302,-122.4245405:37.8282282,-122.4246255:37.828209,-122.4246248:37.828205,-122.4247923:37.8280942,-122.424788:37.8280983,-122.424619 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048024 || last_edit_time -> 1546697601000 || last_edit_user_id -> 14293 || name -> Powerhouse || last_edit_version -> 5 || wikidata -> Q1142612 || building -> yes +24433395000000 && 37.8277813,-122.4249597:37.8278188,-122.4248746:37.8278604,-122.4247795:37.8278732,-122.4247885:37.8278827,-122.4247668:37.827125,-122.4242353:37.8271159,-122.424256:37.8271287,-122.424265:37.8270878,-122.4243583:37.8270503,-122.4244436 && last_edit_user_name -> eduaddad || name:pt -> Construção de novas indústrias || last_edit_changeset -> 73895096 || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> New Industries Building || disused -> yes || last_edit_version -> 7 || building -> yes +24433437000000 && 37.8267327,-122.4231328:37.8268581,-122.4229687:37.827148,-122.4233237:37.8271271,-122.4233509:37.8271737,-122.423408:37.827092,-122.423515:37.8270451,-122.4234575:37.8270223,-122.4234875 && last_edit_user_name -> eduaddad || name:pt -> Sala de jantar || last_edit_changeset -> 73895096 || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Dining Hall || last_edit_version -> 10 || building -> yes +396629347000000 && 37.8276814,-122.4244759:37.8275329,-122.4242608:37.8276187,-122.4241659:37.8277671,-122.424381 && area -> yes || last_edit_user_name -> Johnny Mapperseed || last_edit_changeset -> 37071222 || surface -> paved || last_edit_time -> 1454890655000 || last_edit_user_id -> 1723055 || last_edit_version -> 1 +27996732000000 && 37.8278343,-122.4232984:37.8277424,-122.4233639:37.8276054,-122.4230558:37.8276973,-122.4229903 && last_edit_user_name -> eduaddad || name:pt -> Post Exchange & Officers' Club || last_edit_changeset -> 73895096 || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Post Exchange & Officers' Club || disused -> yes || last_edit_version -> 7 || building -> yes +24433344000000 && 37.8258434,-122.4230873:37.8258231,-122.4229929:37.8257756,-122.4229328:37.8257214,-122.4227869:37.8256265,-122.4227526:37.8255092,-122.4227072:37.8254955,-122.4226802:37.8254881,-122.4226473:37.8254888,-122.4225911:37.8254677,-122.4225276:37.8254698,-122.4225087:37.8254502,-122.4224522:37.8253078,-122.4221432:37.8251465,-122.4218792:37.82511,-122.421809:37.8250854,-122.4217247:37.8250726,-122.4216129:37.8250787,-122.421493:37.8251055,-122.4211995:37.8250846,-122.421176:37.8251705,-122.4208744:37.8251842,-122.4208369:37.8252309,-122.4207283:37.8252773,-122.4206896:37.8253961,-122.4205712:37.8254269,-122.4205455:37.8254308,-122.4205432:37.8254639,-122.4205238:37.8256533,-122.4204309:37.8257327,-122.4203839:37.8258285,-122.4204073:37.825891,-122.4204324:37.8261174,-122.4204776:37.8261966,-122.4205613:37.8262384,-122.420595:37.8262743,-122.4206387:37.826416,-122.420818:37.8264623,-122.4208766:37.8264889,-122.4208486:37.8265016,-122.420877:37.8266576,-122.4210652:37.8267031,-122.4210044:37.8267317,-122.420966:37.8267594,-122.4209676:37.8270426,-122.4214009:37.8270274,-122.4214142:37.826957,-122.421478:37.8271293,-122.4218:37.8270678,-122.4218551:37.827098,-122.4219104:37.8271993,-122.4219715:37.8272468,-122.4220831:37.8272875,-122.4222119:37.827295,-122.4222585:37.8274079,-122.4224738:37.8274786,-122.4226689:37.8277624,-122.422991:37.8278503,-122.4231184:37.8279927,-122.423248:37.8280748,-122.4234665:37.8280889,-122.4235093:37.828102,-122.423556:37.8281056,-122.4236155:37.8281077,-122.4236952:37.8281054,-122.4238064:37.8281905,-122.4239048:37.8282311,-122.4239992:37.8283112,-122.4240915:37.8283586,-122.4241774:37.8283593,-122.424289:37.8283107,-122.4244188:37.8283586,-122.4246237:37.8283518,-122.4247439:37.8283247,-122.4248039:37.8283722,-122.4248898:37.8283586,-122.4249756:37.8283654,-122.4250357:37.8283857,-122.4250958:37.8283247,-122.4252073:37.8283654,-122.425276:37.8283247,-122.4254477:37.8282513,-122.4255674:37.828174,-122.4256104:37.8281195,-122.4256233:37.8280807,-122.4255997:37.8280425,-122.4256157:37.827954,-122.4255445:37.827884,-122.425561:37.8277791,-122.4253253:37.8278093,-122.425264:37.8277688,-122.4251902:37.8277078,-122.4251387:37.8276739,-122.4251215:37.827579,-122.4250357:37.8275443,-122.4250204:37.8275161,-122.4250375:37.827341,-122.4249618:37.8272552,-122.4249189:37.8271703,-122.4248737:37.8271174,-122.4248392:37.8270945,-122.4248205:37.8269523,-122.4246717:37.8268793,-122.424465:37.8267926,-122.4244949:37.8267315,-122.4245207:37.8267112,-122.424555:37.8266999,-122.4245634:37.8266871,-122.4245682:37.8266755,-122.4245682:37.8266638,-122.4245647:37.8266339,-122.4245303:37.8266318,-122.4245057:37.8266027,-122.4244864:37.8265529,-122.4243945:37.826575,-122.4243214:37.826565,-122.4243015:37.8265699,-122.4242874:37.826585,-122.4242512:37.8265692,-122.4242024:37.8265494,-122.4241953:37.8264875,-122.4240229:37.8265183,-122.4239573:37.82653,-122.4238988:37.8265283,-122.423846:37.8264904,-122.4237927:37.8264491,-122.4237511:37.8264056,-122.42373:37.8262488,-122.4237363:37.8261621,-122.4237654:37.8260875,-122.4237053:37.8260265,-122.4235336:37.8259519,-122.4233534:37.8258841,-122.4232762:37.8258705,-122.4231903 && boundary -> national_park || last_edit_changeset -> 71376374 || natural -> coastline || source -> yahoo || gnis:county_id -> 075 || last_edit_user_name -> KaL3288 || gnis:created -> 01/19/1981 || alt_name -> Alcatraz || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Alcatraz Island || gnis:feature_id -> 218080 || gnis:state_id -> 06 || place -> islet || wikipedia -> en:Alcatraz Island || last_edit_version -> 18 || wikidata -> Q131354 || ele -> 37 +232326055000000 && 37.8262097,-122.4221137:37.8261656,-122.422067:37.8261393,-122.4221067:37.8261834,-122.4221535 && last_edit_user_name -> Nautic || last_edit_changeset -> 17214523 || last_edit_time -> 1375620456000 || last_edit_user_id -> 449569 || last_edit_version -> 1 || building -> yes +28000214000000 && 37.8262666,-122.4211063:37.8263971,-122.421303:37.8264185,-122.4213353:37.8266246,-122.4212888:37.8266819,-122.4213756:37.8268112,-122.4216098:37.8268181,-122.421603:37.826957,-122.421478:37.8270274,-122.4214142:37.8270426,-122.4214009:37.8267594,-122.4209676:37.8267317,-122.420966:37.8267031,-122.4210044:37.8266576,-122.4210652:37.8265016,-122.420877:37.8264889,-122.4208486:37.8264623,-122.4208766:37.826416,-122.420818:37.8264021,-122.4208428:37.8263926,-122.4208604 && area -> yes || last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || surface -> concrete || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || name -> Dock || highway -> pedestrian || last_edit_version -> 9 +698078900000000 && 37.8257327,-122.4203839:37.8258285,-122.4204073:37.825891,-122.4204324:37.8261174,-122.4204776:37.8261966,-122.4205613:37.8262384,-122.420595:37.8264623,-122.4208766:37.8264889,-122.4208486:37.8265016,-122.420877:37.8266576,-122.4210652:37.8267317,-122.420966:37.8267594,-122.4209676:37.8270426,-122.4214009:37.8270274,-122.4214142:37.826957,-122.421478:37.8271293,-122.4218:37.8270678,-122.4218551:37.827098,-122.4219104:37.8271993,-122.4219715:37.8272468,-122.4220831:37.8272875,-122.4222119:37.827295,-122.4222585:37.8274079,-122.4224738:37.8274786,-122.4226689:37.8277624,-122.422991:37.8278503,-122.4231184:37.8279927,-122.423248:37.8280748,-122.4234665:37.8280889,-122.4235093:37.828102,-122.423556:37.8281056,-122.4236155:37.8281077,-122.4236952:37.8281054,-122.4238064:37.8281905,-122.4239048:37.8282311,-122.4239992:37.8283112,-122.4240915:37.8283586,-122.4241774:37.8283593,-122.424289:37.8283107,-122.4244188:37.8283586,-122.4246237:37.8283518,-122.4247439:37.8283247,-122.4248039:37.8283722,-122.4248898:37.8283586,-122.4249756:37.8283654,-122.4250357:37.8283857,-122.4250958:37.8283247,-122.4252073:37.8283654,-122.425276:37.8283247,-122.4254477:37.8282513,-122.4255674:37.828174,-122.4256104:37.8281195,-122.4256233:37.8280807,-122.4255997:37.8280425,-122.4256157:37.827954,-122.4255445:37.827884,-122.425561:37.8277791,-122.4253253:37.8278093,-122.425264:37.8277688,-122.4251902:37.8277078,-122.4251387:37.8276739,-122.4251215:37.827579,-122.4250357:37.8275443,-122.4250204:37.8275161,-122.4250375:37.827341,-122.4249618:37.8271703,-122.4248737:37.8271174,-122.4248392:37.8270945,-122.4248205:37.8269523,-122.4246717:37.8268793,-122.424465:37.8267926,-122.4244949:37.8267315,-122.4245207:37.8267112,-122.424555:37.8266999,-122.4245634:37.8266871,-122.4245682:37.8266755,-122.4245682:37.8266638,-122.4245647:37.8266339,-122.4245303:37.8266318,-122.4245057:37.8266027,-122.4244864:37.8265529,-122.4243945:37.826575,-122.4243214:37.826565,-122.4243015:37.8265699,-122.4242874:37.826585,-122.4242512:37.8265692,-122.4242024:37.8265494,-122.4241953:37.8264875,-122.4240229:37.8265183,-122.4239573:37.82653,-122.4238988:37.8265283,-122.423846:37.8264904,-122.4237927:37.8264491,-122.4237511:37.8264056,-122.42373:37.8262488,-122.4237363:37.8261621,-122.4237654:37.8260875,-122.4237053:37.8260265,-122.4235336:37.8259519,-122.4233534:37.8258841,-122.4232762:37.8258705,-122.4231903:37.8258434,-122.4230873:37.8258231,-122.4229929:37.8257756,-122.4229328:37.8257214,-122.4227869:37.8256265,-122.4227526:37.8255092,-122.4227072:37.8254955,-122.4226802:37.8254881,-122.4226473:37.8254888,-122.4225911:37.8254677,-122.4225276:37.8254698,-122.4225087:37.8254502,-122.4224522:37.8253078,-122.4221432:37.8251465,-122.4218792:37.82511,-122.421809:37.8250854,-122.4217247:37.8250726,-122.4216129:37.8250787,-122.421493:37.8251055,-122.4211995:37.8250846,-122.421176:37.8251705,-122.4208744:37.8251842,-122.4208369:37.8252309,-122.4207283:37.8252773,-122.4206896:37.8253961,-122.4205712:37.8254308,-122.4205432:37.8254639,-122.4205238:37.8256533,-122.4204309 && last_edit_user_name -> eduaddad || name:pt -> Ilha de Alcatraz || last_edit_changeset -> 73895096 || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Alcatraz Island || last_edit_version -> 3 || leisure -> park || name:es -> Isla de Alcatraz +128245368000000 && 37.8264062,-122.422042:37.8264517,-122.4219629:37.8265746,-122.4220782:37.8266339,-122.422144:37.8266509,-122.4221225:37.8268299,-122.4223357:37.8268987,-122.4224323:37.826904,-122.4224631:37.8268426,-122.4225275:37.8266731,-122.4223478:37.8264665,-122.4221448:37.8264892,-122.4221076:37.8264516,-122.4220708:37.8264461,-122.4220798:37.8264253,-122.4220595 && last_edit_user_name -> Nautic || last_edit_changeset -> 17223234 || last_edit_time -> 1375682658000 || last_edit_user_id -> 449569 || last_edit_version -> 2 || leisure -> garden +232403925000000 && 37.8263359,-122.422198:37.8263275,-122.4221888:37.8263342,-122.4221796:37.8262984,-122.4221378:37.8262544,-122.4222046:37.8262486,-122.4221987:37.8261921,-122.4222817:37.8262055,-122.4223525:37.8262139,-122.4223532 && last_edit_user_name -> Nautic || last_edit_changeset -> 17223234 || last_edit_time -> 1375682658000 || last_edit_user_id -> 449569 || landuse -> grass || last_edit_version -> 1 +295140461000000 && 37.8271993,-122.4219715:37.827098,-122.4219104:37.8270678,-122.4218551:37.8271293,-122.4218:37.826957,-122.421478:37.8270274,-122.4214142:37.8270426,-122.4214009:37.8267594,-122.4209676:37.8267317,-122.420966:37.8267031,-122.4210044:37.8266576,-122.4210652:37.8265016,-122.420877:37.8264889,-122.4208486:37.8264623,-122.4208766:37.826416,-122.420818:37.8262743,-122.4206387:37.8262384,-122.420595:37.8261966,-122.4205613:37.8261174,-122.4204776:37.825891,-122.4204324:37.8258285,-122.4204073:37.8257327,-122.4203839:37.8256533,-122.4204309:37.8254639,-122.4205238:37.8254269,-122.4205455:37.8253961,-122.4205712:37.8252773,-122.4206896:37.8252309,-122.4207283:37.8251842,-122.4208369:37.8251711,-122.4208726:37.8251705,-122.4208744:37.8250846,-122.421176:37.8251055,-122.4211995:37.8250787,-122.421493:37.8250726,-122.4216129:37.8250854,-122.4217247:37.82511,-122.421809:37.8251465,-122.4218792:37.8253078,-122.4221432:37.8254502,-122.4224522:37.8254698,-122.4225087:37.8254677,-122.4225276:37.8254888,-122.4225911:37.8254881,-122.4226473:37.8254955,-122.4226802:37.8255092,-122.4227072:37.8256265,-122.4227526:37.8257214,-122.4227869:37.8257756,-122.4229328:37.8258231,-122.4229929:37.8258434,-122.4230873:37.8258705,-122.4231903:37.8258841,-122.4232762:37.8259519,-122.4233534:37.8260265,-122.4235336:37.8260875,-122.4237053:37.8261621,-122.4237654:37.8262488,-122.4237363:37.8264056,-122.42373:37.8264491,-122.4237511:37.8264904,-122.4237927:37.8265283,-122.423846:37.82653,-122.4238988:37.8265183,-122.4239573:37.8264875,-122.4240229:37.8265494,-122.4241953:37.8265692,-122.4242024:37.826585,-122.4242512:37.8265699,-122.4242874:37.826565,-122.4243015:37.826575,-122.4243214:37.8265529,-122.4243945:37.8266027,-122.4244864:37.8266318,-122.4245057:37.8266339,-122.4245303:37.8266638,-122.4245647:37.8266755,-122.4245682:37.8266871,-122.4245682:37.8266999,-122.4245634:37.8267112,-122.424555:37.8267315,-122.4245207:37.8267926,-122.4244949:37.8268793,-122.424465:37.8269523,-122.4246717:37.8270945,-122.4248205:37.8271174,-122.4248392:37.8271703,-122.4248737:37.8272552,-122.4249189:37.827341,-122.4249618:37.8275161,-122.4250375:37.8275443,-122.4250204:37.827579,-122.4250357:37.8276739,-122.4251215:37.8277078,-122.4251387:37.8277688,-122.4251902:37.8278093,-122.425264:37.8277791,-122.4253253:37.827884,-122.425561:37.827954,-122.4255445:37.8280425,-122.4256157:37.8280807,-122.4255997:37.8281195,-122.4256233:37.828174,-122.4256104:37.8282513,-122.4255674:37.8283247,-122.4254477:37.8283654,-122.425276:37.8283247,-122.4252073:37.8283857,-122.4250958:37.8283654,-122.4250357:37.8283586,-122.4249756:37.8283722,-122.4248898:37.8283247,-122.4248039:37.8283518,-122.4247439:37.8283586,-122.4246237:37.8283107,-122.4244188:37.8283593,-122.424289:37.8283586,-122.4241774:37.8283112,-122.4240915:37.8282311,-122.4239992:37.8281905,-122.4239048:37.8281054,-122.4238064:37.8281077,-122.4236952:37.8281056,-122.4236155:37.828102,-122.423556:37.8280889,-122.4235093:37.8280748,-122.4234665:37.8279927,-122.423248:37.8278503,-122.4231184:37.8277624,-122.422991:37.8274786,-122.4226689:37.8274079,-122.4224738:37.827295,-122.4222585:37.8272875,-122.4222119:37.8272468,-122.4220831 && boundary -> national_park || last_edit_user_name -> KaL3288 || name:ko -> 앨커트래즈 섬 || last_edit_changeset -> 71376374 || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || admin_level -> 1 || name -> Alcatraz Island (GGNRA) || last_edit_version -> 6 || leisure -> common || park:type -> national_recreational_area || wikidata -> Q131354 +24617219000000 && 37.8270206,-122.4220063:37.8268901,-122.4221255:37.826553,-122.421542:37.826439,-122.4215701:37.826407,-122.421369:37.8266219,-122.4213147 && last_edit_user_name -> Edward || last_edit_changeset -> 49588498 || last_edit_time -> 1497611818000 || last_edit_user_id -> 364 || name -> Building 64 || last_edit_version -> 4 || wikidata -> Q1142634 || building -> yes +# Lines +99202295000000 && 37.8266988,-122.4231768:37.8268332,-122.4233348:37.8266955,-122.4236195:37.8271206,-122.4239856:37.827279,-122.4241222:37.8274396,-122.423822:37.8271835,-122.4235957:37.8272745,-122.42347:37.8270849,-122.4231766:37.8270545,-122.4231082:37.8269623,-122.4229781 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || barrier -> wall || last_edit_time -> 1314664599000 || last_edit_user_id -> 27824 || last_edit_version -> 2 +128245365000000 && 37.8274697,-122.4234167:37.8273627,-122.4231471:37.8272208,-122.4229661:37.8269189,-122.4222928 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || barrier -> wall || last_edit_time -> 1314664590000 || last_edit_user_id -> 27824 || last_edit_version -> 1 +128245369000000 && 37.8272398,-122.4226402:37.8271487,-122.4224752:37.826992,-122.4221507:37.8268924,-122.4221547 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || barrier -> wall || last_edit_time -> 1314664590000 || last_edit_user_id -> 27824 || last_edit_version -> 1 +128245372000000 && 37.8265445,-122.423597:37.8265163,-122.4236856 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || barrier -> fence || last_edit_time -> 1314664591000 || last_edit_user_id -> 27824 || last_edit_version -> 1 +232403927000000 && 37.8277451,-122.4236998:37.8277597,-122.4238259:37.8280206,-122.4242526 && last_edit_user_name -> Nautic || last_edit_changeset -> 17223234 || barrier -> wall || last_edit_time -> 1375682658000 || last_edit_user_id -> 449569 || last_edit_version -> 1 +232403928000000 && 37.8261322,-122.4218812:37.8260594,-122.4220336:37.8260332,-122.4221509:37.8261251,-122.422657 && last_edit_user_name -> Nautic || last_edit_changeset -> 17223234 || barrier -> wall || last_edit_time -> 1375682658000 || last_edit_user_id -> 449569 || last_edit_version -> 1 +232403930000000 && 37.8261435,-122.4220086:37.8260847,-122.4220927:37.8260791,-122.4221136:37.8260777,-122.4221391:37.8260831,-122.4221639:37.8260918,-122.4221829:37.8261314,-122.422226 && last_edit_user_name -> Nautic || last_edit_changeset -> 17223234 || barrier -> wall || last_edit_time -> 1375682658000 || last_edit_user_id -> 449569 || last_edit_version -> 1 +629121016000000 && 37.8263291,-122.4221048:37.8263835,-122.4221473:37.8264374,-122.422186:37.8264945,-122.4222404:37.8265266,-122.4222765 && last_edit_user_name -> Michiel M || last_edit_changeset -> 63050387 || barrier -> fence || last_edit_time -> 1538262194000 || last_edit_user_id -> 9446 || last_edit_version -> 1 +660872606000000 && 37.825136,-122.4212307:37.8251055,-122.4211995:37.8250846,-122.421176:37.8251711,-122.4208726:37.8251842,-122.4208369:37.8252309,-122.4207283:37.8252773,-122.4206896:37.8253961,-122.4205712:37.8254308,-122.4205432:37.8254639,-122.4205238:37.8256533,-122.4204309:37.8257327,-122.4203839:37.8258285,-122.4204073:37.825891,-122.4204324:37.8261174,-122.4204776:37.8261966,-122.4205613:37.8262384,-122.420595:37.8262743,-122.4206387:37.826416,-122.420818:37.8264623,-122.4208766 && last_edit_user_name -> KaL3288 || last_edit_changeset -> 71376374 || barrier -> wall || last_edit_time -> 1560882267000 || last_edit_user_id -> 7134350 || last_edit_version -> 2 +660872945000000 && 37.8280585,-122.4246222:37.8280558,-122.4247123:37.827991,-122.4247105:37.8279795,-122.4247304:37.8279757,-122.4250288:37.8277915,-122.4250161:37.8275772,-122.4248659:37.8271931,-122.4246216:37.8270237,-122.4245035 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063123 || barrier -> fence || last_edit_time -> 1546755783000 || last_edit_user_id -> 14293 || last_edit_version -> 1 +# Points +4553243887000000 && 37.825229,-122.4208047 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755473000 || last_edit_user_id -> 14293 || amenity -> bench || last_edit_version -> 2 +2407548249000000 && 37.8275485,-122.4238341 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66062793 || seamark:landmark:category -> tower || last_edit_time -> 1546753099000 || last_edit_user_id -> 14293 || seamark:name -> Water tower || last_edit_version -> 2 || seamark:type -> landmark +3054493437000000 && 37.8268591,-122.4227493 && gnis:county_name -> San Francisco || wheelchair -> yes || last_edit_changeset -> 46480561 || addr:state -> CA || amenity -> toilets || gnis:reviewed -> no || source -> USGS Geonames || building -> yes || last_edit_user_name -> BizV || toilets:wheelchair -> yes || last_edit_time -> 1488323986000 || last_edit_user_id -> 5404994 || gnis:import_uuid -> 57871b70-0100-4405-bb30-88b2e001a944 || name -> Alcatraz || gnis:feature_id -> 1657175 || last_edit_version -> 4 || ele -> 32 +1526523486000000 && 37.8266704,-122.4208218 && last_edit_user_name -> eduaddad || name:pt -> Terminal de Balsa de Alcatraz || last_edit_changeset -> 73895096 || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || amenity -> ferry_terminal || ferry -> yes || name -> Alcatraz Ferry Terminal || public_transport -> stop_position || last_edit_version -> 7 || operator -> Alcatraz Cruises, LLC +1526523481000000 && 37.8266151,-122.4213351 && last_edit_user_name -> eduaddad || name:pt -> Loja de Presentes Alcatraz || last_edit_changeset -> 73895096 || shop -> gift || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Alcatraz Giftshop || source -> Bing || last_edit_version -> 2 +1417681435000000 && 37.8271206,-122.4239856 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048128 || barrier -> gate || last_edit_time -> 1546697857000 || last_edit_user_id -> 14293 || last_edit_version -> 2 +2113145715000000 && 37.8280442,-122.4255358 && last_edit_changeset -> 27604782 || seamark:light:character -> F || seamark:light:colour -> amber || seamark:light:exhibition -> night || noaa:geohash -> 468dc0f8411d || seamark:fog_signal:period -> 30 || seamark:fog_signal:sequence -> 02.0+(02.0)+02.0+(24.0) || seamark:fog_signal:generation -> automatic || seamark:type -> landmark || last_edit_user_name -> malcolmh || seamark:status -> permanent || last_edit_time -> 1419154709000 || last_edit_user_id -> 128186 || noaa:taghash -> 4577c29d2bcf9219ae4f037344d3ad80dcb37af2 || noaa:lnam -> 13fb0fb7 || seamark:fog_signal:category -> horn || seamark:landmark:conspicuity -> conspicuous || last_edit_version -> 7 || seamark:fog_signal:group -> 2 || seamark:light:category -> floodlight +2407548242000000 && 37.8263372,-122.4221889 && last_edit_user_name -> Nautic || last_edit_changeset -> 17223234 || last_edit_time -> 1375682657000 || last_edit_user_id -> 449569 || fire_hydrant:position -> sidewalk || emergency -> fire_hydrant || fire_hydrant:type -> pillar || last_edit_version -> 1 +5784941541000000 && 37.8265179,-122.4209758 && last_edit_user_name -> randoogle || last_edit_changeset -> 61065995 || last_edit_time -> 1532546136000 || last_edit_user_id -> 245874 || tourism -> picnic_site || last_edit_version -> 1 +307446867000000 && 37.8271132,-122.42221 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224953895000 || last_edit_user_id -> 35195 || last_edit_version -> 1 || created_by -> JOSM +2407548229000000 && 37.8262306,-122.4221503 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755471000 || last_edit_user_id -> 14293 || seamark:name -> Lighthouse || last_edit_version -> 3 || seamark:type -> landmark +3202364309000000 && 37.8270878,-122.4243583 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048024 || last_edit_time -> 1546697600000 || last_edit_user_id -> 14293 || entrance -> yes || last_edit_version -> 2 +1526523442000000 && 37.8253259,-122.4213619 && last_edit_user_name -> Dedus D D || wheelchair -> limited || last_edit_changeset -> 52209203 || last_edit_time -> 1505909499000 || last_edit_user_id -> 4509564 || name -> Alcatraz || tourism -> viewpoint || last_edit_version -> 3 +307446854000000 && 37.8271046,-122.4221259 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224953892000 || last_edit_user_id -> 35195 || last_edit_version -> 1 || created_by -> JOSM +3202364310000000 && 37.8278188,-122.4248746 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66048024 || last_edit_time -> 1546697600000 || last_edit_user_id -> 14293 || entrance -> main || last_edit_version -> 2 +1526527959000000 && 37.8268449,-122.4229523 && last_edit_user_name -> JessAk71 || last_edit_changeset -> 10008969 || last_edit_time -> 1322753403000 || last_edit_user_id -> 381909 || tourism -> information || last_edit_version -> 1 +3202359193000000 && 37.8070389,-122.4041405 && wheelchair -> yes || last_edit_changeset -> 69833614 || addr:state -> CA || amenity -> ferry_terminal || addr:postcode -> 94111 || operator -> Alcatraz Cruises, LLC || addr:city -> San Francisco || name:ca -> Ferry Alcatraz || last_edit_user_name -> gpesquero || toilets:wheelchair -> yes || addr:housenumber -> Pier 33 || last_edit_time -> 1556873987000 || last_edit_user_id -> 3584 || name -> Alcatraz Cruises, LLC || name:en -> Alcatraz Cruises, LLC || addr:street -> Alcatraz Landing || last_edit_version -> 7 || name:zh -> 恶魔岛渡轮 +307446857000000 && 37.8270224,-122.4219712 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224953892000 || last_edit_user_id -> 35195 || last_edit_version -> 1 || created_by -> JOSM +307446836000000 && 37.827774,-122.4249791 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || access -> private || barrier -> gate || last_edit_time -> 1546755472000 || last_edit_user_id -> 14293 || last_edit_version -> 3 +307459622000000 && 37.8279428,-122.4243196 && last_edit_user_name -> Walter Schlögl || last_edit_changeset -> 6060751 || access -> private || barrier -> gate || last_edit_time -> 1287257739000 || last_edit_user_id -> 78656 || last_edit_version -> 5 +1526523433000000 && 37.8262052,-122.4224616 && last_edit_user_name -> wheelmap_visitor || toilets:wheelchair -> yes || wheelchair -> limited || last_edit_changeset -> 58254331 || last_edit_time -> 1524212918000 || last_edit_user_id -> 290680 || tourism -> viewpoint || last_edit_version -> 3 +265562462000000 && 37.827056,-122.4230597 && last_edit_user_name -> eduaddad || name:pt -> Ilha de Alcatraz || wheelchair -> yes || last_edit_changeset -> 73895096 || historic -> yes || last_edit_time -> 1567102630000 || last_edit_user_id -> 7489282 || name -> Alcatraz Island || tourism -> attraction || last_edit_version -> 9 || name:zh -> 恶魔岛,夺命岛 +4553243888000000 && 37.8254858,-122.4205524 && last_edit_user_name -> KindredCoda || last_edit_changeset -> 66063078 || last_edit_time -> 1546755472000 || last_edit_user_id -> 14293 || amenity -> bench || last_edit_version -> 2 +1417681445000000 && 37.826779,-122.4214171 && last_edit_user_name -> phut || last_edit_changeset -> 9163280 || last_edit_time -> 1314664588000 || last_edit_user_id -> 27824 || tourism -> information || last_edit_version -> 1 +307431811000000 && 37.827076,-122.421781 && last_edit_user_name -> nvk || last_edit_changeset -> 33170078 || man_made -> tower || last_edit_time -> 1438920317000 || last_edit_user_id -> 131996 || name -> Guard Tower || last_edit_version -> 3 +307446860000000 && 37.8268908,-122.4217373 && last_edit_user_name -> will l || last_edit_changeset -> 571017 || last_edit_time -> 1224953893000 || last_edit_user_id -> 35195 || last_edit_version -> 1 || created_by -> JOSM +# Relations +9451753000000 && 24433344000000 -> inner -> A && last_edit_user_name -> StenSoft || last_edit_changeset -> 69717377 || name:de -> Bucht von San Francisco || natural -> bay || last_edit_time -> 1556592508000 || last_edit_user_id -> 255936 || name -> San Francisco Bay || wikipedia -> en:San Francisco Bay || type -> multipolygon || last_edit_version -> 2 || name:cs -> Sanfranciský záliv || wikidata -> Q232264 diff --git a/src/test/resources/data/Alcatraz/Alcatraz.osm b/src/test/resources/data/Alcatraz/Alcatraz.osm new file mode 100644 index 0000000000..5aa623809f --- /dev/null +++ b/src/test/resources/data/Alcatraz/Alcatraz.osm @@ -0,0 +1,1037 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/data/ButterflyPark/ButterflyPark.atlas.txt b/src/test/resources/data/ButterflyPark/ButterflyPark.atlas.txt new file mode 100644 index 0000000000..21cea863b7 --- /dev/null +++ b/src/test/resources/data/ButterflyPark/ButterflyPark.atlas.txt @@ -0,0 +1,441 @@ +# Nodes +5138971723000000 && 1.2556477,103.8175322 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707359000 || last_edit_user_id -> 7671613 || last_edit_version -> 3 +5133972846000000 && 1.2557524,103.817218 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707357000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +5175392808000000 && 1.2560357,103.8160113 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707364000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +4733403532000000 && 1.2542395,103.8179569 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +5209288659000000 && 1.2563089,103.8160091 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707364000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 2 +4731977864000000 && 1.2535375,103.8173137 && last_edit_user_name -> FlyTy || last_edit_changeset -> 54417250 || last_edit_time -> 1512603533000 || last_edit_user_id -> 6095805 || last_edit_version -> 2 +5138650779000000 && 1.2551525,103.8170011 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304368000 || last_edit_user_id -> 6346054 || last_edit_version -> 8 +4731913776000000 && 1.2552127,103.816772 && last_edit_user_name -> pushian || last_edit_changeset -> 46831836 || last_edit_time -> 1489476091000 || last_edit_user_id -> 4989626 || last_edit_version -> 2 +1717556458000000 && 1.2555488,103.8161391 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707350000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +5145931993000000 && 1.2560356,103.8161935 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707362000 || last_edit_user_id -> 7671613 || last_edit_version -> 8 +5138650784000000 && 1.2551718,103.8163177 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304368000 || last_edit_user_id -> 6346054 || last_edit_version -> 7 +5146232330000000 && 1.2554904,103.8160242 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707362000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 7 +5175384349000000 && 1.2552509,103.8187571 && last_edit_user_name -> CapAhab || last_edit_changeset -> 53052207 || last_edit_time -> 1508359102000 || last_edit_user_id -> 6172866 || last_edit_version -> 1 +5209288656000000 && 1.2551718,103.8167514 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || last_edit_version -> 1 +4568857132000000 && 1.2555951,103.8166206 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707355000 || last_edit_user_id -> 7671613 || last_edit_version -> 4 +5145931994000000 && 1.2558988,103.8160258 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707362000 || last_edit_user_id -> 7671613 || last_edit_version -> 8 +5138672444000000 && 1.2552652,103.8169729 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || last_edit_version -> 1 +1867526773000000 && 1.2556997,103.8160146 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707351000 || last_edit_user_id -> 7671613 || last_edit_version -> 8 +2558083041000000 && 1.2550414,103.8188556 && last_edit_user_name -> khatulistiwa || last_edit_changeset -> 19189134 || last_edit_time -> 1385784269000 || last_edit_user_id -> 94431 || last_edit_version -> 1 +471491305000000 && 1.256038,103.8164078 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707350000 || last_edit_user_id -> 7671613 || last_edit_version -> 10 +4566570125000000 && 1.2550724,103.8173982 && last_edit_user_name -> pushian || last_edit_changeset -> 52501454 || last_edit_time -> 1506756672000 || last_edit_user_id -> 4989626 || last_edit_version -> 2 +5138710145000000 && 1.2543873,103.819763 && last_edit_user_name -> pushian || last_edit_changeset -> 52497277 || last_edit_time -> 1506732398000 || last_edit_user_id -> 4989626 || last_edit_version -> 1 +2558468525000000 && 1.2558972,103.8186614 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707354000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +5138672449000000 && 1.2554976,103.8160891 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707358000 || last_edit_user_id -> 7671613 || last_edit_version -> 3 +4279963875000000 && 1.2555597,103.815744 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707355000 || last_edit_user_id -> 7671613 || last_edit_version -> 3 +2362722970000000 && 1.2545466,103.8177089 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +5138682822000000 && 1.2551056,103.8185825 && last_edit_user_name -> pushian || last_edit_changeset -> 52497277 || last_edit_time -> 1506732399000 || last_edit_user_id -> 4989626 || last_edit_version -> 2 +4566570124000000 && 1.2550877,103.8174563 && last_edit_user_name -> pushian || last_edit_changeset -> 44586417 || last_edit_time -> 1482399260000 || last_edit_user_id -> 4989626 || last_edit_version -> 1 +5137340959000000 && 1.255278,103.8171476 && last_edit_user_name -> pushian || last_edit_changeset -> 52472107 || last_edit_time -> 1506684670000 || last_edit_user_id -> 4989626 || last_edit_version -> 1 +5138971722000000 && 1.2557114,103.8172062 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707359000 || last_edit_user_id -> 7671613 || last_edit_version -> 3 +4731977868000000 && 1.2536168,103.8168848 && last_edit_user_name -> FlyTy || last_edit_changeset -> 54417250 || last_edit_time -> 1512603533000 || last_edit_user_id -> 6095805 || last_edit_version -> 2 +2663353454000000 && 1.2531882,103.8159215 && last_edit_user_name -> andi9876 || last_edit_changeset -> 20467454 || last_edit_time -> 1391961920000 || last_edit_user_id -> 1256497 || last_edit_version -> 1 +5138693281000000 && 1.2565672,103.8159474 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707358000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +2103671644000000 && 1.2542217,103.8170064 && last_edit_user_name -> alexrudd || last_edit_changeset -> 14593518 || last_edit_time -> 1357787367000 || last_edit_user_id -> 5611 || last_edit_version -> 1 +5138710151000000 && 1.2549677,103.8187549 && last_edit_user_name -> pushian || last_edit_changeset -> 52497277 || last_edit_time -> 1506732398000 || last_edit_user_id -> 4989626 || last_edit_version -> 1 +4733403539000000 && 1.2544035,103.8177439 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +5209288647000000 && 1.2551103,103.8188232 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749607000 || last_edit_user_id -> 6172527 || highway -> crossing || last_edit_version -> 1 +1717556688000000 && 1.2553055,103.8167848 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334481120000 || last_edit_user_id -> 257555 || last_edit_version -> 1 +5138638004000000 && 1.2561024,103.8160087 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707358000 || last_edit_user_id -> 7671613 || last_edit_version -> 11 +2558468523000000 && 1.255212,103.8189822 && last_edit_user_name -> khatulistiwa || last_edit_changeset -> 19195140 || last_edit_time -> 1385821267000 || last_edit_user_id -> 94431 || last_edit_version -> 1 +471491310000000 && 1.255184,103.8163823 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304366000 || last_edit_user_id -> 6346054 || last_edit_version -> 8 +5139421331000000 && 1.2565929,103.8119988 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52685203 || last_edit_time -> 1507300654000 || last_edit_user_id -> 6346054 || last_edit_version -> 3 +6728983057000000 && 1.2565055,103.8183895 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || last_edit_version -> 1 +5209288652000000 && 1.2586232,103.8137176 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || highway -> crossing || last_edit_version -> 1 +5138650785000000 && 1.2552148,103.8161948 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304368000 || last_edit_user_id -> 6346054 || last_edit_version -> 7 +5138672448000000 && 1.2554871,103.816002 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707358000 || last_edit_user_id -> 7671613 || last_edit_version -> 9 +4733403769000000 && 1.2542972,103.8177678 && last_edit_user_name -> pushian || last_edit_changeset -> 46830399 || last_edit_time -> 1489469670000 || last_edit_user_id -> 4989626 || last_edit_version -> 1 +5145848424000000 && 1.2557383,103.8188748 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69415695 || last_edit_time -> 1555818629000 || last_edit_user_id -> 741163 || last_edit_version -> 3 +5209288657000000 && 1.2552052,103.8167682 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || last_edit_version -> 1 +5278186712000000 && 1.255276,103.8169656 && last_edit_user_name -> happy-camper || last_edit_changeset -> 54547742 || last_edit_time -> 1513018302000 || last_edit_user_id -> 6346054 || last_edit_version -> 1 +5137340960000000 && 1.2553364,103.8162287 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || last_edit_version -> 2 +4568857133000000 && 1.2558069,103.8175439 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707355000 || last_edit_user_id -> 7671613 || last_edit_version -> 4 +4733403533000000 && 1.254327,103.8176708 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +5138672445000000 && 1.2552404,103.8170098 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304368000 || last_edit_user_id -> 6346054 || last_edit_version -> 8 +5133972841000000 && 1.2555783,103.8165643 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707357000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +5175066692000000 && 1.2560358,103.8159433 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707363000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +5139019659000000 && 1.2563128,103.8159095 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707359000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +4752344968000000 && 1.2561001,103.8159494 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707355000 || last_edit_user_id -> 7671613 || last_edit_version -> 7 +5209288654000000 && 1.2580669,103.8149089 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707364000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 2 +1717541875000000 && 1.2553003,103.816949 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304366000 || last_edit_user_id -> 6346054 || name -> Imbiah Lookout || highway -> bus_stop || last_edit_version -> 11 +4731913768000000 && 1.2543716,103.8175247 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +4566570120000000 && 1.2550171,103.8175161 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +4731913779000000 && 1.255064,103.817368 && last_edit_user_name -> pushian || last_edit_changeset -> 52243602 || last_edit_time -> 1505999486000 || last_edit_user_id -> 4989626 || entrance -> yes || last_edit_version -> 2 +1717541891000000 && 1.255115,103.8173869 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334480146000 || last_edit_user_id -> 257555 || last_edit_version -> 1 +5286594356000000 && 1.2552897,103.8171688 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || last_edit_version -> 1 +5138638008000000 && 1.256104,103.8166767 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707358000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 9 +5139015613000000 && 1.2561417,103.8166742 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707359000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +2558083054000000 && 1.2566936,103.8183352 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707352000 || last_edit_user_id -> 7671613 || last_edit_version -> 3 +4279963872000000 && 1.2559298,103.8171479 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707354000 || last_edit_user_id -> 7671613 || last_edit_version -> 3 +5139019660000000 && 1.2563075,103.8160443 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707359000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +4752207927000000 && 1.2539855,103.8192619 && last_edit_user_name -> Evandering || last_edit_changeset -> 53115984 || last_edit_time -> 1508541322000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +5175384350000000 && 1.2549476,103.8188959 && last_edit_user_name -> CapAhab || last_edit_changeset -> 53052207 || last_edit_time -> 1508359102000 || last_edit_user_id -> 6172866 || last_edit_version -> 1 +5138693319000000 && 1.2581363,103.8153039 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707359000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +4733410887000000 && 1.2552998,103.8170992 && last_edit_user_name -> pushian || last_edit_changeset -> 46831836 || last_edit_time -> 1489476091000 || last_edit_user_id -> 4989626 || last_edit_version -> 2 +1739490136000000 && 1.2547974,103.817368 && last_edit_user_name -> pushian || last_edit_changeset -> 52501454 || last_edit_time -> 1506756672000 || last_edit_user_id -> 4989626 || last_edit_version -> 2 +4752282593000000 && 1.255332,103.8161785 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304367000 || last_edit_user_id -> 6346054 || last_edit_version -> 7 +5286594351000000 && 1.2552784,103.8173867 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372481000 || last_edit_user_id -> 6095772 || last_edit_version -> 1 +5119743804000000 && 1.2555443,103.815627 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707357000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +4752344963000000 && 1.2553069,103.8160742 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507303876000 || last_edit_user_id -> 6346054 || last_edit_version -> 6 +1422900685000000 && 1.2548287,103.8172656 && last_edit_user_name -> twut || last_edit_changeset -> 10877772 || last_edit_time -> 1330945638000 || last_edit_user_id -> 522960 || last_edit_version -> 2 +4736068646000000 && 1.2545648,103.8177049 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +5209288658000000 && 1.2563109,103.8159577 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707364000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 2 +5209288650000000 && 1.2579967,103.8124881 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || highway -> crossing || last_edit_version -> 1 +4752344943000000 && 1.2580641,103.8152654 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707355000 || last_edit_user_id -> 7671613 || last_edit_version -> 3 +5138672446000000 && 1.2551583,103.8167451 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749609000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +4731913772000000 && 1.2550794,103.8176549 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || last_edit_time -> 1489396888000 || last_edit_user_id -> 4989626 || last_edit_version -> 1 +5146232329000000 && 1.2554965,103.8160775 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707362000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 8 +4733403528000000 && 1.2544761,103.8177277 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +4568857131000000 && 1.2558354,103.8165649 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707355000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +5209288660000000 && 1.2560381,103.8166812 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707364000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 2 +5119744356000000 && 1.257109,103.8152189 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707357000 || last_edit_user_id -> 7671613 || last_edit_version -> 3 +2362722968000000 && 1.2548562,103.8176669 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +2558083058000000 && 1.2567434,103.8184641 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707352000 || last_edit_user_id -> 7671613 || last_edit_version -> 3 +4733074345000000 && 1.2543494,103.8175973 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +1717541894000000 && 1.2551852,103.8171392 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304367000 || last_edit_user_id -> 6346054 || last_edit_version -> 8 +5138693286000000 && 1.2577373,103.8156241 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707358000 || last_edit_user_id -> 7671613 || last_edit_version -> 2 +603563258000000 && 1.25429,103.8170499 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334479744000 || last_edit_user_id -> 257555 || last_edit_version -> 2 +4733403538000000 && 1.2542634,103.8178787 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +1717541881000000 && 1.255232,103.8170032 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304367000 || last_edit_user_id -> 6346054 || last_edit_version -> 8 +4752282589000000 && 1.2552681,103.8162152 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304367000 || last_edit_user_id -> 6346054 || last_edit_version -> 7 +1422900678000000 && 1.2521213,103.8168274 && last_edit_user_name -> twut || last_edit_changeset -> 10877772 || last_edit_time -> 1330945639000 || last_edit_user_id -> 522960 || last_edit_version -> 2 +4731913778000000 && 1.2551596,103.8173956 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || last_edit_time -> 1489396888000 || last_edit_user_id -> 4989626 || last_edit_version -> 1 +2362722969000000 && 1.2542172,103.8180295 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +4731770686000000 && 1.255176,103.8167019 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69405864 || last_edit_time -> 1555776545000 || last_edit_user_id -> 741163 || last_edit_version -> 2 +613981020000000 && 1.2578449,103.8122184 && last_edit_user_name -> CapAhab || last_edit_changeset -> 53046295 || last_edit_time -> 1508347696000 || last_edit_user_id -> 6172866 || last_edit_version -> 4 +4736068642000000 && 1.2543692,103.8174987 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060528000 || last_edit_user_id -> 6172527 || last_edit_version -> 2 +5139015612000000 && 1.2560081,103.8166832 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707359000 || last_edit_user_id -> 7671613 || last_edit_version -> 8 +6596068720000000 && 1.2564458,103.8160077 && last_edit_user_name -> div006 || last_edit_changeset -> 72006528 || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || last_edit_version -> 1 +4733410886000000 && 1.255316,103.8170979 && last_edit_user_name -> pushian || last_edit_changeset -> 46830546 || last_edit_time -> 1489470416000 || last_edit_user_id -> 4989626 || last_edit_version -> 1 +5286594350000000 && 1.2552553,103.8174222 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372481000 || last_edit_user_id -> 6095772 || last_edit_version -> 1 +5145931987000000 && 1.2550605,103.8176481 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || last_edit_time -> 1507060526000 || last_edit_user_id -> 6172527 || last_edit_version -> 1 +# Edges +480175917000001 && 1.2548562,103.8176669:1.2545648,103.8177049 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +-480175917000001 && 1.2545648,103.8177049:1.2548562,103.8176669 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +480175917000002 && 1.2545648,103.8177049:1.2545466,103.8177089 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +-480175917000002 && 1.2545466,103.8177089:1.2545648,103.8177049 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +480632927000001 && 1.2543692,103.8174987:1.2542873,103.8177619:1.2542012,103.8180387:1.2542135,103.8180424:1.2543104,103.8180521:1.2544048,103.8180283:1.2544855,103.817974:1.2545431,103.8178955:1.2545706,103.8178021:1.2545648,103.8177049 && area -> yes || last_edit_user_name -> KingRichardV || last_edit_changeset -> 66408527 || last_edit_time -> 1547760796000 || last_edit_user_id -> 9004577 || highway -> pedestrian || last_edit_version -> 3 +-480632927000001 && 1.2545648,103.8177049:1.2545706,103.8178021:1.2545431,103.8178955:1.2544855,103.817974:1.2544048,103.8180283:1.2543104,103.8180521:1.2542135,103.8180424:1.2542012,103.8180387:1.2542873,103.8177619:1.2543692,103.8174987 && area -> yes || last_edit_user_name -> KingRichardV || last_edit_changeset -> 66408527 || last_edit_time -> 1547760796000 || last_edit_user_id -> 9004577 || highway -> pedestrian || last_edit_version -> 3 +480632927000002 && 1.2545648,103.8177049:1.2545383,103.8176355:1.2544948,103.8175754:1.2544371,103.8175287:1.2543692,103.8174987 && area -> yes || last_edit_user_name -> KingRichardV || last_edit_changeset -> 66408527 || last_edit_time -> 1547760796000 || last_edit_user_id -> 9004577 || highway -> pedestrian || last_edit_version -> 3 +-480632927000002 && 1.2543692,103.8174987:1.2544371,103.8175287:1.2544948,103.8175754:1.2545383,103.8176355:1.2545648,103.8177049 && area -> yes || last_edit_user_name -> KingRichardV || last_edit_changeset -> 66408527 || last_edit_time -> 1547760796000 || last_edit_user_id -> 9004577 || highway -> pedestrian || last_edit_version -> 3 +528898916000000 && 1.2551718,103.8163177:1.2552148,103.8161948 && last_edit_user_name -> rudroju || last_edit_changeset -> 65163560 || psv -> yes || access -> private || last_edit_time -> 1543935529000 || last_edit_user_id -> 7804792 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 4 || foot -> no || oneway -> yes +528240337000000 && 1.2554976,103.8160891:1.2555488,103.8161391 && last_edit_user_name -> pushian || last_edit_changeset -> 52496567 || last_edit_time -> 1506727734000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +-528240337000000 && 1.2555488,103.8161391:1.2554976,103.8160891 && last_edit_user_name -> pushian || last_edit_changeset -> 52496567 || last_edit_time -> 1506727734000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +482407211000001 && 1.2556997,103.8160146:1.2554965,103.8160775 && last_edit_changeset -> 72006528 || surface -> asphalt || oneway -> yes || last_edit_user_name -> div006 || maxheight -> 4.5 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 17 || foot -> no +482407211000002 && 1.2554965,103.8160775:1.2554177,103.8161207:1.255332,103.8161785 && last_edit_changeset -> 72006528 || surface -> asphalt || oneway -> yes || last_edit_user_name -> div006 || maxheight -> 4.5 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 17 || foot -> no +528906080000000 && 1.2551056,103.8185825:1.2550281,103.8186729:1.2549677,103.8187549 && last_edit_user_name -> pushian || last_edit_changeset -> 52497277 || last_edit_time -> 1506732398000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-528906080000000 && 1.2549677,103.8187549:1.2550281,103.8186729:1.2551056,103.8185825 && last_edit_user_name -> pushian || last_edit_changeset -> 52497277 || last_edit_time -> 1506732398000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +730092327000000 && 1.2567434,103.8184641:1.2565883,103.8184816:1.2558972,103.8186614 && last_edit_user_name -> div006 || last_edit_changeset -> 75129398 || last_edit_time -> 1569910184000 || last_edit_user_id -> 8911933 || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || highway -> service || last_edit_version -> 1 +-730092327000000 && 1.2558972,103.8186614:1.2565883,103.8184816:1.2567434,103.8184641 && last_edit_user_name -> div006 || last_edit_changeset -> 75129398 || last_edit_time -> 1569910184000 || last_edit_user_id -> 8911933 || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || highway -> service || last_edit_version -> 1 +480175920000001 && 1.2552127,103.816772:1.2552052,103.8167682 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749610000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +-480175920000001 && 1.2552052,103.8167682:1.2552127,103.816772 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749610000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +480175920000002 && 1.2552052,103.8167682:1.2551718,103.8167514 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749610000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +-480175920000002 && 1.2551718,103.8167514:1.2552052,103.8167682 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749610000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +480175920000003 && 1.2551718,103.8167514:1.2551583,103.8167451 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749610000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +-480175920000003 && 1.2551583,103.8167451:1.2551718,103.8167514 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749610000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +526495379000001 && 1.257109,103.8152189:1.2571849,103.8152445:1.2572649,103.8152494:1.2573433,103.815233:1.2574045,103.8152095:1.2574567,103.8151699:1.2574959,103.8151174:1.2575189,103.815056:1.2575493,103.814908:1.257528,103.81475:1.257518,103.814611:1.257577,103.814375:1.25759,103.814216:1.2576283,103.8140861:1.2577044,103.8139742:1.2577764,103.8139282:1.2578377,103.8138905:1.2578863,103.8138374:1.2579182,103.8137728:1.257931,103.813702:1.2578569,103.8134666:1.2577795,103.8133392:1.2576869,103.8132896:1.2575829,103.8132746:1.25748,103.813296:1.257349,103.813479:1.2570988,103.8137332:1.256836,103.813937:1.256651,103.81398:1.256477,103.814122:1.2559355,103.8149113:1.2559119,103.8149495:1.2559024,103.8149935:1.2559081,103.8150381:1.2559236,103.8152187:1.255918,103.8152487:1.255904,103.815276:1.2558828,103.815298:1.2558562,103.815313:1.2558264,103.8153197:1.2555443,103.815627 && last_edit_user_name -> Evandering || last_edit_changeset -> 52610584 || last_edit_time -> 1507071692000 || last_edit_user_id -> 6172527 || name -> Imbiah Trail || highway -> footway || last_edit_version -> 6 +-526495379000001 && 1.2555443,103.815627:1.2558264,103.8153197:1.2558562,103.815313:1.2558828,103.815298:1.255904,103.815276:1.255918,103.8152487:1.2559236,103.8152187:1.2559081,103.8150381:1.2559024,103.8149935:1.2559119,103.8149495:1.2559355,103.8149113:1.256477,103.814122:1.256651,103.81398:1.256836,103.813937:1.2570988,103.8137332:1.257349,103.813479:1.25748,103.813296:1.2575829,103.8132746:1.2576869,103.8132896:1.2577795,103.8133392:1.2578569,103.8134666:1.257931,103.813702:1.2579182,103.8137728:1.2578863,103.8138374:1.2578377,103.8138905:1.2577764,103.8139282:1.2577044,103.8139742:1.2576283,103.8140861:1.25759,103.814216:1.257577,103.814375:1.257518,103.814611:1.257528,103.81475:1.2575493,103.814908:1.2575189,103.815056:1.2574959,103.8151174:1.2574567,103.8151699:1.2574045,103.8152095:1.2573433,103.815233:1.2572649,103.8152494:1.2571849,103.8152445:1.257109,103.8152189 && last_edit_user_name -> Evandering || last_edit_changeset -> 52610584 || last_edit_time -> 1507071692000 || last_edit_user_id -> 6172527 || name -> Imbiah Trail || highway -> footway || last_edit_version -> 6 +526495379000002 && 1.2555443,103.815627:1.2555597,103.815744 && last_edit_user_name -> Evandering || last_edit_changeset -> 52610584 || last_edit_time -> 1507071692000 || last_edit_user_id -> 6172527 || name -> Imbiah Trail || highway -> footway || last_edit_version -> 6 +-526495379000002 && 1.2555597,103.815744:1.2555443,103.815627 && last_edit_user_name -> Evandering || last_edit_changeset -> 52610584 || last_edit_time -> 1507071692000 || last_edit_user_id -> 6172527 || name -> Imbiah Trail || highway -> footway || last_edit_version -> 6 +533417988000000 && 1.2561001,103.8159494:1.2561024,103.8160087 && last_edit_changeset -> 53046295 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> CapAhab || psv -> yes || last_edit_time -> 1508347695000 || last_edit_user_id -> 6172866 || lanes -> 2 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 1 || foot -> no +260774257000000 && 1.25429,103.8170499:1.2542373,103.8169698:1.2541108,103.8168991:1.2540962,103.8168321:1.2541521,103.8167856:1.2542598,103.8168117:1.2543901,103.8168937:1.2544876,103.816974:1.2545722,103.8170407:1.2546311,103.8170698:1.2547068,103.8169988:1.2547503,103.8168416:1.2547254,103.816778:1.2546819,103.8167156:1.2546806,103.8166255:1.2546301,103.8165553:1.2545483,103.8165437:1.2544863,103.8165667:1.2543994,103.8166001:1.2543239,103.8166277:1.2542436,103.8166373:1.2541914,103.8165958:1.2541752,103.8165262:1.2541992,103.8163935:1.2541619,103.8163022:1.254096,103.8162606:1.2540049,103.8162514:1.2539338,103.8162285:1.2538887,103.8161712:1.2538656,103.816074:1.2538415,103.8160059:1.2537812,103.8159439:1.2536669,103.815895:1.253576,103.8159081:1.2534981,103.8159041:1.253402,103.8158711:1.2532796,103.815885:1.2531882,103.8159215 && last_edit_user_name -> andi9876 || last_edit_changeset -> 20467454 || surface -> asphalt || last_edit_time -> 1391962038000 || last_edit_user_id -> 1256497 || cutting -> no || name -> Luge Dragon Trail || highway -> track || last_edit_version -> 3 || oneway -> yes +639335710000000 && 1.2560357,103.8160113:1.2558988,103.8160258 && last_edit_changeset -> 64005613 || surface -> asphalt || oneway -> yes || last_edit_user_name -> Ramanyam || maxheight -> 4.5 || psv -> yes || last_edit_time -> 1540893405000 || last_edit_user_id -> 7671600 || lanes -> 2 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 1 || foot -> no +526499229000000 && 1.257109,103.8152189:1.2569769,103.8153199:1.2568383,103.815426:1.2565773,103.815507:1.2561883,103.81557:1.2558913,103.815556:1.2557193,103.815654:1.2555597,103.815744 && last_edit_user_name -> tommystyle84 || last_edit_changeset -> 71136817 || last_edit_time -> 1560251836000 || last_edit_user_id -> 438632 || highway -> footway || last_edit_version -> 2 +-526499229000000 && 1.2555597,103.815744:1.2557193,103.815654:1.2558913,103.815556:1.2561883,103.81557:1.2565773,103.815507:1.2568383,103.815426:1.2569769,103.8153199:1.257109,103.8152189 && last_edit_user_name -> tommystyle84 || last_edit_changeset -> 71136817 || last_edit_time -> 1560251836000 || last_edit_user_id -> 438632 || highway -> footway || last_edit_version -> 2 +480175915000001 && 1.2548562,103.8176669:1.2548422,103.8176272:1.2548436,103.817585:1.2548605,103.8175463:1.2548903,103.8175165:1.2549289,103.8174996:1.2549711,103.8174982:1.2550171,103.8175161 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 2 +-480175915000001 && 1.2550171,103.8175161:1.2549711,103.8174982:1.2549289,103.8174996:1.2548903,103.8175165:1.2548605,103.8175463:1.2548436,103.817585:1.2548422,103.8176272:1.2548562,103.8176669 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 2 +480175915000002 && 1.2550171,103.8175161:1.2550511,103.817552:1.2550665,103.817599:1.2550605,103.8176481 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 2 +-480175915000002 && 1.2550605,103.8176481:1.2550665,103.817599:1.2550511,103.817552:1.2550171,103.8175161 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 2 +480175915000003 && 1.2550605,103.8176481:1.2550341,103.8176899:1.2550002,103.8177133:1.2549602,103.817723:1.2549193,103.8177177:1.2548831,103.8176982:1.2548562,103.8176669 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 2 +-480175915000003 && 1.2548562,103.8176669:1.2548831,103.8176982:1.2549193,103.8177177:1.2549602,103.817723:1.2550002,103.8177133:1.2550341,103.8176899:1.2550605,103.8176481 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 2 +528929961000000 && 1.2557114,103.8172062:1.2557291,103.8173139:1.2556477,103.8175322 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606119 || last_edit_time -> 1507058170000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 2 +-528929961000000 && 1.2556477,103.8175322:1.2557291,103.8173139:1.2557114,103.8172062 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606119 || last_edit_time -> 1507058170000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 2 +480339638000000 && 1.2552998,103.8170992:1.255316,103.8170979 && last_edit_user_name -> pushian || last_edit_changeset -> 46830546 || shelter -> yes || last_edit_time -> 1489470416000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480339638000000 && 1.255316,103.8170979:1.2552998,103.8170992 && last_edit_user_name -> pushian || last_edit_changeset -> 46830546 || shelter -> yes || last_edit_time -> 1489470416000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +533417989000000 && 1.256038,103.8164078:1.2560356,103.8161935 && last_edit_changeset -> 66635979 || surface -> asphalt || oneway -> yes || turn:lanes -> left;right || last_edit_user_name -> sravan7 || psv -> yes || last_edit_time -> 1548431890000 || last_edit_user_id -> 8911008 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 4 || foot -> no +528934982000001 && 1.2552784,103.8173867:1.255197,103.8173761:1.2552897,103.8171688 && area -> yes || last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> pedestrian || last_edit_version -> 2 +-528934982000001 && 1.2552897,103.8171688:1.255197,103.8173761:1.2552784,103.8173867 && area -> yes || last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> pedestrian || last_edit_version -> 2 +528934982000002 && 1.2552897,103.8171688:1.2552975,103.8171514:1.2553713,103.8171498:1.2553556,103.8172299:1.2554409,103.8172409:1.2553875,103.8174012:1.2552784,103.8173867 && area -> yes || last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> pedestrian || last_edit_version -> 2 +-528934982000002 && 1.2552784,103.8173867:1.2553875,103.8174012:1.2554409,103.8172409:1.2553556,103.8172299:1.2553713,103.8171498:1.2552975,103.8171514:1.2552897,103.8171688 && area -> yes || last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> pedestrian || last_edit_version -> 2 +716057445000000 && 1.2565055,103.8183895:1.2566936,103.8183352 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || turn:lanes:forward -> left || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || turn:lanes:backward -> through || highway -> service || last_edit_version -> 1 +-716057445000000 && 1.2566936,103.8183352:1.2565055,103.8183895 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || turn:lanes:forward -> left || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || turn:lanes:backward -> through || highway -> service || last_edit_version -> 1 +128869879000000 && 1.2521213,103.8168274:1.2548287,103.8172656 && last_edit_user_name -> sgmapperhh || last_edit_changeset -> 75596691 || last_edit_time -> 1570875303000 || last_edit_user_id -> 10373200 || aerialway -> chair_lift || name -> SkyRide || highway -> raceway || last_edit_version -> 3 || layer -> 2 || oneway -> no +-128869879000000 && 1.2548287,103.8172656:1.2521213,103.8168274 && last_edit_user_name -> sgmapperhh || last_edit_changeset -> 75596691 || last_edit_time -> 1570875303000 || last_edit_user_id -> 10373200 || aerialway -> chair_lift || name -> SkyRide || highway -> raceway || last_edit_version -> 3 || layer -> 2 || oneway -> no +528906082000001 && 1.2553364,103.8162287:1.2553921,103.8161783:1.2554976,103.8160891 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +-528906082000001 && 1.2554976,103.8160891:1.2553921,103.8161783:1.2553364,103.8162287 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +528906082000002 && 1.2554976,103.8160891:1.2557117,103.8160541:1.2558,103.8160758:1.2558767,103.8161247:1.2559336,103.8161957:1.2560102,103.8164384:1.2560081,103.8166832 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +-528906082000002 && 1.2560081,103.8166832:1.2560102,103.8164384:1.2559336,103.8161957:1.2558767,103.8161247:1.2558,103.8160758:1.2557117,103.8160541:1.2554976,103.8160891 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +528906082000003 && 1.2560081,103.8166832:1.2560081,103.8167623:1.256025,103.8168277:1.2561164,103.8169633:1.2562553,103.8171746:1.2562869,103.8172734:1.2562827,103.8173771:1.2562433,103.8174731:1.2560003,103.8179546:1.2558878,103.818134:1.2558222,103.8182134:1.2557207,103.8183124:1.2556211,103.8183751:1.2552244,103.8185142:1.2551056,103.8185825 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +-528906082000003 && 1.2551056,103.8185825:1.2552244,103.8185142:1.2556211,103.8183751:1.2557207,103.8183124:1.2558222,103.8182134:1.2558878,103.818134:1.2560003,103.8179546:1.2562433,103.8174731:1.2562827,103.8173771:1.2562869,103.8172734:1.2562553,103.8171746:1.2561164,103.8169633:1.256025,103.8168277:1.2560081,103.8167623:1.2560081,103.8166832 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +650732280000000 && 1.2558988,103.8160258:1.2558092,103.8160176:1.2556997,103.8160146 && last_edit_user_name -> daram2 || last_edit_changeset -> 67086910 || psv -> yes || surface -> asphalt || last_edit_time -> 1549854189000 || last_edit_user_id -> 8967880 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 2 || foot -> no || oneway -> yes +461380737000001 && 1.2558069,103.8175439:1.2557265,103.8175493:1.2556477,103.8175322 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || shelter -> yes || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 7 +-461380737000001 && 1.2556477,103.8175322:1.2557265,103.8175493:1.2558069,103.8175439 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || shelter -> yes || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 7 +461380737000002 && 1.2556477,103.8175322:1.2552553,103.8174222 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || shelter -> yes || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 7 +-461380737000002 && 1.2552553,103.8174222:1.2556477,103.8175322 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || shelter -> yes || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 7 +461380737000003 && 1.2552553,103.8174222:1.2551596,103.8173956 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || shelter -> yes || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 7 +-461380737000003 && 1.2551596,103.8173956:1.2552553,103.8174222 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || shelter -> yes || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 7 +461380737000004 && 1.2551596,103.8173956:1.255115,103.8173869 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || shelter -> yes || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 7 +-461380737000004 && 1.255115,103.8173869:1.2551596,103.8173956 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || shelter -> yes || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 7 +538105767000000 && 1.2553055,103.8167848:1.2552127,103.816772 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 1 +-538105767000000 && 1.2552127,103.816772:1.2553055,103.8167848 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 1 +526481397000000 && 1.255064,103.817368:1.2550134,103.8173354:1.2549636,103.8173111:1.2549087,103.8172896:1.2548287,103.8172656 && last_edit_user_name -> pushian || last_edit_changeset -> 52243602 || last_edit_time -> 1505999485000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-526481397000000 && 1.2548287,103.8172656:1.2549087,103.8172896:1.2549636,103.8173111:1.2550134,103.8173354:1.255064,103.817368 && last_edit_user_name -> pushian || last_edit_changeset -> 52243602 || last_edit_time -> 1505999485000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +480175918000001 && 1.2543716,103.8175247:1.2544315,103.8175525:1.2544827,103.8175944:1.2545219,103.8176476:1.2545466,103.8177089 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +-480175918000001 && 1.2545466,103.8177089:1.2545219,103.8176476:1.2544827,103.8175944:1.2544315,103.8175525:1.2543716,103.8175247 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +480175918000002 && 1.2545466,103.8177089:1.2545542,103.8178011:1.2545295,103.8178903:1.2544755,103.8179654:1.2543989,103.8180173:1.2543091,103.8180396:1.2542172,103.8180295 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +-480175918000002 && 1.2542172,103.8180295:1.2543091,103.8180396:1.2543989,103.8180173:1.2544755,103.8179654:1.2545295,103.8178903:1.2545542,103.8178011:1.2545466,103.8177089 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +480338714000001 && 1.2543494,103.8175973:1.2544295,103.8176461:1.2544761,103.8177277 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480338714000001 && 1.2544761,103.8177277:1.2544295,103.8176461:1.2543494,103.8175973 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +480338714000002 && 1.2544761,103.8177277:1.254476,103.8178269:1.2544255,103.8179122:1.2543386,103.81796:1.2542395,103.8179569 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480338714000002 && 1.2542395,103.8179569:1.2543386,103.81796:1.2544255,103.8179122:1.254476,103.8178269:1.2544761,103.8177277 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +260777732000000 && 1.2560356,103.8161935:1.256002,103.8161264:1.2559571,103.8160674:1.2558988,103.8160258 && last_edit_user_name -> naik4 || last_edit_changeset -> 65020111 || psv -> yes || surface -> asphalt || last_edit_time -> 1543515811000 || last_edit_user_id -> 7671613 || lanes -> 1 || noname -> yes || highway -> unclassified || last_edit_version -> 8 || foot -> no || oneway -> yes +428880136000001 && 1.2559298,103.8171479:1.2558354,103.8165649 && horse -> no || last_edit_user_name -> MyWayOrNoHighway || last_edit_changeset -> 45230182 || bicycle -> no || surface -> metal || last_edit_time -> 1484608695000 || last_edit_user_id -> 4351550 || bridge -> yes || highway -> path || last_edit_version -> 3 || layer -> 1 +-428880136000001 && 1.2558354,103.8165649:1.2559298,103.8171479 && horse -> no || last_edit_user_name -> MyWayOrNoHighway || last_edit_changeset -> 45230182 || bicycle -> no || surface -> metal || last_edit_time -> 1484608695000 || last_edit_user_id -> 4351550 || bridge -> yes || highway -> path || last_edit_version -> 3 || layer -> 1 +428880136000002 && 1.2558354,103.8165649:1.2558247,103.8165046:1.2556903,103.8161419:1.2555597,103.815744 && horse -> no || last_edit_user_name -> MyWayOrNoHighway || last_edit_changeset -> 45230182 || bicycle -> no || surface -> metal || last_edit_time -> 1484608695000 || last_edit_user_id -> 4351550 || bridge -> yes || highway -> path || last_edit_version -> 3 || layer -> 1 +-428880136000002 && 1.2555597,103.815744:1.2556903,103.8161419:1.2558247,103.8165046:1.2558354,103.8165649 && horse -> no || last_edit_user_name -> MyWayOrNoHighway || last_edit_changeset -> 45230182 || bicycle -> no || surface -> metal || last_edit_time -> 1484608695000 || last_edit_user_id -> 4351550 || bridge -> yes || highway -> path || last_edit_version -> 3 || layer -> 1 +546165469000000 && 1.2553003,103.816949:1.255276,103.8169656 && last_edit_user_name -> happy-camper || last_edit_changeset -> 54547742 || last_edit_time -> 1513018302000 || last_edit_user_id -> 6346054 || covered -> yes || highway -> footway || last_edit_version -> 1 +-546165469000000 && 1.255276,103.8169656:1.2553003,103.816949 && last_edit_user_name -> happy-camper || last_edit_changeset -> 54547742 || last_edit_time -> 1513018302000 || last_edit_user_id -> 6346054 || covered -> yes || highway -> footway || last_edit_version -> 1 +528935623000001 && 1.2563128,103.8159095:1.2563109,103.8159577 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +-528935623000001 && 1.2563109,103.8159577:1.2563128,103.8159095 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +528935623000002 && 1.2563109,103.8159577:1.2563089,103.8160091 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +-528935623000002 && 1.2563089,103.8160091:1.2563109,103.8159577 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +528935623000003 && 1.2563089,103.8160091:1.2563075,103.8160443 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +-528935623000003 && 1.2563075,103.8160443:1.2563089,103.8160091 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +528240340000001 && 1.2552148,103.8161948:1.2552008,103.8161773:1.2551925,103.8161565:1.2551907,103.8161341:1.2551953,103.8161123:1.2552062,103.8160927:1.2552222,103.8160771:1.2552421,103.8160668:1.2552641,103.8160628:1.2552864,103.8160653:1.2553069,103.8160742 && junction -> roundabout || last_edit_user_name -> harishpuppala || last_edit_changeset -> 66895408 || psv -> yes || surface -> asphalt || last_edit_time -> 1549266975000 || last_edit_user_id -> 7446646 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 11 || foot -> no +528240340000002 && 1.2553069,103.8160742:1.2553223,103.8160867:1.2553338,103.8161027:1.2553409,103.8161212:1.255343,103.8161408:1.25534,103.8161604:1.255332,103.8161785 && junction -> roundabout || last_edit_user_name -> harishpuppala || last_edit_changeset -> 66895408 || psv -> yes || surface -> asphalt || last_edit_time -> 1549266975000 || last_edit_user_id -> 7446646 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 11 || foot -> no +528240340000003 && 1.255332,103.8161785:1.2553153,103.8161978:1.2552932,103.8162105:1.2552681,103.8162152 && junction -> roundabout || last_edit_user_name -> harishpuppala || last_edit_changeset -> 66895408 || psv -> yes || surface -> asphalt || last_edit_time -> 1549266975000 || last_edit_user_id -> 7446646 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 11 || foot -> no +528240340000004 && 1.2552681,103.8162152:1.2552487,103.8162131:1.2552306,103.8162061:1.2552148,103.8161948 && junction -> roundabout || last_edit_user_name -> harishpuppala || last_edit_changeset -> 66895408 || psv -> yes || surface -> asphalt || last_edit_time -> 1549266975000 || last_edit_user_id -> 7446646 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 11 || foot -> no +547131784000001 && 1.255278,103.8171476:1.2552897,103.8171688 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 1 +-547131784000001 && 1.2552897,103.8171688:1.255278,103.8171476 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 1 +547131784000002 && 1.2552897,103.8171688:1.2553015,103.8172019:1.2553088,103.8172509:1.2553055,103.8173052:1.2552954,103.8173488:1.2552784,103.8173867 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 1 +-547131784000002 && 1.2552784,103.8173867:1.2552954,103.8173488:1.2553055,103.8173052:1.2553088,103.8172509:1.2553015,103.8172019:1.2552897,103.8171688 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 1 +547131784000003 && 1.2552784,103.8173867:1.2552553,103.8174222 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 1 +-547131784000003 && 1.2552553,103.8174222:1.2552784,103.8173867 && last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> footway || last_edit_version -> 1 +526495380000000 && 1.2555597,103.815744:1.2554913,103.8158824:1.2554871,103.816002 && last_edit_user_name -> Evandering || last_edit_changeset -> 52610966 || surface -> asphalt || last_edit_time -> 1507074076000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +-526495380000000 && 1.2554871,103.816002:1.2554913,103.8158824:1.2555597,103.815744 && last_edit_user_name -> Evandering || last_edit_changeset -> 52610966 || surface -> asphalt || last_edit_time -> 1507074076000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +538105768000001 && 1.2561024,103.8160087:1.256104,103.8166767 && last_edit_changeset -> 64183540 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> Neela1 || psv -> yes || last_edit_time -> 1541386415000 || last_edit_user_id -> 7795604 || lanes -> 2 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 2 || foot -> no +538105768000002 && 1.256104,103.8166767:1.2561072,103.8167292:1.256126,103.8167828:1.2562006,103.8168992:1.2563315,103.8171142:1.2563714,103.8172177:1.2563864,103.8173204:1.2563581,103.8174425:1.256075,103.8179538:1.2559849,103.8181156:1.2559018,103.8182311:1.2558086,103.8183251:1.2556682,103.8184334:1.2552383,103.8185969:1.2551858,103.8186349:1.2550941,103.8187449:1.2550414,103.8188556 && last_edit_changeset -> 64183540 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> Neela1 || psv -> yes || last_edit_time -> 1541386415000 || last_edit_user_id -> 7795604 || lanes -> 2 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 2 || foot -> no +702400958000001 && 1.2564458,103.8160077:1.2563089,103.8160091 && last_edit_changeset -> 72006528 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || turn:lanes -> left;through || last_edit_user_name -> div006 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 1 || foot -> no +702400958000002 && 1.2563089,103.8160091:1.2561024,103.8160087 && last_edit_changeset -> 72006528 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || turn:lanes -> left;through || last_edit_user_name -> div006 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 1 || foot -> no +530020436000001 && 1.2551525,103.8170011:1.2551718,103.816993:1.2551926,103.8169906:1.2552132,103.8169941:1.255232,103.8170032 && junction -> roundabout || last_edit_user_name -> udaykiran3 || last_edit_changeset -> 73290653 || psv -> yes || access -> private || surface -> asphalt || last_edit_time -> 1565674130000 || last_edit_user_id -> 7671560 || lanes -> 1 || highway -> unclassified || last_edit_version -> 9 || foot -> no +530020436000002 && 1.255232,103.8170032:1.2552404,103.8170098 && junction -> roundabout || last_edit_user_name -> udaykiran3 || last_edit_changeset -> 73290653 || psv -> yes || access -> private || surface -> asphalt || last_edit_time -> 1565674130000 || last_edit_user_id -> 7671560 || lanes -> 1 || highway -> unclassified || last_edit_version -> 9 || foot -> no +530020436000003 && 1.2552404,103.8170098:1.2552549,103.8170276:1.2552632,103.8170491:1.2552646,103.817072:1.255259,103.8170942:1.2552468,103.8171137:1.2552293,103.8171286:1.2552081,103.8171373:1.2551852,103.8171392 && junction -> roundabout || last_edit_user_name -> udaykiran3 || last_edit_changeset -> 73290653 || psv -> yes || access -> private || surface -> asphalt || last_edit_time -> 1565674130000 || last_edit_user_id -> 7671560 || lanes -> 1 || highway -> unclassified || last_edit_version -> 9 || foot -> no +530020436000004 && 1.2551852,103.8171392:1.2551593,103.8171325:1.2551374,103.817117:1.2551225,103.8170949:1.2551163,103.8170688:1.2551198,103.8170423:1.2551324,103.8170187:1.2551525,103.8170011 && junction -> roundabout || last_edit_user_name -> udaykiran3 || last_edit_changeset -> 73290653 || psv -> yes || access -> private || surface -> asphalt || last_edit_time -> 1565674130000 || last_edit_user_id -> 7671560 || lanes -> 1 || highway -> unclassified || last_edit_version -> 9 || foot -> no +528901320000001 && 1.255276,103.8169656:1.2552652,103.8169729 && last_edit_user_name -> happy-camper || last_edit_changeset -> 54547742 || last_edit_time -> 1513018304000 || last_edit_user_id -> 6346054 || highway -> footway || last_edit_version -> 2 +-528901320000001 && 1.2552652,103.8169729:1.255276,103.8169656 && last_edit_user_name -> happy-camper || last_edit_changeset -> 54547742 || last_edit_time -> 1513018304000 || last_edit_user_id -> 6346054 || highway -> footway || last_edit_version -> 2 +528901320000002 && 1.2552652,103.8169729:1.2552404,103.8170098 && last_edit_user_name -> happy-camper || last_edit_changeset -> 54547742 || last_edit_time -> 1513018304000 || last_edit_user_id -> 6346054 || highway -> footway || last_edit_version -> 2 +-528901320000002 && 1.2552404,103.8170098:1.2552652,103.8169729 && last_edit_user_name -> happy-camper || last_edit_changeset -> 54547742 || last_edit_time -> 1513018304000 || last_edit_user_id -> 6346054 || highway -> footway || last_edit_version -> 2 +639335779000000 && 1.2560357,103.8160113:1.2560358,103.8159433 && last_edit_changeset -> 65016111 || surface -> asphalt || noname -> yes || oneway -> yes || last_edit_user_name -> sai_sri || psv -> yes || last_edit_time -> 1543508722000 || last_edit_user_id -> 8911951 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 2 || foot -> no +478164185000001 && 1.2549476,103.8188959:1.2550546,103.8187039:1.255092,103.818644:1.2551506,103.8185862:1.2552384,103.8185377:1.255653,103.818386:1.2557393,103.818332:1.2558563,103.8182142:1.2559241,103.8181233:1.2560288,103.8179454:1.2563107,103.817427:1.2563238,103.8173145:1.2563115,103.8172383:1.256273,103.8171451:1.2561336,103.8169394:1.256075,103.8168593:1.2560489,103.8168185:1.2560381,103.8167676:1.2560381,103.8166812 && last_edit_changeset -> 64183540 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> Neela1 || psv -> yes || last_edit_time -> 1541386415000 || last_edit_user_id -> 7795604 || lanes -> 2 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 13 || foot -> no +478164185000002 && 1.2560381,103.8166812:1.256038,103.8164078 && last_edit_changeset -> 64183540 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> Neela1 || psv -> yes || last_edit_time -> 1541386415000 || last_edit_user_id -> 7795604 || lanes -> 2 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 13 || foot -> no +482416616000001 && 1.2561001,103.8159494:1.2563109,103.8159577 && last_edit_changeset -> 72006528 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> div006 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 6 || foot -> no +482416616000002 && 1.2563109,103.8159577:1.2566241,103.8159701:1.2567884,103.8159634:1.256968,103.815956:1.2572858,103.8158836:1.2574293,103.8158226:1.2577725,103.8156348:1.257855,103.8155832:1.2579602,103.8154578:1.2580641,103.8152654 && last_edit_changeset -> 72006528 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> div006 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 6 || foot -> no +639335711000000 && 1.2560358,103.8159433:1.2561001,103.8159494 && last_edit_changeset -> 72006528 || surface -> asphalt || oneway -> yes || last_edit_user_name -> div006 || maxheight -> 4.5 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 2 || foot -> no +480175926000000 && 1.255064,103.817368:1.2550724,103.8173982 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || last_edit_time -> 1489396889000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480175926000000 && 1.2550724,103.8173982:1.255064,103.817368 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || last_edit_time -> 1489396889000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +162005451000000 && 1.2547974,103.817368:1.2548287,103.8172656 && last_edit_user_name -> cboothroyd || last_edit_changeset -> 11491444 || last_edit_time -> 1336070510000 || last_edit_user_id -> 255802 || highway -> path || last_edit_version -> 1 +-162005451000000 && 1.2548287,103.8172656:1.2547974,103.817368 && last_edit_user_name -> cboothroyd || last_edit_changeset -> 11491444 || last_edit_time -> 1336070510000 || last_edit_user_id -> 255802 || highway -> path || last_edit_version -> 1 +480338771000000 && 1.2544035,103.8177439:1.2542972,103.8177678 && last_edit_user_name -> pushian || last_edit_changeset -> 46830399 || last_edit_time -> 1489469670000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480338771000000 && 1.2542972,103.8177678:1.2544035,103.8177439 && last_edit_user_name -> pushian || last_edit_changeset -> 46830399 || last_edit_time -> 1489469670000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +480183445000001 && 1.2543716,103.8175247:1.2543692,103.8174987 && last_edit_user_name -> pushian || last_edit_changeset -> 46868015 || last_edit_time -> 1489581874000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +-480183445000001 && 1.2543692,103.8174987:1.2543716,103.8175247 && last_edit_user_name -> pushian || last_edit_changeset -> 46868015 || last_edit_time -> 1489581874000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +480183445000002 && 1.2543692,103.8174987:1.2543526,103.8174381:1.2537653,103.8172369:1.25365,103.8173683:1.2535375,103.8173137 && last_edit_user_name -> pushian || last_edit_changeset -> 46868015 || last_edit_time -> 1489581874000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +-480183445000002 && 1.2535375,103.8173137:1.25365,103.8173683:1.2537653,103.8172369:1.2543526,103.8174381:1.2543692,103.8174987 && last_edit_user_name -> pushian || last_edit_changeset -> 46868015 || last_edit_time -> 1489581874000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +529787039000000 && 1.2550794,103.8176549:1.2550605,103.8176481 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060527000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 1 +-529787039000000 && 1.2550605,103.8176481:1.2550794,103.8176549 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060527000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 1 +480338716000001 && 1.2545466,103.8177089:1.2544761,103.8177277 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480338716000001 && 1.2544761,103.8177277:1.2545466,103.8177089 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +480338716000002 && 1.2544761,103.8177277:1.2544035,103.8177439 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480338716000002 && 1.2544035,103.8177439:1.2544761,103.8177277 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +480175924000000 && 1.2550877,103.8174563:1.2551596,103.8173956 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || shelter -> yes || last_edit_time -> 1489396889000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480175924000000 && 1.2551596,103.8173956:1.2550877,103.8174563 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || shelter -> yes || last_edit_time -> 1489396889000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +528898917000001 && 1.2551525,103.8170011:1.2551719,103.8169196:1.2551763,103.8168342:1.2551718,103.8167514 && last_edit_changeset -> 65163560 || access -> private || surface -> asphalt || oneway -> yes || last_edit_user_name -> rudroju || psv -> yes || last_edit_time -> 1543935529000 || last_edit_user_id -> 7804792 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 10 || foot -> no +528898917000002 && 1.2551718,103.8167514:1.255171,103.8167364:1.2551466,103.8165992:1.2551218,103.8164965:1.2551198,103.8164596:1.2551273,103.8164194:1.2551718,103.8163177 && last_edit_changeset -> 65163560 || access -> private || surface -> asphalt || oneway -> yes || last_edit_user_name -> rudroju || psv -> yes || last_edit_time -> 1543935529000 || last_edit_user_id -> 7804792 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 10 || foot -> no +528904857000000 && 1.2577373,103.8156241:1.2573982,103.8158064:1.2572742,103.8158574:1.2569612,103.8159354:1.2567292,103.8159524:1.2565672,103.8159474 && last_edit_user_name -> pushian || last_edit_changeset -> 52497070 || last_edit_time -> 1506731022000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-528904857000000 && 1.2565672,103.8159474:1.2567292,103.8159524:1.2569612,103.8159354:1.2572742,103.8158574:1.2573982,103.8158064:1.2577373,103.8156241 && last_edit_user_name -> pushian || last_edit_changeset -> 52497070 || last_edit_time -> 1506731022000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +528900905000001 && 1.2551852,103.8171392:1.2551768,103.8172073:1.2551546,103.8172737:1.255115,103.8173869 && last_edit_changeset -> 73567273 || surface -> asphalt || turn:lanes:forward -> through || lanes:forward -> 1 || lanes:backward -> 1 || last_edit_user_name -> pk_ravi || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || lanes -> 2 || name -> Imbiah Road || turn:lanes:backward -> left || highway -> service || last_edit_version -> 3 || foot -> no +-528900905000001 && 1.255115,103.8173869:1.2551546,103.8172737:1.2551768,103.8172073:1.2551852,103.8171392 && last_edit_changeset -> 73567273 || surface -> asphalt || turn:lanes:forward -> through || lanes:forward -> 1 || lanes:backward -> 1 || last_edit_user_name -> pk_ravi || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || lanes -> 2 || name -> Imbiah Road || turn:lanes:backward -> left || highway -> service || last_edit_version -> 3 || foot -> no +528900905000002 && 1.255115,103.8173869:1.2550724,103.8173982 && last_edit_changeset -> 73567273 || surface -> asphalt || turn:lanes:forward -> through || lanes:forward -> 1 || lanes:backward -> 1 || last_edit_user_name -> pk_ravi || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || lanes -> 2 || name -> Imbiah Road || turn:lanes:backward -> left || highway -> service || last_edit_version -> 3 || foot -> no +-528900905000002 && 1.2550724,103.8173982:1.255115,103.8173869 && last_edit_changeset -> 73567273 || surface -> asphalt || turn:lanes:forward -> through || lanes:forward -> 1 || lanes:backward -> 1 || last_edit_user_name -> pk_ravi || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || lanes -> 2 || name -> Imbiah Road || turn:lanes:backward -> left || highway -> service || last_edit_version -> 3 || foot -> no +528902956000000 && 1.2552509,103.8187571:1.2553881,103.818717:1.2557658,103.8186065:1.2560674,103.8185183:1.2563658,103.8184311:1.2565055,103.8183895 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || highway -> service || last_edit_version -> 9 +-528902956000000 && 1.2565055,103.8183895:1.2563658,103.8184311:1.2560674,103.8185183:1.2557658,103.8186065:1.2553881,103.818717:1.2552509,103.8187571 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || highway -> service || last_edit_version -> 9 +528240338000001 && 1.2555783,103.8165643:1.2555951,103.8166206 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606119 || last_edit_time -> 1507058169000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +-528240338000001 && 1.2555951,103.8166206:1.2555783,103.8165643 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606119 || last_edit_time -> 1507058169000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +528240338000002 && 1.2555951,103.8166206:1.2555981,103.816645:1.2556071,103.816852:1.2556842,103.8171013:1.2557114,103.8172062 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606119 || last_edit_time -> 1507058169000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +-528240338000002 && 1.2557114,103.8172062:1.2556842,103.8171013:1.2556071,103.816852:1.2555981,103.816645:1.2555951,103.8166206 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606119 || last_edit_time -> 1507058169000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +528240338000003 && 1.2557114,103.8172062:1.2557524,103.817218 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606119 || last_edit_time -> 1507058169000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +-528240338000003 && 1.2557524,103.817218:1.2557114,103.8172062 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606119 || last_edit_time -> 1507058169000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +461110977000001 && 1.2550171,103.8175161:1.2550877,103.8174563 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || shelter -> yes || last_edit_time -> 1489396889000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +-461110977000001 && 1.2550877,103.8174563:1.2550171,103.8175161 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || shelter -> yes || last_edit_time -> 1489396889000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +461110977000002 && 1.2550877,103.8174563:1.2550724,103.8173982 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || shelter -> yes || last_edit_time -> 1489396889000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +-461110977000002 && 1.2550724,103.8173982:1.2550877,103.8174563 && last_edit_user_name -> pushian || last_edit_changeset -> 46806385 || shelter -> yes || last_edit_time -> 1489396889000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +482407210000000 && 1.2552681,103.8162152:1.255184,103.8163823 && last_edit_user_name -> anu_smita || last_edit_changeset -> 67122864 || psv -> yes || access -> private || last_edit_time -> 1549957185000 || last_edit_user_id -> 8967885 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 7 || foot -> no || oneway -> yes +128869880000001 && 1.2547974,103.817368:1.2546697,103.8173603:1.2545321,103.8173245:1.2543927,103.8171623:1.25429,103.8170499 && last_edit_user_name -> pushian || last_edit_changeset -> 52501454 || surface -> asphalt || last_edit_time -> 1506756672000 || last_edit_user_id -> 4989626 || cutting -> no || name -> Luge Trail || highway -> track || last_edit_version -> 5 || oneway -> yes +128869880000002 && 1.25429,103.8170499:1.2542217,103.8170064 && last_edit_user_name -> pushian || last_edit_changeset -> 52501454 || surface -> asphalt || last_edit_time -> 1506756672000 || last_edit_user_id -> 4989626 || cutting -> no || name -> Luge Trail || highway -> track || last_edit_version -> 5 || oneway -> yes +528246258000000 && 1.2539855,103.8192619:1.2541076,103.8192021:1.2543857,103.8190694:1.254434,103.8190868:1.2545768,103.8190547:1.2546206,103.8190266:1.2547068,103.8189715:1.2548362,103.8188213:1.2549616,103.8186758:1.2550313,103.8186302:1.2551056,103.8185825 && last_edit_user_name -> Evandering || last_edit_changeset -> 53115984 || last_edit_time -> 1508541322000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +-528246258000000 && 1.2551056,103.8185825:1.2550313,103.8186302:1.2549616,103.8186758:1.2548362,103.8188213:1.2547068,103.8189715:1.2546206,103.8190266:1.2545768,103.8190547:1.254434,103.8190868:1.2543857,103.8190694:1.2541076,103.8192021:1.2539855,103.8192619 && last_edit_user_name -> Evandering || last_edit_changeset -> 53115984 || last_edit_time -> 1508541322000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 3 +646425420000000 && 1.2550724,103.8173982:1.2549776,103.8174195:1.2547564,103.8174721:1.2545781,103.8174628:1.2544446,103.8173782:1.2542019,103.8171228:1.2540528,103.8170529:1.253656,103.8168978:1.2536168,103.8168848 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || surface -> asphalt || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || lanes -> 2 || lanes:forward -> 1 || name -> Imbiah Road || lanes:backward -> 1 || highway -> service || last_edit_version -> 2 +-646425420000000 && 1.2536168,103.8168848:1.253656,103.8168978:1.2540528,103.8170529:1.2542019,103.8171228:1.2544446,103.8173782:1.2545781,103.8174628:1.2547564,103.8174721:1.2549776,103.8174195:1.2550724,103.8173982 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || surface -> asphalt || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || lanes -> 2 || lanes:forward -> 1 || name -> Imbiah Road || lanes:backward -> 1 || highway -> service || last_edit_version -> 2 +528901322000001 && 1.2554871,103.816002:1.2554904,103.8160242 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749612000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +-528901322000001 && 1.2554904,103.8160242:1.2554871,103.816002 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749612000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +528901322000002 && 1.2554904,103.8160242:1.2554965,103.8160775 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749612000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +-528901322000002 && 1.2554965,103.8160775:1.2554904,103.8160242 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749612000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +528901322000003 && 1.2554965,103.8160775:1.2554976,103.8160891 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749612000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +-528901322000003 && 1.2554976,103.8160891:1.2554965,103.8160775 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749612000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 3 +249144132000000 && 1.2558972,103.8186614:1.2556102,103.8187361:1.2554232,103.8187864:1.255335,103.8188441:1.2552442,103.8189425:1.255212,103.8189822 && last_edit_user_name -> div006 || last_edit_changeset -> 75129398 || last_edit_time -> 1569910184000 || last_edit_user_id -> 8911933 || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || highway -> service || last_edit_version -> 6 +-249144132000000 && 1.255212,103.8189822:1.2552442,103.8189425:1.255335,103.8188441:1.2554232,103.8187864:1.2556102,103.8187361:1.2558972,103.8186614 && last_edit_user_name -> div006 || last_edit_changeset -> 75129398 || last_edit_time -> 1569910184000 || last_edit_user_id -> 8911933 || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || highway -> service || last_edit_version -> 6 +480338715000001 && 1.254327,103.8176708:1.2543748,103.8176974:1.2544035,103.8177439 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480338715000001 && 1.2544035,103.8177439:1.2543748,103.8176974:1.254327,103.8176708 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +480338715000002 && 1.2544035,103.8177439:1.2544049,103.8178033:1.254375,103.8178545:1.2543226,103.8178824:1.2542634,103.8178787 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +-480338715000002 && 1.2542634,103.8178787:1.2543226,103.8178824:1.254375,103.8178545:1.2544049,103.8178033:1.2544035,103.8177439 && last_edit_user_name -> pushian || last_edit_changeset -> 46830384 || last_edit_time -> 1489469612000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 1 +482416618000001 && 1.2553069,103.8160742:1.2553621,103.8160732:1.2554045,103.8160631:1.2554904,103.8160242 && last_edit_changeset -> 72006528 || surface -> asphalt || oneway -> yes || turn:lanes -> through;right || last_edit_user_name -> div006 || maxheight -> 4.5 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 13 || foot -> no +482416618000002 && 1.2554904,103.8160242:1.2556619,103.815964:1.2557628,103.8159365:1.2558724,103.8159277:1.2560358,103.8159433 && last_edit_changeset -> 72006528 || surface -> asphalt || oneway -> yes || turn:lanes -> through;right || last_edit_user_name -> div006 || maxheight -> 4.5 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 13 || foot -> no +533417990000000 && 1.2560356,103.8161935:1.2560357,103.8160113 && last_edit_user_name -> naik4 || last_edit_changeset -> 65010043 || psv -> yes || surface -> asphalt || last_edit_time -> 1543497734000 || last_edit_user_id -> 7671613 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 4 || foot -> no || oneway -> yes +480324970000001 && 1.2543716,103.8175247:1.2543494,103.8175973 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +-480324970000001 && 1.2543494,103.8175973:1.2543716,103.8175247 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +480324970000002 && 1.2543494,103.8175973:1.254327,103.8176708 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +-480324970000002 && 1.254327,103.8176708:1.2543494,103.8175973 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +480324970000003 && 1.254327,103.8176708:1.2542972,103.8177678 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +-480324970000003 && 1.2542972,103.8177678:1.254327,103.8176708 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +480324970000004 && 1.2542972,103.8177678:1.2542634,103.8178787 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +-480324970000004 && 1.2542634,103.8178787:1.2542972,103.8177678 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +480324970000005 && 1.2542634,103.8178787:1.254256,103.8179027:1.2542395,103.8179569 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +-480324970000005 && 1.2542395,103.8179569:1.254256,103.8179027:1.2542634,103.8178787 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +480324970000006 && 1.2542395,103.8179569:1.2542347,103.8179725:1.2542172,103.8180295 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +-480324970000006 && 1.2542172,103.8180295:1.2542347,103.8179725:1.2542395,103.8179569 && last_edit_user_name -> Evandering || last_edit_changeset -> 52607303 || shelter -> yes || last_edit_time -> 1507060530000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 5 +528904849000001 && 1.2565672,103.8159474:1.2563128,103.8159095 && last_edit_user_name -> pushian || last_edit_changeset -> 52501454 || last_edit_time -> 1506756672000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +-528904849000001 && 1.2563128,103.8159095:1.2565672,103.8159474 && last_edit_user_name -> pushian || last_edit_changeset -> 52501454 || last_edit_time -> 1506756672000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +528904849000002 && 1.2563128,103.8159095:1.2561254,103.8158815:1.2559171,103.8158742:1.2556825,103.815909:1.2554871,103.816002 && last_edit_user_name -> pushian || last_edit_changeset -> 52501454 || last_edit_time -> 1506756672000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +-528904849000002 && 1.2554871,103.816002:1.2556825,103.815909:1.2559171,103.8158742:1.2561254,103.8158815:1.2563128,103.8159095 && last_edit_user_name -> pushian || last_edit_changeset -> 52501454 || last_edit_time -> 1506756672000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 2 +538105766000000 && 1.2551583,103.8167451:1.255176,103.8167019 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 1 +-538105766000000 && 1.255176,103.8167019:1.2551583,103.8167451 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || shelter -> yes || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 1 +533462931000001 && 1.2550414,103.8188556:1.2551103,103.8188232 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || turn:lanes:forward -> through || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || turn:lanes:backward -> left || highway -> service || last_edit_version -> 4 +-533462931000001 && 1.2551103,103.8188232:1.2550414,103.8188556 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || turn:lanes:forward -> through || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || turn:lanes:backward -> left || highway -> service || last_edit_version -> 4 +533462931000002 && 1.2551103,103.8188232:1.2552509,103.8187571 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || turn:lanes:forward -> through || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || turn:lanes:backward -> left || highway -> service || last_edit_version -> 4 +-533462931000002 && 1.2552509,103.8187571:1.2551103,103.8188232 && last_edit_user_name -> pk_ravi || last_edit_changeset -> 73567273 || last_edit_time -> 1566373604000 || last_edit_user_id -> 9393824 || turn:lanes:forward -> through || lanes -> 2 || lanes:forward -> 1 || lanes:backward -> 1 || turn:lanes:backward -> left || highway -> service || last_edit_version -> 4 +478164981000001 && 1.255184,103.8163823:1.2551637,103.816438:1.2551587,103.8164771:1.2551617,103.816509:1.2551803,103.8165855:1.2552024,103.8167435:1.2552052,103.8167682 && last_edit_changeset -> 65147309 || access -> private || surface -> asphalt || oneway -> yes || turn:lanes -> through || last_edit_user_name -> poornima1 || psv -> yes || last_edit_time -> 1543901775000 || last_edit_user_id -> 7868172 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 14 || foot -> no +478164981000002 && 1.2552052,103.8167682:1.255232,103.8170032 && last_edit_changeset -> 65147309 || access -> private || surface -> asphalt || oneway -> yes || turn:lanes -> through || last_edit_user_name -> poornima1 || psv -> yes || last_edit_time -> 1543901775000 || last_edit_user_id -> 7868172 || lanes -> 1 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 14 || foot -> no +480175922000001 && 1.2553364,103.8162287:1.2552734,103.8163156:1.2552264,103.8164529:1.2552006,103.8165879:1.2551979,103.8166889:1.2552127,103.816772 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +-480175922000001 && 1.2552127,103.816772:1.2551979,103.8166889:1.2552006,103.8165879:1.2552264,103.8164529:1.2552734,103.8163156:1.2553364,103.8162287 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +480175922000002 && 1.2552127,103.816772:1.2552288,103.8168632:1.2552359,103.8169194:1.2552652,103.8169729 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +-480175922000002 && 1.2552652,103.8169729:1.2552359,103.8169194:1.2552288,103.8168632:1.2552127,103.816772 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +480175922000003 && 1.2552652,103.8169729:1.255274,103.8169958:1.2552958,103.8170416:1.2552998,103.8170992 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +-480175922000003 && 1.2552998,103.8170992:1.2552958,103.8170416:1.255274,103.8169958:1.2552652,103.8169729 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +480175922000004 && 1.2552998,103.8170992:1.255278,103.8171476 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +-480175922000004 && 1.255278,103.8171476:1.2552998,103.8170992 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +480175922000005 && 1.255278,103.8171476:1.2552663,103.8171703:1.2552113,103.8172763:1.2551596,103.8173956 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +-480175922000005 && 1.2551596,103.8173956:1.2552113,103.8172763:1.2552663,103.8171703:1.255278,103.8171476 && last_edit_user_name -> pushian || last_edit_changeset -> 52496545 || shelter -> yes || last_edit_time -> 1506727568000 || last_edit_user_id -> 4989626 || highway -> footway || last_edit_version -> 6 +528906088000001 && 1.2581363,103.8153039:1.2580323,103.8154909:1.2579273,103.8156129:1.2578443,103.8156819:1.2575033,103.8158789:1.2573793,103.8159299:1.2570663,103.8160079:1.2568343,103.8160249:1.2566723,103.8160199:1.2563075,103.8160443 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +-528906088000001 && 1.2563075,103.8160443:1.2566723,103.8160199:1.2568343,103.8160249:1.2570663,103.8160079:1.2573793,103.8159299:1.2575033,103.8158789:1.2578443,103.8156819:1.2579273,103.8156129:1.2580323,103.8154909:1.2581363,103.8153039 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +528906088000002 && 1.2563075,103.8160443:1.2562599,103.8160475:1.2562158,103.8160623:1.2561788,103.8160906:1.256153,103.8161293:1.2561411,103.8161743:1.2561279,103.8163784:1.2561297,103.8164499:1.2561417,103.8166742 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +-528906088000002 && 1.2561417,103.8166742:1.2561297,103.8164499:1.2561279,103.8163784:1.2561411,103.8161743:1.256153,103.8161293:1.2561788,103.8160906:1.2562158,103.8160623:1.2562599,103.8160475:1.2563075,103.8160443 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +528906088000003 && 1.2561417,103.8166742:1.2561437,103.8167119:1.2561583,103.8167845:1.2562216,103.8168838:1.256358,103.8170893:1.2563947,103.8171803:1.2564185,103.81729:1.2564107,103.817402:1.2563721,103.8175074:1.2560932,103.8179758:1.2559895,103.8181625:1.2559195,103.8182565:1.2558005,103.8183775:1.2556965,103.8184475:1.255211,103.8186698:1.2551131,103.8187931:1.2551103,103.8188232 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +-528906088000003 && 1.2551103,103.8188232:1.2551131,103.8187931:1.255211,103.8186698:1.2556965,103.8184475:1.2558005,103.8183775:1.2559195,103.8182565:1.2559895,103.8181625:1.2560932,103.8179758:1.2563721,103.8175074:1.2564107,103.817402:1.2564185,103.81729:1.2563947,103.8171803:1.256358,103.8170893:1.2562216,103.8168838:1.2561583,103.8167845:1.2561437,103.8167119:1.2561417,103.8166742 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +528906088000004 && 1.2551103,103.8188232:1.2551043,103.8188876:1.2550354,103.8190007:1.2548672,103.8192216:1.2543873,103.819763 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +-528906088000004 && 1.2543873,103.819763:1.2548672,103.8192216:1.2550354,103.8190007:1.2551043,103.8188876:1.2551103,103.8188232 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || highway -> footway || last_edit_version -> 4 +528934981000001 && 1.2560081,103.8166832:1.2560381,103.8166812 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +-528934981000001 && 1.2560381,103.8166812:1.2560081,103.8166832 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +528934981000002 && 1.2560381,103.8166812:1.256104,103.8166767 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +-528934981000002 && 1.256104,103.8166767:1.2560381,103.8166812 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +528934981000003 && 1.256104,103.8166767:1.2561417,103.8166742 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +-528934981000003 && 1.2561417,103.8166742:1.256104,103.8166767 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749613000 || last_edit_user_id -> 6172527 || footway -> crossing || highway -> footway || last_edit_version -> 2 +528897519000001 && 1.2578449,103.8122184:1.25792,103.8123632:1.2579967,103.8124881 && last_edit_changeset -> 72006528 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> div006 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 16 || foot -> no +528897519000002 && 1.2579967,103.8124881:1.2582418,103.8128876:1.2584389,103.813247:1.2586232,103.8137176 && last_edit_changeset -> 72006528 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> div006 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 16 || foot -> no +528897519000003 && 1.2586232,103.8137176:1.2586548,103.8137982:1.2587017,103.813937:1.2587211,103.8140476:1.258701,103.8141449:1.2586192,103.8142314:1.2585388,103.8142723:1.258288,103.8143279:1.258164,103.8143655:1.258101,103.8144165:1.2580393,103.8145137:1.2580072,103.8146377:1.2580125,103.8147222:1.2580669,103.8149089 && last_edit_changeset -> 72006528 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> div006 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 16 || foot -> no +528897519000004 && 1.2580669,103.8149089:1.2581204,103.8150837:1.2581258,103.8151816:1.2581137,103.8152882:1.2580078,103.815484:1.257916,103.8156006:1.2578281,103.8156711:1.2574708,103.8158648:1.2573381,103.8159232:1.2569881,103.8159956:1.256791,103.816009:1.2566255,103.8160097:1.2564458,103.8160077 && last_edit_changeset -> 72006528 || surface -> asphalt || maxspeed -> 40 || oneway -> yes || last_edit_user_name -> div006 || psv -> yes || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || lanes -> 1 || name -> Siloso Road || highway -> unclassified || last_edit_version -> 16 || foot -> no +533463813000000 && 1.2561024,103.8160087:1.2560357,103.8160113 && last_edit_changeset -> 64005613 || surface -> asphalt || oneway -> yes || last_edit_user_name -> Ramanyam || maxheight -> 4.5 || psv -> yes || last_edit_time -> 1540893405000 || last_edit_user_id -> 7671600 || lanes -> 2 || name -> Imbiah Road || highway -> unclassified || last_edit_version -> 2 || foot -> no +249198509000000 && 1.2558972,103.8186614:1.2557991,103.8188466:1.2557383,103.8188748 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606037 || last_edit_time -> 1507058093000 || last_edit_user_id -> 6172527 || service -> parking_aisle || highway -> service || last_edit_version -> 2 || oneway -> yes +461380736000000 && 1.2558354,103.8165649:1.2555951,103.8166206 && last_edit_user_name -> MyWayOrNoHighway || last_edit_changeset -> 45230182 || surface -> metal || last_edit_time -> 1484608695000 || last_edit_user_id -> 4351550 || bridge -> yes || highway -> path || last_edit_version -> 2 || layer -> 1 +-461380736000000 && 1.2555951,103.8166206:1.2558354,103.8165649 && last_edit_user_name -> MyWayOrNoHighway || last_edit_changeset -> 45230182 || surface -> metal || last_edit_time -> 1484608695000 || last_edit_user_id -> 4351550 || bridge -> yes || highway -> path || last_edit_version -> 2 || layer -> 1 +100985094000000 && 1.2555443,103.815627:1.2553517,103.8150997:1.2553216,103.8150299:1.2553168,103.8149541:1.255338,103.814881:1.2553799,103.8148195:1.2555488,103.8146183:1.2558706,103.814216:1.2559564,103.8140014:1.2559832,103.813827:1.2559896,103.8137491:1.2559564,103.8136259:1.2558119,103.813448:1.2558462,103.8133009:1.2560128,103.8130116:1.2563903,103.8126047:1.2565324,103.8123252:1.2565927,103.8120234:1.2565929,103.8119988 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749609000 || last_edit_user_id -> 6172527 || name -> Imbiah Trail || highway -> footway || last_edit_version -> 5 +-100985094000000 && 1.2565929,103.8119988:1.2565927,103.8120234:1.2565324,103.8123252:1.2563903,103.8126047:1.2560128,103.8130116:1.2558462,103.8133009:1.2558119,103.813448:1.2559564,103.8136259:1.2559896,103.8137491:1.2559832,103.813827:1.2559564,103.8140014:1.2558706,103.814216:1.2555488,103.8146183:1.2553799,103.8148195:1.255338,103.814881:1.2553168,103.8149541:1.2553216,103.8150299:1.2553517,103.8150997:1.2555443,103.815627 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749609000 || last_edit_user_id -> 6172527 || name -> Imbiah Trail || highway -> footway || last_edit_version -> 5 +# Areas +159637730000000 && 1.2559725,103.8172264:1.2560168,103.8172368:1.2560047,103.8172817:1.2559616,103.8172721 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334481789000 || last_edit_user_id -> 257555 || last_edit_version -> 2 || building -> yes +480632927000000 && 1.2543692,103.8174987:1.2542873,103.8177619:1.2542012,103.8180387:1.2542135,103.8180424:1.2543104,103.8180521:1.2544048,103.8180283:1.2544855,103.817974:1.2545431,103.8178955:1.2545706,103.8178021:1.2545648,103.8177049:1.2545383,103.8176355:1.2544948,103.8175754:1.2544371,103.8175287 && area -> yes || last_edit_user_name -> KingRichardV || last_edit_changeset -> 66408527 || last_edit_time -> 1547760796000 || last_edit_user_id -> 9004577 || highway -> pedestrian || last_edit_version -> 3 +159637726000000 && 1.2554288,103.8163349:1.2554917,103.8163369:1.2555495,103.8163704:1.2555978,103.8164348:1.2555394,103.8164516:1.2555334,103.8164294:1.2555071,103.8164388:1.2554932,103.8163926:1.2554422,103.8164053:1.2554516,103.8164187:1.2554228,103.8164261:1.2554054,103.8164335:1.255402,103.8164154:1.2552544,103.8164757:1.2553007,103.8165669:1.2553127,103.8166795:1.2553457,103.8166963:1.2553336,103.8166728:1.2554227,103.8166319:1.2554181,103.8166219:1.2555401,103.8165736:1.2555433,103.8165823:1.2555783,103.8165643:1.2556023,103.8165541:1.2555824,103.8164878:1.2556479,103.8164616:1.2556434,103.8164408:1.2557258,103.8163798:1.2556796,103.8163121:1.2556385,103.8163349:1.2556345,103.8163268:1.2556514,103.8162886:1.2556279,103.8162779:1.2556655,103.8161887:1.2555488,103.8161391:1.2555327,103.8161854:1.2555146,103.8161753:1.2554684,103.8162665:1.2554335,103.8162437:1.2553658,103.8162973:1.2554066,103.8163375 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69415093 || addr:housenumber -> 51B || last_edit_time -> 1555813895000 || last_edit_user_id -> 741163 || addr:country -> SG || building:levels -> 2 || addr:street -> Imbiah Road || last_edit_version -> 5 || addr:postcode -> 099708 || building -> yes || addr:city -> Singapore +159637722000000 && 1.2559812,103.8173695:1.2559725,103.8174138:1.2559417,103.8174078:1.2559397,103.8174178:1.2559591,103.8174239:1.255941,103.8175063:1.2559065,103.8175014:1.2559404,103.81736 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334481786000 || last_edit_user_id -> 257555 || last_edit_version -> 2 || building -> yes +159637735000000 && 1.2552361,103.8169116:1.255276,103.8169656:1.2552994,103.8169974:1.2553472,103.8169621:1.255284,103.8168763 && last_edit_user_name -> happy-camper || last_edit_changeset -> 54547742 || last_edit_time -> 1513018303000 || last_edit_user_id -> 6346054 || last_edit_version -> 3 || building -> yes +171984330000000 && 1.2557616,103.8189672:1.2557187,103.818978:1.2557801,103.8192057:1.2563692,103.8190475:1.2565433,103.8196956:1.2565683,103.819789:1.256798,103.8197273:1.2565864,103.8189392:1.2565354,103.8187493 && image -> https://images.mapillary.com/H00qB2807YW4bLqbUqLbxw/thumb-2048.jpg || last_edit_changeset -> 70113476 || website -> https://www.rwsentosa.com/ || addr:country -> SG || building:levels -> 7 || roof:colour -> #b4b7b1 || tourism -> hotel || addr:postcode -> 098271 || roof:material -> metal || building -> yes || operator -> Resorts World || addr:city -> Singapore || last_edit_user_name -> km2bp || addr:housenumber -> 12 || last_edit_time -> 1557500576000 || last_edit_user_id -> 3610826 || name -> Festive Hotel || name:en -> Festive Hotel || addr:street -> Sentosa Gateway || mapillary -> https://www.mapillary.com/map/im/H00qB2807YW4bLqbUqLbxw || last_edit_version -> 12 || name:zh -> 节庆酒店 +159637731000000 && 1.2555354,103.816707:1.2554629,103.8167385:1.2554227,103.8166319:1.2553336,103.8166728:1.2553457,103.8166963:1.2553664,103.816756:1.2553055,103.8167848:1.2553135,103.8168049:1.2552948,103.8168103:1.2553356,103.8169116:1.2553758,103.8168948:1.2555795,103.8168096:1.255559,103.8167618 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69415093 || addr:housenumber -> 51A || last_edit_time -> 1555813895000 || last_edit_user_id -> 741163 || addr:country -> SG || addr:street -> Imbiah Road || last_edit_version -> 3 || addr:postcode -> 099703 || building -> yes || addr:city -> Singapore +159637729000000 && 1.2557808,103.8174621:1.2557882,103.8174312:1.2558132,103.817438:1.2558058,103.8174688 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334481788000 || last_edit_user_id -> 257555 || last_edit_version -> 2 || building -> yes +159638484000000 && 1.2539915,103.8149151:1.2542215,103.8151963:1.2543327,103.8150448:1.2543756,103.8153653:1.2548422,103.8153251:1.2548489,103.8151534:1.2548717,103.8148503:1.2549736,103.814625:1.2550313,103.8146599:1.2553692,103.8142361:1.2554067,103.8142669:1.2554496,103.8142307:1.2554925,103.8142937:1.2554054,103.8144104:1.2553598,103.8144211:1.2552914,103.8144681:1.2552646,103.8145486:1.2552632,103.8146116:1.2553571,103.8147135:1.255451,103.8146974:1.2555059,103.8146491:1.2555247,103.8145727:1.2555099,103.8144721:1.2555475,103.8144131:1.2556682,103.8142441:1.2558036,103.8141194:1.2558934,103.8139236:1.2561026,103.8137372:1.2562434,103.8135427:1.2563332,103.8133402:1.2564767,103.8132007:1.2568816,103.8128789:1.2570894,103.8126013:1.2572007,103.8124859:1.2573468,103.8124028:1.2578013,103.8123424:1.257863,103.8124658:1.2581821,103.8129312:1.2583605,103.8132665:1.2584235,103.8134448:1.2585468,103.8138203:1.2585736,103.8139719:1.2585656,103.8140604:1.2585307,103.8141328:1.2584329,103.8141771:1.2582532,103.8142012:1.2580735,103.8142803:1.2579555,103.8144024:1.2579073,103.8145807:1.2579046,103.8147417:1.2580266,103.8150555:1.2580279,103.8151842:1.257973,103.8153532:1.2578617,103.8155115:1.2576981,103.8156174:1.2573643,103.8158025:1.2572288,103.8158508:1.2569473,103.8159151:1.2567274,103.8159192:1.2562232,103.8159057:1.2560959,103.8158789:1.2557902,103.815887:1.2555435,103.815946:1.2555073,103.8158347:1.2554375,103.8158628:1.2554563,103.8159688:1.2554013,103.816005:1.2553638,103.8159956:1.2553196,103.8159406:1.2552244,103.8159057:1.2551305,103.8159205:1.2550527,103.8159674:1.2550085,103.8160868:1.2550581,103.816241:1.2550192,103.8163322:1.2548597,103.8168673:1.2550487,103.8170993:1.2551024,103.8171838:1.2550822,103.8172764:1.2549803,103.8172569:1.2548959,103.8173756:1.2547893,103.8174125:1.2546264,103.8174366:1.2545426,103.8174011:1.2544816,103.8173394:1.2542469,103.8171215:1.2541296,103.817037:1.2537937,103.8169008:1.2535893,103.8168546:1.253472,103.8168418:1.2532849,103.8168713:1.2531334,103.8169451:1.2529993,103.8170323:1.2528887,103.8171416:1.2527841,103.8173193:1.2527245,103.8175111:1.2527198,103.8176297:1.2527392,103.8178369:1.2528197,103.8180153:1.2531193,103.8184183:1.2532829,103.8186872:1.2532615,103.8187006:1.2522063,103.818073:1.2522524,103.817963:1.2520977,103.8178637:1.2523559,103.8173261:1.2521122,103.8171665:1.2520548,103.8172844:1.2519046,103.817287:1.2519314,103.8170537:1.2520735,103.8168847:1.2525608,103.8170215:1.2525911,103.8168257:1.2522076,103.8165789:1.2524007,103.8164368:1.2526078,103.8162155:1.252823,103.8159292:1.2531106,103.815712:1.2538568,103.8150233 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69405864 || last_edit_time -> 1555776556000 || last_edit_user_id -> 741163 || landuse -> forest || last_edit_version -> 15 +684974293000000 && 1.2549783,103.8187925:1.2549314,103.8187549:1.2546505,103.8190969:1.2545211,103.8191378:1.2544541,103.8191378:1.2543508,103.8191915:1.2544025,103.8193054:1.2543428,103.8193423:1.2544809,103.8194013:1.2548235,103.8190459:1.2548972,103.8189467 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69405864 || natural -> wood || last_edit_time -> 1555776516000 || last_edit_user_id -> 741163 || last_edit_version -> 1 +159637725000000 && 1.2552805,103.8175453:1.2552755,103.817593:1.2557367,103.8176415:1.2557345,103.8176595:1.2557616,103.8176623:1.2557531,103.8177462:1.2557238,103.8177431:1.2557169,103.8178091:1.2552624,103.8177609:1.2552524,103.8178034:1.2552282,103.8178365:1.2551883,103.8178444:1.2550922,103.8178342:1.255097,103.817788:1.2551855,103.8177968:1.255211,103.8175384 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69415093 || addr:housenumber -> 50 || last_edit_time -> 1555813895000 || last_edit_user_id -> 741163 || addr:country -> SG || name -> Singapore Cable Car Station || addr:street -> Imbiah Road || last_edit_version -> 5 || addr:postcode -> 099706 || building -> yes || addr:city -> Singapore +176839461000000 && 1.2544868,103.8152068:1.2544937,103.8152978:1.2544089,103.8153042:1.2543811,103.8149459:1.25492,103.8143029:1.254988,103.8143596:1.2549783,103.8143713:1.2551279,103.8145045:1.2550161,103.8146302:1.2548706,103.8145008:1.2544699,103.8149823:1.2544843,103.8151726:1.2545205,103.8151702:1.2545172,103.8151231:1.2545566,103.8151203:1.2545642,103.8152289:1.2545248,103.8152317:1.2545229,103.8152044 && last_edit_changeset -> 59646385 || website -> www.silosobeachresort.com || addr:country -> SG || tourism -> hotel || source -> Bing || addr:postcode -> 099538 || building -> yes || addr:city -> Singapore || last_edit_user_name -> Mapier || addr:housenumber -> 51 || last_edit_time -> 1528398175000 || last_edit_user_id -> 616884 || name -> Siloso Beach Resort || name:en -> Siloso Beach Resort || addr:street -> Imbiah Walk || last_edit_version -> 2 || name:zh -> 喜乐圣淘沙度假旅馆 +684974066000000 && 1.2552068,103.8187353:1.2552733,103.8186248:1.2553537,103.8185866:1.2557701,103.8184109:1.2559001,103.818301:1.2559859,103.8182031:1.25612,103.8179764:1.2564056,103.8174628:1.2564364,103.8173085:1.256415,103.8172106:1.2563694,103.8170779:1.2561696,103.8167801:1.2561643,103.8167252:1.2561763,103.8161954:1.2562112,103.8161096:1.2562742,103.8160788:1.2564337,103.8160506:1.2571162,103.8160305:1.2574112,103.8159554:1.2576565,103.8158374:1.2579341,103.815655:1.2580668,103.815494:1.2581593,103.8152862:1.2581687,103.8151427:1.2581406,103.8149817:1.2580869,103.8148342:1.2580561,103.8147202:1.2580708,103.8145781:1.2580963,103.8144909:1.258162,103.8144386:1.2582398,103.8144024:1.2585589,103.8143326:1.2586608,103.8142857:1.2587439,103.8141784:1.2587801,103.814055:1.2587573,103.8139424:1.2586594,103.8136554:1.2584691,103.8131914:1.2579435,103.8123183:1.2576552,103.8117389:1.2576539,103.8115807:1.2577343,103.8113862:1.2578697,103.8114117:1.2579341,103.811354:1.2581942,103.8118382:1.2580199,103.8119562:1.2583122,103.8124014:1.2590416,103.8121292:1.2589196,103.8127555:1.2589209,103.813308:1.2589732,103.8137519:1.2589303,103.8141046:1.2587573,103.8146988:1.2585066,103.8152701:1.2583846,103.8154954:1.2583202,103.8157247:1.2581365,103.8161378:1.2576203,103.8164556:1.2571229,103.816579:1.2569446,103.8168123:1.2568132,103.8170604:1.2567622,103.817279:1.2567354,103.8179335:1.2567448,103.8180716:1.2567354,103.8181548:1.2566349,103.8182808:1.2565973,103.8183117:1.2555529,103.8185853 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69405864 || natural -> wood || last_edit_time -> 1555776509000 || last_edit_user_id -> 741163 || last_edit_version -> 1 +689159154000000 && 1.2566324,103.8187761:1.2566542,103.8187736:1.2566741,103.8187642:1.2566899,103.8187489:1.2567,103.8187294:1.2567033,103.8187077:1.2566993,103.8186854:1.2566881,103.8186656:1.2566711,103.8186507:1.25665,103.8186422:1.2566273,103.8186411:1.2566056,103.8186476:1.2565872,103.8186609:1.2565742,103.8186795:1.2565682,103.8187014:1.2565695,103.8187233:1.2565777,103.8187437:1.256592,103.8187603:1.2566109,103.8187715 && last_edit_user_name -> km2bp || image -> https://images.mapillary.com/H00qB2807YW4bLqbUqLbxw/thumb-2048.jpg || shelter_type -> lean_to || last_edit_changeset -> 70113476 || last_edit_time -> 1557500573000 || last_edit_user_id -> 3610826 || amenity -> shelter || mapillary -> https://www.mapillary.com/map/im/H00qB2807YW4bLqbUqLbxw || last_edit_version -> 1 || building -> yes +478355767000000 && 1.2551122,103.8173239:1.2550335,103.8173117:1.2550217,103.8173401:1.2550134,103.8173354:1.254989,103.8173861:1.255064,103.817368:1.2550903,103.8173616 && last_edit_changeset -> 60730660 || website -> http://www.skylineluge.sg || tourism -> theme_park || source -> Kaart Ground Survey 2017 || building -> yes || addr:city -> Singapore || last_edit_user_name -> gunsno || last_edit_time -> 1531657336000 || last_edit_user_id -> 7277094 || name -> Skyline Luge Sentosa || opening_hours -> Mo-Su 10:00-21:30 || addr:street -> Imbiah Road || last_edit_version -> 5 +159637728000000 && 1.2553593,103.8175531:1.2553667,103.8174787:1.2552949,103.8174715:1.255297,103.8174497:1.2551355,103.8174337:1.2551178,103.8176126:1.2550839,103.8176093:1.2550794,103.8176549:1.2550749,103.8177001:1.2551054,103.8177031:1.255097,103.817788:1.2551855,103.8177968:1.255211,103.8175384:1.2552805,103.8175453 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69415093 || addr:housenumber -> 42 || last_edit_time -> 1555813895000 || last_edit_user_id -> 741163 || addr:country -> SG || building:levels -> 2 || addr:street -> Imbiah Road || last_edit_version -> 6 || addr:postcode -> 099701 || building -> yes || addr:city -> Singapore +528934982000000 && 1.2554409,103.8172409:1.2553875,103.8174012:1.2552784,103.8173867:1.255197,103.8173761:1.2552897,103.8171688:1.2552975,103.8171514:1.2553713,103.8171498:1.2553556,103.8172299 && area -> yes || last_edit_user_name -> cartogram || last_edit_changeset -> 54665653 || last_edit_time -> 1513372482000 || last_edit_user_id -> 6095772 || highway -> pedestrian || last_edit_version -> 2 +159637741000000 && 1.2555079,103.8172851:1.255507,103.8172833:1.255544,103.8173541:1.2555358,103.8173834:1.2554677,103.8174198:1.2554235,103.8173353 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334481791000 || last_edit_user_id -> 257555 || last_edit_version -> 2 || building -> yes +480159876000000 && 1.254893,103.8168369:1.255041,103.8164779:1.255176,103.8167019:1.255035,103.8170579 && image -> https://images.mapillary.com/48gXOPD0CLfUnPMiflSVbQ/thumb-2048.jpg || last_edit_changeset -> 70104334 || addr:country -> SG || building:levels -> 2 || addr:postcode -> 098466 || building -> yes || addr:city -> Singapore || last_edit_user_name -> km2bp || addr:housenumber -> 60 || last_edit_time -> 1557485049000 || last_edit_user_id -> 3610826 || addr:street -> Imbiah Road || mapillary -> https://www.mapillary.com/map/im/48gXOPD0CLfUnPMiflSVbQ || last_edit_version -> 4 +159637737000000 && 1.255994,103.8172985:1.2559806,103.8173555:1.2559434,103.8173479:1.2559573,103.8172898 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334481790000 || last_edit_user_id -> 257555 || last_edit_version -> 2 || building -> yes +159638790000000 && 1.2549438,103.8184377:1.2550108,103.8185184:1.255088,103.8184543:1.255021,103.8183736 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69415093 || last_edit_time -> 1555813896000 || last_edit_user_id -> 741163 || roof:shape -> pyramidal || building:levels -> 1 || roof:levels -> 1 || last_edit_version -> 2 || roof:material -> roof_tiles || building -> yes +159637733000000 && 1.2559633,103.8171961:1.2559744,103.8171563:1.2559298,103.8171479:1.2558941,103.8171402:1.2558853,103.8171773 && last_edit_user_name -> Evandering || last_edit_changeset -> 52606119 || last_edit_time -> 1507058169000 || last_edit_user_id -> 6172527 || last_edit_version -> 3 || building -> yes +260774258000000 && 1.2548596,103.8171658:1.25481,103.817147:1.254743,103.8171322:1.2547027,103.8171269:1.2546773,103.8172637:1.2547014,103.8172851:1.2547389,103.8172878:1.254747,103.8172409:1.2548422,103.8172529:1.2548757,103.8172167:1.2548395,103.8172087 && last_edit_changeset -> 69415093 || addr:country -> SG || addr:postcode -> 099003 || building -> yes || addr:city -> Singapore || last_edit_user_name -> JaLooNz || addr:housenumber -> 45 || last_edit_time -> 1555813897000 || last_edit_user_id -> 741163 || name -> Skyline Luge Office || addr:street -> Siloso Beach Walk || last_edit_version -> 4 || name:zh -> 斜坡滑車&空中吊椅 +159638788000000 && 1.2547207,103.8186175:1.2547695,103.818676:1.2548041,103.8186472:1.2547553,103.8185886 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334481988000 || last_edit_user_id -> 257555 || last_edit_version -> 1 || building -> yes +684974295000000 && 1.2543636,103.8185987:1.2546492,103.8177699:1.254674,103.8177276:1.2548362,103.8177035:1.2549187,103.8177504:1.2549508,103.8177565:1.254924,103.8178899:1.255211,103.8179281:1.2552565,103.8178651:1.2552847,103.8178021:1.2557278,103.8178517:1.2557372,103.8177632:1.2557701,103.8177659:1.2557929,103.8176036:1.2559216,103.8176163:1.2560375,103.8172207:1.255994,103.8172106:1.2559967,103.8171503:1.255941,103.8171362:1.2558358,103.816467:1.2556829,103.8160821:1.255764,103.8160519:1.2558927,103.816062:1.255945,103.8160908:1.2559893,103.8161425:1.2560201,103.8162176:1.2560194,103.8167741:1.2560496,103.8168633:1.2562434,103.8171771:1.2562655,103.8173092:1.2562494,103.8174427:1.2558827,103.8181166:1.2557855,103.8182158:1.2556963,103.8182909:1.255585,103.8183513:1.2554898,103.8183747:1.2554959,103.8182064:1.2550541,103.8182071:1.2546894,103.8185249:1.2547028,103.8187663:1.2545808,103.8187999 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69405864 || natural -> wood || last_edit_time -> 1555776517000 || last_edit_user_id -> 741163 || last_edit_version -> 1 +159637727000000 && 1.2557965,103.8171709:1.2558684,103.8171882:1.2558718,103.8171741:1.2558853,103.8171773:1.2559633,103.8171961:1.2559789,103.8171998:1.2559725,103.8172264:1.2559616,103.8172721:1.2559573,103.8172898:1.2559434,103.8173479:1.2559404,103.81736:1.2559065,103.8175014:1.2559014,103.8175227:1.2558826,103.8175182:1.2558209,103.8175033:1.2557988,103.817498:1.2558058,103.8174688:1.2558132,103.817438:1.2558515,103.8172785:1.2557751,103.8172602:1.2557524,103.817218 && last_edit_changeset -> 69415093 || website -> https://www.sentosa.com.sg/Explore/Sentosa-Nature-Discovery-Walking-Trails/Sentosa-Nature-Discovery || addr:country -> SG || tourism -> museum || addr:postcode -> 098968 || building -> yes || addr:city -> Singapore || last_edit_user_name -> JaLooNz || addr:housenumber -> 21 || last_edit_time -> 1555813895000 || last_edit_user_id -> 741163 || name -> Sentosa Nature Discovery || addr:street -> Siloso Road || last_edit_version -> 5 +159637723000000 && 1.2553886,103.8171275:1.2553157,103.8171245:1.255316,103.8170979:1.2553166,103.8170328:1.2553669,103.8170333:1.2553457,103.8169753:1.2553758,103.8168948:1.2555795,103.8168096:1.255559,103.8167618:1.2555981,103.816645:1.2556326,103.8167312:1.2556499,103.8167942:1.2556472,103.8169156:1.2556071,103.816852:1.2556025,103.8171281:1.2556011,103.8171657:1.2555601,103.8172488:1.2553873,103.8172073 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69415093 || addr:housenumber -> 51 || last_edit_time -> 1555813894000 || last_edit_user_id -> 741163 || addr:country -> SG || addr:street -> Imbiah Road || last_edit_version -> 5 || addr:postcode -> 099702 || building -> yes || addr:city -> Singapore +159637740000000 && 1.2556973,103.8172189:1.2556751,103.8172988:1.2557082,103.817308:1.2556841,103.8173947:1.2556463,103.8173842:1.2556238,103.8174653:1.255521,103.8174367:1.2555358,103.8173834:1.255544,103.8173541:1.2555898,103.8171891 && last_edit_user_name -> rene78 || last_edit_changeset -> 11307160 || last_edit_time -> 1334481790000 || last_edit_user_id -> 257555 || last_edit_version -> 2 || building -> yes +176839464000000 && 1.2549498,103.8175571:1.2549714,103.8175585:1.2549908,103.817568:1.255005,103.8175842:1.255012,103.8176047:1.2550106,103.8176262:1.2550011,103.8176456:1.2549849,103.8176599:1.2549645,103.8176668:1.254943,103.8176655:1.2549236,103.817656:1.2549093,103.8176398:1.2549023,103.8176193:1.2549037,103.8175978:1.2549132,103.8175784:1.2549294,103.8175641 && image -> https://images.mapillary.com/Xo7nB4YlPWSyZBmXuSOPzQ/thumb-2048.jpg || last_edit_changeset -> 70108984 || website -> http://www.skytower.com.sg/ || man_made -> tower || tourism -> attraction || building -> yes || tower:type -> observation || last_edit_user_name -> km2bp || old_name -> Carlsberg Sky Tower || last_edit_time -> 1557493286000 || last_edit_user_id -> 3610826 || name -> Tiger Sky Tower || opening_hours -> 09:00-21:00 || mapillary -> https://www.mapillary.com/map/im/Xo7nB4YlPWSyZBmXuSOPzQ || wikipedia -> en:Tiger Sky Tower || last_edit_version -> 5 || height -> 110 || name:zh -> Tiger 摩天塔 +159637736000000 && 1.2558673,103.8175848:1.2558049,103.8175707:1.2558069,103.8175439:1.2558209,103.8175033:1.2558826,103.8175182 && last_edit_user_name -> pushian || last_edit_changeset -> 46816490 || last_edit_time -> 1489420121000 || last_edit_user_id -> 4989626 || last_edit_version -> 3 || building -> yes +159638789000000 && 1.2548368,103.8185325:1.2549,103.8186104:1.254979,103.8185462:1.2549158,103.8184683 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 69415093 || last_edit_time -> 1555813895000 || last_edit_user_id -> 741163 || roof:shape -> pyramidal || building:levels -> 1 || roof:levels -> 1 || last_edit_version -> 2 || roof:material -> roof_tiles || building -> yes +# Lines +28755868000000 && 1.2555875,103.8176818:1.2586261,103.818041 && last_edit_user_name -> km2bp || last_edit_changeset -> 70113476 || last_edit_time -> 1557500575000 || last_edit_user_id -> 3610826 || aerialway -> gondola || name -> Singapore-Sentosa Cable Car || last_edit_version -> 10 || layer -> 2 +688904126000000 && 1.2567636,103.8181642:1.2567019,103.8182446:1.2566282,103.8183197:1.2564901,103.8183519:1.2555581,103.8185985:1.2552041,103.8187534:1.2551238,103.8188784:1.2549321,103.8191861:1.25435,103.8198425:1.2536597,103.8207042:1.2533647,103.8211012:1.2529558,103.8216765:1.2528445,103.8218643:1.2527654,103.8220789:1.2527573,103.8221888:1.2527372,103.8228366:1.2526688,103.8232389:1.2526112,103.8234964:1.2525146,103.8237244:1.2523149,103.824006:1.2518429,103.8245988:1.2518054,103.8246873:1.2518027,103.8247356:1.2518335,103.8247986:1.2520561,103.8249689:1.2522639,103.8251688:1.2528284,103.8257535:1.2529504,103.8258608:1.2530483,103.825905:1.2531408,103.8259278:1.2532628,103.8259292:1.253366,103.8259144:1.2534787,103.8258688:1.2536248,103.8257562:1.253775,103.8255389:1.2538621,103.8254517:1.2559336,103.8237673:1.2562219,103.8236278:1.2564606,103.8235849:1.2567091,103.8235736:1.2567098,103.823498:1.2567564,103.8234498:1.256828,103.8234197:1.2574154,103.8232975:1.2574578,103.8232702:1.2574909,103.8232246:1.2575146,103.8231721:1.2575731,103.8230422:1.2575991,103.8229848 && last_edit_user_name -> km2bp || last_edit_changeset -> 70073507 || last_edit_time -> 1557408324000 || last_edit_user_id -> 3610826 || last_edit_version -> 1 +689102082000000 && 1.2543463,103.8182899:1.2544518,103.8180627 && last_edit_user_name -> km2bp || last_edit_changeset -> 70104334 || last_edit_time -> 1557485045000 || last_edit_user_id -> 3610826 || aerialway -> cable_car || name -> Sentosa Line || last_edit_version -> 1 +689102084000000 && 1.2545761,103.8177946:1.2547353,103.817453 && last_edit_user_name -> km2bp || last_edit_changeset -> 70104334 || last_edit_time -> 1557485045000 || last_edit_user_id -> 3610826 || aerialway -> cable_car || name -> Sentosa Line || last_edit_version -> 1 +689102085000000 && 1.2544518,103.8180627:1.2545761,103.8177946 && last_edit_user_name -> km2bp || image -> https://images.mapillary.com/tM5pTW4qQVgbrk7FB2O0ow/thumb-2048.jpg || last_edit_changeset -> 70104334 || last_edit_time -> 1557485045000 || last_edit_user_id -> 3610826 || aerialway -> cable_car || name -> Sentosa Line || mapillary -> https://www.mapillary.com/map/im/tM5pTW4qQVgbrk7FB2O0ow || last_edit_version -> 1 +689102086000000 && 1.2547353,103.817453:1.2549477,103.816994 && last_edit_user_name -> km2bp || image -> https://images.mapillary.com/48gXOPD0CLfUnPMiflSVbQ/thumb-2048.jpg || last_edit_changeset -> 70104334 || last_edit_time -> 1557485045000 || last_edit_user_id -> 3610826 || aerialway -> cable_car || name -> Sentosa Line || mapillary -> https://www.mapillary.com/map/im/48gXOPD0CLfUnPMiflSVbQ || last_edit_version -> 1 +689102087000000 && 1.2549477,103.816994:1.2550487,103.8167759:1.2553994,103.8159077 && last_edit_user_name -> km2bp || last_edit_changeset -> 70104334 || last_edit_time -> 1557485045000 || last_edit_user_id -> 3610826 || aerialway -> cable_car || name -> Sentosa Line || last_edit_version -> 1 +689102088000000 && 1.2553994,103.8159077:1.2558502,103.8148119 && last_edit_user_name -> km2bp || image -> https://images.mapillary.com/K6QdH5wB__71miZmpGihNA/thumb-2048.jpg || last_edit_changeset -> 70104334 || last_edit_time -> 1557485046000 || last_edit_user_id -> 3610826 || aerialway -> cable_car || name -> Sentosa Line || mapillary -> https://www.mapillary.com/map/im/K6QdH5wB__71miZmpGihNA || last_edit_version -> 1 +# Points +6464873707000000 && 1.2553279,103.8173871 && last_edit_user_name -> km2bp || image -> https://images.mapillary.com/-3LjtvvOeOm6KqBpMPBaNg/thumb-2048.jpg || last_edit_changeset -> 70113476 || last_edit_time -> 1557500572000 || last_edit_user_id -> 3610826 || amenity -> fountain || mapillary -> https://www.mapillary.com/map/im/-3LjtvvOeOm6KqBpMPBaNg || last_edit_version -> 1 +5209288654000000 && 1.2580669,103.8149089 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707364000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 2 +4485958594000000 && 1.2555099,103.8174809 && last_edit_user_name -> gps-newcomer || last_edit_changeset -> 43450576 || last_edit_time -> 1478464761000 || last_edit_user_id -> 94213 || amenity -> toilets || fee -> no || last_edit_version -> 1 +5209288650000000 && 1.2579967,103.8124881 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || highway -> crossing || last_edit_version -> 1 +1717541875000000 && 1.2553003,103.816949 && last_edit_user_name -> happy-camper || last_edit_changeset -> 52686350 || last_edit_time -> 1507304366000 || last_edit_user_id -> 6346054 || name -> Imbiah Lookout || highway -> bus_stop || last_edit_version -> 11 +4893137468000000 && 1.2552108,103.8182922 && last_edit_user_name -> DAJIBA || last_edit_changeset -> 49211674 || last_edit_time -> 1496462062000 || last_edit_user_id -> 360397 || name -> Former Mini Golf || tourism -> attraction || last_edit_version -> 1 +5146232329000000 && 1.2554965,103.8160775 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707362000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 8 +5209288659000000 && 1.2563089,103.8160091 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707364000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 2 +4731913779000000 && 1.255064,103.817368 && last_edit_user_name -> pushian || last_edit_changeset -> 52243602 || last_edit_time -> 1505999486000 || last_edit_user_id -> 4989626 || entrance -> yes || last_edit_version -> 2 +6464873706000000 && 1.2553539,103.817251 && last_edit_user_name -> km2bp || image -> https://images.mapillary.com/sDvht2V9qXhRVmoSJmWh_w/thumb-2048.jpg || last_edit_changeset -> 70113476 || last_edit_time -> 1557500572000 || last_edit_user_id -> 3610826 || tourism -> artwork || artwork_type -> sculpture || mapillary -> https://www.mapillary.com/map/im/sDvht2V9qXhRVmoSJmWh_w || last_edit_version -> 1 +3686388539000000 && 1.2550487,103.8167759 && last_edit_user_name -> km2bp || last_edit_changeset -> 70104334 || last_edit_time -> 1557485048000 || last_edit_user_id -> 3610826 || aerialway -> station || name -> Imbiah Lookout || last_edit_version -> 9 || wikidata -> Q6003536 +2248167729000000 && 1.2555712,103.8177397 && last_edit_user_name -> Ulmon Community || last_edit_changeset -> 15606850 || last_edit_time -> 1365076619000 || last_edit_user_id -> 1313848 || name -> Southern Ridges || tourism -> viewpoint || last_edit_version -> 1 +4613238712000000 && 1.2552278,103.8175128 && last_edit_user_name -> Canyonsrcool || last_edit_changeset -> 45231662 || last_edit_time -> 1484616741000 || last_edit_user_id -> 126508 || amenity -> fast_food || name -> Subway || cuisine -> sandwich || last_edit_version -> 1 +5209288647000000 && 1.2551103,103.8188232 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749607000 || last_edit_user_id -> 6172527 || highway -> crossing || last_edit_version -> 1 +5138638008000000 && 1.256104,103.8166767 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707358000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 9 +5146232330000000 && 1.2554904,103.8160242 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707362000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 7 +5209288660000000 && 1.2560381,103.8166812 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707364000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 2 +5586638524000000 && 1.2550597,103.8176013 && last_edit_user_name -> Wiple || last_edit_changeset -> 58534348 || name:de -> Tiger Sky Tower || last_edit_time -> 1525026129000 || last_edit_user_id -> 7355545 || tourism -> attraction || last_edit_version -> 1 +5209288652000000 && 1.2586232,103.8137176 && last_edit_user_name -> Evandering || last_edit_changeset -> 53491829 || last_edit_time -> 1509749608000 || last_edit_user_id -> 6172527 || highway -> crossing || last_edit_version -> 1 +4613238713000000 && 1.2551627,103.8175716 && last_edit_user_name -> Canyonsrcool || last_edit_changeset -> 45231662 || last_edit_time -> 1484616741000 || last_edit_user_id -> 126508 || amenity -> cafe || name -> Starbucks || last_edit_version -> 1 +5761606753000000 && 1.2554079,103.8165019 && last_edit_user_name -> gunsno || last_edit_changeset -> 60730626 || last_edit_time -> 1531657248000 || last_edit_user_id -> 7277094 || name -> 4D Adventureland || opening_hours -> Mo-Su 09:00-21:00 || tourism -> attraction || last_edit_version -> 1 +6464873708000000 && 1.2553653,103.8174089 && last_edit_user_name -> km2bp || last_edit_changeset -> 70113476 || last_edit_time -> 1557500572000 || last_edit_user_id -> 3610826 || name -> Butterfly Park & Insect Kigdom || tourism -> artwork || mapillary -> https://www.mapillary.com/map/im/GlnBJBlPZOptpdPiudAvFA || last_edit_version -> 1 +4217410090000000 && 1.2554744,103.8169904 && name:ru -> Парк бабочек и царство насекомых || last_edit_user_name -> rene78 || last_edit_changeset -> 39697306 || website -> http://www.jungle.com.sg/ || last_edit_time -> 1464725794000 || last_edit_user_id -> 257555 || name -> Butterfly Park & Insect Kingdom || tourism -> zoo || last_edit_version -> 2 +316161140000000 && 1.2555875,103.8176818 && last_edit_user_name -> JaLooNz || last_edit_changeset -> 12738977 || last_edit_time -> 1345037425000 || last_edit_user_id -> 741163 || aerialway -> station || name -> Sentosa || last_edit_version -> 3 +5209288658000000 && 1.2563109,103.8159577 && last_edit_user_name -> naik4 || last_edit_changeset -> 59733650 || last_edit_time -> 1528707364000 || last_edit_user_id -> 7671613 || highway -> crossing || last_edit_version -> 2 +# Relations +2353599000000 && 28755868000000 -> -> L || 316161140000000 -> stop -> P && last_edit_changeset -> 70181245 || description -> Singapore Cable Car || type -> route || operator -> Singapore Cable Car || last_edit_user_name -> km2bp || ref -> Singapore Cable Car || route -> aerialway || last_edit_time -> 1557735493000 || last_edit_user_id -> 3610826 || name -> Singapore Cable Car || from -> Mount Faber || to -> Imbian || last_edit_version -> 5 +2581000000000 && 1717541875000000 -> platform -> N || 1717541875000000 -> platform -> P && last_edit_changeset -> 71305177 || fee -> no || type -> route || operator -> Sentosa || network -> Sentosa || last_edit_user_name -> mueschel || ref -> Bus A || roundtrip -> yes || route -> bus || public_transport:version -> 2 || last_edit_time -> 1560703589000 || last_edit_user_id -> 616774 || name -> Sentosa Bus A || opening_hours -> Mo-SU 7:00-00:10 || interval -> 00:10:00 || last_edit_version -> 48 +7616954000000 && 260777732000000 -> -> E || 478164185000001 -> -> E || 478164185000002 -> -> E || 478164981000001 -> -> E || 478164981000002 -> -> E || 482407210000000 -> -> E || 482407211000001 -> -> E || 482407211000002 -> -> E || 482416616000001 -> -> E || 482416616000002 -> -> E || 482416618000001 -> -> E || 482416618000002 -> -> E || 528240340000001 -> -> E || 528240340000002 -> -> E || 528240340000003 -> -> E || 528240340000004 -> -> E || 528897519000001 -> -> E || 528897519000002 -> -> E || 528897519000003 -> -> E || 528897519000004 -> -> E || 528898916000000 -> -> E || 528898917000001 -> -> E || 528898917000002 -> -> E || 530020436000001 -> -> E || 530020436000002 -> -> E || 530020436000003 -> -> E || 530020436000004 -> -> E || 533417989000000 -> -> E || 538105768000001 -> -> E || 538105768000002 -> -> E || 639335711000000 -> -> E || 650732280000000 -> -> E || 702400958000001 -> -> E || 702400958000002 -> -> E && last_edit_user_name -> aj_34 || last_edit_changeset -> 74099421 || route -> bus || last_edit_time -> 1567618893000 || last_edit_user_id -> 8911947 || name -> Sentosa Bus A || type -> route || last_edit_version -> 51 +8865315000000 && 533417988000000 -> via -> E || 533463813000000 -> to -> E || 639335711000000 -> from -> E && last_edit_user_name -> Ramanyam || last_edit_changeset -> 64005613 || last_edit_time -> 1540893405000 || last_edit_user_id -> 7671600 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +8865318000000 && 533417988000000 -> to -> E || 639335711000000 -> via -> E || 639335779000000 -> from -> E && last_edit_user_name -> Ramanyam || last_edit_changeset -> 64005642 || last_edit_time -> 1540893493000 || last_edit_user_id -> 7671600 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +8865326000000 && 533463813000000 -> from -> E || 639335711000000 -> to -> E || 639335779000000 -> via -> E && last_edit_user_name -> Ramanyam || last_edit_changeset -> 64005668 || last_edit_time -> 1540893552000 || last_edit_user_id -> 7671600 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +8870287000000 && 533417988000000 -> from -> E || 533463813000000 -> via -> E || 639335779000000 -> to -> E && last_edit_user_name -> mutpuri || last_edit_changeset -> 64038126 || last_edit_time -> 1540975886000 || last_edit_user_id -> 7795551 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +8883047000000 && 528897519000001 -> to -> E || 528897519000002 -> to -> E || 528897519000003 -> to -> E || 528897519000004 -> to -> E && last_edit_user_name -> div006 || last_edit_changeset -> 72006528 || last_edit_time -> 1562581439000 || last_edit_user_id -> 8911933 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 2 +8923582000000 && 1717541894000000 -> via -> N || -528900905000002 -> from -> E || -528900905000002 -> to -> E || -528900905000001 -> from -> E || -528900905000001 -> to -> E || 528900905000001 -> from -> E || 528900905000001 -> to -> E || 528900905000002 -> from -> E || 528900905000002 -> to -> E && last_edit_user_name -> nadigatla || last_edit_changeset -> 64439376 || last_edit_time -> 1542110062000 || last_edit_user_id -> 8995721 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +8924581000000 && 613981020000000 -> via -> N && last_edit_user_name -> kumar3 || last_edit_changeset -> 64442215 || last_edit_time -> 1542115071000 || last_edit_user_id -> 8924971 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +8993526000000 && 4731977868000000 -> via -> N || -646425420000000 -> from -> E || -646425420000000 -> to -> E || 646425420000000 -> from -> E || 646425420000000 -> to -> E && last_edit_user_name -> aj_34 || last_edit_changeset -> 64661757 || last_edit_time -> 1542641735000 || last_edit_user_id -> 8911947 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +8993527000000 && 4731977868000000 -> via -> N && last_edit_user_name -> aj_34 || last_edit_changeset -> 64661757 || last_edit_time -> 1542641735000 || last_edit_user_id -> 8911947 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +9033062000000 && 5175392808000000 -> via -> N || 533417990000000 -> from -> E || 639335710000000 -> to -> E && last_edit_user_name -> pavani_03 || last_edit_changeset -> 64726693 || last_edit_time -> 1542784300000 || last_edit_user_id -> 8911925 || restriction -> no_left_turn || type -> restriction || last_edit_version -> 1 +9574726000000 && 688904126000000 -> outer -> L && last_edit_changeset -> 70073507 || website -> https://www.rwsentosa.com/ || addr:country -> SG || tourism -> attraction || type -> multipolygon || addr:postcode -> 098269 || addr:city -> Singapore || last_edit_user_name -> km2bp || addr:housenumber -> 8 || last_edit_time -> 1557408326000 || last_edit_user_id -> 3610826 || landuse -> retail || name -> Resorts World Sentosa || name:en -> Resorts World Sentosa || addr:street -> Sentosa Gateway || wikipedia -> en:Resorts World Sentosa || last_edit_version -> 1 || wikidata -> Q5955134 || email -> enquiries@rwsentosa.com || name:zh -> 圣淘沙名胜世界 +10100363000000 && 2558468523000000 -> via -> N && last_edit_user_name -> div006 || last_edit_changeset -> 75129398 || last_edit_time -> 1569910184000 || last_edit_user_id -> 8911933 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +10100364000000 && 2558468523000000 -> via -> N || -249144132000000 -> from -> E || -249144132000000 -> to -> E || 249144132000000 -> from -> E || 249144132000000 -> to -> E && last_edit_user_name -> div006 || last_edit_changeset -> 75129398 || last_edit_time -> 1569910184000 || last_edit_user_id -> 8911933 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +10100365000000 && 2558468525000000 -> via -> N || -249144132000000 -> from -> E || -249144132000000 -> to -> E || 249144132000000 -> from -> E || 249144132000000 -> to -> E && last_edit_user_name -> div006 || last_edit_changeset -> 75129398 || last_edit_time -> 1569910184000 || last_edit_user_id -> 8911933 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +10100366000000 && 2558468525000000 -> via -> N || -730092327000000 -> from -> E || -730092327000000 -> to -> E || 730092327000000 -> from -> E || 730092327000000 -> to -> E && last_edit_user_name -> div006 || last_edit_changeset -> 75129398 || last_edit_time -> 1569910184000 || last_edit_user_id -> 8911933 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +10100367000000 && 2558083058000000 -> via -> N || -730092327000000 -> from -> E || -730092327000000 -> to -> E || 730092327000000 -> from -> E || 730092327000000 -> to -> E && last_edit_user_name -> div006 || last_edit_changeset -> 75129398 || last_edit_time -> 1569910184000 || last_edit_user_id -> 8911933 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +10101134000000 && 2558083041000000 -> via -> N || -533462931000002 -> from -> E || -533462931000002 -> to -> E || -533462931000001 -> from -> E || -533462931000001 -> to -> E || 533462931000001 -> from -> E || 533462931000001 -> to -> E || 533462931000002 -> from -> E || 533462931000002 -> to -> E && last_edit_user_name -> div006 || last_edit_changeset -> 75137528 || last_edit_time -> 1569920612000 || last_edit_user_id -> 8911933 || restriction -> no_u_turn || type -> restriction || last_edit_version -> 1 +2353586000000 && 2581000000000 -> -> R && last_edit_user_name -> JaLooNz || last_edit_changeset -> 61784221 || last_edit_time -> 1534652650000 || last_edit_user_id -> 741163 || name -> Sentosa Public Transport Services || type -> operator || last_edit_version -> 4 || network -> Sentosa diff --git a/src/test/resources/data/ButterflyPark/ButterflyPark.osm b/src/test/resources/data/ButterflyPark/ButterflyPark.osm new file mode 100644 index 0000000000..df88ca1ded --- /dev/null +++ b/src/test/resources/data/ButterflyPark/ButterflyPark.osm @@ -0,0 +1,1634 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/data/polygon/jsonl/samples.jsonl b/src/test/resources/data/polygon/jsonl/samples.jsonl new file mode 100644 index 0000000000..28e6c14bb1 --- /dev/null +++ b/src/test/resources/data/polygon/jsonl/samples.jsonl @@ -0,0 +1,15 @@ +[[113.57867490000001,22.1617608],[113.5787899,22.1617241],[113.5788852,22.1615436],[113.58502050000001,22.1635048],[113.5848607,22.1639116],[113.5847,22.1645271],[113.5848301,22.16492],[113.58499850000001,22.165171],[113.58519380000001,22.1653409],[113.5858101,22.1655501],[113.58638560000001,22.16573],[113.5867355,22.1657165],[113.58719470000001,22.165483],[113.58740010000001,22.1653146],[113.58751760000001,22.1650176],[113.587962,22.1637974],[113.5919202,22.1529645],[113.5982532,22.1359751],[113.5983546,22.13555],[113.5982529,22.1352573],[113.59810250000001,22.1349825],[113.59787770000001,22.134755],[113.59584810000001,22.1340829],[113.5955243,22.1340282],[113.59521,22.134029],[113.5947813,22.1342792],[113.5944831,22.1345515],[113.5943677,22.1349493],[113.5908334,22.144539],[113.5904614,22.1455896],[113.590239,22.1460792],[113.5900867,22.146118],[113.5899435,22.146128],[113.58994860000001,22.1460879],[113.58987780000001,22.1460474],[113.58976910000001,22.1461969],[113.58964440000001,22.1463103],[113.5893676,22.1472007],[113.5894167,22.1473249],[113.58958050000001,22.1475323],[113.5767795,22.1554512],[113.5767596,22.1554508],[113.5766004,22.1553861],[113.5766518,22.1553386],[113.5766367,22.1552071],[113.575556,22.1542885],[113.5710616,22.1523376],[113.5704741,22.1533031],[113.569752,22.1544897],[113.56972180000001,22.1548813],[113.5697522,22.1550823],[113.56981850000001,22.1552505],[113.5699057,22.1554302],[113.5700359,22.1556198],[113.5701742,22.1557569],[113.57035,22.1558939],[113.5736248,22.1574451],[113.5738848,22.1575983],[113.5741076,22.1577823],[113.574296,22.1579898],[113.5744435,22.1582094],[113.5745862,22.1585032],[113.57464580000001,22.1586628],[113.5746813,22.1587732],[113.574954,22.1596271],[113.5749476,22.1598031],[113.57488780000001,22.1601868],[113.57445220000001,22.1608593],[113.57409770000001,22.1612627],[113.57393110000001,22.1616227],[113.57363,22.1622396],[113.5736099,22.162423],[113.573614,22.1625198],[113.57363,22.1626045],[113.5736766,22.1626672],[113.57375,22.1627199],[113.5738512,22.1627473],[113.5739659,22.1627333],[113.574725,22.1625465],[113.5749168,22.1625754],[113.5750619,22.1625981],[113.57867490000001,22.1617608]] +[[-122.4222554295,37.8248723624],[-122.4237360089,37.825397785],[-122.4246586888,37.8257452398],[-122.4255813687,37.826448619],[-122.4251307575,37.8271096198],[-122.4240149586,37.8283214392],[-122.4211181729,37.8279824707],[-122.4206353752,37.8266689532],[-122.4216224281,37.825397785],[-122.4222554295,37.8248723624]] +[[104.0012689,1.3298339],[104.0023393,1.3293889],[104.0006106,1.3253055],[104.00137570000001,1.3249994],[104.0017675,1.3258912],[104.0030763,1.3253328],[104.0034219,1.32607],[104.0036036,1.3265069],[104.00409250000001,1.3263057],[104.0039942,1.3260757],[104.0040443,1.3259904],[104.004137,1.3259533],[104.0040294,1.3256473],[104.00419640000001,1.3255768],[104.0039237,1.3249444],[104.007315,1.323511],[104.0077227,1.323629],[104.00801240000001,1.3238757],[104.01095210000001,1.330751],[104.01310860000001,1.3358673],[104.01498620000001,1.3404258],[104.0176469,1.3466575],[104.0199965,1.3521492],[104.0215307,1.3558496],[104.0215307,1.3563215],[104.0213269,1.356772],[104.02099430000001,1.3570402],[104.0204353,1.3572565],[104.0193313,1.3576837],[104.0183121,1.3580913],[104.0158287,1.3590646],[104.0175298,1.3631728],[104.0167078,1.363513],[104.0150066,1.3594048],[104.013946,1.3598853],[104.0012689,1.3298339]] +[[103.9713552,1.3413031],[103.972305,1.3405202],[103.9749743,1.3394137],[103.9784163,1.337997],[103.9794011,1.3381183],[103.9800878,1.3388782],[103.9813761,1.3384824],[103.9817473,1.3377078],[103.981902,1.33701],[103.9814785,1.3358926],[103.9834757,1.335044],[103.9835767,1.3342784],[103.98076990000001,1.3275081],[103.9788494,1.322635],[103.9804982,1.323031],[103.9811172,1.3227241],[103.9811578,1.3223531],[103.9807667,1.3212517],[103.9824174,1.3204245],[103.98295750000001,1.3203138],[103.98337330000001,1.3203384],[103.98385610000001,1.3204865],[103.98423810000001,1.3207518],[103.9844994,1.3210849],[104.0023106,1.3637289],[104.0039464,1.3674551],[104.003957,1.3678025],[104.0038059,1.368311],[104.00352930000001,1.3688136],[104.0031199,1.3691444],[104.0003518,1.3703536],[104.0001418,1.3705751],[103.9998915,1.3710473],[103.99972070000001,1.3715334],[103.9996581,1.3720635],[103.9996883,1.3734455],[103.9977972,1.3742601],[103.9955209,1.368784],[103.991663,1.3704769],[103.998084,1.385966],[103.9965914,1.3866055],[103.99680230000001,1.387099],[103.99599850000001,1.3874475],[103.9968906,1.3894406],[103.99412190000001,1.3897295],[103.9931568,1.3874164],[103.9919835,1.3879203],[103.9916641,1.3871306],[103.9908817,1.3874363],[103.9879901,1.3805408],[103.9874338,1.3772738],[103.9713552,1.3413031]] +[[103.8618108,1.4103426],[103.8612068,1.4093894],[103.8607667,1.4088788],[103.8606449,1.4084926],[103.8606857,1.4078665],[103.860675,1.4075965],[103.860616,1.4073266],[103.860321,1.4068349],[103.8600881,1.4064186],[103.8601096,1.4062331],[103.8628984,1.4043086],[103.8640385,1.406202],[103.8680919,1.4122578],[103.8695671,1.4113094],[103.8699513,1.4118973],[103.8692741,1.4123534],[103.8697729,1.4131512],[103.871453,1.4156424],[103.8709427,1.4159833],[103.8734169,1.4197729],[103.8737339,1.4195427],[103.8745809,1.4208349],[103.874584,1.421049],[103.8741932,1.4216462],[103.8738063,1.4220361],[103.8734881,1.4219623],[103.8730552,1.4220908],[103.8731026,1.4223908],[103.8729595,1.4227112],[103.8730112,1.4229981],[103.8728557,1.4231683],[103.8727174,1.4234246],[103.8730275,1.4239577],[103.873705,1.4232741],[103.8747738,1.4248319],[103.8738714,1.4254075],[103.8754989,1.4279216],[103.8744483,1.4286122],[103.8721701,1.4252403],[103.8719102,1.4247713],[103.8715338,1.4241805],[103.8713168,1.4241859],[103.86986750000001,1.4245786],[103.8691425,1.424517],[103.868598,1.4245542],[103.8673568,1.4245616],[103.8671265,1.4243387],[103.8674272,1.4240344],[103.8679261,1.4237443],[103.866421,1.4214335],[103.86571540000001,1.4219151],[103.8641323,1.4194202],[103.86369130000001,1.4184195],[103.8631901,1.4177118],[103.8625806,1.416788],[103.862529,1.4166122],[103.8625064,1.4163251],[103.86245120000001,1.4162245],[103.86197530000001,1.4153855],[103.86224630000001,1.4141277],[103.8623754,1.4128183],[103.8623007,1.4118791],[103.8620339,1.410912],[103.8618108,1.4103426]] +[[113.89361140000001,22.3110822],[113.8932252,22.3109233],[113.892839,22.3106454],[113.8925995,22.3102756],[113.8924956,22.3098514],[113.8924956,22.3094146],[113.892513,22.3091865],[113.8926244,22.3088588],[113.8928819,22.3085015],[113.8932681,22.3083029],[113.8937831,22.3080647],[113.89570990000001,22.3076301],[113.8959188,22.3075057],[113.8960869,22.3073522],[113.896193,22.3072104],[113.8963568,22.3067777],[113.8969313,22.3052605],[113.8967004,22.3051816],[113.8970489,22.3042791],[113.89735630000001,22.3043775],[113.89812280000001,22.3023661],[113.8980253,22.3022728],[113.8980849,22.3020425],[113.8980982,22.3017759],[113.8980317,22.3014453],[113.8944268,22.2968679],[113.894154,22.2964714],[113.8939773,22.2961292],[113.8938751,22.2957451],[113.8938689,22.2952796],[113.89391680000001,22.2949052],[113.8940835,22.2946443],[113.894341,22.2943267],[113.8947272,22.294009],[113.89528050000001,22.2937002],[113.8960576,22.2936119],[113.8968301,22.2936119],[113.89760260000001,22.2935834],[113.89908930000001,22.293632],[113.9003095,22.2939546],[113.9012008,22.294282],[113.9023232,22.2945339],[113.9035794,22.294762],[113.904984,22.2949335],[113.90631440000001,22.2950526],[113.9079903,22.2951888],[113.9106955,22.2951945],[113.9116259,22.2951888],[113.9144851,22.2953306],[113.9150661,22.2953306],[113.9157227,22.2953306],[113.9159585,22.29531],[113.91609910000001,22.2952114],[113.9163059,22.2951967],[113.9165558,22.2951789],[113.9168915,22.2950938],[113.9171741,22.2950148],[113.917483,22.2949489],[113.9178713,22.2948455],[113.9182717,22.2947195],[113.9194426,22.2943414],[113.9199703,22.2941585],[113.9205833,22.2939737],[113.9212495,22.2937729],[113.92253640000001,22.2933849],[113.9244002,22.292848],[113.9272084,22.2920391],[113.927618,22.291899],[113.9280031,22.291785],[113.928436,22.2916353],[113.9287749,22.2915227],[113.9289428,22.2914871],[113.9290383,22.2915213],[113.929078,22.2916132],[113.9313551,22.2909542],[113.931996,22.2931665],[113.9328771,22.2946032],[113.933284,22.2955042],[113.9396112,22.3111174],[113.943222,22.3158458],[113.9443305,22.3194954],[113.9449129,22.3203413],[113.9458058,22.3208891],[113.9456745,22.3230817],[113.9449535,22.3238158],[113.9447377,22.3238203],[113.9445526,22.3235556],[113.94420860000001,22.3232021],[113.9436936,22.3226463],[113.9395894,22.3218646],[113.9392319,22.3217854],[113.939026,22.3217191],[113.9360386,22.3207566],[113.9358255,22.3207739],[113.93566310000001,22.3208721],[113.9346461,22.3234694],[113.9337197,22.3239464],[113.9278739,22.3221448],[113.9208644,22.3198762],[113.89361140000001,22.3110822]] +[[103.6992448,1.3832097],[103.6991025,1.3829774],[103.6976545,1.3808068],[103.6976208,1.3807517],[103.6976637,1.3807257],[103.6974768,1.3804333],[103.6971726,1.3799252],[103.6969507,1.3794709],[103.6968577,1.3792205],[103.6967718,1.3789486],[103.696804,1.3789128],[103.6967718,1.3787947],[103.6963102,1.37726],[103.695929,1.3759434],[103.6956228,1.3748325],[103.695451,1.3739524],[103.6975991,1.3728511],[103.6979064,1.3727517],[103.6980749,1.3727271],[103.6991843,1.3726965],[103.7013769,1.3725678],[103.7020698,1.372538],[103.7020702,1.3725588],[103.7021297,1.3725567],[103.7021296,1.3725351],[103.7037545,1.3724862],[103.7037545,1.3725122],[103.7038154,1.3725111],[103.7038154,1.3724876],[103.7042544,1.3724325],[103.7042958,1.3724065],[103.7044475,1.3723758],[103.7045103,1.3723743],[103.7045961,1.3723651],[103.7046589,1.372287],[103.7047784,1.3722196],[103.7049102,1.3721644],[103.7051791,1.3719959],[103.7058748,1.3715318],[103.7060602,1.3714018],[103.7061802,1.3712938],[103.7066983,1.3709265],[103.7068467,1.3708441],[103.7070867,1.3707212],[103.7073296,1.3706357],[103.7075515,1.3705832],[103.7077277,1.370557],[103.7078657,1.3705548],[103.7081735,1.3705772],[103.7082565,1.3705997],[103.7094993,1.3713948],[103.7101975,1.3718394],[103.711056,1.3724316],[103.7121604,1.3732126],[103.7129004,1.373373],[103.7131628,1.373418],[103.713627,1.3735305],[103.713585,1.3736669],[103.7137413,1.37383],[103.7138159,1.3739052],[103.7141368,1.3739697],[103.7144457,1.3744899],[103.7147426,1.3745499],[103.7148111,1.3745652],[103.7148679,1.3745778],[103.715027,1.3752075],[103.7151648,1.3757417],[103.7154628,1.3769947],[103.7156133,1.3777283],[103.7157088,1.3780688],[103.7157716,1.3782945],[103.7158256,1.3784323],[103.7159168,1.3786348],[103.7159705,1.3789287],[103.7162296,1.3803478],[103.7162519,1.3804231],[103.7163055,1.3805047],[103.7163479,1.3805514],[103.7164115,1.3806097],[103.7164613,1.3806383],[103.7165207,1.3806648],[103.7165928,1.3806764],[103.718212,1.3808464],[103.7183882,1.3808786],[103.7187473,1.3809888],[103.7189483,1.3810675],[103.7190997,1.3811559],[103.7191815,1.3812219],[103.7192459,1.3813253],[103.71931,1.3814745],[103.7193363,1.3815757],[103.7193498,1.3817203],[103.7193438,1.3818388],[103.719331,1.3819257],[103.719286,1.3820614],[103.7192317,1.3821686],[103.7191447,1.3822938],[103.7190247,1.3824114],[103.71809,1.3833444],[103.718037,1.3834154],[103.7180083,1.3835002],[103.718002,1.3835786],[103.7180115,1.3836783],[103.7180507,1.383673],[103.7180963,1.383762],[103.7181536,1.3838299],[103.7186944,1.3843175],[103.7188471,1.3845019],[103.7196467,1.3857045],[103.7197509,1.3859114],[103.7197994,1.3860449],[103.7198449,1.3862136],[103.7198727,1.3864572],[103.7198698,1.3865438],[103.7198316,1.3865482],[103.7198272,1.3867228],[103.7198096,1.3868915],[103.7197627,1.3870661],[103.7197018,1.3872363],[103.7195946,1.3874534],[103.7193407,1.3877923],[103.7190714,1.3881092],[103.7197832,1.3887166],[103.7198023,1.3892873],[103.7197509,1.3897025],[103.7197216,1.3900635],[103.7200591,1.3911917],[103.7200444,1.3914705],[103.7199901,1.3914763],[103.7195808,1.3916072],[103.7195452,1.3916185],[103.7195099,1.3916298],[103.7194897,1.3916363],[103.7193635,1.3914998],[103.7189496,1.3916201],[103.7189082,1.3914809],[103.7187031,1.3907912],[103.7180016,1.3908073],[103.7177235,1.3911418],[103.7172744,1.3910523],[103.7169134,1.3908102],[103.7167585,1.3905462],[103.7165149,1.3901691],[103.7155544,1.3899329],[103.7153717,1.3894135],[103.7152469,1.3885772],[103.7147443,1.3876383],[103.7146775,1.3875414],[103.7146158,1.3874871],[103.7145557,1.3874637],[103.7144353,1.3874343],[103.7140288,1.3875649],[103.7138439,1.3876529],[103.7136693,1.3877747],[103.713524,1.3879111],[103.7130874,1.3884467],[103.7149417,1.3916348],[103.7157767,1.3921952],[103.7159132,1.3923111],[103.7161304,1.3925811],[103.7162163,1.3927219],[103.7162808,1.3928701],[103.7163425,1.3930491],[103.7163836,1.3932149],[103.7164012,1.3933675],[103.7164012,1.3935362],[103.7163806,1.3937724],[103.716341,1.3939661],[103.7162639,1.3941612],[103.716148,1.3943842],[103.7159998,1.394575],[103.7158428,1.3947349],[103.7143231,1.3961111],[103.7142864,1.3961903],[103.7139342,1.3964602],[103.7139826,1.396585],[103.7154898,1.3977543],[103.7155588,1.3977396],[103.7176002,1.3950855],[103.7175297,1.3950298],[103.7184052,1.3939808],[103.7189548,1.3935802],[103.719712,1.3933396],[103.7197759,1.3933264],[103.7196996,1.3930799],[103.7198757,1.3929846],[103.7198581,1.3928599],[103.7198966,1.3928426],[103.719927,1.392829],[103.7199755,1.3929376],[103.7201868,1.3929816],[103.7203071,1.3931841],[103.720426,1.3931724],[103.7205948,1.3931724],[103.7209191,1.3934027],[103.7216265,1.394192],[103.7216683,1.3944194],[103.7217314,1.3945148],[103.7221248,1.3947921],[103.722204,1.3948302],[103.7222847,1.3948361],[103.7226487,1.3948038],[103.7229334,1.3949491],[103.7231256,1.3950899],[103.7233839,1.3950767],[103.723718,1.3954798],[103.7238259,1.3958242],[103.7239266,1.396069],[103.7240381,1.3962838],[103.7243039,1.3966156],[103.7243588,1.3966842],[103.7244117,1.396844],[103.7244315,1.3969374],[103.7244387,1.3970256],[103.7244283,1.3972548],[103.7244086,1.3973274],[103.7243796,1.3974021],[103.724309,1.3975318],[103.7242187,1.3976553],[103.7240428,1.3978489],[103.7239048,1.3979956],[103.7237933,1.3980602],[103.7236494,1.3981042],[103.7234381,1.398113],[103.7231152,1.3980749],[103.7228951,1.3980103],[103.7223286,1.3976025],[103.7220028,1.3975056],[103.7214114,1.3974352],[103.7209124,1.3975907],[103.720839,1.398559],[103.72023,1.3993513],[103.7200157,1.3994423],[103.7198836,1.399765],[103.7195637,1.3998883],[103.7191087,1.4000115],[103.7188182,1.400123],[103.7187389,1.4001788],[103.7178936,1.4010708],[103.7177439,1.4012615],[103.7175091,1.4017662],[103.7174386,1.4019892],[103.7173623,1.4022739],[103.717195,1.403028],[103.7150964,1.402224],[103.7136427,1.4013072],[103.713537,1.4012236],[103.7135091,1.4012661],[103.7132542,1.4014265],[103.7132278,1.4013832],[103.713164,1.4014206],[103.7131654,1.4014625],[103.7130766,1.4014623],[103.7129956,1.4015764],[103.7117057,1.4022694],[103.7114276,1.4025703],[103.7103907,1.4030703],[103.7098442,1.4039327],[103.7097743,1.4040026],[103.7097075,1.4040527],[103.7096392,1.4040967],[103.7095708,1.4041225],[103.7094843,1.4041392],[103.7094281,1.4041407],[103.7093507,1.4041286],[103.7092656,1.4041043],[103.7090728,1.4040177],[103.7083454,1.403702],[103.7082512,1.4037111],[103.7081389,1.4037308],[103.7079263,1.4037961],[103.7077288,1.4038675],[103.7076271,1.4038902],[103.7075026,1.4038933],[103.7073583,1.4038933],[103.7071898,1.4038781],[103.7070379,1.4038356],[103.7069073,1.4037764],[103.7068405,1.4037278],[103.7067843,1.4036443],[103.7067175,1.4035046],[103.7066841,1.4034242],[103.7066446,1.4032906],[103.7066127,1.4031266],[103.706555,1.4030598],[103.7064806,1.4029839],[103.706391,1.4029338],[103.7062726,1.4028974],[103.7061951,1.4028883],[103.7056583,1.4029429],[103.7047431,1.4030534],[103.7044511,1.4030887],[103.7040312,1.4029338],[103.7038581,1.4029278],[103.7037381,1.4029187],[103.7036379,1.4028959],[103.7029325,1.4026393],[103.7022811,1.4023979],[103.7020616,1.4023296],[103.701679,1.4022165],[103.7013468,1.402085],[103.7007414,1.4020944],[103.7006961,1.401911],[103.7006637,1.401725],[103.7006528,1.4014517],[103.7007085,1.4014517],[103.7007091,1.4014112],[103.7006562,1.401408],[103.7006757,1.4010657],[103.7007061,1.4010075],[103.7007592,1.4009569],[103.7008301,1.4009241],[103.7008098,1.4008608],[103.7007617,1.4008077],[103.7006706,1.4007318],[103.7006856,1.4003267],[103.7008703,1.4001775],[103.7008444,1.4000555],[103.7007752,1.3997518],[103.7005576,1.3997409],[103.7004009,1.399767],[103.7003177,1.3997358],[103.7002693,1.3996401],[103.7001362,1.398921],[103.6999162,1.3978394],[103.6978431,1.3853736],[103.6977528,1.3848306],[103.6979914,1.3845907],[103.6981778,1.3844629],[103.6983013,1.3843242],[103.6983858,1.3841509],[103.6991009,1.3833602],[103.6992448,1.3832097]] +[[104.0012689,1.3298339],[104.0023393,1.3293889],[104.0006106,1.3253055],[104.0013757,1.3249994],[104.0017675,1.3258912],[104.0030763,1.3253328],[104.0034219,1.32607],[104.0036036,1.3265069],[104.0040925,1.3263057],[104.0039942,1.3260757],[104.0040443,1.3259904],[104.004137,1.3259533],[104.0040294,1.3256473],[104.0041964,1.3255768],[104.0039237,1.3249444],[104.007315,1.323511],[104.0077227,1.323629],[104.0080124,1.3238757],[104.0109521,1.330751],[104.0131086,1.3358673],[104.0149862,1.3404258],[104.0176469,1.3466575],[104.0199965,1.3521492],[104.0215307,1.3558496],[104.0215307,1.3563215],[104.0213269,1.356772],[104.0209943,1.3570402],[104.0204353,1.3572565],[104.0193313,1.3576837],[104.0183121,1.3580913],[104.0158287,1.3590646],[104.0175298,1.3631728],[104.0167078,1.363513],[104.0150066,1.3594048],[104.013946,1.3598853],[104.0012689,1.3298339]] +[[103.8128607,1.443512],[103.8142125,1.4428899],[103.8171308,1.4412167],[103.8184741,1.4401489],[103.8197388,1.4389957],[103.8211004,1.4375701],[103.8226889,1.435528],[103.8234024,1.4351293],[103.8239856,1.434943],[103.8231911,1.434315],[103.822492,1.4346891],[103.8223664,1.4344168],[103.822871,1.4340571],[103.8230623,1.4340304],[103.8241408,1.4349022],[103.8243845,1.434891],[103.8244402,1.4348951],[103.8255361,1.4349742],[103.8258043,1.4348298],[103.8261725,1.4335319],[103.8263792,1.4319641],[103.8264188,1.4312537],[103.8264526,1.4305984],[103.8263935,1.4305306],[103.8263295,1.4304013],[103.8263553,1.4303012],[103.8264482,1.4301868],[103.8266743,1.4287401],[103.8251601,1.4291726],[103.8240831,1.4287704],[103.8244618,1.4283663],[103.82428,1.4275824],[103.8238795,1.4275223],[103.823706,1.426758],[103.8235994,1.4262163],[103.8237057,1.4260447],[103.8231166,1.4244516],[103.8251858,1.4238417],[103.8249169,1.4225223],[103.8247317,1.4222102],[103.8245428,1.422038],[103.8245225,1.4219032],[103.8246031,1.4217943],[103.8245844,1.42124],[103.8246905,1.4210765],[103.8244027,1.4194585],[103.8226617,1.4194529],[103.8226066,1.4190893],[103.8225907,1.4190161],[103.8225611,1.4189593],[103.8224339,1.4188273],[103.8217946,1.4183259],[103.8216711,1.4182435],[103.820845,1.4177962],[103.8197528,1.4172473],[103.8196942,1.417243],[103.8195436,1.4175566],[103.8194848,1.4177404],[103.8194486,1.4179335],[103.8192544,1.4179097],[103.8190348,1.4179474],[103.8186849,1.417984],[103.8184958,1.4180278],[103.8175801,1.4188651],[103.8175496,1.4189292],[103.8175466,1.4189881],[103.8175659,1.419047],[103.8176411,1.4191222],[103.8175453,1.4192342],[103.8173264,1.4190523],[103.8170498,1.4192464],[103.8167783,1.4193],[103.8161714,1.4194158],[103.816143,1.4194616],[103.8160586,1.4194799],[103.8160037,1.4194494],[103.8154799,1.4195422],[103.8154732,1.4195124],[103.8154683,1.4194905],[103.8155037,1.4194318],[103.8154025,1.4189568],[103.8155016,1.4185592],[103.8153717,1.418247],[103.8153019,1.4179657],[103.8152502,1.4178047],[103.8147282,1.4176486],[103.8146441,1.417727],[103.814111,1.4178132],[103.8140169,1.4173287],[103.8135099,1.4173862],[103.8131241,1.4169121],[103.8123632,1.4168731],[103.8113214,1.4181371],[103.8107033,1.4185785],[103.8099424,1.418858],[103.8091744,1.418115],[103.8092487,1.4167847],[103.8088594,1.4159143],[103.8088481,1.4156471],[103.8064448,1.4146604],[103.8059513,1.4147891],[103.805608,1.415068],[103.8003938,1.4197014],[103.7962938,1.4224298],[103.796171,1.4229032],[103.7961784,1.423393],[103.7994496,1.4271235],[103.8105647,1.4401227],[103.811702,1.4422464],[103.8120453,1.4437265],[103.8128607,1.443512]] +[[103.9713552,1.3413031],[103.972305,1.3405202],[103.9749743,1.3394137],[103.9784163,1.337997],[103.9794011,1.3381183],[103.9800878,1.3388782],[103.9813761,1.3384824],[103.9817473,1.3377078],[103.981902,1.33701],[103.9814785,1.3358926],[103.9834757,1.335044],[103.9835767,1.3342784],[103.9807699,1.3275081],[103.9788494,1.322635],[103.9804982,1.323031],[103.9811172,1.3227241],[103.9811578,1.3223531],[103.9807667,1.3212517],[103.9824174,1.3204245],[103.9829575,1.3203138],[103.9833733,1.3203384],[103.9838561,1.3204865],[103.9842381,1.3207518],[103.9844994,1.3210849],[104.0023106,1.3637289],[104.0039464,1.3674551],[104.003957,1.3678025],[104.0038059,1.368311],[104.0035293,1.3688136],[104.0031199,1.3691444],[104.0003518,1.3703536],[104.0001418,1.3705751],[103.9998915,1.3710473],[103.9997207,1.3715334],[103.9996581,1.3720635],[103.9996883,1.3734455],[103.9977972,1.3742601],[103.9955209,1.368784],[103.991663,1.3704769],[103.998084,1.385966],[103.9965914,1.3866055],[103.9968023,1.387099],[103.9959985,1.3874475],[103.9968906,1.3894406],[103.9941219,1.3897295],[103.9931568,1.3874164],[103.9919835,1.3879203],[103.9916641,1.3871306],[103.9908817,1.3874363],[103.9879901,1.3805408],[103.9874338,1.3772738],[103.9713552,1.3413031]] +[[103.9875214,1.3873926],[103.9886479,1.3875213],[103.98912,1.3878967],[103.9907669,1.3873229],[103.9853681,1.3730217],[103.9843623,1.3733419],[103.9826704,1.3713238],[103.9823485,1.3733081],[103.9819483,1.3739104],[103.9814119,1.3743608],[103.9815438,1.3754747],[103.9811329,1.375959],[103.9778746,1.3765473],[103.9785473,1.3773211],[103.978794,1.3782757],[103.9788644,1.3787205],[103.9792009,1.3791361],[103.978768,1.3792456],[103.9785795,1.3841213],[103.9787511,1.3851295],[103.9792795,1.3860771],[103.9806737,1.3868863],[103.9807475,1.3871328],[103.9809446,1.3870738],[103.9812885,1.387169],[103.9816423,1.3873052],[103.9815851,1.387532],[103.9824394,1.3877752],[103.9833132,1.3877417],[103.9833144,1.3876163],[103.9834485,1.3876118],[103.983418,1.3869649],[103.9832657,1.3865745],[103.983335,1.3854545],[103.9821731,1.3834911],[103.9822745,1.3821743],[103.9845506,1.3805383],[103.9849572,1.3809904],[103.9845227,1.3818399],[103.9851812,1.3827709],[103.9859204,1.3833844],[103.9861672,1.3831444],[103.986455,1.3832822],[103.986411,1.38558],[103.9875413,1.3856212],[103.9875214,1.3873926]] +[[103.9020233,1.3388959],[103.9028379,1.3410049],[103.9067845,1.3409146],[103.9079985,1.3409244],[103.9084277,1.3410478],[103.9100584,1.3422598],[103.9115819,1.3433538],[103.9134537,1.3418311],[103.9139499,1.3425731],[103.9140408,1.3426463],[103.9153107,1.3417224],[103.9159457,1.3429042],[103.9158532,1.3429776],[103.9140941,1.3443743],[103.9140654,1.3444446],[103.9140566,1.3445179],[103.9140787,1.3445835],[103.9141146,1.3446537],[103.9144502,1.3448907],[103.9148587,1.3460027],[103.9154376,1.3464915],[103.9158196,1.3466307],[103.9162082,1.3468868],[103.9161936,1.3470546],[103.9162659,1.3472296],[103.9163946,1.347664],[103.9177671,1.3508888],[103.9194845,1.3550112],[103.9229231,1.36312],[103.9231645,1.3633559],[103.9250367,1.3677052],[103.9239155,1.3688583],[103.9210126,1.3719972],[103.9203045,1.3731073],[103.9199673,1.3741353],[103.9198861,1.3759764],[103.9198471,1.3766193],[103.9196447,1.3782503],[103.9193282,1.3790225],[103.9189151,1.3795481],[103.9183787,1.3798859],[103.9178154,1.3799825],[103.9171725,1.3798361],[103.9155202,1.3790263],[103.9148658,1.3785543],[103.9144259,1.3779752],[103.9139216,1.3767256],[103.9104401,1.3684882],[103.9089166,1.364761],[103.9077898,1.3637099],[103.9071783,1.3630556],[103.9062663,1.3628625],[103.9052627,1.3609013],[103.9036319,1.3569328],[103.9017602,1.356234],[103.9001192,1.3527481],[103.9002158,1.3523941],[103.8998982,1.3521491],[103.898997,1.3498323],[103.8987889,1.3491871],[103.8985249,1.3481054],[103.898643,1.3471401],[103.8990195,1.3456905],[103.8991912,1.3450952],[103.8992878,1.3443497],[103.8992985,1.3437118],[103.8994272,1.3432613],[103.8993682,1.3427358],[103.8985421,1.340789],[103.8984509,1.3405477],[103.8985582,1.3403117],[103.8999798,1.3395555],[103.9012509,1.3390514],[103.9020233,1.3388959]] +[[103.8618108,1.4103426],[103.8612068,1.4093894],[103.8607667,1.4088788],[103.8606449,1.4084926],[103.8606857,1.4078665],[103.860675,1.4075965],[103.860616,1.4073266],[103.860321,1.4068349],[103.8600881,1.4064186],[103.8601096,1.4062331],[103.8628984,1.4043086],[103.8640385,1.406202],[103.8680919,1.4122578],[103.8695671,1.4113094],[103.8699513,1.4118973],[103.8692741,1.4123534],[103.8697729,1.4131512],[103.8706255,1.4144104],[103.8717721,1.4136798],[103.8724065,1.4146464],[103.8713014,1.4153811],[103.871453,1.4156424],[103.8709427,1.4159833],[103.8734169,1.4197729],[103.8737339,1.4195427],[103.8745809,1.4208349],[103.874584,1.421049],[103.8741932,1.4216462],[103.8738063,1.4220361],[103.8734881,1.4219623],[103.8730552,1.4220908],[103.8731026,1.4223908],[103.873028,1.4225579],[103.8729595,1.4227112],[103.8730112,1.4229981],[103.8728557,1.4231683],[103.8727174,1.4234246],[103.8730275,1.4239577],[103.873705,1.4232741],[103.8747738,1.4248319],[103.8738714,1.4254075],[103.8754989,1.4279216],[103.8744483,1.4286122],[103.8721701,1.4252403],[103.8719102,1.4247713],[103.8715338,1.4241805],[103.8713168,1.4241859],[103.8698675,1.4245786],[103.8691425,1.424517],[103.868598,1.4245542],[103.8673568,1.4245616],[103.8671265,1.4243387],[103.8674272,1.4240344],[103.8679261,1.4237443],[103.866421,1.4214335],[103.8657154,1.4219151],[103.8641323,1.4194202],[103.8636913,1.4184195],[103.8631901,1.4177118],[103.8625806,1.416788],[103.862529,1.4166122],[103.8625064,1.4163251],[103.8624512,1.4162245],[103.8619753,1.4153855],[103.8622463,1.4141277],[103.8623754,1.4128183],[103.8623007,1.4118791],[103.8620339,1.410912],[103.8618108,1.4103426]] +[[113.5786749,22.1617608],[113.5750619,22.1625981],[113.5749168,22.1625754],[113.574725,22.1625465],[113.5739659,22.1627333],[113.5738512,22.1627473],[113.57375,22.1627199],[113.5736766,22.1626672],[113.57363,22.1626045],[113.573614,22.1625198],[113.5736099,22.162423],[113.57363,22.1622396],[113.5739311,22.1616227],[113.5740977,22.1612627],[113.5744522,22.1608593],[113.5748878,22.1601868],[113.5749476,22.1598031],[113.574954,22.1596271],[113.5746813,22.1587732],[113.5746755,22.1587552],[113.5746458,22.1586628],[113.5746039,22.1585507],[113.5745862,22.1585032],[113.5745219,22.1583708],[113.5744435,22.1582094],[113.574296,22.1579898],[113.5742727,22.1579642],[113.5741076,22.1577823],[113.5738848,22.1575983],[113.5738772,22.1575938],[113.5736248,22.1574451],[113.5732693,22.1572767],[113.57035,22.1558939],[113.5701742,22.1557569],[113.5700359,22.1556198],[113.5699057,22.1554302],[113.5698185,22.1552505],[113.5697522,22.1550823],[113.5697218,22.1548813],[113.569752,22.1544897],[113.5704667,22.1533025],[113.5710616,22.1523376],[113.575556,22.1542885],[113.5766367,22.1552071],[113.5766518,22.1553386],[113.5766004,22.1553861],[113.576744,22.1554431],[113.576759,22.1554339],[113.5895686,22.1475204],[113.5894167,22.1473249],[113.5893676,22.1472007],[113.5896444,22.1463103],[113.5897691,22.1461969],[113.5898778,22.1460474],[113.5899486,22.1460879],[113.5899435,22.146128],[113.5900867,22.146118],[113.590239,22.1460792],[113.5904614,22.1455896],[113.5908334,22.144539],[113.5943677,22.1349493],[113.5944831,22.1345515],[113.5947813,22.1342792],[113.59521,22.134029],[113.5955243,22.1340282],[113.5958481,22.1340829],[113.5978777,22.134755],[113.5981025,22.1349825],[113.5982529,22.1352573],[113.5983546,22.13555],[113.5982532,22.1359751],[113.5919202,22.1529645],[113.587962,22.1637974],[113.5875176,22.1650176],[113.5874001,22.1653146],[113.5871947,22.165483],[113.5867355,22.1657165],[113.5863856,22.16573],[113.5858101,22.1655501],[113.5851938,22.1653409],[113.5849985,22.165171],[113.5848301,22.16492],[113.5847,22.1645271],[113.5848314,22.16401],[113.5848607,22.1639116],[113.5850322,22.163465],[113.5788975,22.1615078],[113.5787899,22.1617241],[113.5786749,22.1617608]] +[[113.8936114,22.3110822],[113.8932252,22.3109233],[113.892839,22.3106454],[113.8925995,22.3102756],[113.8924956,22.3098514],[113.8924956,22.3094146],[113.892513,22.3091865],[113.8926244,22.3088588],[113.8928819,22.3085015],[113.8932681,22.3083029],[113.8937831,22.3080647],[113.8957099,22.3076301],[113.8959188,22.3075057],[113.8960869,22.3073522],[113.896193,22.3072104],[113.8963568,22.3067777],[113.8969313,22.3052605],[113.8967004,22.3051816],[113.8970489,22.3042791],[113.8973563,22.3043775],[113.8981228,22.3023661],[113.8980253,22.3022728],[113.8980849,22.3020425],[113.8980982,22.3017759],[113.8980317,22.3014453],[113.8944268,22.2968679],[113.894154,22.2964714],[113.8939773,22.2961292],[113.8938751,22.2957451],[113.8938689,22.2952796],[113.8939168,22.2949052],[113.8940835,22.2946443],[113.894341,22.2943267],[113.8947272,22.294009],[113.8952805,22.2937002],[113.8960576,22.2936119],[113.8968301,22.2936119],[113.8976026,22.2935834],[113.8990893,22.293632],[113.9003095,22.2939546],[113.9012008,22.294282],[113.9023232,22.2945339],[113.9035794,22.294762],[113.904984,22.2949335],[113.9063144,22.2950526],[113.9079903,22.2951888],[113.9106955,22.2951945],[113.9116259,22.2951888],[113.9144851,22.2953306],[113.9150661,22.2953306],[113.9157227,22.2953306],[113.9159585,22.29531],[113.9160991,22.2952114],[113.9163059,22.2951967],[113.9165558,22.2951789],[113.9168915,22.2950938],[113.9171741,22.2950148],[113.917483,22.2949489],[113.9178713,22.2948455],[113.9182717,22.2947195],[113.9194426,22.2943414],[113.9199703,22.2941585],[113.9205833,22.2939737],[113.9212495,22.2937729],[113.9225364,22.2933849],[113.9244002,22.292848],[113.9272084,22.2920391],[113.927618,22.291899],[113.9280031,22.291785],[113.928436,22.2916353],[113.9287749,22.2915227],[113.9289428,22.2914871],[113.9290383,22.2915213],[113.929078,22.2916132],[113.9313551,22.2909542],[113.931996,22.2931665],[113.9328771,22.2946032],[113.933284,22.2955042],[113.9396112,22.3111174],[113.943222,22.3158458],[113.9443305,22.3194954],[113.9449129,22.3203413],[113.9458058,22.3208891],[113.9459933,22.3228069],[113.9458835,22.3228166],[113.9457611,22.3229167],[113.9457789,22.3231954],[113.9456664,22.3233406],[113.945174,22.3235324],[113.9451198,22.3236117],[113.945057,22.3236522],[113.9449778,22.3236801],[113.9449856,22.3237533],[113.9447461,22.3237739],[113.9447278,22.3236687],[113.9445526,22.3235556],[113.9442086,22.3232021],[113.9436936,22.3226463],[113.9395894,22.3218646],[113.9392319,22.3217854],[113.939026,22.3217191],[113.9360386,22.3207566],[113.9358255,22.3207739],[113.9356631,22.3208721],[113.9346461,22.3234694],[113.9337197,22.3239464],[113.9278739,22.3221448],[113.9208644,22.3198762],[113.8936114,22.3110822]]