source: josm/trunk/src/org/openstreetmap/josm/tools/SearchCompilerQueryWizard.java@ 16966

Last change on this file since 16966 was 16358, checked in by simon04, 4 years ago

see #18164 - Rename class to SearchCompilerQueryWizard

File size: 10.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import java.util.ArrayList;
5import java.util.Collections;
6import java.util.EnumSet;
7import java.util.List;
8import java.util.Locale;
9import java.util.Optional;
10import java.util.Set;
11import java.util.regex.Matcher;
12import java.util.regex.Pattern;
13import java.util.stream.Collectors;
14import java.util.stream.Stream;
15
16import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
17import org.openstreetmap.josm.data.osm.search.SearchCompiler;
18import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
19import org.openstreetmap.josm.data.osm.search.SearchParseError;
20
21/**
22 * Builds an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} query.
23 *
24 * @since 8744 (using tyrasd/overpass-wizard), 16262 (standalone)
25 */
26public final class SearchCompilerQueryWizard {
27
28 private static final SearchCompilerQueryWizard instance = new SearchCompilerQueryWizard();
29
30 /**
31 * Replies the unique instance of this class.
32 *
33 * @return the unique instance of this class
34 */
35 public static SearchCompilerQueryWizard getInstance() {
36 return instance;
37 }
38
39 private SearchCompilerQueryWizard() {
40 // private constructor for utility class
41 }
42
43 /**
44 * Builds an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} like query.
45 * @param search the {@link org.openstreetmap.josm.actions.search.SearchAction} like query
46 * @return an Overpass QL query
47 * @throws UncheckedParseException when the parsing fails
48 */
49 public String constructQuery(final String search) {
50 try {
51 Matcher matcher = Pattern.compile("\\s+GLOBAL\\s*$", Pattern.CASE_INSENSITIVE).matcher(search);
52 if (matcher.find()) {
53 final Match match = SearchCompiler.compile(matcher.replaceFirst(""));
54 return constructQuery(match, ";", "");
55 }
56
57 matcher = Pattern.compile("\\s+IN BBOX\\s*$", Pattern.CASE_INSENSITIVE).matcher(search);
58 if (matcher.find()) {
59 final Match match = SearchCompiler.compile(matcher.replaceFirst(""));
60 return constructQuery(match, "[bbox:{{bbox}}];", "");
61 }
62
63 matcher = Pattern.compile("\\s+(?<mode>IN|AROUND)\\s+(?<area>[^\" ]+|\"[^\"]+\")\\s*$", Pattern.CASE_INSENSITIVE).matcher(search);
64 if (matcher.find()) {
65 final Match match = SearchCompiler.compile(matcher.replaceFirst(""));
66 final String mode = matcher.group("mode").toUpperCase(Locale.ENGLISH);
67 final String area = Utils.strip(matcher.group("area"), "\"");
68 if ("IN".equals(mode)) {
69 return constructQuery(match, ";\n{{geocodeArea:" + area + "}}->.searchArea;", "(area.searchArea)");
70 } else if ("AROUND".equals(mode)) {
71 return constructQuery(match, ";\n{{radius=1000}}", "(around:{{radius}},{{geocodeCoords:" + area + "}})");
72 } else {
73 throw new IllegalStateException(mode);
74 }
75 }
76
77 final Match match = SearchCompiler.compile(search);
78 return constructQuery(match, "[bbox:{{bbox}}];", "");
79 } catch (SearchParseError | UnsupportedOperationException e) {
80 throw new UncheckedParseException(e);
81 }
82 }
83
84 private String constructQuery(final Match match, final String bounds, final String queryLineSuffix) {
85 final List<Match> normalized = normalizeToDNF(match);
86 final List<String> queryLines = new ArrayList<>();
87 queryLines.add("[out:xml][timeout:90]" + bounds);
88 queryLines.add("(");
89 for (Match conjunction : normalized) {
90 final EnumSet<OsmPrimitiveType> types = EnumSet.noneOf(OsmPrimitiveType.class);
91 final String query = constructQuery(conjunction, types);
92 (types.isEmpty() || types.size() == 3
93 ? Stream.of("nwr")
94 : types.stream().map(OsmPrimitiveType::getAPIName))
95 .forEach(type -> queryLines.add(" " + type + query + queryLineSuffix + ";"));
96 }
97 queryLines.add(");");
98 queryLines.add("(._;>;);");
99 queryLines.add("out meta;");
100 return String.join("\n", queryLines);
101 }
102
103 private static String constructQuery(Match match, final Set<OsmPrimitiveType> types) {
104 final boolean negated;
105 if (match instanceof SearchCompiler.Not) {
106 negated = true;
107 match = ((SearchCompiler.Not) match).getMatch();
108 } else {
109 negated = false;
110 }
111 if (match instanceof SearchCompiler.And) {
112 return ((SearchCompiler.And) match).map(m -> constructQuery(m, types), (s1, s2) -> s1 + s2);
113 } else if (match instanceof SearchCompiler.KeyValue) {
114 final String key = ((SearchCompiler.KeyValue) match).getKey();
115 final String value = ((SearchCompiler.KeyValue) match).getValue();
116 if ("newer".equals(key)) {
117 return "(newer:" + quote("{{date:" + value + "}}") + ")";
118 }
119 return "[~" + quote(key) + "~" + quote(value) + "]";
120 } else if (match instanceof SearchCompiler.ExactKeyValue) {
121 // https://wiki.openstreetmap.org/wiki/Overpass_API/Language_Guide
122 // ["key"] -- filter objects tagged with this key and any value
123 // [!"key"] -- filter objects not tagged with this key and any value
124 // ["key"="value"] -- filter objects tagged with this key and this value
125 // ["key"!="value"] -- filter objects tagged with this key but not this value, or not tagged with this key
126 // ["key"~"value"] -- filter objects tagged with this key and a value matching a regular expression
127 // ["key"!~"value"] -- filter objects tagged with this key but a value not matching a regular expression
128 // [~"key"~"value"] -- filter objects tagged with a key and a value matching regular expressions
129 // [~"key"~"value", i] -- filter objects tagged with a key and a case-insensitive value matching regular expressions
130 final String key = ((SearchCompiler.ExactKeyValue) match).getKey();
131 final String value = ((SearchCompiler.ExactKeyValue) match).getValue();
132 final SearchCompiler.ExactKeyValue.Mode mode = ((SearchCompiler.ExactKeyValue) match).getMode();
133 switch (mode) {
134 case ANY_VALUE:
135 return "[" + (negated ? "!" : "") + quote(key) + "]";
136 case EXACT:
137 return "[" + quote(key) + (negated ? "!=" : "=") + quote(value) + "]";
138 case EXACT_REGEXP:
139 final Matcher matcher = Pattern.compile("/(?<regex>.*)/(?<flags>i)?").matcher(value);
140 final String valueQuery = matcher.matches()
141 ? quote(matcher.group("regex")) + Optional.ofNullable(matcher.group("flags")).map(f -> "," + f).orElse("")
142 : quote(value);
143 return "[" + quote(key) + (negated ? "!~" : "~") + valueQuery + "]";
144 case MISSING_KEY:
145 // special case for empty values, see https://github.com/drolbr/Overpass-API/issues/53
146 return "[" + quote(key) + (negated ? "!~" : "~") + quote("^$") + "]";
147 default:
148 return "";
149 }
150 } else if (match instanceof SearchCompiler.BooleanMatch) {
151 final String key = ((SearchCompiler.BooleanMatch) match).getKey();
152 return negated
153 ? "[" + quote(key) + "~\"false|no|0|off\"]"
154 : "[" + quote(key) + "~\"true|yes|1|on\"]";
155 } else if (match instanceof SearchCompiler.UserMatch) {
156 final String user = ((SearchCompiler.UserMatch) match).getUser();
157 return user.matches("\\d+")
158 ? "(uid:" + user + ")"
159 : "(user:" + quote(user) + ")";
160 } else if (match instanceof SearchCompiler.ExactType) {
161 types.add(((SearchCompiler.ExactType) match).getType());
162 return "";
163 }
164 Logging.warn("Unsupported match type {0}: {1}", match.getClass(), match);
165 return "/*" + match + "*/";
166 }
167
168 /**
169 * Quotes the given string for its use in Overpass QL
170 * @param s the string to quote
171 * @return the quoted string
172 */
173 private static String quote(final String s) {
174 return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
175 }
176
177 /**
178 * Normalizes the match to disjunctive normal form: A∧(B∨C) ⇔ (A∧B)∨(A∧C)
179 * @param match the match to normalize
180 * @return the match in disjunctive normal form
181 */
182 private static List<Match> normalizeToDNF(final Match match) {
183 if (match instanceof SearchCompiler.And) {
184 return ((SearchCompiler.And) match).map(SearchCompilerQueryWizard::normalizeToDNF, (lhs, rhs) -> lhs.stream()
185 .flatMap(l -> rhs.stream().map(r -> new SearchCompiler.And(l, r)))
186 .collect(Collectors.toList()));
187 } else if (match instanceof SearchCompiler.Or) {
188 return ((SearchCompiler.Or) match).map(SearchCompilerQueryWizard::normalizeToDNF, CompositeList::new);
189 } else if (match instanceof SearchCompiler.Xor) {
190 throw new UnsupportedOperationException(match.toString());
191 } else if (match instanceof SearchCompiler.Not) {
192 // only support negated KeyValue or ExactKeyValue matches
193 final Match innerMatch = ((SearchCompiler.Not) match).getMatch();
194 if (innerMatch instanceof SearchCompiler.BooleanMatch
195 || innerMatch instanceof SearchCompiler.KeyValue
196 || innerMatch instanceof SearchCompiler.ExactKeyValue) {
197 return Collections.singletonList(match);
198 }
199 throw new UnsupportedOperationException(match.toString());
200 } else {
201 return Collections.singletonList(match);
202 }
203 }
204
205}
Note: See TracBrowser for help on using the repository browser.