Skip to content

Commit 4db4206

Browse files
committed
feat: support for and/not/or in queries
1 parent fdab561 commit 4db4206

File tree

7 files changed

+183
-126
lines changed

7 files changed

+183
-126
lines changed

freepapermaps/src/main/java/io/github/mrmaxguns/freepapermaps/osm/TagList.java

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import org.w3c.dom.NodeList;
88

99
import java.util.HashMap;
10-
import java.util.Map;
1110

1211

1312
/**
@@ -55,38 +54,4 @@ public void insertFromXML(NodeList tags, XMLTools xmlTools) throws UserInputExce
5554
}
5655
}
5756
}
58-
59-
/**
60-
* Whether this TagList is a subset of <code>other</code>.
61-
* <p>
62-
* This is a special type of subset that is useful for queries. There is a very particular treatment of ""/null values
63-
* that allows ""/null to be used as a "wildcard". In other words, if this TagList contains a key with a ""/null value,
64-
* it will count as a match even if the other TagList we're checking has a non-null value. The wildcard property
65-
* does not work the other way around.
66-
*/
67-
public boolean isQuerySubset(TagList other) {
68-
if (this == other) {
69-
return true;
70-
}
71-
72-
for (Map.Entry<String, String> entry : entrySet()) {
73-
if (!other.containsKey(entry.getKey())) {
74-
return false;
75-
}
76-
77-
String val = entry.getValue();
78-
String otherVal = other.get(entry.getKey());
79-
boolean valIsEmpty = val == null || val.isEmpty();
80-
boolean otherValIsEmpty = otherVal == null || otherVal.isEmpty();
81-
82-
if (!valIsEmpty && otherValIsEmpty) {
83-
return false;
84-
} else if (!valIsEmpty) {
85-
if (!val.equals(otherVal)) {
86-
return false;
87-
}
88-
}
89-
}
90-
return true;
91-
}
9257
}

