001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.util.ArrayList; 005import java.util.Collections; 006import java.util.EnumSet; 007import java.util.List; 008import java.util.Locale; 009import java.util.Optional; 010import java.util.Set; 011import java.util.regex.Matcher; 012import java.util.regex.Pattern; 013import java.util.stream.Collectors; 014import java.util.stream.Stream; 015 016import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 017import org.openstreetmap.josm.data.osm.search.SearchCompiler; 018import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match; 019import org.openstreetmap.josm.data.osm.search.SearchParseError; 020 021/** 022 * Builds an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} query. 023 * 024 * @since 8744 (using tyrasd/overpass-wizard), 16262 (standalone) 025 */ 026public final class SearchCompilerQueryWizard { 027 028 private SearchCompilerQueryWizard() { 029 // private constructor for utility class 030 } 031 032 /** 033 * Builds an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} like query. 034 * @param search the {@link org.openstreetmap.josm.actions.search.SearchAction} like query 035 * @return an Overpass QL query 036 * @throws UncheckedParseException when the parsing fails 037 */ 038 public static String constructQuery(final String search) { 039 try { 040 Matcher matcher = Pattern.compile("\\s+GLOBAL\\s*$", Pattern.CASE_INSENSITIVE).matcher(search); 041 if (matcher.find()) { 042 final Match match = SearchCompiler.compile(matcher.replaceFirst("")); 043 return constructQuery(match, ";", ""); 044 } 045 046 matcher = Pattern.compile("\\s+IN BBOX\\s*$", Pattern.CASE_INSENSITIVE).matcher(search); 047 if (matcher.find()) { 048 final Match match = SearchCompiler.compile(matcher.replaceFirst("")); 049 return constructQuery(match, "[bbox:{{bbox}}];", ""); 050 } 051 052 matcher = Pattern.compile("\\s+(?<mode>IN|AROUND)\\s+(?<area>[^\" ]+|\"[^\"]+\")\\s*$", Pattern.CASE_INSENSITIVE).matcher(search); 053 if (matcher.find()) { 054 final Match match = SearchCompiler.compile(matcher.replaceFirst("")); 055 final String mode = matcher.group("mode").toUpperCase(Locale.ENGLISH); 056 final String area = Utils.strip(matcher.group("area"), "\""); 057 if ("IN".equals(mode)) { 058 return constructQuery(match, ";\n{{geocodeArea:" + area + "}}->.searchArea;", "(area.searchArea)"); 059 } else if ("AROUND".equals(mode)) { 060 return constructQuery(match, ";\n{{radius=1000}}", "(around:{{radius}},{{geocodeCoords:" + area + "}})"); 061 } else { 062 throw new IllegalStateException(mode); 063 } 064 } 065 066 final Match match = SearchCompiler.compile(search); 067 return constructQuery(match, "[bbox:{{bbox}}];", ""); 068 } catch (SearchParseError | UnsupportedOperationException e) { 069 throw new UncheckedParseException(e); 070 } 071 } 072 073 private static String constructQuery(final Match match, final String bounds, final String queryLineSuffix) { 074 final List<Match> normalized = normalizeToDNF(match); 075 final List<String> queryLines = new ArrayList<>(); 076 queryLines.add("[out:xml][timeout:90]" + bounds); 077 queryLines.add("("); 078 for (Match conjunction : normalized) { 079 final EnumSet<OsmPrimitiveType> types = EnumSet.noneOf(OsmPrimitiveType.class); 080 final String query = constructQuery(conjunction, types); 081 (types.isEmpty() || types.size() == 3 082 ? Stream.of("nwr") 083 : types.stream().map(OsmPrimitiveType::getAPIName)) 084 .forEach(type -> queryLines.add(" " + type + query + queryLineSuffix + ";")); 085 } 086 queryLines.add(");"); 087 queryLines.add("(._;>;);"); 088 queryLines.add("out meta;"); 089 return String.join("\n", queryLines); 090 } 091 092 private static String constructQuery(Match match, final Set<OsmPrimitiveType> types) { 093 final boolean negated; 094 if (match instanceof SearchCompiler.Not) { 095 negated = true; 096 match = ((SearchCompiler.Not) match).getMatch(); 097 } else { 098 negated = false; 099 } 100 if (match instanceof SearchCompiler.And) { 101 return ((SearchCompiler.And) match).map(m -> constructQuery(m, types), (s1, s2) -> s1 + s2); 102 } else if (match instanceof SearchCompiler.KeyValue) { 103 final String key = ((SearchCompiler.KeyValue) match).getKey(); 104 final String value = ((SearchCompiler.KeyValue) match).getValue(); 105 if ("newer".equals(key)) { 106 return "(newer:" + quote("{{date:" + value + "}}") + ")"; 107 } 108 return "[~" + quote(key) + "~" + quote(value) + "]"; 109 } else if (match instanceof SearchCompiler.ExactKeyValue) { 110 // https://wiki.openstreetmap.org/wiki/Overpass_API/Language_Guide 111 // ["key"] -- filter objects tagged with this key and any value 112 // [!"key"] -- filter objects not tagged with this key and any value 113 // ["key"="value"] -- filter objects tagged with this key and this value 114 // ["key"!="value"] -- filter objects tagged with this key but not this value, or not tagged with this key 115 // ["key"~"value"] -- filter objects tagged with this key and a value matching a regular expression 116 // ["key"!~"value"] -- filter objects tagged with this key but a value not matching a regular expression 117 // [~"key"~"value"] -- filter objects tagged with a key and a value matching regular expressions 118 // [~"key"~"value", i] -- filter objects tagged with a key and a case-insensitive value matching regular expressions 119 final String key = ((SearchCompiler.ExactKeyValue) match).getKey(); 120 final String value = ((SearchCompiler.ExactKeyValue) match).getValue(); 121 final SearchCompiler.ExactKeyValue.Mode mode = ((SearchCompiler.ExactKeyValue) match).getMode(); 122 switch (mode) { 123 case ANY_VALUE: 124 return "[" + (negated ? "!" : "") + quote(key) + "]"; 125 case EXACT: 126 return "[" + quote(key) + (negated ? "!=" : "=") + quote(value) + "]"; 127 case ANY_KEY: // *=value 128 // fall through 129 case EXACT_REGEXP: 130 final Matcher matcher = Pattern.compile("/(?<regex>.*)/(?<flags>i)?").matcher(value); 131 final String valueQuery = matcher.matches() 132 ? quote(matcher.group("regex")) + Optional.ofNullable(matcher.group("flags")).map(f -> "," + f).orElse("") 133 : quote(value); 134 if (mode == SearchCompiler.ExactKeyValue.Mode.ANY_KEY) 135 return "[~\"^.*$\"" + (negated ? "!~" : "~") + valueQuery + "]"; 136 return "[" + quote(key) + (negated ? "!~" : "~") + valueQuery + "]"; 137 case MISSING_KEY: 138 // special case for empty values, see https://github.com/drolbr/Overpass-API/issues/53 139 return "[" + quote(key) + (negated ? "!~" : "~") + quote("^$") + "]"; 140 default: 141 return ""; 142 } 143 } else if (match instanceof SearchCompiler.BooleanMatch) { 144 final String key = ((SearchCompiler.BooleanMatch) match).getKey(); 145 return negated 146 ? "[" + quote(key) + "~\"false|no|0|off\"]" 147 : "[" + quote(key) + "~\"true|yes|1|on\"]"; 148 } else if (match instanceof SearchCompiler.UserMatch) { 149 final String user = ((SearchCompiler.UserMatch) match).getUser(); 150 return user.matches("\\d+") 151 ? "(uid:" + user + ")" 152 : "(user:" + quote(user) + ")"; 153 } else if (match instanceof SearchCompiler.ExactType) { 154 types.add(((SearchCompiler.ExactType) match).getType()); 155 return ""; 156 } 157 Logging.warn("Unsupported match type {0}: {1}", match.getClass(), match); 158 return "/*" + match + "*/"; 159 } 160 161 /** 162 * Quotes the given string for its use in Overpass QL 163 * @param s the string to quote 164 * @return the quoted string 165 */ 166 private static String quote(final String s) { 167 return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; 168 } 169 170 /** 171 * Normalizes the match to disjunctive normal form: A∧(B∨C) ⇔ (A∧B)∨(A∧C) 172 * @param match the match to normalize 173 * @return the match in disjunctive normal form 174 */ 175 private static List<Match> normalizeToDNF(final Match match) { 176 if (match instanceof SearchCompiler.And) { 177 return ((SearchCompiler.And) match).map(SearchCompilerQueryWizard::normalizeToDNF, (lhs, rhs) -> lhs.stream() 178 .flatMap(l -> rhs.stream().map(r -> new SearchCompiler.And(l, r))) 179 .collect(Collectors.toList())); 180 } else if (match instanceof SearchCompiler.Or) { 181 return ((SearchCompiler.Or) match).map(SearchCompilerQueryWizard::normalizeToDNF, CompositeList::new); 182 } else if (match instanceof SearchCompiler.Xor) { 183 throw new UnsupportedOperationException(match.toString()); 184 } else if (match instanceof SearchCompiler.Not) { 185 // only support negated KeyValue or ExactKeyValue matches 186 final Match innerMatch = ((SearchCompiler.Not) match).getMatch(); 187 if (innerMatch instanceof SearchCompiler.BooleanMatch 188 || innerMatch instanceof SearchCompiler.KeyValue 189 || innerMatch instanceof SearchCompiler.ExactKeyValue) { 190 return Collections.singletonList(match); 191 } 192 throw new UnsupportedOperationException(match.toString()); 193 } else { 194 return Collections.singletonList(match); 195 } 196 } 197 198}