close Warning: HTML preview using PatchRenderer failed (ValueError: not enough values to unpack (expected 2, got 1))

Ticket #17177: 17177-r17862.patch

File 17177-r17862.patch, 685.4 KB (added by simon04, 5 years ago)
Line 
1From 900593d0f434dc3ca92919ae2104043a402d0e8e Mon Sep 17 00:00:00 2001
2From: Taylor Smock <tsmock@fb.com>
3Date: Thu, 8 Apr 2021 13:37:54 -0600
4Subject: [PATCH 01/50] Protobuf: Initial implementation
5
6Signed-off-by: Taylor Smock <tsmock@fb.com>
7---
8 .../josm/data/protobuf/ProtoBufPacked.java | 62 +++++
9 .../josm/data/protobuf/ProtoBufParser.java | 245 ++++++++++++++++++
10 .../josm/data/protobuf/ProtoBufRecord.java | 152 +++++++++++
11 .../josm/data/protobuf/WireType.java | 61 +++++
12 test/data/pbf/openinframap/17/26028/50060.pbf | Bin 0 -> 1082 bytes
13 .../data/protobuf/ProtoBufParserTest.java | 51 ++++
14 .../data/protobuf/ProtoBufRecordTest.java | 30 +++
15 .../josm/data/protobuf/ProtoBufTest.java | 211 +++++++++++++++
16 8 files changed, 812 insertions(+)
17 create mode 100644 src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
18 create mode 100644 src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
19 create mode 100644 src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
20 create mode 100644 src/org/openstreetmap/josm/data/protobuf/WireType.java
21 create mode 100644 test/data/pbf/openinframap/17/26028/50060.pbf
22 create mode 100644 test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java
23 create mode 100644 test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java
24 create mode 100644 test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
25
26diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
27new file mode 100644
28index 000000000..109f8915a
29--- /dev/null
30+++ b/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
31@@ -0,0 +1,62 @@
32+// License: GPL. For details, see LICENSE file.
33+package org.openstreetmap.josm.data.protobuf;
34+
35+import java.util.ArrayList;
36+import java.util.List;
37+
38+/**
39+ * Parse packed values (only numerical values)
40+ *
41+ * @author Taylor Smock
42+ * @since xxx
43+ */
44+public class ProtoBufPacked {
45+ private final byte[] bytes;
46+ private final Number[] numbers;
47+ private int location;
48+
49+ /**
50+ * Create a new ProtoBufPacked object
51+ *
52+ * @param bytes The packed bytes
53+ */
54+ public ProtoBufPacked(byte[] bytes) {
55+ this.location = 0;
56+ this.bytes = bytes;
57+ List<Number> numbersT = new ArrayList<>();
58+ while (this.location < bytes.length) {
59+ numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE));
60+ }
61+
62+ this.numbers = new Number[numbersT.size()];
63+ for (int i = 0; i < numbersT.size(); i++) {
64+ this.numbers[i] = numbersT.get(i);
65+ }
66+ }
67+
68+ /**
69+ * Get the parsed number array
70+ *
71+ * @return The number array
72+ */
73+ public Number[] getArray() {
74+ return this.numbers;
75+ }
76+
77+ private byte[] nextVarInt() {
78+ List<Byte> byteList = new ArrayList<>();
79+ while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE)
80+ == ProtoBufParser.MOST_SIGNIFICANT_BYTE) {
81+ // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
82+ byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE));
83+ }
84+ // The last byte doesn't drop the most significant bit
85+ byteList.add(this.bytes[this.location++]);
86+ byte[] byteArray = new byte[byteList.size()];
87+ for (int i = 0; i < byteList.size(); i++) {
88+ byteArray[i] = byteList.get(i);
89+ }
90+
91+ return byteArray;
92+ }
93+}
94diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
95new file mode 100644
96index 000000000..18059e339
97--- /dev/null
98+++ b/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
99@@ -0,0 +1,245 @@
100+// License: GPL. For details, see LICENSE file.
101+package org.openstreetmap.josm.data.protobuf;
102+
103+import java.io.BufferedInputStream;
104+import java.io.ByteArrayInputStream;
105+import java.io.IOException;
106+import java.io.InputStream;
107+import java.util.ArrayList;
108+import java.util.Collection;
109+import java.util.List;
110+
111+import org.openstreetmap.josm.tools.Logging;
112+
113+/**
114+ * A basic Protobuf parser
115+ *
116+ * @author Taylor Smock
117+ * @since xxx
118+ */
119+public class ProtoBufParser implements AutoCloseable {
120+ /**
121+ * The default byte size (see {@link #VAR_INT_BYTE_SIZE} for var ints)
122+ */
123+ public static final byte BYTE_SIZE = 8;
124+ /**
125+ * The byte size for var ints (since the first byte is just an indicator for if the var int is done)
126+ */
127+ public static final byte VAR_INT_BYTE_SIZE = BYTE_SIZE - 1;
128+ /**
129+ * Used to get the most significant byte
130+ */
131+ static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7);
132+ /**
133+ * Convert a byte array to a number (little endian)
134+ *
135+ * @param bytes The bytes to convert
136+ * @param byteSize The size of the byte. For var ints, this is 7, for other ints, this is 8.
137+ * @return An appropriate {@link Number} class.
138+ */
139+ public static Number convertByteArray(byte[] bytes, byte byteSize) {
140+ long number = 0;
141+ for (int i = 0; i < bytes.length; i++) {
142+ // Need to convert to uint64 in order to avoid bit operation from filling in 1's and overflow issues
143+ number += Byte.toUnsignedLong(bytes[i]) << (byteSize * i);
144+ }
145+ return convertLong(number);
146+ }
147+
148+ /**
149+ * Convert a long to an appropriate {@link Number} class
150+ *
151+ * @param number The long to convert
152+ * @return A {@link Number}
153+ */
154+ public static Number convertLong(long number) {
155+ // TODO deal with booleans
156+ if (number <= Byte.MAX_VALUE && number >= Byte.MIN_VALUE) {
157+ return (byte) number;
158+ } else if (number <= Short.MAX_VALUE && number >= Short.MIN_VALUE) {
159+ return (short) number;
160+ } else if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) {
161+ return (int) number;
162+ }
163+ return number;
164+ }
165+
166+ /**
167+ * Decode a zig-zag encoded value
168+ *
169+ * @param signed The value to decode
170+ * @return The decoded value
171+ */
172+ public static Number decodeZigZag(Number signed) {
173+ final long value = signed.longValue();
174+ return convertLong((value >> 1) ^ -(value & 1));
175+ }
176+
177+ /**
178+ * Encode a number to a zig-zag encode value
179+ *
180+ * @param signed The number to encode
181+ * @return The encoded value
182+ */
183+ public static Number encodeZigZag(Number signed) {
184+ final long value = signed.longValue();
185+ // This boundary condition could be >= or <= or both. Tests indicate that it doesn't actually matter.
186+ // The only difference would be the number type returned, except it is always converted to the most basic type.
187+ final int shift = (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE ? Long.BYTES : Integer.BYTES) * 8 - 1;
188+ return convertLong((value << 1) ^ (value >> shift));
189+ }
190+
191+ private final InputStream inputStream;
192+
193+ /**
194+ * Create a new parser
195+ *
196+ * @param bytes The bytes to parse
197+ */
198+ public ProtoBufParser(byte[] bytes) {
199+ this(new ByteArrayInputStream(bytes));
200+ }
201+
202+ /**
203+ * Create a new parser
204+ *
205+ * @param inputStream The InputStream (will be fully read at this time)
206+ */
207+ public ProtoBufParser(InputStream inputStream) {
208+ if (inputStream.markSupported()) {
209+ this.inputStream = inputStream;
210+ } else {
211+ this.inputStream = new BufferedInputStream(inputStream);
212+ }
213+ }
214+
215+ /**
216+ * Read all records
217+ *
218+ * @return A collection of all records
219+ * @throws IOException - if an IO error occurs
220+ */
221+ public Collection<ProtoBufRecord> allRecords() throws IOException {
222+ Collection<ProtoBufRecord> records = new ArrayList<>();
223+ while (this.hasNext()) {
224+ records.add(new ProtoBufRecord(this));
225+ }
226+ return records;
227+ }
228+
229+ @Override
230+ public void close() {
231+ try {
232+ this.inputStream.close();
233+ } catch (IOException e) {
234+ Logging.error(e);
235+ }
236+ }
237+
238+ /**
239+ * Check if there is more data to read
240+ *
241+ * @return {@code true} if there is more data to read
242+ * @throws IOException - if an IO error occurs
243+ */
244+ public boolean hasNext() throws IOException {
245+ return this.inputStream.available() > 0;
246+ }
247+
248+ /**
249+ * Get the "next" WireType
250+ *
251+ * @return {@link WireType} expected
252+ * @throws IOException - if an IO error occurs
253+ */
254+ public WireType next() throws IOException {
255+ this.inputStream.mark(16);
256+ try {
257+ return WireType.values()[this.inputStream.read() << 3];
258+ } finally {
259+ this.inputStream.reset();
260+ }
261+ }
262+
263+ /**
264+ * Get the next byte
265+ *
266+ * @return The next byte
267+ * @throws IOException - if an IO error occurs
268+ */
269+ public int nextByte() throws IOException {
270+ return this.inputStream.read();
271+ }
272+
273+ /**
274+ * Get the next 32 bits ({@link WireType#THIRTY_TWO_BIT})
275+ *
276+ * @return a byte array of the next 32 bits (4 bytes)
277+ * @throws IOException - if an IO error occurs
278+ */
279+ public byte[] nextFixed32() throws IOException {
280+ // 4 bytes == 32 bits
281+ return readNextBytes(4);
282+ }
283+
284+ /**
285+ * Get the next 64 bits ({@link WireType#SIXTY_FOUR_BIT})
286+ *
287+ * @return a byte array of the next 64 bits (8 bytes)
288+ * @throws IOException - if an IO error occurs
289+ */
290+ public byte[] nextFixed64() throws IOException {
291+ // 8 bytes == 64 bits
292+ return readNextBytes(8);
293+ }
294+
295+ /**
296+ * Get the next delimited message ({@link WireType#LENGTH_DELIMITED})
297+ *
298+ * @return The next length delimited message
299+ * @throws IOException - if an IO error occurs
300+ */
301+ public byte[] nextLengthDelimited() throws IOException {
302+ int length = convertByteArray(this.nextVarInt(), VAR_INT_BYTE_SIZE).intValue();
303+ return readNextBytes(length);
304+ }
305+
306+ /**
307+ * Get the next var int ({@code WireType#VARINT})
308+ *
309+ * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
310+ * @throws IOException - if an IO error occurs
311+ */
312+ public byte[] nextVarInt() throws IOException {
313+ List<Byte> byteList = new ArrayList<>();
314+ int currentByte = this.nextByte();
315+ while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE && currentByte > 0) {
316+ // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
317+ byteList.add((byte) (currentByte ^ MOST_SIGNIFICANT_BYTE));
318+ currentByte = this.nextByte();
319+ }
320+ // The last byte doesn't drop the most significant bit
321+ byteList.add((byte) currentByte);
322+ byte[] byteArray = new byte[byteList.size()];
323+ for (int i = 0; i < byteList.size(); i++) {
324+ byteArray[i] = byteList.get(i);
325+ }
326+
327+ return byteArray;
328+ }
329+
330+ /**
331+ * Read an arbitrary number of bytes
332+ *
333+ * @param size The number of bytes to read
334+ * @return a byte array of the specified size, filled with bytes read (unsigned)
335+ * @throws IOException - if an IO error occurs
336+ */
337+ private byte[] readNextBytes(int size) throws IOException {
338+ byte[] bytesRead = new byte[size];
339+ for (int i = 0; i < bytesRead.length; i++) {
340+ bytesRead[i] = (byte) this.nextByte();
341+ }
342+ return bytesRead;
343+ }
344+}
345diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
346new file mode 100644
347index 000000000..1eb5d38a6
348--- /dev/null
349+++ b/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
350@@ -0,0 +1,152 @@
351+// License: GPL. For details, see LICENSE file.
352+package org.openstreetmap.josm.data.protobuf;
353+
354+import java.io.IOException;
355+import java.nio.charset.StandardCharsets;
356+import java.util.stream.Stream;
357+
358+import org.openstreetmap.josm.tools.Utils;
359+
360+/**
361+ * A protobuf record, storing the {@link WireType}, the parsed field number, and the bytes for it.
362+ *
363+ * @author Taylor Smock
364+ * @since xxx
365+ */
366+public class ProtoBufRecord implements AutoCloseable {
367+ private static final byte[] EMPTY_BYTES = {};
368+ private final WireType type;
369+ private final int field;
370+ private byte[] bytes;
371+
372+ /**
373+ * Create a new Protobuf record
374+ *
375+ * @param parser The parser to use to create the record
376+ * @throws IOException - if an IO error occurs
377+ */
378+ public ProtoBufRecord(ProtoBufParser parser) throws IOException {
379+ Number number = ProtoBufParser.convertByteArray(parser.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE);
380+ // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3}
381+ this.field = (int) number.longValue() >> 3;
382+ // 7 is 111 (so last three bits)
383+ byte wireType = (byte) (number.longValue() & 7);
384+ this.type = Stream.of(WireType.values()).filter(wType -> wType.getTypeRepresentation() == wireType).findFirst()
385+ .orElse(WireType.UNKNOWN);
386+
387+ if (this.type == WireType.VARINT) {
388+ this.bytes = parser.nextVarInt();
389+ } else if (this.type == WireType.SIXTY_FOUR_BIT) {
390+ this.bytes = parser.nextFixed64();
391+ } else if (this.type == WireType.THIRTY_TWO_BIT) {
392+ this.bytes = parser.nextFixed32();
393+ } else if (this.type == WireType.LENGTH_DELIMITED) {
394+ this.bytes = parser.nextLengthDelimited();
395+ } else {
396+ this.bytes = EMPTY_BYTES;
397+ }
398+ }
399+
400+ /**
401+ * Get as a double ({@link WireType#SIXTY_FOUR_BIT})
402+ *
403+ * @return the double
404+ */
405+ public double asDouble() {
406+ long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue();
407+ return Double.longBitsToDouble(doubleNumber);
408+ }
409+
410+ /**
411+ * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT})
412+ *
413+ * @return a byte array of the 32 bits (4 bytes)
414+ */
415+ public byte[] asFixed32() {
416+ // TODO verify, or just assume?
417+ // 4 bytes == 32 bits
418+ return this.bytes;
419+ }
420+
421+ /**
422+ * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT})
423+ *
424+ * @return a byte array of the 64 bits (8 bytes)
425+ */
426+ public byte[] asFixed64() {
427+ // TODO verify, or just assume?
428+ // 8 bytes == 64 bits
429+ return this.bytes;
430+ }
431+
432+ /**
433+ * Get as a float ({@link WireType#THIRTY_TWO_BIT})
434+ *
435+ * @return the float
436+ */
437+ public float asFloat() {
438+ int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue();
439+ return Float.intBitsToFloat(floatNumber);
440+ }
441+
442+ /**
443+ * Get the signed var int ({@code WireType#VARINT}).
444+ * These are specially encoded so that they take up less space.
445+ *
446+ * @return The signed var int ({@code sint32} or {@code sint64})
447+ */
448+ public Number asSignedVarInt() {
449+ final Number signed = this.asUnsignedVarInt();
450+ return ProtoBufParser.decodeZigZag(signed);
451+ }
452+
453+ /**
454+ * Get as a string ({@link WireType#LENGTH_DELIMITED})
455+ *
456+ * @return The string (encoded as {@link StandardCharsets#UTF_8})
457+ */
458+ public String asString() {
459+ return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8));
460+ }
461+
462+ /**
463+ * Get the var int ({@code WireType#VARINT})
464+ *
465+ * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
466+ */
467+ public Number asUnsignedVarInt() {
468+ return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE);
469+ }
470+
471+ @Override
472+ public void close() {
473+ this.bytes = null;
474+ }
475+
476+ /**
477+ * Get the raw bytes for this record
478+ *
479+ * @return The bytes
480+ */
481+ public byte[] getBytes() {
482+ return this.bytes;
483+ }
484+
485+ /**
486+ * Get the field value
487+ *
488+ * @return The field value
489+ */
490+ public int getField() {
491+ return this.field;
492+ }
493+
494+ /**
495+ * Get the WireType of the data
496+ *
497+ * @return The {@link WireType} of the data
498+ */
499+ public WireType getType() {
500+ return this.type;
501+ }
502+}
503diff --git a/src/org/openstreetmap/josm/data/protobuf/WireType.java b/src/org/openstreetmap/josm/data/protobuf/WireType.java
504new file mode 100644
505index 000000000..41edc8e4f
506--- /dev/null
507+++ b/src/org/openstreetmap/josm/data/protobuf/WireType.java
508@@ -0,0 +1,61 @@
509+// License: GPL. For details, see LICENSE file.
510+package org.openstreetmap.josm.data.protobuf;
511+
512+/**
513+ * The WireTypes
514+ *
515+ * @author Taylor Smock
516+ * @since xxx
517+ */
518+public enum WireType {
519+ /**
520+ * int32, int64, uint32, uint64, sing32, sint64, bool, enum
521+ */
522+ VARINT(0),
523+ /**
524+ * fixed64, sfixed64, double
525+ */
526+ SIXTY_FOUR_BIT(1),
527+ /**
528+ * string, bytes, embedded messages, packed repeated fields
529+ */
530+ LENGTH_DELIMITED(2),
531+ /**
532+ * start groups
533+ *
534+ * @deprecated Unknown reason. Deprecated since at least 2012.
535+ */
536+ @Deprecated
537+ START_GROUP(3),
538+ /**
539+ * end groups
540+ *
541+ * @deprecated Unknown reason. Deprecated since at least 2012.
542+ */
543+ @Deprecated
544+ END_GROUP(4),
545+ /**
546+ * fixed32, sfixed32, float
547+ */
548+ THIRTY_TWO_BIT(5),
549+
550+ /**
551+ * For unknown WireTypes
552+ */
553+ UNKNOWN(Byte.MAX_VALUE);
554+
555+ private final byte type;
556+
557+ WireType(int value) {
558+ this.type = (byte) value;
559+ }
560+
561+ /**
562+ * Get the type representation (byte form)
563+ *
564+ * @return The wire type byte representation
565+ */
566+ public byte getTypeRepresentation() {
567+ return this.type;
568+ }
569+}
570diff --git a/test/data/pbf/openinframap/17/26028/50060.pbf b/test/data/pbf/openinframap/17/26028/50060.pbf
571new file mode 100644
572index 0000000000000000000000000000000000000000..358270e1b307f7373ec494adbd1ceb31c72e2956
573GIT binary patch
574literal 1082
575zcma)5Jx>%t7@pZZ?zlPRxV;mOA(+cna=*Y(pceiCXec!6xHrHiyEB`eSx*~tu>li<
576z5L>mjkXj2R#L`Y<<4386XkmznCKT$-&V9gzf+;rpem(E|JTos|LMY~Kns{NrD7IIF
577z7S79*F&g60ko!aioZ(y+9P>F7GD&^ybMuYudplduwJreHLckqqY(L+R&UYHOEbO3?
578z0hJos_@>Z=@rm??kQuHC%%aKRfEV+CNfW->!-_+~q%$`PoE(Gz)NOaRcsSR&Ja2s%
579zUp|0)@XBknylG#5RM^UaH>I*6Gd%SDolmbUJ+wi;hAO6chHm;gRn;SJSpYy-hwe!6
580z*~O2G1}#StO7O{;#c80<-qRnK2JjCcDjWP&%4k9)Pn;=*jx@Gq*>=v(I_xfNEOo^f
581zJWYIxbMkgfaUl>M<1ISWvebD0@Ymr#eV|)FBZ7w{aWSfAKVH1XCBMl-Ndn)CiMq?d
582zL<^{D23n&;MkMyK5~r+&QiQw1{9VL(p2|3tu3@E_8Nt>qR2yRCX;GYYs31l)r%X57
583zZ5KZ`zsxuHs{owaP#4zvSp((bCj5i&w-Nw%;n$L~*Wn9@-&FwUz_%4;gO{-Ry;rhw
584zpi98+5m(E&e%tuTBmj3FPwIp{m{}dDO{3!k6*7p3q%F@d!TF=gy2m6OkUW)|Mu~dw
585z>BxjIvBHz6_tg6lpGPreLSMV7siH9>o@fWsUo%b@%}5$jl`K5<iBcAry~}pe<jkO2
586zKc__`@qEf;uU`$F=?5bsx{EW7oS4dlhcu16IEblUVuN-TjSoU?8ipshMIZ;+Z6#!K
587G9OOSgfkw9g
588
589literal 0
590HcmV?d00001
591
592diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java
593new file mode 100644
594index 000000000..bdfdf86b7
595--- /dev/null
596+++ b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java
597@@ -0,0 +1,51 @@
598+// License: GPL. For details, see LICENSE file.
599+package org.openstreetmap.josm.data.protobuf;
600+
601+import static org.junit.jupiter.api.Assertions.assertEquals;
602+
603+import org.junit.jupiter.api.Test;
604+
605+/**
606+ * Test class for {@link ProtoBufParser}
607+ * @author Taylor Smock
608+ * @since xxx
609+ */
610+class ProtoBufParserTest {
611+ /**
612+ * Check that we are appropriately converting values to the "smallest" type
613+ */
614+ @Test
615+ void testConvertLong() {
616+ // No casting due to auto conversions
617+ assertEquals(Byte.MAX_VALUE, ProtoBufParser.convertLong(Byte.MAX_VALUE));
618+ assertEquals(Byte.MIN_VALUE, ProtoBufParser.convertLong(Byte.MIN_VALUE));
619+ assertEquals(Short.MIN_VALUE, ProtoBufParser.convertLong(Short.MIN_VALUE));
620+ assertEquals(Short.MAX_VALUE, ProtoBufParser.convertLong(Short.MAX_VALUE));
621+ assertEquals(Integer.MAX_VALUE, ProtoBufParser.convertLong(Integer.MAX_VALUE));
622+ assertEquals(Integer.MIN_VALUE, ProtoBufParser.convertLong(Integer.MIN_VALUE));
623+ assertEquals(Long.MIN_VALUE, ProtoBufParser.convertLong(Long.MIN_VALUE));
624+ assertEquals(Long.MAX_VALUE, ProtoBufParser.convertLong(Long.MAX_VALUE));
625+ }
626+
627+ /**
628+ * Check that zig zags are appropriately encoded.
629+ */
630+ @Test
631+ void testEncodeZigZag() {
632+ assertEquals(0, ProtoBufParser.encodeZigZag(0).byteValue());
633+ assertEquals(1, ProtoBufParser.encodeZigZag(-1).byteValue());
634+ assertEquals(2, ProtoBufParser.encodeZigZag(1).byteValue());
635+ assertEquals(3, ProtoBufParser.encodeZigZag(-2).byteValue());
636+ assertEquals(254, ProtoBufParser.encodeZigZag(Byte.MAX_VALUE).shortValue());
637+ assertEquals(255, ProtoBufParser.encodeZigZag(Byte.MIN_VALUE).shortValue());
638+ assertEquals(65_534, ProtoBufParser.encodeZigZag(Short.MAX_VALUE).intValue());
639+ assertEquals(65_535, ProtoBufParser.encodeZigZag(Short.MIN_VALUE).intValue());
640+ // These integers check a possible boundary condition (the boundary between using the 32/64 bit encoding methods)
641+ assertEquals(4_294_967_292L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE - 1).longValue());
642+ assertEquals(4_294_967_293L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE + 1).longValue());
643+ assertEquals(4_294_967_294L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE).longValue());
644+ assertEquals(4_294_967_295L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE).longValue());
645+ assertEquals(4_294_967_296L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE + 1L).longValue());
646+ assertEquals(4_294_967_297L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE - 1L).longValue());
647+ }
648+}
649diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java
650new file mode 100644
651index 000000000..d0e204c6a
652--- /dev/null
653+++ b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java
654@@ -0,0 +1,30 @@
655+// License: GPL. For details, see LICENSE file.
656+package org.openstreetmap.josm.data.protobuf;
657+
658+import static org.junit.jupiter.api.Assertions.assertEquals;
659+
660+
661+import java.io.IOException;
662+
663+import org.junit.jupiter.api.Test;
664+
665+/**
666+ * Test class for specific {@link ProtoBufRecord} functionality
667+ */
668+class ProtoBufRecordTest {
669+ @Test
670+ void testFixed32() throws IOException {
671+ ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f}));
672+ ProtoBufRecord thirtyTwoBit = new ProtoBufRecord(parser);
673+ assertEquals(WireType.THIRTY_TWO_BIT, thirtyTwoBit.getType());
674+ assertEquals(1f, thirtyTwoBit.asFloat());
675+ }
676+
677+ @Test
678+ void testUnknown() throws IOException {
679+ ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f}));
680+ ProtoBufRecord unknown = new ProtoBufRecord(parser);
681+ assertEquals(WireType.UNKNOWN, unknown.getType());
682+ assertEquals(0, unknown.getBytes().length);
683+ }
684+}
685diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
686new file mode 100644
687index 000000000..043481efe
688--- /dev/null
689+++ b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
690@@ -0,0 +1,211 @@
691+// License: GPL. For details, see LICENSE file.
692+package org.openstreetmap.josm.data.protobuf;
693+
694+import static org.junit.jupiter.api.Assertions.assertEquals;
695+import static org.junit.jupiter.api.Assertions.assertNotNull;
696+import static org.junit.jupiter.api.Assertions.fail;
697+
698+import java.awt.geom.Ellipse2D;
699+import java.io.ByteArrayInputStream;
700+import java.io.File;
701+import java.io.IOException;
702+import java.io.InputStream;
703+import java.nio.file.Paths;
704+import java.text.MessageFormat;
705+import java.util.ArrayList;
706+import java.util.Collection;
707+import java.util.List;
708+import java.util.stream.Collectors;
709+
710+import org.openstreetmap.josm.TestUtils;
711+import org.openstreetmap.josm.data.coor.LatLon;
712+import org.openstreetmap.josm.data.imagery.ImageryInfo;
713+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature;
714+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
715+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
716+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
717+import org.openstreetmap.josm.data.osm.BBox;
718+import org.openstreetmap.josm.data.osm.Node;
719+import org.openstreetmap.josm.data.osm.Way;
720+import org.openstreetmap.josm.data.vector.VectorDataSet;
721+import org.openstreetmap.josm.data.vector.VectorNode;
722+import org.openstreetmap.josm.data.vector.VectorWay;
723+import org.openstreetmap.josm.io.Compression;
724+import org.openstreetmap.josm.testutils.JOSMTestRules;
725+
726+import org.junit.jupiter.api.Test;
727+import org.junit.jupiter.api.extension.RegisterExtension;
728+
729+/**
730+ * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord}
731+ *
732+ * @author Taylor Smock
733+ * @since xxx
734+ */
735+class ProtoBufTest {
736+ /**
737+ * Convert an int array into a byte array
738+ * @param intArray The int array to convert (NOTE: numbers must be below 255)
739+ * @return A byte array that can be used
740+ */
741+ static byte[] toByteArray(int[] intArray) {
742+ byte[] byteArray = new byte[intArray.length];
743+ for (int i = 0; i < intArray.length; i++) {
744+ if (intArray[i] > Byte.MAX_VALUE - Byte.MIN_VALUE) {
745+ throw new IllegalArgumentException();
746+ }
747+ byteArray[i] = Integer.valueOf(intArray[i]).byteValue();
748+ }
749+ return byteArray;
750+ }
751+
752+ @RegisterExtension
753+ JOSMTestRules josmTestRules = new JOSMTestRules().preferences();
754+
755+ private Number bytesToVarInt(int... bytes) {
756+ byte[] byteArray = new byte[bytes.length];
757+ for (int i = 0; i < bytes.length; i++) {
758+ byteArray[i] = (byte) bytes[i];
759+ }
760+ return ProtoBufParser.convertByteArray(byteArray, ProtoBufParser.VAR_INT_BYTE_SIZE);
761+ }
762+
763+ /**
764+ * Test reading tile from Mapillary ( 14/3248/6258 )
765+ *
766+ * @throws IOException if there is a problem reading the file
767+ */
768+ @Test
769+ void testRead_14_3248_6258() throws IOException {
770+ File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "14", "3248", "6258.mvt").toFile();
771+ InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
772+ Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
773+ assertEquals(2, records.size());
774+ List<Layer> layers = new ArrayList<>();
775+ for (ProtoBufRecord record : records) {
776+ if (record.getField() == Layer.LAYER_FIELD) {
777+ layers.add(new Layer(record.getBytes()));
778+ } else {
779+ fail(MessageFormat.format("Invalid field {0}", record.getField()));
780+ }
781+ }
782+ Layer mapillarySequences = layers.get(0);
783+ Layer mapillaryPictures = layers.get(1);
784+ assertEquals("mapillary-sequences", mapillarySequences.getName());
785+ assertEquals("mapillary-images", mapillaryPictures.getName());
786+ assertEquals(2048, mapillarySequences.getExtent());
787+ assertEquals(2048, mapillaryPictures.getExtent());
788+
789+ assertEquals(1,
790+ mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 233760500).count());
791+ Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 233760500)
792+ .findAny().orElse(null);
793+ assertEquals("dpudn262yz6aitu33zh7bl", testSequence.getTags().get("key"));
794+ assertEquals("clnaw3kpokIAe_CsN5Qmiw", testSequence.getTags().get("ikey"));
795+ assertEquals("B1iNjH4Ohn25cRAGPhetfw", testSequence.getTags().get("userkey"));
796+ assertEquals(Long.valueOf(1557535457401L), Long.valueOf(testSequence.getTags().get("captured_at")));
797+ assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano")));
798+ }
799+
800+ @Test
801+ void testRead_17_26028_50060() throws IOException {
802+ File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "openinframap", "17", "26028", "50060.pbf")
803+ .toFile();
804+ InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
805+ Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
806+ List<Layer> layers = new ArrayList<>();
807+ for (ProtoBufRecord record : records) {
808+ if (record.getField() == Layer.LAYER_FIELD) {
809+ layers.add(new Layer(record.getBytes()));
810+ } else {
811+ fail(MessageFormat.format("Invalid field {0}", record.getField()));
812+ }
813+ }
814+ assertEquals(19, layers.size());
815+ List<Layer> dataLayers = layers.stream().filter(layer -> !layer.getFeatures().isEmpty())
816+ .collect(Collectors.toList());
817+ // power_plant, power_plant_point, power_generator, power_heatmap_solar, and power_generator_area
818+ assertEquals(5, dataLayers.size());
819+
820+ // power_generator_area was rendered incorrectly
821+ final Layer powerGeneratorArea = dataLayers.stream()
822+ .filter(layer -> "power_generator_area".equals(layer.getName())).findAny().orElse(null);
823+ assertNotNull(powerGeneratorArea);
824+ final int extent = powerGeneratorArea.getExtent();
825+ // 17/26028/50060 bounds
826+ VectorDataSet vectorDataSet = new VectorDataSet();
827+ MVTTile vectorTile1 = new MVTTile(new MapboxVectorTileSource(new ImageryInfo("Test info", "example.org")),
828+ 26028, 50060, 17);
829+ vectorTile1.loadImage(Compression.getUncompressedFileInputStream(vectorTile));
830+ vectorDataSet.addTileData(vectorTile1);
831+ vectorDataSet.setZoom(17);
832+ final Way one = new Way();
833+ one.addNode(new Node(new LatLon(39.0687509, -108.5100816)));
834+ one.addNode(new Node(new LatLon(39.0687509, -108.5095751)));
835+ one.addNode(new Node(new LatLon(39.0687169, -108.5095751)));
836+ one.addNode(new Node(new LatLon(39.0687169, -108.5100816)));
837+ one.addNode(one.getNode(0));
838+ one.setOsmId(666293899, 2);
839+ final BBox searchBBox = one.getBBox();
840+ searchBBox.addPrimitive(one, 0.00001);
841+ final Collection<VectorNode> searchedNodes = vectorDataSet.searchNodes(searchBBox);
842+ final Collection<VectorWay> searchedWays = vectorDataSet.searchWays(searchBBox);
843+ assertEquals(4, searchedNodes.size());
844+ }
845+
846+ @Test
847+ void testReadVarInt() {
848+ assertEquals(ProtoBufParser.convertLong(0), bytesToVarInt(0x0));
849+ assertEquals(ProtoBufParser.convertLong(1), bytesToVarInt(0x1));
850+ assertEquals(ProtoBufParser.convertLong(127), bytesToVarInt(0x7f));
851+ // This should b 0xff 0xff 0xff 0xff 0x07, but we drop the leading bit when reading to a byte array
852+ Number actual = bytesToVarInt(0x7f, 0x7f, 0x7f, 0x7f, 0x07);
853+ assertEquals(ProtoBufParser.convertLong(Integer.MAX_VALUE), actual,
854+ MessageFormat.format("Expected {0} but got {1}", Integer.toBinaryString(Integer.MAX_VALUE),
855+ Long.toBinaryString(actual.longValue())));
856+ }
857+
858+ /**
859+ * Test simple message.
860+ * Check that a simple message is readable
861+ *
862+ * @throws IOException - if an IO error occurs
863+ */
864+ @Test
865+ void testSimpleMessage() throws IOException {
866+ ProtoBufParser parser = new ProtoBufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01});
867+ ProtoBufRecord record = new ProtoBufRecord(parser);
868+ assertEquals(WireType.VARINT, record.getType());
869+ assertEquals(150, record.asUnsignedVarInt().intValue());
870+ }
871+
872+ @Test
873+ void testSingletonMultiPoint() throws IOException {
874+ Collection<ProtoBufRecord> records = new ProtoBufParser(new ByteArrayInputStream(toByteArray(
875+ new int[] {0x1a, 0x2c, 0x78, 0x02, 0x0a, 0x03, 0x74, 0x6d, 0x70, 0x28, 0x80, 0x20, 0x1a, 0x04, 0x6e,
876+ 0x61, 0x6d, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x54, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x61, 0x6d, 0x65,
877+ 0x12, 0x0d, 0x18, 0x01, 0x12, 0x02, 0x00, 0x00, 0x22, 0x05, 0x09, 0xe0, 0x3e, 0x84, 0x27})))
878+ .allRecords();
879+ List<Layer> layers = new ArrayList<>();
880+ for (ProtoBufRecord record : records) {
881+ if (record.getField() == Layer.LAYER_FIELD) {
882+ layers.add(new Layer(record.getBytes()));
883+ } else {
884+ fail(MessageFormat.format("Invalid field {0}", record.getField()));
885+ }
886+ }
887+ assertEquals(1, layers.size());
888+ assertEquals(1, layers.get(0).getGeometry().size());
889+ Ellipse2D shape = (Ellipse2D) layers.get(0).getGeometry().iterator().next().getShapes().iterator().next();
890+ assertEquals(4016, shape.getCenterX());
891+ assertEquals(2498, shape.getCenterY());
892+ }
893+
894+ @Test
895+ void testZigZag() {
896+ assertEquals(0, ProtoBufParser.decodeZigZag(0).intValue());
897+ assertEquals(-1, ProtoBufParser.decodeZigZag(1).intValue());
898+ assertEquals(1, ProtoBufParser.decodeZigZag(2).intValue());
899+ assertEquals(-2, ProtoBufParser.decodeZigZag(3).intValue());
900+ }
901+}
902--
903GitLab
904
905
906From f38f09e1036905ea7e7499c66b5a9fb691cd38c8 Mon Sep 17 00:00:00 2001
907From: Taylor Smock <tsmock@fb.com>
908Date: Thu, 8 Apr 2021 14:24:54 -0600
909Subject: [PATCH 02/50] Initial Mapbox Vector Tile implementation
910
911Signed-off-by: Taylor Smock <tsmock@fb.com>
912---
913 .../data/imagery/vectortile/VectorTile.java | 25 ++
914 .../imagery/vectortile/mapbox/Command.java | 48 ++++
915 .../vectortile/mapbox/CommandInteger.java | 62 +++++
916 .../imagery/vectortile/mapbox/Feature.java | 175 +++++++++++++
917 .../imagery/vectortile/mapbox/Geometry.java | 102 ++++++++
918 .../vectortile/mapbox/GeometryTypes.java | 31 +++
919 .../InvalidMapboxVectorTileException.java | 25 ++
920 .../data/imagery/vectortile/mapbox/Layer.java | 245 ++++++++++++++++++
921 .../imagery/vectortile/mapbox/MVTFile.java | 34 +++
922 .../imagery/vectortile/mapbox/MVTTile.java | 119 +++++++++
923 .../mapbox/MapBoxVectorCachedTileLoader.java | 79 ++++++
924 .../MapBoxVectorCachedTileLoaderJob.java | 26 ++
925 .../mapbox/MapboxVectorTileSource.java | 92 +++++++
926 test/data/mapillary.json | 111 ++++++++
927 .../vectortile/mapbox/FeatureTest.java | 122 +++++++++
928 .../vectortile/mapbox/GeometryTest.java | 169 ++++++++++++
929 .../vectortile/mapbox/GeometryTypesTest.java | 47 ++++
930 .../imagery/vectortile/mapbox/LayerTest.java | 135 ++++++++++
931 .../vectortile/mapbox/MVTTileTest.java | 82 ++++++
932 .../mapbox/MapboxVectorTileSourceTest.java | 77 ++++++
933 20 files changed, 1806 insertions(+)
934 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java
935 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java
936 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java
937 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
938 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
939 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
940 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
941 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
942 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
943 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
944 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
945 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
946 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
947 create mode 100644 test/data/mapillary.json
948 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java
949 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java
950 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java
951 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
952 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
953 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java
954
955diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java
956new file mode 100644
957index 000000000..692f3ea8c
958--- /dev/null
959+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java
960@@ -0,0 +1,25 @@
961+// License: GPL. For details, see LICENSE file.
962+package org.openstreetmap.josm.data.imagery.vectortile;
963+
964+import java.util.Collection;
965+
966+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
967+
968+/**
969+ * An interface that is used to draw vector tiles, instead of using images
970+ * @author Taylor Smock
971+ * @since xxx
972+ */
973+public interface VectorTile {
974+ /**
975+ * Get the layers for this vector tile
976+ * @return A collection of layers
977+ */
978+ Collection<Layer> getLayers();
979+
980+ /**
981+ * Get the extent of the tile (in pixels)
982+ * @return The tile extent (pixels)
983+ */
984+ int getExtent();
985+}
986diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java
987new file mode 100644
988index 000000000..05ffcf945
989--- /dev/null
990+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java
991@@ -0,0 +1,48 @@
992+// License: GPL. For details, see LICENSE file.
993+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
994+
995+/**
996+ * Command integers for Mapbox Vector Tiles
997+ * @author Taylor Smock
998+ * @since xxx
999+ */
1000+public enum Command {
1001+ /**
1002+ * For {@link GeometryTypes#POINT}, each {@link #MoveTo} is a new point.
1003+ * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #MoveTo} is a new geometry of the same type.
1004+ */
1005+ MoveTo((byte) 1, (byte) 2),
1006+ /**
1007+ * While not explicitly prohibited for {@link GeometryTypes#POINT}, it should be ignored.
1008+ * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #LineTo} extends that geometry.
1009+ */
1010+ LineTo((byte) 2, (byte) 2),
1011+ /**
1012+ * This is only explicitly valid for {@link GeometryTypes#POLYGON}. It closes the {@link GeometryTypes#POLYGON}.
1013+ */
1014+ ClosePath((byte) 7, (byte) 0);
1015+
1016+ private final byte id;
1017+ private final byte parameters;
1018+
1019+ Command(byte id, byte parameters) {
1020+ this.id = id;
1021+ this.parameters = parameters;
1022+ }
1023+
1024+ /**
1025+ * Get the command id
1026+ * @return The id
1027+ */
1028+ public byte getId() {
1029+ return this.id;
1030+ }
1031+
1032+ /**
1033+ * Get the number of parameters
1034+ * @return The number of parameters
1035+ */
1036+ public byte getParameterNumber() {
1037+ return this.parameters;
1038+ }
1039+}
1040diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java
1041new file mode 100644
1042index 000000000..5213bf0e8
1043--- /dev/null
1044+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java
1045@@ -0,0 +1,62 @@
1046+// License: GPL. For details, see LICENSE file.
1047+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1048+
1049+import java.util.stream.Stream;
1050+
1051+/**
1052+ * An indicator for a command to be executed
1053+ * @author Taylor Smock
1054+ * @since xxx
1055+ */
1056+public class CommandInteger {
1057+ private final Command type;
1058+ private final short[] parameters;
1059+ private int added;
1060+
1061+ /**
1062+ * Create a new command
1063+ * @param command the command (treated as an unsigned int)
1064+ */
1065+ public CommandInteger(final int command) {
1066+ // Technically, the int is unsigned, but it is easier to work with the long
1067+ final long unsigned = Integer.toUnsignedLong(command);
1068+ this.type = Stream.of(Command.values()).filter(e -> e.getId() == (unsigned & 0x7)).findAny()
1069+ .orElseThrow(InvalidMapboxVectorTileException::new);
1070+ // This is safe, since we are shifting right 3 when we converted an int to a long (for unsigned).
1071+ // So we <i>cannot</i> lose anything.
1072+ final int operationsInt = (int) (unsigned >> 3);
1073+ this.parameters = new short[operationsInt * this.type.getParameterNumber()];
1074+ }
1075+
1076+ /**
1077+ * Add a parameter
1078+ * @param parameterInteger The parameter to add (converted to {@link short}).
1079+ */
1080+ public void addParameter(Number parameterInteger) {
1081+ this.parameters[added++] = parameterInteger.shortValue();
1082+ }
1083+
1084+ /**
1085+ * Get the operations for the command
1086+ * @return The operations
1087+ */
1088+ public short[] getOperations() {
1089+ return this.parameters;
1090+ }
1091+
1092+ /**
1093+ * Get the command type
1094+ * @return the command type
1095+ */
1096+ public Command getType() {
1097+ return this.type;
1098+ }
1099+
1100+ /**
1101+ * Get the expected parameter length
1102+ * @return The expected parameter size
1103+ */
1104+ public boolean hasAllExpectedParameters() {
1105+ return this.added >= this.parameters.length;
1106+ }
1107+}
1108diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
1109new file mode 100644
1110index 000000000..df194cc00
1111--- /dev/null
1112+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
1113@@ -0,0 +1,175 @@
1114+// License: GPL. For details, see LICENSE file.
1115+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1116+
1117+import java.io.IOException;
1118+import java.text.NumberFormat;
1119+import java.util.ArrayList;
1120+import java.util.List;
1121+
1122+import org.openstreetmap.josm.data.osm.TagMap;
1123+import org.openstreetmap.josm.data.protobuf.ProtoBufPacked;
1124+import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
1125+import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
1126+import org.openstreetmap.josm.tools.Utils;
1127+
1128+/**
1129+ * A Feature for a {@link Layer}
1130+ *
1131+ * @author Taylor Smock
1132+ * @since xxx
1133+ */
1134+public class Feature {
1135+ private static final byte ID_FIELD = 1;
1136+ private static final byte TAG_FIELD = 2;
1137+ private static final byte GEOMETRY_TYPE_FIELD = 3;
1138+ private static final byte GEOMETRY_FIELD = 4;
1139+ /**
1140+ * The geometry of the feature. Required.
1141+ */
1142+ private final List<CommandInteger> geometry = new ArrayList<>();
1143+
1144+ /**
1145+ * The geometry type of the feature. Required.
1146+ */
1147+ private final GeometryTypes geometryType;
1148+ /**
1149+ * The id of the feature. Optional.
1150+ */
1151+ // Technically, uint64
1152+ private final long id;
1153+ /**
1154+ * The tags of the feature. Optional.
1155+ */
1156+ private TagMap tags;
1157+ private Geometry geometryObject;
1158+
1159+ /**
1160+ * Create a new Feature
1161+ *
1162+ * @param layer The layer the feature is part of (required for tags)
1163+ * @param record The record to create the feature from
1164+ * @throws IOException - if an IO error occurs
1165+ */
1166+ public Feature(Layer layer, ProtoBufRecord record) throws IOException {
1167+ long tId = 0;
1168+ GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN;
1169+ String key = null;
1170+ try (ProtoBufParser parser = new ProtoBufParser(record.getBytes())) {
1171+ while (parser.hasNext()) {
1172+ try (ProtoBufRecord next = new ProtoBufRecord(parser)) {
1173+ if (next.getField() == TAG_FIELD) {
1174+ if (tags == null) {
1175+ tags = new TagMap();
1176+ }
1177+ // This is packed in v1 and v2
1178+ ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
1179+ for (Number number : packed.getArray()) {
1180+ key = parseTagValue(key, layer, number);
1181+ }
1182+ } else if (next.getField() == GEOMETRY_FIELD) {
1183+ // This is packed in v1 and v2
1184+ ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
1185+ CommandInteger currentCommand = null;
1186+ for (Number number : packed.getArray()) {
1187+ if (currentCommand != null && currentCommand.hasAllExpectedParameters()) {
1188+ currentCommand = null;
1189+ }
1190+ if (currentCommand == null) {
1191+ currentCommand = new CommandInteger(number.intValue());
1192+ this.geometry.add(currentCommand);
1193+ } else {
1194+ currentCommand.addParameter(ProtoBufParser.decodeZigZag(number));
1195+ }
1196+ }
1197+ // TODO fallback to non-packed
1198+ } else if (next.getField() == GEOMETRY_TYPE_FIELD) {
1199+ geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()];
1200+ } else if (next.getField() == ID_FIELD) {
1201+ tId = next.asUnsignedVarInt().longValue();
1202+ }
1203+ }
1204+ }
1205+ }
1206+ this.id = tId;
1207+ this.geometryType = geometryTypeTemp;
1208+ record.close();
1209+ }
1210+
1211+ /**
1212+ * Parse a tag value
1213+ *
1214+ * @param key The current key (or {@code null}, if {@code null}, the returned value will be the new key)
1215+ * @param layer The layer with key/value information
1216+ * @param number The number to get the value from
1217+ * @return The new key (if {@code null}, then a value was parsed and added to tags)
1218+ */
1219+ private String parseTagValue(String key, Layer layer, Number number) {
1220+ if (key == null) {
1221+ key = layer.getKey(number.intValue());
1222+ } else {
1223+ Object value = layer.getValue(number.intValue());
1224+ if (value instanceof Double || value instanceof Float) {
1225+ // reset grouping if the instance is a singleton
1226+ final NumberFormat numberFormat = NumberFormat.getNumberInstance();
1227+ final boolean grouping = numberFormat.isGroupingUsed();
1228+ try {
1229+ numberFormat.setGroupingUsed(false);
1230+ this.tags.put(key, numberFormat.format(value));
1231+ } finally {
1232+ numberFormat.setGroupingUsed(grouping);
1233+ }
1234+ } else {
1235+ this.tags.put(key, Utils.intern(value.toString()));
1236+ }
1237+ key = null;
1238+ }
1239+ return key;
1240+ }
1241+
1242+ /**
1243+ * Get the geometry instructions
1244+ *
1245+ * @return The geometry
1246+ */
1247+ public List<CommandInteger> getGeometry() {
1248+ return this.geometry;
1249+ }
1250+
1251+ /**
1252+ * Get the geometry type
1253+ *
1254+ * @return The {@link GeometryTypes}
1255+ */
1256+ public GeometryTypes getGeometryType() {
1257+ return this.geometryType;
1258+ }
1259+
1260+ /**
1261+ * Get the id of the object
1262+ *
1263+ * @return The unique id in the layer, or 0.
1264+ */
1265+ public long getId() {
1266+ return this.id;
1267+ }
1268+
1269+ /**
1270+ * Get the tags
1271+ *
1272+ * @return A tag map
1273+ */
1274+ public TagMap getTags() {
1275+ return this.tags;
1276+ }
1277+
1278+ /**
1279+ * Get the an object with shapes for the geometry
1280+ * @return An object with usable geometry information
1281+ */
1282+ public Geometry getGeometryObject() {
1283+ if (this.geometryObject == null) {
1284+ this.geometryObject = new Geometry(this.getGeometryType(), this.getGeometry());
1285+ }
1286+ return this.geometryObject;
1287+ }
1288+}
1289diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
1290new file mode 100644
1291index 000000000..c612c7e83
1292--- /dev/null
1293+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
1294@@ -0,0 +1,102 @@
1295+// License: GPL. For details, see LICENSE file.
1296+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1297+
1298+import static org.openstreetmap.josm.tools.I18n.tr;
1299+
1300+import java.awt.Shape;
1301+import java.awt.geom.Area;
1302+import java.awt.geom.Ellipse2D;
1303+import java.awt.geom.Path2D;
1304+import java.util.ArrayList;
1305+import java.util.Collection;
1306+import java.util.Collections;
1307+import java.util.List;
1308+
1309+/**
1310+ * A class to generate geometry for a vector tile
1311+ * @author Taylor Smock
1312+ * @since xxx
1313+ */
1314+public class Geometry {
1315+ final Collection<Shape> shapes = new ArrayList<>();
1316+
1317+ /**
1318+ * Create a {@link Geometry} for a {@link Feature}
1319+ * @param geometryType The type of geometry
1320+ * @param commands The commands used to create the geometry
1321+ */
1322+ public Geometry(GeometryTypes geometryType, List<CommandInteger> commands) {
1323+ if (geometryType == GeometryTypes.POINT) {
1324+ for (CommandInteger command : commands) {
1325+ final short[] operations = command.getOperations();
1326+ // Each MoveTo command is a new point
1327+ if (command.getType() == Command.MoveTo && operations.length % 2 == 0 && operations.length > 0) {
1328+ for (int i = 0; i < operations.length / 2; i++) {
1329+ // Just using Ellipse2D since it extends Shape
1330+ shapes.add(new Ellipse2D.Float(operations[2 * i],
1331+ operations[2 * i + 1], 0, 0));
1332+ }
1333+ } else {
1334+ throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));
1335+ }
1336+ }
1337+ } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) {
1338+ Path2D.Float line = null;
1339+ Area area = null;
1340+ // MVT uses delta encoding. Each feature starts at (0, 0).
1341+ double x = 0;
1342+ double y = 0;
1343+ // Area is used to determine the inner/outer of a polygon
1344+ double areaAreaSq = 0;
1345+ for (CommandInteger command : commands) {
1346+ final short[] operations = command.getOperations();
1347+ // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior
1348+ if (command.getType() == Command.MoveTo && operations.length == 2) {
1349+ areaAreaSq = 0;
1350+ x += operations[0];
1351+ y += operations[1];
1352+ line = new Path2D.Float();
1353+ line.moveTo(x, y);
1354+ shapes.add(line);
1355+ } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) {
1356+ for (int i = 0; i < operations.length / 2; i++) {
1357+ final double lx = x;
1358+ final double ly = y;
1359+ x += operations[2 * i];
1360+ y += operations[2 * i + 1];
1361+ areaAreaSq += lx * y - x * ly;
1362+ line.lineTo(x, y);
1363+ }
1364+ // ClosePath should only be used with Polygon geometry
1365+ } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) {
1366+ shapes.remove(line);
1367+ // new Area() closes the line if it isn't already closed
1368+ if (area == null) {
1369+ area = new Area();
1370+ shapes.add(area);
1371+ }
1372+
1373+ Area nArea = new Area(line);
1374+ // SonarLint thinks that this is never > 0. It can be.
1375+ if (areaAreaSq > 0) {
1376+ area.add(nArea);
1377+ } else if (areaAreaSq < 0) {
1378+ area.exclusiveOr(nArea);
1379+ } else {
1380+ throw new IllegalArgumentException(tr("{0} cannot have zero area", geometryType));
1381+ }
1382+ } else {
1383+ throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));
1384+ }
1385+ }
1386+ }
1387+ }
1388+
1389+ /**
1390+ * Get the shapes to draw this geometry with
1391+ * @return A collection of shapes
1392+ */
1393+ public Collection<Shape> getShapes() {
1394+ return Collections.unmodifiableCollection(this.shapes);
1395+ }
1396+}
1397diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
1398new file mode 100644
1399index 000000000..0dc29c6a6
1400--- /dev/null
1401+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
1402@@ -0,0 +1,31 @@
1403+// License: GPL. For details, see LICENSE file.
1404+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1405+
1406+/**
1407+ * Geometry types used by Mapbox Vector Tiles
1408+ * @author Taylor Smock
1409+ * @since xxx
1410+ */
1411+public enum GeometryTypes {
1412+ /** May be ignored */
1413+ UNKNOWN,
1414+ /** May be a point or a multipoint geometry. Uses <i>only</i> {@link Command#MoveTo}. Multiple {@link Command#MoveTo}
1415+ * indicates that it is a multi-point object. */
1416+ POINT,
1417+ /** May be a line or a multiline geometry. Each line {@link Command#MoveTo} and one or more {@link Command#LineTo}. */
1418+ LINESTRING,
1419+ /** May be a polygon or a multipolygon. Each ring uses a {@link Command#MoveTo}, one or more {@link Command#LineTo},
1420+ * and one {@link Command#ClosePath} command. See {@link Ring}s. */
1421+ POLYGON;
1422+
1423+ /**
1424+ * Rings used by {@link GeometryTypes#POLYGON}
1425+ * @author Taylor Smock
1426+ */
1427+ public enum Ring {
1428+ /** A ring that goes in the clockwise direction */
1429+ ExteriorRing,
1430+ /** A ring that goes in the anti-clockwise direction */
1431+ InteriorRing
1432+ }
1433+}
1434diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
1435new file mode 100644
1436index 000000000..d1186ad3f
1437--- /dev/null
1438+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
1439@@ -0,0 +1,25 @@
1440+// License: GPL. For details, see LICENSE file.
1441+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1442+
1443+/**
1444+ * Thrown when a mapbox vector tile does not match specifications.
1445+ *
1446+ * @author Taylor Smock
1447+ * @since xxx
1448+ */
1449+public class InvalidMapboxVectorTileException extends RuntimeException {
1450+ /**
1451+ * Create a default {@link InvalidMapboxVectorTileException}.
1452+ */
1453+ public InvalidMapboxVectorTileException() {
1454+ super();
1455+ }
1456+
1457+ /**
1458+ * Create a new {@link InvalidMapboxVectorTile} exception with a message
1459+ * @param message The message
1460+ */
1461+ public InvalidMapboxVectorTileException(final String message) {
1462+ super(message);
1463+ }
1464+}
1465diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
1466new file mode 100644
1467index 000000000..09851e8c7
1468--- /dev/null
1469+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
1470@@ -0,0 +1,245 @@
1471+// License: GPL. For details, see LICENSE file.
1472+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1473+import static org.openstreetmap.josm.tools.I18n.tr;
1474+
1475+import java.io.IOException;
1476+import java.util.ArrayList;
1477+import java.util.Arrays;
1478+import java.util.Collection;
1479+import java.util.Collections;
1480+import java.util.HashSet;
1481+import java.util.List;
1482+import java.util.Map;
1483+import java.util.Objects;
1484+import java.util.function.Function;
1485+import java.util.stream.Collectors;
1486+
1487+import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
1488+import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
1489+import org.openstreetmap.josm.tools.Logging;
1490+
1491+/**
1492+ * A Mapbox Vector Tile Layer
1493+ * @author Taylor Smock
1494+ * @since xxx
1495+ */
1496+public final class Layer {
1497+ private static final class ValueFields<T> {
1498+ static final ValueFields<String> STRING = new ValueFields<>(1, ProtoBufRecord::asString);
1499+ static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtoBufRecord::asFloat);
1500+ static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtoBufRecord::asDouble);
1501+ static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtoBufRecord::asUnsignedVarInt);
1502+ // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE})
1503+ static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtoBufRecord::asUnsignedVarInt);
1504+ static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtoBufRecord::asSignedVarInt);
1505+ static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0);
1506+
1507+ /**
1508+ * A collection of methods to map a record to a type
1509+ */
1510+ public static final Collection<ValueFields<?>> MAPPERS =
1511+ Collections.unmodifiableList(Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL));
1512+
1513+ private final byte field;
1514+ private final Function<ProtoBufRecord, T> conversion;
1515+ private ValueFields(int field, Function<ProtoBufRecord, T> conversion) {
1516+ this.field = (byte) field;
1517+ this.conversion = conversion;
1518+ }
1519+
1520+ /**
1521+ * Get the field identifier for the value
1522+ * @return The identifier
1523+ */
1524+ public byte getField() {
1525+ return this.field;
1526+ }
1527+
1528+ /**
1529+ * Convert a protobuf record to a value
1530+ * @param protobufRecord The record to convert
1531+ * @return the converted value
1532+ */
1533+ public T convertValue(ProtoBufRecord protobufRecord) {
1534+ return this.conversion.apply(protobufRecord);
1535+ }
1536+ }
1537+
1538+ /** The field value for a layer (in {@link ProtoBufRecord#getField}) */
1539+ public static final byte LAYER_FIELD = 3;
1540+ private static final byte VERSION_FIELD = 15;
1541+ private static final byte NAME_FIELD = 1;
1542+ private static final byte FEATURE_FIELD = 2;
1543+ private static final byte KEY_FIELD = 3;
1544+ private static final byte VALUE_FIELD = 4;
1545+ private static final byte EXTENT_FIELD = 5;
1546+ /** The default extent for a vector tile */
1547+ static final int DEFAULT_EXTENT = 4096;
1548+ private static final byte DEFAULT_VERSION = 1;
1549+ /** This is <i>technically</i> an integer, but there are currently only two major versions (1, 2). Required. */
1550+ private final byte version;
1551+ /** A unique name for the layer. This <i>must</i> be unique on a per-tile basis. Required. */
1552+ private final String name;
1553+
1554+ /** The extent of the tile, typically 4096. Required. */
1555+ private final int extent;
1556+
1557+ /** A list of unique keys. Order is important. Optional. */
1558+ private final List<String> keyList = new ArrayList<>();
1559+ /** A list of unique values. Order is important. Optional. */
1560+ private final List<Object> valueList = new ArrayList<>();
1561+ /** The actual features of this layer in this tile */
1562+ private final List<Feature> featureCollection;
1563+
1564+ /**
1565+ * Create a layer from a collection of records
1566+ * @param records The records to convert to a layer
1567+ * @throws IOException - if an IO error occurs
1568+ */
1569+ public Layer(Collection<ProtoBufRecord> records) throws IOException {
1570+ // Do the unique required fields first
1571+ Map<Integer, List<ProtoBufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtoBufRecord::getField));
1572+ this.version = sorted.getOrDefault((int) VERSION_FIELD, Collections.emptyList()).parallelStream()
1573+ .map(ProtoBufRecord::asUnsignedVarInt).map(Number::byteValue).findFirst().orElse(DEFAULT_VERSION);
1574+ // Per spec, we cannot continue past this until we have checked the version number
1575+ if (this.version != 1 && this.version != 2) {
1576+ throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version));
1577+ }
1578+ this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst()
1579+ .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name")));
1580+ this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asSignedVarInt)
1581+ .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT);
1582+
1583+ sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString)
1584+ .forEachOrdered(this.keyList::add);
1585+ sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::getBytes)
1586+ .map(ProtoBufParser::new).map(parser1 -> {
1587+ try {
1588+ return new ProtoBufRecord(parser1);
1589+ } catch (IOException e) {
1590+ Logging.error(e);
1591+ return null;
1592+ }
1593+ })
1594+ .filter(Objects::nonNull)
1595+ .map(value -> ValueFields.MAPPERS.parallelStream()
1596+ .filter(v -> v.getField() == value.getField())
1597+ .map(v -> v.convertValue(value)).findFirst()
1598+ .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField()))))
1599+ .forEachOrdered(this.valueList::add);
1600+ Collection<IOException> exceptions = new HashSet<>(0);
1601+ this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> {
1602+ try {
1603+ return new Feature(this, feature);
1604+ } catch (IOException e) {
1605+ exceptions.add(e);
1606+ }
1607+ return null;
1608+ }).collect(Collectors.toList());
1609+ if (!exceptions.isEmpty()) {
1610+ throw exceptions.iterator().next();
1611+ }
1612+ // Cleanup bytes (for memory)
1613+ for (ProtoBufRecord record : records) {
1614+ record.close();
1615+ }
1616+ }
1617+
1618+ /**
1619+ * Get all the records from a array of bytes
1620+ * @param bytes The byte information
1621+ * @return All the protobuf records
1622+ * @throws IOException If there was an error reading the bytes (unlikely)
1623+ */
1624+ private static Collection<ProtoBufRecord> getAllRecords(byte[] bytes) throws IOException {
1625+ try (ProtoBufParser parser = new ProtoBufParser(bytes)) {
1626+ return parser.allRecords();
1627+ }
1628+ }
1629+
1630+ /**
1631+ * Create a new layer
1632+ * @param bytes The bytes that the layer comes from
1633+ * @throws IOException - if an IO error occurs
1634+ */
1635+ public Layer(byte[] bytes) throws IOException {
1636+ this(getAllRecords(bytes));
1637+ }
1638+
1639+ /**
1640+ * Get the extent of the tile
1641+ * @return The layer extent
1642+ */
1643+ public int getExtent() {
1644+ return this.extent;
1645+ }
1646+
1647+ /**
1648+ * Get the feature on this layer
1649+ * @return the features
1650+ */
1651+ public Collection<Feature> getFeatures() {
1652+ return Collections.unmodifiableCollection(this.featureCollection);
1653+ }
1654+
1655+ /**
1656+ * Get the geometry for this layer
1657+ * @return The geometry
1658+ */
1659+ public Collection<Geometry> getGeometry() {
1660+ return getFeatures().stream().map(Feature::getGeometryObject).collect(Collectors.toList());
1661+ }
1662+
1663+ /**
1664+ * Get a specified key
1665+ * @param index The index in the key list
1666+ * @return The actual key
1667+ */
1668+ public String getKey(int index) {
1669+ return this.keyList.get(index);
1670+ }
1671+
1672+ /**
1673+ * Get the name of the layer
1674+ * @return The layer name
1675+ */
1676+ public String getName() {
1677+ return this.name;
1678+ }
1679+
1680+ /**
1681+ * Get a specified value
1682+ * @param index The index in the value list
1683+ * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value.
1684+ */
1685+ public Object getValue(int index) {
1686+ return this.valueList.get(index);
1687+ }
1688+
1689+ /**
1690+ * Get the MapBox Vector Tile version specification for this layer
1691+ * @return The version of the MapBox Vector Tile specification
1692+ */
1693+ public byte getVersion() {
1694+ return this.version;
1695+ }
1696+
1697+ @Override
1698+ public boolean equals(Object other) {
1699+ if (other instanceof Layer) {
1700+ Layer o = (Layer) other;
1701+ return this.extent == o.extent
1702+ && this.version == o.version
1703+ && Objects.equals(this.name, o.name)
1704+ && Objects.equals(this.featureCollection, o.featureCollection)
1705+ && Objects.equals(this.keyList, o.keyList)
1706+ && Objects.equals(this.valueList, o.valueList);
1707+ }
1708+ return false;
1709+ }
1710+
1711+ @Override
1712+ public int hashCode() {
1713+ return Objects.hash(this.name, this.version, this.extent, this.featureCollection, this.keyList, this.valueList);
1714+ }
1715+}
1716diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
1717new file mode 100644
1718index 000000000..84ac8ae89
1719--- /dev/null
1720+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
1721@@ -0,0 +1,34 @@
1722+// License: GPL. For details, see LICENSE file.
1723+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1724+
1725+import java.util.Arrays;
1726+import java.util.Collections;
1727+import java.util.List;
1728+
1729+/**
1730+ * Items that MAY be used to figure out if a file or server response MAY BE a Mapbox Vector Tile
1731+ * @author Taylor Smock
1732+ * @since xxx
1733+ */
1734+public final class MVTFile {
1735+ /**
1736+ * Extensions for Mapbox Vector Tiles.
1737+ * This is a SHOULD, <i>not</i> a MUST.
1738+ */
1739+ public static final List<String> EXTENSION = Collections.unmodifiableList(Arrays.asList("mvt"));
1740+
1741+ /**
1742+ * mimetypes for Mapbox Vector Tiles
1743+ * This is a SHOULD, <i>not</i> a MUST.
1744+ */
1745+ public static final List<String> MIMETYPE = Collections.unmodifiableList(Arrays.asList("application/vnd.mapbox-vector-tile"));
1746+
1747+ /**
1748+ * The default projection. This is Web Mercator, per specification.
1749+ */
1750+ public static final String DEFAULT_PROJECTION = "EPSG:3857";
1751+
1752+ private MVTFile() {
1753+ // Hide the constructor
1754+ }
1755+}
1756diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
1757new file mode 100644
1758index 000000000..5d1d781dd
1759--- /dev/null
1760+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
1761@@ -0,0 +1,119 @@
1762+// License: GPL. For details, see LICENSE file.
1763+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1764+
1765+import java.awt.image.BufferedImage;
1766+import java.io.IOException;
1767+import java.io.InputStream;
1768+import java.util.Collection;
1769+import java.util.HashSet;
1770+import java.util.List;
1771+import java.util.stream.Collectors;
1772+
1773+import org.openstreetmap.gui.jmapviewer.Tile;
1774+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
1775+import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
1776+import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
1777+import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
1778+import org.openstreetmap.josm.tools.ListenerList;
1779+import org.openstreetmap.josm.tools.Logging;
1780+
1781+/**
1782+ * A class for MapBox Vector Tiles
1783+ *
1784+ * @author Taylor Smock
1785+ * @since xxx
1786+ */
1787+public class MVTTile extends Tile implements VectorTile {
1788+ private final ListenerList<TileListener> listenerList = ListenerList.create();
1789+ private Collection<Layer> layers;
1790+ private int extent = Layer.DEFAULT_EXTENT;
1791+ static final BufferedImage CLEAR_LOADED = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR);
1792+
1793+ /**
1794+ * Create a new Tile
1795+ * @param source The source of the tile
1796+ * @param xtile The x coordinate for the tile
1797+ * @param ytile The y coordinate for the tile
1798+ * @param zoom The zoom for the tile
1799+ */
1800+ public MVTTile(TileSource source, int xtile, int ytile, int zoom) {
1801+ super(source, xtile, ytile, zoom);
1802+ }
1803+
1804+ @Override
1805+ public void loadImage(final InputStream inputStream) throws IOException {
1806+ if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) {
1807+ this.initLoading();
1808+ ProtoBufParser parser = new ProtoBufParser(inputStream);
1809+ Collection<ProtoBufRecord> protoBufRecords = parser.allRecords();
1810+ this.layers = new HashSet<>();
1811+ this.layers = protoBufRecords.stream().map(record -> {
1812+ Layer mvtLayer = null;
1813+ if (record.getField() == Layer.LAYER_FIELD) {
1814+ try (ProtoBufParser tParser = new ProtoBufParser(record.getBytes())) {
1815+ mvtLayer = new Layer(tParser.allRecords());
1816+ } catch (IOException e) {
1817+ Logging.error(e);
1818+ } finally {
1819+ // Cleanup bytes
1820+ record.close();
1821+ }
1822+ }
1823+ return mvtLayer;
1824+ }).collect(Collectors.toCollection(HashSet::new));
1825+ this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT);
1826+ this.finishLoading();
1827+ this.listenerList.fireEvent(event -> event.finishedLoading(this));
1828+ // Ensure that we don't keep the loading image around
1829+ this.image = CLEAR_LOADED;
1830+ }
1831+ }
1832+
1833+ @Override
1834+ public Collection<Layer> getLayers() {
1835+ return this.layers;
1836+ }
1837+
1838+ @Override
1839+ public int getExtent() {
1840+ return this.extent;
1841+ }
1842+
1843+ /**
1844+ * Add a tile loader finisher listener
1845+ *
1846+ * @param listener The listener to add
1847+ */
1848+ public void addTileLoaderFinisher(TileListener listener) {
1849+ // Add as weak listeners since we don't want to keep unnecessary references.
1850+ this.listenerList.addWeakListener(listener);
1851+ }
1852+
1853+ /**
1854+ * A class that can be notified that a tile has finished loading
1855+ *
1856+ * @author Taylor Smock
1857+ */
1858+ public interface TileListener {
1859+ /**
1860+ * Called when the MVTTile is finished loading
1861+ *
1862+ * @param tile The tile that finished loading
1863+ */
1864+ void finishedLoading(MVTTile tile);
1865+ }
1866+
1867+ /**
1868+ * A class used to set the layers that an MVTTile will show.
1869+ *
1870+ * @author Taylor Smock
1871+ */
1872+ public interface LayerShower {
1873+ /**
1874+ * Get a list of layers to show
1875+ *
1876+ * @return A list of layer names
1877+ */
1878+ List<String> layersToShow();
1879+ }
1880+}
1881diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
1882new file mode 100644
1883index 000000000..bf1b368d9
1884--- /dev/null
1885+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
1886@@ -0,0 +1,79 @@
1887+// License: GPL. For details, see LICENSE file.
1888+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1889+
1890+import java.util.concurrent.ThreadPoolExecutor;
1891+
1892+import org.openstreetmap.gui.jmapviewer.Tile;
1893+import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
1894+import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
1895+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
1896+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
1897+import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
1898+import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
1899+import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
1900+import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
1901+import org.openstreetmap.josm.data.imagery.TileJobOptions;
1902+import org.openstreetmap.josm.data.preferences.IntegerProperty;
1903+import org.openstreetmap.josm.tools.CheckParameterUtil;
1904+
1905+import org.apache.commons.jcs3.access.behavior.ICacheAccess;
1906+
1907+/**
1908+ * A TileLoader class for MVT tiles
1909+ * @author Taylor Smock
1910+ * @since xxx
1911+ */
1912+public class MapBoxVectorCachedTileLoader implements TileLoader, CachedTileLoader {
1913+ protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
1914+ protected final TileLoaderListener listener;
1915+ protected final TileJobOptions options;
1916+ private static final IntegerProperty THREAD_LIMIT =
1917+ new IntegerProperty("imagery.vector.mvtloader.maxjobs", TMSCachedTileLoader.THREAD_LIMIT.getDefaultValue());
1918+ private static final ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER =
1919+ TMSCachedTileLoader.getNewThreadPoolExecutor("MVT-downloader-%d", THREAD_LIMIT.get());
1920+
1921+ /**
1922+ * Constructor
1923+ * @param listener called when tile loading has finished
1924+ * @param cache of the cache
1925+ * @param options tile job options
1926+ */
1927+ public MapBoxVectorCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
1928+ TileJobOptions options) {
1929+ CheckParameterUtil.ensureParameterNotNull(cache, "cache");
1930+ this.cache = cache;
1931+ this.options = options;
1932+ this.listener = listener;
1933+ }
1934+
1935+ @Override
1936+ public void clearCache(TileSource source) {
1937+ this.cache.remove(source.getName() + ':');
1938+ }
1939+
1940+ @Override
1941+ public TileJob createTileLoaderJob(Tile tile) {
1942+ return new MapBoxVectorCachedTileLoaderJob(
1943+ listener,
1944+ tile,
1945+ cache,
1946+ options,
1947+ getDownloadExecutor());
1948+ }
1949+
1950+ @Override
1951+ public void cancelOutstandingTasks() {
1952+ final ThreadPoolExecutor executor = getDownloadExecutor();
1953+ executor.getQueue().stream().filter(executor::remove).filter(MapBoxVectorCachedTileLoaderJob.class::isInstance)
1954+ .map(MapBoxVectorCachedTileLoaderJob.class::cast).forEach(JCSCachedTileLoaderJob::handleJobCancellation);
1955+ }
1956+
1957+ @Override
1958+ public boolean hasOutstandingTasks() {
1959+ return getDownloadExecutor().getTaskCount() > getDownloadExecutor().getCompletedTaskCount();
1960+ }
1961+
1962+ private static ThreadPoolExecutor getDownloadExecutor() {
1963+ return DEFAULT_DOWNLOAD_JOB_DISPATCHER;
1964+ }
1965+}
1966diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
1967new file mode 100644
1968index 000000000..748172f5f
1969--- /dev/null
1970+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
1971@@ -0,0 +1,26 @@
1972+// License: GPL. For details, see LICENSE file.
1973+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
1974+
1975+import java.util.concurrent.ThreadPoolExecutor;
1976+
1977+import org.openstreetmap.gui.jmapviewer.Tile;
1978+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
1979+import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry;
1980+import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob;
1981+import org.openstreetmap.josm.data.imagery.TileJobOptions;
1982+
1983+import org.apache.commons.jcs3.access.behavior.ICacheAccess;
1984+
1985+/**
1986+ * Bridge to JCS cache for MVT tiles
1987+ * @author Taylor Smock
1988+ * @since xxx
1989+ */
1990+public class MapBoxVectorCachedTileLoaderJob extends TMSCachedTileLoaderJob {
1991+
1992+ public MapBoxVectorCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
1993+ ICacheAccess<String, BufferedImageCacheEntry> cache, TileJobOptions options,
1994+ ThreadPoolExecutor downloadExecutor) {
1995+ super(listener, tile, cache, options, downloadExecutor);
1996+ }
1997+}
1998diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
1999new file mode 100644
2000index 000000000..413c7b32b
2001--- /dev/null
2002+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
2003@@ -0,0 +1,92 @@
2004+// License: GPL. For details, see LICENSE file.
2005+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
2006+import static org.openstreetmap.josm.tools.I18n.tr;
2007+
2008+import java.io.IOException;
2009+import java.io.InputStream;
2010+import java.util.List;
2011+import java.util.Objects;
2012+import java.util.stream.Collectors;
2013+
2014+import javax.json.Json;
2015+import javax.json.JsonException;
2016+import javax.json.JsonReader;
2017+
2018+import org.openstreetmap.josm.data.imagery.ImageryInfo;
2019+import org.openstreetmap.josm.data.imagery.JosmTemplatedTMSTileSource;
2020+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapBoxVectorStyle;
2021+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.Source;
2022+import org.openstreetmap.josm.gui.ExtendedDialog;
2023+import org.openstreetmap.josm.gui.MainApplication;
2024+import org.openstreetmap.josm.gui.util.GuiHelper;
2025+import org.openstreetmap.josm.gui.widgets.JosmComboBox;
2026+import org.openstreetmap.josm.io.CachedFile;
2027+import org.openstreetmap.josm.tools.Logging;
2028+
2029+/**
2030+ * Tile Source handling for Mapbox Vector Tile sources
2031+ * @author Taylor Smock
2032+ * @since xxx
2033+ */
2034+public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource {
2035+ private final MapBoxVectorStyle styleSource;
2036+
2037+ /**
2038+ * Create a new {@link MapboxVectorTileSource} from an {@link ImageryInfo}
2039+ * @param info The info to create the source from
2040+ */
2041+ public MapboxVectorTileSource(ImageryInfo info) {
2042+ super(info);
2043+ MapBoxVectorStyle mapBoxVectorStyle = null;
2044+ try (CachedFile style = new CachedFile(info.getUrl());
2045+ InputStream inputStream = style.getInputStream();
2046+ JsonReader reader = Json.createReader(inputStream)) {
2047+ reader.readObject();
2048+ // OK, we have a stylesheet
2049+ mapBoxVectorStyle = MapBoxVectorStyle.getMapBoxVectorStyle(info.getUrl());
2050+ } catch (IOException | JsonException e) {
2051+ Logging.trace(e);
2052+ }
2053+ this.styleSource = mapBoxVectorStyle;
2054+ if (this.styleSource != null) {
2055+ final Source source;
2056+ List<Source> sources = this.styleSource.getSources().keySet().stream().filter(Objects::nonNull)
2057+ .collect(Collectors.toList());
2058+ if (sources.size() == 1) {
2059+ source = sources.get(0);
2060+ } else if (!sources.isEmpty()) {
2061+ // Ask user what source they want.
2062+ source = GuiHelper.runInEDTAndWaitAndReturn(() -> {
2063+ ExtendedDialog dialog = new ExtendedDialog(MainApplication.getMainFrame(),
2064+ tr("Select Vector Tile Layers"), tr("Add layers"));
2065+ JosmComboBox<Source> comboBox = new JosmComboBox<>(sources.toArray(new Source[0]));
2066+ comboBox.setSelectedIndex(0);
2067+ dialog.setContent(comboBox);
2068+ dialog.showDialog();
2069+ return (Source) comboBox.getSelectedItem();
2070+ });
2071+ } else {
2072+ // Umm. What happened? We probably have an invalid style source.
2073+ throw new InvalidMapboxVectorTileException(tr("Cannot understand style source: {0}", info.getUrl()));
2074+ }
2075+ if (source != null) {
2076+ this.name = name + ": " + source.getName();
2077+ // There can technically be multiple URL's for this field; unfortunately, JOSM can only handle one right now.
2078+ this.baseUrl = source.getUrls().get(0);
2079+ this.minZoom = source.getMinZoom();
2080+ this.maxZoom = source.getMaxZoom();
2081+ if (source.getAttributionText() != null) {
2082+ this.setAttributionText(source.getAttributionText());
2083+ }
2084+ }
2085+ }
2086+ }
2087+
2088+ /**
2089+ * Get the style source for this Vector Tile source
2090+ * @return The source to use for styling
2091+ */
2092+ public MapBoxVectorStyle getStyleSource() {
2093+ return this.styleSource;
2094+ }
2095+}
2096diff --git a/test/data/mapillary.json b/test/data/mapillary.json
2097new file mode 100644
2098index 000000000..0f6f9483d
2099--- /dev/null
2100+++ b/test/data/mapillary.json
2101@@ -0,0 +1,111 @@
2102+{
2103+ "version":8,
2104+ "name":"Mapillary",
2105+ "owner":"Mapillary",
2106+ "id":"mapillary",
2107+ "sources":{
2108+ "mapillary-source":{
2109+ "type":"vector",
2110+ "tiles":[
2111+ "https://tiles3.mapillary.com/v0.1/{z}/{x}/{y}.mvt"
2112+ ],
2113+ "maxzoom":14
2114+ },
2115+ "mapillary-features-source": {
2116+ "maxzoom": 20,
2117+ "minzoom": 14,
2118+ "tiles": [ "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_&layers=points&per_page=1000" ],
2119+ "type": "vector"
2120+ },
2121+ "mapillary-traffic-signs-source": {
2122+ "maxzoom": 20,
2123+ "minzoom": 14,
2124+ "tiles": [ "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_&layers=trafficsigns&per_page=1000" ],
2125+ "type": "vector"
2126+ }
2127+ },
2128+ "layers":[
2129+ {
2130+ "filter": [ "==", "pano", 1 ],
2131+ "id": "mapillary-panos",
2132+ "type": "circle",
2133+ "source": "mapillary-source",
2134+ "source-layer": "mapillary-images",
2135+ "minzoom": 17,
2136+ "paint": {
2137+ "circle-color": "#05CB63",
2138+ "circle-opacity": 0.5,
2139+ "circle-radius": 18
2140+ }
2141+ },
2142+ {
2143+ "id": "mapillary-dots",
2144+ "type": "circle",
2145+ "source": "mapillary-source",
2146+ "source-layer": "mapillary-images",
2147+ "interactive": true,
2148+ "minzoom": 14,
2149+ "paint": {
2150+ "circle-color": "#05CB63",
2151+ "circle-radius": 6
2152+ }
2153+ },
2154+ {
2155+ "id": "mapillary-lines",
2156+ "type": "line",
2157+ "source": "mapillary-source",
2158+ "source-layer": "mapillary-sequences",
2159+ "minzoom": 6,
2160+ "paint": {
2161+ "line-color": "#05CB63",
2162+ "line-width": 2
2163+ }
2164+ },
2165+ {
2166+ "id": "mapillary-overview",
2167+ "type": "circle",
2168+ "source": "mapillary-source",
2169+ "source-layer": "mapillary-sequence-overview",
2170+ "maxzoom": 6,
2171+ "paint": {
2172+ "circle-radius": 4,
2173+ "circle-opacity": 0.6,
2174+ "circle-color": "#05CB63"
2175+ }
2176+ },
2177+ {
2178+ "id": "mapillary-features",
2179+ "type": "symbol",
2180+ "source": "mapillary-features-source",
2181+ "source-layer": "mapillary-map-features",
2182+ "interactive": true,
2183+ "minzoom": 14,
2184+ "layout": {
2185+ "icon-image": "{value}",
2186+ "icon-allow-overlap": true,
2187+ "symbol-avoid-edges": true
2188+ },
2189+ "paint": {
2190+ "text-color": "#fff",
2191+ "text-halo-color": "#000"
2192+ }
2193+ },
2194+ {
2195+ "id": "mapillary-traffic-signs",
2196+ "type": "symbol",
2197+ "source": "mapillary-traffic-signs-source",
2198+ "source-layer": "mapillary-map-features",
2199+ "interactive": true,
2200+ "minzoom": 14,
2201+ "layout": {
2202+ "icon-image": "{value}",
2203+ "icon-allow-overlap": true,
2204+ "symbol-avoid-edges": true
2205+ },
2206+ "paint": {
2207+ "text-color": "#fff",
2208+ "text-halo-color": "#000"
2209+ }
2210+ }
2211+ ]
2212+}
2213diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java
2214new file mode 100644
2215index 000000000..5468fe649
2216--- /dev/null
2217+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java
2218@@ -0,0 +1,122 @@
2219+// License: GPL. For details, see LICENSE file.
2220+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
2221+
2222+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
2223+import static org.junit.jupiter.api.Assertions.assertEquals;
2224+import static org.junit.jupiter.api.Assertions.assertNotNull;
2225+import static org.junit.jupiter.api.Assertions.assertSame;
2226+import static org.junit.jupiter.api.Assertions.assertTrue;
2227+
2228+
2229+import static org.openstreetmap.josm.data.imagery.vectortile.mapbox.LayerTest.getSimpleFeatureLayerBytes;
2230+import static org.openstreetmap.josm.data.imagery.vectortile.mapbox.LayerTest.getLayer;
2231+
2232+import java.text.NumberFormat;
2233+import java.util.Arrays;
2234+
2235+import org.junit.jupiter.api.Test;
2236+
2237+/**
2238+ * Test class for {@link Feature}
2239+ */
2240+class FeatureTest {
2241+ /**
2242+ * This can be used to replace bytes 11-14 (inclusive) in {@link LayerTest#simpleFeatureLayerBytes}.
2243+ */
2244+ private final byte[] nonPackedTags = new byte[] {0x10, 0x00, 0x10, 0x00};
2245+
2246+ @Test
2247+ void testCreation() {
2248+ testCreation(getSimpleFeatureLayerBytes());
2249+ }
2250+
2251+ @Test
2252+ void testCreationUnpacked() {
2253+ byte[] copyBytes = getSimpleFeatureLayerBytes();
2254+ System.arraycopy(nonPackedTags, 0, copyBytes, 13, nonPackedTags.length);
2255+ testCreation(copyBytes);
2256+ }
2257+
2258+ @Test
2259+ void testCreationTrueToFalse() {
2260+ byte[] copyBytes = getSimpleFeatureLayerBytes();
2261+ copyBytes[copyBytes.length - 1] = 0x00; // set value=false
2262+ Layer layer = assertDoesNotThrow(() -> getLayer(copyBytes));
2263+ assertSame(Boolean.FALSE, layer.getValue(0));
2264+ }
2265+
2266+ @Test
2267+ void testNumberGrouping() {
2268+ // This is the float we are adding
2269+ // 49 74 24 00 == 1_000_000f
2270+ // 3f 80 00 00 == 1f
2271+ byte[] newBytes = new byte[] {0x22, 0x09, 0x15, 0x00, 0x24, 0x74, 0x49};
2272+ byte[] copyBytes = Arrays.copyOf(getSimpleFeatureLayerBytes(), getSimpleFeatureLayerBytes().length + newBytes.length - 4);
2273+ // Change last few bytes
2274+ System.arraycopy(newBytes, 0, copyBytes, 25, newBytes.length);
2275+ // Update the length of the record
2276+ copyBytes[1] = (byte) (copyBytes[1] + newBytes.length - 4);
2277+ final NumberFormat numberFormat = NumberFormat.getNumberInstance();
2278+ final boolean numberFormatGroupingUsed = numberFormat.isGroupingUsed();
2279+ // Sanity check
2280+ Layer layer;
2281+ try {
2282+ numberFormat.setGroupingUsed(true);
2283+ layer = assertDoesNotThrow(() -> getLayer(copyBytes));
2284+ assertTrue(numberFormat.isGroupingUsed());
2285+ } finally {
2286+ numberFormat.setGroupingUsed(numberFormatGroupingUsed);
2287+ }
2288+ assertEquals(1, layer.getFeatures().size());
2289+ assertEquals("t", layer.getName());
2290+ assertEquals(2, layer.getVersion());
2291+ assertEquals("a", layer.getKey(0));
2292+ assertEquals(1_000_000f, ((Number) layer.getValue(0)).floatValue(), 0.00001);
2293+
2294+ // Feature check
2295+ Feature feature = layer.getFeatures().iterator().next();
2296+ checkDefaultGeometry(feature);
2297+ assertEquals("1000000", feature.getTags().get("a"));
2298+ }
2299+
2300+ private void testCreation(byte[] bytes) {
2301+ Layer layer = assertDoesNotThrow(() -> getLayer(bytes));
2302+ // Sanity check the layer
2303+ assertEquals(1, layer.getFeatures().size());
2304+ assertEquals("t", layer.getName());
2305+ assertEquals(2, layer.getVersion());
2306+ assertEquals("a", layer.getKey(0));
2307+ assertSame(Boolean.TRUE, layer.getValue(0));
2308+
2309+ // OK. Get the feature.
2310+ Feature feature = layer.getFeatures().iterator().next();
2311+
2312+ checkDefaultTags(feature);
2313+
2314+ // Check id (should be the default of 0)
2315+ assertEquals(1, feature.getId());
2316+
2317+ checkDefaultGeometry(feature);
2318+ }
2319+
2320+ private void checkDefaultTags(Feature feature) {
2321+ // Check tags
2322+ assertEquals(1, feature.getTags().size());
2323+ assertTrue(feature.getTags().containsKey("a"));
2324+ // We are converting to a tag map (Map<String, String>), so "true"
2325+ assertEquals("true", feature.getTags().get("a"));
2326+ }
2327+
2328+ private void checkDefaultGeometry(Feature feature) {
2329+ // Check the geometry
2330+ assertEquals(GeometryTypes.POINT, feature.getGeometryType());
2331+ assertEquals(1, feature.getGeometry().size());
2332+ CommandInteger geometry = feature.getGeometry().get(0);
2333+ assertEquals(Command.MoveTo, geometry.getType());
2334+ assertEquals(2, geometry.getOperations().length);
2335+ assertEquals(25, geometry.getOperations()[0]);
2336+ assertEquals(17, geometry.getOperations()[1]);
2337+ assertNotNull(feature.getGeometryObject());
2338+ assertEquals(feature.getGeometryObject(), feature.getGeometryObject());
2339+ }
2340+}
2341diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java
2342new file mode 100644
2343index 000000000..175d64cd7
2344--- /dev/null
2345+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java
2346@@ -0,0 +1,169 @@
2347+// License: GPL. For details, see LICENSE file.
2348+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
2349+
2350+import static org.junit.jupiter.api.Assertions.assertEquals;
2351+import static org.junit.jupiter.api.Assertions.assertFalse;
2352+import static org.junit.jupiter.api.Assertions.assertThrows;
2353+import static org.junit.jupiter.api.Assertions.assertTrue;
2354+
2355+
2356+import java.awt.geom.Area;
2357+import java.awt.geom.Ellipse2D;
2358+import java.awt.geom.Path2D;
2359+import java.awt.geom.PathIterator;
2360+import java.awt.geom.Point2D;
2361+import java.util.ArrayList;
2362+import java.util.Arrays;
2363+import java.util.Collections;
2364+import java.util.List;
2365+
2366+import org.junit.jupiter.api.Test;
2367+
2368+/**
2369+ * Test class for {@link Geometry}
2370+ * @author Taylor Smock
2371+ * @since xxx
2372+ */
2373+class GeometryTest {
2374+ /**
2375+ * Create a command integer fairly easily
2376+ * @param command The command type (see {@link Command})
2377+ * @param parameters The parameters for the command
2378+ * @return A command integer
2379+ */
2380+ private static CommandInteger createCommandInteger(int command, int... parameters) {
2381+ CommandInteger commandInteger = new CommandInteger(command);
2382+ if (parameters != null) {
2383+ for (int parameter : parameters) {
2384+ commandInteger.addParameter(parameter);
2385+ }
2386+ }
2387+ return commandInteger;
2388+ }
2389+
2390+ /**
2391+ * Check the current
2392+ * @param pathIterator The path to check
2393+ * @param expected The expected coords
2394+ */
2395+ private static void checkCurrentSegmentAndIncrement(PathIterator pathIterator, float... expected) {
2396+ float[] coords = new float[6];
2397+ int type = pathIterator.currentSegment(coords);
2398+ pathIterator.next();
2399+ for (int i = 0; i < expected.length; i++) {
2400+ assertEquals(expected[i], coords[i]);
2401+ }
2402+ if (Arrays.asList(PathIterator.SEG_MOVETO, PathIterator.SEG_LINETO).contains(type)) {
2403+ assertEquals(2, expected.length, "You should check both x and y coordinates");
2404+ } else if (PathIterator.SEG_QUADTO == type) {
2405+ assertEquals(4, expected.length, "You should check all x and y coordinates");
2406+ } else if (PathIterator.SEG_CUBICTO == type) {
2407+ assertEquals(6, expected.length, "You should check all x and y coordinates");
2408+ } else if (PathIterator.SEG_CLOSE == type) {
2409+ assertEquals(0, expected.length, "CloseTo has no expected coordinates to check");
2410+ }
2411+ }
2412+
2413+ @Test
2414+ void testBadGeometry() {
2415+ IllegalArgumentException badPointException = assertThrows(IllegalArgumentException.class,
2416+ () -> new Geometry(GeometryTypes.POINT, Collections.singletonList(createCommandInteger(1))));
2417+ assertEquals("POINT with 0 arguments is not understood", badPointException.getMessage());
2418+ IllegalArgumentException badLineException = assertThrows(IllegalArgumentException.class,
2419+ () -> new Geometry(GeometryTypes.LINESTRING, Collections.singletonList(createCommandInteger(15))));
2420+ assertEquals("LINESTRING with 0 arguments is not understood", badLineException.getMessage());
2421+ }
2422+
2423+ @Test
2424+ void testPoint() {
2425+ CommandInteger moveTo = createCommandInteger(9, 17, 34);
2426+ Geometry geometry = new Geometry(GeometryTypes.POINT, Collections.singletonList(moveTo));
2427+ assertEquals(1, geometry.getShapes().size());
2428+ Ellipse2D shape = (Ellipse2D) geometry.getShapes().iterator().next();
2429+ assertEquals(17, shape.getCenterX());
2430+ assertEquals(34, shape.getCenterY());
2431+ }
2432+
2433+ @Test
2434+ void testLine() {
2435+ CommandInteger moveTo = createCommandInteger(9, 2, 2);
2436+ CommandInteger lineTo = createCommandInteger(18, 0, 8, 8, 0);
2437+ Geometry geometry = new Geometry(GeometryTypes.LINESTRING, Arrays.asList(moveTo, lineTo));
2438+ assertEquals(1, geometry.getShapes().size());
2439+ Path2D path = (Path2D) geometry.getShapes().iterator().next();
2440+ PathIterator pathIterator = path.getPathIterator(null);
2441+ checkCurrentSegmentAndIncrement(pathIterator, 2, 2);
2442+ checkCurrentSegmentAndIncrement(pathIterator, 2, 10);
2443+ checkCurrentSegmentAndIncrement(pathIterator, 10, 10);
2444+ assertTrue(pathIterator.isDone());
2445+ }
2446+
2447+ @Test
2448+ void testPolygon() {
2449+ List<CommandInteger> commands = new ArrayList<>(3);
2450+ commands.add(createCommandInteger(9, 3, 6));
2451+ commands.add(createCommandInteger(18, 5, 6, 12, 22));
2452+ commands.add(createCommandInteger(15));
2453+
2454+ Geometry geometry = new Geometry(GeometryTypes.POLYGON, commands);
2455+ assertEquals(1, geometry.getShapes().size());
2456+
2457+ Area area = (Area) geometry.getShapes().iterator().next();
2458+ PathIterator pathIterator = area.getPathIterator(null);
2459+ checkCurrentSegmentAndIncrement(pathIterator, 3, 6);
2460+ // This is somewhat unexpected, and may change based off of JVM implementations
2461+ // But for whatever reason, Java flips the inner coordinates in this case.
2462+ checkCurrentSegmentAndIncrement(pathIterator, 20, 34);
2463+ checkCurrentSegmentAndIncrement(pathIterator, 8, 12);
2464+ checkCurrentSegmentAndIncrement(pathIterator, 3, 6);
2465+ checkCurrentSegmentAndIncrement(pathIterator);
2466+ assertTrue(pathIterator.isDone());
2467+ }
2468+
2469+ @Test
2470+ void testBadPolygon() {
2471+ /*
2472+ * "Linear rings MUST be geometric objects that have no anomalous geometric points,
2473+ * such as self-intersection or self-tangency. The position of the cursor before
2474+ * calling the ClosePath command of a linear ring SHALL NOT repeat the same position
2475+ * as the first point in the linear ring as this would create a zero-length line
2476+ * segment. A linear ring SHOULD NOT have an area calculated by the surveyor's
2477+ * formula equal to zero, as this would signify a ring with anomalous geometric points."
2478+ */
2479+ List<CommandInteger> commands = new ArrayList<>(3);
2480+ commands.add(createCommandInteger(9, 0, 0));
2481+ commands.add(createCommandInteger(18, 0, 0));
2482+ commands.add(createCommandInteger(15));
2483+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new Geometry(GeometryTypes.POLYGON, commands));
2484+ assertEquals("POLYGON cannot have zero area", exception.getMessage());
2485+ }
2486+
2487+ @Test
2488+ void testMultiPolygon() {
2489+ List<CommandInteger> commands = new ArrayList<>(10);
2490+ // Polygon 1
2491+ commands.add(createCommandInteger(9, 0, 0));
2492+ commands.add(createCommandInteger(26, 10, 0, 0, 10, -10, 0));
2493+ commands.add(createCommandInteger(15));
2494+ // Polygon 2 outer
2495+ commands.add(createCommandInteger(9, 11, 1));
2496+ commands.add(createCommandInteger(26, 9, 0, 0, 9, -9, 0));
2497+ commands.add(createCommandInteger(15));
2498+ // Polygon 2 inner
2499+ commands.add(createCommandInteger(9, 2, -7));
2500+ commands.add(createCommandInteger(26, 0, 4, 4, 0, 0, -4));
2501+ commands.add(createCommandInteger(15));
2502+
2503+ Geometry geometry = new Geometry(GeometryTypes.POLYGON, commands);
2504+ assertEquals(1, geometry.getShapes().size());
2505+ Area area = (Area) geometry.getShapes().iterator().next();
2506+ assertFalse(area.isSingular());
2507+ PathIterator pathIterator = area.getPathIterator(null);
2508+ assertEquals(PathIterator.WIND_NON_ZERO, pathIterator.getWindingRule());
2509+ assertTrue(area.contains(new Point2D.Float(5, 5)));
2510+ assertTrue(area.contains(new Point2D.Float(12, 12)));
2511+ assertFalse(area.contains(new Point2D.Float(15, 15)));
2512+ assertFalse(area.contains(new Point2D.Float(10, 11)));
2513+ assertFalse(area.contains(new Point2D.Float(-1, -1)));
2514+ }
2515+}
2516diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java
2517new file mode 100644
2518index 000000000..da0f9b3c7
2519--- /dev/null
2520+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java
2521@@ -0,0 +1,47 @@
2522+// License: GPL. For details, see LICENSE file.
2523+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
2524+
2525+import static org.junit.jupiter.api.Assertions.assertEquals;
2526+import static org.junit.jupiter.api.Assertions.fail;
2527+
2528+
2529+import org.openstreetmap.josm.TestUtils;
2530+
2531+import org.junit.jupiter.api.Test;
2532+import org.junit.jupiter.params.ParameterizedTest;
2533+import org.junit.jupiter.params.provider.EnumSource;
2534+
2535+/**
2536+ * Test class for {@link GeometryTypes}
2537+ * @author Taylor Smock
2538+ * @since xxx
2539+ */
2540+class GeometryTypesTest {
2541+ @Test
2542+ void testNaiveEnumTest() {
2543+ TestUtils.superficialEnumCodeCoverage(GeometryTypes.class);
2544+ TestUtils.superficialEnumCodeCoverage(GeometryTypes.Ring.class);
2545+ }
2546+
2547+ @ParameterizedTest
2548+ @EnumSource(GeometryTypes.class)
2549+ void testExpectedIds(GeometryTypes type) {
2550+ // Ensure that users can get the type from the ordinal
2551+ // See https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L8
2552+ // for the expected values
2553+ final int expectedId;
2554+ if (type == GeometryTypes.UNKNOWN) {
2555+ expectedId = 0;
2556+ } else if (type == GeometryTypes.POINT) {
2557+ expectedId = 1;
2558+ } else if (type == GeometryTypes.LINESTRING) {
2559+ expectedId = 2;
2560+ } else if (type == GeometryTypes.POLYGON) {
2561+ expectedId = 3;
2562+ } else {
2563+ fail("Unknown geometry type, see vector tile spec");
2564+ expectedId = Integer.MIN_VALUE;
2565+ }
2566+ assertEquals(expectedId, type.ordinal());
2567+ }
2568+}
2569diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
2570new file mode 100644
2571index 000000000..fc3ba9c27
2572--- /dev/null
2573+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
2574@@ -0,0 +1,135 @@
2575+// License: GPL. For details, see LICENSE file.
2576+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
2577+
2578+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
2579+import static org.junit.jupiter.api.Assertions.assertEquals;
2580+import static org.junit.jupiter.api.Assertions.assertThrows;
2581+
2582+import java.io.FileInputStream;
2583+import java.io.IOException;
2584+import java.util.Arrays;
2585+import java.util.List;
2586+
2587+import org.openstreetmap.josm.TestUtils;
2588+import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
2589+import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
2590+
2591+import nl.jqno.equalsverifier.EqualsVerifier;
2592+import org.junit.jupiter.api.Test;
2593+
2594+/**
2595+ * Test class for {@link Layer}
2596+ */
2597+public class LayerTest {
2598+ /**
2599+ * This looks something like this (if it were json). Note that some keys could be repeated,
2600+ * and so could be better represented as an array. Specifically, "features", "key", and "value".
2601+ * "layer": {
2602+ * "name": "t",
2603+ * "version": 2,
2604+ * "features": {
2605+ * "type": "POINT",
2606+ * "tags": [0, 0],
2607+ * "geometry": [9, 50, 34]
2608+ * },
2609+ * "key": "a",
2610+ * "value": true
2611+ * }
2612+ *
2613+ * WARNING: DO NOT MODIFY THIS ARRAY DIRECTLY -- it could contaminate other tests
2614+ */
2615+ private static final byte[] simpleFeatureLayerBytes = new byte[] {
2616+ 0x1a, 0x1b, // layer, 27 bytes for the rest
2617+ 0x0a, 0x01, 0x74, // name=t
2618+ 0x78, 0x02, // version=2
2619+ 0x12, 0x0d, // features, 11 bytes
2620+ 0x08, 0x01, // id=1
2621+ 0x18, 0x01, // type=POINT
2622+ 0x12, 0x02, 0x00, 0x00, // tags=[0, 0] (packed). Non-packed would be [0x10, 0x00, 0x10, 0x00]
2623+ 0x22, 0x03, 0x09, 0x32, 0x22, // geometry=[9, 50, 34]
2624+ 0x1a, 0x01, 0x61, // key=a
2625+ 0x22, 0x02, 0x38, 0x01, // value=true (boolean)
2626+ };
2627+
2628+ /**
2629+ * Gets a copy of {@link #simpleFeatureLayerBytes} so that a test doesn't accidentally change the bytes
2630+ * @return An array that can be modified.
2631+ */
2632+ static byte[] getSimpleFeatureLayerBytes() {
2633+ return Arrays.copyOf(simpleFeatureLayerBytes, simpleFeatureLayerBytes.length);
2634+ }
2635+
2636+ /**
2637+ * Create a layer from bytes
2638+ * @param bytes The bytes that make up the layer
2639+ * @return The generated layer
2640+ * @throws IOException If something happened (should never trigger)
2641+ */
2642+ static Layer getLayer(byte[] bytes) throws IOException {
2643+ List<ProtoBufRecord> records = (List<ProtoBufRecord>) new ProtoBufParser(bytes).allRecords();
2644+ assertEquals(1, records.size());
2645+ return new Layer(new ProtoBufParser(records.get(0).getBytes()).allRecords());
2646+ }
2647+
2648+ @Test
2649+ void testLayerCreation() throws IOException {
2650+ List<ProtoBufRecord> layers = (List<ProtoBufRecord>) new ProtoBufParser(new FileInputStream(TestUtils.getTestDataRoot()
2651+ + "pbf/mapillary/14/3249/6258.mvt")).allRecords();
2652+ Layer sequenceLayer = new Layer(layers.get(0).getBytes());
2653+ assertEquals("mapillary-sequences", sequenceLayer.getName());
2654+ assertEquals(1, sequenceLayer.getFeatures().size());
2655+ assertEquals(1, sequenceLayer.getGeometry().size());
2656+ assertEquals(2048, sequenceLayer.getExtent());
2657+ assertEquals(1, sequenceLayer.getVersion());
2658+
2659+ Layer imageLayer = new Layer(layers.get(1).getBytes());
2660+ assertEquals("mapillary-images", imageLayer.getName());
2661+ assertEquals(116, imageLayer.getFeatures().size());
2662+ assertEquals(116, imageLayer.getGeometry().size());
2663+ assertEquals(2048, imageLayer.getExtent());
2664+ assertEquals(1, imageLayer.getVersion());
2665+ }
2666+
2667+ @Test
2668+ void testLayerEqualsHashCode() throws IOException {
2669+ List<ProtoBufRecord> layers = (List<ProtoBufRecord>) new ProtoBufParser(new FileInputStream(TestUtils.getTestDataRoot()
2670+ + "pbf/mapillary/14/3249/6258.mvt")).allRecords();
2671+ EqualsVerifier.forClass(Layer.class).withPrefabValues(byte[].class, layers.get(0).getBytes(), layers.get(1).getBytes())
2672+ .verify();
2673+ }
2674+
2675+ @Test
2676+ void testVersionsNumbers() {
2677+ byte[] copyByte = getSimpleFeatureLayerBytes();
2678+ assertEquals(2, assertDoesNotThrow(() -> getLayer(copyByte)).getVersion());
2679+ copyByte[6] = 1;
2680+ assertEquals(1, assertDoesNotThrow(() -> getLayer(copyByte)).getVersion());
2681+ copyByte[6] = 0;
2682+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte));
2683+ assertEquals("We do not understand version 0 of the vector tile specification", exception.getMessage());
2684+ copyByte[6] = 3;
2685+ exception = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte));
2686+ assertEquals("We do not understand version 3 of the vector tile specification", exception.getMessage());
2687+ // Remove version number (AKA change it to some unknown field). Default is version=1.
2688+ copyByte[5] = 0x18;
2689+ assertEquals(1, assertDoesNotThrow(() -> getLayer(copyByte)).getVersion());
2690+ }
2691+
2692+ @Test
2693+ void testLayerName() throws IOException {
2694+ byte[] copyByte = getSimpleFeatureLayerBytes();
2695+ Layer layer = getLayer(copyByte);
2696+ assertEquals("t", layer.getName());
2697+ copyByte[2] = 0x1a; // name=t -> ?
2698+ Exception noNameException = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte));
2699+ assertEquals("Vector tile layers must have a layer name", noNameException.getMessage());
2700+ }
2701+
2702+ @Test
2703+ void testUnknownField() {
2704+ byte[] copyByte = getSimpleFeatureLayerBytes();
2705+ copyByte[27] = 0x78;
2706+ Exception unknownField = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte));
2707+ assertEquals("Unknown field in vector tile layer value (15)", unknownField.getMessage());
2708+ }
2709+}
2710diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
2711new file mode 100644
2712index 000000000..66e4ea781
2713--- /dev/null
2714+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
2715@@ -0,0 +1,82 @@
2716+// License: GPL. For details, see LICENSE file.
2717+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
2718+
2719+import static org.junit.jupiter.api.Assertions.assertEquals;
2720+import static org.junit.jupiter.api.Assertions.assertNull;
2721+
2722+import java.awt.image.BufferedImage;
2723+import java.util.Collections;
2724+import java.util.stream.Stream;
2725+
2726+import org.openstreetmap.gui.jmapviewer.Tile;
2727+import org.openstreetmap.gui.jmapviewer.interfaces.TileJob;
2728+import org.openstreetmap.josm.TestUtils;
2729+import org.openstreetmap.josm.data.cache.JCSCacheManager;
2730+import org.openstreetmap.josm.data.imagery.ImageryInfo;
2731+import org.openstreetmap.josm.data.imagery.TileJobOptions;
2732+import org.openstreetmap.josm.testutils.JOSMTestRules;
2733+
2734+import org.awaitility.Awaitility;
2735+import org.awaitility.Durations;
2736+import org.junit.jupiter.api.BeforeEach;
2737+import org.junit.jupiter.api.extension.RegisterExtension;
2738+import org.junit.jupiter.params.ParameterizedTest;
2739+import org.junit.jupiter.params.provider.Arguments;
2740+import org.junit.jupiter.params.provider.MethodSource;
2741+
2742+/**
2743+ * Test class for {@link MVTTile}
2744+ */
2745+public class MVTTileTest {
2746+ private MapboxVectorTileSource tileSource;
2747+ private MapBoxVectorCachedTileLoader loader;
2748+ @RegisterExtension
2749+ JOSMTestRules rule = new JOSMTestRules();
2750+ @BeforeEach
2751+ void setup() {
2752+ tileSource = new MapboxVectorTileSource(new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot()
2753+ + "pbf/mapillary/{z}/{x}/{y}.mvt"));
2754+ loader = new MapBoxVectorCachedTileLoader(null,
2755+ JCSCacheManager.getCache("testMapillaryCache"), new TileJobOptions(1, 1, Collections
2756+ .emptyMap(), 3600));
2757+ }
2758+
2759+ /**
2760+ * Provide arguments for {@link #testMVTTile(BufferedImage, Boolean)}
2761+ * @return The arguments to use
2762+ */
2763+ private static Stream<Arguments> testMVTTile() {
2764+ return Stream.of(
2765+ Arguments.of(null, Boolean.TRUE),
2766+ Arguments.of(Tile.LOADING_IMAGE, Boolean.TRUE),
2767+ Arguments.of(Tile.ERROR_IMAGE, Boolean.TRUE),
2768+ Arguments.of(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), Boolean.FALSE)
2769+ );
2770+ }
2771+
2772+ @ParameterizedTest
2773+ @MethodSource("testMVTTile")
2774+ void testMVTTile(BufferedImage image, Boolean isLoaded) {
2775+ MVTTile tile = new MVTTile(tileSource, 3249, 6258, 14);
2776+ tile.setImage(image);
2777+ assertEquals(image, tile.getImage());
2778+
2779+ TileJob job = loader.createTileLoaderJob(tile);
2780+ job.submit();
2781+ Awaitility.await().atMost(Durations.ONE_SECOND).until(tile::isLoaded);
2782+ if (isLoaded) {
2783+ Awaitility.await().atMost(Durations.ONE_SECOND).until(() -> tile.getLayers() != null && tile.getLayers().size() > 1);
2784+ assertEquals(2, tile.getLayers().size());
2785+ // The test Mapillary tiles have 2048 instead of 4096 for their extent. This *may* change
2786+ // in future Mapillary tiles, so if the test PBF files are updated, beware.
2787+ assertEquals(2048, tile.getExtent());
2788+ // Ensure that we have the clear image set, such that the tile doesn't add to the dataset again
2789+ // and we don't have a loading image
2790+ assertEquals(MVTTile.CLEAR_LOADED, tile.getImage());
2791+ } else {
2792+ assertNull(tile.getLayers());
2793+ assertEquals(image, tile.getImage());
2794+ }
2795+ }
2796+
2797+}
2798diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java
2799new file mode 100644
2800index 000000000..5b9f16842
2801--- /dev/null
2802+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java
2803@@ -0,0 +1,77 @@
2804+// License: GPL. For details, see LICENSE file.
2805+package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
2806+
2807+import static org.junit.jupiter.api.Assertions.assertEquals;
2808+import static org.junit.jupiter.api.Assertions.assertNotNull;
2809+import static org.junit.jupiter.api.Assertions.assertNull;
2810+
2811+
2812+import java.util.stream.Stream;
2813+
2814+import org.junit.jupiter.api.extension.RegisterExtension;
2815+import org.openstreetmap.josm.TestUtils;
2816+import org.openstreetmap.josm.data.imagery.ImageryInfo;
2817+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapBoxVectorStyle;
2818+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.Source;
2819+import org.openstreetmap.josm.gui.ExtendedDialog;
2820+import org.openstreetmap.josm.gui.widgets.JosmComboBox;
2821+import org.openstreetmap.josm.testutils.JOSMTestRules;
2822+import org.openstreetmap.josm.testutils.mockers.ExtendedDialogMocker;
2823+
2824+import org.junit.jupiter.api.Test;
2825+import org.junit.jupiter.params.ParameterizedTest;
2826+import org.junit.jupiter.params.provider.Arguments;
2827+import org.junit.jupiter.params.provider.MethodSource;
2828+
2829+/**
2830+ * Test class for {@link MapboxVectorTileSource}
2831+ * @author Taylor Smock
2832+ * @since xxx
2833+ */
2834+class MapboxVectorTileSourceTest {
2835+ @RegisterExtension
2836+ JOSMTestRules rule = new JOSMTestRules();
2837+ private static class SelectLayerDialogMocker extends ExtendedDialogMocker {
2838+ int index;
2839+ @Override
2840+ protected void act(final ExtendedDialog instance) {
2841+ ((JosmComboBox<?>) this.getContent(instance)).setSelectedIndex(index);
2842+ }
2843+
2844+ @Override
2845+ protected String getString(final ExtendedDialog instance) {
2846+ return String.join(";", ((Source) ((JosmComboBox<?>) this.getContent(instance)).getSelectedItem()).getUrls());
2847+ }
2848+ }
2849+
2850+ @Test
2851+ void testNoStyle() {
2852+ MapboxVectorTileSource tileSource = new MapboxVectorTileSource(
2853+ new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot() + "pbf/mapillary/{z}/{x}/{y}.mvt"));
2854+ assertNull(tileSource.getStyleSource());
2855+ }
2856+
2857+ private static Stream<Arguments> testMapillaryStyle() {
2858+ return Stream.of(Arguments.of(0, "Test Mapillary: mapillary-source", "https://tiles3.mapillary.com/v0.1/{z}/{x}/{y}.mvt"),
2859+ Arguments.of(1, "Test Mapillary: mapillary-features-source",
2860+ "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_"
2861+ + "&layers=points&per_page=1000"),
2862+ Arguments.of(2, "Test Mapillary: mapillary-traffic-signs-source",
2863+ "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_"
2864+ + "&layers=trafficsigns&per_page=1000"));
2865+ }
2866+
2867+ @ParameterizedTest
2868+ @MethodSource("testMapillaryStyle")
2869+ void testMapillaryStyle(Integer index, String expected, String dialogMockerText) {
2870+ TestUtils.assumeWorkingJMockit();
2871+ SelectLayerDialogMocker extendedDialogMocker = new SelectLayerDialogMocker();
2872+ extendedDialogMocker.index = index;
2873+ extendedDialogMocker.getMockResultMap().put(dialogMockerText, "Add layers");
2874+ MapboxVectorTileSource tileSource = new MapboxVectorTileSource(
2875+ new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot() + "mapillary.json"));
2876+ MapBoxVectorStyle styleSource = tileSource.getStyleSource();
2877+ assertNotNull(styleSource);
2878+ assertEquals(expected, tileSource.toString());
2879+ }
2880+}
2881--
2882GitLab
2883
2884
2885From 391b4caff31a6e8b87be6d4c256ac2303315e36a Mon Sep 17 00:00:00 2001
2886From: Taylor Smock <tsmock@fb.com>
2887Date: Mon, 19 Apr 2021 16:09:36 -0600
2888Subject: [PATCH 03/50] Layer: FIXUP: extent is uint not sint
2889
2890Signed-off-by: Taylor Smock <tsmock@fb.com>
2891---
2892 .../josm/data/imagery/vectortile/mapbox/Layer.java | 2 +-
2893 1 file changed, 1 insertion(+), 1 deletion(-)
2894
2895diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
2896index 09851e8c7..1c496d55d 100644
2897--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
2898+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
2899@@ -107,7 +107,7 @@ public final class Layer {
2900 }
2901 this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst()
2902 .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name")));
2903- this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asSignedVarInt)
2904+ this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asUnsignedVarInt)
2905 .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT);
2906
2907 sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString)
2908--
2909GitLab
2910
2911
2912From cd40f9014f178fd97f2679656cc328e73998f26f Mon Sep 17 00:00:00 2001
2913From: Taylor Smock <tsmock@fb.com>
2914Date: Thu, 8 Apr 2021 14:25:43 -0600
2915Subject: [PATCH 04/50] Initial Mapbox Vector Style implementation
2916
2917Signed-off-by: Taylor Smock <tsmock@fb.com>
2918---
2919 .../vectortile/mapbox/style/Expression.java | 99 +++
2920 .../vectortile/mapbox/style/Layers.java | 519 +++++++++++++++
2921 .../mapbox/style/MapBoxVectorStyle.java | 266 ++++++++
2922 .../vectortile/mapbox/style/Scheme.java | 12 +
2923 .../vectortile/mapbox/style/Source.java | 254 ++++++++
2924 .../vectortile/mapbox/style/SourceType.java | 17 +
2925 .../josm/data/osm/IPrimitive.java | 9 +
2926 .../josm/gui/mappaint/ElemStyles.java | 118 ++--
2927 .../mapbox/style/ExpressionTest.java | 53 ++
2928 .../vectortile/mapbox/style/LayersTest.java | 601 ++++++++++++++++++
2929 .../mapbox/style/MapBoxVectorStyleTest.java | 300 +++++++++
2930 .../vectortile/mapbox/style/SourceTest.java | 188 ++++++
2931 12 files changed, 2383 insertions(+), 53 deletions(-)
2932 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
2933 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
2934 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
2935 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java
2936 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
2937 create mode 100644 src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
2938 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java
2939 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
2940 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java
2941 create mode 100644 test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java
2942
2943diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
2944new file mode 100644
2945index 000000000..a7f677755
2946--- /dev/null
2947+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
2948@@ -0,0 +1,99 @@
2949+// License: GPL. For details, see LICENSE file.
2950+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
2951+
2952+import java.util.Arrays;
2953+import java.util.Objects;
2954+import java.util.stream.Collectors;
2955+
2956+import javax.json.JsonArray;
2957+import javax.json.JsonObject;
2958+import javax.json.JsonString;
2959+import javax.json.JsonValue;
2960+
2961+/**
2962+ * A MapBox vector style expression (immutable)
2963+ * @author Taylor Smock
2964+ * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/">https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/</a>
2965+ * @since xxx
2966+ */
2967+public final class Expression {
2968+ /** An empty expression to use */
2969+ public static final Expression EMPTY_EXPRESSION = new Expression(JsonValue.NULL);
2970+ private static final String EMPTY_STRING = "";
2971+
2972+ private final String mapcssFilterExpression;
2973+
2974+ /**
2975+ * Create a new filter expression. <i>Please note that this currently only supports basic comparators!</i>
2976+ * @param value The value to parse
2977+ */
2978+ public Expression(JsonValue value) {
2979+ if (value.getValueType() == JsonValue.ValueType.ARRAY) {
2980+ final JsonArray array = value.asJsonArray();
2981+ if (!array.isEmpty() && array.get(0).getValueType() == JsonValue.ValueType.STRING) {
2982+ if ("==".equals(array.getString(0))) {
2983+ // The mapcss equivalent of == is = (for the most part)
2984+ this.mapcssFilterExpression = convertToString(array.get(1)) + "=" + convertToString(array.get(2));
2985+ } else if (Arrays.asList("<=", ">=", ">", "<", "!=").contains(array.getString(0))) {
2986+ this.mapcssFilterExpression = convertToString(array.get(1)) + array.getString(0) + convertToString(array.get(2));
2987+ } else {
2988+ this.mapcssFilterExpression = EMPTY_STRING;
2989+ }
2990+ } else {
2991+ this.mapcssFilterExpression = EMPTY_STRING;
2992+ }
2993+ } else {
2994+ this.mapcssFilterExpression = EMPTY_STRING;
2995+ }
2996+ }
2997+
2998+ /**
2999+ * Convert a value to a string
3000+ * @param value The value to convert
3001+ * @return A string
3002+ */
3003+ private static String convertToString(JsonValue value) {
3004+ switch (value.getValueType()) {
3005+ case STRING:
3006+ return ((JsonString) value).getString();
3007+ case FALSE:
3008+ return Boolean.FALSE.toString();
3009+ case TRUE:
3010+ return Boolean.TRUE.toString();
3011+ case NUMBER:
3012+ return value.toString();
3013+ case ARRAY:
3014+ return '['
3015+ + ((JsonArray) value).stream().map(Expression::convertToString).collect(Collectors.joining(","))
3016+ + ']';
3017+ case OBJECT:
3018+ return '{'
3019+ + ((JsonObject) value).entrySet().stream()
3020+ .map(entry -> entry.getKey() + ":" + convertToString(entry.getValue())).collect(
3021+ Collectors.joining(","))
3022+ + '}';
3023+ case NULL:
3024+ default:
3025+ return EMPTY_STRING;
3026+ }
3027+ }
3028+
3029+ @Override
3030+ public String toString() {
3031+ return !EMPTY_STRING.equals(this.mapcssFilterExpression) ? '[' + this.mapcssFilterExpression + ']' : EMPTY_STRING;
3032+ }
3033+
3034+ @Override
3035+ public boolean equals(Object other) {
3036+ if (other instanceof Expression) {
3037+ Expression o = (Expression) other;
3038+ return Objects.equals(this.mapcssFilterExpression, o.mapcssFilterExpression);
3039+ }
3040+ return false;
3041+ }
3042+
3043+ @Override
3044+ public int hashCode() {
3045+ return Objects.hash(this.mapcssFilterExpression);
3046+ }
3047+}
3048diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
3049new file mode 100644
3050index 000000000..9488c3d19
3051--- /dev/null
3052+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
3053@@ -0,0 +1,519 @@
3054+// License: GPL. For details, see LICENSE file.
3055+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
3056+
3057+import java.awt.Font;
3058+import java.awt.GraphicsEnvironment;
3059+import java.text.MessageFormat;
3060+import java.util.Arrays;
3061+import java.util.Collection;
3062+import java.util.List;
3063+import java.util.Locale;
3064+import java.util.Objects;
3065+import java.util.regex.Matcher;
3066+import java.util.regex.Pattern;
3067+import java.util.stream.Collectors;
3068+import java.util.stream.Stream;
3069+
3070+import javax.json.JsonArray;
3071+import javax.json.JsonNumber;
3072+import javax.json.JsonObject;
3073+import javax.json.JsonString;
3074+import javax.json.JsonValue;
3075+
3076+/**
3077+ * MapBox style layers
3078+ * @author Taylor Smock
3079+ * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/">https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/</a>
3080+ * @since xxx
3081+ */
3082+public class Layers {
3083+ /**
3084+ * The layer type. This affects the rendering.
3085+ * @author Taylor Smock
3086+ * @since xxx
3087+ */
3088+ enum Type {
3089+ /** Filled polygon with an (optional) border */
3090+ FILL,
3091+ /** A line */
3092+ LINE,
3093+ /** A symbol */
3094+ SYMBOL,
3095+ /** A circle */
3096+ CIRCLE,
3097+ /** A heatmap */
3098+ HEATMAP,
3099+ /** A 3D polygon extrusion */
3100+ FILL_EXTRUSION,
3101+ /** Raster */
3102+ RASTER,
3103+ /** Hillshade data */
3104+ HILLSHADE,
3105+ /** A background color or pattern */
3106+ BACKGROUND,
3107+ /** The fallback layer */
3108+ SKY
3109+ }
3110+
3111+ private static final String EMPTY_STRING = "";
3112+ private static final char SEMI_COLON = ';';
3113+ private static final Pattern CURLY_BRACES = Pattern.compile("(\\{(.*?)})");
3114+
3115+ /** A required unique layer name */
3116+ private final String id;
3117+ /** The required type */
3118+ private final Type type;
3119+ /** An optional expression */
3120+ private final Expression filter;
3121+ /** The max zoom for the layer */
3122+ private final int maxZoom;
3123+ /** The min zoom for the layer */
3124+ private final int minZoom;
3125+
3126+ /** Default paint properties for this layer */
3127+ private final String paint;
3128+
3129+ /** A source description to be used with this layer. Required for everything <i>but</i> {@link Type#BACKGROUND} */
3130+ private final String source;
3131+ /** Layer to use from the vector tile source. Only allowed with {@link SourceType#VECTOR}. */
3132+ private final String sourceLayer;
3133+ /** The id for the style -- used for image paths */
3134+ private final String styleId;
3135+ /**
3136+ * Create a layer object
3137+ * @param layerInfo The info to use to create the layer
3138+ */
3139+ public Layers(final JsonObject layerInfo) {
3140+ this (null, layerInfo);
3141+ }
3142+
3143+ /**
3144+ * Create a layer object
3145+ * @param styleId The id for the style (image paths require this)
3146+ * @param layerInfo The info to use to create the layer
3147+ */
3148+ public Layers(final String styleId, final JsonObject layerInfo) {
3149+ this.id = layerInfo.getString("id");
3150+ this.styleId = styleId;
3151+ this.type = Type.valueOf(layerInfo.getString("type").replace("-", "_").toUpperCase(Locale.ROOT));
3152+ if (layerInfo.containsKey("filter")) {
3153+ this.filter = new Expression(layerInfo.get("filter"));
3154+ } else {
3155+ this.filter = Expression.EMPTY_EXPRESSION;
3156+ }
3157+ this.maxZoom = layerInfo.getInt("maxzoom", Integer.MAX_VALUE);
3158+ this.minZoom = layerInfo.getInt("minzoom", Integer.MIN_VALUE);
3159+ // There is a metadata field (I don't *think* I need it?)
3160+ // source is only optional with {@link Type#BACKGROUND}.
3161+ if (this.type == Type.BACKGROUND) {
3162+ this.source = layerInfo.getString("source", null);
3163+ } else {
3164+ this.source = layerInfo.getString("source");
3165+ }
3166+ if (layerInfo.containsKey("paint") && layerInfo.get("paint").getValueType() == JsonValue.ValueType.OBJECT) {
3167+ final JsonObject paintObject = layerInfo.getJsonObject("paint");
3168+ final JsonObject layoutObject = layerInfo.getOrDefault("layout", JsonValue.EMPTY_JSON_OBJECT).asJsonObject();
3169+ // Don't throw exceptions here, since we may just point at the styling
3170+ if ("visible".equalsIgnoreCase(layoutObject.getString("visibility", "visible"))) {
3171+ switch (type) {
3172+ case FILL:
3173+ // area
3174+ this.paint = parsePaintFill(paintObject);
3175+ break;
3176+ case LINE:
3177+ // way
3178+ this.paint = parsePaintLine(layoutObject, paintObject);
3179+ break;
3180+ case CIRCLE:
3181+ // point
3182+ this.paint = parsePaintCircle(paintObject);
3183+ break;
3184+ case SYMBOL:
3185+ // point
3186+ this.paint = parsePaintSymbol(layoutObject, paintObject);
3187+ break;
3188+ case BACKGROUND:
3189+ // canvas only
3190+ this.paint = parsePaintBackground(paintObject);
3191+ break;
3192+ default:
3193+ this.paint = EMPTY_STRING;
3194+ }
3195+ } else {
3196+ this.paint = EMPTY_STRING;
3197+ }
3198+ } else {
3199+ this.paint = EMPTY_STRING;
3200+ }
3201+ this.sourceLayer = layerInfo.getString("source-layer", null);
3202+ }
3203+
3204+ /**
3205+ * Get the filter for this layer
3206+ * @return The filter
3207+ */
3208+ public Expression getFilter() {
3209+ return this.filter;
3210+ }
3211+
3212+ /**
3213+ * Get the unique id for this layer
3214+ * @return The unique id
3215+ */
3216+ public String getId() {
3217+ return this.id;
3218+ }
3219+
3220+ /**
3221+ * Get the type of this layer
3222+ * @return The layer type
3223+ */
3224+ public Type getType() {
3225+ return this.type;
3226+ }
3227+
3228+ private static String parsePaintLine(final JsonObject layoutObject, final JsonObject paintObject) {
3229+ final StringBuilder sb = new StringBuilder(36);
3230+ // line-blur, default 0 (px)
3231+ // line-color, default #000000, disabled by line-pattern
3232+ final String color = paintObject.getString("line-color", "#000000");
3233+ sb.append("color:").append(color).append(SEMI_COLON);
3234+ // line-opacity, default 1 (0-1)
3235+ final JsonNumber opacity = paintObject.getJsonNumber("line-opacity");
3236+ if (opacity != null) {
3237+ sb.append("opacity:").append(opacity.numberValue().doubleValue()).append(SEMI_COLON);
3238+ }
3239+ // line-cap, default butt (butt|round|square)
3240+ final String cap = layoutObject.getString("line-cap", "butt");
3241+ sb.append("linecap:");
3242+ switch (cap) {
3243+ case "round":
3244+ case "square":
3245+ sb.append(cap);
3246+ break;
3247+ case "butt":
3248+ default:
3249+ sb.append("none");
3250+ }
3251+
3252+ sb.append(SEMI_COLON);
3253+ // line-dasharray, array of number >= 0, units in line widths, disabled by line-pattern
3254+ if (paintObject.containsKey("line-dasharray")) {
3255+ final JsonArray dashArray = paintObject.getJsonArray("line-dasharray");
3256+ sb.append("dashes:");
3257+ sb.append(dashArray.stream().filter(JsonNumber.class::isInstance).map(JsonNumber.class::cast)
3258+ .map(JsonNumber::toString).collect(Collectors.joining(",")));
3259+ sb.append(SEMI_COLON);
3260+ }
3261+ // line-gap-width
3262+ // line-gradient
3263+ // line-join
3264+ // line-miter-limit
3265+ // line-offset
3266+ // line-pattern TODO this first, since it disables stuff
3267+ // line-round-limit
3268+ // line-sort-key
3269+ // line-translate
3270+ // line-translate-anchor
3271+ // line-width
3272+ final JsonNumber width = paintObject.getJsonNumber("line-width");
3273+ sb.append("width:").append(width == null ? 1 : width.toString()).append(SEMI_COLON);
3274+ return sb.toString();
3275+ }
3276+
3277+ private static String parsePaintCircle(final JsonObject paintObject) {
3278+ final StringBuilder sb = new StringBuilder(150).append("symbol-shape:circle;")
3279+ // circle-blur
3280+ // circle-color
3281+ .append("symbol-fill-color:").append(paintObject.getString("circle-color", "#000000")).append(SEMI_COLON);
3282+ // circle-opacity
3283+ final JsonNumber fillOpacity = paintObject.getJsonNumber("circle-opacity");
3284+ sb.append("symbol-fill-opacity:").append(fillOpacity != null ? fillOpacity.numberValue().toString() : "1").append(SEMI_COLON);
3285+ // circle-pitch-alignment // not 3D
3286+ // circle-pitch-scale // not 3D
3287+ // circle-radius
3288+ final JsonNumber radius = paintObject.getJsonNumber("circle-radius");
3289+ sb.append("symbol-size:").append(radius != null ? (2 * radius.numberValue().doubleValue()) : "10").append(SEMI_COLON)
3290+ // circle-sort-key
3291+ // circle-stroke-color
3292+ .append("symbol-stroke-color:").append(paintObject.getString("circle-stroke-color", "#000000")).append(SEMI_COLON);
3293+ // circle-stroke-opacity
3294+ final JsonNumber strokeOpacity = paintObject.getJsonNumber("circle-stroke-opacity");
3295+ sb.append("symbol-stroke-opacity:").append(strokeOpacity != null ? strokeOpacity.numberValue().toString() : "1").append(SEMI_COLON);
3296+ // circle-stroke-width
3297+ final JsonNumber strokeWidth = paintObject.getJsonNumber("circle-stroke-width");
3298+ sb.append("symbol-stroke-width:").append(strokeWidth != null ? strokeWidth.numberValue().toString() : "0").append(SEMI_COLON);
3299+ // circle-translate
3300+ // circle-translate-anchor
3301+ return sb.toString();
3302+ }
3303+
3304+ private String parsePaintSymbol(
3305+ final JsonObject layoutObject,
3306+ final JsonObject paintObject) {
3307+ final StringBuilder sb = new StringBuilder();
3308+ // icon-allow-overlap
3309+ // icon-anchor
3310+ // icon-color
3311+ // icon-halo-blur
3312+ // icon-halo-color
3313+ // icon-halo-width
3314+ // icon-ignore-placement
3315+ // icon-image
3316+ boolean iconImage = false;
3317+ if (layoutObject.containsKey("icon-image")) {
3318+ sb.append("icon-image:concat(");
3319+ if (this.styleId != null && !this.styleId.trim().isEmpty()) {
3320+ sb.append('"').append(this.styleId).append('/').append("\",");
3321+ }
3322+ Matcher matcher = CURLY_BRACES.matcher(layoutObject.getString("icon-image"));
3323+ StringBuffer stringBuffer = new StringBuffer();
3324+ int previousMatch;
3325+ if (matcher.lookingAt()) {
3326+ matcher.appendReplacement(stringBuffer, "tag(\"$2\"),\"");
3327+ previousMatch = matcher.end();
3328+ } else {
3329+ previousMatch = 0;
3330+ stringBuffer.append('"');
3331+ }
3332+ while (matcher.find()) {
3333+ if (matcher.start() == previousMatch) {
3334+ matcher.appendReplacement(stringBuffer, ",tag(\"$2\")");
3335+ } else {
3336+ matcher.appendReplacement(stringBuffer, "\",tag(\"$2\"),\"");
3337+ }
3338+ previousMatch = matcher.end();
3339+ }
3340+ if (matcher.hitEnd() && stringBuffer.toString().endsWith(",\"")) {
3341+ stringBuffer.delete(stringBuffer.length() - ",\"".length(), stringBuffer.length());
3342+ } else if (!matcher.hitEnd()) {
3343+ stringBuffer.append('"');
3344+ }
3345+ StringBuffer tail = new StringBuffer();
3346+ matcher.appendTail(tail);
3347+ if (tail.length() > 0) {
3348+ String current = stringBuffer.toString();
3349+ if (!"\"".equals(current) && !current.endsWith(",\"")) {
3350+ stringBuffer.append(",\"");
3351+ }
3352+ stringBuffer.append(tail);
3353+ stringBuffer.append('"');
3354+ }
3355+
3356+ sb.append(stringBuffer).append(')').append(SEMI_COLON);
3357+ iconImage = true;
3358+ }
3359+ // icon-keep-upright
3360+ // icon-offset
3361+ if (iconImage && layoutObject.containsKey("icon-offset")) {
3362+ // default [0, 0], right,down == positive, left,up == negative
3363+ final List<JsonNumber> offset = layoutObject.getJsonArray("icon-offset").getValuesAs(JsonNumber.class);
3364+ // Assume that the offset must be size 2. Probably not necessary, but docs aren't necessary clear.
3365+ if (offset.size() == 2) {
3366+ sb.append("icon-offset-x:").append(offset.get(0).doubleValue()).append(SEMI_COLON)
3367+ .append("icon-offset-y:").append(offset.get(1).doubleValue()).append(SEMI_COLON);
3368+ }
3369+ }
3370+ // icon-opacity
3371+ if (iconImage && paintObject.containsKey("icon-opacity")) {
3372+ final double opacity = paintObject.getJsonNumber("icon-opacity").doubleValue();
3373+ sb.append("icon-opacity:").append(opacity).append(SEMI_COLON);
3374+ }
3375+ // icon-optional
3376+ // icon-padding
3377+ // icon-pitch-alignment
3378+ // icon-rotate
3379+ if (iconImage && layoutObject.containsKey("icon-rotate")) {
3380+ final double rotation = layoutObject.getJsonNumber("icon-rotate").doubleValue();
3381+ sb.append("icon-rotation:").append(rotation).append(SEMI_COLON);
3382+ }
3383+ // icon-rotation-alignment
3384+ // icon-size
3385+ // icon-text-fit
3386+ // icon-text-fit-padding
3387+ // icon-translate
3388+ // icon-translate-anchor
3389+ // symbol-avoid-edges
3390+ // symbol-placement
3391+ // symbol-sort-key
3392+ // symbol-spacing
3393+ // symbol-z-order
3394+ // text-allow-overlap
3395+ // text-anchor
3396+ // text-color
3397+ if (paintObject.containsKey("text-color")) {
3398+ sb.append("text-color:").append(paintObject.getString("text-color")).append(SEMI_COLON);
3399+ }
3400+ // text-field
3401+ if (layoutObject.containsKey("text-field")) {
3402+ sb.append("text:")
3403+ .append(layoutObject.getString("text-field").replace("}", EMPTY_STRING).replace("{", EMPTY_STRING))
3404+ .append(SEMI_COLON);
3405+ }
3406+ // text-font
3407+ if (layoutObject.containsKey("text-font")) {
3408+ List<String> fonts = layoutObject.getJsonArray("text-font").stream().filter(JsonString.class::isInstance)
3409+ .map(JsonString.class::cast).map(JsonString::getString).collect(Collectors.toList());
3410+ Font[] systemFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts();
3411+ for (String fontString : fonts) {
3412+ Collection<Font> fontMatches = Stream.of(systemFonts)
3413+ .filter(font -> Arrays.asList(font.getName(), font.getFontName(), font.getFamily(), font.getPSName()).contains(fontString))
3414+ .collect(Collectors.toList());
3415+ if (!fontMatches.isEmpty()) {
3416+ final Font setFont = fontMatches.stream().filter(font -> font.getName().equals(fontString)).findAny()
3417+ .orElseGet(() -> fontMatches.stream().filter(font -> font.getFontName().equals(fontString)).findAny()
3418+ .orElseGet(() -> fontMatches.stream().filter(font -> font.getPSName().equals(fontString)).findAny()
3419+ .orElseGet(() -> fontMatches.stream().filter(font -> font.getFamily().equals(fontString)).findAny().orElse(null))));
3420+ if (setFont != null) {
3421+ sb.append("font-family:\"").append(setFont.getFamily()).append('"').append(SEMI_COLON);
3422+ sb.append("font-weight:").append(setFont.isBold() ? "bold" : "normal").append(SEMI_COLON);
3423+ sb.append("font-style:").append(setFont.isItalic() ? "italic" : "normal").append(SEMI_COLON);
3424+ break;
3425+ }
3426+ }
3427+ }
3428+ }
3429+ // text-halo-blur
3430+ // text-halo-color
3431+ if (paintObject.containsKey("text-halo-color")) {
3432+ sb.append("text-halo-color:").append(paintObject.getString("text-halo-color")).append(SEMI_COLON);
3433+ }
3434+ // text-halo-width
3435+ if (paintObject.containsKey("text-halo-width")) {
3436+ sb.append("text-halo-radius:").append(paintObject.getJsonNumber("text-halo-width").intValue()).append(SEMI_COLON);
3437+ }
3438+ // text-ignore-placement
3439+ // text-justify
3440+ // text-keep-upright
3441+ // text-letter-spacing
3442+ // text-line-height
3443+ // text-max-angle
3444+ // text-max-width
3445+ // text-offset
3446+ // text-opacity
3447+ if (paintObject.containsKey("text-opacity")) {
3448+ sb.append("text-opacity:").append(paintObject.getJsonNumber("text-opacity").doubleValue()).append(SEMI_COLON);
3449+ }
3450+ // text-optional
3451+ // text-padding
3452+ // text-pitch-alignment
3453+ // text-radial-offset
3454+ // text-rotate
3455+ // text-rotation-alignment
3456+ // text-size
3457+ final JsonNumber textSize = layoutObject.getJsonNumber("text-size");
3458+ sb.append("font-size:").append(textSize != null ? textSize.numberValue().toString() : "16").append(SEMI_COLON);
3459+ // text-transform
3460+ // text-translate
3461+ // text-translate-anchor
3462+ // text-variable-anchor
3463+ // text-writing-mode
3464+ return sb.toString();
3465+ }
3466+
3467+ private static String parsePaintBackground(final JsonObject paintObject) {
3468+ final StringBuilder sb = new StringBuilder(20);
3469+ // background-color
3470+ final String bgColor = paintObject.getString("background-color", null);
3471+ if (bgColor != null) {
3472+ sb.append("fill-color:").append(bgColor).append(SEMI_COLON);
3473+ }
3474+ // background-opacity
3475+ // background-pattern
3476+ return sb.toString();
3477+ }
3478+
3479+ private static String parsePaintFill(final JsonObject paintObject) {
3480+ StringBuilder sb = new StringBuilder(50)
3481+ // fill-antialias
3482+ // fill-color
3483+ .append("fill-color:").append(paintObject.getString("fill-color", "#000000")).append(SEMI_COLON);
3484+ // fill-opacity
3485+ final JsonNumber opacity = paintObject.getJsonNumber("fill-opacity");
3486+ sb.append("fill-opacity:").append(opacity != null ? opacity.numberValue().toString() : "1").append(SEMI_COLON)
3487+ // fill-outline-color
3488+ .append("color:").append(paintObject.getString("fill-outline-color",
3489+ paintObject.getString("fill-color", "#000000"))).append(SEMI_COLON);
3490+ // fill-pattern
3491+ // fill-sort-key
3492+ // fill-translate
3493+ // fill-translate-anchor
3494+ return sb.toString();
3495+ }
3496+
3497+ /**
3498+ * Converts this layer object to a mapcss entry string (to be parsed later)
3499+ * @return The mapcss entry (string form)
3500+ */
3501+ @Override
3502+ public String toString() {
3503+ if (this.filter.toString().isEmpty() && this.paint.isEmpty()) {
3504+ return EMPTY_STRING;
3505+ } else if (this.type == Type.BACKGROUND) {
3506+ // AFAIK, paint has no zoom levels, and doesn't accept a layer
3507+ return "canvas{" + this.paint + "}";
3508+ }
3509+
3510+ final String zoomSelector;
3511+ if (this.minZoom == this.maxZoom) {
3512+ zoomSelector = "|z" + this.minZoom;
3513+ } else if (this.minZoom > Integer.MIN_VALUE && this.maxZoom == Integer.MAX_VALUE) {
3514+ zoomSelector = "|z" + this.minZoom + "-";
3515+ } else if (this.minZoom == Integer.MIN_VALUE && this.maxZoom < Integer.MAX_VALUE) {
3516+ zoomSelector = "|z-" + this.maxZoom;
3517+ } else if (this.minZoom > Integer.MIN_VALUE) {
3518+ zoomSelector = MessageFormat.format("|z{0}-{1}", this.minZoom, this.maxZoom);
3519+ } else {
3520+ zoomSelector = EMPTY_STRING;
3521+ }
3522+ final String commonData = zoomSelector + this.filter.toString() + "::" + this.id + "{" + this.paint + "}";
3523+
3524+ if (this.type == Type.CIRCLE || this.type == Type.SYMBOL) {
3525+ return "node" + commonData;
3526+ } else if (this.type == Type.FILL) {
3527+ return "area" + commonData;
3528+ } else if (this.type == Type.LINE) {
3529+ return "way" + commonData;
3530+ }
3531+ return super.toString();
3532+ }
3533+
3534+ /**
3535+ * Get the source that this applies to
3536+ * @return The source name
3537+ */
3538+ public String getSource() {
3539+ return this.source;
3540+ }
3541+
3542+ /**
3543+ * Get the layer that this applies to
3544+ * @return The layer name
3545+ */
3546+ public String getSourceLayer() {
3547+ return this.sourceLayer;
3548+ }
3549+
3550+ @Override
3551+ public boolean equals(Object other) {
3552+ if (other != null && this.getClass() == other.getClass()) {
3553+ Layers o = (Layers) other;
3554+ return this.type == o.type
3555+ && this.minZoom == o.minZoom
3556+ && this.maxZoom == o.maxZoom
3557+ && Objects.equals(this.id, o.id)
3558+ && Objects.equals(this.styleId, o.styleId)
3559+ && Objects.equals(this.sourceLayer, o.sourceLayer)
3560+ && Objects.equals(this.source, o.source)
3561+ && Objects.equals(this.filter, o.filter)
3562+ && Objects.equals(this.paint, o.paint);
3563+ }
3564+ return false;
3565+ }
3566+
3567+ @Override
3568+ public int hashCode() {
3569+ return Objects.hash(this.type, this.minZoom, this.maxZoom, this.id, this.styleId, this.sourceLayer, this.source,
3570+ this.filter, this.paint);
3571+ }
3572+}
3573diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
3574new file mode 100644
3575index 000000000..746913042
3576--- /dev/null
3577+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
3578@@ -0,0 +1,266 @@
3579+// License: GPL. For details, see LICENSE file.
3580+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
3581+
3582+import static org.openstreetmap.josm.tools.I18n.tr;
3583+
3584+import java.awt.Image;
3585+import java.awt.image.BufferedImage;
3586+import java.io.BufferedReader;
3587+import java.io.File;
3588+import java.io.IOException;
3589+import java.io.InputStream;
3590+import java.io.OutputStream;
3591+import java.nio.charset.StandardCharsets;
3592+import java.nio.file.Files;
3593+import java.util.Collections;
3594+import java.util.LinkedHashMap;
3595+import java.util.List;
3596+import java.util.Map;
3597+import java.util.Objects;
3598+import java.util.Optional;
3599+import java.util.concurrent.ConcurrentHashMap;
3600+import java.util.stream.Collectors;
3601+
3602+import javax.imageio.ImageIO;
3603+import javax.json.Json;
3604+import javax.json.JsonArray;
3605+import javax.json.JsonObject;
3606+import javax.json.JsonReader;
3607+import javax.json.JsonStructure;
3608+import javax.json.JsonValue;
3609+
3610+import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
3611+import org.openstreetmap.josm.gui.MainApplication;
3612+import org.openstreetmap.josm.gui.mappaint.ElemStyles;
3613+import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
3614+import org.openstreetmap.josm.io.CachedFile;
3615+import org.openstreetmap.josm.spi.preferences.Config;
3616+import org.openstreetmap.josm.tools.Logging;
3617+
3618+/**
3619+ * Create a mapping for a Mapbox Vector Style
3620+ *
3621+ * @author Taylor Smock
3622+ * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/">https://docs.mapbox.com/mapbox-gl-js/style-spec/</a>
3623+ * @since xxx
3624+ */
3625+public class MapBoxVectorStyle {
3626+
3627+ private static final ConcurrentHashMap<String, MapBoxVectorStyle> STYLE_MAPPING = new ConcurrentHashMap<>();
3628+
3629+ /**
3630+ * Get a MapBoxVector style for a URL
3631+ * @param url The url to get
3632+ * @return The MapBox Vector Style. May be {@code null} if there was an error.
3633+ */
3634+ public static MapBoxVectorStyle getMapBoxVectorStyle(String url) {
3635+ return STYLE_MAPPING.computeIfAbsent(url, key -> {
3636+ try (CachedFile style = new CachedFile(url);
3637+ BufferedReader reader = style.getContentReader();
3638+ JsonReader jsonReader = Json.createReader(reader)) {
3639+ JsonStructure structure = jsonReader.read();
3640+ return new MapBoxVectorStyle(structure.asJsonObject());
3641+ } catch (IOException e) {
3642+ Logging.error(e);
3643+ }
3644+ // Documentation indicates that this will <i>not</i> be entered into the map, which means that this will be
3645+ // retried if something goes wrong.
3646+ return null;
3647+ });
3648+ }
3649+
3650+ /** The version for the style specification */
3651+ private final int version;
3652+ /** The optional name for the vector style */
3653+ private final String name;
3654+ /** The optional URL for sprites. This mush be absolute (so it must contain the scheme, authority, and path). */
3655+ private final String spriteUrl;
3656+ /** The optional URL for glyphs. This may have replaceable values in it. */
3657+ private final String glyphUrl;
3658+ /** The required collection of sources with a list of layers that are applicable for that source*/
3659+ private final Map<Source, ElemStyles> sources;
3660+
3661+ /**
3662+ * Create a new MapBoxVector style. You should prefer {@link #getMapBoxVectorStyle(String)}
3663+ * for deduplication purposes.
3664+ *
3665+ * @param jsonObject The object to create the style from
3666+ * @see #getMapBoxVectorStyle(String)
3667+ */
3668+ public MapBoxVectorStyle(JsonObject jsonObject) {
3669+ // There should be a version specifier. We currently only support version 8.
3670+ // This can throw an NPE when there is no version number.
3671+ this.version = jsonObject.getInt("version");
3672+ if (this.version == 8) {
3673+ this.name = jsonObject.getString("name", null);
3674+ String id = jsonObject.getString("id", this.name);
3675+ this.spriteUrl = jsonObject.getString("sprite", null);
3676+ this.glyphUrl = jsonObject.getString("glyphs", null);
3677+ final List<Source> sourceList;
3678+ if (jsonObject.containsKey("sources") && jsonObject.get("sources").getValueType() == JsonValue.ValueType.OBJECT) {
3679+ final JsonObject sourceObj = jsonObject.getJsonObject("sources");
3680+ sourceList = sourceObj.entrySet().stream().filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT)
3681+ .map(entry -> new Source(entry.getKey(), entry.getValue().asJsonObject())).collect(Collectors.toList());
3682+ } else {
3683+ sourceList = Collections.emptyList();
3684+ }
3685+ final List<Layers> layers;
3686+ if (jsonObject.containsKey("layers") && jsonObject.get("layers").getValueType() == JsonValue.ValueType.ARRAY) {
3687+ JsonArray lArray = jsonObject.getJsonArray("layers");
3688+ layers = lArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast).map(obj -> new Layers(id, obj))
3689+ .collect(Collectors.toList());
3690+ } else {
3691+ layers = Collections.emptyList();
3692+ }
3693+ final Map<Optional<Source>, List<Layers>> sourceLayer = layers.stream().collect(
3694+ Collectors.groupingBy(layer -> sourceList.stream().filter(source -> source.getName().equals(layer.getSource()))
3695+ .findFirst(), LinkedHashMap::new, Collectors.toList()));
3696+ // Abuse HashMap null (null == default)
3697+ this.sources = new LinkedHashMap<>();
3698+ for (Map.Entry<Optional<Source>, List<Layers>> entry : sourceLayer.entrySet()) {
3699+ final Source source = entry.getKey().orElse(null);
3700+ final String data = entry.getValue().stream().map(Layers::toString).collect(Collectors.joining());
3701+ final String metaData = "meta{title:" + (source == null ? "Generated Style" :
3702+ source.getName()) + ";version:\"autogenerated\";description:\"auto generated style\";}";
3703+
3704+ // This is the default canvas
3705+ final String canvas = "canvas{default-points:false;default-lines:false;}";
3706+ final MapCSSStyleSource style = new MapCSSStyleSource(metaData + canvas + data);
3707+ // Save to directory
3708+ MainApplication.worker.execute(() -> this.save((source == null ? data.hashCode() : source.getName()) + ".mapcss", style));
3709+ this.sources.put(source, new ElemStyles(Collections.singleton(style)));
3710+ }
3711+ if (this.spriteUrl != null && !this.spriteUrl.trim().isEmpty()) {
3712+ MainApplication.worker.execute(this::fetchSprites);
3713+ }
3714+ } else {
3715+ throw new IllegalArgumentException(tr("Vector Tile Style Version not understood: version {0} (json: {1})",
3716+ this.version, jsonObject));
3717+ }
3718+ }
3719+
3720+ /**
3721+ * Fetch sprites. Please note that this is (literally) only png. Unfortunately.
3722+ * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/</a>
3723+ */
3724+ private void fetchSprites() {
3725+ // HiDPI images first -- if this succeeds, don't bother with the lower resolution (JOSM has no method to switch)
3726+ try (CachedFile spriteJson = new CachedFile(this.spriteUrl + "@2x.json");
3727+ CachedFile spritePng = new CachedFile(this.spriteUrl + "@2x.png")) {
3728+ if (parseSprites(spriteJson, spritePng)) {
3729+ return;
3730+ }
3731+ }
3732+ try (CachedFile spriteJson = new CachedFile(this.spriteUrl + ".json");
3733+ CachedFile spritePng = new CachedFile(this.spriteUrl + ".png")) {
3734+ parseSprites(spriteJson, spritePng);
3735+ }
3736+ }
3737+
3738+ private boolean parseSprites(CachedFile spriteJson, CachedFile spritePng) {
3739+ /* JSON looks like this:
3740+ * { "image-name": {"width": width, "height": height, "x": x, "y": y, "pixelRatio": 1 }}
3741+ * width/height are the dimensions of the image
3742+ * x -- distance right from top left
3743+ * y -- distance down from top left
3744+ * pixelRatio -- this <i>appears</i> to be from the "@2x" (default 1)
3745+ * content -- [left, top corner, right, bottom corner]
3746+ * stretchX -- [[from, to], [from, to], ...]
3747+ * stretchY -- [[from, to], [from, to], ...]
3748+ */
3749+ final JsonObject spriteObject;
3750+ final BufferedImage spritePngImage;
3751+ try (BufferedReader spriteJsonBufferedReader = spriteJson.getContentReader();
3752+ JsonReader spriteJsonReader = Json.createReader(spriteJsonBufferedReader);
3753+ InputStream spritePngBufferedReader = spritePng.getInputStream()
3754+ ) {
3755+ spriteObject = spriteJsonReader.read().asJsonObject();
3756+ spritePngImage = ImageIO.read(spritePngBufferedReader);
3757+ } catch (IOException e) {
3758+ Logging.error(e);
3759+ return false;
3760+ }
3761+ for (Map.Entry<String, JsonValue> entry : spriteObject.entrySet()) {
3762+ final JsonObject info = entry.getValue().asJsonObject();
3763+ int width = info.getInt("width");
3764+ int height = info.getInt("height");
3765+ int x = info.getInt("x");
3766+ int y = info.getInt("y");
3767+ save(entry.getKey() + ".png", spritePngImage.getSubimage(x, y, width, height));
3768+ }
3769+ return true;
3770+ }
3771+
3772+ private void save(String name, Object object) {
3773+ final File cache;
3774+ if (object instanceof Image) {
3775+ // Images have a specific location where they are looked for
3776+ cache = new File(Config.getDirs().getUserDataDirectory(true), "images");
3777+ } else {
3778+ cache = JosmBaseDirectories.getInstance().getCacheDirectory(true);
3779+ }
3780+ final File location = new File(cache, this.name != null ? this.name : Integer.toString(this.hashCode()));
3781+ if ((!location.exists() && !location.mkdirs()) || (location.exists() && !location.isDirectory())) {
3782+ // Don't try to save if the file exists and is not a directory or we couldn't create it
3783+ return;
3784+ }
3785+ final File toSave = new File(location, name);
3786+ try (OutputStream fileOutputStream = Files.newOutputStream(toSave.toPath())) {
3787+ if (object instanceof String) {
3788+ fileOutputStream.write(((String) object).getBytes(StandardCharsets.UTF_8));
3789+ } else if (object instanceof MapCSSStyleSource) {
3790+ MapCSSStyleSource source = (MapCSSStyleSource) object;
3791+ try (InputStream inputStream = source.getSourceInputStream()) {
3792+ int byteVal = inputStream.read();
3793+ do {
3794+ fileOutputStream.write(byteVal);
3795+ byteVal = inputStream.read();
3796+ } while (byteVal > -1);
3797+ source.url = "file:/" + toSave.getAbsolutePath().replace('\\', '/');
3798+ if (source.isLoaded()) {
3799+ source.loadStyleSource();
3800+ }
3801+ }
3802+ } else if (object instanceof BufferedImage) {
3803+ // This directory is checked first when getting images
3804+ ImageIO.write((BufferedImage) object, "png", toSave);
3805+ }
3806+ } catch (IOException e) {
3807+ Logging.info(e);
3808+ }
3809+ }
3810+
3811+ /**
3812+ * Get the generated layer->style mapping
3813+ * @return The mapping (use to enable/disable a paint style)
3814+ */
3815+ public Map<Source, ElemStyles> getSources() {
3816+ return this.sources;
3817+ }
3818+
3819+ /**
3820+ * Get the sprite url for the style
3821+ * @return The base sprite url
3822+ */
3823+ public String getSpriteUrl() {
3824+ return this.spriteUrl;
3825+ }
3826+
3827+ @Override
3828+ public boolean equals(Object other) {
3829+ if (other != null && other.getClass() == this.getClass()) {
3830+ MapBoxVectorStyle o = (MapBoxVectorStyle) other;
3831+ return this.version == o.version
3832+ && Objects.equals(this.name, o.name)
3833+ && Objects.equals(this.glyphUrl, o.glyphUrl)
3834+ && Objects.equals(this.spriteUrl, o.spriteUrl)
3835+ && Objects.equals(this.sources, o.sources);
3836+ }
3837+ return false;
3838+ }
3839+
3840+ @Override
3841+ public int hashCode() {
3842+ return Objects.hash(this.name, this.version, this.glyphUrl, this.spriteUrl, this.sources);
3843+ }
3844+}
3845diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java
3846new file mode 100644
3847index 000000000..e8583b940
3848--- /dev/null
3849+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java
3850@@ -0,0 +1,12 @@
3851+// License: GPL. For details, see LICENSE file.
3852+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
3853+
3854+/**
3855+ * The scheme used for tiles
3856+ */
3857+public enum Scheme {
3858+ /** Standard slippy map scheme */
3859+ XYZ,
3860+ /** OSGeo specification scheme */
3861+ TMS
3862+}
3863diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
3864new file mode 100644
3865index 000000000..dc7c62d62
3866--- /dev/null
3867+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
3868@@ -0,0 +1,254 @@
3869+// License: GPL. For details, see LICENSE file.
3870+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
3871+
3872+import java.text.MessageFormat;
3873+import java.util.ArrayList;
3874+import java.util.Arrays;
3875+import java.util.Collection;
3876+import java.util.Collections;
3877+import java.util.List;
3878+import java.util.Locale;
3879+import java.util.Objects;
3880+import java.util.function.IntFunction;
3881+
3882+import javax.json.JsonArray;
3883+import javax.json.JsonObject;
3884+import javax.json.JsonString;
3885+import javax.json.JsonValue;
3886+
3887+import org.openstreetmap.josm.data.Bounds;
3888+
3889+/**
3890+ * A source from a MapBox Vector Style
3891+ *
3892+ * @author Taylor Smock
3893+ * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/</a>
3894+ * @since xxx
3895+ */
3896+public class Source {
3897+ /**
3898+ * A common function for zoom constraints
3899+ */
3900+ private static class ZoomBoundFunction implements IntFunction<Integer> {
3901+ private final int min;
3902+ private final int max;
3903+ /**
3904+ * Create a new bound for zooms
3905+ * @param min The min zoom
3906+ * @param max The max zoom
3907+ */
3908+ ZoomBoundFunction(int min, int max) {
3909+ this.min = min;
3910+ this.max = max;
3911+ }
3912+
3913+ @Override public Integer apply(int value) {
3914+ return Math.max(min, Math.min(value, max));
3915+ }
3916+ }
3917+
3918+ /**
3919+ * WMS servers should contain a "{bbox-epsg-3857}" parameter for the bbox
3920+ */
3921+ private static final String WMS_BBOX = "bbox-epsg-3857";
3922+
3923+ private static final String[] NO_URLS = new String[0];
3924+
3925+ /**
3926+ * Constrain the min/max zooms to be between 0 and 30, as per tilejson spec
3927+ */
3928+ private static final IntFunction<Integer> ZOOM_BOUND_FUNCTION = new ZoomBoundFunction(0, 30);
3929+
3930+ /* Common items */
3931+ /**
3932+ * The name of the source
3933+ */
3934+ private final String name;
3935+ /**
3936+ * The type of the source
3937+ */
3938+ private final SourceType sourceType;
3939+
3940+ /* Common tiled data */
3941+ /**
3942+ * The minimum zoom supported
3943+ */
3944+ private final int minZoom;
3945+ /**
3946+ * The maximum zoom supported
3947+ */
3948+ private final int maxZoom;
3949+ /**
3950+ * The tile urls. These usually have replaceable fields.
3951+ */
3952+ private final String[] tileUrls;
3953+
3954+ /* Vector and raster data */
3955+ /**
3956+ * The attribution to display for the user
3957+ */
3958+ private final String attribution;
3959+ /**
3960+ * The bounds of the data. We should not request data outside of the bounds
3961+ */
3962+ private final Bounds bounds;
3963+ /**
3964+ * The property to use as a feature id. Can be parameterized
3965+ */
3966+ private final String promoteId;
3967+ /**
3968+ * The tile scheme
3969+ */
3970+ private final Scheme scheme;
3971+ /**
3972+ * {@code true} if the tiles should not be cached
3973+ */
3974+ private final boolean volatileCache;
3975+
3976+ /* Raster data */
3977+ /**
3978+ * The tile size
3979+ */
3980+ private final int tileSize;
3981+
3982+ /**
3983+ * Create a new Source object
3984+ *
3985+ * @param name The name of the source object
3986+ * @param data The data to set the source information with
3987+ */
3988+ public Source(final String name, final JsonObject data) {
3989+ Objects.requireNonNull(name, "Name cannot be null");
3990+ Objects.requireNonNull(data, "Data cannot be null");
3991+ this.name = name;
3992+ // "type" is required (so throw an NPE if it doesn't exist)
3993+ final String type = data.getString("type");
3994+ this.sourceType = SourceType.valueOf(type.replace("-", "_").toUpperCase(Locale.ROOT));
3995+ // This can also contain SourceType.RASTER_DEM (only needs encoding)
3996+ if (SourceType.VECTOR == this.sourceType || SourceType.RASTER == this.sourceType) {
3997+ if (data.containsKey("url")) {
3998+ // TODO implement https://github.com/mapbox/tilejson-spec
3999+ throw new UnsupportedOperationException();
4000+ } else {
4001+ this.minZoom = ZOOM_BOUND_FUNCTION.apply(data.getInt("minzoom", 0));
4002+ this.maxZoom = ZOOM_BOUND_FUNCTION.apply(data.getInt("maxzoom", 22));
4003+ this.attribution = data.getString("attribution", null);
4004+ if (data.containsKey("bounds") && data.get("bounds").getValueType() == JsonValue.ValueType.ARRAY) {
4005+ final JsonArray bJsonArray = data.getJsonArray("bounds");
4006+ if (bJsonArray.size() != 4) {
4007+ throw new IllegalArgumentException(MessageFormat.format("bounds must have four values, but has {0}", bJsonArray.size()));
4008+ }
4009+ final double[] bArray = new double[bJsonArray.size()];
4010+ for (int i = 0; i < bJsonArray.size(); i++) {
4011+ bArray[i] = bJsonArray.getJsonNumber(i).doubleValue();
4012+ }
4013+ // The order in the response is
4014+ // [south-west longitude, south-west latitude, north-east longitude, north-east latitude]
4015+ this.bounds = new Bounds(bArray[1], bArray[0], bArray[3], bArray[2]);
4016+ } else {
4017+ // Don't use a static instance for bounds, as it is not a immutable class
4018+ this.bounds = new Bounds(-85.051129, -180, 85.051129, 180);
4019+ }
4020+ this.promoteId = data.getString("promoteId", null);
4021+ this.scheme = Scheme.valueOf(data.getString("scheme", "xyz").toUpperCase(Locale.ROOT));
4022+ if (data.containsKey("tiles") && data.get("tiles").getValueType() == JsonValue.ValueType.ARRAY) {
4023+ this.tileUrls = data.getJsonArray("tiles").stream().filter(JsonString.class::isInstance)
4024+ .map(JsonString.class::cast).map(JsonString::getString)
4025+ // Replace bbox-epsg-3857 with bbox (already encased with {})
4026+ .map(url -> url.replace(WMS_BBOX, "bbox")).toArray(String[]::new);
4027+ } else {
4028+ this.tileUrls = NO_URLS;
4029+ }
4030+ this.volatileCache = data.getBoolean("volatile", false);
4031+ this.tileSize = data.getInt("tileSize", 512);
4032+ }
4033+ } else {
4034+ throw new UnsupportedOperationException();
4035+ }
4036+ }
4037+
4038+ /**
4039+ * Get the bounds for this source
4040+ * @return The bounds where this source can be used
4041+ */
4042+ public Bounds getBounds() {
4043+ return this.bounds;
4044+ }
4045+
4046+ /**
4047+ * Get the source name
4048+ * @return the name
4049+ */
4050+ public String getName() {
4051+ return name;
4052+ }
4053+
4054+ /**
4055+ * Get the URLs that can be used to get vector data
4056+ *
4057+ * @return The urls
4058+ */
4059+ public List<String> getUrls() {
4060+ return Collections.unmodifiableList(Arrays.asList(this.tileUrls));
4061+ }
4062+
4063+ /**
4064+ * Get the minimum zoom
4065+ *
4066+ * @return The min zoom (default {@code 0})
4067+ */
4068+ public int getMinZoom() {
4069+ return this.minZoom;
4070+ }
4071+
4072+ /**
4073+ * Get the max zoom
4074+ *
4075+ * @return The max zoom (default {@code 22})
4076+ */
4077+ public int getMaxZoom() {
4078+ return this.maxZoom;
4079+ }
4080+
4081+ /**
4082+ * Get the attribution for this source
4083+ *
4084+ * @return The attribution text. May be {@code null}.
4085+ */
4086+ public String getAttributionText() {
4087+ return this.attribution;
4088+ }
4089+
4090+ @Override
4091+ public String toString() {
4092+ Collection<String> parts = new ArrayList<>(1 + this.getUrls().size());
4093+ parts.add(this.getName());
4094+ parts.addAll(this.getUrls());
4095+ return String.join(" ", parts);
4096+ }
4097+
4098+ @Override
4099+ public boolean equals(Object other) {
4100+ if (other != null && this.getClass() == other.getClass()) {
4101+ Source o = (Source) other;
4102+ return Objects.equals(this.name, o.name)
4103+ && this.sourceType == o.sourceType
4104+ && this.minZoom == o.minZoom
4105+ && this.maxZoom == o.maxZoom
4106+ && Objects.equals(this.attribution, o.attribution)
4107+ && Objects.equals(this.promoteId, o.promoteId)
4108+ && this.scheme == o.scheme
4109+ && this.volatileCache == o.volatileCache
4110+ && this.tileSize == o.tileSize
4111+ && Objects.equals(this.bounds, o.bounds)
4112+ && Objects.deepEquals(this.tileUrls, o.tileUrls);
4113+ }
4114+ return false;
4115+ }
4116+
4117+ @Override
4118+ public int hashCode() {
4119+ return Objects.hash(this.name, this.sourceType, this.minZoom, this.maxZoom, this.attribution, this.promoteId,
4120+ this.scheme, this.volatileCache, this.tileSize, this.bounds, Arrays.hashCode(this.tileUrls));
4121+ }
4122+}
4123diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
4124new file mode 100644
4125index 000000000..a086289d6
4126--- /dev/null
4127+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
4128@@ -0,0 +1,17 @@
4129+// License: GPL. For details, see LICENSE file.
4130+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
4131+
4132+/**
4133+ * The "source type" for the data (MapBox Vector Style specification)
4134+ *
4135+ * @author Taylor Smock
4136+ * @since xxx
4137+ */
4138+public enum SourceType {
4139+ VECTOR,
4140+ RASTER,
4141+ RASTER_DEM,
4142+ GEOJSON,
4143+ IMAGE,
4144+ VIDEO
4145+}
4146diff --git a/src/org/openstreetmap/josm/data/osm/IPrimitive.java b/src/org/openstreetmap/josm/data/osm/IPrimitive.java
4147index cdabcd1b6..34e2ca0be 100644
4148--- a/src/org/openstreetmap/josm/data/osm/IPrimitive.java
4149+++ b/src/org/openstreetmap/josm/data/osm/IPrimitive.java
4150@@ -391,6 +391,15 @@ public interface IPrimitive extends IQuadBucketType, Tagged, PrimitiveId, Stylab
4151 return getName();
4152 }
4153
4154+ /**
4155+ * Get an object to synchronize the style cache on. This <i>should</i> be a field that does not change during paint.
4156+ * By default, it returns the current object, but should be overriden to avoid some performance issues.
4157+ * @return A non-{@code null} object to synchronize on when painting
4158+ */
4159+ default Object getStyleCacheSyncObject() {
4160+ return this;
4161+ }
4162+
4163 /**
4164 * Replies the display name of a primitive formatted by <code>formatter</code>
4165 * @param formatter formatter to use
4166diff --git a/src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java b/src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java
4167index fef095ea1..970635d96 100644
4168--- a/src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java
4169+++ b/src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java
4170@@ -85,6 +85,17 @@ public class ElemStyles implements PreferenceChangedListener {
4171 Config.getPref().addPreferenceChangeListener(this);
4172 }
4173
4174+ /**
4175+ * Constructs a new {@code ElemStyles} with specific style sources. This does not listen to preference changes,
4176+ * and therefore should only be used with layers that have specific drawing requirements.
4177+ *
4178+ * @param sources The style sources (these cannot be added to, or removed from)
4179+ * @since xxx
4180+ */
4181+ public ElemStyles(Collection<StyleSource> sources) {
4182+ this.styleSources.addAll(sources);
4183+ }
4184+
4185 /**
4186 * Clear the style cache for all primitives of all DataSets.
4187 */
4188@@ -151,69 +162,71 @@ public class ElemStyles implements PreferenceChangedListener {
4189 * @since 13810 (signature)
4190 */
4191 public Pair<StyleElementList, Range> getStyleCacheWithRange(IPrimitive osm, double scale, NavigatableComponent nc) {
4192- if (!osm.isCachedStyleUpToDate() || scale <= 0) {
4193- osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE);
4194- } else {
4195- Pair<StyleElementList, Range> lst = osm.getCachedStyle().getWithRange(scale, osm.isSelected());
4196- if (lst.a != null)
4197- return lst;
4198- }
4199- Pair<StyleElementList, Range> p = getImpl(osm, scale, nc);
4200- if (osm instanceof INode && isDefaultNodes()) {
4201- if (p.a.isEmpty()) {
4202- if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
4203- p.a = DefaultStyles.DEFAULT_NODE_STYLELIST_TEXT;
4204- } else {
4205- p.a = DefaultStyles.DEFAULT_NODE_STYLELIST;
4206- }
4207+ synchronized (osm.getStyleCacheSyncObject()) {
4208+ if (!osm.isCachedStyleUpToDate() || scale <= 0) {
4209+ osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE);
4210 } else {
4211- boolean hasNonModifier = false;
4212- boolean hasText = false;
4213- for (StyleElement s : p.a) {
4214- if (s instanceof BoxTextElement) {
4215- hasText = true;
4216+ Pair<StyleElementList, Range> lst = osm.getCachedStyle().getWithRange(scale, osm.isSelected());
4217+ if (lst.a != null)
4218+ return lst;
4219+ }
4220+ Pair<StyleElementList, Range> p = getImpl(osm, scale, nc);
4221+ if (osm instanceof INode && isDefaultNodes()) {
4222+ if (p.a.isEmpty()) {
4223+ if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
4224+ p.a = DefaultStyles.DEFAULT_NODE_STYLELIST_TEXT;
4225 } else {
4226- if (!s.isModifier) {
4227- hasNonModifier = true;
4228+ p.a = DefaultStyles.DEFAULT_NODE_STYLELIST;
4229+ }
4230+ } else {
4231+ boolean hasNonModifier = false;
4232+ boolean hasText = false;
4233+ for (StyleElement s : p.a) {
4234+ if (s instanceof BoxTextElement) {
4235+ hasText = true;
4236+ } else {
4237+ if (!s.isModifier) {
4238+ hasNonModifier = true;
4239+ }
4240 }
4241 }
4242- }
4243- if (!hasNonModifier) {
4244- p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_ELEMSTYLE);
4245- if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
4246- p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_TEXT_ELEMSTYLE);
4247+ if (!hasNonModifier) {
4248+ p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_ELEMSTYLE);
4249+ if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) {
4250+ p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_TEXT_ELEMSTYLE);
4251+ }
4252 }
4253 }
4254- }
4255- } else if (osm instanceof IWay && isDefaultLines()) {
4256- boolean hasProperLineStyle = false;
4257- for (StyleElement s : p.a) {
4258- if (s.isProperLineStyle()) {
4259- hasProperLineStyle = true;
4260- break;
4261- }
4262- }
4263- if (!hasProperLineStyle) {
4264- LineElement line = LineElement.UNTAGGED_WAY;
4265- for (StyleElement element : p.a) {
4266- if (element instanceof AreaElement) {
4267- line = LineElement.createSimpleLineStyle(((AreaElement) element).color, true);
4268+ } else if (osm instanceof IWay && isDefaultLines()) {
4269+ boolean hasProperLineStyle = false;
4270+ for (StyleElement s : p.a) {
4271+ if (s.isProperLineStyle()) {
4272+ hasProperLineStyle = true;
4273 break;
4274 }
4275 }
4276- p.a = new StyleElementList(p.a, line);
4277+ if (!hasProperLineStyle) {
4278+ LineElement line = LineElement.UNTAGGED_WAY;
4279+ for (StyleElement element : p.a) {
4280+ if (element instanceof AreaElement) {
4281+ line = LineElement.createSimpleLineStyle(((AreaElement) element).color, true);
4282+ break;
4283+ }
4284+ }
4285+ p.a = new StyleElementList(p.a, line);
4286+ }
4287 }
4288+ StyleCache style = osm.getCachedStyle() != null ? osm.getCachedStyle() : StyleCache.EMPTY_STYLECACHE;
4289+ try {
4290+ osm.setCachedStyle(style.put(p.a, p.b, osm.isSelected()));
4291+ } catch (RangeViolatedError e) {
4292+ throw new AssertionError("Range violated: " + e.getMessage()
4293+ + " (object: " + osm.getPrimitiveId() + ", current style: " + osm.getCachedStyle()
4294+ + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e);
4295+ }
4296+ osm.declareCachedStyleUpToDate();
4297+ return p;
4298 }
4299- StyleCache style = osm.getCachedStyle() != null ? osm.getCachedStyle() : StyleCache.EMPTY_STYLECACHE;
4300- try {
4301- osm.setCachedStyle(style.put(p.a, p.b, osm.isSelected()));
4302- } catch (RangeViolatedError e) {
4303- throw new AssertionError("Range violated: " + e.getMessage()
4304- + " (object: " + osm.getPrimitiveId() + ", current style: "+osm.getCachedStyle()
4305- + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e);
4306- }
4307- osm.declareCachedStyleUpToDate();
4308- return p;
4309 }
4310
4311 /**
4312@@ -376,7 +389,6 @@ public class ElemStyles implements PreferenceChangedListener {
4313 * @since 13810 (signature)
4314 */
4315 public Pair<StyleElementList, Range> generateStyles(IPrimitive osm, double scale, boolean pretendWayIsClosed) {
4316-
4317 List<StyleElement> sl = new ArrayList<>();
4318 MultiCascade mc = new MultiCascade();
4319 Environment env = new Environment(osm, mc, null, null);
4320diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java
4321new file mode 100644
4322index 000000000..0130da35e
4323--- /dev/null
4324+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java
4325@@ -0,0 +1,53 @@
4326+// License: GPL. For details, see LICENSE file.
4327+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
4328+
4329+import static org.junit.jupiter.api.Assertions.assertEquals;
4330+
4331+
4332+import javax.json.Json;
4333+import javax.json.JsonValue;
4334+
4335+import nl.jqno.equalsverifier.EqualsVerifier;
4336+import org.junit.jupiter.api.Test;
4337+
4338+/**
4339+ * Test class for {@link Expression}
4340+ * @author Taylor Smock
4341+ * @since xxx
4342+ */
4343+class ExpressionTest {
4344+ @Test
4345+ void testInvalidJson() {
4346+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.NULL));
4347+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.FALSE));
4348+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.TRUE));
4349+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.EMPTY_JSON_OBJECT));
4350+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.EMPTY_JSON_ARRAY));
4351+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createObjectBuilder().add("bad", "value").build()));
4352+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createValue(1)));
4353+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createValue(1.0)));
4354+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createValue("bad string")));
4355+ }
4356+
4357+ @Test
4358+ void testBasicExpressions() {
4359+ // "filter": [ "==|>=|<=|<|>", "key", "value" ]
4360+ assertEquals("[key=value]", new Expression(Json.createArrayBuilder().add("==").add("key").add("value").build()).toString());
4361+ assertEquals("[key>=true]", new Expression(Json.createArrayBuilder().add(">=").add("key").add(true).build()).toString());
4362+ assertEquals("[key<=false]", new Expression(Json.createArrayBuilder().add("<=").add("key").add(false).build()).toString());
4363+ assertEquals("[key<1]", new Expression(Json.createArrayBuilder().add("<").add("key").add(1).build()).toString());
4364+ assertEquals("[key>2.5]", new Expression(Json.createArrayBuilder().add(">").add("key").add(2.5).build()).toString());
4365+ // Test bad expression
4366+ assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createArrayBuilder().add(">>").add("key").add("value").build()));
4367+
4368+ // Test expressions with a subarray and object. This is expected to fail when properly supported, so it should be fixed.
4369+ assertEquals("[key=[{bad:value}]]", new Expression(Json.createArrayBuilder().add("==").add("key").add(
4370+ Json.createArrayBuilder().add(Json.createObjectBuilder().add("bad", "value"))).build()).toString());
4371+ assertEquals("[key=]", new Expression(Json.createArrayBuilder().add("==").add("key").add(JsonValue.NULL).build()).toString());
4372+ }
4373+
4374+ @Test
4375+ void testEquals() {
4376+ EqualsVerifier.forClass(Expression.class).verify();
4377+ }
4378+}
4379diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
4380new file mode 100644
4381index 000000000..28b09b950
4382--- /dev/null
4383+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
4384@@ -0,0 +1,601 @@
4385+// License: GPL. For details, see LICENSE file.
4386+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
4387+
4388+import static org.junit.jupiter.api.Assertions.assertEquals;
4389+import static org.junit.jupiter.api.Assertions.assertNull;
4390+import static org.junit.jupiter.api.Assertions.assertSame;
4391+import static org.junit.jupiter.api.Assertions.assertThrows;
4392+
4393+import java.text.MessageFormat;
4394+import java.util.Locale;
4395+
4396+import javax.json.Json;
4397+import javax.json.JsonObject;
4398+import javax.json.JsonValue;
4399+
4400+import nl.jqno.equalsverifier.EqualsVerifier;
4401+import org.junit.jupiter.api.Test;
4402+
4403+/**
4404+ * Test class for {@link Layers}.
4405+ * @implNote Tests will fail when support is added for new styling information.
4406+ * All current (2021-03-31) properties are checked for in some form or another.
4407+ * @author Taylor Smock
4408+ * @since xxx
4409+ */
4410+class LayersTest {
4411+ @Test
4412+ void testBackground() {
4413+ // Test an empty background layer
4414+ Layers emptyBackgroundLayer = new Layers(Json.createObjectBuilder()
4415+ .add("type", Layers.Type.BACKGROUND.name())
4416+ .add("id", "Empty Background").build());
4417+ assertEquals("Empty Background", emptyBackgroundLayer.getId());
4418+ assertEquals(Layers.Type.BACKGROUND, emptyBackgroundLayer.getType());
4419+ assertNull(emptyBackgroundLayer.getSource());
4420+ assertSame(Expression.EMPTY_EXPRESSION, emptyBackgroundLayer.getFilter());
4421+ assertEquals("", emptyBackgroundLayer.toString());
4422+
4423+ // Test a background layer with some styling information
4424+ JsonObject allProperties = Json.createObjectBuilder()
4425+ .add("background-color", "#fff000") // fill-color:#fff000;
4426+ .add("background-opacity", 0.5) // No good mapping for JOSM yet
4427+ .add("background-pattern", "null") // This should be an image, not implemented
4428+ .build();
4429+ Layers backgroundLayer = new Layers(Json.createObjectBuilder()
4430+ .add("id", "Background layer")
4431+ .add("type", Layers.Type.BACKGROUND.name().toLowerCase(Locale.ROOT))
4432+ .add("paint", allProperties)
4433+ .build());
4434+ assertEquals("canvas{fill-color:#fff000;}", backgroundLayer.toString());
4435+
4436+ // Test a background layer with some styling information, but invisible
4437+ Layers invisibleBackgroundLayer = new Layers(Json.createObjectBuilder()
4438+ .add("id", "Background layer")
4439+ .add("type", Layers.Type.BACKGROUND.name().toLowerCase(Locale.ROOT))
4440+ .add("layout", Json.createObjectBuilder().add("visibility", "none").build())
4441+ .add("paint", allProperties).build());
4442+ assertEquals("", invisibleBackgroundLayer.toString());
4443+ }
4444+
4445+ @Test
4446+ void testFill() {
4447+ // Test a layer without a source (should fail)
4448+ assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
4449+ .add("type", Layers.Type.FILL.name())
4450+ .add("id", "Empty Fill").build()));
4451+
4452+ // Test an empty fill layer
4453+ Layers emptyFillLayer = new Layers(Json.createObjectBuilder()
4454+ .add("type", Layers.Type.FILL.name())
4455+ .add("id", "Empty Fill")
4456+ .add("source", "Random source").build());
4457+ assertEquals("Empty Fill", emptyFillLayer.getId());
4458+ assertEquals("Random source", emptyFillLayer.getSource());
4459+ assertEquals("", emptyFillLayer.toString());
4460+
4461+ // Test a fully implemented fill layer
4462+ JsonObject allLayoutProperties = Json.createObjectBuilder()
4463+ .add("fill-sort-key", 5)
4464+ .add("visibility", "visible")
4465+ .build();
4466+ JsonObject allPaintProperties = Json.createObjectBuilder()
4467+ .add("fill-antialias", false)
4468+ .add("fill-color", "#fff000") // fill-color:#fff000
4469+ .add("fill-opacity", 0.5) // fill-opacity:0.5
4470+ .add("fill-outline-color", "#ffff00") // fill-color:#ffff00 (defaults to fill-color)
4471+ .add("fill-pattern", JsonValue.NULL) // disables fill-outline-color and fill-color
4472+ .add("fill-translate", Json.createArrayBuilder().add(5).add(5))
4473+ .add("fill-translate-anchor", "viewport") // requires fill-translate
4474+ .build();
4475+
4476+ Layers fullFillLayer = new Layers(Json.createObjectBuilder()
4477+ .add("type", Layers.Type.FILL.toString())
4478+ .add("id", "random-layer-id")
4479+ .add("source", "Random source")
4480+ .add("layout", allLayoutProperties)
4481+ .add("paint", allPaintProperties)
4482+ .build());
4483+ assertEquals("random-layer-id", fullFillLayer.getId());
4484+ assertEquals(Layers.Type.FILL, fullFillLayer.getType());
4485+ assertEquals("area::random-layer-id{fill-color:#fff000;fill-opacity:0.5;color:#ffff00;}", fullFillLayer.toString());
4486+
4487+ // Test a fully implemented fill layer (invisible)
4488+ Layers fullFillInvisibleLayer = new Layers(Json.createObjectBuilder()
4489+ .add("type", Layers.Type.FILL.toString())
4490+ .add("id", "random-layer-id")
4491+ .add("source", "Random source")
4492+ .add("layout", Json.createObjectBuilder(allLayoutProperties)
4493+ .add("visibility", "none"))
4494+ .add("paint", allPaintProperties)
4495+ .build());
4496+ assertEquals("random-layer-id", fullFillInvisibleLayer.getId());
4497+ assertEquals(Layers.Type.FILL, fullFillInvisibleLayer.getType());
4498+ assertEquals("", fullFillInvisibleLayer.toString());
4499+ }
4500+
4501+ @Test
4502+ void testLine() {
4503+ // Test a layer without a source (should fail)
4504+ assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
4505+ .add("type", Layers.Type.LINE.name())
4506+ .add("id", "Empty Line").build()));
4507+
4508+ JsonObject allLayoutProperties = Json.createObjectBuilder()
4509+ .add("line-cap", "round") // linecap:round;
4510+ .add("line-join", "bevel")
4511+ .add("line-miter-limit", 65)
4512+ .add("line-round-limit", 1.5)
4513+ .add("line-sort-key", 3)
4514+ .add("visibility", "visible")
4515+ .build();
4516+ JsonObject allPaintProperties = Json.createObjectBuilder()
4517+ .add("line-blur", 5)
4518+ .add("line-color", "#fff000") // color:#fff000;
4519+ .add("line-dasharray", Json.createArrayBuilder().add(1).add(5).add(1)) // dashes:1,5,1;
4520+ .add("line-gap-width", 6)
4521+ .add("line-gradient", "#ffff00") // disabled by line-dasharray/line-pattern, source must be "geojson"
4522+ .add("line-offset", 12)
4523+ .add("line-opacity", 0.5) // opacity:0.5;
4524+ .add("line-pattern", JsonValue.NULL)
4525+ .add("line-translate", Json.createArrayBuilder().add(-1).add(-2))
4526+ .add("line-translate-anchor", "viewport")
4527+ .add("line-width", 22) // width:22;
4528+ .build();
4529+
4530+ // Test fully defined line
4531+ Layers fullLineLayer = new Layers(Json.createObjectBuilder()
4532+ .add("type", Layers.Type.LINE.name().toLowerCase(Locale.ROOT))
4533+ .add("id", "random-layer-id")
4534+ .add("source", "Random source")
4535+ .add("layout", allLayoutProperties)
4536+ .add("paint", allPaintProperties)
4537+ .build());
4538+ assertEquals("random-layer-id", fullLineLayer.getId());
4539+ assertEquals(Layers.Type.LINE, fullLineLayer.getType());
4540+ assertEquals("way::random-layer-id{color:#fff000;opacity:0.5;linecap:round;dashes:1,5,1;width:22;}", fullLineLayer.toString());
4541+
4542+ // Test invisible line
4543+ Layers fullLineInvisibleLayer = new Layers(Json.createObjectBuilder()
4544+ .add("type", Layers.Type.LINE.name().toLowerCase(Locale.ROOT))
4545+ .add("id", "random-layer-id")
4546+ .add("source", "Random source")
4547+ .add("layout", Json.createObjectBuilder(allLayoutProperties)
4548+ .add("visibility", "none"))
4549+ .add("paint", allPaintProperties)
4550+ .build());
4551+ assertEquals("random-layer-id", fullLineInvisibleLayer.getId());
4552+ assertEquals(Layers.Type.LINE, fullLineInvisibleLayer.getType());
4553+ assertEquals("", fullLineInvisibleLayer.toString());
4554+ }
4555+
4556+ @Test
4557+ void testSymbol() {
4558+ // Test a layer without a source (should fail)
4559+ assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
4560+ .add("type", Layers.Type.SYMBOL.name())
4561+ .add("id", "Empty Symbol").build()));
4562+
4563+ JsonObject allPaintProperties = Json.createObjectBuilder()
4564+ .add("icon-color", "#fff000") // also requires sdf icons
4565+ .add("icon-halo-blur", 5)
4566+ .add("icon-halo-color", "#ffff00")
4567+ .add("icon-halo-width", 6)
4568+ .add("icon-opacity", 0.5) // icon-opacity:0.5;
4569+ .add("icon-translate", Json.createArrayBuilder().add(11).add(12))
4570+ .add("icon-translate-anchor", "viewport") // also requires icon-translate
4571+ .add("text-color", "#fffff0") // text-color:#fffff0;
4572+ .add("text-halo-blur", 15)
4573+ .add("text-halo-color", "#ffffff") // text-halo-color:#ffffff;
4574+ .add("text-halo-width", 16) // text-halo-radius:16;
4575+ .add("text-opacity", 0.6) // text-opacity:0.6;
4576+ .add("text-translate", Json.createArrayBuilder().add(26).add(27))
4577+ .add("text-translate-anchor", "viewport")
4578+ .build();
4579+ JsonObject allLayoutProperties = Json.createObjectBuilder()
4580+ .add("icon-allow-overlap", true)
4581+ .add("icon-anchor", "left")
4582+ .add("icon-ignore-placement", true)
4583+ .add("icon-image", "random-image") // icon-image:concat(\"random-image\");
4584+ .add("icon-keep-upright", true) // also requires icon-rotation-alignment=map and symbol-placement=line|line-center
4585+ .add("icon-offset", Json.createArrayBuilder().add(2).add(3)) // icon-offset-x:2.0;icon-offset-y:3.0;
4586+ .add("icon-optional", true) // also requires text-field
4587+ .add("icon-padding", 4)
4588+ .add("icon-pitch-alignment", "viewport")
4589+ .add("icon-rotate", 30) // icon-rotation:30.0;
4590+ .add("icon-rotation-alignment", "map")
4591+ .add("icon-size", 2)
4592+ .add("icon-text-fit", "width") // also requires text-field
4593+ .add("icon-text-fit-padding", Json.createArrayBuilder().add(7).add(8).add(9).add(10))
4594+ .add("symbol-avoid-edges", true)
4595+ .add("symbol-placement", "line")
4596+ .add("symbol-sort-key", 13)
4597+ .add("symbol-spacing", 14) // requires symbol-placement=line
4598+ .add("symbol-z-order", "source")
4599+ .add("text-allow-overlap", true) // requires text-field
4600+ .add("text-anchor", "left") // requires text-field, disabled by text-variable-anchor
4601+ .add("text-field", "something") // text:something;
4602+ .add("text-font", Json.createArrayBuilder().add("SansSerif")) // DroidSans isn't always available in an IDE
4603+ .add("text-ignore-placement", true)
4604+ .add("text-justify", "left")
4605+ .add("text-keep-upright", false)
4606+ .add("text-letter-spacing", 17)
4607+ .add("text-line-height", 1.3)
4608+ .add("text-max-angle", 18)
4609+ .add("text-max-width", 19)
4610+ .add("text-offset", Json.createArrayBuilder().add(20).add(21))
4611+ .add("text-optional", true)
4612+ .add("text-padding", 22)
4613+ .add("text-pitch-alignment", "viewport")
4614+ .add("text-radial-offset", 23)
4615+ .add("text-rotate", 24)
4616+ .add("text-rotation-alignment", "viewport")
4617+ .add("text-size", 25) // font-size:25;
4618+ .add("text-transform", "uppercase")
4619+ .add("text-variable-anchor", "left")
4620+ .add("text-writing-mode", "vertical")
4621+ .add("visibility", "visible").build();
4622+
4623+ // Test fully defined symbol
4624+ Layers fullLineLayer = new Layers(Json.createObjectBuilder()
4625+ .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
4626+ .add("id", "random-layer-id")
4627+ .add("source", "Random source")
4628+ .add("layout", allLayoutProperties)
4629+ .add("paint", allPaintProperties)
4630+ .build());
4631+ assertEquals("random-layer-id", fullLineLayer.getId());
4632+ assertEquals(Layers.Type.SYMBOL, fullLineLayer.getType());
4633+ assertEquals("node::random-layer-id{icon-image:concat(\"random-image\");icon-offset-x:2.0;icon-offset-y:3.0;"
4634+ + "icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";font-weight:normal;"
4635+ + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}", fullLineLayer.toString());
4636+
4637+ // Test an invisible symbol
4638+ Layers fullLineInvisibleLayer = new Layers(Json.createObjectBuilder()
4639+ .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
4640+ .add("id", "random-layer-id")
4641+ .add("source", "Random source")
4642+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
4643+ .add("paint", allPaintProperties)
4644+ .build());
4645+ assertEquals("random-layer-id", fullLineInvisibleLayer.getId());
4646+ assertEquals(Layers.Type.SYMBOL, fullLineInvisibleLayer.getType());
4647+ assertEquals("", fullLineInvisibleLayer.toString());
4648+
4649+ // Test with placeholders in icon-image
4650+ Layers fullOneIconImagePlaceholderLineLayer = new Layers(Json.createObjectBuilder()
4651+ .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
4652+ .add("id", "random-layer-id")
4653+ .add("source", "Random source")
4654+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("icon-image", "{value}"))
4655+ .add("paint", allPaintProperties)
4656+ .build());
4657+ assertEquals("node::random-layer-id{icon-image:concat(tag(\"value\"));icon-offset-x:2.0;icon-offset-y:3.0;"
4658+ + "icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";font-weight:normal;"
4659+ + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}",
4660+ fullOneIconImagePlaceholderLineLayer.toString());
4661+
4662+ // Test with placeholders in icon-image
4663+ Layers fullOneIconImagePlaceholderExtraLineLayer = new Layers(Json.createObjectBuilder()
4664+ .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
4665+ .add("id", "random-layer-id")
4666+ .add("source", "Random source")
4667+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("icon-image", "something/{value}/random"))
4668+ .add("paint", allPaintProperties)
4669+ .build());
4670+ assertEquals("node::random-layer-id{icon-image:concat(\"something/\",tag(\"value\"),\"/random\");icon-offset-x:2.0;"
4671+ + "icon-offset-y:3.0;icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";"
4672+ + "font-weight:normal;font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}",
4673+ fullOneIconImagePlaceholderExtraLineLayer.toString());
4674+
4675+ // Test with placeholders in icon-image
4676+ Layers fullTwoIconImagePlaceholderExtraLineLayer = new Layers(Json.createObjectBuilder()
4677+ .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT))
4678+ .add("id", "random-layer-id")
4679+ .add("source", "Random source")
4680+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("icon-image", "something/{value}/random/{value2}"))
4681+ .add("paint", allPaintProperties)
4682+ .build());
4683+ assertEquals("node::random-layer-id{icon-image:concat(\"something/\",tag(\"value\"),\"/random/\",tag(\"value2\"));"
4684+ + "icon-offset-x:2.0;icon-offset-y:3.0;icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;"
4685+ + "font-family:\"SansSerif\";font-weight:normal;font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;"
4686+ + "text-opacity:0.6;font-size:25;}", fullTwoIconImagePlaceholderExtraLineLayer.toString());
4687+ }
4688+
4689+ @Test
4690+ void testRaster() {
4691+ // Test a layer without a source (should fail)
4692+ assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
4693+ .add("type", Layers.Type.RASTER.name())
4694+ .add("id", "Empty Raster").build()));
4695+
4696+ JsonObject allPaintProperties = Json.createObjectBuilder()
4697+ .add("raster-brightness-max", 0.5)
4698+ .add("raster-brightness-min", 0.6)
4699+ .add("raster-contrast", 0.7)
4700+ .add("raster-fade-duration", 1)
4701+ .add("raster-hue-rotate", 2)
4702+ .add("raster-opacity", 0.7)
4703+ .add("raster-resampling", "nearest")
4704+ .add("raster-saturation", 0.8)
4705+ .build();
4706+ JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build();
4707+
4708+ // Test fully defined raster
4709+ Layers fullRaster = new Layers(Json.createObjectBuilder()
4710+ .add("id", "test-raster")
4711+ .add("type", Layers.Type.RASTER.name().toLowerCase(Locale.ROOT))
4712+ .add("source", "Random source")
4713+ .add("layout", allLayoutProperties)
4714+ .add("paint", allPaintProperties)
4715+ .build());
4716+ assertEquals(Layers.Type.RASTER, fullRaster.getType());
4717+ assertEquals("test-raster", fullRaster.getId());
4718+ assertEquals("Random source", fullRaster.getSource());
4719+ assertEquals("", fullRaster.toString());
4720+
4721+ // Test fully defined invisible raster
4722+ Layers fullInvisibleRaster = new Layers(Json.createObjectBuilder()
4723+ .add("id", "test-raster")
4724+ .add("type", Layers.Type.RASTER.name().toLowerCase(Locale.ROOT))
4725+ .add("source", "Random source")
4726+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
4727+ .add("paint", allPaintProperties)
4728+ .build());
4729+ assertEquals("", fullInvisibleRaster.toString());
4730+ }
4731+
4732+ @Test
4733+ void testCircle() {
4734+ // Test a layer without a source (should fail)
4735+ assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
4736+ .add("type", Layers.Type.CIRCLE.name())
4737+ .add("id", "Empty Circle").build()));
4738+
4739+ JsonObject allPaintProperties = Json.createObjectBuilder()
4740+ .add("circle-blur", 1)
4741+ .add("circle-color", "#fff000") // symbol-fill-color:#fff000;
4742+ .add("circle-opacity", 0.5) // symbol-fill-opacity:0.5;
4743+ .add("circle-pitch-alignment", "map")
4744+ .add("circle-pitch-scale", "viewport")
4745+ .add("circle-radius", 2) // symbol-size:4.0; (we use width)
4746+ .add("circle-stroke-color", "#ffff00") // symbol-stroke-color:#ffff00;
4747+ .add("circle-stroke-opacity", 0.6) // symbol-stroke-opacity:0.6;
4748+ .add("circle-stroke-width", 5) // symbol-stroke-width:5.0;
4749+ .add("circle-translate", Json.createArrayBuilder().add(3).add(4))
4750+ .add("circle-translate-anchor", "viewport")
4751+ .build();
4752+ JsonObject allLayoutProperties = Json.createObjectBuilder()
4753+ .add("circle-sort-key", 3)
4754+ .add("visibility", "visible")
4755+ .build();
4756+
4757+ Layers fullCircleLayer = new Layers(Json.createObjectBuilder()
4758+ .add("id", "Full circle layer")
4759+ .add("type", Layers.Type.CIRCLE.name().toLowerCase(Locale.ROOT))
4760+ .add("source", "Random source")
4761+ .add("layout", allLayoutProperties)
4762+ .add("paint", allPaintProperties)
4763+ .build());
4764+ assertEquals(Layers.Type.CIRCLE, fullCircleLayer.getType());
4765+ assertEquals("Full circle layer", fullCircleLayer.getId());
4766+ assertEquals("Random source", fullCircleLayer.getSource());
4767+ assertEquals("node::Full circle layer{symbol-shape:circle;symbol-fill-color:#fff000;symbol-fill-opacity:0.5;"
4768+ + "symbol-size:4.0;symbol-stroke-color:#ffff00;symbol-stroke-opacity:0.6;symbol-stroke-width:5;}", fullCircleLayer.toString());
4769+
4770+ Layers fullCircleInvisibleLayer = new Layers(Json.createObjectBuilder()
4771+ .add("id", "Full circle layer")
4772+ .add("type", Layers.Type.CIRCLE.name().toLowerCase(Locale.ROOT))
4773+ .add("source", "Random source")
4774+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
4775+ .add("paint", allPaintProperties)
4776+ .build());
4777+ assertEquals(Layers.Type.CIRCLE, fullCircleInvisibleLayer.getType());
4778+ assertEquals("Full circle layer", fullCircleInvisibleLayer.getId());
4779+ assertEquals("Random source", fullCircleInvisibleLayer.getSource());
4780+ assertEquals("", fullCircleInvisibleLayer.toString());
4781+ }
4782+
4783+ @Test
4784+ void testFillExtrusion() {
4785+ // Test a layer without a source (should fail)
4786+ assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
4787+ .add("type", Layers.Type.FILL_EXTRUSION.name())
4788+ .add("id", "Empty Fill Extrusion").build()));
4789+
4790+ JsonObject allPaintProperties = Json.createObjectBuilder()
4791+ .add("fill-extrusion-base", 1)
4792+ .add("fill-extrusion-color", "#fff000")
4793+ .add("fill-extrusion-height", 2)
4794+ .add("fill-extrusion-opacity", 0.5)
4795+ .add("fill-extrusion-pattern", "something-random")
4796+ .add("fill-extrusion-translate", Json.createArrayBuilder().add(3).add(4))
4797+ .add("fill-extrusion-translate-anchor", "viewport")
4798+ .add("fill-extrusion-vertical-gradient", false)
4799+ .build();
4800+ JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build();
4801+
4802+ Layers fullFillLayer = new Layers(Json.createObjectBuilder()
4803+ .add("id", "Fill Extrusion")
4804+ .add("type", Layers.Type.FILL_EXTRUSION.name().toLowerCase(Locale.ROOT))
4805+ .add("source", "Random source")
4806+ .add("layout", allLayoutProperties)
4807+ .add("paint", allPaintProperties)
4808+ .build());
4809+ assertEquals("", fullFillLayer.toString());
4810+ assertEquals(Layers.Type.FILL_EXTRUSION, fullFillLayer.getType());
4811+ Layers fullFillInvisibleLayer = new Layers(Json.createObjectBuilder()
4812+ .add("id", "Fill Extrusion")
4813+ .add("type", Layers.Type.FILL_EXTRUSION.name().toLowerCase(Locale.ROOT))
4814+ .add("source", "Random source")
4815+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
4816+ .add("paint", allPaintProperties)
4817+ .build());
4818+ assertEquals("", fullFillInvisibleLayer.toString());
4819+ assertEquals(Layers.Type.FILL_EXTRUSION, fullFillInvisibleLayer.getType());
4820+ }
4821+
4822+ @Test
4823+ void testHeatmap() {
4824+ // Test a layer without a source (should fail)
4825+ assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
4826+ .add("type", Layers.Type.HEATMAP.name())
4827+ .add("id", "Empty Heatmap").build()));
4828+
4829+ JsonObject allPaintProperties = Json.createObjectBuilder()
4830+ .add("heatmap-color", "#fff000") // This will probably be a gradient of some type
4831+ .add("heatmap-intensity", 0.5)
4832+ .add("heatmap-opacity", 0.6)
4833+ .add("heatmap-radius", 1) // This is in pixels
4834+ .add("heatmap-weight", 0.7)
4835+ .build();
4836+ JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build();
4837+
4838+ Layers fullHeatmapLayer = new Layers(Json.createObjectBuilder()
4839+ .add("id", "Full heatmap")
4840+ .add("type", Layers.Type.HEATMAP.name().toLowerCase(Locale.ROOT))
4841+ .add("source", "Random source")
4842+ .add("paint", allPaintProperties)
4843+ .add("layout", allLayoutProperties)
4844+ .build());
4845+ assertEquals(Layers.Type.HEATMAP, fullHeatmapLayer.getType());
4846+ assertEquals("", fullHeatmapLayer.toString());
4847+
4848+ Layers fullHeatmapInvisibleLayer = new Layers(Json.createObjectBuilder()
4849+ .add("id", "Full heatmap")
4850+ .add("type", Layers.Type.HEATMAP.name().toLowerCase(Locale.ROOT))
4851+ .add("source", "Random source")
4852+ .add("paint", allPaintProperties)
4853+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
4854+ .build());
4855+ assertEquals(Layers.Type.HEATMAP, fullHeatmapInvisibleLayer.getType());
4856+ assertEquals("", fullHeatmapInvisibleLayer.toString());
4857+ }
4858+
4859+ @Test
4860+ void testHillshade() {
4861+ // Test a layer without a source (should fail)
4862+ assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
4863+ .add("type", Layers.Type.HILLSHADE.name())
4864+ .add("id", "Empty Hillshade").build()));
4865+
4866+ JsonObject allPaintProperties = Json.createObjectBuilder()
4867+ .add("hillshade-accent-color", "#fff000")
4868+ .add("hillshade-exaggeration", 0.6)
4869+ .add("hillshade-highlight-color", "#ffff00")
4870+ .add("hillshade-illumination-anchor", "map")
4871+ .add("hillshade-illumination-direction", 90)
4872+ .add("hillshade-shadow-color", "#fffff0")
4873+ .build();
4874+ JsonObject allLayoutProperties = Json.createObjectBuilder()
4875+ .add("visibility", "visible")
4876+ .build();
4877+
4878+ Layers fullHillshadeLayer = new Layers(Json.createObjectBuilder()
4879+ .add("id", "Hillshade")
4880+ .add("type", Layers.Type.HILLSHADE.toString().toLowerCase(Locale.ROOT))
4881+ .add("source", "Random source")
4882+ .add("paint", allPaintProperties)
4883+ .add("layout", allLayoutProperties)
4884+ .build());
4885+ assertEquals(Layers.Type.HILLSHADE, fullHillshadeLayer.getType());
4886+ assertEquals("", fullHillshadeLayer.toString());
4887+
4888+ Layers fullHillshadeInvisibleLayer = new Layers(Json.createObjectBuilder()
4889+ .add("id", "Hillshade")
4890+ .add("type", Layers.Type.HILLSHADE.toString().toLowerCase(Locale.ROOT))
4891+ .add("source", "Random source")
4892+ .add("paint", allPaintProperties)
4893+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
4894+ .build());
4895+ assertEquals(Layers.Type.HILLSHADE, fullHillshadeInvisibleLayer.getType());
4896+ assertEquals("", fullHillshadeInvisibleLayer.toString());
4897+ }
4898+
4899+ @Test
4900+ void testSky() {
4901+ // Test a layer without a source (should fail)
4902+ assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder()
4903+ .add("type", Layers.Type.SKY.name())
4904+ .add("id", "Empty Sky").build()));
4905+
4906+ JsonObject allPaintProperties = Json.createObjectBuilder()
4907+ .add("sky-atmosphere-color", "red")
4908+ .add("sky-atmosphere-halo-color", "yellow")
4909+ // 360180 is apparently included in this? Or it might be a formatting issue in the docs.
4910+ .add("sky-atmosphere-sun", Json.createArrayBuilder().add(0, 360180))
4911+ .add("sky-atmosphere-sun-intensity", 99)
4912+ .add("sky-gradient", "#fff000")
4913+ .add("sky-gradient-center", Json.createArrayBuilder().add(0).add(360180)) // see note on 360180 above
4914+ .add("sky-gradient-radius", 1)
4915+ .add("sky-opacity", 0.5)
4916+ .add("sky-type", "gradient")
4917+ .build();
4918+ JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build();
4919+
4920+ Layers fullSkyLayer = new Layers(Json.createObjectBuilder()
4921+ .add("id", "Sky")
4922+ .add("type", Layers.Type.SKY.name().toLowerCase(Locale.ROOT))
4923+ .add("source", "Random source")
4924+ .add("paint", allPaintProperties)
4925+ .add("layout", allLayoutProperties)
4926+ .build());
4927+ assertEquals(Layers.Type.SKY, fullSkyLayer.getType());
4928+ assertEquals("", fullSkyLayer.toString());
4929+
4930+ Layers fullSkyInvisibleLayer = new Layers(Json.createObjectBuilder()
4931+ .add("id", "Sky")
4932+ .add("type", Layers.Type.SKY.name().toLowerCase(Locale.ROOT))
4933+ .add("source", "Random source")
4934+ .add("paint", allPaintProperties)
4935+ .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none"))
4936+ .build());
4937+ assertEquals(Layers.Type.SKY, fullSkyInvisibleLayer.getType());
4938+ assertEquals("", fullSkyInvisibleLayer.toString());
4939+ }
4940+
4941+ @Test
4942+ void testZoomLevels() {
4943+ JsonObject baseInformation = Json.createObjectBuilder()
4944+ .add("id", "dots")
4945+ .add("type", "CiRcLe")
4946+ .add("source", "osm-source")
4947+ .add("source-layer", "osm-images")
4948+ .add("paint", Json.createObjectBuilder()
4949+ .add("circle-color", "#fff000")
4950+ .add("circle-radius", 6)
4951+ ).build();
4952+ Layers noZoomLayer = new Layers(baseInformation);
4953+ String baseString = "node{0}::dots'{symbol-shape:circle;symbol-fill-color:#fff000;symbol-fill-opacity:1;"
4954+ + "symbol-size:12.0;symbol-stroke-color:#000000;symbol-stroke-opacity:1;symbol-stroke-width:0;}'";
4955+ assertEquals("osm-images", noZoomLayer.getSourceLayer());
4956+ assertEquals(MessageFormat.format(baseString, ""), noZoomLayer.toString());
4957+
4958+ Layers minZoomLayer = new Layers(Json.createObjectBuilder(baseInformation)
4959+ .add("minzoom", 0)
4960+ .build());
4961+ assertEquals(MessageFormat.format(baseString, "|z0-"), minZoomLayer.toString());
4962+
4963+ Layers maxZoomLayer = new Layers(Json.createObjectBuilder(baseInformation)
4964+ .add("maxzoom", 24)
4965+ .build());
4966+ assertEquals(MessageFormat.format(baseString, "|z-24"), maxZoomLayer.toString());
4967+
4968+ Layers minMaxZoomLayer = new Layers(Json.createObjectBuilder(baseInformation)
4969+ .add("minzoom", 1)
4970+ .add("maxzoom", 2)
4971+ .build());
4972+ assertEquals(MessageFormat.format(baseString, "|z1-2"), minMaxZoomLayer.toString());
4973+
4974+ Layers sameMinMaxZoomLayer = new Layers(Json.createObjectBuilder(baseInformation)
4975+ .add("minzoom", 2)
4976+ .add("maxzoom", 2)
4977+ .build());
4978+ assertEquals(MessageFormat.format(baseString, "|z2"), sameMinMaxZoomLayer.toString());
4979+ }
4980+
4981+ @Test
4982+ void testEquals() {
4983+ EqualsVerifier.forClass(Layers.class).usingGetClass().verify();
4984+ }
4985+}
4986diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java
4987new file mode 100644
4988index 000000000..1fcb7bfe8
4989--- /dev/null
4990+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java
4991@@ -0,0 +1,300 @@
4992+// License: GPL. For details, see LICENSE file.
4993+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
4994+
4995+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4996+import static org.junit.jupiter.api.Assertions.assertEquals;
4997+import static org.junit.jupiter.api.Assertions.assertFalse;
4998+import static org.junit.jupiter.api.Assertions.assertNotNull;
4999+import static org.junit.jupiter.api.Assertions.assertThrows;
5000+import static org.junit.jupiter.api.Assertions.assertTrue;
5001+import static org.junit.jupiter.api.Assertions.fail;
5002+
5003+
5004+import java.awt.Color;
5005+import java.awt.Graphics2D;
5006+import java.awt.image.BufferedImage;
5007+import java.io.ByteArrayInputStream;
5008+import java.io.File;
5009+import java.io.FileOutputStream;
5010+import java.io.IOException;
5011+import java.nio.charset.StandardCharsets;
5012+import java.nio.file.Paths;
5013+import java.text.MessageFormat;
5014+import java.util.ArrayList;
5015+import java.util.Collection;
5016+import java.util.Map;
5017+import java.util.Objects;
5018+import java.util.Optional;
5019+import java.util.concurrent.atomic.AtomicBoolean;
5020+import java.util.stream.Collectors;
5021+
5022+import javax.imageio.ImageIO;
5023+import javax.json.Json;
5024+import javax.json.JsonObject;
5025+import javax.json.JsonObjectBuilder;
5026+import javax.json.JsonReader;
5027+import javax.json.JsonStructure;
5028+import javax.json.JsonValue;
5029+
5030+import org.openstreetmap.josm.TestUtils;
5031+import org.openstreetmap.josm.gui.MainApplication;
5032+import org.openstreetmap.josm.gui.mappaint.ElemStyles;
5033+import org.openstreetmap.josm.gui.mappaint.Keyword;
5034+import org.openstreetmap.josm.gui.mappaint.StyleSource;
5035+import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction;
5036+import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule;
5037+import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
5038+import org.openstreetmap.josm.testutils.JOSMTestRules;
5039+import org.openstreetmap.josm.tools.ColorHelper;
5040+import org.openstreetmap.josm.tools.ImageProvider;
5041+
5042+import nl.jqno.equalsverifier.EqualsVerifier;
5043+import org.awaitility.Awaitility;
5044+import org.awaitility.Durations;
5045+import org.junit.jupiter.api.Test;
5046+import org.junit.jupiter.api.extension.RegisterExtension;
5047+import org.junit.jupiter.api.io.TempDir;
5048+
5049+/**
5050+ * Test class for {@link MapBoxVectorStyle}
5051+ * @author Taylor Smock
5052+ */
5053+public class MapBoxVectorStyleTest {
5054+ /** Used to store sprite files (specifically, sprite{,@2x}.{png,json}) */
5055+ @TempDir
5056+ File spritesDirectory;
5057+
5058+ // Needed for osm primitives (we really just need to initialize the config)
5059+ // OSM primitives are called when we load style sources
5060+ @RegisterExtension
5061+ JOSMTestRules rules = new JOSMTestRules();
5062+
5063+ /** The base information */
5064+ private static final String BASE_STYLE = "'{'\"version\":8,\"name\":\"test style\",\"owner\":\"josm test\",\"id\":\"{0}\",{1}'}'";
5065+ /** Source 1 */
5066+ private static final String SOURCE1 = "\"source1\":{\"type\":\"vector\",\"tiles\":[\"https://example.org/{z}/{x}/{y}.mvt\"]}";
5067+ /** Layer 1 */
5068+ private static final String LAYER1 = "{\"id\":\"layer1\",\"type\":\"circle\",\"source\":\"source1\",\"source-layer\":\"nodes\"}";
5069+ /** Source 2 */
5070+ private static final String SOURCE2 = "\"source2\":{\"type\":\"vector\",\"tiles\":[\"https://example.org/{z}2/{x}/{y}.mvt\"]}";
5071+ /** Layer 2 */
5072+ private static final String LAYER2 = "{\"id\":\"layer2\",\"type\":\"circle\",\"source\":\"source2\",\"source-layer\":\"nodes\"}";
5073+
5074+ /**
5075+ * Check that the version matches the supported style version(s). Currently, only version 8 exists and is (partially)
5076+ * supported.
5077+ */
5078+ @Test
5079+ void testVersionChecks() {
5080+ assertThrows(NullPointerException.class, () -> new MapBoxVectorStyle(JsonValue.EMPTY_JSON_OBJECT));
5081+ IllegalArgumentException badVersion = assertThrows(IllegalArgumentException.class,
5082+ () -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 7).build()));
5083+ assertEquals("Vector Tile Style Version not understood: version 7 (json: {\"version\":7})", badVersion.getMessage());
5084+ badVersion = assertThrows(IllegalArgumentException.class,
5085+ () -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 9).build()));
5086+ assertEquals("Vector Tile Style Version not understood: version 9 (json: {\"version\":9})", badVersion.getMessage());
5087+ assertDoesNotThrow(() -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 8).build()));
5088+ }
5089+
5090+ @Test
5091+ void testSources() {
5092+ // Check with an invalid sources list
5093+ assertTrue(new MapBoxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty());
5094+ Map<Source, ElemStyles> sources = new MapBoxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test",
5095+ MessageFormat.format("\"sources\":'{'{0},{1},\"source3\":[\"bad source\"]'}',\"layers\":[{2},{3},{4}]",
5096+ SOURCE1, SOURCE2, LAYER1, LAYER2, LAYER2.replace('2', '3'))))).getSources();
5097+ assertEquals(3, sources.size());
5098+ assertTrue(sources.containsKey(null)); // This is due to there being no source3 layer
5099+ sources.remove(null); // Avoid null checks later
5100+ assertTrue(sources.keySet().stream().map(Source::getName).anyMatch("source1"::equals));
5101+ assertTrue(sources.keySet().stream().map(Source::getName).anyMatch("source2"::equals));
5102+ assertTrue(sources.keySet().stream().map(Source::getName).noneMatch("source3"::equals));
5103+ }
5104+
5105+ @Test
5106+ void testSavedFiles() {
5107+ assertTrue(new MapBoxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty());
5108+ Map<Source, ElemStyles> sources = new MapBoxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test",
5109+ MessageFormat.format("\"sources\":'{'{0},{1}'}',\"layers\":[{2},{3}]", SOURCE1, SOURCE2, LAYER1, LAYER2)))).getSources();
5110+ assertEquals(2, sources.size());
5111+ // For various reasons, the map _must_ be reliably ordered in the order of encounter
5112+ Source source1 = sources.keySet().iterator().next();
5113+ Source source2 = sources.keySet().stream().skip(1).findFirst().orElseGet(() -> fail("No second source"));
5114+ assertEquals("source1", source1.getName());
5115+ assertEquals("source2", source2.getName());
5116+
5117+ // Check that the files have been saved. Ideally, we would check that they haven't been
5118+ // saved earlier, since this is in a different thread. Unfortunately, that is a _race condition_.
5119+ MapCSSStyleSource styleSource1 = (MapCSSStyleSource) sources.get(source1).getStyleSources().get(0);
5120+ MapCSSStyleSource styleSource2 = (MapCSSStyleSource) sources.get(source2).getStyleSources().get(0);
5121+
5122+ AtomicBoolean saveFinished = new AtomicBoolean();
5123+ MainApplication.worker.execute(() -> saveFinished.set(true));
5124+ Awaitility.await().atMost(Durations.ONE_SECOND).until(saveFinished::get);
5125+
5126+ assertTrue(styleSource1.url.endsWith("source1.mapcss"));
5127+ assertTrue(styleSource2.url.endsWith("source2.mapcss"));
5128+
5129+ MapCSSStyleSource mapCSSStyleSource1 = new MapCSSStyleSource(styleSource1.url, styleSource1.name, styleSource1.title);
5130+ MapCSSStyleSource mapCSSStyleSource2 = new MapCSSStyleSource(styleSource2.url, styleSource2.name, styleSource2.title);
5131+
5132+ assertEquals(styleSource1, mapCSSStyleSource1);
5133+ assertEquals(styleSource2, mapCSSStyleSource2);
5134+ }
5135+
5136+ @Test
5137+ void testSprites() throws IOException {
5138+ generateSprites(false);
5139+ // Ensure that we fall back to 1x sprites
5140+ assertTrue(new File(this.spritesDirectory, "sprite.png").exists());
5141+ assertFalse(new File(this.spritesDirectory, "sprite@2x.png").exists());
5142+ assertTrue(new File(this.spritesDirectory, "sprite.json").exists());
5143+ assertFalse(new File(this.spritesDirectory, "sprite@2x.json").exists());
5144+
5145+ checkImages(false);
5146+
5147+ generateSprites(true);
5148+ checkImages(true);
5149+ }
5150+
5151+ private void checkImages(boolean hiDpi) {
5152+ // Ensure that we don't have images saved in the ImageProvider cache
5153+ ImageProvider.clearCache();
5154+ int hiDpiScalar = hiDpi ? 2 : 1;
5155+ String spritePath = new File(this.spritesDirectory, "sprite").getPath();
5156+ MapBoxVectorStyle style = new MapBoxVectorStyle(getJson(JsonObject.class,
5157+ MessageFormat.format(BASE_STYLE, "sprite_test", "\"sprite\":\"file:/" + spritePath + "\"")));
5158+ assertEquals("file:/" + spritePath, style.getSpriteUrl());
5159+
5160+ AtomicBoolean saveFinished = new AtomicBoolean();
5161+ MainApplication.worker.execute(() -> saveFinished.set(true));
5162+ Awaitility.await().atMost(Durations.ONE_SECOND).until(saveFinished::get);
5163+
5164+ int scalar = 28; // 255 / 9 (could be 4, but this was a nicer number)
5165+ for (int x = 0; x < 3; x++) {
5166+ for (int y = 0; y < 3; y++) {
5167+ // Expected color
5168+ Color color = new Color(scalar * x, scalar * y, scalar * x * y);
5169+ int finalX = x;
5170+ int finalY = y;
5171+ BufferedImage image = (BufferedImage) assertDoesNotThrow(
5172+ () -> ImageProvider.get(new File("test style", MessageFormat.format("({0},{1})", finalX, finalY)).getPath()))
5173+ .getImage();
5174+ assertEquals(3 * hiDpiScalar, image.getWidth(null));
5175+ assertEquals(3 * hiDpiScalar, image.getHeight(null));
5176+ for (int x2 = 0; x2 < image.getWidth(null); x2++) {
5177+ for (int y2 = 0; y2 < image.getHeight(null); y2++) {
5178+ assertEquals(color.getRGB(), image.getRGB(x2, y2));
5179+ }
5180+ }
5181+ }
5182+ }
5183+ }
5184+
5185+ private void generateSprites(boolean hiDpi) throws IOException {
5186+ // Create a 3x3 grid of 3x3 or 6x6 pixel squares (depends upon the dpi setting)
5187+ int hiDpiScale = hiDpi ? 2 : 1;
5188+ BufferedImage nineByNine = new BufferedImage(hiDpiScale * 9, hiDpiScale * 9, BufferedImage.TYPE_4BYTE_ABGR);
5189+ int scalar = 28; // 255 / 9 (could be 4, but this was a nicer number)
5190+ Graphics2D g = nineByNine.createGraphics();
5191+ JsonObjectBuilder json = Json.createObjectBuilder();
5192+ for (int x = 0; x < 3; x++) {
5193+ for (int y = 0; y < 3; y++) {
5194+ Color color = new Color(scalar * x, scalar * y, scalar * x * y);
5195+ g.setColor(color);
5196+ g.drawRect(3 * hiDpiScale * x, 3 * hiDpiScale * y, 3 * hiDpiScale, 3 * hiDpiScale);
5197+ g.fillRect(3 * hiDpiScale * x, 3 * hiDpiScale * y, 3 * hiDpiScale, 3 * hiDpiScale);
5198+
5199+ JsonObjectBuilder sprite = Json.createObjectBuilder();
5200+ sprite.add("height", hiDpiScale * 3);
5201+ sprite.add("pixelRatio", hiDpiScale);
5202+ sprite.add("width", hiDpiScale * 3);
5203+ sprite.add("x", 3 * hiDpiScale * x);
5204+ sprite.add("y", 3 * hiDpiScale * y);
5205+
5206+ json.add(MessageFormat.format("({0},{1})", x, y), sprite);
5207+ }
5208+ }
5209+ String imageName = hiDpi ? "sprite@2x.png" : "sprite.png";
5210+ ImageIO.write(nineByNine, "png", new File(this.spritesDirectory, imageName));
5211+ String jsonName = hiDpi ? "sprite@2x.json" : "sprite.json";
5212+ File jsonFile = new File(this.spritesDirectory, jsonName);
5213+ try (FileOutputStream fileOutputStream = new FileOutputStream(jsonFile)) {
5214+ fileOutputStream.write(json.build().toString().getBytes(StandardCharsets.UTF_8));
5215+ }
5216+ }
5217+
5218+ private static <T extends JsonStructure> T getJson(Class<T> clazz, String json) {
5219+ try (JsonReader reader = Json.createReader(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)))) {
5220+ JsonStructure structure = reader.read();
5221+ if (clazz.isAssignableFrom(structure.getClass())) {
5222+ return clazz.cast(structure);
5223+ }
5224+ }
5225+ fail("Could not cast to expected class");
5226+ throw new IllegalArgumentException();
5227+ }
5228+
5229+ @Test
5230+ void testMapillaryStyle() {
5231+ final String file = Paths.get("file:", TestUtils.getTestDataRoot(), "mapillary.json").toString();
5232+ final MapBoxVectorStyle style = MapBoxVectorStyle.getMapBoxVectorStyle(file);
5233+ assertNotNull(style);
5234+ // There are three "sources" in the mapillary.json file
5235+ assertEquals(3, style.getSources().size());
5236+ final ElemStyles mapillarySource = style.getSources().entrySet().stream()
5237+ .filter(source -> "mapillary-source".equals(source.getKey().getName())).map(
5238+ Map.Entry::getValue).findAny().orElse(null);
5239+ assertNotNull(mapillarySource);
5240+ mapillarySource.getStyleSources().forEach(StyleSource::loadStyleSource);
5241+ assertEquals(1, mapillarySource.getStyleSources().size());
5242+ final MapCSSStyleSource mapillaryCssSource = (MapCSSStyleSource) mapillarySource.getStyleSources().get(0);
5243+ assertTrue(mapillaryCssSource.getErrors().isEmpty());
5244+ final MapCSSRule mapillaryOverview = getRule(mapillaryCssSource, "node", "mapillary-overview");
5245+ assertNotNull(mapillaryOverview);
5246+ assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-shape", new Keyword("circle"));
5247+ assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-color", ColorHelper.html2color("#05CB63"));
5248+ assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-opacity", 0.6f);
5249+ // Docs indicate that symbol-size is total width, while we are translating from a radius. So 2 * 4 = 8.
5250+ assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-size", 8.0f);
5251+ }
5252+
5253+ @Test
5254+ void testEqualsContract() {
5255+ // We need to "load" the style sources to avoid the verifier from thinking they are equal
5256+ StyleSource canvas = new MapCSSStyleSource("meta{title:\"canvas\";}canvas{default-points:false;}");
5257+ StyleSource node = new MapCSSStyleSource("meta{title:\"node\";}node{text:ref;}");
5258+ node.loadStyleSource();
5259+ canvas.loadStyleSource();
5260+ EqualsVerifier.forClass(MapBoxVectorStyle.class)
5261+ .withPrefabValues(ImageProvider.class, new ImageProvider("cancel"), new ImageProvider("ok"))
5262+ .withPrefabValues(StyleSource.class, canvas, node)
5263+ .usingGetClass().verify();
5264+ }
5265+
5266+ /**
5267+ * Check that an instruction is in a collection of instructions, and return it
5268+ * @param instructions The instructions to search
5269+ * @param key The key to look for
5270+ * @param value The expected value for the key
5271+ */
5272+ private void assertInInstructions(Collection<Instruction> instructions, String key, Object value) {
5273+ // In JOSM, all Instruction objects are AssignmentInstruction objects
5274+ Collection<Instruction.AssignmentInstruction> instructionKeys = instructions.stream()
5275+ .filter(Instruction.AssignmentInstruction.class::isInstance)
5276+ .map(Instruction.AssignmentInstruction.class::cast).filter(instruction -> Objects.equals(key, instruction.key))
5277+ .collect(Collectors.toList());
5278+ Optional<Instruction.AssignmentInstruction> instructionOptional = instructionKeys.stream()
5279+ .filter(instruction -> Objects.equals(value, instruction.val)).findAny();
5280+ assertTrue(instructionOptional.isPresent(), MessageFormat
5281+ .format("Expected {0}, but got {1}", value, instructionOptional.orElse(instructionKeys.stream().findAny()
5282+ .orElseThrow(() -> new AssertionError("No instruction with "+key+" found"))).val));
5283+ }
5284+
5285+ private static MapCSSRule getRule(MapCSSStyleSource source, String base, String subpart) {
5286+ // We need to do a new arraylist just to avoid the occasional ConcurrentModificationException
5287+ return new ArrayList<>(source.rules).stream().filter(rule -> rule.selectors.stream()
5288+ .anyMatch(selector -> base.equals(selector.getBase()) && subpart.equals(selector.getSubpart().getId(null))))
5289+ .findAny().orElse(null);
5290+ }
5291+}
5292diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java
5293new file mode 100644
5294index 000000000..500b5f8b5
5295--- /dev/null
5296+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java
5297@@ -0,0 +1,188 @@
5298+// License: GPL. For details, see LICENSE file.
5299+package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
5300+
5301+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
5302+import static org.junit.jupiter.api.Assertions.assertEquals;
5303+import static org.junit.jupiter.api.Assertions.assertNull;
5304+import static org.junit.jupiter.api.Assertions.assertThrows;
5305+import static org.junit.jupiter.api.Assertions.assertTrue;
5306+
5307+
5308+import java.util.Locale;
5309+import java.util.stream.Collectors;
5310+import java.util.stream.Stream;
5311+
5312+import javax.json.Json;
5313+import javax.json.JsonObject;
5314+import javax.json.JsonValue;
5315+
5316+import org.openstreetmap.josm.data.Bounds;
5317+
5318+import nl.jqno.equalsverifier.EqualsVerifier;
5319+import org.junit.jupiter.api.Test;
5320+
5321+/**
5322+ * Test class for {@link Source}
5323+ * @author Taylor Smock
5324+ * @since xxx
5325+ */
5326+public class SourceTest {
5327+ @Test
5328+ void testEquals() {
5329+ EqualsVerifier.forClass(Source.class).usingGetClass().verify();
5330+ }
5331+
5332+ @Test
5333+ void testSimpleSources() {
5334+ final JsonObject emptyObject = Json.createObjectBuilder().build();
5335+ assertThrows(NullPointerException.class, () -> new Source("Test source", emptyObject));
5336+
5337+ final JsonObject badTypeValue = Json.createObjectBuilder().add("type", "bad type value").build();
5338+ assertThrows(IllegalArgumentException.class, () -> new Source("Test source", badTypeValue));
5339+
5340+ // Only SourceType.{VECTOR,RASTER} are supported
5341+ final SourceType[] supported = new SourceType[] {SourceType.VECTOR, SourceType.RASTER};
5342+ for (SourceType type : supported) {
5343+ final JsonObject goodSourceType = Json.createObjectBuilder().add("type", type.toString().toLowerCase(Locale.ROOT)).build();
5344+ Source source = assertDoesNotThrow(() -> new Source(type.name(), goodSourceType));
5345+ // Check defaults
5346+ assertEquals(0, source.getMinZoom());
5347+ assertEquals(22, source.getMaxZoom());
5348+ assertEquals(type.name(), source.getName());
5349+ assertNull(source.getAttributionText());
5350+ assertTrue(source.getUrls().isEmpty());
5351+ assertEquals(new Bounds(-85.051129, -180, 85.051129, 180), source.getBounds());
5352+ }
5353+
5354+ // Check that unsupported types throw
5355+ for (SourceType type : Stream.of(SourceType.values()).filter(t -> Stream.of(supported).noneMatch(t::equals)).collect(
5356+ Collectors.toList())) {
5357+ final JsonObject goodSourceType = Json.createObjectBuilder().add("type", type.toString().toLowerCase(Locale.ROOT)).build();
5358+ assertThrows(UnsupportedOperationException.class, () -> new Source(type.name(), goodSourceType));
5359+ }
5360+ }
5361+
5362+ @Test
5363+ void testTileJsonSpec() {
5364+ // This isn't currently implemented, so it should throw. Mostly here to remind implementor to add tests...
5365+ final JsonObject tileJsonSpec = Json.createObjectBuilder()
5366+ .add("type", SourceType.VECTOR.name()).add("url", "some-random-url.com")
5367+ .build();
5368+ assertThrows(UnsupportedOperationException.class, () -> new Source("Test TileJson", tileJsonSpec));
5369+ }
5370+
5371+ @Test
5372+ void testBounds() {
5373+ // Check a "good" bounds
5374+ final JsonObject tileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("bounds",
5375+ Json.createArrayBuilder().add(-1).add(-2).add(3).add(4)).build();
5376+ Source source = new Source("Test Bounds[-1, -2, 3, 4]", tileJsonSpec);
5377+ assertEquals(new Bounds(-2, -1, 4, 3), source.getBounds());
5378+
5379+ // Check "bad" bounds
5380+ final JsonObject tileJsonSpecShort = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("bounds",
5381+ Json.createArrayBuilder().add(-1).add(-2).add(3)).build();
5382+ IllegalArgumentException badLengthException = assertThrows(IllegalArgumentException.class,
5383+ () -> new Source("Test Bounds[-1, -2, 3]", tileJsonSpecShort));
5384+ assertEquals("bounds must have four values, but has 3", badLengthException.getMessage());
5385+
5386+ final JsonObject tileJsonSpecLong = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("bounds",
5387+ Json.createArrayBuilder().add(-1).add(-2).add(3).add(4).add(5)).build();
5388+ badLengthException = assertThrows(IllegalArgumentException.class, () -> new Source("Test Bounds[-1, -2, 3, 4, 5]", tileJsonSpecLong));
5389+ assertEquals("bounds must have four values, but has 5", badLengthException.getMessage());
5390+ }
5391+
5392+ @Test
5393+ void testTiles() {
5394+ // No url
5395+ final JsonObject tileJsonSpecEmpty = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles",
5396+ JsonValue.NULL).build();
5397+ Source source = new Source("Test Tile[]", tileJsonSpecEmpty);
5398+ assertTrue(source.getUrls().isEmpty());
5399+
5400+ // Create a tile URL
5401+ final JsonObject tileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles",
5402+ Json.createArrayBuilder().add("https://example.org/{bbox-epsg-3857}")).build();
5403+ source = new Source("Test Tile[https://example.org/{bbox-epsg-3857}]", tileJsonSpec);
5404+ assertEquals(1, source.getUrls().size());
5405+ // Make certain that {bbox-epsg-3857} is replaced with the JOSM equivalent
5406+ assertEquals("https://example.org/{bbox}", source.getUrls().get(0));
5407+
5408+ // Check with invalid data
5409+ final JsonObject tileJsonSpecBad = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles",
5410+ Json.createArrayBuilder().add(1).add("https://example.org/{bbox-epsg-3857}").add(false).add(Json.createArrayBuilder().add("hello"))
5411+ .add(Json.createObjectBuilder().add("bad", "array"))).build();
5412+ source = new Source("Test Tile[1, https://example.org/{bbox-epsg-3857}, false, [\"hello\"], {\"bad\": \"array\"}]", tileJsonSpecBad);
5413+ assertEquals(1, source.getUrls().size());
5414+ // Make certain that {bbox-epsg-3857} is replaced with the JOSM equivalent
5415+ assertEquals("https://example.org/{bbox}", source.getUrls().get(0));
5416+ }
5417+
5418+ @Test
5419+ void testZoom() {
5420+ // Min zoom
5421+ final JsonObject minZoom5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("minzoom",
5422+ 5).build();
5423+ Source source = new Source("Test Zoom[minzoom=5]", minZoom5);
5424+ assertEquals(5, source.getMinZoom());
5425+ assertEquals(22, source.getMaxZoom());
5426+
5427+ // Negative min zoom
5428+ final JsonObject minZoomNeg1 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("minzoom",
5429+ -1).build();
5430+ source = new Source("Test Zoom[minzoom=-1]", minZoomNeg1);
5431+ assertEquals(0, source.getMinZoom());
5432+ assertEquals(22, source.getMaxZoom());
5433+
5434+ // Max zoom
5435+ final JsonObject maxZoom5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom",
5436+ 5).build();
5437+ source = new Source("Test Zoom[maxzoom=5]", maxZoom5);
5438+ assertEquals(0, source.getMinZoom());
5439+ assertEquals(5, source.getMaxZoom());
5440+
5441+ // Big Max zoom
5442+ final JsonObject maxZoom31 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom",
5443+ 31).build();
5444+ source = new Source("Test Zoom[maxzoom=31]", maxZoom31);
5445+ assertEquals(0, source.getMinZoom());
5446+ assertEquals(30, source.getMaxZoom());
5447+
5448+ // Negative max zoom
5449+ final JsonObject maxZoomNeg5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom",
5450+ -5).build();
5451+ source = new Source("Test Zoom[maxzoom=-5]", maxZoomNeg5);
5452+ assertEquals(0, source.getMinZoom());
5453+ assertEquals(0, source.getMaxZoom());
5454+
5455+ // Min max zoom
5456+ final JsonObject minZoom1MaxZoom5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom",
5457+ 5).add("minzoom", 1).build();
5458+ source = new Source("Test Zoom[minzoom=1,maxzoom=5]", minZoom1MaxZoom5);
5459+ assertEquals(1, source.getMinZoom());
5460+ assertEquals(5, source.getMaxZoom());
5461+ }
5462+
5463+ @Test
5464+ void testToString() {
5465+ // Simple (no urls)
5466+ final JsonObject noTileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).build();
5467+ Source source = new Source("Test String[]", noTileJsonSpec);
5468+ assertEquals("Test String[]", source.toString());
5469+
5470+ // With one url
5471+ final JsonObject tileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles",
5472+ Json.createArrayBuilder().add("https://example.org/{bbox-epsg-3857}")).build();
5473+ source = new Source("Test String[https://example.org/{bbox-epsg-3857}]", tileJsonSpec);
5474+ assertEquals("Test String[https://example.org/{bbox-epsg-3857}] https://example.org/{bbox}", source.toString());
5475+
5476+ // With two URLs
5477+ final JsonObject tileJsonSpecMultiple = Json.createObjectBuilder().add("type", SourceType.VECTOR.name())
5478+ .add("tiles", Json.createArrayBuilder()
5479+ .add("https://example.org/{bbox-epsg-3857}")
5480+ .add("https://example.com/{bbox-epsg-3857}")).build();
5481+ source = new Source("Test String[https://example.org/{bbox-epsg-3857},https://example.com/{bbox-epsg-3857}]", tileJsonSpecMultiple);
5482+ assertEquals("Test String[https://example.org/{bbox-epsg-3857},https://example.com/{bbox-epsg-3857}] https://example.org/{bbox} "
5483+ + "https://example.com/{bbox}", source.toString());
5484+ }
5485+}
5486--
5487GitLab
5488
5489
5490From f8f62e7ec24b189bfab536c45d6b3d2c2f97c70a Mon Sep 17 00:00:00 2001
5491From: Taylor Smock <tsmock@fb.com>
5492Date: Thu, 8 Apr 2021 15:56:16 -0600
5493Subject: [PATCH 05/50] Vector data storage
5494
5495Signed-off-by: Taylor Smock <tsmock@fb.com>
5496---
5497 .../josm/data/vector/DataLayer.java | 23 +
5498 .../josm/data/vector/DataStore.java | 126 ++++
5499 .../josm/data/vector/VectorDataSet.java | 541 ++++++++++++++++++
5500 .../josm/data/vector/VectorDataStore.java | 354 ++++++++++++
5501 .../josm/data/vector/VectorNode.java | 113 ++++
5502 .../josm/data/vector/VectorPrimitive.java | 256 +++++++++
5503 .../josm/data/vector/VectorRelation.java | 114 ++++
5504 .../data/vector/VectorRelationMember.java | 70 +++
5505 .../josm/data/vector/VectorWay.java | 132 +++++
5506 .../josm/data/vector/VectorDataSetTest.java | 141 +++++
5507 .../josm/data/vector/VectorNodeTest.java | 153 +++++
5508 .../josm/data/vector/VectorRelationTest.java | 45 ++
5509 .../josm/data/vector/VectorWayTest.java | 117 ++++
5510 13 files changed, 2185 insertions(+)
5511 create mode 100644 src/org/openstreetmap/josm/data/vector/DataLayer.java
5512 create mode 100644 src/org/openstreetmap/josm/data/vector/DataStore.java
5513 create mode 100644 src/org/openstreetmap/josm/data/vector/VectorDataSet.java
5514 create mode 100644 src/org/openstreetmap/josm/data/vector/VectorDataStore.java
5515 create mode 100644 src/org/openstreetmap/josm/data/vector/VectorNode.java
5516 create mode 100644 src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
5517 create mode 100644 src/org/openstreetmap/josm/data/vector/VectorRelation.java
5518 create mode 100644 src/org/openstreetmap/josm/data/vector/VectorRelationMember.java
5519 create mode 100644 src/org/openstreetmap/josm/data/vector/VectorWay.java
5520 create mode 100644 test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
5521 create mode 100644 test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java
5522 create mode 100644 test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java
5523 create mode 100644 test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java
5524
5525diff --git a/src/org/openstreetmap/josm/data/vector/DataLayer.java b/src/org/openstreetmap/josm/data/vector/DataLayer.java
5526new file mode 100644
5527index 000000000..9052e5b1a
5528--- /dev/null
5529+++ b/src/org/openstreetmap/josm/data/vector/DataLayer.java
5530@@ -0,0 +1,23 @@
5531+// License: GPL. For details, see LICENSE file.
5532+package org.openstreetmap.josm.data.vector;
5533+
5534+/**
5535+ * An interface for objects that are part of a data layer
5536+ * @param <T> The type used to identify a layer, typically a string
5537+ */
5538+public interface DataLayer<T> {
5539+ /**
5540+ * Get the layer
5541+ * @return The layer
5542+ */
5543+ T getLayer();
5544+
5545+ /**
5546+ * Set the layer
5547+ * @param layer The layer to set
5548+ * @return {@code true} if the layer was set -- some objects may never change layers.
5549+ */
5550+ default boolean setLayer(T layer) {
5551+ return layer != null && layer.equals(getLayer());
5552+ }
5553+}
5554diff --git a/src/org/openstreetmap/josm/data/vector/DataStore.java b/src/org/openstreetmap/josm/data/vector/DataStore.java
5555new file mode 100644
5556index 000000000..9de044f62
5557--- /dev/null
5558+++ b/src/org/openstreetmap/josm/data/vector/DataStore.java
5559@@ -0,0 +1,126 @@
5560+// License: GPL. For details, see LICENSE file.
5561+package org.openstreetmap.josm.data.vector;
5562+
5563+import java.util.Collection;
5564+import java.util.Collections;
5565+import java.util.HashMap;
5566+import java.util.HashSet;
5567+import java.util.LinkedList;
5568+import java.util.Map;
5569+import java.util.Set;
5570+import java.util.concurrent.locks.ReentrantReadWriteLock;
5571+
5572+import org.openstreetmap.gui.jmapviewer.Tile;
5573+import org.openstreetmap.josm.data.DataSource;
5574+import org.openstreetmap.josm.data.osm.DataSet;
5575+import org.openstreetmap.josm.data.osm.INode;
5576+import org.openstreetmap.josm.data.osm.IPrimitive;
5577+import org.openstreetmap.josm.data.osm.IRelation;
5578+import org.openstreetmap.josm.data.osm.IWay;
5579+import org.openstreetmap.josm.data.osm.PrimitiveId;
5580+import org.openstreetmap.josm.data.osm.QuadBucketPrimitiveStore;
5581+import org.openstreetmap.josm.data.osm.Storage;
5582+
5583+/**
5584+ * A class that stores data (essentially a simple {@link DataSet})
5585+ * @author Taylor Smock
5586+ * @since xxx
5587+ */
5588+class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> {
5589+ /**
5590+ * This literally only exists to make {@link QuadBucketPrimitiveStore#removePrimitive} public
5591+ *
5592+ * @param <N> The node type
5593+ * @param <W> The way type
5594+ * @param <R> The relation type
5595+ */
5596+ static class LocalQuadBucketPrimitiveStore<N extends INode, W extends IWay<N>, R extends IRelation<?>>
5597+ extends QuadBucketPrimitiveStore<N, W, R> {
5598+ // Allow us to remove primitives (protected in {@link QuadBucketPrimitiveStore})
5599+ @Override
5600+ public void removePrimitive(IPrimitive primitive) {
5601+ super.removePrimitive(primitive);
5602+ }
5603+ }
5604+
5605+ protected final int zoom;
5606+ protected final LocalQuadBucketPrimitiveStore<N, W, R> store = new LocalQuadBucketPrimitiveStore<>();
5607+ protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true);
5608+ protected final Set<Tile> addedTiles = new HashSet<>();
5609+ protected final Map<PrimitiveId, O> primitivesMap = allPrimitives
5610+ .foreignKey(new Storage.PrimitiveIdHash());
5611+ protected final Collection<DataSource> dataSources = new LinkedList<>();
5612+ private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
5613+
5614+ DataStore(int zoom) {
5615+ this.zoom = zoom;
5616+ }
5617+
5618+ public int getZoom() {
5619+ return this.zoom;
5620+ }
5621+
5622+ public QuadBucketPrimitiveStore<N, W, R> getStore() {
5623+ return this.store;
5624+ }
5625+
5626+ public Storage<O> getAllPrimitives() {
5627+ return this.allPrimitives;
5628+ }
5629+
5630+ public Map<PrimitiveId, O> getPrimitivesMap() {
5631+ if (this.readWriteLock.isWriteLocked()) {
5632+ return new HashMap<>(this.primitivesMap);
5633+ }
5634+ return this.primitivesMap;
5635+ }
5636+
5637+ public Collection<DataSource> getDataSources() {
5638+ return Collections.unmodifiableCollection(dataSources);
5639+ }
5640+
5641+ /**
5642+ * Add a datasource to this data set
5643+ * @param dataSource The data soure to add
5644+ */
5645+ public void addDataSource(DataSource dataSource) {
5646+ this.dataSources.add(dataSource);
5647+ }
5648+
5649+ /**
5650+ * Add a primitive to this dataset
5651+ * @param primitive The primitive to remove
5652+ */
5653+ @SuppressWarnings("squid:S2445")
5654+ protected void removePrimitive(O primitive) {
5655+ if (primitive == null) {
5656+ return;
5657+ }
5658+ // This is deliberate -- attempting to remove the primitive twice causes issues
5659+ synchronized (primitive) {
5660+ if (this.allPrimitives.contains(primitive)) {
5661+ this.store.removePrimitive(primitive);
5662+ this.allPrimitives.remove(primitive);
5663+ this.primitivesMap.remove(primitive.getPrimitiveId());
5664+ }
5665+ }
5666+ }
5667+
5668+ /**
5669+ * Add a primitive to this dataset
5670+ * @param primitive The primitive to add
5671+ */
5672+ protected void addPrimitive(O primitive) {
5673+ this.store.addPrimitive(primitive);
5674+ this.allPrimitives.add(primitive);
5675+ this.primitivesMap.put(primitive.getPrimitiveId(), primitive);
5676+ }
5677+
5678+ /**
5679+ * Get the read/write lock for this dataset
5680+ * @return The read/write lock
5681+ */
5682+ protected ReentrantReadWriteLock getReadWriteLock() {
5683+ return this.readWriteLock;
5684+ }
5685+}
5686diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
5687new file mode 100644
5688index 000000000..dfa9334a3
5689--- /dev/null
5690+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
5691@@ -0,0 +1,541 @@
5692+// License: GPL. For details, see LICENSE file.
5693+package org.openstreetmap.josm.data.vector;
5694+
5695+import java.util.ArrayList;
5696+import java.util.Arrays;
5697+import java.util.Collection;
5698+import java.util.Collections;
5699+import java.util.HashSet;
5700+import java.util.List;
5701+import java.util.Map;
5702+import java.util.Objects;
5703+import java.util.Optional;
5704+import java.util.concurrent.ConcurrentHashMap;
5705+import java.util.concurrent.locks.Lock;
5706+import java.util.concurrent.locks.ReentrantReadWriteLock;
5707+import java.util.function.Predicate;
5708+import java.util.function.Supplier;
5709+import java.util.stream.Collectors;
5710+import java.util.stream.IntStream;
5711+import java.util.stream.Stream;
5712+
5713+import org.openstreetmap.gui.jmapviewer.Tile;
5714+import org.openstreetmap.josm.data.DataSource;
5715+import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
5716+import org.openstreetmap.josm.data.osm.BBox;
5717+import org.openstreetmap.josm.data.osm.DataSelectionListener;
5718+import org.openstreetmap.josm.data.osm.DownloadPolicy;
5719+import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
5720+import org.openstreetmap.josm.data.osm.OsmData;
5721+import org.openstreetmap.josm.data.osm.PrimitiveId;
5722+import org.openstreetmap.josm.data.osm.UploadPolicy;
5723+import org.openstreetmap.josm.data.osm.WaySegment;
5724+import org.openstreetmap.josm.gui.mappaint.ElemStyles;
5725+import org.openstreetmap.josm.tools.ListenerList;
5726+import org.openstreetmap.josm.tools.Logging;
5727+import org.openstreetmap.josm.tools.SubclassFilteredCollection;
5728+
5729+/**
5730+ * A data class for Vector Data
5731+ *
5732+ * @author Taylor Smock
5733+ * @since xxx
5734+ */
5735+public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation> {
5736+ // Note: In Java 8, computeIfAbsent is blocking for both pre-existing and new values. In Java 9, it is only blocking
5737+ // for new values (perf increase). See JDK-8161372 for more info.
5738+ private final Map<Integer, VectorDataStore> dataStoreMap = new ConcurrentHashMap<>();
5739+ private final Collection<PrimitiveId> selected = new HashSet<>();
5740+ // Both of these listener lists are useless, since they expect OsmPrimitives at this time
5741+ private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create();
5742+ private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create();
5743+ private boolean lock = true;
5744+ private String name;
5745+ private short mappaintCacheIdx = 1;
5746+
5747+ private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
5748+
5749+ /**
5750+ * The distance to consider nodes duplicates -- mostly a memory saving measure.
5751+ * 0.000_000_1 ~1.2 cm (+- 5.57 mm)
5752+ * Descriptions from <a href="https://xkcd.com/2170/">https://xkcd.com/2170/</a>
5753+ * Notes on <a href="https://wiki.openstreetmap.org/wiki/Node">https://wiki.openstreetmap.org/wiki/Node</a> indicate
5754+ * that IEEE 32-bit floats should not be used at high longitude (0.000_01 precision)
5755+ */
5756+ protected static final float DUPE_NODE_DISTANCE = 0.000_000_1f;
5757+
5758+ /**
5759+ * The current zoom we are getting/adding to
5760+ */
5761+ private int zoom;
5762+ /**
5763+ * Default to normal download policy
5764+ */
5765+ private DownloadPolicy downloadPolicy = DownloadPolicy.NORMAL;
5766+ /**
5767+ * Default to a blocked upload policy
5768+ */
5769+ private UploadPolicy uploadPolicy = UploadPolicy.BLOCKED;
5770+ /**
5771+ * The paint style for this layer
5772+ */
5773+ private ElemStyles styles;
5774+
5775+ @Override
5776+ public Collection<DataSource> getDataSources() {
5777+ final int currentZoom = this.zoom;
5778+ final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
5779+ return dataStore.getDataSources();
5780+ }
5781+
5782+ /**
5783+ * Add a data source
5784+ *
5785+ * @param currentZoom the zoom
5786+ * @param dataSource The datasource to add at the zoom level
5787+ */
5788+ public void addDataSource(int currentZoom, DataSource dataSource) {
5789+ final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
5790+ dataStore.addDataSource(dataSource);
5791+ }
5792+
5793+ @Override
5794+ public void lock() {
5795+ this.lock = true;
5796+ }
5797+
5798+ @Override
5799+ public void unlock() {
5800+ this.lock = false;
5801+ }
5802+
5803+ @Override
5804+ public boolean isLocked() {
5805+ return this.lock;
5806+ }
5807+
5808+ @Override
5809+ public String getVersion() {
5810+ return "8"; // TODO get this dynamically. Not critical, as this is currently the _only_ version.
5811+ }
5812+
5813+ @Override
5814+ public String getName() {
5815+ return this.name;
5816+ }
5817+
5818+ @Override
5819+ public void setName(String name) {
5820+ this.name = name;
5821+ }
5822+
5823+ @Override
5824+ public void addPrimitive(VectorPrimitive primitive) {
5825+ primitive.setDataSet(this);
5826+ final int currentZoom = this.zoom;
5827+ final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
5828+ tryWrite(dataStore, () -> dataStore.addPrimitive(primitive));
5829+ }
5830+
5831+ /**
5832+ * Remove a primitive from this dataset
5833+ *
5834+ * @param primitive The primitive to remove
5835+ */
5836+ protected void removePrimitive(VectorPrimitive primitive) {
5837+ if (primitive.getDataSet() == this) {
5838+ primitive.setDataSet(null);
5839+ this.dataStoreMap.values()
5840+ .forEach(vectorDataStore -> tryWrite(vectorDataStore, () -> vectorDataStore.removePrimitive(primitive)));
5841+ }
5842+ }
5843+
5844+ @Override
5845+ public void clear() {
5846+ synchronized (this.dataStoreMap) {
5847+ this.dataStoreMap.clear();
5848+ }
5849+ }
5850+
5851+ @Override
5852+ public List<VectorNode> searchNodes(BBox bbox) {
5853+ return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchNodes(bbox))
5854+ .orElseGet(Collections::emptyList);
5855+ }
5856+
5857+ @Override
5858+ public boolean containsNode(VectorNode vectorNode) {
5859+ return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsNode(vectorNode)).orElse(false);
5860+ }
5861+
5862+ @Override
5863+ public List<VectorWay> searchWays(BBox bbox) {
5864+ return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchWays(bbox))
5865+ .orElseGet(Collections::emptyList);
5866+ }
5867+
5868+ @Override
5869+ public boolean containsWay(VectorWay vectorWay) {
5870+ return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsWay(vectorWay)).orElse(false);
5871+ }
5872+
5873+ @Override
5874+ public List<VectorRelation> searchRelations(BBox bbox) {
5875+ return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchRelations(bbox))
5876+ .orElseGet(Collections::emptyList);
5877+ }
5878+
5879+ @Override
5880+ public boolean containsRelation(VectorRelation vectorRelation) {
5881+ return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsRelation(vectorRelation)).orElse(false);
5882+ }
5883+
5884+ @Override
5885+ public VectorPrimitive getPrimitiveById(PrimitiveId primitiveId) {
5886+ return this.getBestZoomDataStore().map(VectorDataStore::getPrimitivesMap).map(m -> m .get(primitiveId)).orElse(null);
5887+ }
5888+
5889+ // The last return statement is "unchecked", even though it is literally the same as the previous return, except
5890+ // as an optional.
5891+ @SuppressWarnings("unchecked")
5892+ @Override
5893+ public <T extends VectorPrimitive> Collection<T> getPrimitives(
5894+ Predicate<? super VectorPrimitive> predicate) {
5895+ final VectorDataStore dataStore = this.getBestZoomDataStore().orElse(null);
5896+ if (dataStore == null) {
5897+ return Collections.emptyList();
5898+ }
5899+
5900+ if (dataStore.getReadWriteLock().isWriteLocked()) {
5901+ return new SubclassFilteredCollection<>(new HashSet<>(dataStore.getAllPrimitives()), predicate);
5902+ }
5903+ return (Collection<T>) tryRead(dataStore, () -> new SubclassFilteredCollection<>(dataStore.getAllPrimitives(), predicate))
5904+ // Throw an NPE if we don't have a collection (this should never happen, so if it does, _something_ is wrong)
5905+ .orElseThrow(NullPointerException::new);
5906+ }
5907+
5908+ @Override
5909+ public Collection<VectorNode> getNodes() {
5910+ return this.getPrimitives(VectorNode.class::isInstance);
5911+ }
5912+
5913+ @Override
5914+ public Collection<VectorWay> getWays() {
5915+ return this.getPrimitives(VectorWay.class::isInstance);
5916+ }
5917+
5918+ @Override
5919+ public Collection<VectorRelation> getRelations() {
5920+ return this.getPrimitives(VectorRelation.class::isInstance);
5921+ }
5922+
5923+ @Override
5924+ public DownloadPolicy getDownloadPolicy() {
5925+ return this.downloadPolicy;
5926+ }
5927+
5928+ @Override
5929+ public void setDownloadPolicy(DownloadPolicy downloadPolicy) {
5930+ this.downloadPolicy = downloadPolicy;
5931+ }
5932+
5933+ @Override
5934+ public UploadPolicy getUploadPolicy() {
5935+ return this.uploadPolicy;
5936+ }
5937+
5938+ @Override
5939+ public void setUploadPolicy(UploadPolicy uploadPolicy) {
5940+ this.uploadPolicy = uploadPolicy;
5941+ }
5942+
5943+ /**
5944+ * Get the current Read/Write lock
5945+ * @implNote This changes based off of zoom level. Please do not use this in a finally block
5946+ * @return The current read/write lock
5947+ */
5948+ @Override
5949+ public Lock getReadLock() {
5950+ return getBestZoomDataStore().map(VectorDataStore::getReadWriteLock).map(ReentrantReadWriteLock::readLock)
5951+ .orElse(this.readWriteLock.readLock());
5952+ }
5953+
5954+ @Override
5955+ public Collection<WaySegment> getHighlightedVirtualNodes() {
5956+ // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
5957+ return Collections.emptyList();
5958+ }
5959+
5960+ @Override
5961+ public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) {
5962+ // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
5963+ }
5964+
5965+ @Override
5966+ public Collection<WaySegment> getHighlightedWaySegments() {
5967+ // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
5968+ return Collections.emptyList();
5969+ }
5970+
5971+ @Override
5972+ public void setHighlightedWaySegments(Collection<WaySegment> waySegments) {
5973+ // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
5974+ }
5975+
5976+ @Override
5977+ public void addHighlightUpdateListener(HighlightUpdateListener listener) {
5978+ this.highlightUpdateListenerListenerList.addListener(listener);
5979+ }
5980+
5981+ @Override
5982+ public void removeHighlightUpdateListener(HighlightUpdateListener listener) {
5983+ this.highlightUpdateListenerListenerList.removeListener(listener);
5984+ }
5985+
5986+ @Override
5987+ public Collection<VectorPrimitive> getAllSelected() {
5988+ final Optional<VectorDataStore> dataStore = this.getBestZoomDataStore();
5989+ return dataStore.map(vectorDataStore -> vectorDataStore.getAllPrimitives().stream()
5990+ .filter(primitive -> this.selected.contains(primitive.getPrimitiveId()))
5991+ .collect(Collectors.toList())).orElse(Collections.emptyList());
5992+ }
5993+
5994+ /**
5995+ * Get the best zoom datastore
5996+ * @return A datastore with data, or {@code null} if no good datastore exists.
5997+ */
5998+ private Optional<VectorDataStore> getBestZoomDataStore() {
5999+ final int currentZoom = this.zoom;
6000+ if (this.dataStoreMap.containsKey(currentZoom)) {
6001+ return Optional.of(this.dataStoreMap.get(currentZoom));
6002+ }
6003+ // Check up to two zooms higher (may cause perf hit)
6004+ for (int tZoom = currentZoom + 1; tZoom < currentZoom + 3; tZoom++) {
6005+ if (this.dataStoreMap.containsKey(tZoom)) {
6006+ return Optional.of(this.dataStoreMap.get(tZoom));
6007+ }
6008+ }
6009+ // Return *any* lower zoom data (shouldn't cause a perf hit...)
6010+ for (int tZoom = currentZoom - 1; tZoom >= 0; tZoom--) {
6011+ if (this.dataStoreMap.containsKey(tZoom)) {
6012+ return Optional.of(this.dataStoreMap.get(tZoom));
6013+ }
6014+ }
6015+ // Check higher level zooms. May cause perf issues if selected datastore has a lot of data.
6016+ for (int tZoom = currentZoom + 3; tZoom < 34; tZoom++) {
6017+ if (this.dataStoreMap.containsKey(tZoom)) {
6018+ return Optional.of(this.dataStoreMap.get(tZoom));
6019+ }
6020+ }
6021+ return Optional.empty();
6022+ }
6023+
6024+ @Override
6025+ public boolean selectionEmpty() {
6026+ return this.selected.isEmpty();
6027+ }
6028+
6029+ @Override
6030+ public boolean isSelected(VectorPrimitive osm) {
6031+ return this.selected.contains(osm.getPrimitiveId());
6032+ }
6033+
6034+ @Override
6035+ public void toggleSelected(Collection<? extends PrimitiveId> osm) {
6036+ this.toggleSelectedImpl(osm.stream());
6037+ }
6038+
6039+ @Override
6040+ public void toggleSelected(PrimitiveId... osm) {
6041+ this.toggleSelectedImpl(Stream.of(osm));
6042+ }
6043+
6044+ private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) {
6045+ osm.forEach(primitiveId -> {
6046+ if (this.selected.contains(primitiveId)) {
6047+ this.selected.remove(primitiveId);
6048+ } else {
6049+ this.selected.add(primitiveId);
6050+ }
6051+ });
6052+ }
6053+
6054+ @Override
6055+ public void setSelected(Collection<? extends PrimitiveId> selection) {
6056+ this.setSelectedImpl(selection.stream());
6057+ }
6058+
6059+ @Override
6060+ public void setSelected(PrimitiveId... osm) {
6061+ this.setSelectedImpl(Stream.of(osm));
6062+ }
6063+
6064+ private void setSelectedImpl(Stream<? extends PrimitiveId> osm) {
6065+ this.selected.clear();
6066+ osm.forEach(this.selected::add);
6067+ }
6068+
6069+ @Override
6070+ public void addSelected(Collection<? extends PrimitiveId> selection) {
6071+ this.addSelectedImpl(selection.stream());
6072+ }
6073+
6074+ @Override
6075+ public void addSelected(PrimitiveId... osm) {
6076+ this.addSelectedImpl(Stream.of(osm));
6077+ }
6078+
6079+ private void addSelectedImpl(Stream<? extends PrimitiveId> osm) {
6080+ osm.forEach(this.selected::add);
6081+ }
6082+
6083+ @Override
6084+ public void clearSelection(PrimitiveId... osm) {
6085+ this.clearSelectionImpl(Stream.of(osm));
6086+ }
6087+
6088+ @Override
6089+ public void clearSelection(Collection<? extends PrimitiveId> list) {
6090+ this.clearSelectionImpl(list.stream());
6091+ }
6092+
6093+ @Override
6094+ public void clearSelection() {
6095+ this.clearSelectionImpl(new ArrayList<>(this.selected).stream());
6096+ }
6097+
6098+ private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) {
6099+ osm.forEach(this.selected::remove);
6100+ }
6101+
6102+ @Override
6103+ public void addSelectionListener(DataSelectionListener listener) {
6104+ this.dataSelectionListenerListenerList.addListener(listener);
6105+ }
6106+
6107+ @Override
6108+ public void removeSelectionListener(DataSelectionListener listener) {
6109+ this.dataSelectionListenerListenerList.removeListener(listener);
6110+ }
6111+
6112+ public short getMappaintCacheIndex() {
6113+ return this.mappaintCacheIdx;
6114+ }
6115+
6116+ @Override
6117+ public void clearMappaintCache() {
6118+ this.mappaintCacheIdx++;
6119+ }
6120+
6121+ public void setZoom(int zoom) {
6122+ if (zoom == this.zoom) {
6123+ return; // Do nothing -- zoom isn't actually changing
6124+ }
6125+ this.zoom = zoom;
6126+ this.clearMappaintCache();
6127+ final int[] nearestZoom = {-1, -1, -1, -1};
6128+ nearestZoom[0] = zoom;
6129+ // Create a new list to avoid concurrent modification issues
6130+ synchronized (this.dataStoreMap) {
6131+ final int[] keys = new ArrayList<>(this.dataStoreMap.keySet()).stream().filter(Objects::nonNull)
6132+ .mapToInt(Integer::intValue).sorted().toArray();
6133+ final int index;
6134+ if (this.dataStoreMap.containsKey(zoom)) {
6135+ index = Arrays.binarySearch(keys, zoom);
6136+ } else {
6137+ // (-(insertion point) - 1) = return -> insertion point = -(return + 1)
6138+ index = -(Arrays.binarySearch(keys, zoom) + 1);
6139+ }
6140+ if (index > 0) {
6141+ nearestZoom[1] = keys[index - 1];
6142+ }
6143+ if (index < keys.length - 2) {
6144+ nearestZoom[2] = keys[index + 1];
6145+ }
6146+
6147+ nearestZoom[3] = this.getBestZoomDataStore().map(VectorDataStore::getZoom).orElse(-1);
6148+ IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key))
6149+ .mapToObj(this.dataStoreMap::get).forEach(VectorDataStore::destroy);
6150+ IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key))
6151+ .forEach(this.dataStoreMap::remove);
6152+ }
6153+ }
6154+
6155+ public int getZoom() {
6156+ return this.zoom;
6157+ }
6158+
6159+ /**
6160+ * Add tile data to this dataset
6161+ * @param tile The tile to add
6162+ * @param <T> The tile type
6163+ */
6164+ public <T extends Tile & VectorTile> void addTileData(T tile) {
6165+ final int currentZoom = tile.getZoom();
6166+ // computeIfAbsent should be thread safe (ConcurrentHashMap indicates it is, anyway)
6167+ final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
6168+ tryWrite(dataStore, () -> dataStore.addTile(tile));
6169+ }
6170+
6171+ /**
6172+ * Try to read something (here to avoid boilerplate)
6173+ *
6174+ * @param supplier The reading function
6175+ * @param <T> The return type
6176+ * @return The optional return
6177+ */
6178+ private static <T> Optional<T> tryRead(VectorDataStore dataStore, Supplier<T> supplier) {
6179+ try {
6180+ dataStore.getReadWriteLock().readLock().lockInterruptibly();
6181+ return Optional.ofNullable(supplier.get());
6182+ } catch (InterruptedException e) {
6183+ Logging.error(e);
6184+ Thread.currentThread().interrupt();
6185+ } finally {
6186+ dataStore.getReadWriteLock().readLock().unlock();
6187+ }
6188+ return Optional.empty();
6189+ }
6190+
6191+ /**
6192+ * Try to write something (here to avoid boilerplate)
6193+ *
6194+ * @param runnable The writing function
6195+ */
6196+ private static void tryWrite(VectorDataStore dataStore, Runnable runnable) {
6197+ try {
6198+ dataStore.getReadWriteLock().writeLock().lockInterruptibly();
6199+ runnable.run();
6200+ } catch (InterruptedException e) {
6201+ Logging.error(e);
6202+ Thread.currentThread().interrupt();
6203+ } finally {
6204+ if (dataStore.getReadWriteLock().isWriteLockedByCurrentThread()) {
6205+ dataStore.getReadWriteLock().writeLock().unlock();
6206+ }
6207+ }
6208+ }
6209+
6210+ /**
6211+ * Get the styles for this layer
6212+ *
6213+ * @return The styles
6214+ */
6215+ public ElemStyles getStyles() {
6216+ return this.styles;
6217+ }
6218+
6219+ /**
6220+ * Set the styles for this layer
6221+ * @param styles The styles to set for this layer
6222+ */
6223+ public void setStyles(Collection<ElemStyles> styles) {
6224+ if (styles.size() == 1) {
6225+ this.styles = styles.iterator().next();
6226+ } else if (!styles.isEmpty()) {
6227+ this.styles = new ElemStyles(styles.stream().flatMap(style -> style.getStyleSources().stream()).collect(Collectors.toList()));
6228+ } else {
6229+ this.styles = null;
6230+ }
6231+ }
6232+}
6233diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
6234new file mode 100644
6235index 000000000..f486651b6
6236--- /dev/null
6237+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
6238@@ -0,0 +1,354 @@
6239+// License: GPL. For details, see LICENSE file.
6240+package org.openstreetmap.josm.data.vector;
6241+
6242+import java.awt.geom.Area;
6243+import java.awt.geom.Ellipse2D;
6244+import java.awt.geom.Path2D;
6245+import java.awt.geom.PathIterator;
6246+import java.util.ArrayList;
6247+import java.util.Collection;
6248+import java.util.Collections;
6249+import java.util.List;
6250+import java.util.Objects;
6251+import java.util.Optional;
6252+import java.util.stream.Collectors;
6253+
6254+import org.openstreetmap.gui.jmapviewer.Coordinate;
6255+import org.openstreetmap.gui.jmapviewer.Tile;
6256+import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
6257+import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
6258+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
6259+import org.openstreetmap.josm.data.osm.BBox;
6260+import org.openstreetmap.josm.data.osm.INode;
6261+import org.openstreetmap.josm.data.osm.IRelation;
6262+import org.openstreetmap.josm.data.osm.IWay;
6263+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
6264+import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
6265+import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
6266+import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter;
6267+import org.openstreetmap.josm.tools.Destroyable;
6268+import org.openstreetmap.josm.tools.Geometry;
6269+
6270+/**
6271+ * A data store for Vector Data sets
6272+ * @author Taylor Smock
6273+ * @since xxx
6274+ */
6275+class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> implements Destroyable {
6276+ private static final String JOSM_MERGE_TYPE_KEY = "josm_merge_type";
6277+ private final VectorDataSet dataSet;
6278+
6279+ VectorDataStore(VectorDataSet dataSet, int zoom) {
6280+ super(zoom);
6281+ this.dataSet = dataSet;
6282+ }
6283+
6284+ @Override
6285+ protected void addPrimitive(VectorPrimitive primitive) {
6286+ primitive.setDataSet(this.dataSet);
6287+ // The field is uint64, so we can use negative numbers to indicate that it is a "generated" object (e.g., nodes for ways)
6288+ if (primitive.getUniqueId() == 0) {
6289+ final UniqueIdGenerator generator = primitive.getIdGenerator();
6290+ long id;
6291+ do {
6292+ id = generator.generateUniqueId();
6293+ } while (this.primitivesMap.containsKey(new SimplePrimitiveId(id, primitive.getType())));
6294+ primitive.setId(primitive.getIdGenerator().generateUniqueId());
6295+ }
6296+ if (primitive instanceof VectorRelation && !primitive.isMultipolygon()) {
6297+ primitive = mergeWays((VectorRelation) primitive);
6298+ }
6299+ final VectorPrimitive alreadyAdded = this.primitivesMap.get(primitive.getPrimitiveId());
6300+ final VectorRelation mergedRelation = (VectorRelation) this.primitivesMap
6301+ .get(new SimplePrimitiveId(primitive.getPrimitiveId().getUniqueId(),
6302+ OsmPrimitiveType.RELATION));
6303+ if (alreadyAdded == null || alreadyAdded.equals(primitive)) {
6304+ super.addPrimitive(primitive);
6305+ } else if (mergedRelation != null && mergedRelation.get(JOSM_MERGE_TYPE_KEY) != null) {
6306+ mergedRelation.addRelationMember(new VectorRelationMember("", primitive));
6307+ super.addPrimitive(primitive);
6308+ // Check that all primitives can be merged
6309+ if (mergedRelation.getMemberPrimitivesList().stream().allMatch(IWay.class::isInstance)) {
6310+ // This pretty much does the "right" thing
6311+ this.mergeWays(mergedRelation);
6312+ } else if (!(primitive instanceof IWay)) {
6313+ // Can't merge, ever (one of the childs is a node/relation)
6314+ mergedRelation.remove(JOSM_MERGE_TYPE_KEY);
6315+ }
6316+ } else if (mergedRelation != null && primitive instanceof IRelation) {
6317+ // Just add to the relation
6318+ ((VectorRelation) primitive).getMembers().forEach(mergedRelation::addRelationMember);
6319+ } else if (alreadyAdded instanceof VectorWay && primitive instanceof VectorWay) {
6320+ final VectorRelation temporaryRelation =
6321+ mergedRelation == null ? new VectorRelation(primitive.getLayer()) : mergedRelation;
6322+ if (mergedRelation == null) {
6323+ temporaryRelation.put(JOSM_MERGE_TYPE_KEY, "merge");
6324+ temporaryRelation.addRelationMember(new VectorRelationMember("", alreadyAdded));
6325+ }
6326+ temporaryRelation.addRelationMember(new VectorRelationMember("", primitive));
6327+ temporaryRelation.setDataSet(this.dataSet);
6328+ super.addPrimitive(primitive);
6329+ super.addPrimitive(temporaryRelation);
6330+ }
6331+ }
6332+
6333+ private VectorPrimitive mergeWays(VectorRelation relation) {
6334+ List<VectorRelationMember> members = RelationSorter.sortMembersByConnectivity(relation.getMembers());
6335+ Collection<VectorWay> relationWayList = members.stream().map(VectorRelationMember::getMember)
6336+ .filter(VectorWay.class::isInstance)
6337+ .map(VectorWay.class::cast).collect(Collectors.toCollection(ArrayList::new));
6338+ // Only support way-only relations
6339+ if (relationWayList.size() != relation.getMemberPrimitivesList().size()) {
6340+ return relation;
6341+ }
6342+ List<VectorWay> wayList = new ArrayList<>(relation.getMembersCount());
6343+ // Assume that the order may not be correct, worst case O(n), best case O(n/2)
6344+ // Assume that the ways were drawn in order
6345+ final int maxIteration = relationWayList.size();
6346+ int iteration = 0;
6347+ while (iteration < maxIteration && wayList.size() < relationWayList.size()) {
6348+ for (VectorWay way : relationWayList) {
6349+ if (wayList.isEmpty()) {
6350+ wayList.add(way);
6351+ continue;
6352+ }
6353+ // Check first/last ways (last first, since the list *should* be sorted)
6354+ if (canMergeWays(wayList.get(wayList.size() - 1), way, false)) {
6355+ wayList.add(way);
6356+ } else if (canMergeWays(wayList.get(0), way, false)) {
6357+ wayList.add(0, way);
6358+ }
6359+ }
6360+ iteration++;
6361+ relationWayList.removeIf(wayList::contains);
6362+ }
6363+ if (!relationWayList.isEmpty()) {
6364+ return relation;
6365+ }
6366+ // Merge ways
6367+ List<VectorNode> nodes = new ArrayList<>();
6368+ for (VectorWay way : wayList) {
6369+ for (VectorNode node : way.getNodes()) {
6370+ if (nodes.isEmpty() || !Objects.equals(nodes.get(nodes.size() - 1), node)) {
6371+ nodes.add(node);
6372+ }
6373+ }
6374+ }
6375+ VectorWay way = wayList.get(0);
6376+ way.setNodes(nodes);
6377+ wayList.remove(way);
6378+ wayList.forEach(this::removePrimitive);
6379+ this.removePrimitive(relation);
6380+ return way;
6381+ }
6382+
6383+ private static <N extends INode, W extends IWay<N>> boolean canMergeWays(W old, W toAdd, boolean allowReverse) {
6384+ final List<N> nodes = new ArrayList<>(old.getNodes());
6385+ boolean added = true;
6386+ if (allowReverse && old.firstNode().equals(toAdd.firstNode())) {
6387+ // old <-|-> new becomes old ->|-> new
6388+ Collections.reverse(nodes);
6389+ nodes.addAll(toAdd.getNodes());
6390+ } else if (old.firstNode().equals(toAdd.lastNode())) {
6391+ // old <-|<- new, so we prepend the new nodes in order
6392+ nodes.addAll(0, toAdd.getNodes());
6393+ } else if (old.lastNode().equals(toAdd.firstNode())) {
6394+ // old ->|-> new, we just add it
6395+ nodes.addAll(toAdd.getNodes());
6396+ } else if (allowReverse && old.lastNode().equals(toAdd.lastNode())) {
6397+ // old ->|<- new, we need to reverse new
6398+ final List<N> toAddNodes = new ArrayList<>(toAdd.getNodes());
6399+ Collections.reverse(toAddNodes);
6400+ nodes.addAll(toAddNodes);
6401+ } else {
6402+ added = false;
6403+ }
6404+ if (added) {
6405+ // This is (technically) always correct
6406+ old.setNodes(nodes);
6407+ }
6408+ return added;
6409+ }
6410+
6411+ private synchronized <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer,
6412+ Collection<VectorPrimitive> featureObjects, int x, int y) {
6413+ final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile);
6414+ final int layerExtent = layer.getExtent() * 2;
6415+ final ICoordinate lowerRight = tile.getTileSource()
6416+ .tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
6417+ final ICoordinate coords = new Coordinate(
6418+ upperLeft.getLat() - (upperLeft.getLat() - lowerRight.getLat()) * y / layerExtent,
6419+ upperLeft.getLon() + (lowerRight.getLon() - upperLeft.getLon()) * x / layerExtent);
6420+ final Collection<VectorNode> nodes = this.store
6421+ .searchNodes(new BBox(coords.getLon(), coords.getLat(), VectorDataSet.DUPE_NODE_DISTANCE));
6422+ final VectorNode node;
6423+ if (!nodes.isEmpty()) {
6424+ final VectorNode first = nodes.iterator().next();
6425+ if (first.isDisabled() || !first.isVisible()) {
6426+ // Only replace nodes that are not visible
6427+ node = new VectorNode(layer.getName());
6428+ node.setCoor(node.getCoor());
6429+ first.getReferrers(true).forEach(primitive -> {
6430+ if (primitive instanceof VectorWay) {
6431+ List<VectorNode> nodeList = new ArrayList<>(((VectorWay) primitive).getNodes());
6432+ nodeList.replaceAll(vnode -> vnode.equals(first) ? node : vnode);
6433+ ((VectorWay) primitive).setNodes(nodeList);
6434+ } else if (primitive instanceof VectorRelation) {
6435+ List<VectorRelationMember> members = new ArrayList<>(((VectorRelation) primitive).getMembers());
6436+ members.replaceAll(member ->
6437+ member.getMember().equals(first) ? new VectorRelationMember(member.getRole(), node) : member);
6438+ ((VectorRelation) primitive).setMembers(members);
6439+ }
6440+ });
6441+ this.removePrimitive(first);
6442+ } else {
6443+ node = first;
6444+ }
6445+ } else {
6446+ node = new VectorNode(layer.getName());
6447+ }
6448+ node.setCoor(coords);
6449+ featureObjects.add(node);
6450+ return node;
6451+ }
6452+
6453+ private <T extends Tile & VectorTile> List<VectorWay> pathToWay(T tile, Layer layer,
6454+ Collection<VectorPrimitive> featureObjects, Path2D shape) {
6455+ final PathIterator pathIterator = shape.getPathIterator(null);
6456+ final List<VectorWay> ways = pathIteratorToObjects(tile, layer, featureObjects, pathIterator).stream()
6457+ .filter(VectorWay.class::isInstance).map(VectorWay.class::cast).collect(
6458+ Collectors.toList());
6459+ // These nodes technically do not exist, so we shouldn't show them
6460+ ways.stream().flatMap(way -> way.getNodes().stream())
6461+ .filter(prim -> !prim.isTagged() && prim.getReferrers(true).size() == 1 && prim.getId() <= 0)
6462+ .forEach(prim -> {
6463+ prim.setDisabled(true);
6464+ prim.setVisible(false);
6465+ });
6466+ return ways;
6467+ }
6468+
6469+ private <T extends Tile & VectorTile> List<VectorPrimitive> pathIteratorToObjects(T tile, Layer layer,
6470+ Collection<VectorPrimitive> featureObjects, PathIterator pathIterator) {
6471+ final List<VectorNode> nodes = new ArrayList<>();
6472+ final double[] coords = new double[6];
6473+ final List<VectorPrimitive> ways = new ArrayList<>();
6474+ do {
6475+ final int type = pathIterator.currentSegment(coords);
6476+ pathIterator.next();
6477+ if ((PathIterator.SEG_MOVETO == type || PathIterator.SEG_CLOSE == type) && !nodes.isEmpty()) {
6478+ if (PathIterator.SEG_CLOSE == type) {
6479+ nodes.add(nodes.get(0));
6480+ }
6481+ // New line
6482+ if (!nodes.isEmpty()) {
6483+ final VectorWay way = new VectorWay(layer.getName());
6484+ way.setNodes(nodes);
6485+ featureObjects.add(way);
6486+ ways.add(way);
6487+ }
6488+ nodes.clear();
6489+ }
6490+ if (PathIterator.SEG_MOVETO == type || PathIterator.SEG_LINETO == type) {
6491+ final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1]);
6492+ nodes.add(node);
6493+ } else if (PathIterator.SEG_CLOSE != type) {
6494+ // Vector Tiles only have MoveTo, LineTo, and ClosePath. Anything else is not supported at this time.
6495+ throw new UnsupportedOperationException();
6496+ }
6497+ } while (!pathIterator.isDone());
6498+ if (!nodes.isEmpty()) {
6499+ final VectorWay way = new VectorWay(layer.getName());
6500+ way.setNodes(nodes);
6501+ featureObjects.add(way);
6502+ ways.add(way);
6503+ }
6504+ return ways;
6505+ }
6506+
6507+ private <T extends Tile & VectorTile> VectorRelation areaToRelation(T tile, Layer layer,
6508+ Collection<VectorPrimitive> featureObjects, Area area) {
6509+ final PathIterator pathIterator = area.getPathIterator(null);
6510+ final List<VectorPrimitive> members = pathIteratorToObjects(tile, layer, featureObjects, pathIterator);
6511+ VectorRelation vectorRelation = new VectorRelation(layer.getName());
6512+ for (VectorPrimitive member : members) {
6513+ final String role;
6514+ if (member instanceof VectorWay && ((VectorWay) member).isClosed()) {
6515+ role = Geometry.isClockwise(((VectorWay) member).getNodes()) ? "outer" : "inner";
6516+ } else {
6517+ role = "";
6518+ }
6519+ vectorRelation.addRelationMember(new VectorRelationMember(role, member));
6520+ }
6521+ return vectorRelation;
6522+ }
6523+
6524+ /**
6525+ * Add a tile to this data store
6526+ * @param tile The tile to add
6527+ * @param <T> The tile type
6528+ */
6529+ public synchronized <T extends Tile & VectorTile> void addTile(T tile) {
6530+ Optional<Tile> previous = this.addedTiles.stream()
6531+ .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
6532+ // Check if we have already added the tile (just to save processing time)
6533+ if (!previous.isPresent() || (!previous.get().isLoaded() && !previous.get().isLoading())) {
6534+ previous.ifPresent(this.addedTiles::remove);
6535+ this.addedTiles.add(tile);
6536+ for (Layer layer : tile.getLayers()) {
6537+ layer.getFeatures().forEach(feature -> {
6538+ org.openstreetmap.josm.data.imagery.vectortile.mapbox.Geometry geometry = feature
6539+ .getGeometryObject();
6540+ List<VectorPrimitive> featureObjects = new ArrayList<>();
6541+ List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>();
6542+ geometry.getShapes().forEach(shape -> {
6543+ final VectorPrimitive primitive;
6544+ if (shape instanceof Ellipse2D) {
6545+ primitive = pointToNode(tile, layer, featureObjects,
6546+ (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY());
6547+ } else if (shape instanceof Path2D) {
6548+ primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape).stream().findFirst()
6549+ .orElse(null);
6550+ } else if (shape instanceof Area) {
6551+ primitive = areaToRelation(tile, layer, featureObjects, (Area) shape);
6552+ primitive.put("type", "multipolygon");
6553+ } else {
6554+ // We shouldn't hit this, but just in case
6555+ throw new UnsupportedOperationException();
6556+ }
6557+ primaryFeatureObjects.add(primitive);
6558+ });
6559+ final VectorPrimitive primitive;
6560+ if (primaryFeatureObjects.size() == 1) {
6561+ primitive = primaryFeatureObjects.get(0);
6562+ if (primitive instanceof IRelation && !primitive.isMultipolygon()) {
6563+ primitive.put(JOSM_MERGE_TYPE_KEY, "merge");
6564+ }
6565+ } else if (!primaryFeatureObjects.isEmpty()) {
6566+ VectorRelation relation = new VectorRelation(layer.getName());
6567+ primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim))
6568+ .forEach(relation::addRelationMember);
6569+ primitive = relation;
6570+ } else {
6571+ return;
6572+ }
6573+ primitive.setId(feature.getId());
6574+ feature.getTags().forEach(primitive::put);
6575+ featureObjects.forEach(this::addPrimitive);
6576+ primaryFeatureObjects.forEach(this::addPrimitive);
6577+ this.addPrimitive(primitive);
6578+ });
6579+ }
6580+ }
6581+ }
6582+
6583+ @Override
6584+ public void destroy() {
6585+ this.addedTiles.forEach(tile -> tile.setLoaded(false));
6586+ this.addedTiles.forEach(tile -> tile.setImage(null));
6587+ this.addedTiles.clear();
6588+ this.store.clear();
6589+ this.allPrimitives.clear();
6590+ this.primitivesMap.clear();
6591+ }
6592+}
6593diff --git a/src/org/openstreetmap/josm/data/vector/VectorNode.java b/src/org/openstreetmap/josm/data/vector/VectorNode.java
6594new file mode 100644
6595index 000000000..60aecd8ff
6596--- /dev/null
6597+++ b/src/org/openstreetmap/josm/data/vector/VectorNode.java
6598@@ -0,0 +1,113 @@
6599+// License: GPL. For details, see LICENSE file.
6600+package org.openstreetmap.josm.data.vector;
6601+
6602+import java.util.List;
6603+
6604+import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
6605+import org.openstreetmap.josm.data.coor.EastNorth;
6606+import org.openstreetmap.josm.data.coor.LatLon;
6607+import org.openstreetmap.josm.data.osm.BBox;
6608+import org.openstreetmap.josm.data.osm.INode;
6609+import org.openstreetmap.josm.data.osm.IPrimitive;
6610+import org.openstreetmap.josm.data.osm.IWay;
6611+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
6612+import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
6613+import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
6614+import org.openstreetmap.josm.data.projection.ProjectionRegistry;
6615+
6616+/**
6617+ * The "Node" type of a vector layer
6618+ *
6619+ * @since xxx
6620+ */
6621+public class VectorNode extends VectorPrimitive implements INode {
6622+ private static final UniqueIdGenerator ID_GENERATOR = new UniqueIdGenerator();
6623+ private double lon = Double.NaN;
6624+ private double lat = Double.NaN;
6625+
6626+ /**
6627+ * Create a new vector node
6628+ * @param layer The layer for the vector node
6629+ */
6630+ public VectorNode(String layer) {
6631+ super(layer);
6632+ }
6633+
6634+ @Override
6635+ public double lon() {
6636+ return this.lon;
6637+ }
6638+
6639+ @Override
6640+ public double lat() {
6641+ return this.lat;
6642+ }
6643+
6644+ @Override
6645+ public UniqueIdGenerator getIdGenerator() {
6646+ return ID_GENERATOR;
6647+ }
6648+
6649+ @Override
6650+ public LatLon getCoor() {
6651+ return new LatLon(this.lat, this.lon);
6652+ }
6653+
6654+ @Override
6655+ public void setCoor(LatLon coordinates) {
6656+ this.lat = coordinates.lat();
6657+ this.lon = coordinates.lon();
6658+ }
6659+
6660+ /**
6661+ * Set the coordinates of this node
6662+ *
6663+ * @param coordinates The coordinates to set
6664+ * @see #setCoor(LatLon)
6665+ */
6666+ public void setCoor(ICoordinate coordinates) {
6667+ this.lat = coordinates.getLat();
6668+ this.lon = coordinates.getLon();
6669+ }
6670+
6671+ @Override
6672+ public void setEastNorth(EastNorth eastNorth) {
6673+ final LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth);
6674+ this.lat = ll.lat();
6675+ this.lon = ll.lon();
6676+ }
6677+
6678+ @Override
6679+ public boolean isReferredByWays(int n) {
6680+ // Count only referrers that are members of the same dataset (primitive can have some fake references, for example
6681+ // when way is cloned
6682+ List<? extends IPrimitive> referrers = super.getReferrers();
6683+ if (referrers == null || referrers.isEmpty())
6684+ return false;
6685+ if (referrers instanceof IPrimitive)
6686+ return n <= 1 && referrers instanceof IWay && ((IPrimitive) referrers).getDataSet() == getDataSet();
6687+ else {
6688+ int counter = 0;
6689+ for (IPrimitive o : referrers) {
6690+ if (getDataSet() == o.getDataSet() && o instanceof IWay && ++counter >= n)
6691+ return true;
6692+ }
6693+ return false;
6694+ }
6695+ }
6696+
6697+ @Override
6698+ public void accept(PrimitiveVisitor visitor) {
6699+ visitor.visit(this);
6700+ }
6701+
6702+ @Override
6703+ public BBox getBBox() {
6704+ return new BBox(this.lon, this.lat);
6705+ }
6706+
6707+ @Override
6708+ public OsmPrimitiveType getType() {
6709+ return OsmPrimitiveType.NODE;
6710+ }
6711+}
6712diff --git a/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
6713new file mode 100644
6714index 000000000..17b5bef6f
6715--- /dev/null
6716+++ b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
6717@@ -0,0 +1,256 @@
6718+// License: GPL. For details, see LICENSE file.
6719+package org.openstreetmap.josm.data.vector;
6720+
6721+import java.util.Arrays;
6722+import java.util.List;
6723+import java.util.Map;
6724+import java.util.function.Consumer;
6725+import java.util.stream.Collectors;
6726+import java.util.stream.IntStream;
6727+import java.util.stream.Stream;
6728+
6729+import org.openstreetmap.josm.data.osm.AbstractPrimitive;
6730+import org.openstreetmap.josm.data.osm.IPrimitive;
6731+import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
6732+import org.openstreetmap.josm.gui.mappaint.StyleCache;
6733+import org.openstreetmap.josm.tools.Utils;
6734+
6735+/**
6736+ * The base class for Vector primitives
6737+ * @author Taylor Smock
6738+ * @since xxx
6739+ */
6740+public abstract class VectorPrimitive extends AbstractPrimitive implements DataLayer<String> {
6741+ private VectorDataSet dataSet;
6742+ private boolean highlighted;
6743+ private StyleCache mappaintStyle;
6744+ private final String layer;
6745+
6746+ /**
6747+ * Create a primitive for a specific vector layer
6748+ * @param layer The layer for the primitive
6749+ */
6750+ protected VectorPrimitive(String layer) {
6751+ this.layer = layer;
6752+ this.id = getIdGenerator().generateUniqueId();
6753+ }
6754+
6755+ @Override
6756+ protected void keysChangedImpl(Map<String, String> originalKeys) {
6757+ clearCachedStyle();
6758+ if (dataSet != null) {
6759+ for (IPrimitive ref : getReferrers()) {
6760+ ref.clearCachedStyle();
6761+ }
6762+ }
6763+ }
6764+
6765+ @Override
6766+ public boolean isHighlighted() {
6767+ return this.highlighted;
6768+ }
6769+
6770+ @Override
6771+ public void setHighlighted(boolean highlighted) {
6772+ this.highlighted = highlighted;
6773+ }
6774+
6775+ @Override
6776+ public boolean isTagged() {
6777+ return !this.getInterestingTags().isEmpty();
6778+ }
6779+
6780+ @Override
6781+ public boolean isAnnotated() {
6782+ return this.getInterestingTags().size() - this.getKeys().size() > 0;
6783+ }
6784+
6785+ @Override
6786+ public VectorDataSet getDataSet() {
6787+ return this.dataSet;
6788+ }
6789+
6790+ protected void setDataSet(VectorDataSet dataSet) {
6791+ this.dataSet = dataSet;
6792+ }
6793+
6794+ /*----------
6795+ * MAPPAINT
6796+ *--------*/
6797+ private short mappaintCacheIdx;
6798+
6799+ @Override
6800+ public final StyleCache getCachedStyle() {
6801+ return mappaintStyle;
6802+ }
6803+
6804+ @Override
6805+ public final void setCachedStyle(StyleCache mappaintStyle) {
6806+ this.mappaintStyle = mappaintStyle;
6807+ }
6808+
6809+ @Override
6810+ public final boolean isCachedStyleUpToDate() {
6811+ return mappaintStyle != null && mappaintCacheIdx == dataSet.getMappaintCacheIndex();
6812+ }
6813+
6814+ @Override
6815+ public final void declareCachedStyleUpToDate() {
6816+ this.mappaintCacheIdx = dataSet.getMappaintCacheIndex();
6817+ }
6818+
6819+ @Override
6820+ public boolean hasDirectionKeys() {
6821+ return false;
6822+ }
6823+
6824+ @Override
6825+ public boolean reversedDirection() {
6826+ return false;
6827+ }
6828+
6829+ /*------------
6830+ * Referrers
6831+ ------------*/
6832+ // Largely the same as OsmPrimitive, OsmPrimitive not modified at this time to avoid breaking binary compatibility
6833+
6834+ private Object referrers;
6835+
6836+ @Override
6837+ public final List<VectorPrimitive> getReferrers(boolean allowWithoutDataset) {
6838+ return referrers(allowWithoutDataset, VectorPrimitive.class)
6839+ .collect(Collectors.toList());
6840+ }
6841+
6842+ /**
6843+ * Add new referrer. If referrer is already included then no action is taken
6844+ * @param referrer The referrer to add
6845+ */
6846+ protected void addReferrer(IPrimitive referrer) {
6847+ if (referrers == null) {
6848+ referrers = referrer;
6849+ } else if (referrers instanceof IPrimitive) {
6850+ if (referrers != referrer) {
6851+ referrers = new IPrimitive[] {(IPrimitive) referrers, referrer};
6852+ }
6853+ } else {
6854+ for (IPrimitive primitive:(IPrimitive[]) referrers) {
6855+ if (primitive == referrer)
6856+ return;
6857+ }
6858+ referrers = Utils.addInArrayCopy((IPrimitive[]) referrers, referrer);
6859+ }
6860+ }
6861+
6862+ /**
6863+ * Remove referrer. No action is taken if referrer is not registered
6864+ * @param referrer The referrer to remove
6865+ */
6866+ protected void removeReferrer(IPrimitive referrer) {
6867+ if (referrers instanceof IPrimitive) {
6868+ if (referrers == referrer) {
6869+ referrers = null;
6870+ }
6871+ } else if (referrers instanceof IPrimitive[]) {
6872+ IPrimitive[] orig = (IPrimitive[]) referrers;
6873+ int idx = IntStream.range(0, orig.length)
6874+ .filter(i -> orig[i] == referrer)
6875+ .findFirst().orElse(-1);
6876+ if (idx == -1)
6877+ return;
6878+
6879+ if (orig.length == 2) {
6880+ referrers = orig[1-idx]; // idx is either 0 or 1, take the other
6881+ } else { // downsize the array
6882+ IPrimitive[] smaller = new IPrimitive[orig.length-1];
6883+ System.arraycopy(orig, 0, smaller, 0, idx);
6884+ System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx);
6885+ referrers = smaller;
6886+ }
6887+ }
6888+ }
6889+
6890+ private <T extends IPrimitive> Stream<T> referrers(boolean allowWithoutDataset, Class<T> filter) {
6891+ // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example
6892+ // when way is cloned
6893+
6894+ if (dataSet == null && !allowWithoutDataset) {
6895+ return Stream.empty();
6896+ }
6897+ if (referrers == null) {
6898+ return Stream.empty();
6899+ }
6900+ final Stream<IPrimitive> stream = referrers instanceof IPrimitive // NOPMD
6901+ ? Stream.of((IPrimitive) referrers)
6902+ : Arrays.stream((IPrimitive[]) referrers);
6903+ return stream
6904+ .filter(p -> p.getDataSet() == dataSet)
6905+ .filter(filter::isInstance)
6906+ .map(filter::cast);
6907+ }
6908+
6909+ /**
6910+ * Gets all primitives in the current dataset that reference this primitive.
6911+ * @param filter restrict primitives to subclasses
6912+ * @param <T> type of primitives
6913+ * @return the referrers as Stream
6914+ */
6915+ public final <T extends IPrimitive> Stream<T> referrers(Class<T> filter) {
6916+ return referrers(false, filter);
6917+ }
6918+
6919+ @Override
6920+ public void visitReferrers(PrimitiveVisitor visitor) {
6921+ if (visitor != null)
6922+ doVisitReferrers(o -> o.accept(visitor));
6923+ }
6924+
6925+ private void doVisitReferrers(Consumer<IPrimitive> visitor) {
6926+ if (this.referrers instanceof IPrimitive) {
6927+ IPrimitive ref = (IPrimitive) this.referrers;
6928+ if (ref.getDataSet() == dataSet) {
6929+ visitor.accept(ref);
6930+ }
6931+ } else if (this.referrers instanceof IPrimitive[]) {
6932+ IPrimitive[] refs = (IPrimitive[]) this.referrers;
6933+ for (IPrimitive ref: refs) {
6934+ if (ref.getDataSet() == dataSet) {
6935+ visitor.accept(ref);
6936+ }
6937+ }
6938+ }
6939+ }
6940+
6941+ /**
6942+ * Set the id of the object
6943+ * @param id The id
6944+ */
6945+ protected void setId(long id) {
6946+ this.id = id;
6947+ }
6948+
6949+ /**
6950+ * Make this object disabled
6951+ * @param disabled {@code true} to disable the object
6952+ */
6953+ public void setDisabled(boolean disabled) {
6954+ this.updateFlags(FLAG_DISABLED, disabled);
6955+ }
6956+
6957+ /**
6958+ * Make this object visible
6959+ * @param visible {@code true} to make this object visible (default)
6960+ */
6961+ @Override
6962+ public void setVisible(boolean visible) {
6963+ this.updateFlags(FLAG_VISIBLE, visible);
6964+ }
6965+
6966+ /**************************
6967+ * Data layer information *
6968+ **************************/
6969+ @Override
6970+ public String getLayer() {
6971+ return this.layer;
6972+ }
6973+}
6974diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelation.java b/src/org/openstreetmap/josm/data/vector/VectorRelation.java
6975new file mode 100644
6976index 000000000..0deb57e57
6977--- /dev/null
6978+++ b/src/org/openstreetmap/josm/data/vector/VectorRelation.java
6979@@ -0,0 +1,114 @@
6980+// License: GPL. For details, see LICENSE file.
6981+package org.openstreetmap.josm.data.vector;
6982+
6983+import java.util.ArrayList;
6984+import java.util.Collections;
6985+import java.util.List;
6986+
6987+import org.openstreetmap.josm.data.osm.BBox;
6988+import org.openstreetmap.josm.data.osm.IPrimitive;
6989+import org.openstreetmap.josm.data.osm.IRelation;
6990+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
6991+import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
6992+import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
6993+
6994+/**
6995+ * The "Relation" type for vectors
6996+ *
6997+ * @author Taylor Smock
6998+ * @since xxx
6999+ */
7000+public class VectorRelation extends VectorPrimitive implements IRelation<VectorRelationMember> {
7001+ private static final UniqueIdGenerator RELATION_ID_GENERATOR = new UniqueIdGenerator();
7002+ private final List<VectorRelationMember> members = new ArrayList<>();
7003+ private BBox cachedBBox;
7004+
7005+ /**
7006+ * Create a new relation for a layer
7007+ * @param layer The layer the relation will belong to
7008+ */
7009+ public VectorRelation(String layer) {
7010+ super(layer);
7011+ }
7012+
7013+ @Override
7014+ public UniqueIdGenerator getIdGenerator() {
7015+ return RELATION_ID_GENERATOR;
7016+ }
7017+
7018+ @Override
7019+ public void accept(PrimitiveVisitor visitor) {
7020+ visitor.visit(this);
7021+ }
7022+
7023+ @Override
7024+ public BBox getBBox() {
7025+ if (cachedBBox == null) {
7026+ cachedBBox = new BBox();
7027+ for (IPrimitive member : this.getMemberPrimitivesList()) {
7028+ cachedBBox.add(member.getBBox());
7029+ }
7030+ }
7031+ return cachedBBox;
7032+ }
7033+
7034+ protected void addRelationMember(VectorRelationMember member) {
7035+ this.members.add(member);
7036+ member.getMember().addReferrer(this);
7037+ cachedBBox = null;
7038+ }
7039+
7040+ /**
7041+ * Remove the first instance of a member from the relation
7042+ *
7043+ * @param member The member to remove
7044+ */
7045+ protected void removeRelationMember(VectorRelationMember member) {
7046+ this.members.remove(member);
7047+ if (!this.members.contains(member)) {
7048+ member.getMember().removeReferrer(this);
7049+ }
7050+ }
7051+
7052+ @Override
7053+ public int getMembersCount() {
7054+ return this.members.size();
7055+ }
7056+
7057+ @Override
7058+ public VectorRelationMember getMember(int index) {
7059+ return this.members.get(index);
7060+ }
7061+
7062+ @Override
7063+ public List<VectorRelationMember> getMembers() {
7064+ return Collections.unmodifiableList(this.members);
7065+ }
7066+
7067+ @Override
7068+ public void setMembers(List<VectorRelationMember> members) {
7069+ this.members.clear();
7070+ this.members.addAll(members);
7071+ }
7072+
7073+ @Override
7074+ public long getMemberId(int idx) {
7075+ return this.getMember(idx).getMember().getId();
7076+ }
7077+
7078+ @Override
7079+ public String getRole(int idx) {
7080+ return this.getMember(idx).getRole();
7081+ }
7082+
7083+ @Override
7084+ public OsmPrimitiveType getMemberType(int idx) {
7085+ return this.getMember(idx).getType();
7086+ }
7087+
7088+ @Override
7089+ public OsmPrimitiveType getType() {
7090+ return this.getMembers().stream().map(VectorRelationMember::getType)
7091+ .allMatch(OsmPrimitiveType.CLOSEDWAY::equals) ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION;
7092+ }
7093+}
7094diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java b/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java
7095new file mode 100644
7096index 000000000..56d6dfe77
7097--- /dev/null
7098+++ b/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java
7099@@ -0,0 +1,70 @@
7100+// License: GPL. For details, see LICENSE file.
7101+package org.openstreetmap.josm.data.vector;
7102+
7103+import java.util.Optional;
7104+
7105+import org.openstreetmap.josm.data.osm.INode;
7106+import org.openstreetmap.josm.data.osm.IRelation;
7107+import org.openstreetmap.josm.data.osm.IRelationMember;
7108+import org.openstreetmap.josm.data.osm.IWay;
7109+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
7110+import org.openstreetmap.josm.tools.CheckParameterUtil;
7111+
7112+/**
7113+ * Relation members for a Vector Relation
7114+ */
7115+public class VectorRelationMember implements IRelationMember<VectorPrimitive> {
7116+ private final String role;
7117+ private final VectorPrimitive member;
7118+
7119+ /**
7120+ * Create a new relation member
7121+ * @param role The role of the member
7122+ * @param member The member primitive
7123+ */
7124+ public VectorRelationMember(String role, VectorPrimitive member) {
7125+ CheckParameterUtil.ensureParameterNotNull(member, "member");
7126+ this.role = Optional.ofNullable(role).orElse("").intern();
7127+ this.member = member;
7128+ }
7129+
7130+ @Override
7131+ public String getRole() {
7132+ return this.role;
7133+ }
7134+
7135+ @Override
7136+ public boolean isNode() {
7137+ return this.member instanceof INode;
7138+ }
7139+
7140+ @Override
7141+ public boolean isWay() {
7142+ return this.member instanceof IWay;
7143+ }
7144+
7145+ @Override
7146+ public boolean isRelation() {
7147+ return this.member instanceof IRelation;
7148+ }
7149+
7150+ @Override
7151+ public VectorPrimitive getMember() {
7152+ return this.member;
7153+ }
7154+
7155+ @Override
7156+ public long getUniqueId() {
7157+ return this.member.getId();
7158+ }
7159+
7160+ @Override
7161+ public OsmPrimitiveType getType() {
7162+ return this.member.getType();
7163+ }
7164+
7165+ @Override
7166+ public boolean isNew() {
7167+ return this.member.isNew();
7168+ }
7169+}
7170diff --git a/src/org/openstreetmap/josm/data/vector/VectorWay.java b/src/org/openstreetmap/josm/data/vector/VectorWay.java
7171new file mode 100644
7172index 000000000..582fca2d4
7173--- /dev/null
7174+++ b/src/org/openstreetmap/josm/data/vector/VectorWay.java
7175@@ -0,0 +1,132 @@
7176+// License: GPL. For details, see LICENSE file.
7177+package org.openstreetmap.josm.data.vector;
7178+
7179+import java.util.ArrayList;
7180+import java.util.Collections;
7181+import java.util.List;
7182+import java.util.stream.Collectors;
7183+
7184+import org.openstreetmap.josm.data.osm.BBox;
7185+import org.openstreetmap.josm.data.osm.INode;
7186+import org.openstreetmap.josm.data.osm.IWay;
7187+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
7188+import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
7189+import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
7190+
7191+/**
7192+ * The "Way" type for a Vector layer
7193+ *
7194+ * @author Taylor Smock
7195+ * @since xxx
7196+ */
7197+public class VectorWay extends VectorPrimitive implements IWay<VectorNode> {
7198+ private static final UniqueIdGenerator WAY_GENERATOR = new UniqueIdGenerator();
7199+ private final List<VectorNode> nodes = new ArrayList<>();
7200+ private BBox cachedBBox;
7201+
7202+ /**
7203+ * Create a new way for a layer
7204+ * @param layer The layer for the way
7205+ */
7206+ public VectorWay(String layer) {
7207+ super(layer);
7208+ }
7209+
7210+ @Override
7211+ public UniqueIdGenerator getIdGenerator() {
7212+ return WAY_GENERATOR;
7213+ }
7214+
7215+ @Override
7216+ public void accept(PrimitiveVisitor visitor) {
7217+ visitor.visit(this);
7218+ }
7219+
7220+ @Override
7221+ public BBox getBBox() {
7222+ if (cachedBBox == null) {
7223+ cachedBBox = new BBox();
7224+ for (INode node : this.getNodes()) {
7225+ cachedBBox.add(node.getBBox());
7226+ }
7227+ }
7228+ return cachedBBox;
7229+ }
7230+
7231+ @Override
7232+ public int getNodesCount() {
7233+ return this.getNodes().size();
7234+ }
7235+
7236+ @Override
7237+ public VectorNode getNode(int index) {
7238+ return this.getNodes().get(index);
7239+ }
7240+
7241+ @Override
7242+ public List<VectorNode> getNodes() {
7243+ return Collections.unmodifiableList(this.nodes);
7244+ }
7245+
7246+ @Override
7247+ public void setNodes(List<VectorNode> nodes) {
7248+ this.nodes.forEach(node -> node.removeReferrer(this));
7249+ this.nodes.clear();
7250+ nodes.forEach(node -> node.addReferrer(this));
7251+ this.nodes.addAll(nodes);
7252+ this.cachedBBox = null;
7253+ }
7254+
7255+ @Override
7256+ public List<Long> getNodeIds() {
7257+ return this.getNodes().stream().map(VectorNode::getId).collect(Collectors.toList());
7258+ }
7259+
7260+ @Override
7261+ public long getNodeId(int idx) {
7262+ return this.getNodes().get(idx).getId();
7263+ }
7264+
7265+ @Override
7266+ public boolean isClosed() {
7267+ return this.firstNode() != null && this.firstNode().equals(this.lastNode());
7268+ }
7269+
7270+ @Override
7271+ public VectorNode firstNode() {
7272+ if (this.nodes.isEmpty()) {
7273+ return null;
7274+ }
7275+ return this.getNode(0);
7276+ }
7277+
7278+ @Override
7279+ public VectorNode lastNode() {
7280+ if (this.nodes.isEmpty()) {
7281+ return null;
7282+ }
7283+ return this.getNode(this.getNodesCount() - 1);
7284+ }
7285+
7286+ @Override
7287+ public boolean isFirstLastNode(INode n) {
7288+ if (this.nodes.isEmpty()) {
7289+ return false;
7290+ }
7291+ return this.firstNode().equals(n) || this.lastNode().equals(n);
7292+ }
7293+
7294+ @Override
7295+ public boolean isInnerNode(INode n) {
7296+ if (this.nodes.isEmpty()) {
7297+ return false;
7298+ }
7299+ return !this.firstNode().equals(n) && !this.lastNode().equals(n) && this.nodes.stream()
7300+ .anyMatch(vectorNode -> vectorNode.equals(n));
7301+ }
7302+
7303+ @Override
7304+ public OsmPrimitiveType getType() {
7305+ return this.isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY;
7306+ }
7307+}
7308diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
7309new file mode 100644
7310index 000000000..8983e8397
7311--- /dev/null
7312+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
7313@@ -0,0 +1,141 @@
7314+// License: GPL. For details, see LICENSE file.
7315+package org.openstreetmap.josm.data.vector;
7316+
7317+import static org.junit.jupiter.api.Assertions.assertEquals;
7318+import static org.junit.jupiter.api.Assertions.assertTrue;
7319+
7320+
7321+import java.nio.file.Paths;
7322+import java.text.MessageFormat;
7323+import java.util.ArrayList;
7324+import java.util.Collection;
7325+import java.util.Collections;
7326+import java.util.HashSet;
7327+import java.util.List;
7328+import java.util.Map;
7329+import java.util.stream.Collectors;
7330+
7331+import org.openstreetmap.josm.TestUtils;
7332+import org.openstreetmap.josm.data.imagery.ImageryInfo;
7333+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
7334+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
7335+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
7336+import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
7337+import org.openstreetmap.josm.testutils.JOSMTestRules;
7338+
7339+import org.awaitility.Awaitility;
7340+import org.awaitility.Durations;
7341+import org.junit.jupiter.api.BeforeEach;
7342+import org.junit.jupiter.api.Test;
7343+import org.junit.jupiter.api.extension.RegisterExtension;
7344+
7345+/**
7346+ * A test for {@link VectorDataSet}
7347+ */
7348+class VectorDataSetTest {
7349+ /**
7350+ * Make some methods available for this test class
7351+ */
7352+ private static class MVTLayerMock extends MVTLayer {
7353+ private final Collection<MVTTile> finishedLoading = new HashSet<>();
7354+
7355+ MVTLayerMock(ImageryInfo info) {
7356+ super(info);
7357+ }
7358+
7359+ @Override
7360+ protected MapboxVectorTileSource getTileSource() {
7361+ return super.getTileSource();
7362+ }
7363+
7364+ protected MapBoxVectorCachedTileLoader getTileLoader() {
7365+ if (this.tileLoader == null) {
7366+ this.tileLoader = this.getTileLoaderFactory().makeTileLoader(this, Collections.emptyMap(), 7200);
7367+ }
7368+ if (this.tileLoader instanceof MapBoxVectorCachedTileLoader) {
7369+ return (MapBoxVectorCachedTileLoader) this.tileLoader;
7370+ }
7371+ return null;
7372+ }
7373+
7374+ @Override
7375+ public void finishedLoading(MVTTile tile) {
7376+ super.finishedLoading(tile);
7377+ this.finishedLoading.add(tile);
7378+ }
7379+
7380+ public Collection<MVTTile> finishedLoading() {
7381+ return this.finishedLoading;
7382+ }
7383+ }
7384+
7385+ @RegisterExtension
7386+ JOSMTestRules rule = new JOSMTestRules().projection();
7387+
7388+ /**
7389+ * Load arbitrary tiles
7390+ * @param layer The layer to add the tiles to
7391+ * @param tiles The tiles to load ([z, x, y, z, x, y, ...]) -- must be divisible by three
7392+ */
7393+ private static void loadTile(MVTLayerMock layer, int... tiles) {
7394+ if (tiles.length % 3 != 0 || tiles.length == 0) {
7395+ throw new IllegalArgumentException("Tiles come with a {z}, {x}, and {y} component");
7396+ }
7397+ final MapboxVectorTileSource tileSource = layer.getTileSource();
7398+ MapBoxVectorCachedTileLoader tileLoader = layer.getTileLoader();
7399+ Collection<MVTTile> tilesCollection = new ArrayList<>();
7400+ for (int i = 0; i < tiles.length / 3; i++) {
7401+ final MVTTile tile = (MVTTile) layer.createTile(tileSource, tiles[3 * i + 1], tiles[3 * i + 2], tiles[3 * i]);
7402+ tileLoader.createTileLoaderJob(tile).submit();
7403+ tilesCollection.add(tile);
7404+ }
7405+ Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> layer.finishedLoading().size() == tilesCollection
7406+ .size());
7407+ }
7408+
7409+ private MVTLayerMock layer;
7410+
7411+ @BeforeEach
7412+ void setup() {
7413+ // Create the preconditions for the test
7414+ final ImageryInfo info = new ImageryInfo();
7415+ info.setName("en", "Test info");
7416+ info.setUrl("file:/" + Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "{z}", "{x}", "{y}.mvt"));
7417+ layer = new MVTLayerMock(info);
7418+ }
7419+
7420+ @Test
7421+ void testNodeDeduplication() {
7422+ final VectorDataSet dataSet = this.layer.getData();
7423+ assertTrue(dataSet.allPrimitives().isEmpty());
7424+
7425+ // Set the zoom to 14, as that is the tile we are checking
7426+ dataSet.setZoom(14);
7427+ loadTile(this.layer, 14, 3248, 6258);
7428+
7429+ // There _does_ appear to be some kind of race condition though
7430+ Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> dataSet.getNodes().size() > 50);
7431+ // Actual test
7432+ // With Mapillary, only ends of ways should be untagged
7433+ // There are 55 actual "nodes" in the data with two nodes for the ends of the way.
7434+ // One of the end nodes is a duplicate of an actual node.
7435+ assertEquals(56, dataSet.getNodes().size());
7436+ assertEquals(1, dataSet.getWays().size());
7437+ assertEquals(0, dataSet.getRelations().size());
7438+ }
7439+
7440+ @Test
7441+ void testWayDeduplicationSimple() {
7442+ final VectorDataSet dataSet = this.layer.getData();
7443+ assertTrue(dataSet.allPrimitives().isEmpty());
7444+
7445+ // Set the zoom to 14, as that is the tile we are checking
7446+ dataSet.setZoom(14);
7447+ // Load tiles that are next to each other
7448+ loadTile(this.layer, 14, 3248, 6258, 14, 3248, 6257);
7449+
7450+ Map<Long, List<VectorWay>> wayGroups = dataSet.getWays().stream()
7451+ .collect(Collectors.groupingBy(VectorWay::getId));
7452+ wayGroups.forEach((id, ways) -> assertEquals(1, ways.size(), MessageFormat.format("{0} was not deduplicated", id)));
7453+ }
7454+}
7455diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java
7456new file mode 100644
7457index 000000000..834b46c4d
7458--- /dev/null
7459+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java
7460@@ -0,0 +1,153 @@
7461+// License: GPL. For details, see LICENSE file.
7462+package org.openstreetmap.josm.data.vector;
7463+
7464+import org.junit.jupiter.api.Test;
7465+import org.junit.jupiter.api.extension.RegisterExtension;
7466+import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
7467+import org.openstreetmap.josm.data.coor.EastNorth;
7468+import org.openstreetmap.josm.data.coor.LatLon;
7469+import org.openstreetmap.josm.data.osm.BBox;
7470+import org.openstreetmap.josm.data.osm.INode;
7471+import org.openstreetmap.josm.data.osm.IRelation;
7472+import org.openstreetmap.josm.data.osm.IWay;
7473+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
7474+import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
7475+import org.openstreetmap.josm.data.projection.ProjectionRegistry;
7476+import org.openstreetmap.josm.testutils.JOSMTestRules;
7477+
7478+import java.util.ArrayList;
7479+import java.util.Collections;
7480+import java.util.List;
7481+
7482+import static org.junit.jupiter.api.Assertions.assertEquals;
7483+import static org.junit.jupiter.api.Assertions.assertFalse;
7484+import static org.junit.jupiter.api.Assertions.assertNotNull;
7485+import static org.junit.jupiter.api.Assertions.assertSame;
7486+import static org.junit.jupiter.api.Assertions.assertTrue;
7487+import static org.junit.jupiter.api.Assertions.fail;
7488+
7489+/**
7490+ * Test class for {@link VectorNode}
7491+ * @author Taylor Smock
7492+ * @since xxx
7493+ */
7494+class VectorNodeTest {
7495+ @RegisterExtension
7496+ JOSMTestRules rule = new JOSMTestRules().projection();
7497+
7498+ @Test
7499+ void testLatLon() {
7500+ VectorNode node = new VectorNode("test");
7501+ assertTrue(Double.isNaN(node.lat()));
7502+ assertTrue(Double.isNaN(node.lon()));
7503+ LatLon testLatLon = new LatLon(50, -40);
7504+ node.setCoor(testLatLon);
7505+ assertEquals(50, node.lat());
7506+ assertEquals(-40, node.lon());
7507+ assertEquals(testLatLon, node.getCoor());
7508+ }
7509+
7510+ @Test
7511+ void testSetEastNorth() {
7512+ VectorNode node = new VectorNode("test");
7513+ LatLon latLon = new LatLon(-1, 5);
7514+ EastNorth eastNorth = ProjectionRegistry.getProjection().latlon2eastNorth(latLon);
7515+ node.setEastNorth(eastNorth);
7516+ assertEquals(-1, node.lat(), 0.0000000001);
7517+ assertEquals(5, node.lon(), 0.0000000001);
7518+ }
7519+
7520+ @Test
7521+ void testICoordinate() {
7522+ VectorNode node = new VectorNode("test");
7523+ assertTrue(Double.isNaN(node.lat()));
7524+ assertTrue(Double.isNaN(node.lon()));
7525+ ICoordinate coord = new ICoordinate() {
7526+ @Override
7527+ public double getLat() {
7528+ return 5;
7529+ }
7530+
7531+ @Override
7532+ public void setLat(double lat) {
7533+ // No op
7534+ }
7535+
7536+ @Override
7537+ public double getLon() {
7538+ return -1;
7539+ }
7540+
7541+ @Override
7542+ public void setLon(double lon) {
7543+ // no op
7544+ }
7545+ };
7546+ node.setCoor(coord);
7547+ assertEquals(5, node.lat());
7548+ assertEquals(-1, node.lon());
7549+ }
7550+
7551+ @Test
7552+ void testUniqueIdGenerator() {
7553+ VectorNode node1 = new VectorNode("test");
7554+ VectorNode node2 = new VectorNode("test2");
7555+ assertSame(node1.getIdGenerator(), node2.getIdGenerator());
7556+ assertNotNull(node1.getIdGenerator());
7557+ }
7558+
7559+ @Test
7560+ void testNode() {
7561+ assertEquals(OsmPrimitiveType.NODE, new VectorNode("test").getType());
7562+ }
7563+
7564+ @Test
7565+ void testBBox() {
7566+ VectorNode node = new VectorNode("test");
7567+ node.setCoor(new LatLon(5, -1));
7568+ assertTrue(node.getBBox().bboxIsFunctionallyEqual(new BBox(-1, 5), 0d));
7569+ }
7570+
7571+ @Test
7572+ void testVisitor() {
7573+ List<VectorNode> visited = new ArrayList<>();
7574+ VectorNode node = new VectorNode("test");
7575+ node.accept(new PrimitiveVisitor() {
7576+ @Override
7577+ public void visit(INode n) {
7578+ visited.add((VectorNode) n);
7579+ }
7580+
7581+ @Override
7582+ public void visit(IWay<?> w) {
7583+ fail("Way should not have been visited");
7584+ }
7585+
7586+ @Override
7587+ public void visit(IRelation<?> r) {
7588+ fail("Relation should not have been visited");
7589+ }
7590+ });
7591+
7592+ assertEquals(1, visited.size());
7593+ assertSame(node, visited.get(0));
7594+ }
7595+
7596+ @Test
7597+ void testIsReferredToByWays() {
7598+ VectorWay way = new VectorWay("test");
7599+ VectorNode node = new VectorNode("test");
7600+ assertFalse(node.isReferredByWays(1));
7601+ assertTrue(node.getReferrers(true).isEmpty());
7602+ way.setNodes(Collections.singletonList(node));
7603+ assertEquals(1, node.getReferrers(true).size());
7604+ assertSame(way, node.getReferrers(true).get(0));
7605+ // No dataset yet
7606+ assertFalse(node.isReferredByWays(1));
7607+ VectorDataSet dataSet = new VectorDataSet();
7608+ dataSet.addPrimitive(way);
7609+ dataSet.addPrimitive(node);
7610+ assertTrue(node.isReferredByWays(1));
7611+ assertFalse(node.isReferredByWays(2));
7612+ }
7613+}
7614diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java
7615new file mode 100644
7616index 000000000..941143b25
7617--- /dev/null
7618+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java
7619@@ -0,0 +1,45 @@
7620+// License: GPL. For details, see LICENSE file.
7621+package org.openstreetmap.josm.data.vector;
7622+
7623+import org.junit.jupiter.api.Test;
7624+import org.junit.jupiter.api.extension.RegisterExtension;
7625+import org.openstreetmap.josm.testutils.JOSMTestRules;
7626+
7627+import java.util.Arrays;
7628+
7629+import static org.junit.jupiter.api.Assertions.assertEquals;
7630+import static org.junit.jupiter.api.Assertions.assertFalse;
7631+import static org.junit.jupiter.api.Assertions.assertSame;
7632+import static org.junit.jupiter.api.Assertions.assertThrows;
7633+import static org.junit.jupiter.api.Assertions.assertTrue;
7634+
7635+/**
7636+ * Test class for {@link VectorRelation}
7637+ * @author Taylor Smock
7638+ * @since xxx
7639+ */
7640+class VectorRelationTest {
7641+ @RegisterExtension
7642+ JOSMTestRules rule = new JOSMTestRules();
7643+
7644+ @Test
7645+ void testMembers() {
7646+ VectorNode node1 = new VectorNode("test");
7647+ VectorNode node2 = new VectorNode("test");
7648+ VectorWay way1 = new VectorWay("test");
7649+ way1.setNodes(Arrays.asList(node1, node2));
7650+ VectorRelationMember member1 = new VectorRelationMember("randomRole", node1);
7651+ VectorRelationMember member2 = new VectorRelationMember("role2", way1);
7652+ assertSame(node1, member1.getMember());
7653+ assertSame(node1.getType(), member1.getType());
7654+ assertEquals("randomRole", member1.getRole());
7655+ assertSame(node1.getId(), member1.getUniqueId());
7656+ // Not a way.
7657+ assertThrows(ClassCastException.class, member1::getWay);
7658+
7659+ assertTrue(member1.isNode());
7660+ assertFalse(member1.isWay());
7661+ assertFalse(member2.isNode());
7662+ assertTrue(member2.isWay());
7663+ }
7664+}
7665diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java
7666new file mode 100644
7667index 000000000..db2367e6b
7668--- /dev/null
7669+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java
7670@@ -0,0 +1,117 @@
7671+// License: GPL. For details, see LICENSE file.
7672+package org.openstreetmap.josm.data.vector;
7673+
7674+import org.junit.jupiter.api.Test;
7675+import org.openstreetmap.josm.data.coor.LatLon;
7676+import org.openstreetmap.josm.data.osm.BBox;
7677+import org.openstreetmap.josm.data.osm.INode;
7678+import org.openstreetmap.josm.data.osm.IRelation;
7679+import org.openstreetmap.josm.data.osm.IWay;
7680+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
7681+import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
7682+
7683+import java.util.ArrayList;
7684+import java.util.Arrays;
7685+import java.util.Collections;
7686+import java.util.List;
7687+
7688+import static org.junit.jupiter.api.Assertions.assertEquals;
7689+import static org.junit.jupiter.api.Assertions.assertFalse;
7690+import static org.junit.jupiter.api.Assertions.assertNull;
7691+import static org.junit.jupiter.api.Assertions.assertSame;
7692+import static org.junit.jupiter.api.Assertions.assertTrue;
7693+import static org.junit.jupiter.api.Assertions.fail;
7694+
7695+/**
7696+ * Test class for {@link VectorWay}
7697+ * @author Taylor Smock
7698+ * @since xxx
7699+ */
7700+class VectorWayTest {
7701+ @Test
7702+ void testBBox() {
7703+ VectorNode node1 = new VectorNode("test");
7704+ VectorWay way = new VectorWay("test");
7705+ way.setNodes(Collections.singletonList(node1));
7706+ node1.setCoor(new LatLon(-5, 1));
7707+ assertTrue(node1.getBBox().bboxIsFunctionallyEqual(way.getBBox(), 0.0));
7708+
7709+ VectorNode node2 = new VectorNode("test");
7710+ node2.setCoor(new LatLon(-10, 2));
7711+
7712+ way.setNodes(Arrays.asList(node1, node2));
7713+ assertTrue(way.getBBox().bboxIsFunctionallyEqual(new BBox(2, -10, 1, -5), 0.0));
7714+ }
7715+
7716+ @Test
7717+ void testIdGenerator() {
7718+ assertSame(new VectorWay("test").getIdGenerator(), new VectorWay("test").getIdGenerator());
7719+ }
7720+
7721+ @Test
7722+ void testNodes() {
7723+ VectorNode node1 = new VectorNode("test");
7724+ VectorNode node2 = new VectorNode("test");
7725+ VectorNode node3 = new VectorNode("test");
7726+ node1.setId(1);
7727+ node2.setId(2);
7728+ node3.setId(3);
7729+ VectorWay way = new VectorWay("test");
7730+ assertNull(way.firstNode());
7731+ assertNull(way.lastNode());
7732+ assertFalse(way.isClosed());
7733+ assertFalse(way.isFirstLastNode(node1));
7734+ assertFalse(way.isInnerNode(node2));
7735+ way.setNodes(Arrays.asList(node1, node2, node3));
7736+ assertEquals(3, way.getNodesCount());
7737+ assertEquals(node1, way.getNode(0));
7738+ assertEquals(node2, way.getNode(1));
7739+ assertEquals(node3, way.getNode(2));
7740+ assertTrue(way.isFirstLastNode(node1));
7741+ assertTrue(way.isFirstLastNode(node3));
7742+ assertFalse(way.isFirstLastNode(node2));
7743+ assertTrue(way.isInnerNode(node2));
7744+ assertFalse(way.isInnerNode(node1));
7745+ assertFalse(way.isInnerNode(node3));
7746+
7747+ assertEquals(1, way.getNodeIds().get(0));
7748+ assertEquals(2, way.getNodeIds().get(1));
7749+ assertEquals(3, way.getNodeIds().get(2));
7750+ assertEquals(1, way.getNodeId(0));
7751+ assertEquals(2, way.getNodeId(1));
7752+ assertEquals(3, way.getNodeId(2));
7753+
7754+ assertFalse(way.isClosed());
7755+ assertEquals(OsmPrimitiveType.WAY, way.getType());
7756+ List<VectorNode> nodes = new ArrayList<>(way.getNodes());
7757+ nodes.add(nodes.get(0));
7758+ way.setNodes(nodes);
7759+ assertTrue(way.isClosed());
7760+ assertEquals(OsmPrimitiveType.CLOSEDWAY, way.getType());
7761+ }
7762+
7763+ @Test
7764+ void testAccept() {
7765+ VectorWay way = new VectorWay("test");
7766+ List<VectorWay> visited = new ArrayList<>(1);
7767+ way.accept(new PrimitiveVisitor() {
7768+ @Override
7769+ public void visit(INode n) {
7770+ fail("No nodes should be visited");
7771+ }
7772+
7773+ @Override
7774+ public void visit(IWay<?> w) {
7775+ visited.add((VectorWay) w);
7776+ }
7777+
7778+ @Override
7779+ public void visit(IRelation<?> r) {
7780+ fail("No relations should be visited");
7781+ }
7782+ });
7783+
7784+ assertEquals(1, visited.size());
7785+ assertSame(way, visited.get(0));
7786+ }
7787+}
7788--
7789GitLab
7790
7791
7792From 60533498cd89f7b41b9e9822eb4b83ea55c60f7a Mon Sep 17 00:00:00 2001
7793From: Taylor Smock <tsmock@fb.com>
7794Date: Thu, 8 Apr 2021 16:45:20 -0600
7795Subject: [PATCH 06/50] Vector data test files (Mapillary)
7796
7797Signed-off-by: Taylor Smock <tsmock@fb.com>
7798---
7799 test/data/pbf/mapillary/14/3248/6258.mvt | Bin 0 -> 4256 bytes
7800 test/data/pbf/mapillary/14/3249/6258.mvt | Bin 0 -> 8703 bytes
7801 .../josm/data/vector/VectorDataSetTest.java | 2 +-
7802 3 files changed, 1 insertion(+), 1 deletion(-)
7803 create mode 100644 test/data/pbf/mapillary/14/3248/6258.mvt
7804 create mode 100644 test/data/pbf/mapillary/14/3249/6258.mvt
7805
7806diff --git a/test/data/pbf/mapillary/14/3248/6258.mvt b/test/data/pbf/mapillary/14/3248/6258.mvt
7807new file mode 100644
7808index 0000000000000000000000000000000000000000..ff6a462a3d79d0250c190bf9700e52dd35011d3c
7809GIT binary patch
7810literal 4256
7811zcmaLaeT>`W9S89CdO%xV#!API^f-=iv;~?rvE$fj=}Vl%vE#hIC+%SzpYt9k@k?T7
7812zXsQtF1_F&qs7MTi#29pJswy;1jG=0qs;;YGtERoM0a{_uy)oFPPK>TXd!G12>_4{q
7813zBRbvZ>)-Er9zXVTaeRAb;|*QD-|TdF*>EZ1KoccFR5m@dcKyZIe$d;ram|{wYiDNG
7814zt(%>ln{jFz4j=fy^YS`o^YYyCoZw(La}LfS%qQpg1#W?v6P%ng>^SHwE-X8joy`2Q
7815zlUs@}3QOR32fvkf#^*UF>)@ORfwGR}rGp^D1C7rwfBFgjvXxmV?<nugBEcQW-3;ki
7816z*A@-OY!iN7r75Bef4N%V`>H03D;%#nX8XL<bIxtNaiy=VNQe(H#y-BOYA8Bx;MEQk
7817zp(F7lwB7HunSjUzl?0aVHb+oMPczY?y~&1zU_vGkNi{^ZHgaybY0aZoJsN&tNIGX0
7818z*K9w$sIPT=xUlj1>1s8*d>t&+g1J)HO;_rQE3Uk9Zmsi*4d2^;KQi^Jz)G%%EBUFv
7819zzV8W(Zvej0=9l)qXz{CoUt{y5dtbHqwZN~l`PsYATl_P?uebSM_FS^~XMu0B`9pgi
7820zL#N}v0r+N{KezjY#Xkr9Mw|cIf5zgU2R>)>i^K(s-vr!Y^T&t>ywm<$fY00fEdHd$
7821zZw9_#^9#O{7QY4f;;P>|xj#pI=Pm98ew*$3A7Ym*z6AVso4@OQ5}S^1EAVYLf7tu7
7822z#kT{$!{*1)H!XfA@Vjh&4*A65JAk`vegt{KH|@U@_%53t^SoklH*k;5&$!Q99088n
7823z{L-$&__V$kIA-&syPmbU4>)e~mv)}EI04*m^AB9-ExsH09-BYxIzmkQ-wk}P&0pAY
7824z%;Nii@3;Azcb&5M0pJ0fU%vB##Yx~no1eJzuz%V=1Uzi>b9X#raSC{3)y@5Nar-HY
7825z)4&<q^+&f~uy_<WYxA?)4)31!j{%R{{Qa%ZT08+fY4b<7p0;=jc-rQtZ-2+)8Q@u)
7826zzq@o~&$NFIcz)GapP!{;7B2uV+OB`;wo?`_0WaIG|DE$s7Owz5X!FarK6LlA{~_SZ
7827zw))3!ea_<d0OxFeZt<+e?*-1={GYeHZ}BQ{!RE(q`O4mD{}te(%}*~Jw|EVB-R75X
7828ze%0a);LTMx*YBB||84OW@V3p*%s;np+P?$5Yx9d+Ubnaeyl3+hTOQayy}l28u<GXd
7829zbl!2&;xcf>=0_ce4>)E7-hSGe$1yx|9kYt<P`-Ac@tvk>aEj2AMR0`v{G;q2zWGt_
7830zfb}#5M{bN`Svg4Z9#sz!Z57x3Wp^zA&)NTfi2skHdS-DJH24XNKNxry^a2h1!)B!4
7831z8-@BS?uwt(DJfL16#^ji_IUI0$j4@=7?KDq7R!tc5>XhI4wQ?*XdQ(1WB0twTsA`j
7832zACFh+O}cF0y)3VZL!_K+ra{Q{;!__!|Be|V@=<R_8-_8)Th-hGGNRIQ8iu}n<8OcV
7833z(Hmw+mrJc`yHU*JO1*|gR&p*E--iEZ%oeV5z4#+DWMGke%1Dx!*b${lCs>XR@+chm
7834z=;YImUEeoDQLNpP(DazDj%g%Nt`eG>XJE+q^H$ILV`gZ873w03rJ7t)47EhsC<(X#
7835zX9wnX&CJ(V+p!51RYXCjd+lJDELG%YSca8eTKLjm)AyMngc9SGVX30C_%K$kqxF8Q
7836zTZ6OP=2`!p`TNaKge;HyisEK@cQBrer{xHqF2Z)HM;@0pWzA4C;3I^zuhy%&<9*E-
7837zwgU)1hV8cOdTZ%Q#th~BK943D^;}}$Z`5U0$#Y?E9c+$uAb8swAAhk$m_f8wZ2Mhh
7838z)FY<pRxZt^<YGmF)!sYjeB<MS8PZFEmV!29m7x5-T#b-@Jr;%IdN6SBd*>)KM7Od-
7839zL#SnX#F)jh%(xTvNGzP;AOG{MfBYhBhLAvk2*k#Di3~TiVWXd-IWY)B@2A0PMa&T1
7840zA~nR9Otjb@k9rCoa>V6z*zVF7Td%15%}}+9jYmW%5$?K~mC!)(Bn=sXmEcBr>Mk>s
7841zYmM?^kfLdWN|#D`9H;AI9L{dCr?!|OnJJTW56xq7ArTA_DoPb2E*MHwuP^!CW+<x=
7842zQCc$wqoSS^O1K`&bAce-cu$^MJRyh7P<*7X&?>D+F|okWky_3x^{cSbSAYDk?S}+2
7843zREi4~1uX<{Bu~W{i6}H`ZUGMbS31~vi)M(a(+Mxx>jX+fA}ceVXjny)F!TlR=)Jwu
7844z3=O(%oJ5U8&_7N!n@yvp<~&~5ZgPWTW{5Kg#zXbhU{-9GYpIB<5Y}CA;FH_ZG(+t~
7845zJ~oz-kSiOC31q~D6UCAWEBU{BW_Xe{L%qlfH;DVRkwFpJdH_omwL%hBIs~VeGD9k9
7846zRP{cd;6vV$SfdCwG6>gU2tImm9Wq0q_@I=o7SqL|Ah{^EuXQvn4MVTJ3Vyt1D4JpF
7847z-9U-U=na3M(TH<CEHr|lPksp&DsP4~AywrE4Zlk*;sr(!+I%ppz;(I<4(fmzs@61p
7848zW!UWz*)Wdi9a1K%UN@W_oZQo-85-3ocXx#2L$AB9MEg<Po2rFirOE7em?4&@+8MFa
7849z(2QI(#PhL??4cMK+VlJ5Bab^*+fgMWmGXL%bPsDq2CZS)MWZk@xxpnf<j>YUv^!E|
7850zw2@L$DIp$fHgz~A_&TVpKAdtl?@6hnV4cAjCKHRdBo97-OE5VS@YQx<Br#0ZDxqi{
7851zNi@@uVb)#t!O-4~zxhTVGeabWv;u5*RO>g?U`bcJNJjR<F@fDQ-n`EYX;Mqf8+xjk
7852z@pb6f5FH{yhlOLB?D5t8)M79?qcUX-_ovE*VpfcgIU0uk{`_eczsamr7WEv3#!4YC
7853z-jU>rJZ$GmlXjDbbM-|=w7qV**{akB9>Y&Z+)9-!5wOxEw9TyKB7!+jk8k^siss9)
7854zqqavS!|+`IPma^;%+NScN%S;-TdaU*S4Ms15Xn=p(&SipL7E(AcoLQ3G}tKtywT>;
7855z6s9BI$*C}T5HvG1lni<7%>`PB3mFdMiL6)7YOqq{Yk#|M{{}OJa4e@MB9Tmo?F^fV
7856zq^?ME>(kIryOVa2Vi60(!3(T5r1X)N$%cJ&0S=r5uS{pp3{^?+H9AK57+zK?Za&+P
7857z(vdc-G})P#I+IF43iAn^9BU$KMX3t;cEnFLVI>6)>S{Ao!3*V)XB0%E^>NFSAG_py
7858KI$xiB<NhzTVpF>S
7859
7860literal 0
7861HcmV?d00001
7862
7863diff --git a/test/data/pbf/mapillary/14/3249/6258.mvt b/test/data/pbf/mapillary/14/3249/6258.mvt
7864new file mode 100644
7865index 0000000000000000000000000000000000000000..c5278577ec4e161a9cb5acf787c0a3ec9d5ea6d5
7866GIT binary patch
7867literal 8703
7868zcmaKyd)OOQnZVo93oTGefl@wd`C1BXY11Z?%S?J@a-Uo#llw($oFtRUWpbS)Gno}d
7869zEl{9Ts1*?r70a!5Yh~H4)@~QOwRW*8mI9)Pii(K9mdd)Q$nH6FKFORvcJoJ{?|Xi~
7870zdEYnZoO#bV-_wal&zpB?lke0diSLa}vd|p}t&$)wymiL;Z~x%i?S=EEO`AS_#*FjM
7871zn>lmwjOA}lpL^2{`)BQ&GkvxxXbP?muMb}1um-H2)fDO3=vq&>H@P?1ulGAb>!NEn
7872zPsXluFU?G5CL<Gt$->gm1V5RZ3@t9`zn4~kpH^(Uj+xxHDzo;3X7{R$EwjPBF18^;
7873zZC#h35}Tasq8k!hVw-4lluB)Kugy5<wF&T3Y{S-du`L^kh$rMDDR;mfv~Qu=t-<w_
7874zGe8FIfo0}38`f-CX<NT#rP=DTfZFUqGPq`w-A4vZn@Ep^^18emoY#lUDS}>}SYB8f
7875zUA%3gG!eb5FcDk4ZE<NLvZ%CV+mgtl!jkFJCeCXJqluX{`0t#7EcD<H7nJx;f6x=k
7876z1-?Hqv%|OA%NNgERPGGQEy6*JGzVYn53E+LN){zZN>Yngtc^~)!O%j%EwlDmv!+1e
7877zveejmz!t5x2zx0``?;#nuPDpsUODZqId^&Y4PDD;Oio*M)1*3m;>S14TQHugT9X&S
7878zJS|1jG=Drzvu4eny?FZaS##gp`uMCdp97}xd^nBo^QX7IYv6MMpEt!X(4T*B>xtRp
7879z=Pv~OBJBBdTXxSG^Z9^ZjPYYzUNrCpfM0^~2e#}!fBgJHz%Rx4Yny*+;Fke@ImQod
7880zeqiqS`9*+Vf$^Q2pEK~qfM1F6Gn?+4H-3Hs@T)L>@1_?Gd<ozm!1${hcU>@kektHr
7881zV|?$%rwn`&@MReP<%aDSj-Ou+_%#^cx8Z<+uK@g7jKA#sje%bW_)3i5<J^7GIQ}ZY
7882zS7ZDHb=1JG2Yk&GNAtgfdda}o0&c>dKSXYyKaRf+@bwr!=XlV-&461lexKu*ffImR
7883zF@DVcvVq$Gw`2Tm+q(wt0G!159^3sFkL#lVcVher>tO@m0Qkl!j=uk^#2EwM1o&p`
7884z`CEv$4SWmWTQR=hvS-0K{|$iC82_btzk#~|cVqk}^C<)O0Pe;3<Lh5Ba3A1)jGtb2
7885z%O&Id0l*oIziHZM;6cDc7=O@o%)rBdM=*YD?JEY(0v^TqZ`SNwIL^-j9-HEEy}#VI
7886zX1{?a08e84$n|FoJOy|fkH7k+OULmufM+qjWA#A;&jFst`0-UgG4Kxp{vnLNzH;Yf
7887z<M`VE|1ie)tUO`h1;9Uo@u#nQ+rW9iix_|J+5?x5<Cg$0WBlN?uNb%hcm?BUSL|6d
7888zeqIE;it+nboHFnl;B}0jyk^H0<L4WIOBmmE%^?GC0^Y*-(dB0iybX8<<F73{uy`E5
7889z3wRIX_bz+Qz-7Su7=Lc^zAMMi4*(xx{Fcen2Ce|EV*KFMyC%lZj{w&&{`%6x2L2C#
7890ze-z_;mYy~68v*|q#!r9X{;L*VjrQM*r+pmoo2FyJdrQt-wQv#%3#NTyIxs$o8;>rz
7891zYsuL76g0Nu#&cIaWiW1r#x1yU&sFajj9a0x12>LO9QwdG%WcrO9XEDNylOD+fW}VT
7892zIDF;)rQ;ZPLSq+hyt(+C!T2;Z?!t{ji}zhUj&V0McH_pmE6y2=&p=}jZXCGc$mBT2
7893zXQ6QqZoISTF9zdt(6|>j4lO#lY#ihB(6|pb{&e~7<zwRu(6}ErPF((!!T2IH9>9&A
7894zm+!u29OFyS_%d#sx$H%Q@fB!1h#L=Fws*xi##f=S7dPI$^o+rH2pSLL#?zPXx^^7n
7895z5okP$8+$K3X)qpx#y;G5cj3DR<7?2^j~k~J9=>jz<sYGO05|q7e9d5d9U2F5<Lo8-
7896zSB_(R0~(Lx#?ed88jOE}#uK=)=aNTPjbr>XG!Eg$OAF2!jDLZ~H*w?P1xHqoV|)u5
7897zhjHUC7r$dLz735dxN-dAqt}mP{3|q$;>OO4cdQv3{|1dGapR@=#|_4Jpm7X09-hBr
7898z?KsB2L*qDZynWG=2IITXIDs1nF8ZWt9OHY?IQe%2ZPc$`c*J1*2Q;4gyMeA62QGZY
7899zV0<4Mr*Pxt3+`Dr&hi6j{17*GUU1T2{0JIP<Hl3-UNaa!hQ?{!_|x2->&IE1fyNo!
7900zxM%J@gYhghp2Lk3=N~f|&qL!Uxbf<o7Y)V>(D*5CY@hQdgYh$Hyoei5%)Z?`uJq^7
7901zcnLSo%-U-(egTbN;>IUu9WfZcg2v0Zv3KSfgYj!<{029kJnv0|@mpxTf*a>%Y`2Un
7902z{T(!3#f@DvzHBgl4~^Gw<KXlomcMV63#R=68m}9TnRAP$4Z<5xcoP?9ylI)3QR4Bd
7903zH@cg{`}f358M~oh)E)3rtv@QrrFKsM_vr;$=MyLM^&5;=YjFP#g<NjQQ*mnvx6^K`
7904zF^$To*Hz%9`~Sbu{~!1E8Iv<Xg3<4<vz*@nxj+J?Q4TsY`Dm<QWf^OW3Wgl*WDH2n
7905z-~IKmw{Az$z;Dk>!w%&uGu~V=oa;qBf<FeTI1h+F`tQWAANnd1<AIh`57dhtvs#Wf
7906z)S_MLRu~aT>G`I|zxXvINd%n=aG?P$MznOyPg8Xz+@OK<y%2dD{SK1YVai&Oq-HwR
7907z^^<D8Qz_f?Jy;hk>-3ckz4&m{(<Ia8f~JYI)s<yDoZIGwQt-kVYi_+BNy$js6m$sH
7908zPBJLlSZ}uM$@t6gPZ)5-hj}Cg;svu*5m+@76!^Z?RZYvB7Z!eRYvHG!+>Ioq;~ud!
7909zzp9WW$wHbvQcAP8VLJHb{_`0mHNw6$7mv9`zB+W(Jh>Q4P%bEa=+npkYn3YLg_lx^
7910zPJpR&G>b-Rt&CNQc$^Vf)199K`3@qflM))Tn#o68b|+&hsshy|z}(Nha^G90BgL~w
7911zQV2_2j241iqvNf$#aLS%XhS%ddf|(jdOFeTZ4HVFpNZ2ZZ>Qp*hs;2OHT@zA5*|Pj
7912zWo0YLKs=dg)<()G8{##d)W6+EX#E9AvG`Dx**HzqA|tM=nl)$LNx*SB(*MljnQTi>
7913zS11kTs#d0bCc9a**J+aVWuh?Mja%&>K71pRM3t8d9nr^mvw>2q?B{ZJr3b&=kC*@K
7914z&mVgZNoH%ZGICjk21%8~R>oH5>a_%nbpP?mgT1wFJ)NiE%Z2Gc)1PET2kDBKB0N8Y
7915zHT_Dv?0}y^QZ>wxWYJ8e%$*w9_xDO3n_piltKqyPkrYdeLQyj<xT0QHL+(=celJ^q
7916zgDF?<e&eGel8Qt%)QiY<Hc=ZE>r@p)OR%seeF%0VDc2gvb+cS;t8Cg|i^m8qk*z{$
7917z8vNXaq;kVw4=aq#<Fw^LR8v3T@zr4AAlX*?$EG6DD$}E4v4Yp7wbIU3$k$^fSkpI`
7918z-u#DH7D?<-Bg&odut%6Hsyme_RD`+=2lH7Kyz4h1sg&-~{Z`*2^cXU#rg$ci>`G87
7919z!x=6h$>PzhLNioOWQ*Qf)!TK~z2OK<_k&a5^HC&qHJ;!Tb=l$xTjGiq%sA4N2n(;n
7920zF_@Ap{(h&`tkzjVvE>NbD+#n)h3Q`SA?O$}6e-`RSzP&aQg!f>$z|pgrZLnbodZjJ
7921z!FD7u)}lKfA60WMQ&Zt+Ic8F<J(%wMU-2ILqa8_6t!5t7TK$kF`K?Z`<TY2@1vuiC
7922z^Y<^wPa>&8$)p-4h=3FnYI3NRE(E(>DCu40Ewt!t)`(y7IbxP>SanH#mzT?xC@AR-
7923z<1~`O5*Kfmg+|{~P)+`7kDzS9m<U?pbRhovp)VYt+>S(7!J15kn>M294HJ1sqv~r7
7924zt8hT!lG?frNm{a)jMj#1pym#=X`i1pdj$?k`s!SZq+wcQQmR_vy!lEcYf=jB(I5n+
7925zWe0oJyZ?+{&@$^E<*QsvbH(^(FI?{r`a}_qx;|FlKvI}b^232Wku<SwF&=bu>VjWi
7926z!l2hZ^_9;fsnRy(%qFGI`TS}(CfgV{-)g~hXCFBcA~z$68VOc8qGftbt174!UM_J(
7927zCzSLzrj4YAFB@qGMlGV3WE8?Ax1$|VA9ejzcOfb6kOMufASY-~miDv$W;o=mz(~)4
7928zg^;}&No5P)j#fFfE!I=MZqF>3)9oN!32;Z-&rL-tcWQ1yrHN+Vn~M--yO^%T;rt!?
7929z<KNDxP9zcGPQx3uG3h8B8f8iyuI3U+D1GF&PcQ%3B}f9V66<JMdpy%2L+M0NELdVJ
7930ztV!RkHy|nRsPHrq3ze8)e%O_pk%7zFhtkhi%>Kk@H%>{apUY5%CL8hyxma^3hsi+(
7931z7S6$r??;k7S&4y-zUuBp99FZ&#3@3u!kSjXHJnD$KpK{{As?$Uv?zD8rh>wntT0_0
7932zPF@X3qiiyftAf#rni$?yiWXIKw*u3FZE?{HyO2bf+)T}u)2LjV6r!18VZel&Fr9ur
7933zIE5rjxGoGWRF0&gu3$cBl6s282iH>tejZ1XRg1(;-qMg^Rj*S`QX|^o(>w4z?mML4
7934z$w+D?yD_yxMMq*u86=ggJe2!cm=129kKB%=fG1TRIFvv?l8HBqY`ehJa(V~W`#}~-
7935zVJ*T?0dptF1cDVQpQ|XjWFOY_aoB&tND^qjP4PNxyk%e!T<s=TWYYRhr5{DMBB>It
7936z2h8b2fZ^p_W@N3!vo$6JBk4=!4Yb9o7LUMp<XkpD`5c})RZglJ3kOqQ<6lQoG1P4|
7937z6_2ypX;M93C^s;t{Q)>FaoDGCL{hVF8a5J2f=-tc)@a+t#{=CKjHF8uBqhuWT`5&9
7938zO|e^K9a^eVk5~1Ep&yUVP8~&jM9e<)2`<hOH<c|Tu2Zu*U^=~~=aFP{jxrn-$$>Ku
7939zA=PqaFUxu@?~nLVByn^pAo|o?qapa*X(n7{giabt`tErrlBz?qoA#wzTwjSZ8fEtm
7940zd=4*62U=@+<JXa7FFQ!3AqEK{EM^_;%uq1<G$`o}e=1Tqk*qcpN{N_?=|nTfCPz^*
7941z3MGBSV<?hF(_El#j(dGtv}E$O$u=7v!J72_<%X%6_>`EhI0o(@*KZbzqi}<d^r57m
7942z4VECO7pl~AK}PMF8<|3cbH(g3O~Z70>zR$Dve5GNB4sz99oB_pv>O<edU+U0f6W&m
7943zDJPLmcg-%@L7N*E6LO_AYU#UzUehF!Y$0nSUX2Q*7VO#`X35!zWJ55L-dLST3S~2j
7944zi%ty^DSJ0R$a#vis~U$j-S>I$VURSa<P}Bk6btdVj}}vwPNG!Tdx3r~uONvs)kJ!f
7945zHPzd>P=(3n*mm73z;ycIuZSdpY_>|SKtImA)Gpf?3`lQk0MmW=FmvII^+?J)g+iM5
7946z<XK9U*s`M$CEIrWY;YS58saPHOz!D>B(3XmSMBLaz~boBVKzR3k@T7-P$WhjmRt2q
7947zk+V0uTp<$>`UQ)A^3i+293+La9kRuZ+AYb|ETozaMjKWW@4r%0H3=p;(;qecETKi?
7948zt|-k%5*9a<!r+?lk<}<tHJb0aQ*_ByNjF-}K~-s{do@@SXgq&?Y#EXga*{FGf`xRs
7949zBADum%T0|&B^c=nxHMNHsqSiLYZ}FrWw}0ZH1j<v$=Bduz60(zU+^Hw+;xT$jY=Zh
7950zQ-X=0&z^`=R()maZ}4}K6b%&}RkGWY>r5(6DXnsh)AVgaIr^oE^%nG+^G*xX=)`(_
7951zq)4UeUOPz_dKH)sZsu>?g`{e|*=t(5?)<<z2*(`mP+o}yV1Lpt6O*Wkm82xW3@z4J
7952zs#OW+JWbxr`ipP~^ds92B)NPe(KHgRWwSQ$nIq;vqei6Q5Wq#S=6{eBX0lSL;mt<n
7953zQZ&j1J-jFv(olL3oU{IR13HFu$y#0$iCmsF$HWqo9JV5jHcY24x&ufuMZ{`_OvI=*
7954z39cG7ksS1k8Y~>{KR>(!oyJmZsZ6Wkpw%C1rHD$pK*pnHI0WD-k)Ho8Bxxb9YA^U~
7955zU0=T!lm}d;oQtMmB>h@)6iHploz^NXQjD~#^>##}2du20?)7nc9Z3m47xz`meQ&hU
7956zOWUX*mvssWSU9+E)Q?O_oRAJne8lg|vflon(@f_jO~01tXSyk=m*A6@QX^xP`Anb@
7957zjE}^APu~&rmH8}6M+#O`l;(1IpKNb~<6>DfrNi*sJ^$6t=6TN}sbA~+xu{y{G;@?t
7958zOnVyPbY0&O^saIiNkmfwou^GEOJc#=baL51cw~V?pdXE1K$7I?iX5G@4&p?T5Odj*
7959z*dEp2Ur(oy<m=k2_5o{&ID&48B}t{%BWO6mdb9ryl6u*`KT_vArVf#h`U*LBP?Yp(
7960z(Kn!zNOBZRik9<pVaneh3|qCDlFDRYP4ArnSB|N<OKC}ynvQVEe%@M*D!e<Lcc`!?
7961h{aW%96sZ=<g~4A7N~XRy=OLW^ysCt)ivE8J{uhw)>!Sbw
7962
7963literal 0
7964HcmV?d00001
7965
7966diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
7967index 8983e8397..38ab53ad2 100644
7968--- a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
7969+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
7970@@ -132,7 +132,7 @@ class VectorDataSetTest {
7971 // Set the zoom to 14, as that is the tile we are checking
7972 dataSet.setZoom(14);
7973 // Load tiles that are next to each other
7974- loadTile(this.layer, 14, 3248, 6258, 14, 3248, 6257);
7975+ loadTile(this.layer, 14, 3248, 6258, 14, 3249, 6258);
7976
7977 Map<Long, List<VectorWay>> wayGroups = dataSet.getWays().stream()
7978 .collect(Collectors.groupingBy(VectorWay::getId));
7979--
7980GitLab
7981
7982
7983From 25dfa842314b093a39bb34169c82fa95b72938b7 Mon Sep 17 00:00:00 2001
7984From: Taylor Smock <tsmock@fb.com>
7985Date: Thu, 8 Apr 2021 15:56:47 -0600
7986Subject: [PATCH 07/50] Other frontend items
7987
7988Signed-off-by: Taylor Smock <tsmock@fb.com>
7989---
7990 resources/images/dialogs/add_mvt.svg | 147 +++++++++
7991 .../data/cache/JCSCachedTileLoaderJob.java | 47 ++-
7992 .../josm/data/imagery/ImageryInfo.java | 8 +-
7993 .../data/imagery/TMSCachedTileLoaderJob.java | 10 +-
7994 .../josm/data/osm/IRelationMember.java | 9 +
7995 .../josm/data/osm/IWaySegment.java | 177 +++++++++++
7996 .../josm/data/osm/RelationMember.java | 1 +
7997 .../josm/data/osm/WaySegment.java | 99 +------
7998 .../osm/visitor/paint/StyledMapRenderer.java | 7 +-
7999 .../relation/sort/RelationNodeMap.java | 73 ++---
8000 .../relation/sort/RelationSortUtils.java | 32 +-
8001 .../dialogs/relation/sort/RelationSorter.java | 10 +-
8002 .../gui/layer/AbstractTileSourceLayer.java | 30 +-
8003 .../josm/gui/layer/ImageryLayer.java | 3 +
8004 .../josm/gui/layer/imagery/MVTLayer.java | 278 ++++++++++++++++++
8005 .../gui/mappaint/mapcss/ConditionFactory.java | 11 +
8006 .../preferences/imagery/AddMVTLayerPanel.java | 94 ++++++
8007 .../imagery/ImageryProvidersPanel.java | 11 +-
8008 18 files changed, 891 insertions(+), 156 deletions(-)
8009 create mode 100644 resources/images/dialogs/add_mvt.svg
8010 create mode 100644 src/org/openstreetmap/josm/data/osm/IWaySegment.java
8011 create mode 100644 src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
8012 create mode 100644 src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
8013
8014diff --git a/resources/images/dialogs/add_mvt.svg b/resources/images/dialogs/add_mvt.svg
8015new file mode 100644
8016index 000000000..eeae80f10
8017--- /dev/null
8018+++ b/resources/images/dialogs/add_mvt.svg
8019@@ -0,0 +1,147 @@
8020+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
8021+<svg
8022+ xmlns:dc="http://purl.org/dc/elements/1.1/"
8023+ xmlns:cc="http://creativecommons.org/ns#"
8024+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8025+ xmlns="http://www.w3.org/2000/svg"
8026+ xmlns:xlink="http://www.w3.org/1999/xlink"
8027+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
8028+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
8029+ width="24"
8030+ height="24"
8031+ viewBox="0 0 24 24"
8032+ id="svg2"
8033+ version="1.1"
8034+ inkscape:version="1.0.1 (c497b03c, 2020-09-10)"
8035+ sodipodi:docname="add_mvt.svg">
8036+ <defs
8037+ id="defs4">
8038+ <linearGradient
8039+ gradientTransform="translate(4)"
8040+ gradientUnits="userSpaceOnUse"
8041+ y2="1049.3622"
8042+ x2="12"
8043+ y1="1041.3622"
8044+ x1="4"
8045+ id="linearGradient868"
8046+ xlink:href="#linearGradient866"
8047+ inkscape:collect="always" />
8048+ <linearGradient
8049+ id="linearGradient866"
8050+ inkscape:collect="always">
8051+ <stop
8052+ id="stop862"
8053+ offset="0"
8054+ style="stop-color:#dfdfdf;stop-opacity:1" />
8055+ <stop
8056+ id="stop864"
8057+ offset="1"
8058+ style="stop-color:#949593;stop-opacity:1" />
8059+ </linearGradient>
8060+ </defs>
8061+ <sodipodi:namedview
8062+ id="base"
8063+ pagecolor="#ffffff"
8064+ bordercolor="#666666"
8065+ borderopacity="1.0"
8066+ inkscape:pageopacity="0"
8067+ inkscape:pageshadow="2"
8068+ inkscape:zoom="45.254834"
8069+ inkscape:cx="11.376506"
8070+ inkscape:cy="17.057298"
8071+ inkscape:document-units="px"
8072+ inkscape:current-layer="layer1"
8073+ showgrid="true"
8074+ units="px"
8075+ inkscape:window-width="1920"
8076+ inkscape:window-height="955"
8077+ inkscape:window-x="0"
8078+ inkscape:window-y="23"
8079+ inkscape:window-maximized="1"
8080+ viewbox-height="16"
8081+ inkscape:document-rotation="0">
8082+ <inkscape:grid
8083+ type="xygrid"
8084+ id="grid4136"
8085+ originx="0"
8086+ originy="0"
8087+ spacingx="1"
8088+ spacingy="1" />
8089+ </sodipodi:namedview>
8090+ <metadata
8091+ id="metadata7">
8092+ <rdf:RDF>
8093+ <cc:Work
8094+ rdf:about="">
8095+ <dc:format>image/svg+xml</dc:format>
8096+ <dc:type
8097+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
8098+ <dc:title></dc:title>
8099+ <cc:license
8100+ rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" />
8101+ </cc:Work>
8102+ <cc:License
8103+ rdf:about="http://creativecommons.org/publicdomain/zero/1.0/">
8104+ <cc:permits
8105+ rdf:resource="http://creativecommons.org/ns#Reproduction" />
8106+ <cc:permits
8107+ rdf:resource="http://creativecommons.org/ns#Distribution" />
8108+ <cc:permits
8109+ rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
8110+ </cc:License>
8111+ </rdf:RDF>
8112+ </metadata>
8113+ <g
8114+ inkscape:label="Layer 1"
8115+ inkscape:groupmode="layer"
8116+ id="layer1"
8117+ transform="translate(0,-1037.3622)">
8118+ <rect
8119+ ry="0.48361239"
8120+ y="1043.8622"
8121+ x="5.5"
8122+ height="3"
8123+ width="13"
8124+ id="rect833"
8125+ style="opacity:1;fill:#c1c2c0;fill-opacity:1;fill-rule:nonzero;stroke:#555753;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.839909;stroke-opacity:1;paint-order:normal" />
8126+ <rect
8127+ transform="rotate(-90)"
8128+ ry="0.48361239"
8129+ y="10.5"
8130+ x="-1051.8622"
8131+ height="3"
8132+ width="13"
8133+ id="rect833-5"
8134+ style="opacity:1;fill:#c1c2c0;fill-opacity:1;fill-rule:nonzero;stroke:#555753;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.839909;stroke-opacity:1;paint-order:normal" />
8135+ <path
8136+ inkscape:connector-curvature="0"
8137+ id="path852"
8138+ d="M 6.0000001,1044.3622 H 11 v -5 h 2 v 5 h 5 v 2 h -5 v 5 h -2 v -5 H 6.0000001 Z"
8139+ style="fill:url(#linearGradient868);fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
8140+ <path
8141+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
8142+ d="m 4.5,1060.3625 v -7.5948 l 2,4.3971 2,-4.3971 v 7.5948"
8143+ id="path894"
8144+ sodipodi:nodetypes="ccccc" />
8145+ <path
8146+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
8147+ d="m 17.5,1060.3622 v -8"
8148+ id="path896" />
8149+ <path
8150+ style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
8151+ d="m 15,1052.8622 h 5"
8152+ id="path898" />
8153+ <text
8154+ xml:space="preserve"
8155+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:9.3042px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.894202;stroke-miterlimit:4;stroke-dasharray:none"
8156+ x="10.59868"
8157+ y="898.41876"
8158+ id="text854"
8159+ transform="scale(0.84728029,1.180247)"><tspan
8160+ sodipodi:role="line"
8161+ id="tspan852"
8162+ x="10.59868"
8163+ y="898.41876"
8164+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:9.3042px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill-rule:nonzero;stroke-width:0.894202;stroke-miterlimit:4;stroke-dasharray:none">V</tspan></text>
8165+ </g>
8166+</svg>
8167diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
8168index a8561a771..eeac761c6 100644
8169--- a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
8170+++ b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
8171@@ -1,10 +1,13 @@
8172 // License: GPL. For details, see LICENSE file.
8173 package org.openstreetmap.josm.data.cache;
8174
8175+import java.io.File;
8176 import java.io.FileNotFoundException;
8177 import java.io.IOException;
8178+import java.io.InputStream;
8179 import java.net.HttpURLConnection;
8180 import java.net.URL;
8181+import java.nio.file.Files;
8182 import java.security.SecureRandom;
8183 import java.util.Collections;
8184 import java.util.List;
8185@@ -17,8 +20,6 @@ import java.util.concurrent.ThreadPoolExecutor;
8186 import java.util.concurrent.TimeUnit;
8187 import java.util.regex.Matcher;
8188
8189-import org.apache.commons.jcs3.access.behavior.ICacheAccess;
8190-import org.apache.commons.jcs3.engine.behavior.ICacheElement;
8191 import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult;
8192 import org.openstreetmap.josm.data.imagery.TileJobOptions;
8193 import org.openstreetmap.josm.data.preferences.IntegerProperty;
8194@@ -27,6 +28,10 @@ import org.openstreetmap.josm.tools.HttpClient;
8195 import org.openstreetmap.josm.tools.Logging;
8196 import org.openstreetmap.josm.tools.Utils;
8197
8198+import org.apache.commons.compress.utils.IOUtils;
8199+import org.apache.commons.jcs3.access.behavior.ICacheAccess;
8200+import org.apache.commons.jcs3.engine.behavior.ICacheElement;
8201+
8202 /**
8203 * Generic loader for HTTP based tiles. Uses custom attribute, to check, if entry has expired
8204 * according to HTTP headers sent with tile. If so, it tries to verify using Etags
8205@@ -294,6 +299,43 @@ public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements
8206 if (attributes == null) {
8207 attributes = new CacheEntryAttributes();
8208 }
8209+ final URL url = this.getUrlNoException();
8210+ if (url == null) {
8211+ return false;
8212+ }
8213+
8214+ if (url.getProtocol().contains("http")) {
8215+ return loadObjectHttp();
8216+ }
8217+ if (url.getProtocol().contains("file")) {
8218+ return loadObjectFile(url);
8219+ }
8220+
8221+ return false;
8222+ }
8223+
8224+ private boolean loadObjectFile(URL url) {
8225+ String fileName = url.toExternalForm();
8226+ File file = new File(fileName.substring("file:/".length() - 1));
8227+ if (!file.exists()) {
8228+ file = new File(fileName.substring("file://".length() - 1));
8229+ }
8230+ try (InputStream fileInputStream = Files.newInputStream(file.toPath())) {
8231+ cacheData = createCacheEntry(IOUtils.toByteArray(fileInputStream));
8232+ cache.put(getCacheKey(), cacheData, attributes);
8233+ return true;
8234+ } catch (IOException e) {
8235+ Logging.error(e);
8236+ attributes.setError(e);
8237+ attributes.setException(e);
8238+ }
8239+ return false;
8240+ }
8241+
8242+ /**
8243+ * @return true if object was successfully downloaded via http, false, if there was a loading failure
8244+ */
8245+ private boolean loadObjectHttp() {
8246 try {
8247 // if we have object in cache, and host doesn't support If-Modified-Since nor If-None-Match
8248 // then just use HEAD request and check returned values
8249@@ -553,6 +595,7 @@ public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements
8250 try {
8251 return getUrl();
8252 } catch (IOException e) {
8253+ Logging.trace(e);
8254 return null;
8255 }
8256 }
8257diff --git a/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java b/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
8258index 07cabc76a..32b1055ed 100644
8259--- a/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
8260+++ b/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
8261@@ -61,7 +61,9 @@ public class ImageryInfo extends
8262 /** A WMS endpoint entry only stores the WMS server info, without layer, which are chosen later by the user. **/
8263 WMS_ENDPOINT("wms_endpoint"),
8264 /** WMTS stores GetCapabilities URL. Does not store any information about the layer **/
8265- WMTS("wmts");
8266+ WMTS("wmts"),
8267+ /** MapBox Vector Tiles entry*/
8268+ MVT("mvt");
8269
8270 private final String typeString;
8271
8272@@ -654,7 +656,7 @@ public class ImageryInfo extends
8273 defaultMaxZoom = 0;
8274 defaultMinZoom = 0;
8275 for (ImageryType type : ImageryType.values()) {
8276- Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+)\\])?:(.*)").matcher(url);
8277+ Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+)])?:(.*)").matcher(url);
8278 if (m.matches()) {
8279 this.url = m.group(3);
8280 this.sourceType = type;
8281@@ -669,7 +671,7 @@ public class ImageryInfo extends
8282 }
8283
8284 if (serverProjections.isEmpty()) {
8285- Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)\\}.*").matcher(url.toUpperCase(Locale.ENGLISH));
8286+ Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)}.*").matcher(url.toUpperCase(Locale.ENGLISH));
8287 if (m.matches()) {
8288 setServerProjections(Arrays.asList(m.group(1).split(",", -1)));
8289 }
8290diff --git a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
8291index e9f163781..1f2c0d1d7 100644
8292--- a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
8293+++ b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
8294@@ -10,6 +10,7 @@ import java.net.URL;
8295 import java.nio.charset.StandardCharsets;
8296 import java.util.HashSet;
8297 import java.util.List;
8298+import java.util.Locale;
8299 import java.util.Map;
8300 import java.util.Map.Entry;
8301 import java.util.Optional;
8302@@ -33,6 +34,8 @@ import org.openstreetmap.josm.data.cache.CacheEntry;
8303 import org.openstreetmap.josm.data.cache.CacheEntryAttributes;
8304 import org.openstreetmap.josm.data.cache.ICachedLoaderListener;
8305 import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob;
8306+import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
8307+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
8308 import org.openstreetmap.josm.data.preferences.LongProperty;
8309 import org.openstreetmap.josm.tools.HttpClient;
8310 import org.openstreetmap.josm.tools.Logging;
8311@@ -149,7 +152,7 @@ public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, Buffe
8312 private boolean isNotImage(Map<String, List<String>> headers, int statusCode) {
8313 if (statusCode == 200 && headers.containsKey("Content-Type") && !headers.get("Content-Type").isEmpty()) {
8314 String contentType = headers.get("Content-Type").stream().findAny().get();
8315- if (contentType != null && !contentType.startsWith("image")) {
8316+ if (contentType != null && !contentType.startsWith("image") && !MVTFile.MIMETYPE.contains(contentType.toLowerCase(Locale.ROOT))) {
8317 Logging.warn("Image not returned for tile: " + url + " content type was: " + contentType);
8318 // not an image - do not store response in cache, so next time it will be queried again from the server
8319 return true;
8320@@ -320,10 +323,11 @@ public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, Buffe
8321 private boolean tryLoadTileImage(CacheEntry object) throws IOException {
8322 if (object != null) {
8323 byte[] content = object.getContent();
8324- if (content.length > 0) {
8325+ if (content.length > 0 || tile instanceof VectorTile) {
8326 try (ByteArrayInputStream in = new ByteArrayInputStream(content)) {
8327 tile.loadImage(in);
8328- if (tile.getImage() == null) {
8329+ if ((!(tile instanceof VectorTile) && tile.getImage() == null)
8330+ || ((tile instanceof VectorTile) && !tile.isLoaded())) {
8331 String s = new String(content, StandardCharsets.UTF_8);
8332 Matcher m = SERVICE_EXCEPTION_PATTERN.matcher(s);
8333 if (m.matches()) {
8334diff --git a/src/org/openstreetmap/josm/data/osm/IRelationMember.java b/src/org/openstreetmap/josm/data/osm/IRelationMember.java
8335index c2803e38d..69091056d 100644
8336--- a/src/org/openstreetmap/josm/data/osm/IRelationMember.java
8337+++ b/src/org/openstreetmap/josm/data/osm/IRelationMember.java
8338@@ -66,4 +66,13 @@ public interface IRelationMember<P extends IPrimitive> extends PrimitiveId {
8339 * @since 13766 (IRelationMember)
8340 */
8341 P getMember();
8342+
8343+ /**
8344+ * Returns the relation member as a way.
8345+ * @return Member as a way
8346+ * @since xxx
8347+ */
8348+ default IWay<?> getWay() {
8349+ return (IWay<?>) getMember();
8350+ }
8351 }
8352diff --git a/src/org/openstreetmap/josm/data/osm/IWaySegment.java b/src/org/openstreetmap/josm/data/osm/IWaySegment.java
8353new file mode 100644
8354index 000000000..0735935de
8355--- /dev/null
8356+++ b/src/org/openstreetmap/josm/data/osm/IWaySegment.java
8357@@ -0,0 +1,177 @@
8358+// License: GPL. For details, see LICENSE file.
8359+package org.openstreetmap.josm.data.osm;
8360+
8361+import java.awt.geom.Line2D;
8362+import java.lang.reflect.Constructor;
8363+import java.lang.reflect.InvocationTargetException;
8364+import java.util.Arrays;
8365+import java.util.Objects;
8366+
8367+import org.openstreetmap.josm.tools.Logging;
8368+
8369+/**
8370+ * A segment consisting of 2 consecutive nodes out of a way.
8371+ * @author Taylor Smock
8372+ * @param <N> The node type
8373+ * @param <W> The way type
8374+ * @since xxx
8375+ */
8376+public class IWaySegment<N extends INode, W extends IWay<N>> implements Comparable<IWaySegment<N, W>> {
8377+
8378+ /**
8379+ * The way.
8380+ */
8381+ public final W way;
8382+
8383+ /**
8384+ * The index of one of the 2 nodes in the way. The other node has the
8385+ * index <code>lowerIndex + 1</code>.
8386+ */
8387+ public final int lowerIndex;
8388+
8389+ /**
8390+ * Constructs a new {@code IWaySegment}.
8391+ * @param w The way
8392+ * @param i The node lower index
8393+ * @throws IllegalArgumentException in case of invalid index
8394+ */
8395+ public IWaySegment(W w, int i) {
8396+ way = w;
8397+ lowerIndex = i;
8398+ if (i < 0 || i >= w.getNodesCount() - 1) {
8399+ throw new IllegalArgumentException(toString());
8400+ }
8401+ }
8402+
8403+ /**
8404+ * Returns the first node of the way segment.
8405+ * @return the first node
8406+ */
8407+ public N getFirstNode() {
8408+ return way.getNode(lowerIndex);
8409+ }
8410+
8411+ /**
8412+ * Returns the second (last) node of the way segment.
8413+ * @return the second node
8414+ */
8415+ public N getSecondNode() {
8416+ return way.getNode(lowerIndex + 1);
8417+ }
8418+
8419+ /**
8420+ * Determines and returns the way segment for the given way and node pair.
8421+ * @param way way
8422+ * @param first first node
8423+ * @param second second node
8424+ * @return way segment
8425+ * @throws IllegalArgumentException if the node pair is not part of way
8426+ */
8427+ public static <N extends INode, W extends IWay<N>> IWaySegment<N, W> forNodePair(W way, N first, N second) {
8428+ int endIndex = way.getNodesCount() - 1;
8429+ while (endIndex > 0) {
8430+ final int indexOfFirst = way.getNodes().subList(0, endIndex).lastIndexOf(first);
8431+ if (second.equals(way.getNode(indexOfFirst + 1))) {
8432+ return new IWaySegment<>(way, indexOfFirst);
8433+ }
8434+ endIndex--;
8435+ }
8436+ throw new IllegalArgumentException("Node pair is not part of way!");
8437+ }
8438+
8439+ /**
8440+ * Returns this way segment as complete way.
8441+ * @return the way segment as {@code Way}
8442+ * @throws IllegalAccessException See {@link Constructor#newInstance}
8443+ * @throws IllegalArgumentException See {@link Constructor#newInstance}
8444+ * @throws InstantiationException See {@link Constructor#newInstance}
8445+ * @throws InvocationTargetException See {@link Constructor#newInstance}
8446+ * @throws NoSuchMethodException See {@link Class#getConstructor}
8447+ */
8448+ public W toWay()
8449+ throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
8450+ // If the number of nodes is 2, then don't bother creating a new way
8451+ if (this.way.getNodes().size() == 2) {
8452+ return this.way;
8453+ }
8454+ // Since the way determines the generic class, this.way.getClass() is always Class<W>, assuming
8455+ // that way remains the defining element for the type, and remains final.
8456+ @SuppressWarnings("unchecked")
8457+ Class<W> clazz = (Class<W>) this.way.getClass();
8458+ Constructor<W> constructor;
8459+ W w;
8460+ try {
8461+ // Check for clone constructor
8462+ constructor = clazz.getConstructor(clazz);
8463+ w = constructor.newInstance(this.way);
8464+ } catch (NoSuchMethodException e) {
8465+ Logging.trace(e);
8466+ constructor = clazz.getConstructor();
8467+ w = constructor.newInstance();
8468+ }
8469+
8470+ w.setNodes(Arrays.asList(getFirstNode(), getSecondNode()));
8471+ return w;
8472+ }
8473+
8474+ @Override
8475+ public boolean equals(Object o) {
8476+ if (this == o) return true;
8477+ if (o == null || getClass() != o.getClass()) return false;
8478+ IWaySegment<?, ?> that = (IWaySegment<?, ?>) o;
8479+ return lowerIndex == that.lowerIndex &&
8480+ Objects.equals(way, that.way);
8481+ }
8482+
8483+ @Override
8484+ public int hashCode() {
8485+ return Objects.hash(way, lowerIndex);
8486+ }
8487+
8488+ @Override
8489+ public int compareTo(IWaySegment o) {
8490+ final W thisWay;
8491+ final IWay<?> otherWay;
8492+ try {
8493+ thisWay = toWay();
8494+ otherWay = o == null ? null : o.toWay();
8495+ } catch (ReflectiveOperationException e) {
8496+ Logging.error(e);
8497+ return -1;
8498+ }
8499+ return o == null ? -1 : (equals(o) ? 0 : thisWay.compareTo(otherWay));
8500+ }
8501+
8502+ /**
8503+ * Checks whether this segment crosses other segment
8504+ *
8505+ * @param s2 The other segment
8506+ * @return true if both segments crosses
8507+ */
8508+ public boolean intersects(IWaySegment<?, ?> s2) {
8509+ if (getFirstNode().equals(s2.getFirstNode()) || getSecondNode().equals(s2.getSecondNode()) ||
8510+ getFirstNode().equals(s2.getSecondNode()) || getSecondNode().equals(s2.getFirstNode()))
8511+ return false;
8512+
8513+ return Line2D.linesIntersect(
8514+ getFirstNode().getEastNorth().east(), getFirstNode().getEastNorth().north(),
8515+ getSecondNode().getEastNorth().east(), getSecondNode().getEastNorth().north(),
8516+ s2.getFirstNode().getEastNorth().east(), s2.getFirstNode().getEastNorth().north(),
8517+ s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north());
8518+ }
8519+
8520+ /**
8521+ * Checks whether this segment and another way segment share the same points
8522+ * @param s2 The other segment
8523+ * @return true if other way segment is the same or reverse
8524+ */
8525+ public boolean isSimilar(IWaySegment<?, ?> s2) {
8526+ return (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))
8527+ || (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()));
8528+ }
8529+
8530+ @Override
8531+ public String toString() {
8532+ return "IWaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
8533+ }
8534+}
8535diff --git a/src/org/openstreetmap/josm/data/osm/RelationMember.java b/src/org/openstreetmap/josm/data/osm/RelationMember.java
8536index fc62c71f3..5add40403 100644
8537--- a/src/org/openstreetmap/josm/data/osm/RelationMember.java
8538+++ b/src/org/openstreetmap/josm/data/osm/RelationMember.java
8539@@ -57,6 +57,7 @@ public class RelationMember implements IRelationMember<OsmPrimitive> {
8540 * @return Member as way
8541 * @since 1937
8542 */
8543+ @Override
8544 public Way getWay() {
8545 return (Way) member;
8546 }
8547diff --git a/src/org/openstreetmap/josm/data/osm/WaySegment.java b/src/org/openstreetmap/josm/data/osm/WaySegment.java
8548index 2ca1cc379..302f82842 100644
8549--- a/src/org/openstreetmap/josm/data/osm/WaySegment.java
8550+++ b/src/org/openstreetmap/josm/data/osm/WaySegment.java
8551@@ -1,57 +1,26 @@
8552 // License: GPL. For details, see LICENSE file.
8553 package org.openstreetmap.josm.data.osm;
8554
8555-import java.awt.geom.Line2D;
8556-import java.util.Objects;
8557-
8558 /**
8559 * A segment consisting of 2 consecutive nodes out of a way.
8560 */
8561-public final class WaySegment implements Comparable<WaySegment> {
8562-
8563- /**
8564- * The way.
8565- */
8566- public final Way way;
8567-
8568- /**
8569- * The index of one of the 2 nodes in the way. The other node has the
8570- * index <code>lowerIndex + 1</code>.
8571- */
8572- public final int lowerIndex;
8573+public final class WaySegment extends IWaySegment<Node, Way> {
8574
8575 /**
8576- * Constructs a new {@code WaySegment}.
8577- * @param w The way
8578- * @param i The node lower index
8579+ * Constructs a new {@code IWaySegment}.
8580+ *
8581+ * @param way The way
8582+ * @param i The node lower index
8583 * @throws IllegalArgumentException in case of invalid index
8584 */
8585- public WaySegment(Way w, int i) {
8586- way = w;
8587- lowerIndex = i;
8588- if (i < 0 || i >= w.getNodesCount() - 1) {
8589- throw new IllegalArgumentException(toString());
8590- }
8591+ public WaySegment(Way way, int i) {
8592+ super(way, i);
8593 }
8594
8595 /**
8596- * Returns the first node of the way segment.
8597- * @return the first node
8598- */
8599- public Node getFirstNode() {
8600- return way.getNode(lowerIndex);
8601- }
8602-
8603- /**
8604- * Returns the second (last) node of the way segment.
8605- * @return the second node
8606- */
8607- public Node getSecondNode() {
8608- return way.getNode(lowerIndex + 1);
8609- }
8610-
8611- /**
8612- * Determines and returns the way segment for the given way and node pair.
8613+ * Determines and returns the way segment for the given way and node pair. You should prefer
8614+ * {@link IWaySegment#forNodePair(IWay, INode, INode)} whenever possible.
8615+ *
8616 * @param way way
8617 * @param first first node
8618 * @param second second node
8619@@ -74,6 +43,7 @@ public final class WaySegment implements Comparable<WaySegment> {
8620 * Returns this way segment as complete way.
8621 * @return the way segment as {@code Way}
8622 */
8623+ @Override
8624 public Way toWay() {
8625 Way w = new Way();
8626 w.addNode(getFirstNode());
8627@@ -81,53 +51,6 @@ public final class WaySegment implements Comparable<WaySegment> {
8628 return w;
8629 }
8630
8631- @Override
8632- public boolean equals(Object o) {
8633- if (this == o) return true;
8634- if (o == null || getClass() != o.getClass()) return false;
8635- WaySegment that = (WaySegment) o;
8636- return lowerIndex == that.lowerIndex &&
8637- Objects.equals(way, that.way);
8638- }
8639-
8640- @Override
8641- public int hashCode() {
8642- return Objects.hash(way, lowerIndex);
8643- }
8644-
8645- @Override
8646- public int compareTo(WaySegment o) {
8647- return o == null ? -1 : (equals(o) ? 0 : toWay().compareTo(o.toWay()));
8648- }
8649-
8650- /**
8651- * Checks whether this segment crosses other segment
8652- *
8653- * @param s2 The other segment
8654- * @return true if both segments crosses
8655- */
8656- public boolean intersects(WaySegment s2) {
8657- if (getFirstNode().equals(s2.getFirstNode()) || getSecondNode().equals(s2.getSecondNode()) ||
8658- getFirstNode().equals(s2.getSecondNode()) || getSecondNode().equals(s2.getFirstNode()))
8659- return false;
8660-
8661- return Line2D.linesIntersect(
8662- getFirstNode().getEastNorth().east(), getFirstNode().getEastNorth().north(),
8663- getSecondNode().getEastNorth().east(), getSecondNode().getEastNorth().north(),
8664- s2.getFirstNode().getEastNorth().east(), s2.getFirstNode().getEastNorth().north(),
8665- s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north());
8666- }
8667-
8668- /**
8669- * Checks whether this segment and another way segment share the same points
8670- * @param s2 The other segment
8671- * @return true if other way segment is the same or reverse
8672- */
8673- public boolean isSimilar(WaySegment s2) {
8674- return (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))
8675- || (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()));
8676- }
8677-
8678 @Override
8679 public String toString() {
8680 return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
8681diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
8682index 038374233..03ba0e5b2 100644
8683--- a/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
8684+++ b/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
8685@@ -36,6 +36,7 @@ import java.util.Objects;
8686 import java.util.Optional;
8687 import java.util.concurrent.ForkJoinPool;
8688 import java.util.concurrent.TimeUnit;
8689+import java.util.concurrent.locks.Lock;
8690 import java.util.function.BiConsumer;
8691 import java.util.function.Consumer;
8692 import java.util.function.Supplier;
8693@@ -1637,13 +1638,13 @@ public class StyledMapRenderer extends AbstractMapRenderer {
8694 RenderBenchmarkCollector benchmark = benchmarkFactory.get();
8695 BBox bbox = bounds.toBBox();
8696 getSettings(renderVirtualNodes);
8697-
8698 try {
8699- if (data.getReadLock().tryLock(1, TimeUnit.SECONDS)) {
8700+ Lock readLock = data.getReadLock();
8701+ if (readLock.tryLock(1, TimeUnit.SECONDS)) {
8702 try {
8703 paintWithLock(data, renderVirtualNodes, benchmark, bbox);
8704 } finally {
8705- data.getReadLock().unlock();
8706+ readLock.unlock();
8707 }
8708 } else {
8709 Logging.warn("Cannot paint layer {0}: It is locked.");
8710diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java
8711index 0ac990275..1b768e6fb 100644
8712--- a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java
8713+++ b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java
8714@@ -10,9 +10,10 @@ import java.util.Set;
8715 import java.util.TreeMap;
8716 import java.util.TreeSet;
8717
8718-import org.openstreetmap.josm.data.osm.Node;
8719-import org.openstreetmap.josm.data.osm.RelationMember;
8720-import org.openstreetmap.josm.data.osm.Way;
8721+import org.openstreetmap.josm.data.osm.INode;
8722+import org.openstreetmap.josm.data.osm.IPrimitive;
8723+import org.openstreetmap.josm.data.osm.IRelationMember;
8724+import org.openstreetmap.josm.data.osm.IWay;
8725
8726 /**
8727 * Auxiliary class for relation sorting.
8728@@ -26,15 +27,16 @@ import org.openstreetmap.josm.data.osm.Way;
8729 * (that are shared by other members).
8730 *
8731 * @author Christiaan Welvaart &lt;cjw@time4t.net&gt;
8732- * @since 1785
8733+ * @param <T> The type of {@link IRelationMember}
8734+ * @since 1785, xxx (generics)
8735 */
8736-public class RelationNodeMap {
8737+public class RelationNodeMap<T extends IRelationMember<? extends IPrimitive>> {
8738
8739 private static final String ROLE_BACKWARD = "backward";
8740
8741 private static class NodesWays {
8742- public final Map<Node, Set<Integer>> nodes = new TreeMap<>();
8743- public final Map<Integer, Set<Node>> ways = new TreeMap<>();
8744+ public final Map<INode, Set<Integer>> nodes = new TreeMap<>();
8745+ public final Map<Integer, Set<INode>> ways = new TreeMap<>();
8746 public final boolean oneWay;
8747
8748 NodesWays(boolean oneWay) {
8749@@ -56,7 +58,7 @@ public class RelationNodeMap {
8750 * Used to keep track of what members are done.
8751 */
8752 private final Set<Integer> remaining = new TreeSet<>();
8753- private final Map<Integer, Set<Node>> remainingOneway = new TreeMap<>();
8754+ private final Map<Integer, Set<INode>> remainingOneway = new TreeMap<>();
8755
8756 /**
8757 * All members that are incomplete or not a way
8758@@ -67,8 +69,9 @@ public class RelationNodeMap {
8759 * Gets the start node of the member, respecting the direction role.
8760 * @param m The relation member.
8761 * @return <code>null</code> if the member is no way, the node otherwise.
8762+ * @since xxx (generics)
8763 */
8764- public static Node firstOnewayNode(RelationMember m) {
8765+ public static INode firstOnewayNode(IRelationMember<?> m) {
8766 if (!m.isWay()) return null;
8767 if (ROLE_BACKWARD.equals(m.getRole())) {
8768 return m.getWay().lastNode();
8769@@ -81,7 +84,7 @@ public class RelationNodeMap {
8770 * @param m The relation member.
8771 * @return <code>null</code> if the member is no way, the node otherwise.
8772 */
8773- public static Node lastOnewayNode(RelationMember m) {
8774+ public static INode lastOnewayNode(IRelationMember<?> m) {
8775 if (!m.isWay()) return null;
8776 if (ROLE_BACKWARD.equals(m.getRole())) {
8777 return m.getWay().firstNode();
8778@@ -89,17 +92,17 @@ public class RelationNodeMap {
8779 return m.getWay().lastNode();
8780 }
8781
8782- RelationNodeMap(List<RelationMember> members) {
8783+ RelationNodeMap(List<T> members) {
8784 for (int i = 0; i < members.size(); ++i) {
8785- RelationMember m = members.get(i);
8786+ T m = members.get(i);
8787 if (m.getMember().isIncomplete() || !m.isWay() || m.getWay().getNodesCount() < 2) {
8788 notSortable.add(i);
8789 continue;
8790 }
8791
8792- Way w = m.getWay();
8793+ IWay<?> w = m.getWay();
8794 if (RelationSortUtils.roundaboutType(w) != NONE) {
8795- for (Node nd : w.getNodes()) {
8796+ for (INode nd : w.getNodes()) {
8797 addPair(nd, i);
8798 }
8799 } else if (RelationSortUtils.isOneway(m)) {
8800@@ -118,34 +121,34 @@ public class RelationNodeMap {
8801 remaining.addAll(map.ways.keySet());
8802 }
8803
8804- private void addPair(Node n, int i) {
8805+ private void addPair(INode n, int i) {
8806 map.nodes.computeIfAbsent(n, k -> new TreeSet<>()).add(i);
8807 map.ways.computeIfAbsent(i, k -> new TreeSet<>()).add(n);
8808 }
8809
8810- private void addNodeWayMap(Node n, int i) {
8811+ private void addNodeWayMap(INode n, int i) {
8812 onewayMap.nodes.computeIfAbsent(n, k -> new TreeSet<>()).add(i);
8813 }
8814
8815- private void addWayNodeMap(Node n, int i) {
8816+ private void addWayNodeMap(INode n, int i) {
8817 onewayMap.ways.computeIfAbsent(i, k -> new TreeSet<>()).add(n);
8818 }
8819
8820- private void addNodeWayMapReverse(Node n, int i) {
8821+ private void addNodeWayMapReverse(INode n, int i) {
8822 onewayReverseMap.nodes.computeIfAbsent(n, k -> new TreeSet<>()).add(i);
8823 }
8824
8825- private void addWayNodeMapReverse(Node n, int i) {
8826+ private void addWayNodeMapReverse(INode n, int i) {
8827 onewayReverseMap.ways.computeIfAbsent(i, k -> new TreeSet<>()).add(n);
8828 }
8829
8830- private void addRemainingForward(Node n, int i) {
8831+ private void addRemainingForward(INode n, int i) {
8832 remainingOneway.computeIfAbsent(i, k -> new TreeSet<>()).add(n);
8833 }
8834
8835 private Integer firstOneway;
8836- private Node lastOnewayNode;
8837- private Node firstCircular;
8838+ private INode lastOnewayNode;
8839+ private INode firstCircular;
8840
8841 /**
8842 * Return a relation member that is linked to the member 'i', but has not been popped yet.
8843@@ -158,7 +161,7 @@ public class RelationNodeMap {
8844 if (firstOneway != null) return popForwardOnewayPart(way);
8845
8846 if (map.ways.containsKey(way)) {
8847- for (Node n : map.ways.get(way)) {
8848+ for (INode n : map.ways.get(way)) {
8849 Integer i = deleteAndGetAdjacentNode(map, n);
8850 if (i != null) return i;
8851
8852@@ -176,7 +179,7 @@ public class RelationNodeMap {
8853
8854 private Integer popForwardOnewayPart(Integer way) {
8855 if (onewayMap.ways.containsKey(way)) {
8856- Node exitNode = onewayMap.ways.get(way).iterator().next();
8857+ INode exitNode = onewayMap.ways.get(way).iterator().next();
8858
8859 if (checkIfEndOfLoopReached(exitNode)) {
8860 lastOnewayNode = exitNode;
8861@@ -201,7 +204,7 @@ public class RelationNodeMap {
8862 // Check if the given node can be the end of the loop (i.e. it has
8863 // an outgoing bidirectional or multiple outgoing oneways, or we
8864 // looped back to our first circular node)
8865- private boolean checkIfEndOfLoopReached(Node n) {
8866+ private boolean checkIfEndOfLoopReached(INode n) {
8867 return map.nodes.containsKey(n)
8868 || (onewayMap.nodes.containsKey(n) && (onewayMap.nodes.get(n).size() > 1))
8869 || ((firstCircular != null) && (firstCircular == n));
8870@@ -209,14 +212,14 @@ public class RelationNodeMap {
8871
8872 private Integer popBackwardOnewayPart(int way) {
8873 if (lastOnewayNode != null) {
8874- Set<Node> nodes = new TreeSet<>();
8875+ Set<INode> nodes = new TreeSet<>();
8876 if (onewayReverseMap.ways.containsKey(way)) {
8877 nodes.addAll(onewayReverseMap.ways.get(way));
8878 }
8879 if (map.ways.containsKey(way)) {
8880 nodes.addAll(map.ways.get(way));
8881 }
8882- for (Node n : nodes) {
8883+ for (INode n : nodes) {
8884 if (n == lastOnewayNode) { //if oneway part ends
8885 firstOneway = null;
8886 lastOnewayNode = null;
8887@@ -247,20 +250,20 @@ public class RelationNodeMap {
8888 * @param n node
8889 * @return node next to n
8890 */
8891- private Integer deleteAndGetAdjacentNode(NodesWays nw, Node n) {
8892+ private Integer deleteAndGetAdjacentNode(NodesWays nw, INode n) {
8893 Integer j = findAdjacentWay(nw, n);
8894 if (j == null) return null;
8895 deleteWayNode(nw, j, n);
8896 return j;
8897 }
8898
8899- private static Integer findAdjacentWay(NodesWays nw, Node n) {
8900+ private static Integer findAdjacentWay(NodesWays nw, INode n) {
8901 Set<Integer> adj = nw.nodes.get(n);
8902 if (adj == null || adj.isEmpty()) return null;
8903 return adj.iterator().next();
8904 }
8905
8906- private void deleteWayNode(NodesWays nw, Integer way, Node n) {
8907+ private void deleteWayNode(NodesWays nw, Integer way, INode n) {
8908 if (nw.oneWay) {
8909 doneOneway(way);
8910 } else {
8911@@ -285,7 +288,7 @@ public class RelationNodeMap {
8912
8913 if (remainingOneway.isEmpty()) return null;
8914 for (Integer i : remainingOneway.keySet()) { //find oneway, which is connected to more than one way (is between two oneway loops)
8915- for (Node n : onewayReverseMap.ways.get(i)) {
8916+ for (INode n : onewayReverseMap.ways.get(i)) {
8917 if (onewayReverseMap.nodes.containsKey(n) && onewayReverseMap.nodes.get(n).size() > 1) {
8918 doneOneway(i);
8919 firstCircular = n;
8920@@ -305,8 +308,8 @@ public class RelationNodeMap {
8921 * @param i member key
8922 */
8923 private void doneOneway(Integer i) {
8924- Set<Node> nodesForward = remainingOneway.get(i);
8925- for (Node n : nodesForward) {
8926+ Set<INode> nodesForward = remainingOneway.get(i);
8927+ for (INode n : nodesForward) {
8928 if (onewayMap.nodes.containsKey(n)) {
8929 onewayMap.nodes.get(n).remove(i);
8930 }
8931@@ -319,8 +322,8 @@ public class RelationNodeMap {
8932
8933 private void done(Integer i) {
8934 remaining.remove(i);
8935- Set<Node> nodes = map.ways.get(i);
8936- for (Node n : nodes) {
8937+ Set<INode> nodes = map.ways.get(i);
8938+ for (INode n : nodes) {
8939 boolean result = map.nodes.get(n).remove(i);
8940 if (!result) throw new AssertionError();
8941 }
8942diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java
8943index d7457f7f1..70023011d 100644
8944--- a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java
8945+++ b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java
8946@@ -6,9 +6,9 @@ import static org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType
8947 import static org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction.ROUNDABOUT_RIGHT;
8948
8949 import org.openstreetmap.josm.data.coor.EastNorth;
8950-import org.openstreetmap.josm.data.osm.Node;
8951-import org.openstreetmap.josm.data.osm.RelationMember;
8952-import org.openstreetmap.josm.data.osm.Way;
8953+import org.openstreetmap.josm.data.osm.INode;
8954+import org.openstreetmap.josm.data.osm.IRelationMember;
8955+import org.openstreetmap.josm.data.osm.IWay;
8956 import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction;
8957
8958 /**
8959@@ -24,19 +24,27 @@ final class RelationSortUtils {
8960 * determine, if the way i is a roundabout and if yes, what type of roundabout
8961 * @param member relation member
8962 * @return roundabout type
8963+ * @since xxx (generics)
8964 */
8965- static Direction roundaboutType(RelationMember member) {
8966+ static Direction roundaboutType(IRelationMember<?> member) {
8967 if (member == null || !member.isWay()) return NONE;
8968- return roundaboutType(member.getWay());
8969+ return roundaboutType((IWay<?>) member.getWay());
8970 }
8971
8972- static Direction roundaboutType(Way w) {
8973+ /**
8974+ * Check if a way is a roundabout type
8975+ * @param w The way to check
8976+ * @param <W> The way type
8977+ * @return The roundabout type
8978+ * @since xxx (generics)
8979+ */
8980+ static <W extends IWay<?>> Direction roundaboutType(W w) {
8981 if (w != null && w.hasTag("junction", "circular", "roundabout")) {
8982 int nodesCount = w.getNodesCount();
8983 if (nodesCount > 2 && nodesCount < 200) {
8984- Node n1 = w.getNode(0);
8985- Node n2 = w.getNode(1);
8986- Node n3 = w.getNode(2);
8987+ INode n1 = w.getNode(0);
8988+ INode n2 = w.getNode(1);
8989+ INode n3 = w.getNode(2);
8990 if (n1 != null && n2 != null && n3 != null && w.isClosed()) {
8991 /** do some simple determinant / cross product test on the first 3 nodes
8992 to see, if the roundabout goes clock wise or ccw */
8993@@ -54,15 +62,15 @@ final class RelationSortUtils {
8994 return NONE;
8995 }
8996
8997- static boolean isBackward(final RelationMember member) {
8998+ static boolean isBackward(final IRelationMember<?> member) {
8999 return "backward".equals(member.getRole());
9000 }
9001
9002- static boolean isForward(final RelationMember member) {
9003+ static boolean isForward(final IRelationMember<?> member) {
9004 return "forward".equals(member.getRole());
9005 }
9006
9007- static boolean isOneway(final RelationMember member) {
9008+ static boolean isOneway(final IRelationMember<?> member) {
9009 return isForward(member) || isBackward(member);
9010 }
9011 }
9012diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java
9013index 12713094a..34d6bdf4f 100644
9014--- a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java
9015+++ b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java
9016@@ -15,6 +15,8 @@ import java.util.Objects;
9017 import java.util.stream.Collectors;
9018
9019 import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
9020+import org.openstreetmap.josm.data.osm.IPrimitive;
9021+import org.openstreetmap.josm.data.osm.IRelationMember;
9022 import org.openstreetmap.josm.data.osm.OsmPrimitive;
9023 import org.openstreetmap.josm.data.osm.Relation;
9024 import org.openstreetmap.josm.data.osm.RelationMember;
9025@@ -194,12 +196,12 @@ public class RelationSorter {
9026 * Sorts a list of members by connectivity
9027 * @param defaultMembers The members to sort
9028 * @return A sorted list of the same members
9029+ * @since xxx (signature change, generics)
9030 */
9031- public static List<RelationMember> sortMembersByConnectivity(List<RelationMember> defaultMembers) {
9032+ public static <T extends IRelationMember<? extends IPrimitive>> List<T> sortMembersByConnectivity(List<T> defaultMembers) {
9033+ List<T> newMembers;
9034
9035- List<RelationMember> newMembers;
9036-
9037- RelationNodeMap map = new RelationNodeMap(defaultMembers);
9038+ RelationNodeMap<T> map = new RelationNodeMap<>(defaultMembers);
9039 // List of groups of linked members
9040 //
9041 List<LinkedList<Integer>> allGroups = new ArrayList<>();
9042diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
9043index 4dcb27fd5..c7331faba 100644
9044--- a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
9045+++ b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
9046@@ -86,6 +86,7 @@ import org.openstreetmap.josm.data.imagery.ImageryInfo;
9047 import org.openstreetmap.josm.data.imagery.OffsetBookmark;
9048 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
9049 import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
9050+import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
9051 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
9052 import org.openstreetmap.josm.data.preferences.BooleanProperty;
9053 import org.openstreetmap.josm.data.preferences.IntegerProperty;
9054@@ -109,6 +110,7 @@ import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChan
9055 import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction;
9056 import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction;
9057 import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction;
9058+import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
9059 import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile;
9060 import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction;
9061 import org.openstreetmap.josm.gui.layer.imagery.TileAnchor;
9062@@ -888,7 +890,7 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
9063 if (coordinateConverter.requiresReprojection()) {
9064 tile = new ReprojectionTile(tileSource, x, y, zoom);
9065 } else {
9066- tile = new Tile(tileSource, x, y, zoom);
9067+ tile = createTile(tileSource, x, y, zoom);
9068 }
9069 tileCache.addTile(tile);
9070 }
9071@@ -1041,7 +1043,7 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
9072 img = getLoadedTileImage(tile);
9073 anchorImage = getAnchor(tile, img);
9074 }
9075- if (img == null || anchorImage == null) {
9076+ if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) {
9077 miss = true;
9078 }
9079 }
9080@@ -1050,7 +1052,9 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
9081 return;
9082 }
9083
9084- img = applyImageProcessors(img);
9085+ if (img != null) {
9086+ img = applyImageProcessors(img);
9087+ }
9088
9089 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
9090 synchronized (paintMutex) {
9091@@ -1862,7 +1866,7 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
9092
9093 for (int x = minX; x <= maxX; x++) {
9094 for (int y = minY; y <= maxY; y++) {
9095- requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
9096+ requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel));
9097 }
9098 }
9099 }
9100@@ -1968,6 +1972,20 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
9101 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
9102 }
9103
9104+ /**
9105+ * Create a new tile. Added to allow use of custom {@link Tile} objects.
9106+ *
9107+ * @param source Tile source
9108+ * @param x X coordinate
9109+ * @param y Y coordinate
9110+ * @param zoom Zoom level
9111+ * @return The new {@link Tile}
9112+ * @since xxx
9113+ */
9114+ public Tile createTile(T source, int x, int y, int zoom) {
9115+ return new Tile(source, x, y, zoom);
9116+ }
9117+
9118 @Override
9119 public synchronized void destroy() {
9120 super.destroy();
9121@@ -1988,6 +2006,10 @@ implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi
9122 allocateCacheMemory();
9123 if (memory != null) {
9124 doPaint(graphics);
9125+ if (AbstractTileSourceLayer.this instanceof MVTLayer) {
9126+ AbstractTileSourceLayer.this.paint(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getMapView()
9127+ .getRealBounds());
9128+ }
9129 } else {
9130 Graphics g = graphics.getDefaultGraphics();
9131 Color oldColor = g.getColor();
9132diff --git a/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java b/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
9133index 6c902b92a..933933da3 100644
9134--- a/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
9135+++ b/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
9136@@ -37,6 +37,7 @@ import org.openstreetmap.josm.gui.MainApplication;
9137 import org.openstreetmap.josm.gui.MapView;
9138 import org.openstreetmap.josm.gui.MenuScroller;
9139 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings;
9140+import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
9141 import org.openstreetmap.josm.gui.widgets.UrlLabel;
9142 import org.openstreetmap.josm.tools.GBC;
9143 import org.openstreetmap.josm.tools.ImageProcessor;
9144@@ -168,6 +169,8 @@ public abstract class ImageryLayer extends Layer {
9145 case BING:
9146 case SCANEX:
9147 return new TMSLayer(info);
9148+ case MVT:
9149+ return new MVTLayer(info);
9150 default:
9151 throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType()));
9152 }
9153diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
9154new file mode 100644
9155index 000000000..aa335f7b0
9156--- /dev/null
9157+++ b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
9158@@ -0,0 +1,278 @@
9159+// License: GPL. For details, see LICENSE file.
9160+package org.openstreetmap.josm.gui.layer.imagery;
9161+
9162+import static org.openstreetmap.josm.tools.I18n.tr;
9163+
9164+import java.awt.Component;
9165+import java.awt.Graphics2D;
9166+import java.awt.event.ActionEvent;
9167+import java.util.ArrayList;
9168+import java.util.Arrays;
9169+import java.util.Collection;
9170+import java.util.Collections;
9171+import java.util.HashMap;
9172+import java.util.List;
9173+import java.util.Map;
9174+import java.util.Objects;
9175+import java.util.function.BooleanSupplier;
9176+import java.util.function.Consumer;
9177+import java.util.stream.Collectors;
9178+
9179+import javax.swing.AbstractAction;
9180+import javax.swing.Action;
9181+import javax.swing.JCheckBoxMenuItem;
9182+import javax.swing.JMenuItem;
9183+
9184+import org.openstreetmap.gui.jmapviewer.Tile;
9185+import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
9186+import org.openstreetmap.josm.data.Bounds;
9187+import org.openstreetmap.josm.data.imagery.ImageryInfo;
9188+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
9189+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
9190+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
9191+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.TileListener;
9192+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
9193+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
9194+import org.openstreetmap.josm.data.osm.DataSet;
9195+import org.openstreetmap.josm.data.osm.Node;
9196+import org.openstreetmap.josm.data.osm.OsmPrimitive;
9197+import org.openstreetmap.josm.data.osm.Relation;
9198+import org.openstreetmap.josm.data.osm.RelationMember;
9199+import org.openstreetmap.josm.data.osm.Way;
9200+import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer;
9201+import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory;
9202+import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
9203+import org.openstreetmap.josm.data.vector.VectorDataSet;
9204+import org.openstreetmap.josm.data.vector.VectorNode;
9205+import org.openstreetmap.josm.data.vector.VectorPrimitive;
9206+import org.openstreetmap.josm.data.vector.VectorRelation;
9207+import org.openstreetmap.josm.data.vector.VectorWay;
9208+import org.openstreetmap.josm.gui.MainApplication;
9209+import org.openstreetmap.josm.gui.MapView;
9210+import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
9211+import org.openstreetmap.josm.gui.layer.LayerManager;
9212+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
9213+import org.openstreetmap.josm.gui.mappaint.ElemStyles;
9214+import org.openstreetmap.josm.gui.mappaint.StyleSource;
9215+
9216+/**
9217+ * A layer for MapBox Vector Tiles
9218+ * @author Taylor Smock
9219+ * @since xxx
9220+ */
9221+public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> implements TileListener {
9222+ private static final String CACHE_REGION_NAME = "MVT";
9223+ // Just to avoid allocating a bunch of 0 length action arrays
9224+ private static final Action[] EMPTY_ACTIONS = new Action[0];
9225+ private final Map<String, Boolean> layerNames = new HashMap<>();
9226+ private final VectorDataSet dataSet = new VectorDataSet();
9227+
9228+ /**
9229+ * Creates an instance of an MVT layer
9230+ *
9231+ * @param info ImageryInfo describing the layer
9232+ */
9233+ public MVTLayer(ImageryInfo info) {
9234+ super(info);
9235+ }
9236+
9237+ @Override
9238+ protected Class<? extends TileLoader> getTileLoaderClass() {
9239+ return MapBoxVectorCachedTileLoader.class;
9240+ }
9241+
9242+ @Override
9243+ protected String getCacheName() {
9244+ return CACHE_REGION_NAME;
9245+ }
9246+
9247+ @Override
9248+ public Collection<String> getNativeProjections() {
9249+ // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857
9250+ // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}").
9251+ return Collections.singleton(MVTFile.DEFAULT_PROJECTION);
9252+ }
9253+
9254+ @Override
9255+ public void paint(Graphics2D g, MapView mv, Bounds box) {
9256+ this.dataSet.setZoom(this.getZoomLevel());
9257+ AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, false);
9258+ painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress()
9259+ || !OsmDataLayer.PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get());
9260+ // Set the painter to use our custom style sheet
9261+ if (painter instanceof StyledMapRenderer && this.dataSet.getStyles() != null) {
9262+ ((StyledMapRenderer) painter).setStyles(this.dataSet.getStyles());
9263+ }
9264+ painter.render(this.dataSet, false, box);
9265+ }
9266+
9267+ @Override
9268+ protected MapboxVectorTileSource getTileSource() {
9269+ MapboxVectorTileSource source = new MapboxVectorTileSource(this.info);
9270+ this.info.setAttribution(source);
9271+ if (source.getStyleSource() != null) {
9272+ List<ElemStyles> styles = source.getStyleSource().getSources().entrySet().stream()
9273+ .filter(entry -> entry.getKey() == null || entry.getKey().getUrls().contains(source.getBaseUrl()))
9274+ .map(Map.Entry::getValue).collect(Collectors.toList());
9275+ // load the style sources
9276+ styles.stream().map(ElemStyles::getStyleSources).flatMap(Collection::stream).forEach(StyleSource::loadStyleSource);
9277+ this.dataSet.setStyles(styles);
9278+ this.setName(source.getName());
9279+ }
9280+ return source;
9281+ }
9282+
9283+ @Override
9284+ public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) {
9285+ final MVTTile tile = new MVTTile(source, x, y, zoom);
9286+ tile.addTileLoaderFinisher(this);
9287+ return tile;
9288+ }
9289+
9290+ @Override
9291+ public Action[] getMenuEntries() {
9292+ ArrayList<Action> actions = new ArrayList<>(Arrays.asList(super.getMenuEntries()));
9293+ // Add separator between Info and the layers
9294+ actions.add(SeparatorLayerAction.INSTANCE);
9295+ for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) {
9296+ actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true),
9297+ layer -> {
9298+ layerNames.compute(layer, (key, value) -> Boolean.FALSE.equals(value));
9299+ this.invalidate();
9300+ }));
9301+ }
9302+ // Add separator between layers and convert action
9303+ actions.add(SeparatorLayerAction.INSTANCE);
9304+ actions.add(new ConvertLayerAction(this));
9305+ return actions.toArray(EMPTY_ACTIONS);
9306+ }
9307+
9308+ /**
9309+ * Get the data set for this layer
9310+ */
9311+ public VectorDataSet getData() {
9312+ return this.dataSet;
9313+ }
9314+
9315+ private static class ConvertLayerAction extends AbstractAction implements LayerAction {
9316+ private final MVTLayer layer;
9317+
9318+ ConvertLayerAction(MVTLayer layer) {
9319+ this.layer = layer;
9320+ }
9321+
9322+ @Override
9323+ public void actionPerformed(ActionEvent e) {
9324+ LayerManager manager = MainApplication.getLayerManager();
9325+ VectorDataSet dataSet = layer.getData();
9326+ DataSet osmData = new DataSet();
9327+ // Add nodes first, map is to ensure we can map new nodes to vector nodes
9328+ Map<VectorNode, Node> nodeMap = new HashMap<>(dataSet.getNodes().size());
9329+ for (VectorNode vectorNode : dataSet.getNodes()) {
9330+ Node newNode = new Node(vectorNode.getCoor());
9331+ if (vectorNode.isTagged()) {
9332+ vectorNode.getInterestingTags().forEach(newNode::put);
9333+ }
9334+ nodeMap.put(vectorNode, newNode);
9335+ }
9336+ // Add ways next
9337+ Map<VectorWay, Way> wayMap = new HashMap<>(dataSet.getWays().size());
9338+ for (VectorWay vectorWay : dataSet.getWays()) {
9339+ Way newWay = new Way();
9340+ List<Node> nodes = vectorWay.getNodes().stream().map(nodeMap::get).filter(Objects::nonNull).collect(Collectors.toList());
9341+ newWay.setNodes(nodes);
9342+ if (vectorWay.isTagged()) {
9343+ vectorWay.getInterestingTags().forEach(newWay::put);
9344+ }
9345+ wayMap.put(vectorWay, newWay);
9346+ }
9347+
9348+ // Finally, add Relations
9349+ Map<VectorRelation, Relation> relationMap = new HashMap<>(dataSet.getRelations().size());
9350+ for (VectorRelation vectorRelation : dataSet.getRelations()) {
9351+ Relation relation = new Relation();
9352+ if (vectorRelation.isTagged()) {
9353+ vectorRelation.getInterestingTags().forEach(relation::put);
9354+ }
9355+ List<RelationMember> members = vectorRelation.getMembers().stream().map(member -> {
9356+ final OsmPrimitive primitive;
9357+ final VectorPrimitive vectorPrimitive = member.getMember();
9358+ if (vectorPrimitive instanceof VectorNode) {
9359+ primitive = nodeMap.get(vectorPrimitive);
9360+ } else if (vectorPrimitive instanceof VectorWay) {
9361+ primitive = wayMap.get(vectorPrimitive);
9362+ } else if (vectorPrimitive instanceof VectorRelation) {
9363+ // Hopefully, relations are encountered in order...
9364+ primitive = relationMap.get(vectorPrimitive);
9365+ } else {
9366+ primitive = null;
9367+ }
9368+ if (primitive == null) return null;
9369+ return new RelationMember(member.getRole(), primitive);
9370+ }).filter(Objects::nonNull).collect(Collectors.toList());
9371+ relation.setMembers(members);
9372+ relationMap.put(vectorRelation, relation);
9373+ }
9374+ try {
9375+ osmData.beginUpdate();
9376+ nodeMap.values().forEach(osmData::addPrimitive);
9377+ wayMap.values().forEach(osmData::addPrimitive);
9378+ relationMap.values().forEach(osmData::addPrimitive);
9379+ } finally {
9380+ osmData.endUpdate();
9381+ }
9382+ manager.addLayer(new OsmDataLayer(osmData, this.layer.getName(), null));
9383+ manager.removeLayer(this.layer);
9384+ }
9385+
9386+ @Override
9387+ public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) {
9388+ return layers.stream().allMatch(MVTLayer.class::isInstance);
9389+ }
9390+
9391+ @Override
9392+ public Component createMenuComponent() {
9393+ JMenuItem menuItem = new JMenuItem(tr("Convert to OSM Data"));
9394+ menuItem.addActionListener(this);
9395+ return menuItem;
9396+ }
9397+ }
9398+
9399+ private static class EnableLayerAction extends AbstractAction implements LayerAction {
9400+ private final String layer;
9401+ private final Consumer<String> consumer;
9402+ private final BooleanSupplier state;
9403+
9404+ EnableLayerAction(String layer, BooleanSupplier state, Consumer<String> consumer) {
9405+ super(tr("Toggle layer {0}", layer));
9406+ this.layer = layer;
9407+ this.consumer = consumer;
9408+ this.state = state;
9409+ }
9410+
9411+ @Override
9412+ public void actionPerformed(ActionEvent e) {
9413+ consumer.accept(layer);
9414+ }
9415+
9416+ @Override
9417+ public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) {
9418+ return layers.stream().allMatch(MVTLayer.class::isInstance);
9419+ }
9420+
9421+ @Override
9422+ public Component createMenuComponent() {
9423+ JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
9424+ item.setSelected(this.state.getAsBoolean());
9425+ return item;
9426+ }
9427+ }
9428+
9429+ @Override
9430+ public void finishedLoading(MVTTile tile) {
9431+ for (Layer layer : tile.getLayers()) {
9432+ this.layerNames.putIfAbsent(layer.getName(), true);
9433+ }
9434+ this.dataSet.addTileData(tile);
9435+ }
9436+}
9437diff --git a/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java b/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java
9438index d195742d8..a97573ecc 100644
9439--- a/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java
9440+++ b/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java
9441@@ -870,6 +870,17 @@ public final class ConditionFactory {
9442 }
9443 return e.osm.isSelected();
9444 }
9445+
9446+ /**
9447+ * Check if the object is highlighted (i.e., is hovered over)
9448+ * @param e The MapCSS environment
9449+ * @return {@code true} if the object is highlighted
9450+ * @see IPrimitive#isHighlighted
9451+ * @since xxx
9452+ */
9453+ static boolean highlighted(Environment e) { // NO_UCD (unused code)
9454+ return e.osm.isHighlighted();
9455+ }
9456 }
9457
9458 /**
9459diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
9460new file mode 100644
9461index 000000000..99bbd058d
9462--- /dev/null
9463+++ b/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
9464@@ -0,0 +1,94 @@
9465+// License: GPL. For details, see LICENSE file.
9466+package org.openstreetmap.josm.gui.preferences.imagery;
9467+
9468+import static org.openstreetmap.josm.tools.I18n.tr;
9469+
9470+import java.awt.event.KeyAdapter;
9471+import java.awt.event.KeyEvent;
9472+import java.util.Arrays;
9473+
9474+import javax.swing.JLabel;
9475+
9476+import org.openstreetmap.josm.data.imagery.ImageryInfo;
9477+import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
9478+import org.openstreetmap.josm.gui.widgets.JosmTextArea;
9479+import org.openstreetmap.josm.gui.widgets.JosmTextField;
9480+import org.openstreetmap.josm.tools.GBC;
9481+import org.openstreetmap.josm.tools.Utils;
9482+
9483+/**
9484+ * A panel for adding MapBox Vector Tile layers
9485+ * @author Taylor Smock
9486+ * @since xxx
9487+ */
9488+public class AddMVTLayerPanel extends AddImageryPanel {
9489+ private final JosmTextField mvtZoom = new JosmTextField();
9490+ private final JosmTextArea mvtUrl = new JosmTextArea(3, 40).transferFocusOnTab();
9491+
9492+ /**
9493+ * Constructs a new {@code AddMVTLayerPanel}.
9494+ */
9495+ public AddMVTLayerPanel() {
9496+
9497+ add(new JLabel(tr("{0} Make sure OSM has the permission to use this service", "1.")), GBC.eol());
9498+ add(new JLabel(tr("{0} Enter URL (may be a style sheet url)", "2.")), GBC.eol());
9499+ add(new JLabel("<html>" + Utils.joinAsHtmlUnorderedList(Arrays.asList(
9500+ tr("{0} is replaced by tile zoom level, also supported:<br>" +
9501+ "offsets to the zoom level: {1} or {2}<br>" +
9502+ "reversed zoom level: {3}", "{zoom}", "{zoom+1}", "{zoom-1}", "{19-zoom}"),
9503+ tr("{0} is replaced by X-coordinate of the tile", "{x}"),
9504+ tr("{0} is replaced by Y-coordinate of the tile", "{y}"),
9505+ tr("{0} is replaced by a random selection from the given comma separated list, e.g. {1}", "{switch:...}", "{switch:a,b,c}")
9506+ )) + "</html>"), GBC.eol().fill());
9507+
9508+ final KeyAdapter keyAdapter = new KeyAdapter() {
9509+ @Override
9510+ public void keyReleased(KeyEvent e) {
9511+ mvtUrl.setText(buildMvtUrl());
9512+ }
9513+ };
9514+
9515+ add(rawUrl, GBC.eop().fill());
9516+ rawUrl.setLineWrap(true);
9517+ rawUrl.addKeyListener(keyAdapter);
9518+
9519+ add(new JLabel(tr("{0} Enter maximum zoom (optional)", "3.")), GBC.eol());
9520+ mvtZoom.addKeyListener(keyAdapter);
9521+ add(mvtZoom, GBC.eop().fill());
9522+
9523+ add(new JLabel(tr("{0} Edit generated {1} URL (optional)", "4.", "MVT")), GBC.eol());
9524+ add(mvtUrl, GBC.eop().fill());
9525+ mvtUrl.setLineWrap(true);
9526+
9527+ add(new JLabel(tr("{0} Enter name for this layer", "5.")), GBC.eol());
9528+ add(name, GBC.eop().fill());
9529+
9530+ registerValidableComponent(mvtUrl);
9531+ }
9532+
9533+ private String buildMvtUrl() {
9534+ StringBuilder a = new StringBuilder("mvt");
9535+ String z = sanitize(mvtZoom.getText());
9536+ if (!z.isEmpty()) {
9537+ a.append('[').append(z).append(']');
9538+ }
9539+ a.append(':').append(sanitize(getImageryRawUrl(), ImageryType.MVT));
9540+ return a.toString();
9541+ }
9542+
9543+ @Override
9544+ public ImageryInfo getImageryInfo() {
9545+ final ImageryInfo generated = new ImageryInfo(getImageryName(), getMvtUrl());
9546+ generated.setImageryType(ImageryType.MVT);
9547+ return generated;
9548+ }
9549+
9550+ protected final String getMvtUrl() {
9551+ return sanitize(mvtUrl.getText());
9552+ }
9553+
9554+ @Override
9555+ protected boolean isImageryValid() {
9556+ return !getImageryName().isEmpty() && !getMvtUrl().isEmpty();
9557+ }
9558+}
9559diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
9560index d1d0ed096..cb78f9bda 100644
9561--- a/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
9562+++ b/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
9563@@ -312,6 +312,7 @@ public class ImageryProvidersPanel extends JPanel {
9564 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMS));
9565 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.TMS));
9566 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMTS));
9567+ activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.MVT));
9568 activeToolbar.add(remove);
9569 activePanel.add(activeToolbar, BorderLayout.EAST);
9570 add(activePanel, GBC.eol().fill(GridBagConstraints.BOTH).weight(2.0, 0.4).insets(5, 0, 0, 5));
9571@@ -440,6 +441,9 @@ public class ImageryProvidersPanel extends JPanel {
9572 case WMTS:
9573 icon = /* ICON(dialogs/) */ "add_wmts";
9574 break;
9575+ case MVT:
9576+ icon = /* ICON(dialogs/) */ "add_mvt";
9577+ break;
9578 default:
9579 break;
9580 }
9581@@ -460,6 +464,9 @@ public class ImageryProvidersPanel extends JPanel {
9582 case WMTS:
9583 p = new AddWMTSLayerPanel();
9584 break;
9585+ case MVT:
9586+ p = new AddMVTLayerPanel();
9587+ break;
9588 default:
9589 throw new IllegalStateException("Type " + type + " not supported");
9590 }
9591@@ -741,7 +748,7 @@ public class ImageryProvidersPanel extends JPanel {
9592 private static boolean confirmEulaAcceptance(PreferenceTabbedPane gui, String eulaUrl) {
9593 URL url;
9594 try {
9595- url = new URL(eulaUrl.replaceAll("\\{lang\\}", LanguageInfo.getWikiLanguagePrefix()));
9596+ url = new URL(eulaUrl.replaceAll("\\{lang}", LanguageInfo.getWikiLanguagePrefix()));
9597 JosmEditorPane htmlPane;
9598 try {
9599 htmlPane = new JosmEditorPane(url);
9600@@ -749,7 +756,7 @@ public class ImageryProvidersPanel extends JPanel {
9601 Logging.trace(e1);
9602 // give a second chance with a default Locale 'en'
9603 try {
9604- url = new URL(eulaUrl.replaceAll("\\{lang\\}", ""));
9605+ url = new URL(eulaUrl.replaceAll("\\{lang}", ""));
9606 htmlPane = new JosmEditorPane(url);
9607 } catch (IOException e2) {
9608 Logging.debug(e2);
9609--
9610GitLab
9611
9612
9613From 1f1b2e09776ce71c88845240a32d6532b8b58c56 Mon Sep 17 00:00:00 2001
9614From: Taylor Smock <tsmock@fb.com>
9615Date: Fri, 16 Apr 2021 06:25:45 -0600
9616Subject: [PATCH 08/50] MVT: Update maps.xsd for mvt support
9617
9618Signed-off-by: Taylor Smock <tsmock@fb.com>
9619---
9620 resources/data/maps.xsd | 3 ++-
9621 1 file changed, 2 insertions(+), 1 deletion(-)
9622
9623diff --git a/resources/data/maps.xsd b/resources/data/maps.xsd
9624index 0c7022370..ce5a1e88b 100644
9625--- a/resources/data/maps.xsd
9626+++ b/resources/data/maps.xsd
9627@@ -38,6 +38,7 @@
9628 <xs:enumeration value="wms_endpoint" />
9629 <xs:enumeration value="wmts" />
9630 <xs:enumeration value="tms" />
9631+ <xs:enumeration value="mvt" />
9632 <xs:enumeration value="bing" />
9633 <xs:enumeration value="scanex" />
9634 </xs:restriction>
9635@@ -647,7 +648,7 @@
9636 <xs:element name="id" minOccurs="1" maxOccurs="1" type="tns:id" />
9637 <!-- Historic id for the imagery source -->
9638 <xs:element name="oldid" minOccurs="0" maxOccurs="unbounded" type="tns:oldid" />
9639- <!-- The type. Can be tms, wms and html. In addition, there are the special types bing and scanex
9640+ <!-- The type. Can be mvt, tms, wms and html. In addition, there are the special types bing and scanex
9641 with hardcoded behaviour. -->
9642 <xs:element name="type" minOccurs="1" maxOccurs="1" type="tns:type" />
9643 <!-- To define as default server for this type -->
9644--
9645GitLab
9646
9647
9648From 0c314b387c48828fe7c3e90bb29be80de74585c2 Mon Sep 17 00:00:00 2001
9649From: Taylor Smock <tsmock@fb.com>
9650Date: Fri, 16 Apr 2021 07:03:43 -0600
9651Subject: [PATCH 09/50] MVT: Catch known exception
9652
9653Signed-off-by: Taylor Smock <tsmock@fb.com>
9654---
9655 .../mapbox/InvalidMapboxVectorTileException.java | 2 +-
9656 .../vectortile/mapbox/style/MapBoxVectorStyle.java | 14 +++++++++++++-
9657 .../imagery/vectortile/mapbox/style/Source.java | 3 ++-
9658 3 files changed, 16 insertions(+), 3 deletions(-)
9659
9660diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
9661index d1186ad3f..bd47fe65f 100644
9662--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
9663+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
9664@@ -16,7 +16,7 @@ public class InvalidMapboxVectorTileException extends RuntimeException {
9665 }
9666
9667 /**
9668- * Create a new {@link InvalidMapboxVectorTile} exception with a message
9669+ * Create a new {@link InvalidMapboxVectorTileException} exception with a message
9670 * @param message The message
9671 */
9672 public InvalidMapboxVectorTileException(final String message) {
9673diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
9674index 746913042..ec24ee5cf 100644
9675--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
9676+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
9677@@ -29,6 +29,7 @@ import javax.json.JsonReader;
9678 import javax.json.JsonStructure;
9679 import javax.json.JsonValue;
9680
9681+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.InvalidMapboxVectorTileException;
9682 import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
9683 import org.openstreetmap.josm.gui.MainApplication;
9684 import org.openstreetmap.josm.gui.mappaint.ElemStyles;
9685@@ -100,7 +101,18 @@ public class MapBoxVectorStyle {
9686 if (jsonObject.containsKey("sources") && jsonObject.get("sources").getValueType() == JsonValue.ValueType.OBJECT) {
9687 final JsonObject sourceObj = jsonObject.getJsonObject("sources");
9688 sourceList = sourceObj.entrySet().stream().filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT)
9689- .map(entry -> new Source(entry.getKey(), entry.getValue().asJsonObject())).collect(Collectors.toList());
9690+ .map(entry -> {
9691+ try {
9692+ return new Source(entry.getKey(), entry.getValue().asJsonObject());
9693+ } catch (InvalidMapboxVectorTileException e) {
9694+ Logging.error(e);
9695+ // Reraise if not a known exception
9696+ if (!"TileJson not yet supported".equals(e.getMessage())) {
9697+ throw e;
9698+ }
9699+ }
9700+ return null;
9701+ }).filter(Objects::nonNull).collect(Collectors.toList());
9702 } else {
9703 sourceList = Collections.emptyList();
9704 }
9705diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
9706index dc7c62d62..dd41da72f 100644
9707--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
9708+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
9709@@ -17,6 +17,7 @@ import javax.json.JsonString;
9710 import javax.json.JsonValue;
9711
9712 import org.openstreetmap.josm.data.Bounds;
9713+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.InvalidMapboxVectorTileException;
9714
9715 /**
9716 * A source from a MapBox Vector Style
9717@@ -128,7 +129,7 @@ public class Source {
9718 if (SourceType.VECTOR == this.sourceType || SourceType.RASTER == this.sourceType) {
9719 if (data.containsKey("url")) {
9720 // TODO implement https://github.com/mapbox/tilejson-spec
9721- throw new UnsupportedOperationException();
9722+ throw new InvalidMapboxVectorTileException("TileJson not yet supported");
9723 } else {
9724 this.minZoom = ZOOM_BOUND_FUNCTION.apply(data.getInt("minzoom", 0));
9725 this.maxZoom = ZOOM_BOUND_FUNCTION.apply(data.getInt("maxzoom", 22));
9726--
9727GitLab
9728
9729
9730From 80afe5c40c8c9d9a99b9b336b99f769a8bd09b8e Mon Sep 17 00:00:00 2001
9731From: Taylor Smock <tsmock@fb.com>
9732Date: Fri, 16 Apr 2021 07:10:18 -0600
9733Subject: [PATCH 10/50] Update highlighted mapcss stanza for modified mapcss
9734 function registration
9735
9736Signed-off-by: Taylor Smock <tsmock@fb.com>
9737---
9738 .../josm/gui/mappaint/mapcss/ConditionFactory.java | 3 ++-
9739 1 file changed, 2 insertions(+), 1 deletion(-)
9740
9741diff --git a/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java b/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java
9742index a97573ecc..becc3f0a8 100644
9743--- a/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java
9744+++ b/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java
9745@@ -878,7 +878,7 @@ public final class ConditionFactory {
9746 * @see IPrimitive#isHighlighted
9747 * @since xxx
9748 */
9749- static boolean highlighted(Environment e) { // NO_UCD (unused code)
9750+ static boolean highlighted(Environment e) {
9751 return e.osm.isHighlighted();
9752 }
9753 }
9754@@ -898,6 +898,7 @@ public final class ConditionFactory {
9755 PseudoClassCondition.register("closed2", PseudoClasses::closed2);
9756 PseudoClassCondition.register("completely_downloaded", PseudoClasses::completely_downloaded);
9757 PseudoClassCondition.register("connection", PseudoClasses::connection);
9758+ PseudoClassCondition.register("highlighted", PseudoClasses::highlighted);
9759 PseudoClassCondition.register("inDownloadedArea", PseudoClasses::inDownloadedArea);
9760 PseudoClassCondition.register("modified", PseudoClasses::modified);
9761 PseudoClassCondition.register("new", PseudoClasses::_new);
9762--
9763GitLab
9764
9765
9766From 729060323f9bceececcec48ddf9eb45f5c3cdeff Mon Sep 17 00:00:00 2001
9767From: Taylor Smock <tsmock@fb.com>
9768Date: Wed, 21 Apr 2021 16:23:57 -0600
9769Subject: [PATCH 11/50] Fix dropping features with dupe ids for MVTv1
9770
9771Also, make some performance modifications (hopefully)
9772
9773Signed-off-by: Taylor Smock <tsmock@fb.com>
9774---
9775 .../josm/data/vector/DataStore.java | 13 +-
9776 .../josm/data/vector/VectorDataSet.java | 2 +-
9777 .../josm/data/vector/VectorDataStore.java | 111 +++++++++++-------
9778 .../josm/data/vector/VectorDataSetTest.java | 9 +-
9779 4 files changed, 88 insertions(+), 47 deletions(-)
9780
9781diff --git a/src/org/openstreetmap/josm/data/vector/DataStore.java b/src/org/openstreetmap/josm/data/vector/DataStore.java
9782index 9de044f62..5175a534b 100644
9783--- a/src/org/openstreetmap/josm/data/vector/DataStore.java
9784+++ b/src/org/openstreetmap/josm/data/vector/DataStore.java
9785@@ -20,6 +20,7 @@ import org.openstreetmap.josm.data.osm.IWay;
9786 import org.openstreetmap.josm.data.osm.PrimitiveId;
9787 import org.openstreetmap.josm.data.osm.QuadBucketPrimitiveStore;
9788 import org.openstreetmap.josm.data.osm.Storage;
9789+import org.openstreetmap.josm.tools.Logging;
9790
9791 /**
9792 * A class that stores data (essentially a simple {@link DataSet})
9793@@ -46,6 +47,7 @@ class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R exte
9794 protected final int zoom;
9795 protected final LocalQuadBucketPrimitiveStore<N, W, R> store = new LocalQuadBucketPrimitiveStore<>();
9796 protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true);
9797+ // TODO what happens when I use hashCode?
9798 protected final Set<Tile> addedTiles = new HashSet<>();
9799 protected final Map<PrimitiveId, O> primitivesMap = allPrimitives
9800 .foreignKey(new Storage.PrimitiveIdHash());
9801@@ -96,13 +98,20 @@ class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R exte
9802 if (primitive == null) {
9803 return;
9804 }
9805- // This is deliberate -- attempting to remove the primitive twice causes issues
9806- synchronized (primitive) {
9807+ try {
9808+ this.readWriteLock.writeLock().lockInterruptibly();
9809 if (this.allPrimitives.contains(primitive)) {
9810 this.store.removePrimitive(primitive);
9811 this.allPrimitives.remove(primitive);
9812 this.primitivesMap.remove(primitive.getPrimitiveId());
9813 }
9814+ } catch (InterruptedException e) {
9815+ Logging.error(e);
9816+ Thread.currentThread().interrupt();
9817+ } finally {
9818+ if (this.readWriteLock.isWriteLockedByCurrentThread()) {
9819+ this.readWriteLock.writeLock().unlock();
9820+ }
9821 }
9822 }
9823
9824diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
9825index dfa9334a3..8e18dae25 100644
9826--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
9827+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
9828@@ -474,7 +474,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
9829 final int currentZoom = tile.getZoom();
9830 // computeIfAbsent should be thread safe (ConcurrentHashMap indicates it is, anyway)
9831 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
9832- tryWrite(dataStore, () -> dataStore.addTile(tile));
9833+ dataStore.addTile(tile);
9834 }
9835
9836 /**
9837diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
9838index f486651b6..92a74be74 100644
9839--- a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
9840+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
9841@@ -28,6 +28,7 @@ import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
9842 import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter;
9843 import org.openstreetmap.josm.tools.Destroyable;
9844 import org.openstreetmap.josm.tools.Geometry;
9845+import org.openstreetmap.josm.tools.Logging;
9846
9847 /**
9848 * A data store for Vector Data sets
9849@@ -288,57 +289,83 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
9850 * @param tile The tile to add
9851 * @param <T> The tile type
9852 */
9853- public synchronized <T extends Tile & VectorTile> void addTile(T tile) {
9854+ public <T extends Tile & VectorTile> void addTile(T tile) {
9855 Optional<Tile> previous = this.addedTiles.stream()
9856- .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
9857+ .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
9858 // Check if we have already added the tile (just to save processing time)
9859 if (!previous.isPresent() || (!previous.get().isLoaded() && !previous.get().isLoading())) {
9860 previous.ifPresent(this.addedTiles::remove);
9861 this.addedTiles.add(tile);
9862- for (Layer layer : tile.getLayers()) {
9863- layer.getFeatures().forEach(feature -> {
9864- org.openstreetmap.josm.data.imagery.vectortile.mapbox.Geometry geometry = feature
9865- .getGeometryObject();
9866- List<VectorPrimitive> featureObjects = new ArrayList<>();
9867- List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>();
9868- geometry.getShapes().forEach(shape -> {
9869- final VectorPrimitive primitive;
9870- if (shape instanceof Ellipse2D) {
9871- primitive = pointToNode(tile, layer, featureObjects,
9872- (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY());
9873- } else if (shape instanceof Path2D) {
9874- primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape).stream().findFirst()
9875- .orElse(null);
9876- } else if (shape instanceof Area) {
9877- primitive = areaToRelation(tile, layer, featureObjects, (Area) shape);
9878- primitive.put("type", "multipolygon");
9879- } else {
9880- // We shouldn't hit this, but just in case
9881- throw new UnsupportedOperationException();
9882- }
9883- primaryFeatureObjects.add(primitive);
9884- });
9885+ VectorDataStore tStore = new VectorDataStore(this.dataSet, this.zoom);
9886+ tStore.createDataTile(tile);
9887+ try {
9888+ this.getReadWriteLock().writeLock().lockInterruptibly();
9889+ tStore.getAllPrimitives().forEach(this::addPrimitive);
9890+ } catch (InterruptedException e) {
9891+ Logging.error(e);
9892+ Thread.currentThread().interrupt();
9893+ } finally {
9894+ if (this.getReadWriteLock().isWriteLockedByCurrentThread()) {
9895+ this.getReadWriteLock().writeLock().unlock();
9896+ }
9897+ }
9898+ }
9899+ }
9900+
9901+ private <T extends Tile & VectorTile> void createDataTile(T tile) {
9902+ for (Layer layer : tile.getLayers()) {
9903+ layer.getFeatures().forEach(feature -> {
9904+ org.openstreetmap.josm.data.imagery.vectortile.mapbox.Geometry geometry = feature
9905+ .getGeometryObject();
9906+ List<VectorPrimitive> featureObjects = new ArrayList<>();
9907+ List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>();
9908+ geometry.getShapes().forEach(shape -> {
9909 final VectorPrimitive primitive;
9910- if (primaryFeatureObjects.size() == 1) {
9911- primitive = primaryFeatureObjects.get(0);
9912- if (primitive instanceof IRelation && !primitive.isMultipolygon()) {
9913- primitive.put(JOSM_MERGE_TYPE_KEY, "merge");
9914- }
9915- } else if (!primaryFeatureObjects.isEmpty()) {
9916- VectorRelation relation = new VectorRelation(layer.getName());
9917- primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim))
9918- .forEach(relation::addRelationMember);
9919- primitive = relation;
9920+ if (shape instanceof Ellipse2D) {
9921+ primitive = pointToNode(tile, layer, featureObjects,
9922+ (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY());
9923+ } else if (shape instanceof Path2D) {
9924+ primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape).stream().findFirst()
9925+ .orElse(null);
9926+ } else if (shape instanceof Area) {
9927+ primitive = areaToRelation(tile, layer, featureObjects, (Area) shape);
9928+ primitive.put("type", "multipolygon");
9929 } else {
9930- return;
9931+ // We shouldn't hit this, but just in case
9932+ throw new UnsupportedOperationException();
9933 }
9934- primitive.setId(feature.getId());
9935- feature.getTags().forEach(primitive::put);
9936- featureObjects.forEach(this::addPrimitive);
9937- primaryFeatureObjects.forEach(this::addPrimitive);
9938- this.addPrimitive(primitive);
9939+ primaryFeatureObjects.add(primitive);
9940 });
9941- }
9942+ final VectorPrimitive primitive;
9943+ if (primaryFeatureObjects.size() == 1) {
9944+ primitive = primaryFeatureObjects.get(0);
9945+ if (primitive instanceof IRelation && !primitive.isMultipolygon()) {
9946+ primitive.put(JOSM_MERGE_TYPE_KEY, "merge");
9947+ }
9948+ } else if (!primaryFeatureObjects.isEmpty()) {
9949+ VectorRelation relation = new VectorRelation(layer.getName());
9950+ primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim))
9951+ .forEach(relation::addRelationMember);
9952+ primitive = relation;
9953+ } else {
9954+ return;
9955+ }
9956+ primitive.setId(feature.getId());
9957+ // Version 1 <i>does not guarantee</i> that non-zero ids are unique
9958+ // We depend upon unique ids in the data store
9959+ if (layer.getVersion() == 1 && feature.getId() != 0 && this.primitivesMap.containsKey(primitive.getPrimitiveId())) {
9960+ // Reduce total memory usage by getting pre-existing strings, if they exist
9961+ // Avoid interning, as the intern pool is known to be slow when many strings are added to it (Java 8)
9962+ // HashSets tend to be a bit faster when interning many strings with relatively low usage
9963+ String originalId = Long.toString(feature.getId());
9964+ primitive.put("original_id", this.dataSet.allPrimitives().parallelStream().map(p -> p.get("original_id")).filter(originalId::equals).findAny().orElse(originalId));
9965+ primitive.setId(primitive.getIdGenerator().generateUniqueId());
9966+ }
9967+ feature.getTags().forEach(primitive::put);
9968+ featureObjects.forEach(this::addPrimitive);
9969+ primaryFeatureObjects.forEach(this::addPrimitive);
9970+ this.addPrimitive(primitive);
9971+ });
9972 }
9973 }
9974
9975diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
9976index 38ab53ad2..a33c396f4 100644
9977--- a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
9978+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
9979@@ -15,6 +15,7 @@ import java.util.List;
9980 import java.util.Map;
9981 import java.util.stream.Collectors;
9982
9983+import org.junit.jupiter.api.RepeatedTest;
9984 import org.openstreetmap.josm.TestUtils;
9985 import org.openstreetmap.josm.data.imagery.ImageryInfo;
9986 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
9987@@ -113,13 +114,17 @@ class VectorDataSetTest {
9988 dataSet.setZoom(14);
9989 loadTile(this.layer, 14, 3248, 6258);
9990
9991- // There _does_ appear to be some kind of race condition though
9992- Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> dataSet.getNodes().size() > 50);
9993 // Actual test
9994 // With Mapillary, only ends of ways should be untagged
9995 // There are 55 actual "nodes" in the data with two nodes for the ends of the way.
9996 // One of the end nodes is a duplicate of an actual node.
9997 assertEquals(56, dataSet.getNodes().size());
9998+ // There should be 55 nodes from the mapillary-images layer
9999+ assertEquals(55, dataSet.getNodes().stream().filter(node -> "mapillary-images".equals(node.getLayer())).count());
10000+ // Please note that this dataset originally had the <i>same</i> id for all the images
10001+ // (MVT v2 explicitly said that ids had to be unique in a layer, MVT v1 did not)
10002+ assertEquals(55, dataSet.getNodes().stream().map(node -> node.get("original_id")).count());
10003+ assertEquals(1, dataSet.getNodes().stream().map(node -> node.get("original_id")).distinct().count());
10004 assertEquals(1, dataSet.getWays().size());
10005 assertEquals(0, dataSet.getRelations().size());
10006 }
10007--
10008GitLab
10009
10010
10011From 8ea1c070ed9de9cf0f02d209713a862f0f5f960d Mon Sep 17 00:00:00 2001
10012From: Taylor Smock <tsmock@fb.com>
10013Date: Wed, 21 Apr 2021 16:44:57 -0600
10014Subject: [PATCH 12/50] Add layer filtering back in
10015
10016Signed-off-by: Taylor Smock <tsmock@fb.com>
10017---
10018 .../josm/data/vector/VectorDataSet.java | 21 +++++++++++++-
10019 .../josm/data/vector/VectorDataStore.java | 28 ++++++++++++++++---
10020 .../josm/data/vector/VectorPrimitive.java | 5 ++++
10021 .../josm/gui/layer/imagery/MVTLayer.java | 1 +
10022 4 files changed, 50 insertions(+), 5 deletions(-)
10023
10024diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
10025index 8e18dae25..4e9cb9ffd 100644
10026--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
10027+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
10028@@ -49,6 +49,8 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
10029 // Both of these listener lists are useless, since they expect OsmPrimitives at this time
10030 private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create();
10031 private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create();
10032+ private static final String[] NO_INVISIBLE_LAYERS = new String[0];
10033+ private String[] invisibleLayers = NO_INVISIBLE_LAYERS;
10034 private boolean lock = true;
10035 private String name;
10036 private short mappaintCacheIdx = 1;
10037@@ -474,7 +476,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
10038 final int currentZoom = tile.getZoom();
10039 // computeIfAbsent should be thread safe (ConcurrentHashMap indicates it is, anyway)
10040 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
10041- dataStore.addTile(tile);
10042+ dataStore.addTile(tile, this.invisibleLayers);
10043 }
10044
10045 /**
10046@@ -538,4 +540,21 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
10047 this.styles = null;
10048 }
10049 }
10050+
10051+ /**
10052+ * Mark some layers as invisible
10053+ * @param invisibleLayers The layer to not show
10054+ */
10055+ public void setInvisibleLayers(Collection<String> invisibleLayers) {
10056+ if (invisibleLayers == null || invisibleLayers.isEmpty() || invisibleLayers.stream().filter(Objects::nonNull).filter(string -> !string.isEmpty()).count() == 0) {
10057+ this.invisibleLayers = NO_INVISIBLE_LAYERS;
10058+ return;
10059+ }
10060+ String[] currentInvisibleLayers = invisibleLayers.stream().filter(Objects::nonNull).toArray(String[]::new);
10061+ this.invisibleLayers = currentInvisibleLayers;
10062+ List<String> temporaryList = Arrays.asList(currentInvisibleLayers);
10063+ this.dataStoreMap.values().forEach(dataStore -> {
10064+ dataStore.getAllPrimitives().parallelStream().forEach(primitive -> primitive.setVisible(!temporaryList.contains(primitive.getLayer())));
10065+ });
10066+ }
10067 }
10068diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10069index 92a74be74..4f2e37252 100644
10070--- a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10071+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10072@@ -6,6 +6,7 @@ import java.awt.geom.Ellipse2D;
10073 import java.awt.geom.Path2D;
10074 import java.awt.geom.PathIterator;
10075 import java.util.ArrayList;
10076+import java.util.Arrays;
10077 import java.util.Collection;
10078 import java.util.Collections;
10079 import java.util.List;
10080@@ -28,14 +29,17 @@ import org.openstreetmap.josm.data.osm.UniqueIdGenerator;
10081 import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter;
10082 import org.openstreetmap.josm.tools.Destroyable;
10083 import org.openstreetmap.josm.tools.Geometry;
10084+import org.openstreetmap.josm.tools.JosmRuntimeException;
10085 import org.openstreetmap.josm.tools.Logging;
10086
10087+
10088 /**
10089 * A data store for Vector Data sets
10090 * @author Taylor Smock
10091 * @since xxx
10092 */
10093 class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> implements Destroyable {
10094+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
10095 private static final String JOSM_MERGE_TYPE_KEY = "josm_merge_type";
10096 private final VectorDataSet dataSet;
10097
10098@@ -174,7 +178,7 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10099 private synchronized <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer,
10100 Collection<VectorPrimitive> featureObjects, int x, int y) {
10101 final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile);
10102- final int layerExtent = layer.getExtent() * 2;
10103+ final int layerExtent = layer.getExtent();
10104 final ICoordinate lowerRight = tile.getTileSource()
10105 .tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
10106 final ICoordinate coords = new Coordinate(
10107@@ -286,10 +290,21 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10108
10109 /**
10110 * Add a tile to this data store
10111+ * @param <T> The tile type
10112 * @param tile The tile to add
10113+ */
10114+ public synchronized <T extends Tile & VectorTile> void addTile(T tile) {
10115+ addTile(tile, EMPTY_STRING_ARRAY);
10116+ }
10117+
10118+ /**
10119+ * Add a tile to this data store
10120 * @param <T> The tile type
10121+ * @param tile The tile to add
10122+ * @param invisibleLayers Any invisible current invisible layers
10123 */
10124- public <T extends Tile & VectorTile> void addTile(T tile) {
10125+ public <T extends Tile & VectorTile> void addTile(T tile, String[] invisibleLayers) {
10126+ List<String> invisibleLayerList = Arrays.asList(invisibleLayers);
10127 Optional<Tile> previous = this.addedTiles.stream()
10128 .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
10129 // Check if we have already added the tile (just to save processing time)
10130@@ -297,7 +312,7 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10131 previous.ifPresent(this.addedTiles::remove);
10132 this.addedTiles.add(tile);
10133 VectorDataStore tStore = new VectorDataStore(this.dataSet, this.zoom);
10134- tStore.createDataTile(tile);
10135+ tStore.createDataTile(tile, invisibleLayerList);
10136 try {
10137 this.getReadWriteLock().writeLock().lockInterruptibly();
10138 tStore.getAllPrimitives().forEach(this::addPrimitive);
10139@@ -312,7 +327,7 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10140 }
10141 }
10142
10143- private <T extends Tile & VectorTile> void createDataTile(T tile) {
10144+ private <T extends Tile & VectorTile> void createDataTile(T tile, List<String> invisibleLayerList) {
10145 for (Layer layer : tile.getLayers()) {
10146 layer.getFeatures().forEach(feature -> {
10147 org.openstreetmap.josm.data.imagery.vectortile.mapbox.Geometry geometry = feature
10148@@ -364,6 +379,11 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10149 feature.getTags().forEach(primitive::put);
10150 featureObjects.forEach(this::addPrimitive);
10151 primaryFeatureObjects.forEach(this::addPrimitive);
10152+ if (invisibleLayerList.contains(primitive.getLayer())) {
10153+ primitive.setVisible(false);
10154+ featureObjects.forEach(p -> p.setVisible(false));
10155+ primaryFeatureObjects.forEach(p -> p.setVisible(false));
10156+ }
10157 this.addPrimitive(primitive);
10158 });
10159 }
10160diff --git a/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
10161index 17b5bef6f..ed9c93937 100644
10162--- a/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
10163+++ b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
10164@@ -253,4 +253,9 @@ public abstract class VectorPrimitive extends AbstractPrimitive implements DataL
10165 public String getLayer() {
10166 return this.layer;
10167 }
10168+
10169+ @Override
10170+ public boolean isDrawable() {
10171+ return super.isDrawable() && this.isVisible();
10172+ }
10173 }
10174diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10175index aa335f7b0..4007e8495 100644
10176--- a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10177+++ b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10178@@ -138,6 +138,7 @@ public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSour
10179 actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true),
10180 layer -> {
10181 layerNames.compute(layer, (key, value) -> Boolean.FALSE.equals(value));
10182+ this.dataSet.setInvisibleLayers(layerNames.entrySet().stream().filter(entry -> Boolean.FALSE.equals(entry.getValue())).map(Map.Entry::getKey).collect(Collectors.toList()));
10183 this.invalidate();
10184 }));
10185 }
10186--
10187GitLab
10188
10189
10190From 5f86499538c07e0c2b7659dcb5b2071e49021624 Mon Sep 17 00:00:00 2001
10191From: Taylor Smock <tsmock@fb.com>
10192Date: Wed, 21 Apr 2021 16:45:33 -0600
10193Subject: [PATCH 13/50] MvtLayer: Add additional information to objects when
10194 converting
10195
10196Signed-off-by: Taylor Smock <tsmock@fb.com>
10197---
10198 .../josm/gui/layer/imagery/MVTLayer.java | 14 ++++++++++----
10199 1 file changed, 10 insertions(+), 4 deletions(-)
10200
10201diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10202index 4007e8495..d0b98d633 100644
10203--- a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10204+++ b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10205@@ -173,6 +173,8 @@ public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSour
10206 Node newNode = new Node(vectorNode.getCoor());
10207 if (vectorNode.isTagged()) {
10208 vectorNode.getInterestingTags().forEach(newNode::put);
10209+ newNode.put("layer", vectorNode.getLayer());
10210+ newNode.put("id", Long.toString(vectorNode.getId()));
10211 }
10212 nodeMap.put(vectorNode, newNode);
10213 }
10214@@ -184,6 +186,8 @@ public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSour
10215 newWay.setNodes(nodes);
10216 if (vectorWay.isTagged()) {
10217 vectorWay.getInterestingTags().forEach(newWay::put);
10218+ newWay.put("layer", vectorWay.getLayer());
10219+ newWay.put("id", Long.toString(vectorWay.getId()));
10220 }
10221 wayMap.put(vectorWay, newWay);
10222 }
10223@@ -191,9 +195,11 @@ public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSour
10224 // Finally, add Relations
10225 Map<VectorRelation, Relation> relationMap = new HashMap<>(dataSet.getRelations().size());
10226 for (VectorRelation vectorRelation : dataSet.getRelations()) {
10227- Relation relation = new Relation();
10228+ Relation newRelation = new Relation();
10229 if (vectorRelation.isTagged()) {
10230- vectorRelation.getInterestingTags().forEach(relation::put);
10231+ vectorRelation.getInterestingTags().forEach(newRelation::put);
10232+ newRelation.put("layer", vectorRelation.getLayer());
10233+ newRelation.put("id", Long.toString(vectorRelation.getId()));
10234 }
10235 List<RelationMember> members = vectorRelation.getMembers().stream().map(member -> {
10236 final OsmPrimitive primitive;
10237@@ -211,8 +217,8 @@ public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSour
10238 if (primitive == null) return null;
10239 return new RelationMember(member.getRole(), primitive);
10240 }).filter(Objects::nonNull).collect(Collectors.toList());
10241- relation.setMembers(members);
10242- relationMap.put(vectorRelation, relation);
10243+ newRelation.setMembers(members);
10244+ relationMap.put(vectorRelation, newRelation);
10245 }
10246 try {
10247 osmData.beginUpdate();
10248--
10249GitLab
10250
10251
10252From abb734cd52710755b287f596521510f7659f4dbb Mon Sep 17 00:00:00 2001
10253From: Taylor Smock <tsmock@fb.com>
10254Date: Wed, 21 Apr 2021 17:01:57 -0600
10255Subject: [PATCH 14/50] VectorDataStore: Don't attempt to deduplicate ways
10256
10257This has led to some ConcurrentModificationExceptions and/or primitives
10258that cannot be removed. This functionality should be done in a different
10259ticket.
10260
10261Signed-off-by: Taylor Smock <tsmock@fb.com>
10262---
10263 .../josm/data/vector/DataStore.java | 2 +-
10264 .../josm/data/vector/VectorDataStore.java | 35 +++++--------------
10265 2 files changed, 10 insertions(+), 27 deletions(-)
10266
10267diff --git a/src/org/openstreetmap/josm/data/vector/DataStore.java b/src/org/openstreetmap/josm/data/vector/DataStore.java
10268index 5175a534b..86f1948d1 100644
10269--- a/src/org/openstreetmap/josm/data/vector/DataStore.java
10270+++ b/src/org/openstreetmap/josm/data/vector/DataStore.java
10271@@ -48,7 +48,7 @@ class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R exte
10272 protected final LocalQuadBucketPrimitiveStore<N, W, R> store = new LocalQuadBucketPrimitiveStore<>();
10273 protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true);
10274 // TODO what happens when I use hashCode?
10275- protected final Set<Tile> addedTiles = new HashSet<>();
10276+ protected final Set<Tile> addedTiles = Collections.synchronizedSet(new HashSet<>());
10277 protected final Map<PrimitiveId, O> primitivesMap = allPrimitives
10278 .foreignKey(new Storage.PrimitiveIdHash());
10279 protected final Collection<DataSource> dataSources = new LinkedList<>();
10280diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10281index 4f2e37252..cd5a03fe9 100644
10282--- a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10283+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10284@@ -9,8 +9,8 @@ import java.util.ArrayList;
10285 import java.util.Arrays;
10286 import java.util.Collection;
10287 import java.util.Collections;
10288+import java.util.HashSet;
10289 import java.util.List;
10290-import java.util.Objects;
10291 import java.util.Optional;
10292 import java.util.stream.Collectors;
10293
10294@@ -127,24 +127,7 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10295 iteration++;
10296 relationWayList.removeIf(wayList::contains);
10297 }
10298- if (!relationWayList.isEmpty()) {
10299- return relation;
10300- }
10301- // Merge ways
10302- List<VectorNode> nodes = new ArrayList<>();
10303- for (VectorWay way : wayList) {
10304- for (VectorNode node : way.getNodes()) {
10305- if (nodes.isEmpty() || !Objects.equals(nodes.get(nodes.size() - 1), node)) {
10306- nodes.add(node);
10307- }
10308- }
10309- }
10310- VectorWay way = wayList.get(0);
10311- way.setNodes(nodes);
10312- wayList.remove(way);
10313- wayList.forEach(this::removePrimitive);
10314- this.removePrimitive(relation);
10315- return way;
10316+ return relation;
10317 }
10318
10319 private static <N extends INode, W extends IWay<N>> boolean canMergeWays(W old, W toAdd, boolean allowReverse) {
10320@@ -305,8 +288,11 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10321 */
10322 public <T extends Tile & VectorTile> void addTile(T tile, String[] invisibleLayers) {
10323 List<String> invisibleLayerList = Arrays.asList(invisibleLayers);
10324- Optional<Tile> previous = this.addedTiles.stream()
10325- .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
10326+ Optional<Tile> previous;
10327+ synchronized (this.addedTiles) {
10328+ previous = this.addedTiles.stream()
10329+ .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
10330+ }
10331 // Check if we have already added the tile (just to save processing time)
10332 if (!previous.isPresent() || (!previous.get().isLoaded() && !previous.get().isLoading())) {
10333 previous.ifPresent(this.addedTiles::remove);
10334@@ -369,11 +355,8 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10335 // Version 1 <i>does not guarantee</i> that non-zero ids are unique
10336 // We depend upon unique ids in the data store
10337 if (layer.getVersion() == 1 && feature.getId() != 0 && this.primitivesMap.containsKey(primitive.getPrimitiveId())) {
10338- // Reduce total memory usage by getting pre-existing strings, if they exist
10339- // Avoid interning, as the intern pool is known to be slow when many strings are added to it (Java 8)
10340- // HashSets tend to be a bit faster when interning many strings with relatively low usage
10341- String originalId = Long.toString(feature.getId());
10342- primitive.put("original_id", this.dataSet.allPrimitives().parallelStream().map(p -> p.get("original_id")).filter(originalId::equals).findAny().orElse(originalId));
10343+ // This, unfortunately, makes a new string
10344+ primitive.put("original_id", Long.toString(feature.getId()));
10345 primitive.setId(primitive.getIdGenerator().generateUniqueId());
10346 }
10347 feature.getTags().forEach(primitive::put);
10348--
10349GitLab
10350
10351
10352From 676ec09fa4726006134816f152c00e65364b49dd Mon Sep 17 00:00:00 2001
10353From: Taylor Smock <tsmock@fb.com>
10354Date: Thu, 22 Apr 2021 08:03:40 -0600
10355Subject: [PATCH 15/50] MVTLayer: Make some options expert only
10356
10357Signed-off-by: Taylor Smock <tsmock@fb.com>
10358---
10359 .../josm/gui/layer/imagery/MVTLayer.java | 23 +++++++++++--------
10360 1 file changed, 13 insertions(+), 10 deletions(-)
10361
10362diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10363index d0b98d633..876ce5399 100644
10364--- a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10365+++ b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10366@@ -25,6 +25,7 @@ import javax.swing.JMenuItem;
10367
10368 import org.openstreetmap.gui.jmapviewer.Tile;
10369 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
10370+import org.openstreetmap.josm.actions.ExpertToggleAction;
10371 import org.openstreetmap.josm.data.Bounds;
10372 import org.openstreetmap.josm.data.imagery.ImageryInfo;
10373 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
10374@@ -134,17 +135,19 @@ public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSour
10375 ArrayList<Action> actions = new ArrayList<>(Arrays.asList(super.getMenuEntries()));
10376 // Add separator between Info and the layers
10377 actions.add(SeparatorLayerAction.INSTANCE);
10378- for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) {
10379- actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true),
10380- layer -> {
10381- layerNames.compute(layer, (key, value) -> Boolean.FALSE.equals(value));
10382- this.dataSet.setInvisibleLayers(layerNames.entrySet().stream().filter(entry -> Boolean.FALSE.equals(entry.getValue())).map(Map.Entry::getKey).collect(Collectors.toList()));
10383- this.invalidate();
10384- }));
10385+ if (ExpertToggleAction.isExpert()) {
10386+ for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) {
10387+ actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true),
10388+ layer -> {
10389+ layerNames.compute(layer, (key, value) -> Boolean.FALSE.equals(value));
10390+ this.dataSet.setInvisibleLayers(layerNames.entrySet().stream().filter(entry -> Boolean.FALSE.equals(entry.getValue())).map(Map.Entry::getKey).collect(Collectors.toList()));
10391+ this.invalidate();
10392+ }));
10393+ }
10394+ // Add separator between layers and convert action
10395+ actions.add(SeparatorLayerAction.INSTANCE);
10396+ actions.add(new ConvertLayerAction(this));
10397 }
10398- // Add separator between layers and convert action
10399- actions.add(SeparatorLayerAction.INSTANCE);
10400- actions.add(new ConvertLayerAction(this));
10401 return actions.toArray(EMPTY_ACTIONS);
10402 }
10403
10404--
10405GitLab
10406
10407
10408From 88b205a99a757dd4d929eca11d66b474decf698d Mon Sep 17 00:00:00 2001
10409From: Taylor Smock <tsmock@fb.com>
10410Date: Thu, 22 Apr 2021 08:04:00 -0600
10411Subject: [PATCH 16/50] MVT v1 and v2 don't *require* that ids be unique.
10412
10413Signed-off-by: Taylor Smock <tsmock@fb.com>
10414---
10415 .../josm/data/vector/VectorDataStore.java | 26 ++++++++++++++++---
10416 1 file changed, 22 insertions(+), 4 deletions(-)
10417
10418diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10419index cd5a03fe9..34c5dbd47 100644
10420--- a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10421+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10422@@ -9,11 +9,17 @@ import java.util.ArrayList;
10423 import java.util.Arrays;
10424 import java.util.Collection;
10425 import java.util.Collections;
10426+import java.util.HashMap;
10427 import java.util.HashSet;
10428 import java.util.List;
10429+import java.util.Map;
10430+import java.util.Objects;
10431 import java.util.Optional;
10432 import java.util.stream.Collectors;
10433+import java.util.stream.Stream;
10434
10435+import com.google.common.base.Functions;
10436+import org.antlr.v4.runtime.atn.SemanticContext;
10437 import org.openstreetmap.gui.jmapviewer.Coordinate;
10438 import org.openstreetmap.gui.jmapviewer.Tile;
10439 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
10440@@ -21,6 +27,7 @@ import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
10441 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
10442 import org.openstreetmap.josm.data.osm.BBox;
10443 import org.openstreetmap.josm.data.osm.INode;
10444+import org.openstreetmap.josm.data.osm.IPrimitive;
10445 import org.openstreetmap.josm.data.osm.IRelation;
10446 import org.openstreetmap.josm.data.osm.IWay;
10447 import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
10448@@ -41,6 +48,7 @@ import org.openstreetmap.josm.tools.Logging;
10449 class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> implements Destroyable {
10450 private static final String[] EMPTY_STRING_ARRAY = new String[0];
10451 private static final String JOSM_MERGE_TYPE_KEY = "josm_merge_type";
10452+ private static final String ORIGINAL_ID = "original_id";
10453 private final VectorDataSet dataSet;
10454
10455 VectorDataStore(VectorDataSet dataSet, int zoom) {
10456@@ -352,11 +360,11 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10457 return;
10458 }
10459 primitive.setId(feature.getId());
10460- // Version 1 <i>does not guarantee</i> that non-zero ids are unique
10461+ // Version 1 <i>and</i> 2 <i>do not guarantee</i> that non-zero ids are unique
10462 // We depend upon unique ids in the data store
10463- if (layer.getVersion() == 1 && feature.getId() != 0 && this.primitivesMap.containsKey(primitive.getPrimitiveId())) {
10464+ if (feature.getId() != 0 && this.primitivesMap.containsKey(primitive.getPrimitiveId())) {
10465 // This, unfortunately, makes a new string
10466- primitive.put("original_id", Long.toString(feature.getId()));
10467+ primitive.put(ORIGINAL_ID, Long.toString(feature.getId()));
10468 primitive.setId(primitive.getIdGenerator().generateUniqueId());
10469 }
10470 feature.getTags().forEach(primitive::put);
10471@@ -367,9 +375,19 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10472 featureObjects.forEach(p -> p.setVisible(false));
10473 primaryFeatureObjects.forEach(p -> p.setVisible(false));
10474 }
10475- this.addPrimitive(primitive);
10476+ try {
10477+ this.addPrimitive(primitive);
10478+ } catch (JosmRuntimeException e) {
10479+ Logging.error("{0}/{1}/{2}: {3}", tile.getZoom(), tile.getXtile(), tile.getYtile(), primitive.get("key"));
10480+ throw e;
10481+ }
10482 });
10483 }
10484+ // Replace original_ids with the same object (reduce memory usage)
10485+ // Strings aren't interned automatically (see
10486+ Collection<IPrimitive> primitives = this.dataSet.allPrimitives().stream().filter(p -> p.hasKey(ORIGINAL_ID)).collect(Collectors.toList());
10487+ List<String> toReplace = primitives.stream().map(p -> p.get(ORIGINAL_ID)).filter(Objects::nonNull).collect(Collectors.toList());
10488+ primitives.stream().filter(p -> toReplace.contains(p.get(ORIGINAL_ID))).forEach(p -> p.put(ORIGINAL_ID, toReplace.stream().filter(shared -> shared.equals(p.get(ORIGINAL_ID))).findAny().orElse(null)));
10489 }
10490
10491 @Override
10492--
10493GitLab
10494
10495
10496From 48a97b203f0f6a091fdf698fd4ba75b0a9449feb Mon Sep 17 00:00:00 2001
10497From: Taylor Smock <tsmock@fb.com>
10498Date: Thu, 22 Apr 2021 08:21:10 -0600
10499Subject: [PATCH 17/50] FIXUP: VectorDataStore: Optimize imports
10500
10501Signed-off-by: Taylor Smock <tsmock@fb.com>
10502---
10503 .../josm/data/vector/VectorDataStore.java | 32 ++++++++-----------
10504 1 file changed, 13 insertions(+), 19 deletions(-)
10505
10506diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10507index 34c5dbd47..bd76e3538 100644
10508--- a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10509+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10510@@ -1,25 +1,6 @@
10511 // License: GPL. For details, see LICENSE file.
10512 package org.openstreetmap.josm.data.vector;
10513
10514-import java.awt.geom.Area;
10515-import java.awt.geom.Ellipse2D;
10516-import java.awt.geom.Path2D;
10517-import java.awt.geom.PathIterator;
10518-import java.util.ArrayList;
10519-import java.util.Arrays;
10520-import java.util.Collection;
10521-import java.util.Collections;
10522-import java.util.HashMap;
10523-import java.util.HashSet;
10524-import java.util.List;
10525-import java.util.Map;
10526-import java.util.Objects;
10527-import java.util.Optional;
10528-import java.util.stream.Collectors;
10529-import java.util.stream.Stream;
10530-
10531-import com.google.common.base.Functions;
10532-import org.antlr.v4.runtime.atn.SemanticContext;
10533 import org.openstreetmap.gui.jmapviewer.Coordinate;
10534 import org.openstreetmap.gui.jmapviewer.Tile;
10535 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
10536@@ -39,6 +20,19 @@ import org.openstreetmap.josm.tools.Geometry;
10537 import org.openstreetmap.josm.tools.JosmRuntimeException;
10538 import org.openstreetmap.josm.tools.Logging;
10539
10540+import java.awt.geom.Area;
10541+import java.awt.geom.Ellipse2D;
10542+import java.awt.geom.Path2D;
10543+import java.awt.geom.PathIterator;
10544+import java.util.ArrayList;
10545+import java.util.Arrays;
10546+import java.util.Collection;
10547+import java.util.Collections;
10548+import java.util.List;
10549+import java.util.Objects;
10550+import java.util.Optional;
10551+import java.util.stream.Collectors;
10552+
10553
10554 /**
10555 * A data store for Vector Data sets
10556--
10557GitLab
10558
10559
10560From f8f50fa8560899c79a6d76167d9d4e594644d4af Mon Sep 17 00:00:00 2001
10561From: Taylor Smock <tsmock@fb.com>
10562Date: Thu, 22 Apr 2021 08:31:55 -0600
10563Subject: [PATCH 18/50] FIXUP: PMD
10564
10565Signed-off-by: Taylor Smock <tsmock@fb.com>
10566---
10567 src/org/openstreetmap/josm/data/vector/VectorDataSet.java | 8 ++++----
10568 .../openstreetmap/josm/data/vector/VectorDataStore.java | 8 +++++---
10569 .../openstreetmap/josm/gui/layer/imagery/MVTLayer.java | 4 +++-
10570 .../openstreetmap/josm/data/vector/VectorDataSetTest.java | 1 -
10571 4 files changed, 12 insertions(+), 9 deletions(-)
10572
10573diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
10574index 4e9cb9ffd..61eeb0279 100644
10575--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
10576+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
10577@@ -546,15 +546,15 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
10578 * @param invisibleLayers The layer to not show
10579 */
10580 public void setInvisibleLayers(Collection<String> invisibleLayers) {
10581- if (invisibleLayers == null || invisibleLayers.isEmpty() || invisibleLayers.stream().filter(Objects::nonNull).filter(string -> !string.isEmpty()).count() == 0) {
10582+ if (invisibleLayers == null || invisibleLayers.isEmpty()
10583+ || invisibleLayers.stream().filter(Objects::nonNull).allMatch(String::isEmpty)) {
10584 this.invisibleLayers = NO_INVISIBLE_LAYERS;
10585 return;
10586 }
10587 String[] currentInvisibleLayers = invisibleLayers.stream().filter(Objects::nonNull).toArray(String[]::new);
10588 this.invisibleLayers = currentInvisibleLayers;
10589 List<String> temporaryList = Arrays.asList(currentInvisibleLayers);
10590- this.dataStoreMap.values().forEach(dataStore -> {
10591- dataStore.getAllPrimitives().parallelStream().forEach(primitive -> primitive.setVisible(!temporaryList.contains(primitive.getLayer())));
10592- });
10593+ this.dataStoreMap.values().forEach(dataStore -> dataStore.getAllPrimitives().parallelStream()
10594+ .forEach(primitive -> primitive.setVisible(!temporaryList.contains(primitive.getLayer()))));
10595 }
10596 }
10597diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10598index bd76e3538..7e48e4313 100644
10599--- a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10600+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
10601@@ -33,7 +33,6 @@ import java.util.Objects;
10602 import java.util.Optional;
10603 import java.util.stream.Collectors;
10604
10605-
10606 /**
10607 * A data store for Vector Data sets
10608 * @author Taylor Smock
10609@@ -379,9 +378,12 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
10610 }
10611 // Replace original_ids with the same object (reduce memory usage)
10612 // Strings aren't interned automatically (see
10613- Collection<IPrimitive> primitives = this.dataSet.allPrimitives().stream().filter(p -> p.hasKey(ORIGINAL_ID)).collect(Collectors.toList());
10614+ Collection<IPrimitive> primitives = this.dataSet.allPrimitives().stream().filter(p -> p.hasKey(ORIGINAL_ID))
10615+ .collect(Collectors.toList());
10616 List<String> toReplace = primitives.stream().map(p -> p.get(ORIGINAL_ID)).filter(Objects::nonNull).collect(Collectors.toList());
10617- primitives.stream().filter(p -> toReplace.contains(p.get(ORIGINAL_ID))).forEach(p -> p.put(ORIGINAL_ID, toReplace.stream().filter(shared -> shared.equals(p.get(ORIGINAL_ID))).findAny().orElse(null)));
10618+ primitives.stream().filter(p -> toReplace.contains(p.get(ORIGINAL_ID)))
10619+ .forEach(p -> p.put(ORIGINAL_ID, toReplace.stream().filter(shared -> shared.equals(p.get(ORIGINAL_ID)))
10620+ .findAny().orElse(null)));
10621 }
10622
10623 @Override
10624diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10625index 876ce5399..2e7aac3e6 100644
10626--- a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10627+++ b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
10628@@ -140,7 +140,9 @@ public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSour
10629 actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true),
10630 layer -> {
10631 layerNames.compute(layer, (key, value) -> Boolean.FALSE.equals(value));
10632- this.dataSet.setInvisibleLayers(layerNames.entrySet().stream().filter(entry -> Boolean.FALSE.equals(entry.getValue())).map(Map.Entry::getKey).collect(Collectors.toList()));
10633+ this.dataSet.setInvisibleLayers(layerNames.entrySet().stream()
10634+ .filter(entry -> Boolean.FALSE.equals(entry.getValue()))
10635+ .map(Map.Entry::getKey).collect(Collectors.toList()));
10636 this.invalidate();
10637 }));
10638 }
10639diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
10640index a33c396f4..35e208979 100644
10641--- a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
10642+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
10643@@ -15,7 +15,6 @@ import java.util.List;
10644 import java.util.Map;
10645 import java.util.stream.Collectors;
10646
10647-import org.junit.jupiter.api.RepeatedTest;
10648 import org.openstreetmap.josm.TestUtils;
10649 import org.openstreetmap.josm.data.imagery.ImageryInfo;
10650 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
10651--
10652GitLab
10653
10654
10655From aafdee1cb7cd640b5980d5ccd280f713930d7758 Mon Sep 17 00:00:00 2001
10656From: Taylor Smock <tsmock@fb.com>
10657Date: Thu, 22 Apr 2021 10:54:47 -0600
10658Subject: [PATCH 19/50] FIXUP: Failing tests (largely 2048->4096, extent is
10659 uint not sint)
10660
10661Signed-off-by: Taylor Smock <tsmock@fb.com>
10662---
10663 .../josm/data/imagery/vectortile/mapbox/LayerTest.java | 4 ++--
10664 .../josm/data/imagery/vectortile/mapbox/MVTTileTest.java | 4 +---
10665 .../josm/data/imagery/vectortile/mapbox/style/SourceTest.java | 3 ++-
10666 3 files changed, 5 insertions(+), 6 deletions(-)
10667
10668diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
10669index fc3ba9c27..61e21dc60 100644
10670--- a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
10671+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
10672@@ -79,14 +79,14 @@ public class LayerTest {
10673 assertEquals("mapillary-sequences", sequenceLayer.getName());
10674 assertEquals(1, sequenceLayer.getFeatures().size());
10675 assertEquals(1, sequenceLayer.getGeometry().size());
10676- assertEquals(2048, sequenceLayer.getExtent());
10677+ assertEquals(4096, sequenceLayer.getExtent());
10678 assertEquals(1, sequenceLayer.getVersion());
10679
10680 Layer imageLayer = new Layer(layers.get(1).getBytes());
10681 assertEquals("mapillary-images", imageLayer.getName());
10682 assertEquals(116, imageLayer.getFeatures().size());
10683 assertEquals(116, imageLayer.getGeometry().size());
10684- assertEquals(2048, imageLayer.getExtent());
10685+ assertEquals(4096, imageLayer.getExtent());
10686 assertEquals(1, imageLayer.getVersion());
10687 }
10688
10689diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
10690index 66e4ea781..12b86ebc7 100644
10691--- a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
10692+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
10693@@ -67,9 +67,7 @@ public class MVTTileTest {
10694 if (isLoaded) {
10695 Awaitility.await().atMost(Durations.ONE_SECOND).until(() -> tile.getLayers() != null && tile.getLayers().size() > 1);
10696 assertEquals(2, tile.getLayers().size());
10697- // The test Mapillary tiles have 2048 instead of 4096 for their extent. This *may* change
10698- // in future Mapillary tiles, so if the test PBF files are updated, beware.
10699- assertEquals(2048, tile.getExtent());
10700+ assertEquals(4096, tile.getExtent());
10701 // Ensure that we have the clear image set, such that the tile doesn't add to the dataset again
10702 // and we don't have a loading image
10703 assertEquals(MVTTile.CLEAR_LOADED, tile.getImage());
10704diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java
10705index 500b5f8b5..8513be831 100644
10706--- a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java
10707+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java
10708@@ -20,6 +20,7 @@ import org.openstreetmap.josm.data.Bounds;
10709
10710 import nl.jqno.equalsverifier.EqualsVerifier;
10711 import org.junit.jupiter.api.Test;
10712+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.InvalidMapboxVectorTileException;
10713
10714 /**
10715 * Test class for {@link Source}
10716@@ -68,7 +69,7 @@ public class SourceTest {
10717 final JsonObject tileJsonSpec = Json.createObjectBuilder()
10718 .add("type", SourceType.VECTOR.name()).add("url", "some-random-url.com")
10719 .build();
10720- assertThrows(UnsupportedOperationException.class, () -> new Source("Test TileJson", tileJsonSpec));
10721+ assertThrows(InvalidMapboxVectorTileException.class, () -> new Source("Test TileJson", tileJsonSpec));
10722 }
10723
10724 @Test
10725--
10726GitLab
10727
10728
10729From d924f81efddc1da4c6b54977ece1c76aa2862d6d Mon Sep 17 00:00:00 2001
10730From: Taylor Smock <tsmock@fb.com>
10731Date: Tue, 27 Apr 2021 08:39:14 -0600
10732Subject: [PATCH 20/50] VectorDataSet: Add selection listener interface
10733
10734Signed-off-by: Taylor Smock <tsmock@fb.com>
10735---
10736 .../osm/event/IDataSelectionEventSource.java | 34 ++
10737 .../osm/event/IDataSelectionListener.java | 361 ++++++++++++++++++
10738 .../josm/data/vector/VectorDataSet.java | 88 ++++-
10739 3 files changed, 466 insertions(+), 17 deletions(-)
10740 create mode 100644 src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java
10741 create mode 100644 src/org/openstreetmap/josm/data/osm/event/IDataSelectionListener.java
10742
10743diff --git a/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java
10744new file mode 100644
10745index 000000000..e5fc0ea19
10746--- /dev/null
10747+++ b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java
10748@@ -0,0 +1,34 @@
10749+// License: GPL. For details, see LICENSE file.
10750+package org.openstreetmap.josm.data.osm.event;
10751+
10752+import org.openstreetmap.josm.data.osm.INode;
10753+import org.openstreetmap.josm.data.osm.IPrimitive;
10754+import org.openstreetmap.josm.data.osm.IRelation;
10755+import org.openstreetmap.josm.data.osm.IWay;
10756+import org.openstreetmap.josm.data.osm.OsmData;
10757+
10758+/**
10759+ * This interface indicates that the class can fire {@link IDataSelectionListener}.
10760+ * @author Taylor Smock, Michael Zangl (original code)
10761+ * @since xxx
10762+ * @param <O> the base type of OSM primitives
10763+ * @param <N> type representing OSM nodes
10764+ * @param <W> type representing OSM ways
10765+ * @param <R> type representing OSM relations
10766+ * @param <D> The dataset type
10767+ */
10768+public interface IDataSelectionEventSource<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> {
10769+ /**
10770+ * Add a listener
10771+ * @param listener The listener to add
10772+ * @return {@code true} if the listener was added
10773+ */
10774+ boolean addListener(IDataSelectionListener<O, N, W, R, D> listener);
10775+
10776+ /**
10777+ * Remove a listener
10778+ * @param listener The listener to remove
10779+ * @return {@code true} if the listener was removed
10780+ */
10781+ boolean removeListener(IDataSelectionListener<O, N, W, R, D> listener);
10782+}
10783diff --git a/src/org/openstreetmap/josm/data/osm/event/IDataSelectionListener.java b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionListener.java
10784new file mode 100644
10785index 000000000..5a95aa1c9
10786--- /dev/null
10787+++ b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionListener.java
10788@@ -0,0 +1,361 @@
10789+// License: GPL. For details, see LICENSE file.
10790+package org.openstreetmap.josm.data.osm.event;
10791+
10792+import org.openstreetmap.josm.data.osm.DataSelectionListener;
10793+import org.openstreetmap.josm.data.osm.INode;
10794+import org.openstreetmap.josm.data.osm.IPrimitive;
10795+import org.openstreetmap.josm.data.osm.IRelation;
10796+import org.openstreetmap.josm.data.osm.IWay;
10797+import org.openstreetmap.josm.data.osm.OsmData;
10798+import org.openstreetmap.josm.data.osm.OsmPrimitive;
10799+import org.openstreetmap.josm.tools.CheckParameterUtil;
10800+
10801+import java.util.Collections;
10802+import java.util.HashSet;
10803+import java.util.LinkedHashSet;
10804+import java.util.Set;
10805+import java.util.stream.Collectors;
10806+import java.util.stream.Stream;
10807+
10808+/**
10809+ * This interface is the same as {@link DataSelectionListener}, except it isn't {@link OsmPrimitive} specific.
10810+ * @author Taylor Smock, Michael Zangl (original code)
10811+ * @since xxx
10812+ * @param <O> the base type of OSM primitives
10813+ * @param <N> type representing OSM nodes
10814+ * @param <W> type representing OSM ways
10815+ * @param <R> type representing OSM relations
10816+ * @param <D> The dataset type
10817+ */
10818+@FunctionalInterface
10819+public interface IDataSelectionListener<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> {
10820+ /**
10821+ * Called whenever the selection is changed.
10822+ *
10823+ * You get notified about the new selection, the elements that were added and removed and the layer that triggered the event.
10824+ * @param event The selection change event.
10825+ * @see SelectionChangeEvent
10826+ */
10827+ void selectionChanged(SelectionChangeEvent<O, N, W, R, D> event);
10828+
10829+ /**
10830+ * The event that is fired when the selection changed.
10831+ * @author Michael Zangl
10832+ * @since xxx generics
10833+ * @param <O> the base type of OSM primitives
10834+ * @param <N> type representing OSM nodes
10835+ * @param <W> type representing OSM ways
10836+ * @param <R> type representing OSM relations
10837+ * @param <D> The dataset type
10838+ */
10839+ interface SelectionChangeEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> {
10840+ /**
10841+ * Gets the previous selection
10842+ * <p>
10843+ * This collection cannot be modified and will not change.
10844+ * @return The old selection
10845+ */
10846+ Set<O> getOldSelection();
10847+
10848+ /**
10849+ * Gets the new selection. New elements are added to the end of the collection.
10850+ * <p>
10851+ * This collection cannot be modified and will not change.
10852+ * @return The new selection
10853+ */
10854+ Set<O> getSelection();
10855+
10856+ /**
10857+ * Gets the primitives that have been removed from the selection.
10858+ * <p>
10859+ * Those are the primitives contained in {@link #getOldSelection()} but not in {@link #getSelection()}
10860+ * <p>
10861+ * This collection cannot be modified and will not change.
10862+ * @return The primitives that were removed
10863+ */
10864+ Set<O> getRemoved();
10865+
10866+ /**
10867+ * Gets the primitives that have been added to the selection.
10868+ * <p>
10869+ * Those are the primitives contained in {@link #getSelection()} but not in {@link #getOldSelection()}
10870+ * <p>
10871+ * This collection cannot be modified and will not change.
10872+ * @return The primitives that were added
10873+ */
10874+ Set<O> getAdded();
10875+
10876+ /**
10877+ * Gets the data set that triggered this selection event.
10878+ * @return The data set.
10879+ */
10880+ D getSource();
10881+
10882+ /**
10883+ * Test if this event did not change anything.
10884+ * <p>
10885+ * This will return <code>false</code> for all events that are sent to listeners, so you don't need to test it.
10886+ * @return <code>true</code> if this did not change the selection.
10887+ */
10888+ default boolean isNop() {
10889+ return getAdded().isEmpty() && getRemoved().isEmpty();
10890+ }
10891+ }
10892+
10893+ /**
10894+ * The base class for selection events
10895+ * @author Michael Zangl
10896+ * @since 12048, xxx (generics)
10897+ * @param <O> the base type of OSM primitives
10898+ * @param <N> type representing OSM nodes
10899+ * @param <W> type representing OSM ways
10900+ * @param <R> type representing OSM relations
10901+ * @param <D> The dataset type
10902+ */
10903+ abstract class AbstractSelectionEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> implements SelectionChangeEvent<O, N, W, R, D> {
10904+ private final D source;
10905+ private final Set<O> old;
10906+
10907+ protected AbstractSelectionEvent(D source, Set<O> old) {
10908+ CheckParameterUtil.ensureParameterNotNull(source, "source");
10909+ CheckParameterUtil.ensureParameterNotNull(old, "old");
10910+ this.source = source;
10911+ this.old = Collections.unmodifiableSet(old);
10912+ }
10913+
10914+ @Override
10915+ public Set<O> getOldSelection() {
10916+ return old;
10917+ }
10918+
10919+ @Override
10920+ public D getSource() {
10921+ return source;
10922+ }
10923+ }
10924+
10925+ /**
10926+ * The selection is replaced by a new selection
10927+ * @author Michael Zangl
10928+ * @since xxx (generics)
10929+ * @param <O> the base type of OSM primitives
10930+ * @param <N> type representing OSM nodes
10931+ * @param <W> type representing OSM ways
10932+ * @param <R> type representing OSM relations
10933+ * @param <D> The dataset type
10934+ */
10935+ class SelectionReplaceEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> extends AbstractSelectionEvent<O, N, W, R, D> {
10936+ private final Set<O> current;
10937+ private Set<O> removed;
10938+ private Set<O> added;
10939+
10940+ /**
10941+ * Create a {@link SelectionReplaceEvent}
10942+ * @param source The source dataset
10943+ * @param old The old primitives that were previously selected. The caller needs to ensure that this set is not modified.
10944+ * @param newSelection The primitives of the new selection.
10945+ */
10946+ public SelectionReplaceEvent(D source, Set<O> old, Stream<O> newSelection) {
10947+ super(source, old);
10948+ this.current = newSelection.collect(Collectors.toCollection(LinkedHashSet::new));
10949+ }
10950+
10951+ @Override
10952+ public Set<O> getSelection() {
10953+ return current;
10954+ }
10955+
10956+ @Override
10957+ public synchronized Set<O> getRemoved() {
10958+ if (removed == null) {
10959+ removed = getOldSelection().stream()
10960+ .filter(p -> !current.contains(p))
10961+ .collect(Collectors.toCollection(LinkedHashSet::new));
10962+ }
10963+ return removed;
10964+ }
10965+
10966+ @Override
10967+ public synchronized Set<O> getAdded() {
10968+ if (added == null) {
10969+ added = current.stream()
10970+ .filter(p -> !getOldSelection().contains(p)).collect(Collectors.toCollection(LinkedHashSet::new));
10971+ }
10972+ return added;
10973+ }
10974+
10975+ @Override
10976+ public String toString() {
10977+ return "SelectionReplaceEvent [current=" + current + ", removed=" + removed + ", added=" + added + ']';
10978+ }
10979+ }
10980+
10981+ /**
10982+ * Primitives are added to the selection
10983+ * @author Michael Zangl
10984+ * @since xxx (generics)
10985+ * @param <O> the base type of OSM primitives
10986+ * @param <N> type representing OSM nodes
10987+ * @param <W> type representing OSM ways
10988+ * @param <R> type representing OSM relations
10989+ * @param <D> The dataset type
10990+ */
10991+ class SelectionAddEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> extends AbstractSelectionEvent<O, N, W, R, D> {
10992+ private final Set<O> add;
10993+ private final Set<O> current;
10994+
10995+ /**
10996+ * Create a {@link SelectionAddEvent}
10997+ * @param source The source dataset
10998+ * @param old The old primitives that were previously selected. The caller needs to ensure that this set is not modified.
10999+ * @param toAdd The primitives to add.
11000+ */
11001+ public SelectionAddEvent(D source, Set<O> old, Stream<O> toAdd) {
11002+ super(source, old);
11003+ this.add = toAdd
11004+ .filter(p -> !old.contains(p))
11005+ .collect(Collectors.toCollection(LinkedHashSet::new));
11006+ if (this.add.isEmpty()) {
11007+ this.current = this.getOldSelection();
11008+ } else {
11009+ this.current = new LinkedHashSet<>(old);
11010+ this.current.addAll(add);
11011+ }
11012+ }
11013+
11014+ @Override
11015+ public Set<O> getSelection() {
11016+ return Collections.unmodifiableSet(current);
11017+ }
11018+
11019+ @Override
11020+ public Set<O> getRemoved() {
11021+ return Collections.emptySet();
11022+ }
11023+
11024+ @Override
11025+ public Set<O> getAdded() {
11026+ return Collections.unmodifiableSet(add);
11027+ }
11028+
11029+ @Override
11030+ public String toString() {
11031+ return "SelectionAddEvent [add=" + add + ", current=" + current + ']';
11032+ }
11033+ }
11034+
11035+ /**
11036+ * Primitives are removed from the selection
11037+ * @author Michael Zangl
11038+ * @since 12048, xxx (generics)
11039+ * @param <O> the base type of OSM primitives
11040+ * @param <N> type representing OSM nodes
11041+ * @param <W> type representing OSM ways
11042+ * @param <R> type representing OSM relations
11043+ * @param <D> The dataset type
11044+ */
11045+ class SelectionRemoveEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> extends AbstractSelectionEvent<O, N, W, R, D> {
11046+ private final Set<O> remove;
11047+ private final Set<O> current;
11048+
11049+ /**
11050+ * Create a {@link DataSelectionListener.SelectionRemoveEvent}
11051+ * @param source The source dataset
11052+ * @param old The old primitives that were previously selected. The caller needs to ensure that this set is not modified.
11053+ * @param toRemove The primitives to remove.
11054+ */
11055+ public SelectionRemoveEvent(D source, Set<O> old, Stream<O> toRemove) {
11056+ super(source, old);
11057+ this.remove = toRemove
11058+ .filter(old::contains)
11059+ .collect(Collectors.toCollection(LinkedHashSet::new));
11060+ if (this.remove.isEmpty()) {
11061+ this.current = this.getOldSelection();
11062+ } else {
11063+ HashSet<O> currentSet = new LinkedHashSet<>(old);
11064+ currentSet.removeAll(remove);
11065+ current = currentSet;
11066+ }
11067+ }
11068+
11069+ @Override
11070+ public Set<O> getSelection() {
11071+ return Collections.unmodifiableSet(current);
11072+ }
11073+
11074+ @Override
11075+ public Set<O> getRemoved() {
11076+ return Collections.unmodifiableSet(remove);
11077+ }
11078+
11079+ @Override
11080+ public Set<O> getAdded() {
11081+ return Collections.emptySet();
11082+ }
11083+
11084+ @Override
11085+ public String toString() {
11086+ return "SelectionRemoveEvent [remove=" + remove + ", current=" + current + ']';
11087+ }
11088+ }
11089+
11090+ /**
11091+ * Toggle the selected state of a primitive
11092+ * @author Michael Zangl
11093+ * @since xxx (generics)
11094+ * @param <O> the base type of OSM primitives
11095+ * @param <N> type representing OSM nodes
11096+ * @param <W> type representing OSM ways
11097+ * @param <R> type representing OSM relations
11098+ * @param <D> The dataset type
11099+ */
11100+ class SelectionToggleEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> extends AbstractSelectionEvent<O, N, W, R, D> {
11101+ private final Set<O> current;
11102+ private final Set<O> remove;
11103+ private final Set<O> add;
11104+
11105+ /**
11106+ * Create a {@link SelectionToggleEvent}
11107+ * @param source The source dataset
11108+ * @param old The old primitives that were previously selected. The caller needs to ensure that this set is not modified.
11109+ * @param toToggle The primitives to toggle.
11110+ */
11111+ public SelectionToggleEvent(D source, Set<O> old, Stream<O> toToggle) {
11112+ super(source, old);
11113+ HashSet<O> currentSet = new LinkedHashSet<>(old);
11114+ HashSet<O> removeSet = new LinkedHashSet<>();
11115+ HashSet<O> addSet = new LinkedHashSet<>();
11116+ toToggle.forEach(p -> {
11117+ if (currentSet.remove(p)) {
11118+ removeSet.add(p);
11119+ } else {
11120+ addSet.add(p);
11121+ currentSet.add(p);
11122+ }
11123+ });
11124+ this.current = Collections.unmodifiableSet(currentSet);
11125+ this.remove = Collections.unmodifiableSet(removeSet);
11126+ this.add = Collections.unmodifiableSet(addSet);
11127+ }
11128+
11129+ @Override
11130+ public Set<O> getSelection() {
11131+ return current;
11132+ }
11133+
11134+ @Override
11135+ public Set<O> getRemoved() {
11136+ return remove;
11137+ }
11138+
11139+ @Override
11140+ public Set<O> getAdded() {
11141+ return add;
11142+ }
11143+
11144+ @Override
11145+ public String toString() {
11146+ return "SelectionToggleEvent [current=" + current + ", remove=" + remove + ", add=" + add + ']';
11147+ }
11148+ }
11149+}
11150diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11151index 61eeb0279..229fdf40e 100644
11152--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11153+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11154@@ -6,13 +6,16 @@ import java.util.Arrays;
11155 import java.util.Collection;
11156 import java.util.Collections;
11157 import java.util.HashSet;
11158+import java.util.LinkedHashSet;
11159 import java.util.List;
11160 import java.util.Map;
11161 import java.util.Objects;
11162 import java.util.Optional;
11163+import java.util.Set;
11164 import java.util.concurrent.ConcurrentHashMap;
11165 import java.util.concurrent.locks.Lock;
11166 import java.util.concurrent.locks.ReentrantReadWriteLock;
11167+import java.util.function.Function;
11168 import java.util.function.Predicate;
11169 import java.util.function.Supplier;
11170 import java.util.stream.Collectors;
11171@@ -24,12 +27,17 @@ import org.openstreetmap.josm.data.DataSource;
11172 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
11173 import org.openstreetmap.josm.data.osm.BBox;
11174 import org.openstreetmap.josm.data.osm.DataSelectionListener;
11175+import org.openstreetmap.josm.data.osm.DataSet;
11176 import org.openstreetmap.josm.data.osm.DownloadPolicy;
11177 import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
11178+import org.openstreetmap.josm.data.osm.IPrimitive;
11179 import org.openstreetmap.josm.data.osm.OsmData;
11180+import org.openstreetmap.josm.data.osm.OsmPrimitive;
11181 import org.openstreetmap.josm.data.osm.PrimitiveId;
11182 import org.openstreetmap.josm.data.osm.UploadPolicy;
11183 import org.openstreetmap.josm.data.osm.WaySegment;
11184+import org.openstreetmap.josm.data.osm.event.IDataSelectionEventSource;
11185+import org.openstreetmap.josm.data.osm.event.IDataSelectionListener;
11186 import org.openstreetmap.josm.gui.mappaint.ElemStyles;
11187 import org.openstreetmap.josm.tools.ListenerList;
11188 import org.openstreetmap.josm.tools.Logging;
11189@@ -41,11 +49,10 @@ import org.openstreetmap.josm.tools.SubclassFilteredCollection;
11190 * @author Taylor Smock
11191 * @since xxx
11192 */
11193-public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation> {
11194+public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation>, IDataSelectionEventSource<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> {
11195 // Note: In Java 8, computeIfAbsent is blocking for both pre-existing and new values. In Java 9, it is only blocking
11196 // for new values (perf increase). See JDK-8161372 for more info.
11197 private final Map<Integer, VectorDataStore> dataStoreMap = new ConcurrentHashMap<>();
11198- private final Collection<PrimitiveId> selected = new HashSet<>();
11199 // Both of these listener lists are useless, since they expect OsmPrimitives at this time
11200 private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create();
11201 private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create();
11202@@ -55,6 +62,17 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11203 private String name;
11204 private short mappaintCacheIdx = 1;
11205
11206+ private final Object selectionLock = new Object();
11207+ /**
11208+ * The current selected primitives. This is always a unmodifiable set.
11209+ *
11210+ * The set should be ordered in the order in which the primitives have been added to the selection.
11211+ */
11212+ private Set<PrimitiveId> currentSelectedPrimitives = Collections.emptySet();
11213+
11214+ private final ListenerList<IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet>> listeners =
11215+ ListenerList.create();
11216+
11217 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
11218
11219 /**
11220@@ -298,7 +316,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11221 public Collection<VectorPrimitive> getAllSelected() {
11222 final Optional<VectorDataStore> dataStore = this.getBestZoomDataStore();
11223 return dataStore.map(vectorDataStore -> vectorDataStore.getAllPrimitives().stream()
11224- .filter(primitive -> this.selected.contains(primitive.getPrimitiveId()))
11225+ .filter(primitive -> this.currentSelectedPrimitives.contains(primitive.getPrimitiveId()))
11226 .collect(Collectors.toList())).orElse(Collections.emptyList());
11227 }
11228
11229@@ -334,12 +352,12 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11230
11231 @Override
11232 public boolean selectionEmpty() {
11233- return this.selected.isEmpty();
11234+ return this.currentSelectedPrimitives.isEmpty();
11235 }
11236
11237 @Override
11238 public boolean isSelected(VectorPrimitive osm) {
11239- return this.selected.contains(osm.getPrimitiveId());
11240+ return this.currentSelectedPrimitives.contains(osm.getPrimitiveId());
11241 }
11242
11243 @Override
11244@@ -353,13 +371,8 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11245 }
11246
11247 private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) {
11248- osm.forEach(primitiveId -> {
11249- if (this.selected.contains(primitiveId)) {
11250- this.selected.remove(primitiveId);
11251- } else {
11252- this.selected.add(primitiveId);
11253- }
11254- });
11255+ this.doSelectionChange(old -> new IDataSelectionListener.SelectionToggleEvent<>(this, old,
11256+ osm.map(this::getPrimitiveById).filter(Objects::nonNull)));
11257 }
11258
11259 @Override
11260@@ -373,8 +386,8 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11261 }
11262
11263 private void setSelectedImpl(Stream<? extends PrimitiveId> osm) {
11264- this.selected.clear();
11265- osm.forEach(this.selected::add);
11266+ this.doSelectionChange(old -> new IDataSelectionListener.SelectionReplaceEvent<>(this, old,
11267+ osm.map(this::getPrimitiveById).filter(Objects::nonNull)));
11268 }
11269
11270 @Override
11271@@ -388,7 +401,8 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11272 }
11273
11274 private void addSelectedImpl(Stream<? extends PrimitiveId> osm) {
11275- osm.forEach(this.selected::add);
11276+ this.doSelectionChange(old -> new IDataSelectionListener.SelectionAddEvent<>(this, old,
11277+ osm.map(this::getPrimitiveById).filter(Objects::nonNull)));
11278 }
11279
11280 @Override
11281@@ -403,11 +417,35 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11282
11283 @Override
11284 public void clearSelection() {
11285- this.clearSelectionImpl(new ArrayList<>(this.selected).stream());
11286+ this.clearSelectionImpl(new ArrayList<>(this.currentSelectedPrimitives).stream());
11287 }
11288
11289 private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) {
11290- osm.forEach(this.selected::remove);
11291+ this.doSelectionChange(old -> new IDataSelectionListener.SelectionRemoveEvent<>(this, old,
11292+ osm.map(this::getPrimitiveById).filter(Objects::nonNull)));
11293+ }
11294+
11295+ /**
11296+ * Do a selection change.
11297+ * <p>
11298+ * This is the only method that changes the current selection state.
11299+ * @param command A generator that generates the {@link DataSelectionListener.SelectionChangeEvent}
11300+ * for the given base set of currently selected primitives.
11301+ * @return true iff the command did change the selection.
11302+ */
11303+ private boolean doSelectionChange(final Function<Set<VectorPrimitive>,
11304+ IDataSelectionListener.SelectionChangeEvent<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet>> command) {
11305+ synchronized (this.selectionLock) {
11306+ IDataSelectionListener.SelectionChangeEvent<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> event =
11307+ command.apply(currentSelectedPrimitives.stream().map(this::getPrimitiveById).collect(Collectors.toSet()));
11308+ if (event.isNop()) {
11309+ return false;
11310+ }
11311+ this.currentSelectedPrimitives = event.getSelection().stream().map(IPrimitive::getPrimitiveId)
11312+ .collect(Collectors.toCollection(LinkedHashSet::new));
11313+ this.listeners.fireEvent(l -> l.selectionChanged(event));
11314+ return true;
11315+ }
11316 }
11317
11318 @Override
11319@@ -557,4 +595,20 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11320 this.dataStoreMap.values().forEach(dataStore -> dataStore.getAllPrimitives().parallelStream()
11321 .forEach(primitive -> primitive.setVisible(!temporaryList.contains(primitive.getLayer()))));
11322 }
11323+
11324+ @Override
11325+ public boolean addListener(IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
11326+ if (!this.listeners.containsListener(listener)) {
11327+ this.listeners.addListener(listener);
11328+ }
11329+ return this.listeners.containsListener(listener);
11330+ }
11331+
11332+ @Override
11333+ public boolean removeListener(IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
11334+ if (this.listeners.containsListener(listener)) {
11335+ this.listeners.removeListener(listener);
11336+ }
11337+ return this.listeners.containsListener(listener);
11338+ }
11339 }
11340--
11341GitLab
11342
11343
11344From 46085d1dadda30d4b690776b9ffb02932aa85c0a Mon Sep 17 00:00:00 2001
11345From: Taylor Smock <tsmock@fb.com>
11346Date: Tue, 27 Apr 2021 16:31:32 -0600
11347Subject: [PATCH 21/50] BBox: addPrimitive: Overload so that IPrimitives work
11348
11349Signed-off-by: Taylor Smock <tsmock@fb.com>
11350---
11351 src/org/openstreetmap/josm/data/osm/BBox.java | 10 ++++++++++
11352 1 file changed, 10 insertions(+)
11353
11354diff --git a/src/org/openstreetmap/josm/data/osm/BBox.java b/src/org/openstreetmap/josm/data/osm/BBox.java
11355index e16f984d1..7dcc79a41 100644
11356--- a/src/org/openstreetmap/josm/data/osm/BBox.java
11357+++ b/src/org/openstreetmap/josm/data/osm/BBox.java
11358@@ -174,6 +174,16 @@ public class BBox implements IBounds {
11359 * @param extraSpace the value to extend the primitives bbox. Unit is in LatLon degrees.
11360 */
11361 public void addPrimitive(OsmPrimitive primitive, double extraSpace) {
11362+ this.addPrimitive((IPrimitive) primitive, extraSpace);
11363+ }
11364+
11365+ /**
11366+ * Extends this bbox to include the bbox of the primitive extended by extraSpace.
11367+ * @param primitive an primitive
11368+ * @param extraSpace the value to extend the primitives bbox. Unit is in LatLon degrees.
11369+ * @since xxx
11370+ */
11371+ public void addPrimitive(IPrimitive primitive, double extraSpace) {
11372 IBounds primBbox = primitive.getBBox();
11373 add(primBbox.getMinLon() - extraSpace, primBbox.getMinLat() - extraSpace);
11374 add(primBbox.getMaxLon() + extraSpace, primBbox.getMaxLat() + extraSpace);
11375--
11376GitLab
11377
11378
11379From f898d4e21e97655892cea722693c27f0a8408931 Mon Sep 17 00:00:00 2001
11380From: Taylor Smock <tsmock@fb.com>
11381Date: Wed, 28 Apr 2021 12:18:03 -0600
11382Subject: [PATCH 22/50] Vector Data: Performance fix, fix an
11383 UnsupportedOperationException
11384
11385Signed-off-by: Taylor Smock <tsmock@fb.com>
11386---
11387 .../openstreetmap/josm/data/vector/DataStore.java | 12 +++++++-----
11388 .../josm/data/vector/VectorDataSet.java | 13 +++++++++----
11389 2 files changed, 16 insertions(+), 9 deletions(-)
11390
11391diff --git a/src/org/openstreetmap/josm/data/vector/DataStore.java b/src/org/openstreetmap/josm/data/vector/DataStore.java
11392index 86f1948d1..d3fb4ac59 100644
11393--- a/src/org/openstreetmap/josm/data/vector/DataStore.java
11394+++ b/src/org/openstreetmap/josm/data/vector/DataStore.java
11395@@ -49,8 +49,8 @@ class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R exte
11396 protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true);
11397 // TODO what happens when I use hashCode?
11398 protected final Set<Tile> addedTiles = Collections.synchronizedSet(new HashSet<>());
11399- protected final Map<PrimitiveId, O> primitivesMap = allPrimitives
11400- .foreignKey(new Storage.PrimitiveIdHash());
11401+ protected final Map<PrimitiveId, O> primitivesMap = Collections.synchronizedMap(allPrimitives
11402+ .foreignKey(new Storage.PrimitiveIdHash()));
11403 protected final Collection<DataSource> dataSources = new LinkedList<>();
11404 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
11405
11406@@ -70,10 +70,12 @@ class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R exte
11407 return this.allPrimitives;
11408 }
11409
11410+ /**
11411+ * Get the primitives map.
11412+ * @implNote The returned map is a {@link Collections#synchronizedMap}. Please synchronize on it.
11413+ * @return The Primitives map.
11414+ */
11415 public Map<PrimitiveId, O> getPrimitivesMap() {
11416- if (this.readWriteLock.isWriteLocked()) {
11417- return new HashMap<>(this.primitivesMap);
11418- }
11419 return this.primitivesMap;
11420 }
11421
11422diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11423index 229fdf40e..b262194ab 100644
11424--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11425+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11426@@ -34,6 +34,7 @@ import org.openstreetmap.josm.data.osm.IPrimitive;
11427 import org.openstreetmap.josm.data.osm.OsmData;
11428 import org.openstreetmap.josm.data.osm.OsmPrimitive;
11429 import org.openstreetmap.josm.data.osm.PrimitiveId;
11430+import org.openstreetmap.josm.data.osm.Storage;
11431 import org.openstreetmap.josm.data.osm.UploadPolicy;
11432 import org.openstreetmap.josm.data.osm.WaySegment;
11433 import org.openstreetmap.josm.data.osm.event.IDataSelectionEventSource;
11434@@ -314,10 +315,14 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11435
11436 @Override
11437 public Collection<VectorPrimitive> getAllSelected() {
11438- final Optional<VectorDataStore> dataStore = this.getBestZoomDataStore();
11439- return dataStore.map(vectorDataStore -> vectorDataStore.getAllPrimitives().stream()
11440- .filter(primitive -> this.currentSelectedPrimitives.contains(primitive.getPrimitiveId()))
11441- .collect(Collectors.toList())).orElse(Collections.emptyList());
11442+ final Map<PrimitiveId, VectorPrimitive> dataStore = this.getBestZoomDataStore().map(VectorDataStore::getPrimitivesMap).orElse(null);
11443+ if (dataStore != null) {
11444+ // The dataStore is a final variable from the VectorDataStore.
11445+ synchronized (dataStore) {
11446+ return this.currentSelectedPrimitives.stream().map(dataStore::get).collect(Collectors.toList());
11447+ }
11448+ }
11449+ return Collections.emptyList();
11450 }
11451
11452 /**
11453--
11454GitLab
11455
11456
11457From 3d0eb727137005aa60ff0bd48dd1acfc38453d57 Mon Sep 17 00:00:00 2001
11458From: Taylor Smock <tsmock@fb.com>
11459Date: Wed, 28 Apr 2021 13:08:57 -0600
11460Subject: [PATCH 23/50] VectorData: Add method to highlight primitives
11461
11462Signed-off-by: Taylor Smock <tsmock@fb.com>
11463---
11464 .../josm/data/vector/VectorDataSet.java | 23 ++++++++++++++++++-
11465 1 file changed, 22 insertions(+), 1 deletion(-)
11466
11467diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11468index b262194ab..7e82030eb 100644
11469--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11470+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11471@@ -101,6 +101,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11472 * The paint style for this layer
11473 */
11474 private ElemStyles styles;
11475+ private final Collection<PrimitiveId> highlighted = new HashSet<>();
11476
11477 @Override
11478 public Collection<DataSource> getDataSources() {
11479@@ -303,6 +304,26 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11480 // TODO? This requires a change to WaySegment so that it isn't Way/Node specific
11481 }
11482
11483+ /**
11484+ * Mark some primitives as highlighted
11485+ * @param primitives The primitives to highlight
11486+ * @apiNote This is *highly likely* to change, as the inherited methods are modified to accept primitives other than OSM primitives.
11487+ */
11488+ public void setHighlighted(Collection<PrimitiveId> primitives) {
11489+ this.highlighted.clear();
11490+ this.highlighted.addAll(primitives);
11491+ // The highlight event updates are very OSM specific, and require a DataSet.
11492+ this.highlightUpdateListenerListenerList.fireEvent(event -> event.highlightUpdated(null));
11493+ }
11494+
11495+ /**
11496+ * Get the highlighted objects
11497+ * @return The highlighted objects
11498+ */
11499+ public Collection<PrimitiveId> getHighlighted() {
11500+ return Collections.unmodifiableCollection(this.highlighted);
11501+ }
11502+
11503 @Override
11504 public void addHighlightUpdateListener(HighlightUpdateListener listener) {
11505 this.highlightUpdateListenerListenerList.addListener(listener);
11506@@ -319,7 +340,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11507 if (dataStore != null) {
11508 // The dataStore is a final variable from the VectorDataStore.
11509 synchronized (dataStore) {
11510- return this.currentSelectedPrimitives.stream().map(dataStore::get).collect(Collectors.toList());
11511+ return this.currentSelectedPrimitives.stream().map(dataStore::get).filter(Objects::nonNull).collect(Collectors.toList());
11512 }
11513 }
11514 return Collections.emptyList();
11515--
11516GitLab
11517
11518
11519From fccaf1e53603d221f18667995da72cb6f37ea12f Mon Sep 17 00:00:00 2001
11520From: Taylor Smock <tsmock@fb.com>
11521Date: Thu, 29 Apr 2021 11:32:34 -0600
11522Subject: [PATCH 24/50] VectorTiles: Rework to avoid locks preventing paint
11523
11524Signed-off-by: Taylor Smock <tsmock@fb.com>
11525---
11526 .../data/imagery/vectortile/mapbox/Layer.java | 10 +-
11527 .../imagery/vectortile/mapbox/MVTTile.java | 70 +++--
11528 src/org/openstreetmap/josm/data/osm/BBox.java | 5 +-
11529 .../osm/event/IDataSelectionEventSource.java | 5 +-
11530 .../osm/event/IDataSelectionListener.java | 35 ++-
11531 .../josm/data/vector/DataStore.java | 10 -
11532 .../josm/data/vector/VectorDataSet.java | 249 ++++++++++--------
11533 .../josm/data/vector/VectorDataStore.java | 82 ++----
11534 8 files changed, 240 insertions(+), 226 deletions(-)
11535
11536diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
11537index 1c496d55d..0a6bb073e 100644
11538--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
11539+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
11540@@ -16,6 +16,7 @@ import java.util.stream.Collectors;
11541
11542 import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
11543 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
11544+import org.openstreetmap.josm.tools.Destroyable;
11545 import org.openstreetmap.josm.tools.Logging;
11546
11547 /**
11548@@ -23,7 +24,7 @@ import org.openstreetmap.josm.tools.Logging;
11549 * @author Taylor Smock
11550 * @since xxx
11551 */
11552-public final class Layer {
11553+public final class Layer implements Destroyable {
11554 private static final class ValueFields<T> {
11555 static final ValueFields<String> STRING = new ValueFields<>(1, ProtoBufRecord::asString);
11556 static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtoBufRecord::asFloat);
11557@@ -224,6 +225,13 @@ public final class Layer {
11558 return this.version;
11559 }
11560
11561+ @Override
11562+ public void destroy() {
11563+ this.featureCollection.clear();
11564+ this.keyList.clear();
11565+ this.valueList.clear();
11566+ }
11567+
11568 @Override
11569 public boolean equals(Object other) {
11570 if (other instanceof Layer) {
11571diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
11572index 5d1d781dd..ab77c43f4 100644
11573--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
11574+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
11575@@ -1,33 +1,39 @@
11576 // License: GPL. For details, see LICENSE file.
11577 package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
11578
11579-import java.awt.image.BufferedImage;
11580-import java.io.IOException;
11581-import java.io.InputStream;
11582-import java.util.Collection;
11583-import java.util.HashSet;
11584-import java.util.List;
11585-import java.util.stream.Collectors;
11586-
11587 import org.openstreetmap.gui.jmapviewer.Tile;
11588+import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
11589 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
11590+import org.openstreetmap.josm.data.IQuadBucketType;
11591 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
11592+import org.openstreetmap.josm.data.osm.BBox;
11593 import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
11594 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
11595+import org.openstreetmap.josm.data.vector.VectorDataStore;
11596 import org.openstreetmap.josm.tools.ListenerList;
11597 import org.openstreetmap.josm.tools.Logging;
11598
11599+import java.awt.image.BufferedImage;
11600+import java.io.IOException;
11601+import java.io.InputStream;
11602+import java.util.Collection;
11603+import java.util.HashSet;
11604+import java.util.List;
11605+import java.util.stream.Collectors;
11606+
11607 /**
11608 * A class for MapBox Vector Tiles
11609 *
11610 * @author Taylor Smock
11611 * @since xxx
11612 */
11613-public class MVTTile extends Tile implements VectorTile {
11614+public class MVTTile extends Tile implements VectorTile, IQuadBucketType {
11615 private final ListenerList<TileListener> listenerList = ListenerList.create();
11616 private Collection<Layer> layers;
11617 private int extent = Layer.DEFAULT_EXTENT;
11618 static final BufferedImage CLEAR_LOADED = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR);
11619+ private BBox bbox;
11620+ private VectorDataStore vectorDataStore;
11621
11622 /**
11623 * Create a new Tile
11624@@ -47,25 +53,30 @@ public class MVTTile extends Tile implements VectorTile {
11625 ProtoBufParser parser = new ProtoBufParser(inputStream);
11626 Collection<ProtoBufRecord> protoBufRecords = parser.allRecords();
11627 this.layers = new HashSet<>();
11628- this.layers = protoBufRecords.stream().map(record -> {
11629+ this.layers = protoBufRecords.stream().map(protoBufRecord -> {
11630 Layer mvtLayer = null;
11631- if (record.getField() == Layer.LAYER_FIELD) {
11632- try (ProtoBufParser tParser = new ProtoBufParser(record.getBytes())) {
11633+ if (protoBufRecord.getField() == Layer.LAYER_FIELD) {
11634+ try (ProtoBufParser tParser = new ProtoBufParser(protoBufRecord.getBytes())) {
11635 mvtLayer = new Layer(tParser.allRecords());
11636 } catch (IOException e) {
11637 Logging.error(e);
11638 } finally {
11639 // Cleanup bytes
11640- record.close();
11641+ protoBufRecord.close();
11642 }
11643 }
11644 return mvtLayer;
11645 }).collect(Collectors.toCollection(HashSet::new));
11646 this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT);
11647- this.finishLoading();
11648- this.listenerList.fireEvent(event -> event.finishedLoading(this));
11649- // Ensure that we don't keep the loading image around
11650- this.image = CLEAR_LOADED;
11651+ if (this.getData() != null) {
11652+ this.finishLoading();
11653+ this.listenerList.fireEvent(event -> event.finishedLoading(this));
11654+ // Ensure that we don't keep the loading image around
11655+ this.image = CLEAR_LOADED;
11656+ // Cleanup as much as possible -- layers will still exist, but only base information (like name, extent) will remain.
11657+ // Called last just in case the listeners need the layers.
11658+ this.layers.forEach(Layer::destroy);
11659+ }
11660 }
11661 }
11662
11663@@ -89,6 +100,31 @@ public class MVTTile extends Tile implements VectorTile {
11664 this.listenerList.addWeakListener(listener);
11665 }
11666
11667+ @Override
11668+ public BBox getBBox() {
11669+ if (this.bbox == null) {
11670+ final ICoordinate upperLeft = this.getTileSource().tileXYToLatLon(this);
11671+ final ICoordinate lowerRight = this.getTileSource()
11672+ .tileXYToLatLon(this.getXtile() + 1, this.getYtile() + 1, this.getZoom());
11673+ BBox newBBox = new BBox(upperLeft.getLon(), upperLeft.getLat(), lowerRight.getLon(), lowerRight.getLat());
11674+ this.bbox = newBBox.toImmutable();
11675+ }
11676+ return this.bbox;
11677+ }
11678+
11679+ /**
11680+ * Get the datastore for this tile
11681+ * @return The data
11682+ */
11683+ public VectorDataStore getData() {
11684+ if (this.vectorDataStore == null) {
11685+ VectorDataStore newDataStore = new VectorDataStore();
11686+ newDataStore.addDataTile(this);
11687+ this.vectorDataStore = newDataStore;
11688+ }
11689+ return this.vectorDataStore;
11690+ }
11691+
11692 /**
11693 * A class that can be notified that a tile has finished loading
11694 *
11695diff --git a/src/org/openstreetmap/josm/data/osm/BBox.java b/src/org/openstreetmap/josm/data/osm/BBox.java
11696index 7dcc79a41..106037ec9 100644
11697--- a/src/org/openstreetmap/josm/data/osm/BBox.java
11698+++ b/src/org/openstreetmap/josm/data/osm/BBox.java
11699@@ -465,8 +465,9 @@ public class BBox implements IBounds {
11700 /**
11701 * Returns an immutable version of this bbox, i.e., modifying calls throw an {@link UnsupportedOperationException}.
11702 * @return an immutable version of this bbox
11703+ * @since xxx (interface)
11704 */
11705- BBox toImmutable() {
11706+ public BBox toImmutable() {
11707 return new Immutable(this);
11708 }
11709
11710@@ -482,7 +483,7 @@ public class BBox implements IBounds {
11711 }
11712
11713 @Override
11714- BBox toImmutable() {
11715+ public BBox toImmutable() {
11716 return this;
11717 }
11718 }
11719diff --git a/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java
11720index e5fc0ea19..4f1d75d18 100644
11721--- a/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java
11722+++ b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java
11723@@ -10,14 +10,15 @@ import org.openstreetmap.josm.data.osm.OsmData;
11724 /**
11725 * This interface indicates that the class can fire {@link IDataSelectionListener}.
11726 * @author Taylor Smock, Michael Zangl (original code)
11727- * @since xxx
11728 * @param <O> the base type of OSM primitives
11729 * @param <N> type representing OSM nodes
11730 * @param <W> type representing OSM ways
11731 * @param <R> type representing OSM relations
11732 * @param <D> The dataset type
11733+ * @since xxx
11734 */
11735-public interface IDataSelectionEventSource<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> {
11736+public interface IDataSelectionEventSource<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>,
11737+ D extends OsmData<O, N, W, R>> {
11738 /**
11739 * Add a listener
11740 * @param listener The listener to add
11741diff --git a/src/org/openstreetmap/josm/data/osm/event/IDataSelectionListener.java b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionListener.java
11742index 5a95aa1c9..7550e8dbd 100644
11743--- a/src/org/openstreetmap/josm/data/osm/event/IDataSelectionListener.java
11744+++ b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionListener.java
11745@@ -20,15 +20,16 @@ import java.util.stream.Stream;
11746 /**
11747 * This interface is the same as {@link DataSelectionListener}, except it isn't {@link OsmPrimitive} specific.
11748 * @author Taylor Smock, Michael Zangl (original code)
11749- * @since xxx
11750 * @param <O> the base type of OSM primitives
11751 * @param <N> type representing OSM nodes
11752 * @param <W> type representing OSM ways
11753 * @param <R> type representing OSM relations
11754 * @param <D> The dataset type
11755+ * @since xxx
11756 */
11757 @FunctionalInterface
11758-public interface IDataSelectionListener<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> {
11759+public interface IDataSelectionListener<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>,
11760+ D extends OsmData<O, N, W, R>> {
11761 /**
11762 * Called whenever the selection is changed.
11763 *
11764@@ -41,14 +42,15 @@ public interface IDataSelectionListener<O extends IPrimitive, N extends INode, W
11765 /**
11766 * The event that is fired when the selection changed.
11767 * @author Michael Zangl
11768- * @since xxx generics
11769 * @param <O> the base type of OSM primitives
11770 * @param <N> type representing OSM nodes
11771 * @param <W> type representing OSM ways
11772 * @param <R> type representing OSM relations
11773 * @param <D> The dataset type
11774+ * @since xxx generics
11775 */
11776- interface SelectionChangeEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> {
11777+ interface SelectionChangeEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>,
11778+ D extends OsmData<O, N, W, R>> {
11779 /**
11780 * Gets the previous selection
11781 * <p>
11782@@ -105,14 +107,15 @@ public interface IDataSelectionListener<O extends IPrimitive, N extends INode, W
11783 /**
11784 * The base class for selection events
11785 * @author Michael Zangl
11786- * @since 12048, xxx (generics)
11787 * @param <O> the base type of OSM primitives
11788 * @param <N> type representing OSM nodes
11789 * @param <W> type representing OSM ways
11790 * @param <R> type representing OSM relations
11791 * @param <D> The dataset type
11792+ * @since 12048, xxx (generics)
11793 */
11794- abstract class AbstractSelectionEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> implements SelectionChangeEvent<O, N, W, R, D> {
11795+ abstract class AbstractSelectionEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>,
11796+ D extends OsmData<O, N, W, R>> implements SelectionChangeEvent<O, N, W, R, D> {
11797 private final D source;
11798 private final Set<O> old;
11799
11800@@ -137,14 +140,15 @@ public interface IDataSelectionListener<O extends IPrimitive, N extends INode, W
11801 /**
11802 * The selection is replaced by a new selection
11803 * @author Michael Zangl
11804- * @since xxx (generics)
11805 * @param <O> the base type of OSM primitives
11806 * @param <N> type representing OSM nodes
11807 * @param <W> type representing OSM ways
11808 * @param <R> type representing OSM relations
11809 * @param <D> The dataset type
11810+ * @since xxx (generics)
11811 */
11812- class SelectionReplaceEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> extends AbstractSelectionEvent<O, N, W, R, D> {
11813+ class SelectionReplaceEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>>
11814+ extends AbstractSelectionEvent<O, N, W, R, D> {
11815 private final Set<O> current;
11816 private Set<O> removed;
11817 private Set<O> added;
11818@@ -193,14 +197,15 @@ public interface IDataSelectionListener<O extends IPrimitive, N extends INode, W
11819 /**
11820 * Primitives are added to the selection
11821 * @author Michael Zangl
11822- * @since xxx (generics)
11823 * @param <O> the base type of OSM primitives
11824 * @param <N> type representing OSM nodes
11825 * @param <W> type representing OSM ways
11826 * @param <R> type representing OSM relations
11827 * @param <D> The dataset type
11828+ * @since xxx (generics)
11829 */
11830- class SelectionAddEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> extends AbstractSelectionEvent<O, N, W, R, D> {
11831+ class SelectionAddEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>>
11832+ extends AbstractSelectionEvent<O, N, W, R, D> {
11833 private final Set<O> add;
11834 private final Set<O> current;
11835
11836@@ -247,14 +252,15 @@ public interface IDataSelectionListener<O extends IPrimitive, N extends INode, W
11837 /**
11838 * Primitives are removed from the selection
11839 * @author Michael Zangl
11840- * @since 12048, xxx (generics)
11841 * @param <O> the base type of OSM primitives
11842 * @param <N> type representing OSM nodes
11843 * @param <W> type representing OSM ways
11844 * @param <R> type representing OSM relations
11845 * @param <D> The dataset type
11846+ * @since 12048, xxx (generics)
11847 */
11848- class SelectionRemoveEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> extends AbstractSelectionEvent<O, N, W, R, D> {
11849+ class SelectionRemoveEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>>
11850+ extends AbstractSelectionEvent<O, N, W, R, D> {
11851 private final Set<O> remove;
11852 private final Set<O> current;
11853
11854@@ -302,14 +308,15 @@ public interface IDataSelectionListener<O extends IPrimitive, N extends INode, W
11855 /**
11856 * Toggle the selected state of a primitive
11857 * @author Michael Zangl
11858- * @since xxx (generics)
11859 * @param <O> the base type of OSM primitives
11860 * @param <N> type representing OSM nodes
11861 * @param <W> type representing OSM ways
11862 * @param <R> type representing OSM relations
11863 * @param <D> The dataset type
11864+ * @since xxx (generics)
11865 */
11866- class SelectionToggleEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>> extends AbstractSelectionEvent<O, N, W, R, D> {
11867+ class SelectionToggleEvent<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>, D extends OsmData<O, N, W, R>>
11868+ extends AbstractSelectionEvent<O, N, W, R, D> {
11869 private final Set<O> current;
11870 private final Set<O> remove;
11871 private final Set<O> add;
11872diff --git a/src/org/openstreetmap/josm/data/vector/DataStore.java b/src/org/openstreetmap/josm/data/vector/DataStore.java
11873index d3fb4ac59..9f942a0d6 100644
11874--- a/src/org/openstreetmap/josm/data/vector/DataStore.java
11875+++ b/src/org/openstreetmap/josm/data/vector/DataStore.java
11876@@ -3,7 +3,6 @@ package org.openstreetmap.josm.data.vector;
11877
11878 import java.util.Collection;
11879 import java.util.Collections;
11880-import java.util.HashMap;
11881 import java.util.HashSet;
11882 import java.util.LinkedList;
11883 import java.util.Map;
11884@@ -44,7 +43,6 @@ class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R exte
11885 }
11886 }
11887
11888- protected final int zoom;
11889 protected final LocalQuadBucketPrimitiveStore<N, W, R> store = new LocalQuadBucketPrimitiveStore<>();
11890 protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true);
11891 // TODO what happens when I use hashCode?
11892@@ -54,14 +52,6 @@ class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R exte
11893 protected final Collection<DataSource> dataSources = new LinkedList<>();
11894 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
11895
11896- DataStore(int zoom) {
11897- this.zoom = zoom;
11898- }
11899-
11900- public int getZoom() {
11901- return this.zoom;
11902- }
11903-
11904 public QuadBucketPrimitiveStore<N, W, R> getStore() {
11905 return this.store;
11906 }
11907diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11908index 7e82030eb..55366d555 100644
11909--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11910+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
11911@@ -1,6 +1,25 @@
11912 // License: GPL. For details, see LICENSE file.
11913 package org.openstreetmap.josm.data.vector;
11914
11915+import org.openstreetmap.josm.data.DataSource;
11916+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
11917+import org.openstreetmap.josm.data.osm.BBox;
11918+import org.openstreetmap.josm.data.osm.DataSelectionListener;
11919+import org.openstreetmap.josm.data.osm.DownloadPolicy;
11920+import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
11921+import org.openstreetmap.josm.data.osm.IPrimitive;
11922+import org.openstreetmap.josm.data.osm.OsmData;
11923+import org.openstreetmap.josm.data.osm.PrimitiveId;
11924+import org.openstreetmap.josm.data.osm.Storage;
11925+import org.openstreetmap.josm.data.osm.UploadPolicy;
11926+import org.openstreetmap.josm.data.osm.WaySegment;
11927+import org.openstreetmap.josm.data.osm.event.IDataSelectionEventSource;
11928+import org.openstreetmap.josm.data.osm.event.IDataSelectionListener;
11929+import org.openstreetmap.josm.gui.mappaint.ElemStyles;
11930+import org.openstreetmap.josm.tools.ListenerList;
11931+import org.openstreetmap.josm.tools.Logging;
11932+import org.openstreetmap.josm.tools.SubclassFilteredCollection;
11933+
11934 import java.util.ArrayList;
11935 import java.util.Arrays;
11936 import java.util.Collection;
11937@@ -19,46 +38,22 @@ import java.util.function.Function;
11938 import java.util.function.Predicate;
11939 import java.util.function.Supplier;
11940 import java.util.stream.Collectors;
11941-import java.util.stream.IntStream;
11942 import java.util.stream.Stream;
11943
11944-import org.openstreetmap.gui.jmapviewer.Tile;
11945-import org.openstreetmap.josm.data.DataSource;
11946-import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
11947-import org.openstreetmap.josm.data.osm.BBox;
11948-import org.openstreetmap.josm.data.osm.DataSelectionListener;
11949-import org.openstreetmap.josm.data.osm.DataSet;
11950-import org.openstreetmap.josm.data.osm.DownloadPolicy;
11951-import org.openstreetmap.josm.data.osm.HighlightUpdateListener;
11952-import org.openstreetmap.josm.data.osm.IPrimitive;
11953-import org.openstreetmap.josm.data.osm.OsmData;
11954-import org.openstreetmap.josm.data.osm.OsmPrimitive;
11955-import org.openstreetmap.josm.data.osm.PrimitiveId;
11956-import org.openstreetmap.josm.data.osm.Storage;
11957-import org.openstreetmap.josm.data.osm.UploadPolicy;
11958-import org.openstreetmap.josm.data.osm.WaySegment;
11959-import org.openstreetmap.josm.data.osm.event.IDataSelectionEventSource;
11960-import org.openstreetmap.josm.data.osm.event.IDataSelectionListener;
11961-import org.openstreetmap.josm.gui.mappaint.ElemStyles;
11962-import org.openstreetmap.josm.tools.ListenerList;
11963-import org.openstreetmap.josm.tools.Logging;
11964-import org.openstreetmap.josm.tools.SubclassFilteredCollection;
11965-
11966 /**
11967 * A data class for Vector Data
11968 *
11969 * @author Taylor Smock
11970 * @since xxx
11971 */
11972-public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation>, IDataSelectionEventSource<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> {
11973+public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation>,
11974+ IDataSelectionEventSource<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> {
11975 // Note: In Java 8, computeIfAbsent is blocking for both pre-existing and new values. In Java 9, it is only blocking
11976 // for new values (perf increase). See JDK-8161372 for more info.
11977- private final Map<Integer, VectorDataStore> dataStoreMap = new ConcurrentHashMap<>();
11978+ private final Map<Integer, Storage<MVTTile>> dataStoreMap = new ConcurrentHashMap<>();
11979 // Both of these listener lists are useless, since they expect OsmPrimitives at this time
11980 private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create();
11981 private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create();
11982- private static final String[] NO_INVISIBLE_LAYERS = new String[0];
11983- private String[] invisibleLayers = NO_INVISIBLE_LAYERS;
11984 private boolean lock = true;
11985 private String name;
11986 private short mappaintCacheIdx = 1;
11987@@ -105,20 +100,8 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
11988
11989 @Override
11990 public Collection<DataSource> getDataSources() {
11991- final int currentZoom = this.zoom;
11992- final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
11993- return dataStore.getDataSources();
11994- }
11995-
11996- /**
11997- * Add a data source
11998- *
11999- * @param currentZoom the zoom
12000- * @param dataSource The datasource to add at the zoom level
12001- */
12002- public void addDataSource(int currentZoom, DataSource dataSource) {
12003- final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
12004- dataStore.addDataSource(dataSource);
12005+ // TODO
12006+ return Collections.emptyList();
12007 }
12008
12009 @Override
12010@@ -153,23 +136,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12011
12012 @Override
12013 public void addPrimitive(VectorPrimitive primitive) {
12014- primitive.setDataSet(this);
12015- final int currentZoom = this.zoom;
12016- final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
12017- tryWrite(dataStore, () -> dataStore.addPrimitive(primitive));
12018- }
12019-
12020- /**
12021- * Remove a primitive from this dataset
12022- *
12023- * @param primitive The primitive to remove
12024- */
12025- protected void removePrimitive(VectorPrimitive primitive) {
12026- if (primitive.getDataSet() == this) {
12027- primitive.setDataSet(null);
12028- this.dataStoreMap.values()
12029- .forEach(vectorDataStore -> tryWrite(vectorDataStore, () -> vectorDataStore.removePrimitive(primitive)));
12030- }
12031+ throw new UnsupportedOperationException("Custom vector primitives are not currently supported");
12032 }
12033
12034 @Override
12035@@ -181,59 +148,108 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12036
12037 @Override
12038 public List<VectorNode> searchNodes(BBox bbox) {
12039- return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchNodes(bbox))
12040- .orElseGet(Collections::emptyList);
12041+ return tryRead(this.readWriteLock, () -> {
12042+ final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12043+ if (dataStore != null) {
12044+ return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
12045+ .flatMap(store -> store.searchNodes(bbox).stream()).collect(Collectors.toList());
12046+ }
12047+ return null;
12048+ }).orElseGet(Collections::emptyList);
12049 }
12050
12051 @Override
12052 public boolean containsNode(VectorNode vectorNode) {
12053- return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsNode(vectorNode)).orElse(false);
12054+ return tryRead(this.readWriteLock, () -> {
12055+ final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12056+ return dataStore != null &&
12057+ dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
12058+ .anyMatch(store -> store.containsNode(vectorNode));
12059+ }).orElse(Boolean.FALSE);
12060 }
12061
12062 @Override
12063 public List<VectorWay> searchWays(BBox bbox) {
12064- return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchWays(bbox))
12065- .orElseGet(Collections::emptyList);
12066+ return tryRead(this.readWriteLock, () -> {
12067+ final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12068+ if (dataStore != null) {
12069+ return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
12070+ .flatMap(store -> store.searchWays(bbox).stream()).collect(Collectors.toList());
12071+ }
12072+ return null;
12073+ }).orElseGet(Collections::emptyList);
12074 }
12075
12076 @Override
12077 public boolean containsWay(VectorWay vectorWay) {
12078- return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsWay(vectorWay)).orElse(false);
12079+ return tryRead(this.readWriteLock, () -> {
12080+ final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12081+ return dataStore != null &&
12082+ dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
12083+ .anyMatch(store -> store.containsWay(vectorWay));
12084+ }).orElse(Boolean.FALSE);
12085 }
12086
12087 @Override
12088 public List<VectorRelation> searchRelations(BBox bbox) {
12089- return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchRelations(bbox))
12090- .orElseGet(Collections::emptyList);
12091+ return tryRead(this.readWriteLock, () -> {
12092+ final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12093+ if (dataStore != null) {
12094+ return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
12095+ .flatMap(store -> store.searchRelations(bbox).stream()).collect(Collectors.toList());
12096+ }
12097+ return null;
12098+ }).orElseGet(Collections::emptyList);
12099 }
12100
12101 @Override
12102 public boolean containsRelation(VectorRelation vectorRelation) {
12103- return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsRelation(vectorRelation)).orElse(false);
12104+ return tryRead(this.readWriteLock, () -> {
12105+ final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12106+ return dataStore != null &&
12107+ dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
12108+ .anyMatch(store -> store.containsRelation(vectorRelation));
12109+ }).orElse(Boolean.FALSE);
12110 }
12111
12112+ /**
12113+ * Get a primitive for an id
12114+ * @param primitiveId type and uniqueId of the primitive. Might be &lt; 0 for newly created primitives
12115+ * @return The primitive for the id. Please note that since this is vector data, there may be more primitives with this id.
12116+ * Please use {@link #getPrimitivesById(PrimitiveId...)} to get all primitives for that {@link PrimitiveId}.
12117+ */
12118 @Override
12119 public VectorPrimitive getPrimitiveById(PrimitiveId primitiveId) {
12120- return this.getBestZoomDataStore().map(VectorDataStore::getPrimitivesMap).map(m -> m .get(primitiveId)).orElse(null);
12121+ return this.getPrimitivesById(primitiveId).findFirst().orElse(null);
12122+ }
12123+
12124+ /**
12125+ * Get all primitives for ids
12126+ * @param primitiveIds The ids to search for
12127+ * @return The primitives for the ids (note: as this is vector data, a {@link PrimitiveId} may have multiple associated primitives)
12128+ */
12129+ public Stream<VectorPrimitive> getPrimitivesById(PrimitiveId... primitiveIds) {
12130+ final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12131+ if (dataStore != null) {
12132+ return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getPrimitivesMap)
12133+ .flatMap(m -> Stream.of(primitiveIds).map(m::get));
12134+ }
12135+ return Stream.empty();
12136 }
12137
12138- // The last return statement is "unchecked", even though it is literally the same as the previous return, except
12139- // as an optional.
12140- @SuppressWarnings("unchecked")
12141 @Override
12142 public <T extends VectorPrimitive> Collection<T> getPrimitives(
12143 Predicate<? super VectorPrimitive> predicate) {
12144- final VectorDataStore dataStore = this.getBestZoomDataStore().orElse(null);
12145- if (dataStore == null) {
12146- return Collections.emptyList();
12147- }
12148+ return tryRead(this.readWriteLock, () -> {
12149+ final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12150+ if (dataStore == null) {
12151+ return null;
12152+ }
12153
12154- if (dataStore.getReadWriteLock().isWriteLocked()) {
12155- return new SubclassFilteredCollection<>(new HashSet<>(dataStore.getAllPrimitives()), predicate);
12156- }
12157- return (Collection<T>) tryRead(dataStore, () -> new SubclassFilteredCollection<>(dataStore.getAllPrimitives(), predicate))
12158- // Throw an NPE if we don't have a collection (this should never happen, so if it does, _something_ is wrong)
12159- .orElseThrow(NullPointerException::new);
12160+ // This cast is needed (otherwise, Collections.emptyList doesn't compile)
12161+ return (Collection<T>) new SubclassFilteredCollection<>(dataStore.stream().map(MVTTile::getData)
12162+ .map(VectorDataStore::getAllPrimitives).flatMap(Collection::stream).distinct().collect(Collectors.toList()), predicate);
12163+ }).orElseGet(Collections::emptyList);
12164 }
12165
12166 @Override
12167@@ -278,8 +294,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12168 */
12169 @Override
12170 public Lock getReadLock() {
12171- return getBestZoomDataStore().map(VectorDataStore::getReadWriteLock).map(ReentrantReadWriteLock::readLock)
12172- .orElse(this.readWriteLock.readLock());
12173+ return this.readWriteLock.readLock();
12174 }
12175
12176 @Override
12177@@ -336,21 +351,28 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12178
12179 @Override
12180 public Collection<VectorPrimitive> getAllSelected() {
12181- final Map<PrimitiveId, VectorPrimitive> dataStore = this.getBestZoomDataStore().map(VectorDataStore::getPrimitivesMap).orElse(null);
12182- if (dataStore != null) {
12183- // The dataStore is a final variable from the VectorDataStore.
12184- synchronized (dataStore) {
12185- return this.currentSelectedPrimitives.stream().map(dataStore::get).filter(Objects::nonNull).collect(Collectors.toList());
12186+ return tryRead(this.readWriteLock, () -> {
12187+ final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12188+ if (dataStore != null) {
12189+ // The dataStore is what we don't want to concurrently modify
12190+ synchronized (dataStore) {
12191+ return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getPrimitivesMap).flatMap(dataMap -> {
12192+ // Synchronize on dataMap to avoid concurrent modification errors
12193+ synchronized (dataMap) {
12194+ return this.currentSelectedPrimitives.stream().map(dataMap::get).filter(Objects::nonNull);
12195+ }
12196+ }).collect(Collectors.toList());
12197+ }
12198 }
12199- }
12200- return Collections.emptyList();
12201+ return null;
12202+ }).orElseGet(Collections::emptyList);
12203 }
12204
12205 /**
12206 * Get the best zoom datastore
12207 * @return A datastore with data, or {@code null} if no good datastore exists.
12208 */
12209- private Optional<VectorDataStore> getBestZoomDataStore() {
12210+ private Optional<Storage<MVTTile>> getBestZoomDataStore() {
12211 final int currentZoom = this.zoom;
12212 if (this.dataStoreMap.containsKey(currentZoom)) {
12213 return Optional.of(this.dataStoreMap.get(currentZoom));
12214@@ -519,11 +541,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12215 nearestZoom[2] = keys[index + 1];
12216 }
12217
12218- nearestZoom[3] = this.getBestZoomDataStore().map(VectorDataStore::getZoom).orElse(-1);
12219- IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key))
12220- .mapToObj(this.dataStoreMap::get).forEach(VectorDataStore::destroy);
12221- IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key))
12222- .forEach(this.dataStoreMap::remove);
12223+ // TODO cleanup zooms for memory
12224 }
12225 }
12226
12227@@ -534,13 +552,15 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12228 /**
12229 * Add tile data to this dataset
12230 * @param tile The tile to add
12231- * @param <T> The tile type
12232 */
12233- public <T extends Tile & VectorTile> void addTileData(T tile) {
12234- final int currentZoom = tile.getZoom();
12235- // computeIfAbsent should be thread safe (ConcurrentHashMap indicates it is, anyway)
12236- final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom));
12237- dataStore.addTile(tile, this.invisibleLayers);
12238+ public void addTileData(MVTTile tile) {
12239+ tryWrite(this.readWriteLock, () -> {
12240+ final int currentZoom = tile.getZoom();
12241+ // computeIfAbsent should be thread safe (ConcurrentHashMap indicates it is, anyway)
12242+ final Storage<MVTTile> dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new Storage<>());
12243+ tile.getData().getAllPrimitives().forEach(primitive -> primitive.setDataSet(this));
12244+ dataStore.add(tile);
12245+ });
12246 }
12247
12248 /**
12249@@ -550,15 +570,15 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12250 * @param <T> The return type
12251 * @return The optional return
12252 */
12253- private static <T> Optional<T> tryRead(VectorDataStore dataStore, Supplier<T> supplier) {
12254+ private static <T> Optional<T> tryRead(ReentrantReadWriteLock lock, Supplier<T> supplier) {
12255 try {
12256- dataStore.getReadWriteLock().readLock().lockInterruptibly();
12257+ lock.readLock().lockInterruptibly();
12258 return Optional.ofNullable(supplier.get());
12259 } catch (InterruptedException e) {
12260 Logging.error(e);
12261 Thread.currentThread().interrupt();
12262 } finally {
12263- dataStore.getReadWriteLock().readLock().unlock();
12264+ lock.readLock().unlock();
12265 }
12266 return Optional.empty();
12267 }
12268@@ -568,16 +588,16 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12269 *
12270 * @param runnable The writing function
12271 */
12272- private static void tryWrite(VectorDataStore dataStore, Runnable runnable) {
12273+ private static void tryWrite(ReentrantReadWriteLock lock, Runnable runnable) {
12274 try {
12275- dataStore.getReadWriteLock().writeLock().lockInterruptibly();
12276+ lock.writeLock().lockInterruptibly();
12277 runnable.run();
12278 } catch (InterruptedException e) {
12279 Logging.error(e);
12280 Thread.currentThread().interrupt();
12281 } finally {
12282- if (dataStore.getReadWriteLock().isWriteLockedByCurrentThread()) {
12283- dataStore.getReadWriteLock().writeLock().unlock();
12284+ if (lock.isWriteLockedByCurrentThread()) {
12285+ lock.writeLock().unlock();
12286 }
12287 }
12288 }
12289@@ -610,16 +630,11 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12290 * @param invisibleLayers The layer to not show
12291 */
12292 public void setInvisibleLayers(Collection<String> invisibleLayers) {
12293- if (invisibleLayers == null || invisibleLayers.isEmpty()
12294- || invisibleLayers.stream().filter(Objects::nonNull).allMatch(String::isEmpty)) {
12295- this.invisibleLayers = NO_INVISIBLE_LAYERS;
12296- return;
12297- }
12298 String[] currentInvisibleLayers = invisibleLayers.stream().filter(Objects::nonNull).toArray(String[]::new);
12299- this.invisibleLayers = currentInvisibleLayers;
12300 List<String> temporaryList = Arrays.asList(currentInvisibleLayers);
12301- this.dataStoreMap.values().forEach(dataStore -> dataStore.getAllPrimitives().parallelStream()
12302- .forEach(primitive -> primitive.setVisible(!temporaryList.contains(primitive.getLayer()))));
12303+ this.dataStoreMap.values().stream().flatMap(Collection::stream).map(MVTTile::getData)
12304+ .forEach(dataStore -> dataStore.getAllPrimitives().parallelStream()
12305+ .forEach(primitive -> primitive.setVisible(!temporaryList.contains(primitive.getLayer()))));
12306 }
12307
12308 @Override
12309diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
12310index 7e48e4313..dceef3b8e 100644
12311--- a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
12312+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
12313@@ -4,6 +4,7 @@ package org.openstreetmap.josm.data.vector;
12314 import org.openstreetmap.gui.jmapviewer.Coordinate;
12315 import org.openstreetmap.gui.jmapviewer.Tile;
12316 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
12317+import org.openstreetmap.josm.data.IQuadBucketType;
12318 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
12319 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
12320 import org.openstreetmap.josm.data.osm.BBox;
12321@@ -25,12 +26,10 @@ import java.awt.geom.Ellipse2D;
12322 import java.awt.geom.Path2D;
12323 import java.awt.geom.PathIterator;
12324 import java.util.ArrayList;
12325-import java.util.Arrays;
12326 import java.util.Collection;
12327 import java.util.Collections;
12328 import java.util.List;
12329 import java.util.Objects;
12330-import java.util.Optional;
12331 import java.util.stream.Collectors;
12332
12333 /**
12334@@ -38,20 +37,12 @@ import java.util.stream.Collectors;
12335 * @author Taylor Smock
12336 * @since xxx
12337 */
12338-class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> implements Destroyable {
12339- private static final String[] EMPTY_STRING_ARRAY = new String[0];
12340+public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> implements Destroyable {
12341 private static final String JOSM_MERGE_TYPE_KEY = "josm_merge_type";
12342 private static final String ORIGINAL_ID = "original_id";
12343- private final VectorDataSet dataSet;
12344-
12345- VectorDataStore(VectorDataSet dataSet, int zoom) {
12346- super(zoom);
12347- this.dataSet = dataSet;
12348- }
12349
12350 @Override
12351 protected void addPrimitive(VectorPrimitive primitive) {
12352- primitive.setDataSet(this.dataSet);
12353 // The field is uint64, so we can use negative numbers to indicate that it is a "generated" object (e.g., nodes for ways)
12354 if (primitive.getUniqueId() == 0) {
12355 final UniqueIdGenerator generator = primitive.getIdGenerator();
12356@@ -92,7 +83,6 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
12357 temporaryRelation.addRelationMember(new VectorRelationMember("", alreadyAdded));
12358 }
12359 temporaryRelation.addRelationMember(new VectorRelationMember("", primitive));
12360- temporaryRelation.setDataSet(this.dataSet);
12361 super.addPrimitive(primitive);
12362 super.addPrimitive(temporaryRelation);
12363 }
12364@@ -161,13 +151,21 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
12365
12366 private synchronized <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer,
12367 Collection<VectorPrimitive> featureObjects, int x, int y) {
12368- final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile);
12369+ final BBox tileBbox;
12370+ if (tile instanceof IQuadBucketType) {
12371+ tileBbox = ((IQuadBucketType) tile).getBBox();
12372+ } else {
12373+ final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile);
12374+ final ICoordinate lowerRight = tile.getTileSource()
12375+ .tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
12376+
12377+ tileBbox = new BBox(upperLeft.getLon(), upperLeft.getLat(), lowerRight.getLon(), lowerRight.getLat());
12378+ }
12379 final int layerExtent = layer.getExtent();
12380- final ICoordinate lowerRight = tile.getTileSource()
12381- .tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());
12382 final ICoordinate coords = new Coordinate(
12383- upperLeft.getLat() - (upperLeft.getLat() - lowerRight.getLat()) * y / layerExtent,
12384- upperLeft.getLon() + (lowerRight.getLon() - upperLeft.getLon()) * x / layerExtent);
12385+ tileBbox.getMaxLat() - (tileBbox.getMaxLat() - tileBbox.getMinLat()) * y / layerExtent,
12386+ tileBbox.getMinLon() + (tileBbox.getMaxLon() - tileBbox.getMinLon()) * x / layerExtent
12387+ );
12388 final Collection<VectorNode> nodes = this.store
12389 .searchNodes(new BBox(coords.getLon(), coords.getLat(), VectorDataSet.DUPE_NODE_DISTANCE));
12390 final VectorNode node;
12391@@ -273,48 +271,11 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
12392 }
12393
12394 /**
12395- * Add a tile to this data store
12396- * @param <T> The tile type
12397+ * Add the information from a tile to this object
12398 * @param tile The tile to add
12399- */
12400- public synchronized <T extends Tile & VectorTile> void addTile(T tile) {
12401- addTile(tile, EMPTY_STRING_ARRAY);
12402- }
12403-
12404- /**
12405- * Add a tile to this data store
12406 * @param <T> The tile type
12407- * @param tile The tile to add
12408- * @param invisibleLayers Any invisible current invisible layers
12409 */
12410- public <T extends Tile & VectorTile> void addTile(T tile, String[] invisibleLayers) {
12411- List<String> invisibleLayerList = Arrays.asList(invisibleLayers);
12412- Optional<Tile> previous;
12413- synchronized (this.addedTiles) {
12414- previous = this.addedTiles.stream()
12415- .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny();
12416- }
12417- // Check if we have already added the tile (just to save processing time)
12418- if (!previous.isPresent() || (!previous.get().isLoaded() && !previous.get().isLoading())) {
12419- previous.ifPresent(this.addedTiles::remove);
12420- this.addedTiles.add(tile);
12421- VectorDataStore tStore = new VectorDataStore(this.dataSet, this.zoom);
12422- tStore.createDataTile(tile, invisibleLayerList);
12423- try {
12424- this.getReadWriteLock().writeLock().lockInterruptibly();
12425- tStore.getAllPrimitives().forEach(this::addPrimitive);
12426- } catch (InterruptedException e) {
12427- Logging.error(e);
12428- Thread.currentThread().interrupt();
12429- } finally {
12430- if (this.getReadWriteLock().isWriteLockedByCurrentThread()) {
12431- this.getReadWriteLock().writeLock().unlock();
12432- }
12433- }
12434- }
12435- }
12436-
12437- private <T extends Tile & VectorTile> void createDataTile(T tile, List<String> invisibleLayerList) {
12438+ public <T extends Tile & VectorTile> void addDataTile(T tile) {
12439 for (Layer layer : tile.getLayers()) {
12440 layer.getFeatures().forEach(feature -> {
12441 org.openstreetmap.josm.data.imagery.vectortile.mapbox.Geometry geometry = feature
12442@@ -363,11 +324,6 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
12443 feature.getTags().forEach(primitive::put);
12444 featureObjects.forEach(this::addPrimitive);
12445 primaryFeatureObjects.forEach(this::addPrimitive);
12446- if (invisibleLayerList.contains(primitive.getLayer())) {
12447- primitive.setVisible(false);
12448- featureObjects.forEach(p -> p.setVisible(false));
12449- primaryFeatureObjects.forEach(p -> p.setVisible(false));
12450- }
12451 try {
12452 this.addPrimitive(primitive);
12453 } catch (JosmRuntimeException e) {
12454@@ -377,8 +333,8 @@ class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay,
12455 });
12456 }
12457 // Replace original_ids with the same object (reduce memory usage)
12458- // Strings aren't interned automatically (see
12459- Collection<IPrimitive> primitives = this.dataSet.allPrimitives().stream().filter(p -> p.hasKey(ORIGINAL_ID))
12460+ // Strings aren't interned automatically in some GC implementations
12461+ Collection<IPrimitive> primitives = this.getAllPrimitives().stream().filter(p -> p.hasKey(ORIGINAL_ID))
12462 .collect(Collectors.toList());
12463 List<String> toReplace = primitives.stream().map(p -> p.get(ORIGINAL_ID)).filter(Objects::nonNull).collect(Collectors.toList());
12464 primitives.stream().filter(p -> toReplace.contains(p.get(ORIGINAL_ID)))
12465--
12466GitLab
12467
12468
12469From cf36cba786589d699b6b859b173f730fcf2d945f Mon Sep 17 00:00:00 2001
12470From: Taylor Smock <tsmock@fb.com>
12471Date: Thu, 29 Apr 2021 13:21:57 -0600
12472Subject: [PATCH 25/50] FIXUP: NPE would occur if a null object was passed to
12473 in a selection for Vector data
12474
12475Signed-off-by: Taylor Smock <tsmock@fb.com>
12476---
12477 src/org/openstreetmap/josm/data/vector/VectorDataSet.java | 2 +-
12478 1 file changed, 1 insertion(+), 1 deletion(-)
12479
12480diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
12481index 55366d555..dc79255e7 100644
12482--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
12483+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
12484@@ -435,7 +435,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12485
12486 private void setSelectedImpl(Stream<? extends PrimitiveId> osm) {
12487 this.doSelectionChange(old -> new IDataSelectionListener.SelectionReplaceEvent<>(this, old,
12488- osm.map(this::getPrimitiveById).filter(Objects::nonNull)));
12489+ osm.filter(Objects::nonNull).map(this::getPrimitiveById).filter(Objects::nonNull)));
12490 }
12491
12492 @Override
12493--
12494GitLab
12495
12496
12497From aca31077b866efff187fdf61848fdd73a8722519 Mon Sep 17 00:00:00 2001
12498From: Taylor Smock <tsmock@fb.com>
12499Date: Thu, 29 Apr 2021 14:50:46 -0600
12500Subject: [PATCH 26/50] VectorDataSet: Fix an NPE
12501
12502Signed-off-by: Taylor Smock <tsmock@fb.com>
12503---
12504 src/org/openstreetmap/josm/data/vector/VectorDataSet.java | 2 +-
12505 1 file changed, 1 insertion(+), 1 deletion(-)
12506
12507diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
12508index dc79255e7..1541d169b 100644
12509--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
12510+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
12511@@ -232,7 +232,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
12512 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
12513 if (dataStore != null) {
12514 return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getPrimitivesMap)
12515- .flatMap(m -> Stream.of(primitiveIds).map(m::get));
12516+ .flatMap(m -> Stream.of(primitiveIds).map(m::get)).filter(Objects::nonNull);
12517 }
12518 return Stream.empty();
12519 }
12520--
12521GitLab
12522
12523
12524From 260113af133abd968e0af19821cb082c7490ba94 Mon Sep 17 00:00:00 2001
12525From: Taylor Smock <tsmock@fb.com>
12526Date: Thu, 29 Apr 2021 15:35:26 -0600
12527Subject: [PATCH 27/50] OsmData: Use I<OsmType>.class for get<OsmType> instead
12528 of <OsmType>.class
12529
12530Signed-off-by: Taylor Smock <tsmock@fb.com>
12531---
12532 src/org/openstreetmap/josm/data/osm/OsmData.java | 6 +++---
12533 1 file changed, 3 insertions(+), 3 deletions(-)
12534
12535diff --git a/src/org/openstreetmap/josm/data/osm/OsmData.java b/src/org/openstreetmap/josm/data/osm/OsmData.java
12536index a96515a53..a5be8be9a 100644
12537--- a/src/org/openstreetmap/josm/data/osm/OsmData.java
12538+++ b/src/org/openstreetmap/josm/data/osm/OsmData.java
12539@@ -350,7 +350,7 @@ public interface OsmData<O extends IPrimitive, N extends INode, W extends IWay<N
12540 * @return selected nodes
12541 */
12542 default Collection<N> getSelectedNodes() {
12543- return new SubclassFilteredCollection<>(getSelected(), Node.class::isInstance);
12544+ return new SubclassFilteredCollection<>(getSelected(), INode.class::isInstance);
12545 }
12546
12547 /**
12548@@ -358,7 +358,7 @@ public interface OsmData<O extends IPrimitive, N extends INode, W extends IWay<N
12549 * @return selected ways
12550 */
12551 default Collection<W> getSelectedWays() {
12552- return new SubclassFilteredCollection<>(getSelected(), Way.class::isInstance);
12553+ return new SubclassFilteredCollection<>(getSelected(), IWay.class::isInstance);
12554 }
12555
12556 /**
12557@@ -366,7 +366,7 @@ public interface OsmData<O extends IPrimitive, N extends INode, W extends IWay<N
12558 * @return selected relations
12559 */
12560 default Collection<R> getSelectedRelations() {
12561- return new SubclassFilteredCollection<>(getSelected(), Relation.class::isInstance);
12562+ return new SubclassFilteredCollection<>(getSelected(), IRelation.class::isInstance);
12563 }
12564
12565 /**
12566--
12567GitLab
12568
12569
12570From 6f411f7993cdfd4809c9015bdec17fdaf40c7009 Mon Sep 17 00:00:00 2001
12571From: Simon Legner <simon.legner@gmail.com>
12572Date: Sat, 1 May 2021 22:36:37 +0000
12573Subject: [PATCH 28/50] MapBox -> Mapbox (official spelling) by simon04
12574
12575---
12576 src/org/openstreetmap/josm/data/imagery/ImageryInfo.java | 2 +-
12577 .../josm/data/imagery/vectortile/mapbox/style/Expression.java | 2 +-
12578 .../data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java | 2 +-
12579 3 files changed, 3 insertions(+), 3 deletions(-)
12580
12581diff --git a/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java b/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
12582index 32b1055ed..8a93da2c2 100644
12583--- a/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
12584+++ b/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
12585@@ -62,7 +62,7 @@ public class ImageryInfo extends
12586 WMS_ENDPOINT("wms_endpoint"),
12587 /** WMTS stores GetCapabilities URL. Does not store any information about the layer **/
12588 WMTS("wmts"),
12589- /** MapBox Vector Tiles entry*/
12590+ /** Mapbox Vector Tiles entry*/
12591 MVT("mvt");
12592
12593 private final String typeString;
12594diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
12595index a7f677755..e22f02a45 100644
12596--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
12597+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
12598@@ -11,7 +11,7 @@ import javax.json.JsonString;
12599 import javax.json.JsonValue;
12600
12601 /**
12602- * A MapBox vector style expression (immutable)
12603+ * A Mapbox vector style expression (immutable)
12604 * @author Taylor Smock
12605 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/">https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/</a>
12606 * @since xxx
12607diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
12608index ec24ee5cf..a945cbf57 100644
12609--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
12610+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
12611@@ -45,7 +45,7 @@ import org.openstreetmap.josm.tools.Logging;
12612 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/">https://docs.mapbox.com/mapbox-gl-js/style-spec/</a>
12613 * @since xxx
12614 */
12615-public class MapBoxVectorStyle {
12616+public class MapboxVectorStyle {
12617
12618 private static final ConcurrentHashMap<String, MapBoxVectorStyle> STYLE_MAPPING = new ConcurrentHashMap<>();
12619
12620--
12621GitLab
12622
12623
12624From 82292ff8eee5cbc40887c389c86ce1e401ffda43 Mon Sep 17 00:00:00 2001
12625From: Taylor Smock <tsmock@fb.com>
12626Date: Mon, 3 May 2021 07:54:35 -0600
12627Subject: [PATCH 29/50] FIXUP: MapBox -> Mapbox
12628
12629Signed-off-by: Taylor Smock <tsmock@fb.com>
12630---
12631 .../data/imagery/vectortile/mapbox/Layer.java | 4 +--
12632 .../imagery/vectortile/mapbox/MVTTile.java | 2 +-
12633 ...java => MapboxVectorCachedTileLoader.java} | 12 ++++-----
12634 ...a => MapboxVectorCachedTileLoaderJob.java} | 8 +++---
12635 .../mapbox/MapboxVectorTileSource.java | 10 +++----
12636 .../vectortile/mapbox/style/Layers.java | 2 +-
12637 ...ectorStyle.java => MapboxVectorStyle.java} | 18 ++++++-------
12638 .../vectortile/mapbox/style/Source.java | 2 +-
12639 .../vectortile/mapbox/style/SourceType.java | 2 +-
12640 .../josm/gui/layer/imagery/MVTLayer.java | 8 +++---
12641 .../preferences/imagery/AddMVTLayerPanel.java | 2 +-
12642 .../vectortile/mapbox/MVTTileTest.java | 4 +--
12643 .../mapbox/MapboxVectorTileSourceTest.java | 4 +--
12644 ...leTest.java => MapboxVectorStyleTest.java} | 26 +++++++++----------
12645 .../josm/data/vector/VectorDataSetTest.java | 10 +++----
12646 15 files changed, 57 insertions(+), 57 deletions(-)
12647 rename src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/{MapBoxVectorCachedTileLoader.java => MapboxVectorCachedTileLoader.java} (88%)
12648 rename src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/{MapBoxVectorCachedTileLoaderJob.java => MapboxVectorCachedTileLoaderJob.java} (68%)
12649 rename src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/{MapBoxVectorStyle.java => MapboxVectorStyle.java} (95%)
12650 rename test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/{MapBoxVectorStyleTest.java => MapboxVectorStyleTest.java} (95%)
12651
12652diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
12653index 0a6bb073e..b0b344da1 100644
12654--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
12655+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
12656@@ -218,8 +218,8 @@ public final class Layer implements Destroyable {
12657 }
12658
12659 /**
12660- * Get the MapBox Vector Tile version specification for this layer
12661- * @return The version of the MapBox Vector Tile specification
12662+ * Get the Mapbox Vector Tile version specification for this layer
12663+ * @return The version of the Mapbox Vector Tile specification
12664 */
12665 public byte getVersion() {
12666 return this.version;
12667diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
12668index ab77c43f4..0743bec34 100644
12669--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
12670+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
12671@@ -22,7 +22,7 @@ import java.util.List;
12672 import java.util.stream.Collectors;
12673
12674 /**
12675- * A class for MapBox Vector Tiles
12676+ * A class for Mapbox Vector Tiles
12677 *
12678 * @author Taylor Smock
12679 * @since xxx
12680diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorCachedTileLoader.java
12681similarity index 88%
12682rename from src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
12683rename to src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorCachedTileLoader.java
12684index bf1b368d9..abe8d5992 100644
12685--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
12686+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorCachedTileLoader.java
12687@@ -23,7 +23,7 @@ import org.apache.commons.jcs3.access.behavior.ICacheAccess;
12688 * @author Taylor Smock
12689 * @since xxx
12690 */
12691-public class MapBoxVectorCachedTileLoader implements TileLoader, CachedTileLoader {
12692+public class MapboxVectorCachedTileLoader implements TileLoader, CachedTileLoader {
12693 protected final ICacheAccess<String, BufferedImageCacheEntry> cache;
12694 protected final TileLoaderListener listener;
12695 protected final TileJobOptions options;
12696@@ -38,8 +38,8 @@ public class MapBoxVectorCachedTileLoader implements TileLoader, CachedTileLoade
12697 * @param cache of the cache
12698 * @param options tile job options
12699 */
12700- public MapBoxVectorCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
12701- TileJobOptions options) {
12702+ public MapboxVectorCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache,
12703+ TileJobOptions options) {
12704 CheckParameterUtil.ensureParameterNotNull(cache, "cache");
12705 this.cache = cache;
12706 this.options = options;
12707@@ -53,7 +53,7 @@ public class MapBoxVectorCachedTileLoader implements TileLoader, CachedTileLoade
12708
12709 @Override
12710 public TileJob createTileLoaderJob(Tile tile) {
12711- return new MapBoxVectorCachedTileLoaderJob(
12712+ return new MapboxVectorCachedTileLoaderJob(
12713 listener,
12714 tile,
12715 cache,
12716@@ -64,8 +64,8 @@ public class MapBoxVectorCachedTileLoader implements TileLoader, CachedTileLoade
12717 @Override
12718 public void cancelOutstandingTasks() {
12719 final ThreadPoolExecutor executor = getDownloadExecutor();
12720- executor.getQueue().stream().filter(executor::remove).filter(MapBoxVectorCachedTileLoaderJob.class::isInstance)
12721- .map(MapBoxVectorCachedTileLoaderJob.class::cast).forEach(JCSCachedTileLoaderJob::handleJobCancellation);
12722+ executor.getQueue().stream().filter(executor::remove).filter(MapboxVectorCachedTileLoaderJob.class::isInstance)
12723+ .map(MapboxVectorCachedTileLoaderJob.class::cast).forEach(JCSCachedTileLoaderJob::handleJobCancellation);
12724 }
12725
12726 @Override
12727diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorCachedTileLoaderJob.java
12728similarity index 68%
12729rename from src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
12730rename to src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorCachedTileLoaderJob.java
12731index 748172f5f..a6395cf61 100644
12732--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
12733+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorCachedTileLoaderJob.java
12734@@ -16,11 +16,11 @@ import org.apache.commons.jcs3.access.behavior.ICacheAccess;
12735 * @author Taylor Smock
12736 * @since xxx
12737 */
12738-public class MapBoxVectorCachedTileLoaderJob extends TMSCachedTileLoaderJob {
12739+public class MapboxVectorCachedTileLoaderJob extends TMSCachedTileLoaderJob {
12740
12741- public MapBoxVectorCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
12742- ICacheAccess<String, BufferedImageCacheEntry> cache, TileJobOptions options,
12743- ThreadPoolExecutor downloadExecutor) {
12744+ public MapboxVectorCachedTileLoaderJob(TileLoaderListener listener, Tile tile,
12745+ ICacheAccess<String, BufferedImageCacheEntry> cache, TileJobOptions options,
12746+ ThreadPoolExecutor downloadExecutor) {
12747 super(listener, tile, cache, options, downloadExecutor);
12748 }
12749 }
12750diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
12751index 413c7b32b..62647d1bb 100644
12752--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
12753+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
12754@@ -14,7 +14,7 @@ import javax.json.JsonReader;
12755
12756 import org.openstreetmap.josm.data.imagery.ImageryInfo;
12757 import org.openstreetmap.josm.data.imagery.JosmTemplatedTMSTileSource;
12758-import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapBoxVectorStyle;
12759+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapboxVectorStyle;
12760 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.Source;
12761 import org.openstreetmap.josm.gui.ExtendedDialog;
12762 import org.openstreetmap.josm.gui.MainApplication;
12763@@ -29,7 +29,7 @@ import org.openstreetmap.josm.tools.Logging;
12764 * @since xxx
12765 */
12766 public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource {
12767- private final MapBoxVectorStyle styleSource;
12768+ private final MapboxVectorStyle styleSource;
12769
12770 /**
12771 * Create a new {@link MapboxVectorTileSource} from an {@link ImageryInfo}
12772@@ -37,13 +37,13 @@ public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource {
12773 */
12774 public MapboxVectorTileSource(ImageryInfo info) {
12775 super(info);
12776- MapBoxVectorStyle mapBoxVectorStyle = null;
12777+ MapboxVectorStyle mapBoxVectorStyle = null;
12778 try (CachedFile style = new CachedFile(info.getUrl());
12779 InputStream inputStream = style.getInputStream();
12780 JsonReader reader = Json.createReader(inputStream)) {
12781 reader.readObject();
12782 // OK, we have a stylesheet
12783- mapBoxVectorStyle = MapBoxVectorStyle.getMapBoxVectorStyle(info.getUrl());
12784+ mapBoxVectorStyle = MapboxVectorStyle.getMapboxVectorStyle(info.getUrl());
12785 } catch (IOException | JsonException e) {
12786 Logging.trace(e);
12787 }
12788@@ -86,7 +86,7 @@ public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource {
12789 * Get the style source for this Vector Tile source
12790 * @return The source to use for styling
12791 */
12792- public MapBoxVectorStyle getStyleSource() {
12793+ public MapboxVectorStyle getStyleSource() {
12794 return this.styleSource;
12795 }
12796 }
12797diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
12798index 9488c3d19..d6e55972a 100644
12799--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
12800+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
12801@@ -21,7 +21,7 @@ import javax.json.JsonString;
12802 import javax.json.JsonValue;
12803
12804 /**
12805- * MapBox style layers
12806+ * Mapbox style layers
12807 * @author Taylor Smock
12808 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/">https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/</a>
12809 * @since xxx
12810diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapboxVectorStyle.java
12811similarity index 95%
12812rename from src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
12813rename to src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapboxVectorStyle.java
12814index a945cbf57..68ca3dc52 100644
12815--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
12816+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapboxVectorStyle.java
12817@@ -47,20 +47,20 @@ import org.openstreetmap.josm.tools.Logging;
12818 */
12819 public class MapboxVectorStyle {
12820
12821- private static final ConcurrentHashMap<String, MapBoxVectorStyle> STYLE_MAPPING = new ConcurrentHashMap<>();
12822+ private static final ConcurrentHashMap<String, MapboxVectorStyle> STYLE_MAPPING = new ConcurrentHashMap<>();
12823
12824 /**
12825- * Get a MapBoxVector style for a URL
12826+ * Get a MapboxVector style for a URL
12827 * @param url The url to get
12828- * @return The MapBox Vector Style. May be {@code null} if there was an error.
12829+ * @return The Mapbox Vector Style. May be {@code null} if there was an error.
12830 */
12831- public static MapBoxVectorStyle getMapBoxVectorStyle(String url) {
12832+ public static MapboxVectorStyle getMapboxVectorStyle(String url) {
12833 return STYLE_MAPPING.computeIfAbsent(url, key -> {
12834 try (CachedFile style = new CachedFile(url);
12835 BufferedReader reader = style.getContentReader();
12836 JsonReader jsonReader = Json.createReader(reader)) {
12837 JsonStructure structure = jsonReader.read();
12838- return new MapBoxVectorStyle(structure.asJsonObject());
12839+ return new MapboxVectorStyle(structure.asJsonObject());
12840 } catch (IOException e) {
12841 Logging.error(e);
12842 }
12843@@ -82,13 +82,13 @@ public class MapboxVectorStyle {
12844 private final Map<Source, ElemStyles> sources;
12845
12846 /**
12847- * Create a new MapBoxVector style. You should prefer {@link #getMapBoxVectorStyle(String)}
12848+ * Create a new MapboxVector style. You should prefer {@link #getMapboxVectorStyle(String)}
12849 * for deduplication purposes.
12850 *
12851 * @param jsonObject The object to create the style from
12852- * @see #getMapBoxVectorStyle(String)
12853+ * @see #getMapboxVectorStyle(String)
12854 */
12855- public MapBoxVectorStyle(JsonObject jsonObject) {
12856+ public MapboxVectorStyle(JsonObject jsonObject) {
12857 // There should be a version specifier. We currently only support version 8.
12858 // This can throw an NPE when there is no version number.
12859 this.version = jsonObject.getInt("version");
12860@@ -261,7 +261,7 @@ public class MapboxVectorStyle {
12861 @Override
12862 public boolean equals(Object other) {
12863 if (other != null && other.getClass() == this.getClass()) {
12864- MapBoxVectorStyle o = (MapBoxVectorStyle) other;
12865+ MapboxVectorStyle o = (MapboxVectorStyle) other;
12866 return this.version == o.version
12867 && Objects.equals(this.name, o.name)
12868 && Objects.equals(this.glyphUrl, o.glyphUrl)
12869diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
12870index dd41da72f..4e97e3c5d 100644
12871--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
12872+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
12873@@ -20,7 +20,7 @@ import org.openstreetmap.josm.data.Bounds;
12874 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.InvalidMapboxVectorTileException;
12875
12876 /**
12877- * A source from a MapBox Vector Style
12878+ * A source from a Mapbox Vector Style
12879 *
12880 * @author Taylor Smock
12881 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/</a>
12882diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
12883index a086289d6..35a6114d2 100644
12884--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
12885+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
12886@@ -2,7 +2,7 @@
12887 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
12888
12889 /**
12890- * The "source type" for the data (MapBox Vector Style specification)
12891+ * The "source type" for the data (Mapbox Vector Style specification)
12892 *
12893 * @author Taylor Smock
12894 * @since xxx
12895diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
12896index 2e7aac3e6..6079b24ce 100644
12897--- a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
12898+++ b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
12899@@ -32,7 +32,7 @@ import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer;
12900 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile;
12901 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
12902 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.TileListener;
12903-import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
12904+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorCachedTileLoader;
12905 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
12906 import org.openstreetmap.josm.data.osm.DataSet;
12907 import org.openstreetmap.josm.data.osm.Node;
12908@@ -57,7 +57,7 @@ import org.openstreetmap.josm.gui.mappaint.ElemStyles;
12909 import org.openstreetmap.josm.gui.mappaint.StyleSource;
12910
12911 /**
12912- * A layer for MapBox Vector Tiles
12913+ * A layer for Mapbox Vector Tiles
12914 * @author Taylor Smock
12915 * @since xxx
12916 */
12917@@ -79,7 +79,7 @@ public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSour
12918
12919 @Override
12920 protected Class<? extends TileLoader> getTileLoaderClass() {
12921- return MapBoxVectorCachedTileLoader.class;
12922+ return MapboxVectorCachedTileLoader.class;
12923 }
12924
12925 @Override
12926@@ -89,7 +89,7 @@ public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSour
12927
12928 @Override
12929 public Collection<String> getNativeProjections() {
12930- // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857
12931+ // Mapbox Vector Tiles <i>specifically</i> only support EPSG:3857
12932 // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}").
12933 return Collections.singleton(MVTFile.DEFAULT_PROJECTION);
12934 }
12935diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
12936index 99bbd058d..b1f7a1653 100644
12937--- a/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
12938+++ b/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
12939@@ -17,7 +17,7 @@ import org.openstreetmap.josm.tools.GBC;
12940 import org.openstreetmap.josm.tools.Utils;
12941
12942 /**
12943- * A panel for adding MapBox Vector Tile layers
12944+ * A panel for adding Mapbox Vector Tile layers
12945 * @author Taylor Smock
12946 * @since xxx
12947 */
12948diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
12949index 12b86ebc7..f76f016cf 100644
12950--- a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
12951+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
12952@@ -29,14 +29,14 @@ import org.junit.jupiter.params.provider.MethodSource;
12953 */
12954 public class MVTTileTest {
12955 private MapboxVectorTileSource tileSource;
12956- private MapBoxVectorCachedTileLoader loader;
12957+ private MapboxVectorCachedTileLoader loader;
12958 @RegisterExtension
12959 JOSMTestRules rule = new JOSMTestRules();
12960 @BeforeEach
12961 void setup() {
12962 tileSource = new MapboxVectorTileSource(new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot()
12963 + "pbf/mapillary/{z}/{x}/{y}.mvt"));
12964- loader = new MapBoxVectorCachedTileLoader(null,
12965+ loader = new MapboxVectorCachedTileLoader(null,
12966 JCSCacheManager.getCache("testMapillaryCache"), new TileJobOptions(1, 1, Collections
12967 .emptyMap(), 3600));
12968 }
12969diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java
12970index 5b9f16842..95a9874fe 100644
12971--- a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java
12972+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java
12973@@ -11,7 +11,7 @@ import java.util.stream.Stream;
12974 import org.junit.jupiter.api.extension.RegisterExtension;
12975 import org.openstreetmap.josm.TestUtils;
12976 import org.openstreetmap.josm.data.imagery.ImageryInfo;
12977-import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapBoxVectorStyle;
12978+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapboxVectorStyle;
12979 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.Source;
12980 import org.openstreetmap.josm.gui.ExtendedDialog;
12981 import org.openstreetmap.josm.gui.widgets.JosmComboBox;
12982@@ -70,7 +70,7 @@ class MapboxVectorTileSourceTest {
12983 extendedDialogMocker.getMockResultMap().put(dialogMockerText, "Add layers");
12984 MapboxVectorTileSource tileSource = new MapboxVectorTileSource(
12985 new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot() + "mapillary.json"));
12986- MapBoxVectorStyle styleSource = tileSource.getStyleSource();
12987+ MapboxVectorStyle styleSource = tileSource.getStyleSource();
12988 assertNotNull(styleSource);
12989 assertEquals(expected, tileSource.toString());
12990 }
12991diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapboxVectorStyleTest.java
12992similarity index 95%
12993rename from test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java
12994rename to test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapboxVectorStyleTest.java
12995index 1fcb7bfe8..461feffa0 100644
12996--- a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java
12997+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapboxVectorStyleTest.java
12998@@ -56,10 +56,10 @@ import org.junit.jupiter.api.extension.RegisterExtension;
12999 import org.junit.jupiter.api.io.TempDir;
13000
13001 /**
13002- * Test class for {@link MapBoxVectorStyle}
13003+ * Test class for {@link MapboxVectorStyle}
13004 * @author Taylor Smock
13005 */
13006-public class MapBoxVectorStyleTest {
13007+public class MapboxVectorStyleTest {
13008 /** Used to store sprite files (specifically, sprite{,@2x}.{png,json}) */
13009 @TempDir
13010 File spritesDirectory;
13011@@ -86,21 +86,21 @@ public class MapBoxVectorStyleTest {
13012 */
13013 @Test
13014 void testVersionChecks() {
13015- assertThrows(NullPointerException.class, () -> new MapBoxVectorStyle(JsonValue.EMPTY_JSON_OBJECT));
13016+ assertThrows(NullPointerException.class, () -> new MapboxVectorStyle(JsonValue.EMPTY_JSON_OBJECT));
13017 IllegalArgumentException badVersion = assertThrows(IllegalArgumentException.class,
13018- () -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 7).build()));
13019+ () -> new MapboxVectorStyle(Json.createObjectBuilder().add("version", 7).build()));
13020 assertEquals("Vector Tile Style Version not understood: version 7 (json: {\"version\":7})", badVersion.getMessage());
13021 badVersion = assertThrows(IllegalArgumentException.class,
13022- () -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 9).build()));
13023+ () -> new MapboxVectorStyle(Json.createObjectBuilder().add("version", 9).build()));
13024 assertEquals("Vector Tile Style Version not understood: version 9 (json: {\"version\":9})", badVersion.getMessage());
13025- assertDoesNotThrow(() -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 8).build()));
13026+ assertDoesNotThrow(() -> new MapboxVectorStyle(Json.createObjectBuilder().add("version", 8).build()));
13027 }
13028
13029 @Test
13030 void testSources() {
13031 // Check with an invalid sources list
13032- assertTrue(new MapBoxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty());
13033- Map<Source, ElemStyles> sources = new MapBoxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test",
13034+ assertTrue(new MapboxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty());
13035+ Map<Source, ElemStyles> sources = new MapboxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test",
13036 MessageFormat.format("\"sources\":'{'{0},{1},\"source3\":[\"bad source\"]'}',\"layers\":[{2},{3},{4}]",
13037 SOURCE1, SOURCE2, LAYER1, LAYER2, LAYER2.replace('2', '3'))))).getSources();
13038 assertEquals(3, sources.size());
13039@@ -113,8 +113,8 @@ public class MapBoxVectorStyleTest {
13040
13041 @Test
13042 void testSavedFiles() {
13043- assertTrue(new MapBoxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty());
13044- Map<Source, ElemStyles> sources = new MapBoxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test",
13045+ assertTrue(new MapboxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty());
13046+ Map<Source, ElemStyles> sources = new MapboxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test",
13047 MessageFormat.format("\"sources\":'{'{0},{1}'}',\"layers\":[{2},{3}]", SOURCE1, SOURCE2, LAYER1, LAYER2)))).getSources();
13048 assertEquals(2, sources.size());
13049 // For various reasons, the map _must_ be reliably ordered in the order of encounter
13050@@ -162,7 +162,7 @@ public class MapBoxVectorStyleTest {
13051 ImageProvider.clearCache();
13052 int hiDpiScalar = hiDpi ? 2 : 1;
13053 String spritePath = new File(this.spritesDirectory, "sprite").getPath();
13054- MapBoxVectorStyle style = new MapBoxVectorStyle(getJson(JsonObject.class,
13055+ MapboxVectorStyle style = new MapboxVectorStyle(getJson(JsonObject.class,
13056 MessageFormat.format(BASE_STYLE, "sprite_test", "\"sprite\":\"file:/" + spritePath + "\"")));
13057 assertEquals("file:/" + spritePath, style.getSpriteUrl());
13058
13059@@ -238,7 +238,7 @@ public class MapBoxVectorStyleTest {
13060 @Test
13061 void testMapillaryStyle() {
13062 final String file = Paths.get("file:", TestUtils.getTestDataRoot(), "mapillary.json").toString();
13063- final MapBoxVectorStyle style = MapBoxVectorStyle.getMapBoxVectorStyle(file);
13064+ final MapboxVectorStyle style = MapboxVectorStyle.getMapboxVectorStyle(file);
13065 assertNotNull(style);
13066 // There are three "sources" in the mapillary.json file
13067 assertEquals(3, style.getSources().size());
13068@@ -266,7 +266,7 @@ public class MapBoxVectorStyleTest {
13069 StyleSource node = new MapCSSStyleSource("meta{title:\"node\";}node{text:ref;}");
13070 node.loadStyleSource();
13071 canvas.loadStyleSource();
13072- EqualsVerifier.forClass(MapBoxVectorStyle.class)
13073+ EqualsVerifier.forClass(MapboxVectorStyle.class)
13074 .withPrefabValues(ImageProvider.class, new ImageProvider("cancel"), new ImageProvider("ok"))
13075 .withPrefabValues(StyleSource.class, canvas, node)
13076 .usingGetClass().verify();
13077diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
13078index 35e208979..0e7a572d5 100644
13079--- a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
13080+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
13081@@ -18,7 +18,7 @@ import java.util.stream.Collectors;
13082 import org.openstreetmap.josm.TestUtils;
13083 import org.openstreetmap.josm.data.imagery.ImageryInfo;
13084 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
13085-import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader;
13086+import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorCachedTileLoader;
13087 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource;
13088 import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
13089 import org.openstreetmap.josm.testutils.JOSMTestRules;
13090@@ -48,12 +48,12 @@ class VectorDataSetTest {
13091 return super.getTileSource();
13092 }
13093
13094- protected MapBoxVectorCachedTileLoader getTileLoader() {
13095+ protected MapboxVectorCachedTileLoader getTileLoader() {
13096 if (this.tileLoader == null) {
13097 this.tileLoader = this.getTileLoaderFactory().makeTileLoader(this, Collections.emptyMap(), 7200);
13098 }
13099- if (this.tileLoader instanceof MapBoxVectorCachedTileLoader) {
13100- return (MapBoxVectorCachedTileLoader) this.tileLoader;
13101+ if (this.tileLoader instanceof MapboxVectorCachedTileLoader) {
13102+ return (MapboxVectorCachedTileLoader) this.tileLoader;
13103 }
13104 return null;
13105 }
13106@@ -82,7 +82,7 @@ class VectorDataSetTest {
13107 throw new IllegalArgumentException("Tiles come with a {z}, {x}, and {y} component");
13108 }
13109 final MapboxVectorTileSource tileSource = layer.getTileSource();
13110- MapBoxVectorCachedTileLoader tileLoader = layer.getTileLoader();
13111+ MapboxVectorCachedTileLoader tileLoader = layer.getTileLoader();
13112 Collection<MVTTile> tilesCollection = new ArrayList<>();
13113 for (int i = 0; i < tiles.length / 3; i++) {
13114 final MVTTile tile = (MVTTile) layer.createTile(tileSource, tiles[3 * i + 1], tiles[3 * i + 2], tiles[3 * i]);
13115--
13116GitLab
13117
13118
13119From bca406ffcbebc18ee6bc9e5fd25a720aa6e8a799 Mon Sep 17 00:00:00 2001
13120From: Taylor Smock <tsmock@fb.com>
13121Date: Mon, 3 May 2021 07:19:34 -0600
13122Subject: [PATCH 30/50] JCSCachedTileLoader: Use Utils.readBytesFromStream
13123 (patch by simon04)
13124
13125Signed-off-by: Taylor Smock <tsmock@fb.com>
13126---
13127 .../openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java | 2 +-
13128 1 file changed, 1 insertion(+), 1 deletion(-)
13129
13130diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
13131index eeac761c6..d07dff6d1 100644
13132--- a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
13133+++ b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
13134@@ -321,7 +321,7 @@ public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements
13135 file = new File(fileName.substring("file://".length() - 1));
13136 }
13137 try (InputStream fileInputStream = Files.newInputStream(file.toPath())) {
13138- cacheData = createCacheEntry(IOUtils.toByteArray(fileInputStream));
13139+ cacheData = createCacheEntry(Utils.readBytesFromStream(fileInputStream));
13140 cache.put(getCacheKey(), cacheData, attributes);
13141 return true;
13142 } catch (IOException e) {
13143--
13144GitLab
13145
13146
13147From 1ff96e03474a501025e871db61c5eb72bbf65c8f Mon Sep 17 00:00:00 2001
13148From: Taylor Smock <tsmock@fb.com>
13149Date: Mon, 3 May 2021 07:28:12 -0600
13150Subject: [PATCH 31/50] WaySegment: Add methods for binary compatibility
13151
13152Signed-off-by: Taylor Smock <tsmock@fb.com>
13153---
13154 .../josm/data/osm/WaySegment.java | 50 ++++++++++++++++++-
13155 1 file changed, 48 insertions(+), 2 deletions(-)
13156
13157diff --git a/src/org/openstreetmap/josm/data/osm/WaySegment.java b/src/org/openstreetmap/josm/data/osm/WaySegment.java
13158index 302f82842..a419980b5 100644
13159--- a/src/org/openstreetmap/josm/data/osm/WaySegment.java
13160+++ b/src/org/openstreetmap/josm/data/osm/WaySegment.java
13161@@ -21,8 +21,8 @@ public final class WaySegment extends IWaySegment<Node, Way> {
13162 * Determines and returns the way segment for the given way and node pair. You should prefer
13163 * {@link IWaySegment#forNodePair(IWay, INode, INode)} whenever possible.
13164 *
13165- * @param way way
13166- * @param first first node
13167+ * @param way way
13168+ * @param first first node
13169 * @param second second node
13170 * @return way segment
13171 * @throws IllegalArgumentException if the node pair is not part of way
13172@@ -39,6 +39,19 @@ public final class WaySegment extends IWaySegment<Node, Way> {
13173 throw new IllegalArgumentException("Node pair is not part of way!");
13174 }
13175
13176+ @Override
13177+ public Node getFirstNode() {
13178+ // This is kept for binary compatibility
13179+ return super.getFirstNode();
13180+ }
13181+
13182+ @Override
13183+ public Node getSecondNode() {
13184+ // This is kept for binary compatibility
13185+ return super.getSecondNode();
13186+ }
13187+
13188+
13189 /**
13190 * Returns this way segment as complete way.
13191 * @return the way segment as {@code Way}
13192@@ -51,6 +64,39 @@ public final class WaySegment extends IWaySegment<Node, Way> {
13193 return w;
13194 }
13195
13196+ @Override
13197+ public boolean equals(Object o) {
13198+ // This is kept for binary compatibility
13199+ return super.equals(o);
13200+ }
13201+
13202+ @Override
13203+ public int hashCode() {
13204+ // This is kept for binary compatibility
13205+ return super.hashCode();
13206+ }
13207+
13208+ /**
13209+ * Checks whether this segment crosses other segment
13210+ *
13211+ * @param s2 The other segment
13212+ * @return true if both segments crosses
13213+ */
13214+ public boolean intersects(WaySegment s2) {
13215+ // This is kept for binary compatibility
13216+ return super.intersects(s2);
13217+ }
13218+
13219+ /**
13220+ * Checks whether this segment and another way segment share the same points
13221+ * @param s2 The other segment
13222+ * @return true if other way segment is the same or reverse
13223+ */
13224+ public boolean isSimilar(WaySegment s2) {
13225+ // This is kept for binary compatibility
13226+ return super.isSimilar(s2);
13227+ }
13228+
13229 @Override
13230 public String toString() {
13231 return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']';
13232--
13233GitLab
13234
13235
13236From b24efb00bb6569ba63214ea73ae843248d582ec9 Mon Sep 17 00:00:00 2001
13237From: Taylor Smock <tsmock@fb.com>
13238Date: Mon, 3 May 2021 07:31:28 -0600
13239Subject: [PATCH 32/50] ProtoBuf -> Protobuf
13240
13241Signed-off-by: Taylor Smock <tsmock@fb.com>
13242---
13243 .../imagery/vectortile/mapbox/Feature.java | 18 +++----
13244 .../data/imagery/vectortile/mapbox/Layer.java | 48 ++++++++---------
13245 .../imagery/vectortile/mapbox/MVTTile.java | 12 ++---
13246 ...rotoBufPacked.java => ProtobufPacked.java} | 14 ++---
13247 ...rotoBufParser.java => ProtobufParser.java} | 12 ++---
13248 ...rotoBufRecord.java => ProtobufRecord.java} | 14 ++---
13249 .../imagery/vectortile/mapbox/LayerTest.java | 12 ++---
13250 .../data/protobuf/ProtoBufParserTest.java | 51 -------------------
13251 .../josm/data/protobuf/ProtoBufTest.java | 36 ++++++-------
13252 .../data/protobuf/ProtobufParserTest.java | 51 +++++++++++++++++++
13253 ...ecordTest.java => ProtobufRecordTest.java} | 12 ++---
13254 11 files changed, 140 insertions(+), 140 deletions(-)
13255 rename src/org/openstreetmap/josm/data/protobuf/{ProtoBufPacked.java => ProtobufPacked.java} (79%)
13256 rename src/org/openstreetmap/josm/data/protobuf/{ProtoBufParser.java => ProtobufParser.java} (95%)
13257 rename src/org/openstreetmap/josm/data/protobuf/{ProtoBufRecord.java => ProtobufRecord.java} (86%)
13258 delete mode 100644 test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java
13259 create mode 100644 test/unit/org/openstreetmap/josm/data/protobuf/ProtobufParserTest.java
13260 rename test/unit/org/openstreetmap/josm/data/protobuf/{ProtoBufRecordTest.java => ProtobufRecordTest.java} (67%)
13261
13262diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
13263index df194cc00..f0778b250 100644
13264--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
13265+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
13266@@ -7,9 +7,9 @@ import java.util.ArrayList;
13267 import java.util.List;
13268
13269 import org.openstreetmap.josm.data.osm.TagMap;
13270-import org.openstreetmap.josm.data.protobuf.ProtoBufPacked;
13271-import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
13272-import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
13273+import org.openstreetmap.josm.data.protobuf.ProtobufPacked;
13274+import org.openstreetmap.josm.data.protobuf.ProtobufParser;
13275+import org.openstreetmap.josm.data.protobuf.ProtobufRecord;
13276 import org.openstreetmap.josm.tools.Utils;
13277
13278 /**
13279@@ -50,25 +50,25 @@ public class Feature {
13280 * @param record The record to create the feature from
13281 * @throws IOException - if an IO error occurs
13282 */
13283- public Feature(Layer layer, ProtoBufRecord record) throws IOException {
13284+ public Feature(Layer layer, ProtobufRecord record) throws IOException {
13285 long tId = 0;
13286 GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN;
13287 String key = null;
13288- try (ProtoBufParser parser = new ProtoBufParser(record.getBytes())) {
13289+ try (ProtobufParser parser = new ProtobufParser(record.getBytes())) {
13290 while (parser.hasNext()) {
13291- try (ProtoBufRecord next = new ProtoBufRecord(parser)) {
13292+ try (ProtobufRecord next = new ProtobufRecord(parser)) {
13293 if (next.getField() == TAG_FIELD) {
13294 if (tags == null) {
13295 tags = new TagMap();
13296 }
13297 // This is packed in v1 and v2
13298- ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
13299+ ProtobufPacked packed = new ProtobufPacked(next.getBytes());
13300 for (Number number : packed.getArray()) {
13301 key = parseTagValue(key, layer, number);
13302 }
13303 } else if (next.getField() == GEOMETRY_FIELD) {
13304 // This is packed in v1 and v2
13305- ProtoBufPacked packed = new ProtoBufPacked(next.getBytes());
13306+ ProtobufPacked packed = new ProtobufPacked(next.getBytes());
13307 CommandInteger currentCommand = null;
13308 for (Number number : packed.getArray()) {
13309 if (currentCommand != null && currentCommand.hasAllExpectedParameters()) {
13310@@ -78,7 +78,7 @@ public class Feature {
13311 currentCommand = new CommandInteger(number.intValue());
13312 this.geometry.add(currentCommand);
13313 } else {
13314- currentCommand.addParameter(ProtoBufParser.decodeZigZag(number));
13315+ currentCommand.addParameter(ProtobufParser.decodeZigZag(number));
13316 }
13317 }
13318 // TODO fallback to non-packed
13319diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
13320index b0b344da1..ed36061d1 100644
13321--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
13322+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
13323@@ -14,8 +14,8 @@ import java.util.Objects;
13324 import java.util.function.Function;
13325 import java.util.stream.Collectors;
13326
13327-import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
13328-import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
13329+import org.openstreetmap.josm.data.protobuf.ProtobufParser;
13330+import org.openstreetmap.josm.data.protobuf.ProtobufRecord;
13331 import org.openstreetmap.josm.tools.Destroyable;
13332 import org.openstreetmap.josm.tools.Logging;
13333
13334@@ -26,13 +26,13 @@ import org.openstreetmap.josm.tools.Logging;
13335 */
13336 public final class Layer implements Destroyable {
13337 private static final class ValueFields<T> {
13338- static final ValueFields<String> STRING = new ValueFields<>(1, ProtoBufRecord::asString);
13339- static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtoBufRecord::asFloat);
13340- static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtoBufRecord::asDouble);
13341- static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtoBufRecord::asUnsignedVarInt);
13342+ static final ValueFields<String> STRING = new ValueFields<>(1, ProtobufRecord::asString);
13343+ static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtobufRecord::asFloat);
13344+ static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtobufRecord::asDouble);
13345+ static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtobufRecord::asUnsignedVarInt);
13346 // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE})
13347- static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtoBufRecord::asUnsignedVarInt);
13348- static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtoBufRecord::asSignedVarInt);
13349+ static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtobufRecord::asUnsignedVarInt);
13350+ static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtobufRecord::asSignedVarInt);
13351 static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0);
13352
13353 /**
13354@@ -42,8 +42,8 @@ public final class Layer implements Destroyable {
13355 Collections.unmodifiableList(Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL));
13356
13357 private final byte field;
13358- private final Function<ProtoBufRecord, T> conversion;
13359- private ValueFields(int field, Function<ProtoBufRecord, T> conversion) {
13360+ private final Function<ProtobufRecord, T> conversion;
13361+ private ValueFields(int field, Function<ProtobufRecord, T> conversion) {
13362 this.field = (byte) field;
13363 this.conversion = conversion;
13364 }
13365@@ -61,12 +61,12 @@ public final class Layer implements Destroyable {
13366 * @param protobufRecord The record to convert
13367 * @return the converted value
13368 */
13369- public T convertValue(ProtoBufRecord protobufRecord) {
13370+ public T convertValue(ProtobufRecord protobufRecord) {
13371 return this.conversion.apply(protobufRecord);
13372 }
13373 }
13374
13375- /** The field value for a layer (in {@link ProtoBufRecord#getField}) */
13376+ /** The field value for a layer (in {@link ProtobufRecord#getField}) */
13377 public static final byte LAYER_FIELD = 3;
13378 private static final byte VERSION_FIELD = 15;
13379 private static final byte NAME_FIELD = 1;
13380@@ -97,26 +97,26 @@ public final class Layer implements Destroyable {
13381 * @param records The records to convert to a layer
13382 * @throws IOException - if an IO error occurs
13383 */
13384- public Layer(Collection<ProtoBufRecord> records) throws IOException {
13385+ public Layer(Collection<ProtobufRecord> records) throws IOException {
13386 // Do the unique required fields first
13387- Map<Integer, List<ProtoBufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtoBufRecord::getField));
13388+ Map<Integer, List<ProtobufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtobufRecord::getField));
13389 this.version = sorted.getOrDefault((int) VERSION_FIELD, Collections.emptyList()).parallelStream()
13390- .map(ProtoBufRecord::asUnsignedVarInt).map(Number::byteValue).findFirst().orElse(DEFAULT_VERSION);
13391+ .map(ProtobufRecord::asUnsignedVarInt).map(Number::byteValue).findFirst().orElse(DEFAULT_VERSION);
13392 // Per spec, we cannot continue past this until we have checked the version number
13393 if (this.version != 1 && this.version != 2) {
13394 throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version));
13395 }
13396- this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst()
13397+ this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::asString).findFirst()
13398 .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name")));
13399- this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asUnsignedVarInt)
13400+ this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::asUnsignedVarInt)
13401 .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT);
13402
13403- sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString)
13404+ sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::asString)
13405 .forEachOrdered(this.keyList::add);
13406- sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::getBytes)
13407- .map(ProtoBufParser::new).map(parser1 -> {
13408+ sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::getBytes)
13409+ .map(ProtobufParser::new).map(parser1 -> {
13410 try {
13411- return new ProtoBufRecord(parser1);
13412+ return new ProtobufRecord(parser1);
13413 } catch (IOException e) {
13414 Logging.error(e);
13415 return null;
13416@@ -141,7 +141,7 @@ public final class Layer implements Destroyable {
13417 throw exceptions.iterator().next();
13418 }
13419 // Cleanup bytes (for memory)
13420- for (ProtoBufRecord record : records) {
13421+ for (ProtobufRecord record : records) {
13422 record.close();
13423 }
13424 }
13425@@ -152,8 +152,8 @@ public final class Layer implements Destroyable {
13426 * @return All the protobuf records
13427 * @throws IOException If there was an error reading the bytes (unlikely)
13428 */
13429- private static Collection<ProtoBufRecord> getAllRecords(byte[] bytes) throws IOException {
13430- try (ProtoBufParser parser = new ProtoBufParser(bytes)) {
13431+ private static Collection<ProtobufRecord> getAllRecords(byte[] bytes) throws IOException {
13432+ try (ProtobufParser parser = new ProtobufParser(bytes)) {
13433 return parser.allRecords();
13434 }
13435 }
13436diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
13437index 0743bec34..522038c77 100644
13438--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
13439+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
13440@@ -7,8 +7,8 @@ import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
13441 import org.openstreetmap.josm.data.IQuadBucketType;
13442 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile;
13443 import org.openstreetmap.josm.data.osm.BBox;
13444-import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
13445-import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
13446+import org.openstreetmap.josm.data.protobuf.ProtobufParser;
13447+import org.openstreetmap.josm.data.protobuf.ProtobufRecord;
13448 import org.openstreetmap.josm.data.vector.VectorDataStore;
13449 import org.openstreetmap.josm.tools.ListenerList;
13450 import org.openstreetmap.josm.tools.Logging;
13451@@ -50,13 +50,13 @@ public class MVTTile extends Tile implements VectorTile, IQuadBucketType {
13452 public void loadImage(final InputStream inputStream) throws IOException {
13453 if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) {
13454 this.initLoading();
13455- ProtoBufParser parser = new ProtoBufParser(inputStream);
13456- Collection<ProtoBufRecord> protoBufRecords = parser.allRecords();
13457+ ProtobufParser parser = new ProtobufParser(inputStream);
13458+ Collection<ProtobufRecord> protobufRecords = parser.allRecords();
13459 this.layers = new HashSet<>();
13460- this.layers = protoBufRecords.stream().map(protoBufRecord -> {
13461+ this.layers = protobufRecords.stream().map(protoBufRecord -> {
13462 Layer mvtLayer = null;
13463 if (protoBufRecord.getField() == Layer.LAYER_FIELD) {
13464- try (ProtoBufParser tParser = new ProtoBufParser(protoBufRecord.getBytes())) {
13465+ try (ProtobufParser tParser = new ProtobufParser(protoBufRecord.getBytes())) {
13466 mvtLayer = new Layer(tParser.allRecords());
13467 } catch (IOException e) {
13468 Logging.error(e);
13469diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java b/src/org/openstreetmap/josm/data/protobuf/ProtobufPacked.java
13470similarity index 79%
13471rename from src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
13472rename to src/org/openstreetmap/josm/data/protobuf/ProtobufPacked.java
13473index 109f8915a..a12bbe6b8 100644
13474--- a/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
13475+++ b/src/org/openstreetmap/josm/data/protobuf/ProtobufPacked.java
13476@@ -10,22 +10,22 @@ import java.util.List;
13477 * @author Taylor Smock
13478 * @since xxx
13479 */
13480-public class ProtoBufPacked {
13481+public class ProtobufPacked {
13482 private final byte[] bytes;
13483 private final Number[] numbers;
13484 private int location;
13485
13486 /**
13487- * Create a new ProtoBufPacked object
13488+ * Create a new ProtobufPacked object
13489 *
13490 * @param bytes The packed bytes
13491 */
13492- public ProtoBufPacked(byte[] bytes) {
13493+ public ProtobufPacked(byte[] bytes) {
13494 this.location = 0;
13495 this.bytes = bytes;
13496 List<Number> numbersT = new ArrayList<>();
13497 while (this.location < bytes.length) {
13498- numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE));
13499+ numbersT.add(ProtobufParser.convertByteArray(this.nextVarInt(), ProtobufParser.VAR_INT_BYTE_SIZE));
13500 }
13501
13502 this.numbers = new Number[numbersT.size()];
13503@@ -45,10 +45,10 @@ public class ProtoBufPacked {
13504
13505 private byte[] nextVarInt() {
13506 List<Byte> byteList = new ArrayList<>();
13507- while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE)
13508- == ProtoBufParser.MOST_SIGNIFICANT_BYTE) {
13509+ while ((this.bytes[this.location] & ProtobufParser.MOST_SIGNIFICANT_BYTE)
13510+ == ProtobufParser.MOST_SIGNIFICANT_BYTE) {
13511 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned)
13512- byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE));
13513+ byteList.add((byte) (this.bytes[this.location++] ^ ProtobufParser.MOST_SIGNIFICANT_BYTE));
13514 }
13515 // The last byte doesn't drop the most significant bit
13516 byteList.add(this.bytes[this.location++]);
13517diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java b/src/org/openstreetmap/josm/data/protobuf/ProtobufParser.java
13518similarity index 95%
13519rename from src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
13520rename to src/org/openstreetmap/josm/data/protobuf/ProtobufParser.java
13521index 18059e339..8364cc3b7 100644
13522--- a/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
13523+++ b/src/org/openstreetmap/josm/data/protobuf/ProtobufParser.java
13524@@ -17,7 +17,7 @@ import org.openstreetmap.josm.tools.Logging;
13525 * @author Taylor Smock
13526 * @since xxx
13527 */
13528-public class ProtoBufParser implements AutoCloseable {
13529+public class ProtobufParser implements AutoCloseable {
13530 /**
13531 * The default byte size (see {@link #VAR_INT_BYTE_SIZE} for var ints)
13532 */
13533@@ -96,7 +96,7 @@ public class ProtoBufParser implements AutoCloseable {
13534 *
13535 * @param bytes The bytes to parse
13536 */
13537- public ProtoBufParser(byte[] bytes) {
13538+ public ProtobufParser(byte[] bytes) {
13539 this(new ByteArrayInputStream(bytes));
13540 }
13541
13542@@ -105,7 +105,7 @@ public class ProtoBufParser implements AutoCloseable {
13543 *
13544 * @param inputStream The InputStream (will be fully read at this time)
13545 */
13546- public ProtoBufParser(InputStream inputStream) {
13547+ public ProtobufParser(InputStream inputStream) {
13548 if (inputStream.markSupported()) {
13549 this.inputStream = inputStream;
13550 } else {
13551@@ -119,10 +119,10 @@ public class ProtoBufParser implements AutoCloseable {
13552 * @return A collection of all records
13553 * @throws IOException - if an IO error occurs
13554 */
13555- public Collection<ProtoBufRecord> allRecords() throws IOException {
13556- Collection<ProtoBufRecord> records = new ArrayList<>();
13557+ public Collection<ProtobufRecord> allRecords() throws IOException {
13558+ Collection<ProtobufRecord> records = new ArrayList<>();
13559 while (this.hasNext()) {
13560- records.add(new ProtoBufRecord(this));
13561+ records.add(new ProtobufRecord(this));
13562 }
13563 return records;
13564 }
13565diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java b/src/org/openstreetmap/josm/data/protobuf/ProtobufRecord.java
13566similarity index 86%
13567rename from src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
13568rename to src/org/openstreetmap/josm/data/protobuf/ProtobufRecord.java
13569index 1eb5d38a6..e760996d4 100644
13570--- a/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
13571+++ b/src/org/openstreetmap/josm/data/protobuf/ProtobufRecord.java
13572@@ -13,7 +13,7 @@ import org.openstreetmap.josm.tools.Utils;
13573 * @author Taylor Smock
13574 * @since xxx
13575 */
13576-public class ProtoBufRecord implements AutoCloseable {
13577+public class ProtobufRecord implements AutoCloseable {
13578 private static final byte[] EMPTY_BYTES = {};
13579 private final WireType type;
13580 private final int field;
13581@@ -25,8 +25,8 @@ public class ProtoBufRecord implements AutoCloseable {
13582 * @param parser The parser to use to create the record
13583 * @throws IOException - if an IO error occurs
13584 */
13585- public ProtoBufRecord(ProtoBufParser parser) throws IOException {
13586- Number number = ProtoBufParser.convertByteArray(parser.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE);
13587+ public ProtobufRecord(ProtobufParser parser) throws IOException {
13588+ Number number = ProtobufParser.convertByteArray(parser.nextVarInt(), ProtobufParser.VAR_INT_BYTE_SIZE);
13589 // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3}
13590 this.field = (int) number.longValue() >> 3;
13591 // 7 is 111 (so last three bits)
13592@@ -53,7 +53,7 @@ public class ProtoBufRecord implements AutoCloseable {
13593 * @return the double
13594 */
13595 public double asDouble() {
13596- long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue();
13597+ long doubleNumber = ProtobufParser.convertByteArray(asFixed64(), ProtobufParser.BYTE_SIZE).longValue();
13598 return Double.longBitsToDouble(doubleNumber);
13599 }
13600
13601@@ -85,7 +85,7 @@ public class ProtoBufRecord implements AutoCloseable {
13602 * @return the float
13603 */
13604 public float asFloat() {
13605- int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue();
13606+ int floatNumber = ProtobufParser.convertByteArray(asFixed32(), ProtobufParser.BYTE_SIZE).intValue();
13607 return Float.intBitsToFloat(floatNumber);
13608 }
13609
13610@@ -97,7 +97,7 @@ public class ProtoBufRecord implements AutoCloseable {
13611 */
13612 public Number asSignedVarInt() {
13613 final Number signed = this.asUnsignedVarInt();
13614- return ProtoBufParser.decodeZigZag(signed);
13615+ return ProtobufParser.decodeZigZag(signed);
13616 }
13617
13618 /**
13619@@ -115,7 +115,7 @@ public class ProtoBufRecord implements AutoCloseable {
13620 * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum})
13621 */
13622 public Number asUnsignedVarInt() {
13623- return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE);
13624+ return ProtobufParser.convertByteArray(this.bytes, ProtobufParser.VAR_INT_BYTE_SIZE);
13625 }
13626
13627 @Override
13628diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
13629index 61e21dc60..e9b5bae8d 100644
13630--- a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
13631+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
13632@@ -11,8 +11,8 @@ import java.util.Arrays;
13633 import java.util.List;
13634
13635 import org.openstreetmap.josm.TestUtils;
13636-import org.openstreetmap.josm.data.protobuf.ProtoBufParser;
13637-import org.openstreetmap.josm.data.protobuf.ProtoBufRecord;
13638+import org.openstreetmap.josm.data.protobuf.ProtobufParser;
13639+import org.openstreetmap.josm.data.protobuf.ProtobufRecord;
13640
13641 import nl.jqno.equalsverifier.EqualsVerifier;
13642 import org.junit.jupiter.api.Test;
13643@@ -66,14 +66,14 @@ public class LayerTest {
13644 * @throws IOException If something happened (should never trigger)
13645 */
13646 static Layer getLayer(byte[] bytes) throws IOException {
13647- List<ProtoBufRecord> records = (List<ProtoBufRecord>) new ProtoBufParser(bytes).allRecords();
13648+ List<ProtobufRecord> records = (List<ProtobufRecord>) new ProtobufParser(bytes).allRecords();
13649 assertEquals(1, records.size());
13650- return new Layer(new ProtoBufParser(records.get(0).getBytes()).allRecords());
13651+ return new Layer(new ProtobufParser(records.get(0).getBytes()).allRecords());
13652 }
13653
13654 @Test
13655 void testLayerCreation() throws IOException {
13656- List<ProtoBufRecord> layers = (List<ProtoBufRecord>) new ProtoBufParser(new FileInputStream(TestUtils.getTestDataRoot()
13657+ List<ProtobufRecord> layers = (List<ProtobufRecord>) new ProtobufParser(new FileInputStream(TestUtils.getTestDataRoot()
13658 + "pbf/mapillary/14/3249/6258.mvt")).allRecords();
13659 Layer sequenceLayer = new Layer(layers.get(0).getBytes());
13660 assertEquals("mapillary-sequences", sequenceLayer.getName());
13661@@ -92,7 +92,7 @@ public class LayerTest {
13662
13663 @Test
13664 void testLayerEqualsHashCode() throws IOException {
13665- List<ProtoBufRecord> layers = (List<ProtoBufRecord>) new ProtoBufParser(new FileInputStream(TestUtils.getTestDataRoot()
13666+ List<ProtobufRecord> layers = (List<ProtobufRecord>) new ProtobufParser(new FileInputStream(TestUtils.getTestDataRoot()
13667 + "pbf/mapillary/14/3249/6258.mvt")).allRecords();
13668 EqualsVerifier.forClass(Layer.class).withPrefabValues(byte[].class, layers.get(0).getBytes(), layers.get(1).getBytes())
13669 .verify();
13670diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java
13671deleted file mode 100644
13672index bdfdf86b7..000000000
13673--- a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java
13674+++ /dev/null
13675@@ -1,51 +0,0 @@
13676-// License: GPL. For details, see LICENSE file.
13677-package org.openstreetmap.josm.data.protobuf;
13678-
13679-import static org.junit.jupiter.api.Assertions.assertEquals;
13680-
13681-import org.junit.jupiter.api.Test;
13682-
13683-/**
13684- * Test class for {@link ProtoBufParser}
13685- * @author Taylor Smock
13686- * @since xxx
13687- */
13688-class ProtoBufParserTest {
13689- /**
13690- * Check that we are appropriately converting values to the "smallest" type
13691- */
13692- @Test
13693- void testConvertLong() {
13694- // No casting due to auto conversions
13695- assertEquals(Byte.MAX_VALUE, ProtoBufParser.convertLong(Byte.MAX_VALUE));
13696- assertEquals(Byte.MIN_VALUE, ProtoBufParser.convertLong(Byte.MIN_VALUE));
13697- assertEquals(Short.MIN_VALUE, ProtoBufParser.convertLong(Short.MIN_VALUE));
13698- assertEquals(Short.MAX_VALUE, ProtoBufParser.convertLong(Short.MAX_VALUE));
13699- assertEquals(Integer.MAX_VALUE, ProtoBufParser.convertLong(Integer.MAX_VALUE));
13700- assertEquals(Integer.MIN_VALUE, ProtoBufParser.convertLong(Integer.MIN_VALUE));
13701- assertEquals(Long.MIN_VALUE, ProtoBufParser.convertLong(Long.MIN_VALUE));
13702- assertEquals(Long.MAX_VALUE, ProtoBufParser.convertLong(Long.MAX_VALUE));
13703- }
13704-
13705- /**
13706- * Check that zig zags are appropriately encoded.
13707- */
13708- @Test
13709- void testEncodeZigZag() {
13710- assertEquals(0, ProtoBufParser.encodeZigZag(0).byteValue());
13711- assertEquals(1, ProtoBufParser.encodeZigZag(-1).byteValue());
13712- assertEquals(2, ProtoBufParser.encodeZigZag(1).byteValue());
13713- assertEquals(3, ProtoBufParser.encodeZigZag(-2).byteValue());
13714- assertEquals(254, ProtoBufParser.encodeZigZag(Byte.MAX_VALUE).shortValue());
13715- assertEquals(255, ProtoBufParser.encodeZigZag(Byte.MIN_VALUE).shortValue());
13716- assertEquals(65_534, ProtoBufParser.encodeZigZag(Short.MAX_VALUE).intValue());
13717- assertEquals(65_535, ProtoBufParser.encodeZigZag(Short.MIN_VALUE).intValue());
13718- // These integers check a possible boundary condition (the boundary between using the 32/64 bit encoding methods)
13719- assertEquals(4_294_967_292L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE - 1).longValue());
13720- assertEquals(4_294_967_293L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE + 1).longValue());
13721- assertEquals(4_294_967_294L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE).longValue());
13722- assertEquals(4_294_967_295L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE).longValue());
13723- assertEquals(4_294_967_296L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE + 1L).longValue());
13724- assertEquals(4_294_967_297L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE - 1L).longValue());
13725- }
13726-}
13727diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
13728index 043481efe..e5cd8c738 100644
13729--- a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
13730+++ b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
13731@@ -37,7 +37,7 @@ import org.junit.jupiter.api.Test;
13732 import org.junit.jupiter.api.extension.RegisterExtension;
13733
13734 /**
13735- * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord}
13736+ * Test class for {@link ProtobufParser} and {@link ProtobufRecord}
13737 *
13738 * @author Taylor Smock
13739 * @since xxx
13740@@ -67,7 +67,7 @@ class ProtoBufTest {
13741 for (int i = 0; i < bytes.length; i++) {
13742 byteArray[i] = (byte) bytes[i];
13743 }
13744- return ProtoBufParser.convertByteArray(byteArray, ProtoBufParser.VAR_INT_BYTE_SIZE);
13745+ return ProtobufParser.convertByteArray(byteArray, ProtobufParser.VAR_INT_BYTE_SIZE);
13746 }
13747
13748 /**
13749@@ -79,10 +79,10 @@ class ProtoBufTest {
13750 void testRead_14_3248_6258() throws IOException {
13751 File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "14", "3248", "6258.mvt").toFile();
13752 InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
13753- Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
13754+ Collection<ProtobufRecord> records = new ProtobufParser(inputStream).allRecords();
13755 assertEquals(2, records.size());
13756 List<Layer> layers = new ArrayList<>();
13757- for (ProtoBufRecord record : records) {
13758+ for (ProtobufRecord record : records) {
13759 if (record.getField() == Layer.LAYER_FIELD) {
13760 layers.add(new Layer(record.getBytes()));
13761 } else {
13762@@ -112,9 +112,9 @@ class ProtoBufTest {
13763 File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "openinframap", "17", "26028", "50060.pbf")
13764 .toFile();
13765 InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile);
13766- Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords();
13767+ Collection<ProtobufRecord> records = new ProtobufParser(inputStream).allRecords();
13768 List<Layer> layers = new ArrayList<>();
13769- for (ProtoBufRecord record : records) {
13770+ for (ProtobufRecord record : records) {
13771 if (record.getField() == Layer.LAYER_FIELD) {
13772 layers.add(new Layer(record.getBytes()));
13773 } else {
13774@@ -155,12 +155,12 @@ class ProtoBufTest {
13775
13776 @Test
13777 void testReadVarInt() {
13778- assertEquals(ProtoBufParser.convertLong(0), bytesToVarInt(0x0));
13779- assertEquals(ProtoBufParser.convertLong(1), bytesToVarInt(0x1));
13780- assertEquals(ProtoBufParser.convertLong(127), bytesToVarInt(0x7f));
13781+ assertEquals(ProtobufParser.convertLong(0), bytesToVarInt(0x0));
13782+ assertEquals(ProtobufParser.convertLong(1), bytesToVarInt(0x1));
13783+ assertEquals(ProtobufParser.convertLong(127), bytesToVarInt(0x7f));
13784 // This should b 0xff 0xff 0xff 0xff 0x07, but we drop the leading bit when reading to a byte array
13785 Number actual = bytesToVarInt(0x7f, 0x7f, 0x7f, 0x7f, 0x07);
13786- assertEquals(ProtoBufParser.convertLong(Integer.MAX_VALUE), actual,
13787+ assertEquals(ProtobufParser.convertLong(Integer.MAX_VALUE), actual,
13788 MessageFormat.format("Expected {0} but got {1}", Integer.toBinaryString(Integer.MAX_VALUE),
13789 Long.toBinaryString(actual.longValue())));
13790 }
13791@@ -173,21 +173,21 @@ class ProtoBufTest {
13792 */
13793 @Test
13794 void testSimpleMessage() throws IOException {
13795- ProtoBufParser parser = new ProtoBufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01});
13796- ProtoBufRecord record = new ProtoBufRecord(parser);
13797+ ProtobufParser parser = new ProtobufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01});
13798+ ProtobufRecord record = new ProtobufRecord(parser);
13799 assertEquals(WireType.VARINT, record.getType());
13800 assertEquals(150, record.asUnsignedVarInt().intValue());
13801 }
13802
13803 @Test
13804 void testSingletonMultiPoint() throws IOException {
13805- Collection<ProtoBufRecord> records = new ProtoBufParser(new ByteArrayInputStream(toByteArray(
13806+ Collection<ProtobufRecord> records = new ProtobufParser(new ByteArrayInputStream(toByteArray(
13807 new int[] {0x1a, 0x2c, 0x78, 0x02, 0x0a, 0x03, 0x74, 0x6d, 0x70, 0x28, 0x80, 0x20, 0x1a, 0x04, 0x6e,
13808 0x61, 0x6d, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x54, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x61, 0x6d, 0x65,
13809 0x12, 0x0d, 0x18, 0x01, 0x12, 0x02, 0x00, 0x00, 0x22, 0x05, 0x09, 0xe0, 0x3e, 0x84, 0x27})))
13810 .allRecords();
13811 List<Layer> layers = new ArrayList<>();
13812- for (ProtoBufRecord record : records) {
13813+ for (ProtobufRecord record : records) {
13814 if (record.getField() == Layer.LAYER_FIELD) {
13815 layers.add(new Layer(record.getBytes()));
13816 } else {
13817@@ -203,9 +203,9 @@ class ProtoBufTest {
13818
13819 @Test
13820 void testZigZag() {
13821- assertEquals(0, ProtoBufParser.decodeZigZag(0).intValue());
13822- assertEquals(-1, ProtoBufParser.decodeZigZag(1).intValue());
13823- assertEquals(1, ProtoBufParser.decodeZigZag(2).intValue());
13824- assertEquals(-2, ProtoBufParser.decodeZigZag(3).intValue());
13825+ assertEquals(0, ProtobufParser.decodeZigZag(0).intValue());
13826+ assertEquals(-1, ProtobufParser.decodeZigZag(1).intValue());
13827+ assertEquals(1, ProtobufParser.decodeZigZag(2).intValue());
13828+ assertEquals(-2, ProtobufParser.decodeZigZag(3).intValue());
13829 }
13830 }
13831diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufParserTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufParserTest.java
13832new file mode 100644
13833index 000000000..201644076
13834--- /dev/null
13835+++ b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufParserTest.java
13836@@ -0,0 +1,51 @@
13837+// License: GPL. For details, see LICENSE file.
13838+package org.openstreetmap.josm.data.protobuf;
13839+
13840+import static org.junit.jupiter.api.Assertions.assertEquals;
13841+
13842+import org.junit.jupiter.api.Test;
13843+
13844+/**
13845+ * Test class for {@link ProtobufParser}
13846+ * @author Taylor Smock
13847+ * @since xxx
13848+ */
13849+class ProtobufParserTest {
13850+ /**
13851+ * Check that we are appropriately converting values to the "smallest" type
13852+ */
13853+ @Test
13854+ void testConvertLong() {
13855+ // No casting due to auto conversions
13856+ assertEquals(Byte.MAX_VALUE, ProtobufParser.convertLong(Byte.MAX_VALUE));
13857+ assertEquals(Byte.MIN_VALUE, ProtobufParser.convertLong(Byte.MIN_VALUE));
13858+ assertEquals(Short.MIN_VALUE, ProtobufParser.convertLong(Short.MIN_VALUE));
13859+ assertEquals(Short.MAX_VALUE, ProtobufParser.convertLong(Short.MAX_VALUE));
13860+ assertEquals(Integer.MAX_VALUE, ProtobufParser.convertLong(Integer.MAX_VALUE));
13861+ assertEquals(Integer.MIN_VALUE, ProtobufParser.convertLong(Integer.MIN_VALUE));
13862+ assertEquals(Long.MIN_VALUE, ProtobufParser.convertLong(Long.MIN_VALUE));
13863+ assertEquals(Long.MAX_VALUE, ProtobufParser.convertLong(Long.MAX_VALUE));
13864+ }
13865+
13866+ /**
13867+ * Check that zig zags are appropriately encoded.
13868+ */
13869+ @Test
13870+ void testEncodeZigZag() {
13871+ assertEquals(0, ProtobufParser.encodeZigZag(0).byteValue());
13872+ assertEquals(1, ProtobufParser.encodeZigZag(-1).byteValue());
13873+ assertEquals(2, ProtobufParser.encodeZigZag(1).byteValue());
13874+ assertEquals(3, ProtobufParser.encodeZigZag(-2).byteValue());
13875+ assertEquals(254, ProtobufParser.encodeZigZag(Byte.MAX_VALUE).shortValue());
13876+ assertEquals(255, ProtobufParser.encodeZigZag(Byte.MIN_VALUE).shortValue());
13877+ assertEquals(65_534, ProtobufParser.encodeZigZag(Short.MAX_VALUE).intValue());
13878+ assertEquals(65_535, ProtobufParser.encodeZigZag(Short.MIN_VALUE).intValue());
13879+ // These integers check a possible boundary condition (the boundary between using the 32/64 bit encoding methods)
13880+ assertEquals(4_294_967_292L, ProtobufParser.encodeZigZag(Integer.MAX_VALUE - 1).longValue());
13881+ assertEquals(4_294_967_293L, ProtobufParser.encodeZigZag(Integer.MIN_VALUE + 1).longValue());
13882+ assertEquals(4_294_967_294L, ProtobufParser.encodeZigZag(Integer.MAX_VALUE).longValue());
13883+ assertEquals(4_294_967_295L, ProtobufParser.encodeZigZag(Integer.MIN_VALUE).longValue());
13884+ assertEquals(4_294_967_296L, ProtobufParser.encodeZigZag(Integer.MAX_VALUE + 1L).longValue());
13885+ assertEquals(4_294_967_297L, ProtobufParser.encodeZigZag(Integer.MIN_VALUE - 1L).longValue());
13886+ }
13887+}
13888diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java
13889similarity index 67%
13890rename from test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java
13891rename to test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java
13892index d0e204c6a..6573d36fc 100644
13893--- a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java
13894+++ b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java
13895@@ -9,21 +9,21 @@ import java.io.IOException;
13896 import org.junit.jupiter.api.Test;
13897
13898 /**
13899- * Test class for specific {@link ProtoBufRecord} functionality
13900+ * Test class for specific {@link ProtobufRecord} functionality
13901 */
13902-class ProtoBufRecordTest {
13903+class ProtobufRecordTest {
13904 @Test
13905 void testFixed32() throws IOException {
13906- ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f}));
13907- ProtoBufRecord thirtyTwoBit = new ProtoBufRecord(parser);
13908+ ProtobufParser parser = new ProtobufParser(ProtoBufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f}));
13909+ ProtobufRecord thirtyTwoBit = new ProtobufRecord(parser);
13910 assertEquals(WireType.THIRTY_TWO_BIT, thirtyTwoBit.getType());
13911 assertEquals(1f, thirtyTwoBit.asFloat());
13912 }
13913
13914 @Test
13915 void testUnknown() throws IOException {
13916- ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f}));
13917- ProtoBufRecord unknown = new ProtoBufRecord(parser);
13918+ ProtobufParser parser = new ProtobufParser(ProtoBufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f}));
13919+ ProtobufRecord unknown = new ProtobufRecord(parser);
13920 assertEquals(WireType.UNKNOWN, unknown.getType());
13921 assertEquals(0, unknown.getBytes().length);
13922 }
13923--
13924GitLab
13925
13926
13927From 451f7da6f1ebe81be20a7055b0f91635ed35173d Mon Sep 17 00:00:00 2001
13928From: Taylor Smock <tsmock@fb.com>
13929Date: Mon, 3 May 2021 07:34:45 -0600
13930Subject: [PATCH 33/50] Vector Primitives: Return immutable bboxes
13931
13932Signed-off-by: Taylor Smock <tsmock@fb.com>
13933---
13934 src/org/openstreetmap/josm/data/vector/VectorNode.java | 2 +-
13935 .../openstreetmap/josm/data/vector/VectorRelation.java | 9 +++++----
13936 src/org/openstreetmap/josm/data/vector/VectorWay.java | 9 +++++----
13937 3 files changed, 11 insertions(+), 9 deletions(-)
13938
13939diff --git a/src/org/openstreetmap/josm/data/vector/VectorNode.java b/src/org/openstreetmap/josm/data/vector/VectorNode.java
13940index 60aecd8ff..07e9e512f 100644
13941--- a/src/org/openstreetmap/josm/data/vector/VectorNode.java
13942+++ b/src/org/openstreetmap/josm/data/vector/VectorNode.java
13943@@ -103,7 +103,7 @@ public class VectorNode extends VectorPrimitive implements INode {
13944
13945 @Override
13946 public BBox getBBox() {
13947- return new BBox(this.lon, this.lat);
13948+ return new BBox(this.lon, this.lat).toImmutable();
13949 }
13950
13951 @Override
13952diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelation.java b/src/org/openstreetmap/josm/data/vector/VectorRelation.java
13953index 0deb57e57..bf30365a9 100644
13954--- a/src/org/openstreetmap/josm/data/vector/VectorRelation.java
13955+++ b/src/org/openstreetmap/josm/data/vector/VectorRelation.java
13956@@ -43,13 +43,14 @@ public class VectorRelation extends VectorPrimitive implements IRelation<VectorR
13957
13958 @Override
13959 public BBox getBBox() {
13960- if (cachedBBox == null) {
13961- cachedBBox = new BBox();
13962+ if (this.cachedBBox == null) {
13963+ BBox tBBox = new BBox();
13964 for (IPrimitive member : this.getMemberPrimitivesList()) {
13965- cachedBBox.add(member.getBBox());
13966+ tBBox.add(member.getBBox());
13967 }
13968+ this.cachedBBox = tBBox.toImmutable();
13969 }
13970- return cachedBBox;
13971+ return this.cachedBBox;
13972 }
13973
13974 protected void addRelationMember(VectorRelationMember member) {
13975diff --git a/src/org/openstreetmap/josm/data/vector/VectorWay.java b/src/org/openstreetmap/josm/data/vector/VectorWay.java
13976index 582fca2d4..98fe9aa22 100644
13977--- a/src/org/openstreetmap/josm/data/vector/VectorWay.java
13978+++ b/src/org/openstreetmap/josm/data/vector/VectorWay.java
13979@@ -44,13 +44,14 @@ public class VectorWay extends VectorPrimitive implements IWay<VectorNode> {
13980
13981 @Override
13982 public BBox getBBox() {
13983- if (cachedBBox == null) {
13984- cachedBBox = new BBox();
13985+ if (this.cachedBBox == null) {
13986+ BBox tBBox = new BBox();
13987 for (INode node : this.getNodes()) {
13988- cachedBBox.add(node.getBBox());
13989+ tBBox.add(node.getBBox());
13990 }
13991+ this.cachedBBox = tBBox.toImmutable();
13992 }
13993- return cachedBBox;
13994+ return this.cachedBBox;
13995 }
13996
13997 @Override
13998--
13999GitLab
14000
14001
14002From 6856853d21541edf436d11683590dfd84f6fe08e Mon Sep 17 00:00:00 2001
14003From: Taylor Smock <tsmock@fb.com>
14004Date: Mon, 3 May 2021 07:49:21 -0600
14005Subject: [PATCH 34/50] VectorTile Style: Layers: Use StyleKeys for common
14006 constants
14007
14008Signed-off-by: Taylor Smock <tsmock@fb.com>
14009---
14010 .../vectortile/mapbox/style/Layers.java | 51 ++++++++++---------
14011 1 file changed, 27 insertions(+), 24 deletions(-)
14012
14013diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
14014index d6e55972a..b76389cf8 100644
14015--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
14016+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
14017@@ -1,6 +1,8 @@
14018 // License: GPL. For details, see LICENSE file.
14019 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
14020
14021+import org.openstreetmap.josm.gui.mappaint.StyleKeys;
14022+
14023 import java.awt.Font;
14024 import java.awt.GraphicsEnvironment;
14025 import java.text.MessageFormat;
14026@@ -58,6 +60,7 @@ public class Layers {
14027 private static final String EMPTY_STRING = "";
14028 private static final char SEMI_COLON = ';';
14029 private static final Pattern CURLY_BRACES = Pattern.compile("(\\{(.*?)})");
14030+ private static final String PAINT = "paint";
14031
14032 /** A required unique layer name */
14033 private final String id;
14034@@ -110,8 +113,8 @@ public class Layers {
14035 } else {
14036 this.source = layerInfo.getString("source");
14037 }
14038- if (layerInfo.containsKey("paint") && layerInfo.get("paint").getValueType() == JsonValue.ValueType.OBJECT) {
14039- final JsonObject paintObject = layerInfo.getJsonObject("paint");
14040+ if (layerInfo.containsKey(PAINT) && layerInfo.get(PAINT).getValueType() == JsonValue.ValueType.OBJECT) {
14041+ final JsonObject paintObject = layerInfo.getJsonObject(PAINT);
14042 final JsonObject layoutObject = layerInfo.getOrDefault("layout", JsonValue.EMPTY_JSON_OBJECT).asJsonObject();
14043 // Don't throw exceptions here, since we may just point at the styling
14044 if ("visible".equalsIgnoreCase(layoutObject.getString("visibility", "visible"))) {
14045@@ -177,15 +180,15 @@ public class Layers {
14046 // line-blur, default 0 (px)
14047 // line-color, default #000000, disabled by line-pattern
14048 final String color = paintObject.getString("line-color", "#000000");
14049- sb.append("color:").append(color).append(SEMI_COLON);
14050+ sb.append(StyleKeys.COLOR).append(':').append(color).append(SEMI_COLON);
14051 // line-opacity, default 1 (0-1)
14052 final JsonNumber opacity = paintObject.getJsonNumber("line-opacity");
14053 if (opacity != null) {
14054- sb.append("opacity:").append(opacity.numberValue().doubleValue()).append(SEMI_COLON);
14055+ sb.append(StyleKeys.OPACITY).append(':').append(opacity.numberValue().doubleValue()).append(SEMI_COLON);
14056 }
14057 // line-cap, default butt (butt|round|square)
14058 final String cap = layoutObject.getString("line-cap", "butt");
14059- sb.append("linecap:");
14060+ sb.append(StyleKeys.LINECAP).append(':');
14061 switch (cap) {
14062 case "round":
14063 case "square":
14064@@ -200,7 +203,7 @@ public class Layers {
14065 // line-dasharray, array of number >= 0, units in line widths, disabled by line-pattern
14066 if (paintObject.containsKey("line-dasharray")) {
14067 final JsonArray dashArray = paintObject.getJsonArray("line-dasharray");
14068- sb.append("dashes:");
14069+ sb.append(StyleKeys.DASHES).append(':');
14070 sb.append(dashArray.stream().filter(JsonNumber.class::isInstance).map(JsonNumber.class::cast)
14071 .map(JsonNumber::toString).collect(Collectors.joining(",")));
14072 sb.append(SEMI_COLON);
14073@@ -217,7 +220,7 @@ public class Layers {
14074 // line-translate-anchor
14075 // line-width
14076 final JsonNumber width = paintObject.getJsonNumber("line-width");
14077- sb.append("width:").append(width == null ? 1 : width.toString()).append(SEMI_COLON);
14078+ sb.append(StyleKeys.WIDTH).append(':').append(width == null ? 1 : width.toString()).append(SEMI_COLON);
14079 return sb.toString();
14080 }
14081
14082@@ -341,12 +344,12 @@ public class Layers {
14083 // text-allow-overlap
14084 // text-anchor
14085 // text-color
14086- if (paintObject.containsKey("text-color")) {
14087- sb.append("text-color:").append(paintObject.getString("text-color")).append(SEMI_COLON);
14088+ if (paintObject.containsKey(StyleKeys.TEXT_COLOR)) {
14089+ sb.append(StyleKeys.TEXT_COLOR).append(':').append(paintObject.getString(StyleKeys.TEXT_COLOR)).append(SEMI_COLON);
14090 }
14091 // text-field
14092 if (layoutObject.containsKey("text-field")) {
14093- sb.append("text:")
14094+ sb.append(StyleKeys.TEXT).append(':')
14095 .append(layoutObject.getString("text-field").replace("}", EMPTY_STRING).replace("{", EMPTY_STRING))
14096 .append(SEMI_COLON);
14097 }
14098@@ -365,9 +368,9 @@ public class Layers {
14099 .orElseGet(() -> fontMatches.stream().filter(font -> font.getPSName().equals(fontString)).findAny()
14100 .orElseGet(() -> fontMatches.stream().filter(font -> font.getFamily().equals(fontString)).findAny().orElse(null))));
14101 if (setFont != null) {
14102- sb.append("font-family:\"").append(setFont.getFamily()).append('"').append(SEMI_COLON);
14103- sb.append("font-weight:").append(setFont.isBold() ? "bold" : "normal").append(SEMI_COLON);
14104- sb.append("font-style:").append(setFont.isItalic() ? "italic" : "normal").append(SEMI_COLON);
14105+ sb.append(StyleKeys.FONT_FAMILY).append(':').append('"').append(setFont.getFamily()).append('"').append(SEMI_COLON);
14106+ sb.append(StyleKeys.FONT_WEIGHT).append(':').append(setFont.isBold() ? "bold" : "normal").append(SEMI_COLON);
14107+ sb.append(StyleKeys.FONT_STYLE).append(':').append(setFont.isItalic() ? "italic" : "normal").append(SEMI_COLON);
14108 break;
14109 }
14110 }
14111@@ -375,12 +378,12 @@ public class Layers {
14112 }
14113 // text-halo-blur
14114 // text-halo-color
14115- if (paintObject.containsKey("text-halo-color")) {
14116- sb.append("text-halo-color:").append(paintObject.getString("text-halo-color")).append(SEMI_COLON);
14117+ if (paintObject.containsKey(StyleKeys.TEXT_HALO_COLOR)) {
14118+ sb.append(StyleKeys.TEXT_HALO_COLOR).append(':').append(paintObject.getString(StyleKeys.TEXT_HALO_COLOR)).append(SEMI_COLON);
14119 }
14120 // text-halo-width
14121 if (paintObject.containsKey("text-halo-width")) {
14122- sb.append("text-halo-radius:").append(paintObject.getJsonNumber("text-halo-width").intValue()).append(SEMI_COLON);
14123+ sb.append(StyleKeys.TEXT_HALO_RADIUS).append(':').append(2 * paintObject.getJsonNumber("text-halo-width").intValue()).append(SEMI_COLON);
14124 }
14125 // text-ignore-placement
14126 // text-justify
14127@@ -391,8 +394,8 @@ public class Layers {
14128 // text-max-width
14129 // text-offset
14130 // text-opacity
14131- if (paintObject.containsKey("text-opacity")) {
14132- sb.append("text-opacity:").append(paintObject.getJsonNumber("text-opacity").doubleValue()).append(SEMI_COLON);
14133+ if (paintObject.containsKey(StyleKeys.TEXT_OPACITY)) {
14134+ sb.append(StyleKeys.TEXT_OPACITY).append(':').append(paintObject.getJsonNumber(StyleKeys.TEXT_OPACITY).doubleValue()).append(SEMI_COLON);
14135 }
14136 // text-optional
14137 // text-padding
14138@@ -402,7 +405,7 @@ public class Layers {
14139 // text-rotation-alignment
14140 // text-size
14141 final JsonNumber textSize = layoutObject.getJsonNumber("text-size");
14142- sb.append("font-size:").append(textSize != null ? textSize.numberValue().toString() : "16").append(SEMI_COLON);
14143+ sb.append(StyleKeys.FONT_SIZE).append(':').append(textSize != null ? textSize.numberValue().toString() : "16").append(SEMI_COLON);
14144 // text-transform
14145 // text-translate
14146 // text-translate-anchor
14147@@ -416,7 +419,7 @@ public class Layers {
14148 // background-color
14149 final String bgColor = paintObject.getString("background-color", null);
14150 if (bgColor != null) {
14151- sb.append("fill-color:").append(bgColor).append(SEMI_COLON);
14152+ sb.append(StyleKeys.FILL_COLOR).append(':').append(bgColor).append(SEMI_COLON);
14153 }
14154 // background-opacity
14155 // background-pattern
14156@@ -427,12 +430,12 @@ public class Layers {
14157 StringBuilder sb = new StringBuilder(50)
14158 // fill-antialias
14159 // fill-color
14160- .append("fill-color:").append(paintObject.getString("fill-color", "#000000")).append(SEMI_COLON);
14161+ .append(StyleKeys.FILL_COLOR).append(':').append(paintObject.getString(StyleKeys.FILL_COLOR, "#000000")).append(SEMI_COLON);
14162 // fill-opacity
14163- final JsonNumber opacity = paintObject.getJsonNumber("fill-opacity");
14164- sb.append("fill-opacity:").append(opacity != null ? opacity.numberValue().toString() : "1").append(SEMI_COLON)
14165+ final JsonNumber opacity = paintObject.getJsonNumber(StyleKeys.FILL_OPACITY);
14166+ sb.append(StyleKeys.FILL_OPACITY).append(':').append(opacity != null ? opacity.numberValue().toString() : "1").append(SEMI_COLON)
14167 // fill-outline-color
14168- .append("color:").append(paintObject.getString("fill-outline-color",
14169+ .append(StyleKeys.COLOR).append(':').append(paintObject.getString("fill-outline-color",
14170 paintObject.getString("fill-color", "#000000"))).append(SEMI_COLON);
14171 // fill-pattern
14172 // fill-sort-key
14173--
14174GitLab
14175
14176
14177From 6241b4fe6c0530ea855655551da194c16401344b Mon Sep 17 00:00:00 2001
14178From: Taylor Smock <tsmock@fb.com>
14179Date: Mon, 3 May 2021 08:14:14 -0600
14180Subject: [PATCH 35/50] VectorDataSet: Remove cast in getPrimitives
14181
14182Signed-off-by: Taylor Smock <tsmock@fb.com>
14183---
14184 .../openstreetmap/josm/data/vector/VectorDataSet.java | 11 +++++------
14185 1 file changed, 5 insertions(+), 6 deletions(-)
14186
14187diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
14188index 1541d169b..9ea4aaa73 100644
14189--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
14190+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
14191@@ -238,18 +238,17 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
14192 }
14193
14194 @Override
14195- public <T extends VectorPrimitive> Collection<T> getPrimitives(
14196- Predicate<? super VectorPrimitive> predicate) {
14197- return tryRead(this.readWriteLock, () -> {
14198+ public <T extends VectorPrimitive> Collection<T> getPrimitives(Predicate<? super VectorPrimitive> predicate) {
14199+ Collection<VectorPrimitive> primitives = tryRead(this.readWriteLock, () -> {
14200 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
14201 if (dataStore == null) {
14202 return null;
14203 }
14204+ return dataStore.stream().map(MVTTile::getData)
14205+ .map(VectorDataStore::getAllPrimitives).flatMap(Collection::stream).distinct().collect(Collectors.toList());
14206
14207- // This cast is needed (otherwise, Collections.emptyList doesn't compile)
14208- return (Collection<T>) new SubclassFilteredCollection<>(dataStore.stream().map(MVTTile::getData)
14209- .map(VectorDataStore::getAllPrimitives).flatMap(Collection::stream).distinct().collect(Collectors.toList()), predicate);
14210 }).orElseGet(Collections::emptyList);
14211+ return new SubclassFilteredCollection<>(primitives, predicate);
14212 }
14213
14214 @Override
14215--
14216GitLab
14217
14218
14219From 5d2e8ae0d89e0a2c21ac76a9eee5fbe5bc09985d Mon Sep 17 00:00:00 2001
14220From: Taylor Smock <tsmock@fb.com>
14221Date: Mon, 3 May 2021 08:42:19 -0600
14222Subject: [PATCH 36/50] FIXUP: Line length and some IDE issues
14223
14224Signed-off-by: Taylor Smock <tsmock@fb.com>
14225---
14226 .../josm/data/cache/JCSCachedTileLoaderJob.java | 1 -
14227 .../josm/data/imagery/vectortile/mapbox/style/Layers.java | 6 ++++--
14228 .../openstreetmap/josm/data/vector/VectorPrimitive.java | 7 +++----
14229 3 files changed, 7 insertions(+), 7 deletions(-)
14230
14231diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
14232index d07dff6d1..f8603239a 100644
14233--- a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
14234+++ b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
14235@@ -28,7 +28,6 @@ import org.openstreetmap.josm.tools.HttpClient;
14236 import org.openstreetmap.josm.tools.Logging;
14237 import org.openstreetmap.josm.tools.Utils;
14238
14239-import org.apache.commons.compress.utils.IOUtils;
14240 import org.apache.commons.jcs3.access.behavior.ICacheAccess;
14241 import org.apache.commons.jcs3.engine.behavior.ICacheElement;
14242
14243diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
14244index b76389cf8..ef6bae625 100644
14245--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
14246+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
14247@@ -383,7 +383,8 @@ public class Layers {
14248 }
14249 // text-halo-width
14250 if (paintObject.containsKey("text-halo-width")) {
14251- sb.append(StyleKeys.TEXT_HALO_RADIUS).append(':').append(2 * paintObject.getJsonNumber("text-halo-width").intValue()).append(SEMI_COLON);
14252+ sb.append(StyleKeys.TEXT_HALO_RADIUS).append(':').append(2 * paintObject.getJsonNumber("text-halo-width").intValue())
14253+ .append(SEMI_COLON);
14254 }
14255 // text-ignore-placement
14256 // text-justify
14257@@ -395,7 +396,8 @@ public class Layers {
14258 // text-offset
14259 // text-opacity
14260 if (paintObject.containsKey(StyleKeys.TEXT_OPACITY)) {
14261- sb.append(StyleKeys.TEXT_OPACITY).append(':').append(paintObject.getJsonNumber(StyleKeys.TEXT_OPACITY).doubleValue()).append(SEMI_COLON);
14262+ sb.append(StyleKeys.TEXT_OPACITY).append(':').append(paintObject.getJsonNumber(StyleKeys.TEXT_OPACITY).doubleValue())
14263+ .append(SEMI_COLON);
14264 }
14265 // text-optional
14266 // text-padding
14267diff --git a/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
14268index ed9c93937..114b61730 100644
14269--- a/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
14270+++ b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
14271@@ -67,17 +67,16 @@ public abstract class VectorPrimitive extends AbstractPrimitive implements DataL
14272
14273 @Override
14274 public VectorDataSet getDataSet() {
14275- return this.dataSet;
14276+ return dataSet;
14277 }
14278
14279- protected void setDataSet(VectorDataSet dataSet) {
14280- this.dataSet = dataSet;
14281+ protected void setDataSet(VectorDataSet newDataSet) {
14282+ dataSet = newDataSet;
14283 }
14284
14285 /*----------
14286 * MAPPAINT
14287 *--------*/
14288- private short mappaintCacheIdx;
14289
14290 @Override
14291 public final StyleCache getCachedStyle() {
14292--
14293GitLab
14294
14295
14296From 6314c7c5bccaf2f9b6a6f20ed95a15c9cf1a9602 Mon Sep 17 00:00:00 2001
14297From: Taylor Smock <tsmock@fb.com>
14298Date: Tue, 4 May 2021 09:50:02 -0600
14299Subject: [PATCH 37/50] FilterWorker: Make it generic
14300
14301Currently, Filters do not appear to be used in any plugin (sans
14302Mapillary). This means that now is a good time to generify the filters,
14303as we add VectorPrimitives, which we will eventually want to filter.
14304
14305Signed-off-by: Taylor Smock <tsmock@fb.com>
14306---
14307 .../josm/data/osm/AbstractPrimitive.java | 50 ++++++++++++++++-
14308 .../josm/data/osm/FilterMatcher.java | 42 ++++++++-------
14309 .../josm/data/osm/FilterWorker.java | 31 ++++++-----
14310 .../josm/data/osm/IFilterablePrimitive.java | 51 ++++++++++++++++++
14311 .../josm/data/osm/OsmPrimitive.java | 53 ++-----------------
14312 5 files changed, 146 insertions(+), 81 deletions(-)
14313 create mode 100644 src/org/openstreetmap/josm/data/osm/IFilterablePrimitive.java
14314
14315diff --git a/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java b/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java
14316index e4ffa73e0..c7da8ecc3 100644
14317--- a/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java
14318+++ b/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java
14319@@ -31,7 +31,7 @@ import org.openstreetmap.josm.tools.Utils;
14320 *
14321 * @since 4099
14322 */
14323-public abstract class AbstractPrimitive implements IPrimitive {
14324+public abstract class AbstractPrimitive implements IPrimitive, IFilterablePrimitive {
14325
14326 /**
14327 * This flag shows, that the properties have been changed by the user
14328@@ -352,6 +352,18 @@ public abstract class AbstractPrimitive implements IPrimitive {
14329 }
14330 }
14331
14332+ /**
14333+ * Update flags
14334+ * @param flag The flag to update
14335+ * @param value The value to set
14336+ * @return {@code true} if the flags have changed
14337+ */
14338+ protected boolean updateFlagsChanged(short flag, boolean value) {
14339+ int oldFlags = flags;
14340+ updateFlags(flag, value);
14341+ return oldFlags != flags;
14342+ }
14343+
14344 @Override
14345 public void setModified(boolean modified) {
14346 updateFlags(FLAG_MODIFIED, modified);
14347@@ -409,6 +421,42 @@ public abstract class AbstractPrimitive implements IPrimitive {
14348 return (flags & FLAG_INCOMPLETE) != 0;
14349 }
14350
14351+ @Override
14352+ public boolean getHiddenType() {
14353+ return (flags & FLAG_HIDDEN_TYPE) != 0;
14354+ }
14355+
14356+ @Override
14357+ public boolean getDisabledType() {
14358+ return (flags & FLAG_DISABLED_TYPE) != 0;
14359+ }
14360+
14361+ @Override
14362+ public boolean setDisabledState(boolean hidden) {
14363+ // Store as variables to avoid short circuit boolean return
14364+ final boolean flagDisabled = updateFlagsChanged(FLAG_DISABLED, true);
14365+ final boolean flagHideIfDisabled = updateFlagsChanged(FLAG_HIDE_IF_DISABLED, hidden);
14366+ return flagDisabled || flagHideIfDisabled;
14367+ }
14368+
14369+ @Override
14370+ public boolean unsetDisabledState() {
14371+ // Store as variables to avoid short circuit boolean return
14372+ final boolean flagDisabled = updateFlagsChanged(FLAG_DISABLED, false);
14373+ final boolean flagHideIfDisabled = updateFlagsChanged(FLAG_HIDE_IF_DISABLED, false);
14374+ return flagDisabled || flagHideIfDisabled;
14375+ }
14376+
14377+ @Override
14378+ public void setDisabledType(boolean isExplicit) {
14379+ updateFlags(FLAG_DISABLED_TYPE, isExplicit);
14380+ }
14381+
14382+ @Override
14383+ public void setHiddenType(boolean isExplicit) {
14384+ updateFlags(FLAG_HIDDEN_TYPE, isExplicit);
14385+ }
14386+
14387 protected String getFlagsAsString() {
14388 StringBuilder builder = new StringBuilder();
14389
14390diff --git a/src/org/openstreetmap/josm/data/osm/FilterMatcher.java b/src/org/openstreetmap/josm/data/osm/FilterMatcher.java
14391index a7cb334ee..5cdb25a90 100644
14392--- a/src/org/openstreetmap/josm/data/osm/FilterMatcher.java
14393+++ b/src/org/openstreetmap/josm/data/osm/FilterMatcher.java
14394@@ -161,23 +161,25 @@ public class FilterMatcher {
14395 * @return when hidden is true, returns whether the primitive is hidden
14396 * when hidden is false, returns whether the primitive is disabled or hidden
14397 */
14398- private static boolean isFiltered(OsmPrimitive primitive, boolean hidden) {
14399+ private static boolean isFiltered(IPrimitive primitive, boolean hidden) {
14400 return hidden ? primitive.isDisabledAndHidden() : primitive.isDisabled();
14401 }
14402
14403 /**
14404 * Check if primitive is hidden explicitly.
14405 * Only used for ways and relations.
14406+ * @param <T> The primitive type
14407 * @param primitive the primitive to check
14408 * @param hidden the level where the check is performed
14409 * @return true, if at least one non-inverted filter applies to the primitive
14410 */
14411- private static boolean isFilterExplicit(OsmPrimitive primitive, boolean hidden) {
14412+ private static <T extends IFilterablePrimitive> boolean isFilterExplicit(T primitive, boolean hidden) {
14413 return hidden ? primitive.getHiddenType() : primitive.getDisabledType();
14414 }
14415
14416 /**
14417 * Check if all parent ways are filtered.
14418+ * @param <T> The primitive type
14419 * @param primitive the primitive to check
14420 * @param hidden parameter that indicates the minimum level of filtering:
14421 * true when objects need to be hidden to count as filtered and
14422@@ -187,28 +189,28 @@ public class FilterMatcher {
14423 * parameter <code>hidden</code> and
14424 * (c) at least one of the parent ways is explicitly filtered
14425 */
14426- private static boolean allParentWaysFiltered(OsmPrimitive primitive, boolean hidden) {
14427- List<OsmPrimitive> refs = primitive.getReferrers();
14428+ private static <T extends IPrimitive & IFilterablePrimitive> boolean allParentWaysFiltered(T primitive, boolean hidden) {
14429+ List<? extends IPrimitive> refs = primitive.getReferrers();
14430 boolean isExplicit = false;
14431- for (OsmPrimitive p: refs) {
14432- if (p instanceof Way) {
14433+ for (IPrimitive p: refs) {
14434+ if (p instanceof IWay && p instanceof IFilterablePrimitive) {
14435 if (!isFiltered(p, hidden))
14436 return false;
14437- isExplicit |= isFilterExplicit(p, hidden);
14438+ isExplicit |= isFilterExplicit((IFilterablePrimitive) p, hidden);
14439 }
14440 }
14441 return isExplicit;
14442 }
14443
14444- private static boolean oneParentWayNotFiltered(OsmPrimitive primitive, boolean hidden) {
14445- return primitive.referrers(Way.class)
14446+ private static boolean oneParentWayNotFiltered(IPrimitive primitive, boolean hidden) {
14447+ return primitive.getReferrers().stream().filter(IWay.class::isInstance).map(IWay.class::cast)
14448 .anyMatch(p -> !isFiltered(p, hidden));
14449 }
14450
14451- private static boolean allParentMultipolygonsFiltered(OsmPrimitive primitive, boolean hidden) {
14452+ private static boolean allParentMultipolygonsFiltered(IPrimitive primitive, boolean hidden) {
14453 boolean isExplicit = false;
14454- for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>(
14455- primitive.getReferrers(), OsmPrimitive::isMultipolygon)) {
14456+ for (Relation r : new SubclassFilteredCollection<IPrimitive, Relation>(
14457+ primitive.getReferrers(), IPrimitive::isMultipolygon)) {
14458 if (!isFiltered(r, hidden))
14459 return false;
14460 isExplicit |= isFilterExplicit(r, hidden);
14461@@ -216,12 +218,12 @@ public class FilterMatcher {
14462 return isExplicit;
14463 }
14464
14465- private static boolean oneParentMultipolygonNotFiltered(OsmPrimitive primitive, boolean hidden) {
14466- return new SubclassFilteredCollection<OsmPrimitive, Relation>(primitive.getReferrers(), OsmPrimitive::isMultipolygon).stream()
14467+ private static boolean oneParentMultipolygonNotFiltered(IPrimitive primitive, boolean hidden) {
14468+ return new SubclassFilteredCollection<IPrimitive, IRelation>(primitive.getReferrers(), IPrimitive::isMultipolygon).stream()
14469 .anyMatch(r -> !isFiltered(r, hidden));
14470 }
14471
14472- private static FilterType test(List<FilterInfo> filters, OsmPrimitive primitive, boolean hidden) {
14473+ private static <T extends IPrimitive & IFilterablePrimitive> FilterType test(List<FilterInfo> filters, T primitive, boolean hidden) {
14474 if (primitive.isIncomplete() || primitive.isPreserved())
14475 return FilterType.NOT_FILTERED;
14476
14477@@ -245,7 +247,7 @@ public class FilterMatcher {
14478 }
14479 }
14480
14481- if (primitive instanceof Node) {
14482+ if (primitive instanceof INode) {
14483 if (filtered) {
14484 // If there is a parent way, that is not hidden, we show the
14485 // node anyway, unless there is no non-inverted filter that
14486@@ -266,7 +268,7 @@ public class FilterMatcher {
14487 else
14488 return FilterType.NOT_FILTERED;
14489 }
14490- } else if (primitive instanceof Way) {
14491+ } else if (primitive instanceof IWay) {
14492 if (filtered) {
14493 if (explicitlyFiltered)
14494 return FilterType.EXPLICIT;
14495@@ -295,6 +297,7 @@ public class FilterMatcher {
14496 * Check if primitive is hidden.
14497 * The filter flags for all parent objects must be set correctly, when
14498 * calling this method.
14499+ * @param <T> The primitive type
14500 * @param primitive the primitive
14501 * @return FilterType.NOT_FILTERED when primitive is not hidden;
14502 * FilterType.EXPLICIT when primitive is hidden and there is a non-inverted
14503@@ -302,7 +305,7 @@ public class FilterMatcher {
14504 * FilterType.PASSIV when primitive is hidden and all filters that apply
14505 * are inverted
14506 */
14507- public FilterType isHidden(OsmPrimitive primitive) {
14508+ public <T extends IPrimitive & IFilterablePrimitive> FilterType isHidden(T primitive) {
14509 return test(hiddenFilters, primitive, true);
14510 }
14511
14512@@ -310,6 +313,7 @@ public class FilterMatcher {
14513 * Check if primitive is disabled.
14514 * The filter flags for all parent objects must be set correctly, when
14515 * calling this method.
14516+ * @param <T> The primitive type
14517 * @param primitive the primitive
14518 * @return FilterType.NOT_FILTERED when primitive is not disabled;
14519 * FilterType.EXPLICIT when primitive is disabled and there is a non-inverted
14520@@ -317,7 +321,7 @@ public class FilterMatcher {
14521 * FilterType.PASSIV when primitive is disabled and all filters that apply
14522 * are inverted
14523 */
14524- public FilterType isDisabled(OsmPrimitive primitive) {
14525+ public <T extends IPrimitive & IFilterablePrimitive> FilterType isDisabled(T primitive) {
14526 return test(disabledFilters, primitive, false);
14527 }
14528
14529diff --git a/src/org/openstreetmap/josm/data/osm/FilterWorker.java b/src/org/openstreetmap/josm/data/osm/FilterWorker.java
14530index 6850c47fc..89118bdd0 100644
14531--- a/src/org/openstreetmap/josm/data/osm/FilterWorker.java
14532+++ b/src/org/openstreetmap/josm/data/osm/FilterWorker.java
14533@@ -9,7 +9,7 @@ import org.openstreetmap.josm.data.osm.search.SearchParseError;
14534 import org.openstreetmap.josm.tools.SubclassFilteredCollection;
14535
14536 /**
14537- * Class for applying {@link Filter}s to {@link OsmPrimitive}s.
14538+ * Class for applying {@link Filter}s to {@link IPrimitive}s.
14539 *
14540 * Provides a bridge between Filter GUI and the data.
14541 *
14542@@ -24,37 +24,41 @@ public final class FilterWorker {
14543 /**
14544 * Apply the filters to the primitives of the data set.
14545 *
14546+ * @param <T> The primitive type
14547 * @param all the collection of primitives for that the filter state should be updated
14548 * @param filters the filters
14549 * @return true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
14550 * @throws SearchParseError if the search expression in a filter cannot be parsed
14551- * @since 12383
14552+ * @since 12383, xxx (generics)
14553 */
14554- public static boolean executeFilters(Collection<OsmPrimitive> all, Filter... filters) throws SearchParseError {
14555+ public static <T extends IPrimitive & IFilterablePrimitive> boolean executeFilters(Collection<T> all, Filter... filters)
14556+ throws SearchParseError {
14557 return executeFilters(all, FilterMatcher.of(filters));
14558 }
14559
14560 /**
14561 * Apply the filters to the primitives of the data set.
14562 *
14563+ * @param <T> The primitive type
14564 * @param all the collection of primitives for that the filter state should be updated
14565 * @param filterMatcher the FilterMatcher
14566 * @return true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
14567+ * @since xxx (generics)
14568 */
14569- public static boolean executeFilters(Collection<OsmPrimitive> all, FilterMatcher filterMatcher) {
14570+ public static <T extends IPrimitive & IFilterablePrimitive> boolean executeFilters(Collection<T> all, FilterMatcher filterMatcher) {
14571 boolean changed;
14572 // first relations, then ways and nodes last; this is required to resolve dependencies
14573- changed = doExecuteFilters(SubclassFilteredCollection.filter(all, Relation.class::isInstance), filterMatcher);
14574- changed |= doExecuteFilters(SubclassFilteredCollection.filter(all, Way.class::isInstance), filterMatcher);
14575- changed |= doExecuteFilters(SubclassFilteredCollection.filter(all, Node.class::isInstance), filterMatcher);
14576+ changed = doExecuteFilters(SubclassFilteredCollection.filter(all, IRelation.class::isInstance), filterMatcher);
14577+ changed |= doExecuteFilters(SubclassFilteredCollection.filter(all, IWay.class::isInstance), filterMatcher);
14578+ changed |= doExecuteFilters(SubclassFilteredCollection.filter(all, INode.class::isInstance), filterMatcher);
14579 return changed;
14580 }
14581
14582- private static boolean doExecuteFilters(Collection<OsmPrimitive> all, FilterMatcher filterMatcher) {
14583+ private static <T extends IPrimitive & IFilterablePrimitive> boolean doExecuteFilters(Collection<T> all, FilterMatcher filterMatcher) {
14584
14585 boolean changed = false;
14586
14587- for (OsmPrimitive primitive: all) {
14588+ for (T primitive : all) {
14589 FilterType hiddenType = filterMatcher.isHidden(primitive);
14590 if (hiddenType != FilterType.NOT_FILTERED) {
14591 changed |= primitive.setDisabledState(true);
14592@@ -75,24 +79,27 @@ public final class FilterWorker {
14593 /**
14594 * Apply the filters to a single primitive.
14595 *
14596+ * @param <T> the primitive type
14597 * @param primitive the primitive
14598 * @param filterMatcher the FilterMatcher
14599 * @return true, if the filter state (normal / disabled / hidden)
14600 * of the primitive has changed in the process
14601+ * @since xxx (generics)
14602 */
14603- public static boolean executeFilters(OsmPrimitive primitive, FilterMatcher filterMatcher) {
14604+ public static <T extends IPrimitive & IFilterablePrimitive> boolean executeFilters(T primitive, FilterMatcher filterMatcher) {
14605 return doExecuteFilters(Collections.singleton(primitive), filterMatcher);
14606 }
14607
14608 /**
14609 * Clear all filter flags, i.e.&nbsp;turn off filters.
14610+ * @param <T> the primitive type
14611 * @param prims the primitives
14612 * @return true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
14613 * @since 12388 (signature)
14614 */
14615- public static boolean clearFilterFlags(Collection<OsmPrimitive> prims) {
14616+ public static <T extends IPrimitive & IFilterablePrimitive> boolean clearFilterFlags(Collection<T> prims) {
14617 boolean changed = false;
14618- for (OsmPrimitive osm : prims) {
14619+ for (T osm : prims) {
14620 changed |= osm.unsetDisabledState();
14621 }
14622 return changed;
14623diff --git a/src/org/openstreetmap/josm/data/osm/IFilterablePrimitive.java b/src/org/openstreetmap/josm/data/osm/IFilterablePrimitive.java
14624new file mode 100644
14625index 000000000..f8ba55cfc
14626--- /dev/null
14627+++ b/src/org/openstreetmap/josm/data/osm/IFilterablePrimitive.java
14628@@ -0,0 +1,51 @@
14629+// License: GPL. For details, see LICENSE file.
14630+package org.openstreetmap.josm.data.osm;
14631+
14632+/**
14633+ * An interface used to indicate that a primitive is filterable
14634+ * @author Taylor Smock
14635+ * @since xxx
14636+ */
14637+public interface IFilterablePrimitive {
14638+ /**
14639+ * Get binary property used internally by the filter mechanism.
14640+ * @return {@code true} if this object has the "hidden type" flag enabled
14641+ */
14642+ boolean getHiddenType();
14643+
14644+ /**
14645+ * Get binary property used internally by the filter mechanism.
14646+ * @return {@code true} if this object has the "disabled type" flag enabled
14647+ */
14648+ boolean getDisabledType();
14649+
14650+ /**
14651+ * Make the primitive disabled (e.g.&nbsp;if a filter applies).
14652+ *
14653+ * To enable the primitive again, use unsetDisabledState.
14654+ * @param hidden if the primitive should be completely hidden from view or
14655+ * just shown in gray color.
14656+ * @return true, any flag has changed; false if you try to set the disabled
14657+ * state to the value that is already preset
14658+ */
14659+ boolean setDisabledState(boolean hidden);
14660+
14661+ /**
14662+ * Remove the disabled flag from the primitive.
14663+ * Afterwards, the primitive is displayed normally and can be selected again.
14664+ * @return {@code true} if a change occurred
14665+ */
14666+ boolean unsetDisabledState();
14667+
14668+ /**
14669+ * Set binary property used internally by the filter mechanism.
14670+ * @param isExplicit new "disabled type" flag value
14671+ */
14672+ void setDisabledType(boolean isExplicit);
14673+
14674+ /**
14675+ * Set binary property used internally by the filter mechanism.
14676+ * @param isExplicit new "hidden type" flag value
14677+ */
14678+ void setHiddenType(boolean isExplicit);
14679+}
14680diff --git a/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java b/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
14681index 496751f98..3db783968 100644
14682--- a/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
14683+++ b/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
14684@@ -329,22 +329,11 @@ public abstract class OsmPrimitive extends AbstractPrimitive implements Template
14685 }
14686 }
14687
14688- /**
14689- * Make the primitive disabled (e.g.&nbsp;if a filter applies).
14690- *
14691- * To enable the primitive again, use unsetDisabledState.
14692- * @param hidden if the primitive should be completely hidden from view or
14693- * just shown in gray color.
14694- * @return true, any flag has changed; false if you try to set the disabled
14695- * state to the value that is already preset
14696- */
14697+ @Override
14698 public boolean setDisabledState(boolean hidden) {
14699 boolean locked = writeLock();
14700 try {
14701- int oldFlags = flags;
14702- updateFlagsNoLock(FLAG_DISABLED, true);
14703- updateFlagsNoLock(FLAG_HIDE_IF_DISABLED, hidden);
14704- return oldFlags != flags;
14705+ return super.setDisabledState(hidden);
14706 } finally {
14707 writeUnlock(locked);
14708 }
14709@@ -355,34 +344,16 @@ public abstract class OsmPrimitive extends AbstractPrimitive implements Template
14710 * Afterwards, the primitive is displayed normally and can be selected again.
14711 * @return {@code true} if a change occurred
14712 */
14713+ @Override
14714 public boolean unsetDisabledState() {
14715 boolean locked = writeLock();
14716 try {
14717- int oldFlags = flags;
14718- updateFlagsNoLock(FLAG_DISABLED, false);
14719- updateFlagsNoLock(FLAG_HIDE_IF_DISABLED, false);
14720- return oldFlags != flags;
14721+ return super.unsetDisabledState();
14722 } finally {
14723 writeUnlock(locked);
14724 }
14725 }
14726
14727- /**
14728- * Set binary property used internally by the filter mechanism.
14729- * @param isExplicit new "disabled type" flag value
14730- */
14731- public void setDisabledType(boolean isExplicit) {
14732- updateFlags(FLAG_DISABLED_TYPE, isExplicit);
14733- }
14734-
14735- /**
14736- * Set binary property used internally by the filter mechanism.
14737- * @param isExplicit new "hidden type" flag value
14738- */
14739- public void setHiddenType(boolean isExplicit) {
14740- updateFlags(FLAG_HIDDEN_TYPE, isExplicit);
14741- }
14742-
14743 /**
14744 * Set binary property used internally by the filter mechanism.
14745 * @param isPreserved new "preserved" flag value
14746@@ -402,22 +373,6 @@ public abstract class OsmPrimitive extends AbstractPrimitive implements Template
14747 return ((flags & FLAG_DISABLED) != 0) && ((flags & FLAG_HIDE_IF_DISABLED) != 0);
14748 }
14749
14750- /**
14751- * Get binary property used internally by the filter mechanism.
14752- * @return {@code true} if this object has the "hidden type" flag enabled
14753- */
14754- public boolean getHiddenType() {
14755- return (flags & FLAG_HIDDEN_TYPE) != 0;
14756- }
14757-
14758- /**
14759- * Get binary property used internally by the filter mechanism.
14760- * @return {@code true} if this object has the "disabled type" flag enabled
14761- */
14762- public boolean getDisabledType() {
14763- return (flags & FLAG_DISABLED_TYPE) != 0;
14764- }
14765-
14766 @Override
14767 public boolean isPreserved() {
14768 return (flags & FLAG_PRESERVED) != 0;
14769--
14770GitLab
14771
14772
14773From 337febd24fb7be52bd38cd9135815991e8b3d800 Mon Sep 17 00:00:00 2001
14774From: Taylor Smock <tsmock@fb.com>
14775Date: Tue, 4 May 2021 10:02:28 -0600
14776Subject: [PATCH 38/50] MapboxVectorTileSource: Check that a json has required
14777 keys
14778
14779Signed-off-by: Taylor Smock <tsmock@fb.com>
14780---
14781 .../vectortile/mapbox/MapboxVectorTileSource.java | 9 ++++++---
14782 1 file changed, 6 insertions(+), 3 deletions(-)
14783
14784diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
14785index 62647d1bb..18a4911a7 100644
14786--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
14787+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
14788@@ -10,6 +10,7 @@ import java.util.stream.Collectors;
14789
14790 import javax.json.Json;
14791 import javax.json.JsonException;
14792+import javax.json.JsonObject;
14793 import javax.json.JsonReader;
14794
14795 import org.openstreetmap.josm.data.imagery.ImageryInfo;
14796@@ -41,9 +42,11 @@ public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource {
14797 try (CachedFile style = new CachedFile(info.getUrl());
14798 InputStream inputStream = style.getInputStream();
14799 JsonReader reader = Json.createReader(inputStream)) {
14800- reader.readObject();
14801- // OK, we have a stylesheet
14802- mapBoxVectorStyle = MapboxVectorStyle.getMapboxVectorStyle(info.getUrl());
14803+ JsonObject object = reader.readObject();
14804+ // OK, we may have a stylesheet. "version", "layers", and "sources" are all required.
14805+ if (object.containsKey("version") && object.containsKey("layers") && object.containsKey("sources")) {
14806+ mapBoxVectorStyle = MapboxVectorStyle.getMapboxVectorStyle(info.getUrl());
14807+ }
14808 } catch (IOException | JsonException e) {
14809 Logging.trace(e);
14810 }
14811--
14812GitLab
14813
14814
14815From b6ea0250e2c114923a218c3641d301092f97680e Mon Sep 17 00:00:00 2001
14816From: Taylor Smock <tsmock@fb.com>
14817Date: Tue, 4 May 2021 10:47:57 -0600
14818Subject: [PATCH 39/50] Selection Listener: Rename interface methods
14819
14820Signed-off-by: Taylor Smock <tsmock@fb.com>
14821---
14822 .../josm/data/osm/event/IDataSelectionEventSource.java | 4 ++--
14823 src/org/openstreetmap/josm/data/vector/VectorDataSet.java | 4 ++--
14824 2 files changed, 4 insertions(+), 4 deletions(-)
14825
14826diff --git a/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java
14827index 4f1d75d18..14fe8eee5 100644
14828--- a/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java
14829+++ b/src/org/openstreetmap/josm/data/osm/event/IDataSelectionEventSource.java
14830@@ -24,12 +24,12 @@ public interface IDataSelectionEventSource<O extends IPrimitive, N extends INode
14831 * @param listener The listener to add
14832 * @return {@code true} if the listener was added
14833 */
14834- boolean addListener(IDataSelectionListener<O, N, W, R, D> listener);
14835+ boolean addSelectionListener(IDataSelectionListener<O, N, W, R, D> listener);
14836
14837 /**
14838 * Remove a listener
14839 * @param listener The listener to remove
14840 * @return {@code true} if the listener was removed
14841 */
14842- boolean removeListener(IDataSelectionListener<O, N, W, R, D> listener);
14843+ boolean removeSelectionListener(IDataSelectionListener<O, N, W, R, D> listener);
14844 }
14845diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
14846index 9ea4aaa73..b377065af 100644
14847--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
14848+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
14849@@ -637,7 +637,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
14850 }
14851
14852 @Override
14853- public boolean addListener(IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
14854+ public boolean addSelectionListener(IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
14855 if (!this.listeners.containsListener(listener)) {
14856 this.listeners.addListener(listener);
14857 }
14858@@ -645,7 +645,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
14859 }
14860
14861 @Override
14862- public boolean removeListener(IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
14863+ public boolean removeSelectionListener(IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
14864 if (this.listeners.containsListener(listener)) {
14865 this.listeners.removeListener(listener);
14866 }
14867--
14868GitLab
14869
14870
14871From 01cf96f513a58e0c9bf429046231b7d34b0e6c09 Mon Sep 17 00:00:00 2001
14872From: Taylor Smock <tsmock@fb.com>
14873Date: Tue, 4 May 2021 13:28:52 -0600
14874Subject: [PATCH 40/50] Mapbox Style: Fix text-halo-width
14875
14876* JOSM equivalent is text-halo-radius, which means text-halo-width
14877 needed to be divided by 2
14878
14879Signed-off-by: Taylor Smock <tsmock@fb.com>
14880---
14881 .../josm/data/imagery/vectortile/mapbox/style/Layers.java | 2 +-
14882 .../josm/data/imagery/vectortile/mapbox/style/LayersTest.java | 4 ++--
14883 2 files changed, 3 insertions(+), 3 deletions(-)
14884
14885diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
14886index ef6bae625..157e48961 100644
14887--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
14888+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
14889@@ -383,7 +383,7 @@ public class Layers {
14890 }
14891 // text-halo-width
14892 if (paintObject.containsKey("text-halo-width")) {
14893- sb.append(StyleKeys.TEXT_HALO_RADIUS).append(':').append(2 * paintObject.getJsonNumber("text-halo-width").intValue())
14894+ sb.append(StyleKeys.TEXT_HALO_RADIUS).append(':').append(paintObject.getJsonNumber("text-halo-width").intValue() / 2)
14895 .append(SEMI_COLON);
14896 }
14897 // text-ignore-placement
14898diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
14899index 28b09b950..5db4d5a3d 100644
14900--- a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
14901+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
14902@@ -248,7 +248,7 @@ class LayersTest {
14903 assertEquals(Layers.Type.SYMBOL, fullLineLayer.getType());
14904 assertEquals("node::random-layer-id{icon-image:concat(\"random-image\");icon-offset-x:2.0;icon-offset-y:3.0;"
14905 + "icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";font-weight:normal;"
14906- + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}", fullLineLayer.toString());
14907+ + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:8;text-opacity:0.6;font-size:25;}", fullLineLayer.toString());
14908
14909 // Test an invisible symbol
14910 Layers fullLineInvisibleLayer = new Layers(Json.createObjectBuilder()
14911@@ -272,7 +272,7 @@ class LayersTest {
14912 .build());
14913 assertEquals("node::random-layer-id{icon-image:concat(tag(\"value\"));icon-offset-x:2.0;icon-offset-y:3.0;"
14914 + "icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";font-weight:normal;"
14915- + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}",
14916+ + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:8;text-opacity:0.6;font-size:25;}",
14917 fullOneIconImagePlaceholderLineLayer.toString());
14918
14919 // Test with placeholders in icon-image
14920--
14921GitLab
14922
14923
14924From b05ac219b7fbaafd891b55924122fce4694bf589 Mon Sep 17 00:00:00 2001
14925From: Taylor Smock <tsmock@fb.com>
14926Date: Tue, 4 May 2021 13:31:00 -0600
14927Subject: [PATCH 41/50] ProtoBufTest -> ProtobufTest and 2048 -> 4096
14928
14929Signed-off-by: Taylor Smock <tsmock@fb.com>
14930---
14931 .../josm/data/protobuf/ProtobufRecordTest.java | 4 ++--
14932 .../data/protobuf/{ProtoBufTest.java => ProtobufTest.java} | 6 +++---
14933 2 files changed, 5 insertions(+), 5 deletions(-)
14934 rename test/unit/org/openstreetmap/josm/data/protobuf/{ProtoBufTest.java => ProtobufTest.java} (98%)
14935
14936diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java
14937index 6573d36fc..f99aa7e2e 100644
14938--- a/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java
14939+++ b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java
14940@@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test;
14941 class ProtobufRecordTest {
14942 @Test
14943 void testFixed32() throws IOException {
14944- ProtobufParser parser = new ProtobufParser(ProtoBufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f}));
14945+ ProtobufParser parser = new ProtobufParser(ProtobufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f}));
14946 ProtobufRecord thirtyTwoBit = new ProtobufRecord(parser);
14947 assertEquals(WireType.THIRTY_TWO_BIT, thirtyTwoBit.getType());
14948 assertEquals(1f, thirtyTwoBit.asFloat());
14949@@ -22,7 +22,7 @@ class ProtobufRecordTest {
14950
14951 @Test
14952 void testUnknown() throws IOException {
14953- ProtobufParser parser = new ProtobufParser(ProtoBufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f}));
14954+ ProtobufParser parser = new ProtobufParser(ProtobufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f}));
14955 ProtobufRecord unknown = new ProtobufRecord(parser);
14956 assertEquals(WireType.UNKNOWN, unknown.getType());
14957 assertEquals(0, unknown.getBytes().length);
14958diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufTest.java
14959similarity index 98%
14960rename from test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
14961rename to test/unit/org/openstreetmap/josm/data/protobuf/ProtobufTest.java
14962index e5cd8c738..d6f42cca8 100644
14963--- a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
14964+++ b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufTest.java
14965@@ -42,7 +42,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
14966 * @author Taylor Smock
14967 * @since xxx
14968 */
14969-class ProtoBufTest {
14970+class ProtobufTest {
14971 /**
14972 * Convert an int array into a byte array
14973 * @param intArray The int array to convert (NOTE: numbers must be below 255)
14974@@ -93,8 +93,8 @@ class ProtoBufTest {
14975 Layer mapillaryPictures = layers.get(1);
14976 assertEquals("mapillary-sequences", mapillarySequences.getName());
14977 assertEquals("mapillary-images", mapillaryPictures.getName());
14978- assertEquals(2048, mapillarySequences.getExtent());
14979- assertEquals(2048, mapillaryPictures.getExtent());
14980+ assertEquals(4096, mapillarySequences.getExtent());
14981+ assertEquals(4096, mapillaryPictures.getExtent());
14982
14983 assertEquals(1,
14984 mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 233760500).count());
14985--
14986GitLab
14987
14988
14989From 88d0f9994728aced4b449bff4db974a143c43edf Mon Sep 17 00:00:00 2001
14990From: Taylor Smock <tsmock@fb.com>
14991Date: Tue, 4 May 2021 14:10:22 -0600
14992Subject: [PATCH 42/50] FIXUP: tests were broken due to removal of dedup
14993 functionality
14994
14995Signed-off-by: Taylor Smock <tsmock@fb.com>
14996---
14997 .../josm/data/vector/VectorDataSet.java | 23 +++++++-
14998 .../josm/data/vector/VectorDataSetTest.java | 52 ++++++-------------
14999 2 files changed, 37 insertions(+), 38 deletions(-)
15000
15001diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15002index b377065af..d626156d4 100644
15003--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15004+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15005@@ -51,6 +51,8 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15006 // Note: In Java 8, computeIfAbsent is blocking for both pre-existing and new values. In Java 9, it is only blocking
15007 // for new values (perf increase). See JDK-8161372 for more info.
15008 private final Map<Integer, Storage<MVTTile>> dataStoreMap = new ConcurrentHashMap<>();
15009+ // This is for "custom" data
15010+ private final VectorDataStore customDataStore = new VectorDataStore();
15011 // Both of these listener lists are useless, since they expect OsmPrimitives at this time
15012 private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create();
15013 private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create();
15014@@ -134,9 +136,25 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15015 this.name = name;
15016 }
15017
15018+ /**
15019+ * Add a primitive to the custom data store
15020+ * @param primitive the primitive to add
15021+ */
15022 @Override
15023 public void addPrimitive(VectorPrimitive primitive) {
15024- throw new UnsupportedOperationException("Custom vector primitives are not currently supported");
15025+ tryWrite(this.readWriteLock, () -> {
15026+ this.customDataStore.addPrimitive(primitive);
15027+ primitive.setDataSet(this);
15028+ });
15029+ }
15030+
15031+ /**
15032+ * Remove a primitive from the custom data store
15033+ * @param primitive The primitive to add to the custom data store
15034+ */
15035+ public void removePrimitive(VectorPrimitive primitive) {
15036+ this.customDataStore.removePrimitive(primitive);
15037+ primitive.setDataSet(null);
15038 }
15039
15040 @Override
15041@@ -645,7 +663,8 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15042 }
15043
15044 @Override
15045- public boolean removeSelectionListener(IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
15046+ public boolean removeSelectionListener(
15047+ IDataSelectionListener<VectorPrimitive, VectorNode, VectorWay, VectorRelation, VectorDataSet> listener) {
15048 if (this.listeners.containsListener(listener)) {
15049 this.listeners.removeListener(listener);
15050 }
15051diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
15052index 0e7a572d5..c0a1c8532 100644
15053--- a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
15054+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
15055@@ -1,20 +1,11 @@
15056 // License: GPL. For details, see LICENSE file.
15057 package org.openstreetmap.josm.data.vector;
15058
15059-import static org.junit.jupiter.api.Assertions.assertEquals;
15060-import static org.junit.jupiter.api.Assertions.assertTrue;
15061-
15062-
15063-import java.nio.file.Paths;
15064-import java.text.MessageFormat;
15065-import java.util.ArrayList;
15066-import java.util.Collection;
15067-import java.util.Collections;
15068-import java.util.HashSet;
15069-import java.util.List;
15070-import java.util.Map;
15071-import java.util.stream.Collectors;
15072-
15073+import org.awaitility.Awaitility;
15074+import org.awaitility.Durations;
15075+import org.junit.jupiter.api.BeforeEach;
15076+import org.junit.jupiter.api.Test;
15077+import org.junit.jupiter.api.extension.RegisterExtension;
15078 import org.openstreetmap.josm.TestUtils;
15079 import org.openstreetmap.josm.data.imagery.ImageryInfo;
15080 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile;
15081@@ -23,11 +14,14 @@ import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSou
15082 import org.openstreetmap.josm.gui.layer.imagery.MVTLayer;
15083 import org.openstreetmap.josm.testutils.JOSMTestRules;
15084
15085-import org.awaitility.Awaitility;
15086-import org.awaitility.Durations;
15087-import org.junit.jupiter.api.BeforeEach;
15088-import org.junit.jupiter.api.Test;
15089-import org.junit.jupiter.api.extension.RegisterExtension;
15090+import java.nio.file.Paths;
15091+import java.util.ArrayList;
15092+import java.util.Collection;
15093+import java.util.Collections;
15094+import java.util.HashSet;
15095+
15096+import static org.junit.jupiter.api.Assertions.assertEquals;
15097+import static org.junit.jupiter.api.Assertions.assertTrue;
15098
15099 /**
15100 * A test for {@link VectorDataSet}
15101@@ -122,24 +116,10 @@ class VectorDataSetTest {
15102 assertEquals(55, dataSet.getNodes().stream().filter(node -> "mapillary-images".equals(node.getLayer())).count());
15103 // Please note that this dataset originally had the <i>same</i> id for all the images
15104 // (MVT v2 explicitly said that ids had to be unique in a layer, MVT v1 did not)
15105- assertEquals(55, dataSet.getNodes().stream().map(node -> node.get("original_id")).count());
15106- assertEquals(1, dataSet.getNodes().stream().map(node -> node.get("original_id")).distinct().count());
15107+ // This number is from the 56 nodes - original node with id - single node on mapillary-sequences layer = 54
15108+ assertEquals(54, dataSet.getNodes().stream().filter(node -> node.hasKey("original_id")).count());
15109+ assertEquals(1, dataSet.getNodes().stream().filter(node -> node.hasKey("original_id")).map(node -> node.get("original_id")).distinct().count());
15110 assertEquals(1, dataSet.getWays().size());
15111 assertEquals(0, dataSet.getRelations().size());
15112 }
15113-
15114- @Test
15115- void testWayDeduplicationSimple() {
15116- final VectorDataSet dataSet = this.layer.getData();
15117- assertTrue(dataSet.allPrimitives().isEmpty());
15118-
15119- // Set the zoom to 14, as that is the tile we are checking
15120- dataSet.setZoom(14);
15121- // Load tiles that are next to each other
15122- loadTile(this.layer, 14, 3248, 6258, 14, 3249, 6258);
15123-
15124- Map<Long, List<VectorWay>> wayGroups = dataSet.getWays().stream()
15125- .collect(Collectors.groupingBy(VectorWay::getId));
15126- wayGroups.forEach((id, ways) -> assertEquals(1, ways.size(), MessageFormat.format("{0} was not deduplicated", id)));
15127- }
15128 }
15129--
15130GitLab
15131
15132
15133From 48a0c94f8a31ca8e57e3b3dce677fc034d1d8303 Mon Sep 17 00:00:00 2001
15134From: Taylor Smock <tsmock@fb.com>
15135Date: Tue, 4 May 2021 14:51:45 -0600
15136Subject: [PATCH 43/50] VectorDataSet: Enable custom data for layers
15137
15138Signed-off-by: Taylor Smock <tsmock@fb.com>
15139---
15140 .../josm/data/vector/VectorDataSet.java | 50 ++++++++-----------
15141 1 file changed, 21 insertions(+), 29 deletions(-)
15142
15143diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15144index d626156d4..afb958232 100644
15145--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15146+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15147@@ -168,11 +168,9 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15148 public List<VectorNode> searchNodes(BBox bbox) {
15149 return tryRead(this.readWriteLock, () -> {
15150 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
15151- if (dataStore != null) {
15152- return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
15153- .flatMap(store -> store.searchNodes(bbox).stream()).collect(Collectors.toList());
15154- }
15155- return null;
15156+ final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
15157+ return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
15158+ .flatMap(store -> store.searchNodes(bbox).stream()).collect(Collectors.toList());
15159 }).orElseGet(Collections::emptyList);
15160 }
15161
15162@@ -180,9 +178,9 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15163 public boolean containsNode(VectorNode vectorNode) {
15164 return tryRead(this.readWriteLock, () -> {
15165 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
15166- return dataStore != null &&
15167- dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
15168- .anyMatch(store -> store.containsNode(vectorNode));
15169+ final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
15170+ return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
15171+ .anyMatch(store -> store.containsNode(vectorNode));
15172 }).orElse(Boolean.FALSE);
15173 }
15174
15175@@ -190,11 +188,9 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15176 public List<VectorWay> searchWays(BBox bbox) {
15177 return tryRead(this.readWriteLock, () -> {
15178 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
15179- if (dataStore != null) {
15180- return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
15181- .flatMap(store -> store.searchWays(bbox).stream()).collect(Collectors.toList());
15182- }
15183- return null;
15184+ final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
15185+ return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
15186+ .flatMap(store -> store.searchWays(bbox).stream()).collect(Collectors.toList());
15187 }).orElseGet(Collections::emptyList);
15188 }
15189
15190@@ -202,9 +198,9 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15191 public boolean containsWay(VectorWay vectorWay) {
15192 return tryRead(this.readWriteLock, () -> {
15193 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
15194- return dataStore != null &&
15195- dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
15196- .anyMatch(store -> store.containsWay(vectorWay));
15197+ final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
15198+ return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
15199+ .anyMatch(store -> store.containsWay(vectorWay));
15200 }).orElse(Boolean.FALSE);
15201 }
15202
15203@@ -212,11 +208,9 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15204 public List<VectorRelation> searchRelations(BBox bbox) {
15205 return tryRead(this.readWriteLock, () -> {
15206 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
15207- if (dataStore != null) {
15208- return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
15209- .flatMap(store -> store.searchRelations(bbox).stream()).collect(Collectors.toList());
15210- }
15211- return null;
15212+ final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
15213+ return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
15214+ .flatMap(store -> store.searchRelations(bbox).stream()).collect(Collectors.toList());
15215 }).orElseGet(Collections::emptyList);
15216 }
15217
15218@@ -224,9 +218,9 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15219 public boolean containsRelation(VectorRelation vectorRelation) {
15220 return tryRead(this.readWriteLock, () -> {
15221 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
15222- return dataStore != null &&
15223- dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getStore)
15224- .anyMatch(store -> store.containsRelation(vectorRelation));
15225+ final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
15226+ return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getStore)
15227+ .anyMatch(store -> store.containsRelation(vectorRelation));
15228 }).orElse(Boolean.FALSE);
15229 }
15230
15231@@ -248,11 +242,9 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15232 */
15233 public Stream<VectorPrimitive> getPrimitivesById(PrimitiveId... primitiveIds) {
15234 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
15235- if (dataStore != null) {
15236- return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getPrimitivesMap)
15237- .flatMap(m -> Stream.of(primitiveIds).map(m::get)).filter(Objects::nonNull);
15238- }
15239- return Stream.empty();
15240+ final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
15241+ return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getPrimitivesMap)
15242+ .flatMap(m -> Stream.of(primitiveIds).map(m::get)).filter(Objects::nonNull);
15243 }
15244
15245 @Override
15246--
15247GitLab
15248
15249
15250From 556c1c75df8fdef7fbf1e749a4b4d1e3a62b5aac Mon Sep 17 00:00:00 2001
15251From: Taylor Smock <tsmock@fb.com>
15252Date: Tue, 4 May 2021 16:04:46 -0600
15253Subject: [PATCH 44/50] VectorDataSet: Get all primitives with the id
15254
15255Signed-off-by: Taylor Smock <tsmock@fb.com>
15256---
15257 src/org/openstreetmap/josm/data/vector/VectorDataSet.java | 8 ++++----
15258 1 file changed, 4 insertions(+), 4 deletions(-)
15259
15260diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15261index afb958232..0e8b02ea5 100644
15262--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15263+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15264@@ -429,7 +429,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15265
15266 private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) {
15267 this.doSelectionChange(old -> new IDataSelectionListener.SelectionToggleEvent<>(this, old,
15268- osm.map(this::getPrimitiveById).filter(Objects::nonNull)));
15269+ osm.flatMap(this::getPrimitivesById).filter(Objects::nonNull)));
15270 }
15271
15272 @Override
15273@@ -444,7 +444,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15274
15275 private void setSelectedImpl(Stream<? extends PrimitiveId> osm) {
15276 this.doSelectionChange(old -> new IDataSelectionListener.SelectionReplaceEvent<>(this, old,
15277- osm.filter(Objects::nonNull).map(this::getPrimitiveById).filter(Objects::nonNull)));
15278+ osm.filter(Objects::nonNull).flatMap(this::getPrimitivesById).filter(Objects::nonNull)));
15279 }
15280
15281 @Override
15282@@ -459,7 +459,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15283
15284 private void addSelectedImpl(Stream<? extends PrimitiveId> osm) {
15285 this.doSelectionChange(old -> new IDataSelectionListener.SelectionAddEvent<>(this, old,
15286- osm.map(this::getPrimitiveById).filter(Objects::nonNull)));
15287+ osm.flatMap(this::getPrimitivesById).filter(Objects::nonNull)));
15288 }
15289
15290 @Override
15291@@ -479,7 +479,7 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15292
15293 private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) {
15294 this.doSelectionChange(old -> new IDataSelectionListener.SelectionRemoveEvent<>(this, old,
15295- osm.map(this::getPrimitiveById).filter(Objects::nonNull)));
15296+ osm.flatMap(this::getPrimitivesById).filter(Objects::nonNull)));
15297 }
15298
15299 /**
15300--
15301GitLab
15302
15303
15304From 710dd4df7644917bc8072f674265e36ccac3b56a Mon Sep 17 00:00:00 2001
15305From: Taylor Smock <tsmock@fb.com>
15306Date: Wed, 5 May 2021 09:08:59 -0600
15307Subject: [PATCH 45/50] LayersTest: Fix width/radius issue
15308
15309Signed-off-by: Taylor Smock <tsmock@fb.com>
15310---
15311 .../josm/data/imagery/vectortile/mapbox/style/LayersTest.java | 2 +-
15312 1 file changed, 1 insertion(+), 1 deletion(-)
15313
15314diff --git a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
15315index 5db4d5a3d..d4b87daa0 100644
15316--- a/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
15317+++ b/test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
15318@@ -285,7 +285,7 @@ class LayersTest {
15319 .build());
15320 assertEquals("node::random-layer-id{icon-image:concat(\"something/\",tag(\"value\"),\"/random\");icon-offset-x:2.0;"
15321 + "icon-offset-y:3.0;icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";"
15322- + "font-weight:normal;font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}",
15323+ + "font-weight:normal;font-style:normal;text-halo-color:#ffffff;text-halo-radius:8;text-opacity:0.6;font-size:25;}",
15324 fullOneIconImagePlaceholderExtraLineLayer.toString());
15325
15326 // Test with placeholders in icon-image
15327--
15328GitLab
15329
15330
15331From d836e105168cfb32444ff44296bffe17ab1a1436 Mon Sep 17 00:00:00 2001
15332From: Taylor Smock <tsmock@fb.com>
15333Date: Wed, 5 May 2021 09:19:11 -0600
15334Subject: [PATCH 46/50] VectorDataSet: Use custom data layer when getting all
15335 primitives
15336
15337Signed-off-by: Taylor Smock <tsmock@fb.com>
15338---
15339 src/org/openstreetmap/josm/data/vector/VectorDataSet.java | 6 ++----
15340 1 file changed, 2 insertions(+), 4 deletions(-)
15341
15342diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15343index 0e8b02ea5..1851603a4 100644
15344--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15345+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15346@@ -251,10 +251,8 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15347 public <T extends VectorPrimitive> Collection<T> getPrimitives(Predicate<? super VectorPrimitive> predicate) {
15348 Collection<VectorPrimitive> primitives = tryRead(this.readWriteLock, () -> {
15349 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
15350- if (dataStore == null) {
15351- return null;
15352- }
15353- return dataStore.stream().map(MVTTile::getData)
15354+ final Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
15355+ return Stream.concat(dataStoreStream, Stream.of(this.customDataStore))
15356 .map(VectorDataStore::getAllPrimitives).flatMap(Collection::stream).distinct().collect(Collectors.toList());
15357
15358 }).orElseGet(Collections::emptyList);
15359--
15360GitLab
15361
15362
15363From c3f785230a09868681023c4ff953b69ea25cf2c8 Mon Sep 17 00:00:00 2001
15364From: Taylor Smock <tsmock@fb.com>
15365Date: Wed, 5 May 2021 16:38:17 -0600
15366Subject: [PATCH 47/50] VectorDataSet: Get selection from custom data as well
15367
15368Signed-off-by: Taylor Smock <tsmock@fb.com>
15369---
15370 .../josm/data/vector/VectorDataSet.java | 20 ++++++++-----------
15371 1 file changed, 8 insertions(+), 12 deletions(-)
15372
15373diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15374index 1851603a4..a87fd34fa 100644
15375--- a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15376+++ b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java
15377@@ -360,18 +360,14 @@ public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, Vecto
15378 public Collection<VectorPrimitive> getAllSelected() {
15379 return tryRead(this.readWriteLock, () -> {
15380 final Storage<MVTTile> dataStore = this.getBestZoomDataStore().orElse(null);
15381- if (dataStore != null) {
15382- // The dataStore is what we don't want to concurrently modify
15383- synchronized (dataStore) {
15384- return dataStore.stream().map(MVTTile::getData).map(VectorDataStore::getPrimitivesMap).flatMap(dataMap -> {
15385- // Synchronize on dataMap to avoid concurrent modification errors
15386- synchronized (dataMap) {
15387- return this.currentSelectedPrimitives.stream().map(dataMap::get).filter(Objects::nonNull);
15388- }
15389- }).collect(Collectors.toList());
15390- }
15391- }
15392- return null;
15393+ Stream<VectorDataStore> dataStoreStream = dataStore != null ? dataStore.stream().map(MVTTile::getData) : Stream.empty();
15394+ return Stream.concat(dataStoreStream, Stream.of(this.customDataStore)).map(VectorDataStore::getPrimitivesMap)
15395+ .flatMap(dataMap -> {
15396+ // Synchronize on dataMap to avoid concurrent modification errors
15397+ synchronized (dataMap) {
15398+ return this.currentSelectedPrimitives.stream().map(dataMap::get).filter(Objects::nonNull);
15399+ }
15400+ }).collect(Collectors.toList());
15401 }).orElseGet(Collections::emptyList);
15402 }
15403
15404--
15405GitLab
15406
15407
15408From d4ea49dea0b95392a86e66798b65fbfd206197fc Mon Sep 17 00:00:00 2001
15409From: Taylor Smock <tsmock@fb.com>
15410Date: Wed, 5 May 2021 16:38:33 -0600
15411Subject: [PATCH 48/50] VectorDataSetTest: PMD/checkstyle
15412
15413Signed-off-by: Taylor Smock <tsmock@fb.com>
15414---
15415 src/org/openstreetmap/josm/data/vector/VectorDataStore.java | 4 +++-
15416 .../org/openstreetmap/josm/data/vector/VectorDataSetTest.java | 3 ++-
15417 2 files changed, 5 insertions(+), 2 deletions(-)
15418
15419diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
15420index dceef3b8e..2d1cd17d5 100644
15421--- a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
15422+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
15423@@ -40,6 +40,8 @@ import java.util.stream.Collectors;
15424 public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> implements Destroyable {
15425 private static final String JOSM_MERGE_TYPE_KEY = "josm_merge_type";
15426 private static final String ORIGINAL_ID = "original_id";
15427+ private static final String MULTIPOLYGON_TYPE = "multipolygon";
15428+ private static final String RELATION_TYPE = "type";
15429
15430 @Override
15431 protected void addPrimitive(VectorPrimitive primitive) {
15432@@ -292,7 +294,7 @@ public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect
15433 .orElse(null);
15434 } else if (shape instanceof Area) {
15435 primitive = areaToRelation(tile, layer, featureObjects, (Area) shape);
15436- primitive.put("type", "multipolygon");
15437+ primitive.put(RELATION_TYPE, MULTIPOLYGON_TYPE);
15438 } else {
15439 // We shouldn't hit this, but just in case
15440 throw new UnsupportedOperationException();
15441diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
15442index c0a1c8532..69035abc0 100644
15443--- a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
15444+++ b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
15445@@ -118,7 +118,8 @@ class VectorDataSetTest {
15446 // (MVT v2 explicitly said that ids had to be unique in a layer, MVT v1 did not)
15447 // This number is from the 56 nodes - original node with id - single node on mapillary-sequences layer = 54
15448 assertEquals(54, dataSet.getNodes().stream().filter(node -> node.hasKey("original_id")).count());
15449- assertEquals(1, dataSet.getNodes().stream().filter(node -> node.hasKey("original_id")).map(node -> node.get("original_id")).distinct().count());
15450+ assertEquals(1, dataSet.getNodes().stream().filter(node -> node.hasKey("original_id")).map(node -> node.get("original_id"))
15451+ .distinct().count());
15452 assertEquals(1, dataSet.getWays().size());
15453 assertEquals(0, dataSet.getRelations().size());
15454 }
15455--
15456GitLab
15457
15458
15459From a1387d79c5fa547b0b4ba401cf54b7cc2659f55b Mon Sep 17 00:00:00 2001
15460From: Taylor Smock <tsmock@fb.com>
15461Date: Thu, 6 May 2021 07:14:06 -0600
15462Subject: [PATCH 49/50] Mapbox Vector Tiles: Modify file formats and their
15463 documentation
15464
15465Signed-off-by: Taylor Smock <tsmock@fb.com>
15466---
15467 .../josm/data/imagery/vectortile/mapbox/MVTFile.java | 9 +++++----
15468 1 file changed, 5 insertions(+), 4 deletions(-)
15469
15470diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
15471index 84ac8ae89..7a398537b 100644
15472--- a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
15473+++ b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
15474@@ -13,15 +13,16 @@ import java.util.List;
15475 public final class MVTFile {
15476 /**
15477 * Extensions for Mapbox Vector Tiles.
15478- * This is a SHOULD, <i>not</i> a MUST.
15479+ * {@code mvt} is a SHOULD, <i>not</i> a MUST.
15480 */
15481- public static final List<String> EXTENSION = Collections.unmodifiableList(Arrays.asList("mvt"));
15482+ public static final List<String> EXTENSION = Collections.unmodifiableList(Arrays.asList("mvt", "pbf"));
15483
15484 /**
15485 * mimetypes for Mapbox Vector Tiles
15486- * This is a SHOULD, <i>not</i> a MUST.
15487+ * This {@code application/vnd.mapbox-vector-tile}is a SHOULD, <i>not</i> a MUST.
15488 */
15489- public static final List<String> MIMETYPE = Collections.unmodifiableList(Arrays.asList("application/vnd.mapbox-vector-tile"));
15490+ public static final List<String> MIMETYPE = Collections.unmodifiableList(Arrays.asList("application/vnd.mapbox-vector-tile",
15491+ "application/x-protobuf"));
15492
15493 /**
15494 * The default projection. This is Web Mercator, per specification.
15495--
15496GitLab
15497
15498
15499From 3ee61eadda7b58999c17d5f8bd3706cada90c8ba Mon Sep 17 00:00:00 2001
15500From: Taylor Smock <tsmock@fb.com>
15501Date: Thu, 6 May 2021 07:19:10 -0600
15502Subject: [PATCH 50/50] Vector Tiles: Account for features with no tags
15503
15504Signed-off-by: Taylor Smock <tsmock@fb.com>
15505---
15506 src/org/openstreetmap/josm/data/vector/VectorDataStore.java | 4 +++-
15507 1 file changed, 3 insertions(+), 1 deletion(-)
15508
15509diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
15510index 2d1cd17d5..793bbf34a 100644
15511--- a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
15512+++ b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java
15513@@ -323,7 +323,9 @@ public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect
15514 primitive.put(ORIGINAL_ID, Long.toString(feature.getId()));
15515 primitive.setId(primitive.getIdGenerator().generateUniqueId());
15516 }
15517- feature.getTags().forEach(primitive::put);
15518+ if (feature.getTags() != null) {
15519+ feature.getTags().forEach(primitive::put);
15520+ }
15521 featureObjects.forEach(this::addPrimitive);
15522 primaryFeatureObjects.forEach(this::addPrimitive);
15523 try {
15524--
15525GitLab
15526