freepapermaps/src/main/java/io/github/mrmaxguns/freepapermaps/styling/MapStyle.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public static MapStyle debugMapStyle() {
126126
MapStyle style = new MapStyle();
127127

128128
// Draw all ways
129-
WaySelector waySelector = new WaySelector("ways");
129+
WaySelector waySelector = new WaySelector("ways", new TagQuery(new TagQuery.And()));
130130
style.addWaySelector(waySelector);
131131
style.addWayLayer(new PolylineLayer("ways", null, null, null));
132132

freepapermaps/src/main/java/io/github/mrmaxguns/freepapermaps/styling/NodeSelector.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
/** A Selector for Nodes. */
1010
public class NodeSelector extends Selector<Node> {
11-
public NodeSelector(String id) {
12-
super(id);
11+
public NodeSelector(String id, TagQuery query) {
12+
super(id, query);
1313
}
1414

1515
public static NodeSelector fromXML(Element rawSelector) throws UserInputException {
@@ -19,12 +19,11 @@ public static NodeSelector fromXML(Element rawSelector) throws UserInputExceptio
1919
/** Constructs a NodeSelector from an XML Node. */
2020
public static NodeSelector fromXML(Element rawSelector, XMLTools xmlTools) throws UserInputException {
2121
String id = xmlTools.getAttributeValue(rawSelector, "id");
22-
NodeSelector newNodeSelector = new NodeSelector(id);
23-
newNodeSelector.getTags().insertFromXML(rawSelector.getChildNodes());
24-
return newNodeSelector;
22+
TagQuery query = TagQuery.fromXML(rawSelector, xmlTools);
23+
return new NodeSelector(id, query);
2524
}
2625

2726
public boolean matches(Node val) {
28-
return getTags().isQuerySubset(val.getTags());
27+
return getQuery().matches(val.getTags());
2928
}
3029
}
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package io.github.mrmaxguns.freepapermaps.styling;
22

3-
import io.github.mrmaxguns.freepapermaps.osm.TagList;
4-
53
import java.util.Objects;
64

75

@@ -11,14 +9,13 @@
119
* to specify whether the selector applies to Nodes or Ways.
1210
*/
1311
public abstract class Selector<T> {
14-
/** A list of tags that a geometry element must have to be considered "matching." */
15-
private final TagList tags;
12+
private final TagQuery query;
1613
/** A unique identifier for the selector, so that a layer can refer to it. Never <code>null</code>. */
1714
private String id;
1815

19-
public Selector(String id) {
16+
public Selector(String id, TagQuery query) {
2017
this.id = Objects.requireNonNull(id);
21-
this.tags = new TagList();
18+
this.query = query;
2219
}
2320

2421
/** Returns true whether the given geometry element matches this selector's requirements. */
@@ -32,7 +29,7 @@ public void setId(String id) {
3229
this.id = Objects.requireNonNull(id);
3330
}
3431

35-
public TagList getTags() {
36-
return tags;
32+
public TagQuery getQuery() {
33+
return query;
3734
}
3835
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package io.github.mrmaxguns.freepapermaps.styling;
2+
3+
import io.github.mrmaxguns.freepapermaps.UserInputException;
4+
import io.github.mrmaxguns.freepapermaps.XMLTools;
5+
import io.github.mrmaxguns.freepapermaps.osm.TagList;
6+
import org.w3c.dom.Element;
7+
import org.w3c.dom.Node;
8+
import org.w3c.dom.NodeList;
9+
10+
import java.util.ArrayList;
11+
import java.util.HashMap;
12+
import java.util.Stack;
13+
14+
15+
public class TagQuery {
16+
private final QueryOperator root;
17+
18+
public TagQuery(QueryOperator root) {
19+
this.root = root;
20+
}
21+
22+
public static TagQuery fromXML(Element rootElement) throws UserInputException {
23+
return fromXML(rootElement, new XMLTools());
24+
}
25+
26+
public static TagQuery fromXML(Element rootElement, XMLTools xmlTools) throws UserInputException {
27+
Stack<QueryOperator> stack = new Stack<>();
28+
HashMap<Element, QueryOperator> elementQueryOpMapping = new HashMap<>();
29+
HashMap<QueryOperator, Element> queryOpElementMapping = new HashMap<>();
30+
HashMap<QueryOperator, QueryOperator> parentMapping = new HashMap<>();
31+
HashMap<QueryOperator, Boolean> visited = new HashMap<>();
32+
33+
// It is implied that the outermost <node>/<way> is an <and> operator.
34+
QueryOperator rootOp = new And();
35+
36+
// We are setting up a DFS traversal here. The goal is to take the nested XML hierarchy (which can be viewed
37+
// as a tree) and convert it into QueryOperators.
38+
stack.push(rootOp);
39+
// There is a 1:1 correspondence between an Element and QueryOperator, and we need to keep track of it both ways
40+
elementQueryOpMapping.put(rootElement, rootOp);
41+
queryOpElementMapping.put(rootOp, rootElement);
42+
parentMapping.put(rootOp, null); // Keep track of each node's parent
43+
visited.put(rootOp, false);
44+
45+
while (!stack.isEmpty()) {
46+
QueryOperator currentOp = stack.pop();
47+
48+
if (visited.get(currentOp)) {
49+
continue;
50+
}
51+
52+
visited.put(currentOp, true);
53+
54+
// We add the parent to the stack (if we're not at the root) since DFS graph traversal requires all adjacent
55+
// vertices to be added to the stack.
56+
if (parentMapping.get(currentOp) != null) {
57+
stack.push(currentOp);
58+
}
59+
60+
NodeList children = queryOpElementMapping.get(currentOp).getChildNodes();
61+
for (int i = 0; i < children.getLength(); ++i) {
62+
if (children.item(i).getNodeType() != Node.ELEMENT_NODE) { continue; }
63+
Element childElement = (Element) children.item(i);
64+
65+
QueryOperator childOp;
66+
if (elementQueryOpMapping.containsKey(childElement)) {
67+
// We already parsed this query, so we deal with it normally
68+
childOp = elementQueryOpMapping.get(childElement);
69+
} else {
70+
// We haven't parsed this query, so we need to add it to all the mappings first
71+
childOp = parseElement(childElement, xmlTools);
72+
elementQueryOpMapping.put(childElement, childOp);
73+
queryOpElementMapping.put(childOp, childElement);
74+
parentMapping.put(childOp, currentOp);
75+
visited.put(childOp, false);
76+
currentOp.children.add(childOp);
77+
}
78+
79+
stack.push(childOp);
80+
}
81+
}
82+
83+
return new io.github.mrmaxguns.freepapermaps.styling.TagQuery(rootOp);
84+
}
85+
86+
private static QueryOperator parseElement(Element el, XMLTools xmlTools) throws UserInputException {
87+
switch (el.getTagName()) {
88+
case "tag" -> {
89+
return new TagQueryOperator(xmlTools.getAttributeValue(el, "k"), xmlTools.getAttributeValue(el, "v"));
90+
}
91+
case "and" -> {
92+
return new And();
93+
}
94+
case "or" -> {
95+
return new Or();
96+
}
97+
case "not" -> {
98+
return new Not();
99+
}
100+
default -> throw new UserInputException(
101+
"When processing query expected one of 'tag', 'and', 'not', 'or', but got " + el.getTagName() +
102+
"'.");
103+
}
104+
}
105+
106+
public boolean matches(TagList tags) {
107+
return root.matches(tags);
108+
}
109+
110+
public static abstract class QueryOperator {
111+
public final ArrayList<QueryOperator> children;
112+
113+
protected QueryOperator(ArrayList<QueryOperator> children) { this.children = children; }
114+
115+
public abstract boolean matches(TagList tags);
116+
}
117+
118+
119+
public static class And extends QueryOperator {
120+
public And() { super(new ArrayList<>()); }
121+
122+
public boolean matches(TagList tags) {
123+
return children.stream().allMatch(i -> i.matches(tags));
124+
}
125+
}
126+
127+
128+
public static class Not extends QueryOperator {
129+
public Not() { super(new ArrayList<>()); }
130+
131+
public boolean matches(TagList tags) {
132+
return !children.stream().allMatch(i -> i.matches(tags));
133+
}
134+
}
135+
136+
137+
public static class Or extends QueryOperator {
138+
public Or() { super(new ArrayList<>()); }
139+
140+
public boolean matches(TagList tags) {
141+
return children.stream().anyMatch(i -> i.matches(tags));
142+
}
143+
}
144+
145+
146+
public static class TagQueryOperator extends QueryOperator {
147+
public final String tagKey;
148+
public final String tagValue;
149+
150+
public TagQueryOperator(String tagKey, String tagValue) {
151+
super(null);
152+
this.tagKey = tagKey.toLowerCase();
153+
this.tagValue = tagValue.toLowerCase();
154+
}
155+
156+
public boolean matches(TagList tags) {
157+
if (!tags.containsKey(tagKey)) { return false; }
158+
String otherValue = tags.get(tagKey);
159+
160+
if (tagValue.isEmpty()) {
161+
return true;
162+
}
163+
164+
return otherValue.toLowerCase().equals(tagValue);
165+
}
166+
}
167+
}

freepapermaps/src/main/java/io/github/mrmaxguns/freepapermaps/styling/WaySelector.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
/** A Selector for Ways. */
1010
public class WaySelector extends Selector<Way> {
11-
public WaySelector(String id) {
12-
super(id);
11+
public WaySelector(String id, TagQuery query) {
12+
super(id, query);
1313
}
1414

1515
public static WaySelector fromXML(Element rawSelector) throws UserInputException {
@@ -19,12 +19,11 @@ public static WaySelector fromXML(Element rawSelector) throws UserInputException
1919
/** Constructs a WaySelector from an XML Node. */
2020
public static WaySelector fromXML(Element rawSelector, XMLTools xmlTools) throws UserInputException {
2121
String id = xmlTools.getAttributeValue(rawSelector, "id");
22-
WaySelector newWaySelector = new WaySelector(id);
23-
newWaySelector.getTags().insertFromXML(rawSelector.getChildNodes());
24-
return newWaySelector;
22+
TagQuery query = TagQuery.fromXML(rawSelector, xmlTools);
23+
return new WaySelector(id, query);
2524
}
2625

2726
public boolean matches(Way val) {
28-
return getTags().isQuerySubset(val.getTags());
27+
return getQuery().matches(val.getTags());
2928
}
3029
}

freepapermaps/src/test/java/io/github/mrmaxguns/freepapermaps/osm/TagListTest.java

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -58,74 +58,4 @@ public void testInsertFromXMLMissingRequiredAttribute() throws Exception {
5858
"missing required attribute " + attr + " should cause an error");
5959
}
6060
}
61-
62-
@Test
63-
public void testIsQuerySubsetScenario1() {
64-
TagList other = new TagList();
65-
other.put("aeroway", "aerodrome");
66-
other.put("barrier", "fence");
67-
other.put("landuse", "military");
68-
69-
assertAll(() -> assertTrue(other.isQuerySubset(validTagList),
70-
"a non-proper subset should be properly identified"),
71-
() -> assertFalse(validTagList.isQuerySubset(other),
72-
"a non-proper subset should not be reported to be a superset of its superset"));
73-
}
74-
75-
@Test
76-
public void testIsQuerySubsetScenario2() {
77-
TagList other = new TagList(validTagList);
78-
assertAll(() -> assertTrue(other.isQuerySubset(validTagList), "a proper subset should be properly identified"),
79-
() -> assertTrue(validTagList.isQuerySubset(other), "a proper subset should be properly identified"));
80-
}
81-
82-
@Test
83-
public void testIsQuerySubsetScenario3() {
84-
TagList query = new TagList();
85-
query.put("landuse", "");
86-
assertAll(() -> assertTrue(query.isQuerySubset(validTagList),
87-
"empty strings should be treated as catch-all values"),
88-
() -> assertFalse(validTagList.isQuerySubset(query),
89-
"a non-proper subset should not be reported to be a superset of its superset"));
90-
}
91-
92-
@Test
93-
public void testIsQuerySubsetScenario4() {
94-
TagList query = new TagList();
95-
query.put("aeroway", "aerodrome");
96-
query.put("name", null);
97-
assertAll(() -> assertTrue(query.isQuerySubset(validTagList), "`null`s should be treated as catch-all values"),
98-
() -> assertFalse(validTagList.isQuerySubset(query),
99-
"a non-proper subset should not be reported to be a superset of its superset"));
100-
}
101-
102-
@Test
103-
public void testIsQuerySubsetScenario5() {
104-
TagList query = new TagList();
105-
query.put("barrier", "fence");
106-
query.put("building", null);
107-
assertAll(() -> assertFalse(query.isQuerySubset(validTagList),
108-
"wildcard queries that do not match should not be identified"),
109-
() -> assertFalse(validTagList.isQuerySubset(query),
110-
"a non-proper subset should not be reported to be a superset of its superset"));
111-
}
112-
113-
@Test
114-
public void testIsQuerySubsetScenario6() {
115-
TagList list = new TagList();
116-
list.put("building", "");
117-
TagList query = new TagList();
118-
query.put("building", "house");
119-
assertFalse(query.isQuerySubset(list), "wildcards only work for the query, and not in reverse");
120-
}
121-
122-
@Test
123-
public void testIsQuerySubsetScenario7() {
124-
TagList list = new TagList();
125-
list.put("amenity", null);
126-
TagList query = new TagList();
127-
query.put("amenity", "");
128-
assertAll(() -> assertTrue(query.isQuerySubset(list), "wildcards match both null and strings"),
129-
() -> assertTrue(list.isQuerySubset(query), "wildcards match both null and strings"));
130-
}
13161
}

0 commit comments

Comments
 (0)