Ticket #17177: 17177.6.patch
File 17177.6.patch, 273.5 KB (added by , 4 years ago) |
---|
-
new file resources/images/dialogs/add_mvt.svg
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/resources/images/dialogs/add_mvt.svg b/resources/images/dialogs/add_mvt.svg new file mode 100644
- + 1 <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 <svg 3 xmlns:dc="http://purl.org/dc/elements/1.1/" 4 xmlns:cc="http://creativecommons.org/ns#" 5 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 6 xmlns="http://www.w3.org/2000/svg" 7 xmlns:xlink="http://www.w3.org/1999/xlink" 8 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 9 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 10 width="24" 11 height="24" 12 viewBox="0 0 24 24" 13 id="svg2" 14 version="1.1" 15 inkscape:version="1.0.1 (c497b03c, 2020-09-10)" 16 sodipodi:docname="add_mvt.svg"> 17 <defs 18 id="defs4"> 19 <linearGradient 20 gradientTransform="translate(4)" 21 gradientUnits="userSpaceOnUse" 22 y2="1049.3622" 23 x2="12" 24 y1="1041.3622" 25 x1="4" 26 id="linearGradient868" 27 xlink:href="#linearGradient866" 28 inkscape:collect="always" /> 29 <linearGradient 30 id="linearGradient866" 31 inkscape:collect="always"> 32 <stop 33 id="stop862" 34 offset="0" 35 style="stop-color:#dfdfdf;stop-opacity:1" /> 36 <stop 37 id="stop864" 38 offset="1" 39 style="stop-color:#949593;stop-opacity:1" /> 40 </linearGradient> 41 </defs> 42 <sodipodi:namedview 43 id="base" 44 pagecolor="#ffffff" 45 bordercolor="#666666" 46 borderopacity="1.0" 47 inkscape:pageopacity="0" 48 inkscape:pageshadow="2" 49 inkscape:zoom="45.254834" 50 inkscape:cx="11.376506" 51 inkscape:cy="17.057298" 52 inkscape:document-units="px" 53 inkscape:current-layer="layer1" 54 showgrid="true" 55 units="px" 56 inkscape:window-width="1920" 57 inkscape:window-height="955" 58 inkscape:window-x="0" 59 inkscape:window-y="23" 60 inkscape:window-maximized="1" 61 viewbox-height="16" 62 inkscape:document-rotation="0"> 63 <inkscape:grid 64 type="xygrid" 65 id="grid4136" 66 originx="0" 67 originy="0" 68 spacingx="1" 69 spacingy="1" /> 70 </sodipodi:namedview> 71 <metadata 72 id="metadata7"> 73 <rdf:RDF> 74 <cc:Work 75 rdf:about=""> 76 <dc:format>image/svg+xml</dc:format> 77 <dc:type 78 rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 79 <dc:title></dc:title> 80 <cc:license 81 rdf:resource="http://creativecommons.org/publicdomain/zero/1.0/" /> 82 </cc:Work> 83 <cc:License 84 rdf:about="http://creativecommons.org/publicdomain/zero/1.0/"> 85 <cc:permits 86 rdf:resource="http://creativecommons.org/ns#Reproduction" /> 87 <cc:permits 88 rdf:resource="http://creativecommons.org/ns#Distribution" /> 89 <cc:permits 90 rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> 91 </cc:License> 92 </rdf:RDF> 93 </metadata> 94 <g 95 inkscape:label="Layer 1" 96 inkscape:groupmode="layer" 97 id="layer1" 98 transform="translate(0,-1037.3622)"> 99 <rect 100 ry="0.48361239" 101 y="1043.8622" 102 x="5.5" 103 height="3" 104 width="13" 105 id="rect833" 106 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" /> 107 <rect 108 transform="rotate(-90)" 109 ry="0.48361239" 110 y="10.5" 111 x="-1051.8622" 112 height="3" 113 width="13" 114 id="rect833-5" 115 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" /> 116 <path 117 inkscape:connector-curvature="0" 118 id="path852" 119 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" 120 style="fill:url(#linearGradient868);fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> 121 <path 122 style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" 123 d="m 4.5,1060.3625 v -7.5948 l 2,4.3971 2,-4.3971 v 7.5948" 124 id="path894" 125 sodipodi:nodetypes="ccccc" /> 126 <path 127 style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" 128 d="m 17.5,1060.3622 v -8" 129 id="path896" /> 130 <path 131 style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" 132 d="m 15,1052.8622 h 5" 133 id="path898" /> 134 <text 135 xml:space="preserve" 136 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" 137 x="10.59868" 138 y="898.41876" 139 id="text854" 140 transform="scale(0.84728029,1.180247)"><tspan 141 sodipodi:role="line" 142 id="tspan852" 143 x="10.59868" 144 y="898.41876" 145 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> 146 </g> 147 </svg> -
src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java
a b 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.cache; 3 3 4 import java.io.File; 5 import java.io.FileInputStream; 4 6 import java.io.FileNotFoundException; 5 7 import java.io.IOException; 6 8 import java.net.HttpURLConnection; … … 17 19 import java.util.concurrent.TimeUnit; 18 20 import java.util.regex.Matcher; 19 21 20 import org.apache.commons.jcs3.access.behavior.ICacheAccess;21 import org.apache.commons.jcs3.engine.behavior.ICacheElement;22 22 import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult; 23 23 import org.openstreetmap.josm.data.imagery.TileJobOptions; 24 24 import org.openstreetmap.josm.data.preferences.IntegerProperty; … … 27 27 import org.openstreetmap.josm.tools.Logging; 28 28 import org.openstreetmap.josm.tools.Utils; 29 29 30 import org.apache.commons.compress.utils.IOUtils; 31 import org.apache.commons.jcs3.access.behavior.ICacheAccess; 32 import org.apache.commons.jcs3.engine.behavior.ICacheElement; 33 30 34 /** 31 35 * Generic loader for HTTP based tiles. Uses custom attribute, to check, if entry has expired 32 36 * according to HTTP headers sent with tile. If so, it tries to verify using Etags … … 294 298 if (attributes == null) { 295 299 attributes = new CacheEntryAttributes(); 296 300 } 301 final URL url = this.getUrlNoException(); 302 if (url == null) { 303 return false; 304 } 305 306 if (url.getProtocol().contains("http")) { 307 return loadObjectHttp(); 308 } 309 if (url.getProtocol().contains("file")) { 310 return loadObjectFile(url); 311 } 312 313 return false; 314 } 315 316 private boolean loadObjectFile(URL url) { 317 String fileName = url.toExternalForm(); 318 File file = new File(fileName.substring("file:/".length() - 1)); 319 if (!file.exists()) { 320 file = new File(fileName.substring("file://".length() - 1)); 321 } 322 try (FileInputStream fileInputStream = new FileInputStream(file)) { 323 cacheData = createCacheEntry(IOUtils.toByteArray(fileInputStream)); 324 cache.put(getCacheKey(), cacheData, attributes); 325 return true; 326 } catch (IOException e) { 327 Logging.error(e); 328 attributes.setError(e); 329 attributes.setException(e); 330 } 331 return false; 332 } 333 334 /** 335 * @return true if object was successfully downloaded via http, false, if there was a loading failure 336 */ 337 private boolean loadObjectHttp() { 297 338 try { 298 339 // if we have object in cache, and host doesn't support If-Modified-Since nor If-None-Match 299 340 // then just use HEAD request and check returned values … … 553 594 try { 554 595 return getUrl(); 555 596 } catch (IOException e) { 597 Logging.trace(e); 556 598 return null; 557 599 } 558 600 } -
src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java b/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
a b 61 61 /** A WMS endpoint entry only stores the WMS server info, without layer, which are chosen later by the user. **/ 62 62 WMS_ENDPOINT("wms_endpoint"), 63 63 /** WMTS stores GetCapabilities URL. Does not store any information about the layer **/ 64 WMTS("wmts"); 64 WMTS("wmts"), 65 /** MapBox Vector Tiles entry*/ 66 MVT("mvt"); 65 67 66 68 private final String typeString; 67 69 … … 654 656 defaultMaxZoom = 0; 655 657 defaultMinZoom = 0; 656 658 for (ImageryType type : ImageryType.values()) { 657 Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+) \\])?:(.*)").matcher(url);659 Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+)])?:(.*)").matcher(url); 658 660 if (m.matches()) { 659 661 this.url = m.group(3); 660 662 this.sourceType = type; … … 669 671 } 670 672 671 673 if (serverProjections.isEmpty()) { 672 Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\) \\}.*").matcher(url.toUpperCase(Locale.ENGLISH));674 Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)}.*").matcher(url.toUpperCase(Locale.ENGLISH)); 673 675 if (m.matches()) { 674 676 setServerProjections(Arrays.asList(m.group(1).split(",", -1))); 675 677 } -
src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
a b 10 10 import java.nio.charset.StandardCharsets; 11 11 import java.util.HashSet; 12 12 import java.util.List; 13 import java.util.Locale; 13 14 import java.util.Map; 14 15 import java.util.Map.Entry; 15 16 import java.util.Optional; … … 21 22 import java.util.regex.Matcher; 22 23 import java.util.regex.Pattern; 23 24 24 import org.apache.commons.jcs3.access.behavior.ICacheAccess;25 25 import org.openstreetmap.gui.jmapviewer.Tile; 26 26 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 27 27 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; … … 32 32 import org.openstreetmap.josm.data.cache.CacheEntryAttributes; 33 33 import org.openstreetmap.josm.data.cache.ICachedLoaderListener; 34 34 import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob; 35 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 36 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile; 35 37 import org.openstreetmap.josm.data.preferences.LongProperty; 36 38 import org.openstreetmap.josm.tools.HttpClient; 37 39 import org.openstreetmap.josm.tools.Logging; 38 40 import org.openstreetmap.josm.tools.Utils; 39 41 42 import org.apache.commons.jcs3.access.behavior.ICacheAccess; 43 40 44 /** 41 45 * Class bridging TMS requests to JCS cache requests 42 46 * … … 147 151 private boolean isNotImage(Map<String, List<String>> headers, int statusCode) { 148 152 if (statusCode == 200 && headers.containsKey("Content-Type") && !headers.get("Content-Type").isEmpty()) { 149 153 String contentType = headers.get("Content-Type").stream().findAny().get(); 150 if (contentType != null && !contentType.startsWith("image") ) {154 if (contentType != null && !contentType.startsWith("image") && !MVTFile.MIMETYPE.contains(contentType.toLowerCase(Locale.ROOT))) { 151 155 Logging.warn("Image not returned for tile: " + url + " content type was: " + contentType); 152 156 // not an image - do not store response in cache, so next time it will be queried again from the server 153 157 return true; … … 321 325 if (content.length > 0) { 322 326 try (ByteArrayInputStream in = new ByteArrayInputStream(content)) { 323 327 tile.loadImage(in); 324 if (tile.getImage() == null) { 328 if ((!(tile instanceof VectorTile) && tile.getImage() == null) 329 || ((tile instanceof VectorTile) && !tile.isLoaded())) { 325 330 String s = new String(content, StandardCharsets.UTF_8); 326 331 Matcher m = SERVICE_EXCEPTION_PATTERN.matcher(s); 327 332 if (m.matches()) { -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 /** 5 * Command integers for Mapbox Vector Tiles 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public enum Command { 10 /** 11 * For {@link GeometryTypes#POINT}, each {@link #MoveTo} is a new point. 12 * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #MoveTo} is a new geometry of the same type. 13 */ 14 MoveTo((byte) 1, (byte) 2), 15 /** 16 * While not explicitly prohibited for {@link GeometryTypes#POINT}, it should be ignored. 17 * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #LineTo} extends that geometry. 18 */ 19 LineTo((byte) 2, (byte) 2), 20 /** 21 * This is only explicitly valid for {@link GeometryTypes#POLYGON}. It closes the {@link GeometryTypes#POLYGON}. 22 */ 23 ClosePath((byte) 7, (byte) 0); 24 25 private final byte id; 26 private final byte parameters; 27 28 Command(byte id, byte parameters) { 29 this.id = id; 30 this.parameters = parameters; 31 } 32 33 /** 34 * Get the command id 35 * @return The id 36 */ 37 public byte getId() { 38 return this.id; 39 } 40 41 /** 42 * Get the number of parameters 43 * @return The number of parameters 44 */ 45 public byte getParameterNumber() { 46 return this.parameters; 47 } 48 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.stream.Stream; 5 6 /** 7 * An indicator for a command to be executed 8 * @author Taylor Smock 9 * @since xxx 10 */ 11 public class CommandInteger { 12 private final Command type; 13 private final short[] parameters; 14 private int added; 15 16 /** 17 * Create a new command 18 * @param command the command (treated as an unsigned int) 19 */ 20 public CommandInteger(final int command) { 21 // Technically, the int is unsigned, but it is easier to work with the long 22 final long unsigned = Integer.toUnsignedLong(command); 23 this.type = Stream.of(Command.values()).filter(e -> e.getId() == (unsigned & 0x7)).findAny() 24 .orElseThrow(InvalidMapboxVectorTileException::new); 25 // This is safe, since we are shifting right 3 when we converted an int to a long (for unsigned). 26 // So we <i>cannot</i> lose anything. 27 final int operationsInt = (int) (unsigned >> 3); 28 this.parameters = new short[operationsInt * this.type.getParameterNumber()]; 29 } 30 31 /** 32 * Add a parameter 33 * @param parameterInteger The parameter to add (converted to {@link short}). 34 */ 35 public void addParameter(Number parameterInteger) { 36 this.parameters[added++] = parameterInteger.shortValue(); 37 } 38 39 /** 40 * Get the operations for the command 41 * @return The operations 42 */ 43 public short[] getOperations() { 44 return this.parameters; 45 } 46 47 /** 48 * Get the command type 49 * @return the command type 50 */ 51 public Command getType() { 52 return this.type; 53 } 54 55 /** 56 * Get the expected parameter length 57 * @return The expected parameter size 58 */ 59 public boolean hasAllExpectedParameters() { 60 return this.added >= this.parameters.length; 61 } 62 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.io.IOException; 5 import java.text.NumberFormat; 6 import java.util.ArrayList; 7 import java.util.List; 8 9 import org.openstreetmap.josm.data.osm.TagMap; 10 import org.openstreetmap.josm.data.protobuf.ProtoBufPacked; 11 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 12 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 13 import org.openstreetmap.josm.tools.Utils; 14 15 /** 16 * A Feature for a {@link Layer} 17 * 18 * @author Taylor Smock 19 * @since xxx 20 */ 21 public class Feature { 22 private static final byte ID_FIELD = 1; 23 private static final byte TAG_FIELD = 2; 24 private static final byte GEOMETRY_TYPE_FIELD = 3; 25 private static final byte GEOMETRY_FIELD = 4; 26 /** 27 * The geometry of the feature. Required. 28 */ 29 private final List<CommandInteger> geometry = new ArrayList<>(); 30 31 /** 32 * The geometry type of the feature. Required. 33 */ 34 private final GeometryTypes geometryType; 35 /** 36 * The id of the feature. Optional. 37 */ 38 // Technically, uint64 39 private final long id; 40 /** 41 * The tags of the feature. Optional. 42 */ 43 private TagMap tags; 44 45 /** 46 * Create a new Feature 47 * 48 * @param layer The layer the feature is part of (required for tags) 49 * @param record The record to create the feature from 50 * @throws IOException - if an IO error occurs 51 */ 52 public Feature(Layer layer, ProtoBufRecord record) throws IOException { 53 long tId = 0; 54 GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN; 55 String key = null; 56 try (ProtoBufParser parser = new ProtoBufParser(record.getBytes())) { 57 while (parser.hasNext()) { 58 try (ProtoBufRecord next = new ProtoBufRecord(parser)) { 59 if (next.getField() == TAG_FIELD) { 60 if (tags == null) { 61 tags = new TagMap(); 62 } 63 // This is packed in v1 and v2 64 ProtoBufPacked packed = new ProtoBufPacked(next.getBytes()); 65 for (Number number : packed.getArray()) { 66 key = parseTagValue(key, layer, number); 67 } 68 } else if (next.getField() == GEOMETRY_FIELD) { 69 // This is packed in v1 and v2 70 ProtoBufPacked packed = new ProtoBufPacked(next.getBytes()); 71 CommandInteger currentCommand = null; 72 for (Number number : packed.getArray()) { 73 if (currentCommand != null && currentCommand.hasAllExpectedParameters()) { 74 currentCommand = null; 75 } 76 if (currentCommand == null) { 77 currentCommand = new CommandInteger(number.intValue()); 78 this.geometry.add(currentCommand); 79 } else { 80 currentCommand.addParameter(ParameterInteger.decode(number.intValue())); 81 } 82 } 83 // TODO fallback to non-packed 84 } else if (next.getField() == GEOMETRY_TYPE_FIELD) { 85 geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()]; 86 } else if (next.getField() == ID_FIELD) { 87 tId = next.asUnsignedVarInt().longValue(); 88 } 89 } 90 } 91 } 92 this.id = tId; 93 this.geometryType = geometryTypeTemp; 94 record.close(); 95 } 96 97 /** 98 * Parse a tag value 99 * 100 * @param key The current key (or {@code null}, if {@code null}, the returned value will be the new key) 101 * @param layer The layer with key/value information 102 * @param number The number to get the value from 103 * @return The new key (if {@code null}, then a value was parsed and added to tags) 104 */ 105 private String parseTagValue(String key, Layer layer, Number number) { 106 if (key == null) { 107 key = layer.getKey(number.intValue()); 108 } else { 109 Object value = layer.getValue(number.intValue()); 110 if (value instanceof Double || value instanceof Float) { 111 // reset grouping if the instance is a singleton 112 final NumberFormat numberFormat = NumberFormat.getNumberInstance(); 113 final boolean grouping = numberFormat.isGroupingUsed(); 114 try { 115 numberFormat.setGroupingUsed(false); 116 this.tags.put(key, numberFormat.format(value)); 117 } finally { 118 numberFormat.setGroupingUsed(grouping); 119 } 120 } else { 121 this.tags.put(key, Utils.intern(value.toString())); 122 } 123 key = null; 124 } 125 return key; 126 } 127 128 /** 129 * Get the geometry instructions 130 * 131 * @return The geometry 132 */ 133 public List<CommandInteger> getGeometry() { 134 return this.geometry; 135 } 136 137 /** 138 * Get the geometry type 139 * 140 * @return The {@link GeometryTypes} 141 */ 142 public GeometryTypes getGeometryType() { 143 return this.geometryType; 144 } 145 146 /** 147 * Get the id of the object 148 * 149 * @return The unique id in the layer, or 0. 150 */ 151 public long getId() { 152 return this.id; 153 } 154 155 /** 156 * Get the tags 157 * 158 * @return A tag map 159 */ 160 public TagMap getTags() { 161 return this.tags; 162 } 163 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.Shape; 7 import java.awt.geom.Area; 8 import java.awt.geom.Ellipse2D; 9 import java.awt.geom.Path2D; 10 import java.util.Collection; 11 import java.util.Collections; 12 import java.util.HashSet; 13 import java.util.List; 14 15 /** 16 * A class to generate geometry for a vector tile 17 * @author Taylor Smock 18 * @since xxx 19 */ 20 public class Geometry { 21 private static final byte CIRCLE_SIZE = 0; 22 final Collection<Shape> shapes = new HashSet<>(); 23 private final Feature feature; 24 25 /** 26 * Create a {@link Geometry} for a {@link Feature} 27 * @param feature the {@link Feature} for the geometry 28 */ 29 public Geometry(final Feature feature) { 30 this.feature = feature; 31 final GeometryTypes geometryType = this.feature.getGeometryType(); 32 final List<CommandInteger> commands = this.feature.getGeometry(); 33 final byte circleSize = CIRCLE_SIZE; 34 if (geometryType == GeometryTypes.POINT) { 35 for (CommandInteger command : commands) { 36 final short[] operations = command.getOperations(); 37 // Each MoveTo command is a new point 38 if (command.getType() == Command.MoveTo && operations.length % 2 == 0) { 39 for (int i = 0; i < operations.length / 2; i++) { 40 // move left/up by 1/2 circleSize, so that the circle is centered 41 shapes.add(new Ellipse2D.Float(operations[2 * i] - circleSize / 2f, 42 operations[2 * i + 1] - circleSize / 2f, circleSize, circleSize)); 43 } 44 } else { 45 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 46 } 47 } 48 } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) { 49 Path2D.Float line = null; 50 Area area = null; 51 // MVT uses delta encoding. Each feature starts at (0, 0). 52 double x = 0; 53 double y = 0; 54 // Area is used to determine the inner/outer of a polygon 55 double areaAreaSq = 0; 56 for (CommandInteger command : commands) { 57 final short[] operations = command.getOperations(); 58 // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior 59 if (command.getType() == Command.MoveTo && operations.length == 2) { 60 areaAreaSq = 0; 61 x += operations[0]; 62 y += operations[1]; 63 line = new Path2D.Float(); 64 line.moveTo(x, y); 65 shapes.add(line); 66 } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) { 67 for (int i = 0; i < operations.length / 2; i++) { 68 final double lx = x; 69 final double ly = y; 70 x += operations[2 * i]; 71 y += operations[2 * i + 1]; 72 areaAreaSq += lx * y - x * ly; 73 line.lineTo(x, y); 74 } 75 // ClosePath should only be used with Polygon geometry 76 } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) { 77 // ClosePath specifically does not change the cursor position 78 line.closePath(); 79 line.setWindingRule(Path2D.WIND_NON_ZERO); 80 shapes.remove(line); 81 if (area == null) { 82 area = new Area(line); 83 shapes.add(area); 84 } else { 85 Area nArea = new Area(line); 86 // SonarLint thinks that this is never > 0. It can be. 87 if (areaAreaSq > 0) { 88 area.add(nArea); 89 } else { 90 area.exclusiveOr(nArea); 91 } 92 } 93 } else { 94 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 95 } 96 } 97 } 98 } 99 100 /** 101 * Get the feature for this geometry 102 * @return The feature 103 */ 104 public Feature getFeature() { 105 return this.feature; 106 } 107 108 /** 109 * Get the shapes to draw this geometry with 110 * @return A collection of shapes 111 */ 112 public Collection<Shape> getShapes() { 113 return Collections.unmodifiableCollection(this.shapes); 114 } 115 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 /** 5 * Geometry types used by Mapbox Vector Tiles 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public enum GeometryTypes { 10 /** May be ignored */ 11 UNKNOWN((byte) 0), 12 /** May be a point or a multipoint geometry. Uses <i>only</i> {@link Command#MoveTo}. Multiple {@link Command#MoveTo} 13 * indicates that it is a multi-point object. */ 14 POINT((byte) 1), 15 /** May be a line or a multiline geometry. Each line {@link Command#MoveTo} and one or more {@link Command#LineTo}. */ 16 LINESTRING((byte) 2), 17 /** May be a polygon or a multipolygon. Each ring uses a {@link Command#MoveTo}, one or more {@link Command#LineTo}, 18 * and one {@link Command#ClosePath} command. See {@link Ring}s. */ 19 POLYGON((byte) 3); 20 21 private final byte id; 22 GeometryTypes(byte id) { 23 this.id = id; 24 } 25 26 /** 27 * Get the id for the geometry type 28 * @return The id 29 */ 30 public byte getId() { 31 return this.id; 32 } 33 34 /** 35 * Rings used by {@link GeometryTypes#POLYGON} 36 * @author Taylor Smock 37 */ 38 public enum Ring { 39 /** A ring that goes in the clockwise direction */ 40 ExteriorRing, 41 /** A ring that goes in the anti-clockwise direction */ 42 InteriorRing 43 } 44 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 /** 5 * Thrown when a mapbox vector tile does not match specifications. 6 * 7 * @author Taylor Smock 8 * @since xxx 9 */ 10 public class InvalidMapboxVectorTileException extends RuntimeException { 11 /** 12 * Create a default {@link InvalidMapboxVectorTileException}. 13 */ 14 public InvalidMapboxVectorTileException() { 15 super(); 16 } 17 18 /** 19 * Create a new {@link InvalidMapboxVectorTile} exception with a message 20 * @param message The message 21 */ 22 public InvalidMapboxVectorTileException(final String message) { 23 super(message); 24 } 25 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 import java.io.IOException; 6 import java.util.ArrayList; 7 import java.util.Arrays; 8 import java.util.Collection; 9 import java.util.Collections; 10 import java.util.HashSet; 11 import java.util.List; 12 import java.util.Map; 13 import java.util.Objects; 14 import java.util.function.Function; 15 import java.util.stream.Collectors; 16 17 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 18 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 19 import org.openstreetmap.josm.tools.Logging; 20 21 /** 22 * A Mapbox Vector Tile Layer 23 * @author Taylor Smock 24 * @since xxx 25 */ 26 public class Layer { 27 private static final class ValueFields<T> { 28 static final ValueFields<String> STRING = new ValueFields<>(1, ProtoBufRecord::asString); 29 static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtoBufRecord::asFloat); 30 static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtoBufRecord::asDouble); 31 static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtoBufRecord::asUnsignedVarInt); 32 // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE}) 33 static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtoBufRecord::asUnsignedVarInt); 34 static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtoBufRecord::asSignedVarInt); 35 static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0); 36 37 /** 38 * A collection of methods to map a record to a type 39 */ 40 public static final Collection<ValueFields<?>> MAPPERS = 41 Collections.unmodifiableList(Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL)); 42 43 private final byte field; 44 private final Function<ProtoBufRecord, T> conversion; 45 private ValueFields(int field, Function<ProtoBufRecord, T> conversion) { 46 this.field = (byte) field; 47 this.conversion = conversion; 48 } 49 50 /** 51 * Get the field identifier for the value 52 * @return The identifier 53 */ 54 public byte getField() { 55 return this.field; 56 } 57 58 /** 59 * Convert a protobuf record to a value 60 * @param protobufRecord The record to convert 61 * @return the converted value 62 */ 63 public T convertValue(ProtoBufRecord protobufRecord) { 64 return this.conversion.apply(protobufRecord); 65 } 66 } 67 68 /** The field value for a layer (in {@link ProtoBufRecord#getField}) */ 69 public static final byte LAYER_FIELD = 3; 70 private static final byte VERSION_FIELD = 15; 71 private static final byte NAME_FIELD = 1; 72 private static final byte FEATURE_FIELD = 2; 73 private static final byte KEY_FIELD = 3; 74 private static final byte VALUE_FIELD = 4; 75 private static final byte EXTENT_FIELD = 5; 76 /** The default extent for a vector tile */ 77 static final int DEFAULT_EXTENT = 4096; 78 private static final byte DEFAULT_VERSION = 1; 79 /** This is <i>technically</i> an integer, but there are currently only two major versions (1, 2). Required. */ 80 private final byte version; 81 /** A unique name for the layer. This <i>must</i> be unique on a per-tile basis. Required. */ 82 private final String name; 83 84 /** The extent of the tile, typically 4096. Required. */ 85 private final int extent; 86 87 /** A list of unique keys. Order is important. Optional. */ 88 private final List<String> keyList = new ArrayList<>(); 89 /** A list of unique values. Order is important. Optional. */ 90 private final List<Object> valueList = new ArrayList<>(); 91 /** The actual features of this layer in this tile */ 92 private final List<Feature> featureCollection; 93 /** The shapes to use to draw this layer */ 94 private final List<Geometry> geometryCollection; 95 96 /** 97 * Create a layer from a collection of records 98 * @param records The records to convert to a layer 99 * @throws IOException - if an IO error occurs 100 */ 101 public Layer(Collection<ProtoBufRecord> records) throws IOException { 102 // Do the unique required fields first 103 Map<Integer, List<ProtoBufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtoBufRecord::getField)); 104 this.version = sorted.get((int) VERSION_FIELD).parallelStream().map(ProtoBufRecord::asUnsignedVarInt).map(Number::byteValue) 105 .findFirst().orElse(DEFAULT_VERSION); 106 // Per spec, we cannot continue past this until we have checked the version number 107 if (this.version != 1 && this.version != 2) { 108 throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version)); 109 } 110 this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst() 111 .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name"))); 112 this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asSignedVarInt) 113 .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT); 114 115 sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString) 116 .forEachOrdered(this.keyList::add); 117 sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::getBytes) 118 .map(ProtoBufParser::new).map(parser1 -> { 119 try { 120 return new ProtoBufRecord(parser1); 121 } catch (IOException e) { 122 Logging.error(e); 123 return null; 124 } 125 }) 126 .filter(Objects::nonNull) 127 .map(value -> ValueFields.MAPPERS.parallelStream() 128 .filter(v -> v.getField() == value.getField()) 129 .map(v -> v.convertValue(value)).findFirst() 130 .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField())))) 131 .forEachOrdered(this.valueList::add); 132 Collection<IOException> exceptions = new HashSet<>(0); 133 this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> { 134 try { 135 return new Feature(this, feature); 136 } catch (IOException e) { 137 exceptions.add(e); 138 } 139 return null; 140 }).collect(Collectors.toList()); 141 this.geometryCollection = this.featureCollection.stream().map(Geometry::new).collect(Collectors.toList()); 142 if (!exceptions.isEmpty()) { 143 throw exceptions.iterator().next(); 144 } 145 // Cleanup bytes (for memory) 146 for (ProtoBufRecord record : records) { 147 record.close(); 148 } 149 } 150 151 /** 152 * Get all the records from a array of bytes 153 * @param bytes The byte information 154 * @return All the protobuf records 155 * @throws IOException If there was an error reading the bytes (unlikely) 156 */ 157 private static Collection<ProtoBufRecord> getAllRecords(byte[] bytes) throws IOException { 158 try (ProtoBufParser parser = new ProtoBufParser(bytes)) { 159 return parser.allRecords(); 160 } 161 } 162 163 /** 164 * Create a new layer 165 * @param bytes The bytes that the layer comes from 166 * @throws IOException - if an IO error occurs 167 */ 168 public Layer(byte[] bytes) throws IOException { 169 this(getAllRecords(bytes)); 170 } 171 172 /** 173 * Get the extent of the tile 174 * @return The layer extent 175 */ 176 public int getExtent() { 177 return this.extent; 178 } 179 180 /** 181 * Get the feature on this layer 182 * @return the features 183 */ 184 public Collection<Feature> getFeatures() { 185 return Collections.unmodifiableCollection(this.featureCollection); 186 } 187 188 /** 189 * Get the geometry for this layer 190 * @return The geometry 191 */ 192 public Collection<Geometry> getGeometry() { 193 return Collections.unmodifiableCollection(this.geometryCollection); 194 } 195 196 /** 197 * Get a specified key 198 * @param index The index in the key list 199 * @return The actual key 200 */ 201 public String getKey(int index) { 202 return this.keyList.get(index); 203 } 204 205 /** 206 * Get the name of the layer 207 * @return The layer name 208 */ 209 public String getName() { 210 return this.name; 211 } 212 213 /** 214 * Get a specified value 215 * @param index The index in the value list 216 * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value. 217 */ 218 public Object getValue(int index) { 219 return this.valueList.get(index); 220 } 221 222 /** 223 * Get the MapBox Vector Tile version specification for this layer 224 * @return The version of the MapBox Vector Tile specification 225 */ 226 public byte getVersion() { 227 return this.version; 228 } 229 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.concurrent.ThreadPoolExecutor; 5 6 import org.openstreetmap.gui.jmapviewer.Tile; 7 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 8 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 9 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 10 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 11 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 12 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 13 import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob; 14 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 15 import org.openstreetmap.josm.data.imagery.TileJobOptions; 16 import org.openstreetmap.josm.data.preferences.IntegerProperty; 17 import org.openstreetmap.josm.tools.CheckParameterUtil; 18 19 import org.apache.commons.jcs3.access.behavior.ICacheAccess; 20 21 /** 22 * A TileLoader class for MVT tiles 23 * @author Taylor Smock 24 * @since xxx 25 */ 26 public class MapBoxVectorCachedTileLoader implements TileLoader, CachedTileLoader { 27 protected final ICacheAccess<String, BufferedImageCacheEntry> cache; 28 protected final TileLoaderListener listener; 29 protected final TileJobOptions options; 30 private static final IntegerProperty THREAD_LIMIT = 31 new IntegerProperty("imagery.vector.mvtloader.maxjobs", TMSCachedTileLoader.THREAD_LIMIT.getDefaultValue()); 32 private static final ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = 33 TMSCachedTileLoader.getNewThreadPoolExecutor("MVT-downloader-%d", THREAD_LIMIT.get()); 34 35 /** 36 * Constructor 37 * @param listener called when tile loading has finished 38 * @param cache of the cache 39 * @param options tile job options 40 */ 41 public MapBoxVectorCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache, 42 TileJobOptions options) { 43 CheckParameterUtil.ensureParameterNotNull(cache, "cache"); 44 this.cache = cache; 45 this.options = options; 46 this.listener = listener; 47 } 48 49 @Override 50 public void clearCache(TileSource source) { 51 this.cache.remove(source.getName() + ':'); 52 } 53 54 @Override 55 public TileJob createTileLoaderJob(Tile tile) { 56 return new MapBoxVectorCachedTileLoaderJob( 57 listener, 58 tile, 59 cache, 60 options, 61 getDownloadExecutor()); 62 } 63 64 @Override 65 public void cancelOutstandingTasks() { 66 final ThreadPoolExecutor executor = getDownloadExecutor(); 67 executor.getQueue().stream().filter(executor::remove).filter(MapBoxVectorCachedTileLoaderJob.class::isInstance) 68 .map(MapBoxVectorCachedTileLoaderJob.class::cast).forEach(JCSCachedTileLoaderJob::handleJobCancellation); 69 } 70 71 @Override 72 public boolean hasOutstandingTasks() { 73 return getDownloadExecutor().getTaskCount() > getDownloadExecutor().getCompletedTaskCount(); 74 } 75 76 private static ThreadPoolExecutor getDownloadExecutor() { 77 return DEFAULT_DOWNLOAD_JOB_DISPATCHER; 78 } 79 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.concurrent.ThreadPoolExecutor; 5 6 import org.openstreetmap.gui.jmapviewer.Tile; 7 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 8 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 9 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob; 10 import org.openstreetmap.josm.data.imagery.TileJobOptions; 11 12 import org.apache.commons.jcs3.access.behavior.ICacheAccess; 13 14 /** 15 * Bridge to JCS cache for MVT tiles 16 * @author Taylor Smock 17 * @since xxx 18 */ 19 public class MapBoxVectorCachedTileLoaderJob extends TMSCachedTileLoaderJob { 20 21 public MapBoxVectorCachedTileLoaderJob(TileLoaderListener listener, Tile tile, 22 ICacheAccess<String, BufferedImageCacheEntry> cache, TileJobOptions options, 23 ThreadPoolExecutor downloadExecutor) { 24 super(listener, tile, cache, options, downloadExecutor); 25 } 26 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 import java.io.IOException; 6 import java.io.InputStream; 7 import java.util.Collection; 8 import java.util.List; 9 import java.util.Objects; 10 import java.util.stream.Collectors; 11 12 import javax.json.Json; 13 import javax.json.JsonException; 14 import javax.json.JsonReader; 15 16 import org.openstreetmap.josm.data.imagery.ImageryInfo; 17 import org.openstreetmap.josm.data.imagery.JosmTemplatedTMSTileSource; 18 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapBoxVectorStyle; 19 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.Source; 20 import org.openstreetmap.josm.gui.ExtendedDialog; 21 import org.openstreetmap.josm.gui.MainApplication; 22 import org.openstreetmap.josm.gui.util.GuiHelper; 23 import org.openstreetmap.josm.gui.widgets.JosmComboBox; 24 import org.openstreetmap.josm.io.CachedFile; 25 import org.openstreetmap.josm.tools.Logging; 26 27 /** 28 * Tile Source handling for Mapbox Vector Tile sources 29 * @author Taylor Smock 30 * @since xxx 31 */ 32 public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource { 33 private final MapBoxVectorStyle styleSource; 34 35 public MapboxVectorTileSource(ImageryInfo info) { 36 super(info); 37 MapBoxVectorStyle mapBoxVectorStyle = null; 38 try (CachedFile style = new CachedFile(info.getUrl()); 39 InputStream inputStream = style.getInputStream(); 40 JsonReader reader = Json.createReader(inputStream)) { 41 reader.readObject(); 42 // OK, we have a stylesheet 43 mapBoxVectorStyle = MapBoxVectorStyle.getMapBoxVectorStyle(info.getUrl()); 44 } catch (IOException | JsonException e) { 45 Logging.trace(e); 46 } 47 this.styleSource = mapBoxVectorStyle; 48 if (this.styleSource != null) { 49 final Source source; 50 List<Source> sources = this.styleSource.getSources().keySet().stream().filter(Objects::nonNull) 51 .collect(Collectors.toList()); 52 if (sources.size() == 1) { 53 source = sources.get(0); 54 55 } else if (!sources.isEmpty()) { 56 // Ask user what source they want. 57 source = GuiHelper.runInEDTAndWaitAndReturn(() -> { 58 SelectLayerDialog dialog = new SelectLayerDialog(sources); 59 dialog.showDialog(); 60 return dialog.getSource(); 61 }); 62 } else { 63 // Umm. What happened? 64 throw new InvalidMapboxVectorTileException(tr("Cannot understand style source: {0}", info.getUrl())); 65 } 66 this.name = name + ": " + source.getName(); 67 // There can technically be multiple URL's for this field; unfortunately, JOSM can only handle one right now. 68 this.baseUrl = source.getUrls().get(0); 69 this.minZoom = source.getMinZoom(); 70 this.maxZoom = source.getMaxZoom(); 71 if (source.getAttributionText() != null) { 72 this.setAttributionText(source.getAttributionText()); 73 } 74 } 75 } 76 77 private static class SelectLayerDialog extends ExtendedDialog { 78 private final JosmComboBox<Source> comboBox; 79 80 /** 81 * Create a dialog to show the possible sources 82 * @param sources The sources to show 83 */ 84 SelectLayerDialog(Collection<Source> sources) { 85 super(MainApplication.getMainFrame(), tr("Select Vector Tile Layers"), tr("Add layers")); 86 this.comboBox = new JosmComboBox<>(sources.toArray(new Source[0])); 87 this.comboBox.setSelectedIndex(0); 88 setContent(comboBox); 89 } 90 91 /** 92 * Get the selected source 93 * @return The selected source 94 */ 95 public Source getSource() { 96 Source selected = (Source) this.comboBox.getSelectedItem(); 97 return selected != null ? selected : this.comboBox.getItemAt(0); 98 } 99 } 100 101 /** 102 * Get the style source for this Vector Tile source 103 * @return The source to use for styling 104 */ 105 public MapBoxVectorStyle getStyleSource() { 106 return this.styleSource; 107 } 108 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.Arrays; 5 import java.util.Collections; 6 import java.util.List; 7 8 /** 9 * Items that MAY be used to figure out if a file or server response MAY BE a Mapbox Vector Tile 10 * @author Taylor Smock 11 * @since xxx 12 */ 13 public final class MVTFile { 14 /** 15 * Extensions for Mapbox Vector Tiles. 16 * This is a SHOULD, <i>not</i> a MUST. 17 */ 18 public static final List<String> EXTENSION = Collections.unmodifiableList(Arrays.asList("mvt")); 19 20 /** 21 * mimetypes for Mapbox Vector Tiles 22 * This is a SHOULD, <i>not</i> a MUST. 23 */ 24 public static final List<String> MIMETYPE = Collections.unmodifiableList(Arrays.asList("application/vnd.mapbox-vector-tile")); 25 26 /** 27 * The default projection. This is Web Mercator, per specification. 28 */ 29 public static final String DEFAULT_PROJECTION = "EPSG:3857"; 30 31 private MVTFile() { 32 // Hide the constructor 33 } 34 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.awt.Graphics; 5 import java.io.IOException; 6 import java.io.InputStream; 7 import java.util.Collection; 8 import java.util.HashSet; 9 import java.util.List; 10 import java.util.Objects; 11 import java.util.stream.Collectors; 12 13 import org.openstreetmap.gui.jmapviewer.Tile; 14 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 15 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 16 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 17 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 18 import org.openstreetmap.josm.tools.ListenerList; 19 import org.openstreetmap.josm.tools.Logging; 20 21 /** 22 * A class for MapBox Vector Tiles 23 * 24 * @author Taylor Smock 25 * @since xxx 26 */ 27 public class MVTTile extends Tile implements VectorTile { 28 private final ListenerList<TileListener> listenerList = ListenerList.create(); 29 private Collection<Layer> layers; 30 private int extent = Layer.DEFAULT_EXTENT; 31 32 /** 33 * Create a new Tile 34 * @param source The source of the tile 35 * @param xtile The x coordinate for the tile 36 * @param ytile The y coordinate for the tile 37 * @param zoom The zoom for the tile 38 */ 39 public MVTTile(TileSource source, int xtile, int ytile, int zoom) { 40 super(source, xtile, ytile, zoom); 41 } 42 43 @Override 44 public void paint(final Graphics g, final int x, final int y) { 45 this.paint(g, x, y, 256, 256); 46 } 47 48 @Override 49 public void loadImage(final InputStream inputStream) throws IOException { 50 if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) { 51 this.initLoading(); 52 ProtoBufParser parser = new ProtoBufParser(inputStream); 53 Collection<ProtoBufRecord> protoBufRecords = parser.allRecords(); 54 this.layers = new HashSet<>(); 55 this.layers = protoBufRecords.stream().map(record -> { 56 Layer mvtLayer = null; 57 if (record.getField() == Layer.LAYER_FIELD) { 58 try (ProtoBufParser tParser = new ProtoBufParser(record.getBytes())) { 59 mvtLayer = new Layer(tParser.allRecords()); 60 } catch (IOException e) { 61 Logging.error(e); 62 } finally { 63 // Cleanup bytes 64 record.close(); 65 } 66 } 67 return mvtLayer; 68 }).collect(Collectors.toCollection(HashSet::new)); 69 this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT); 70 // TODO figure out a better way to free memory 71 this.finishLoading(); 72 this.listenerList.fireEvent(event -> event.finishedLoading(this)); 73 } 74 } 75 76 @Override 77 public Collection<Layer> getLayers() { 78 return this.layers; 79 } 80 81 @Override 82 public int getExtent() { 83 return this.extent; 84 } 85 86 /** 87 * Add a tile loader finisher listener 88 * 89 * @param listener The listener to add 90 */ 91 public void addTileLoaderFinisher(TileListener listener) { 92 // Add as weak listeners since we don't want to keep unnecessary references. 93 this.listenerList.addWeakListener(listener); 94 } 95 96 /** 97 * A class that can be notified that a tile has finished loading 98 * 99 * @author Taylor Smock 100 */ 101 public interface TileListener { 102 /** 103 * Called when the MVTTile is finished loading 104 * 105 * @param tile The tile that finished loading 106 */ 107 void finishedLoading(MVTTile tile); 108 } 109 110 /** 111 * A class used to set the layers that an MVTTile will show. 112 * 113 * @author Taylor Smock 114 */ 115 public interface LayerShower { 116 /** 117 * Get a list of layers to show 118 * 119 * @return A list of layer names 120 */ 121 List<String> layersToShow(); 122 } 123 124 @Override 125 public boolean equals(Object obj) { 126 if (!super.equals(obj) || obj.getClass().isAssignableFrom(this.getClass())) { 127 return false; 128 } 129 MVTTile other = (MVTTile) obj; 130 return extent == other.extent 131 && Objects.deepEquals(layers.toArray(), other.layers.toArray()); 132 } 133 134 @Override 135 public int hashCode() { 136 return Objects.hash(super.hashCode(), extent, layers); 137 } 138 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 /** 5 * The parameters that follow the {@link CommandInteger}. 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public final class ParameterInteger { 10 private ParameterInteger() { 11 // Hide constructor 12 } 13 14 /** 15 * Get the value for this ParameterInteger 16 * @param value The zig-zag and delta encoded value to decode 17 * @return The decoded integer value 18 */ 19 public static int decode(int value) { 20 return ((value >> 1) ^ -(value & 1)); 21 } 22 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --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 new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style; 3 4 import java.util.Arrays; 5 import java.util.stream.Collectors; 6 7 import javax.json.JsonArray; 8 import javax.json.JsonObject; 9 import javax.json.JsonString; 10 import javax.json.JsonValue; 11 12 /** 13 * A MapBox vector style expression 14 * @author Taylor Smock 15 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/">https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/</a> 16 * @since xxx 17 */ 18 public class Expression { 19 /** An empty expression to use */ 20 public static final Expression EMPTY_EXPRESSION = new Expression(JsonValue.NULL); 21 private static final String EMPTY_STRING = ""; 22 23 private final String expression; 24 25 /** 26 * Create a new filter expression. <i>Please note that this currently only supports basic comparators!</i> 27 * @param value The value to parse 28 */ 29 public Expression(JsonValue value) { 30 if (value.getValueType() == JsonValue.ValueType.ARRAY) { 31 final JsonArray array = value.asJsonArray(); 32 if (array.get(0).getValueType() == JsonValue.ValueType.STRING) { 33 if ("==".equals(array.getString(0))) { 34 // The mapcss equivalent of == is = (for the most part) 35 this.expression = convertToString(array.get(1)) + "=" + convertToString(array.get(2)); 36 } else if (Arrays.asList("<=", ">=", ">", "<", "!=").contains(array.getString(0))) { 37 this.expression = convertToString(array.get(1)) + array.getString(0) + convertToString(array.get(2)); 38 } else { 39 this.expression = EMPTY_STRING; 40 } 41 } else { 42 this.expression = EMPTY_STRING; 43 } 44 } else { 45 this.expression = EMPTY_STRING; 46 } 47 } 48 49 /** 50 * Convert a value to a string 51 * @param value The value to convert 52 * @return A string 53 */ 54 private static String convertToString(JsonValue value) { 55 switch (value.getValueType()) { 56 case STRING: 57 return ((JsonString) value).getString(); 58 case FALSE: 59 return Boolean.FALSE.toString(); 60 case TRUE: 61 return Boolean.TRUE.toString(); 62 case NUMBER: 63 return value.toString(); 64 case ARRAY: 65 return '[' 66 + ((JsonArray) value).stream().map(Expression::convertToString).collect(Collectors.joining(",")) 67 + ']'; 68 case OBJECT: 69 return '{' 70 + ((JsonObject) value).entrySet().stream() 71 .map(entry -> entry.getKey() + ":" + convertToString(entry.getValue())).collect( 72 Collectors.joining(",")) 73 + '}'; 74 case NULL: 75 default: 76 return EMPTY_STRING; 77 } 78 } 79 80 @Override 81 public String toString() { 82 return !EMPTY_STRING.equals(this.expression) ? '[' + this.expression + ']' : EMPTY_STRING; 83 } 84 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --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 new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style; 3 4 import java.awt.Font; 5 import java.awt.GraphicsEnvironment; 6 import java.text.MessageFormat; 7 import java.util.Arrays; 8 import java.util.Collection; 9 import java.util.List; 10 import java.util.Locale; 11 import java.util.regex.Matcher; 12 import java.util.regex.Pattern; 13 import java.util.stream.Collectors; 14 import java.util.stream.Stream; 15 16 import javax.json.JsonArray; 17 import javax.json.JsonNumber; 18 import javax.json.JsonObject; 19 import javax.json.JsonString; 20 import javax.json.JsonValue; 21 22 /** 23 * MapBox style layers 24 * @author Taylor Smock 25 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/">https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/</a> 26 * @since xxx 27 */ 28 public class Layers { 29 /** 30 * The layer type. This affects the rendering. 31 * @author Taylor Smock 32 * @since xxx 33 */ 34 enum Type { 35 /** Filled polygon with an (optional) border */ 36 FILL, 37 /** A line */ 38 LINE, 39 /** A symbol */ 40 SYMBOL, 41 /** A circle */ 42 CIRCLE, 43 /** A heatmap */ 44 HEATMAP, 45 /** A 3D polygon extrusion */ 46 FILL_EXTRUSION, 47 /** Raster */ 48 RASTER, 49 /** Hillshade data */ 50 HILLSHADE, 51 /** A background color or pattern */ 52 BACKGROUND, 53 /** The fallback layer */ 54 SKY 55 } 56 57 private static final String EMPTY_STRING = ""; 58 private static final char SEMI_COLON = ';'; 59 private static final Pattern CURLY_BRACES = Pattern.compile("(\\{(.*?)})"); 60 61 /** A required unique layer name */ 62 private final String id; 63 /** The required type */ 64 private final Type type; 65 /** An optional expression */ 66 private final Expression filter; 67 /** The max zoom for the layer */ 68 private final int maxZoom; 69 /** The min zoom for the layer */ 70 private final int minZoom; 71 72 /** Default paint properties for this layer */ 73 private final String paint; 74 75 /** A source description to be used with this layer. Required for everything <i>but</i> {@link Type#BACKGROUND} */ 76 private final String source; 77 /** Layer to use from the vector tile source. Only allowed with {@link SourceType#VECTOR}. */ 78 private final String sourceLayer; 79 80 /** 81 * Create a layer object 82 * @param commonStyleInformation The MapBoxVectorStyle with common information 83 * @param layerInfo The info to use to create the layer 84 */ 85 public Layers(final JsonObject layerInfo) { 86 this.id = layerInfo.getString("id"); 87 this.type = Type.valueOf(layerInfo.getString("type").replace("-", "_").toUpperCase(Locale.ROOT)); 88 if (layerInfo.containsKey("filter")) { 89 this.filter = new Expression(layerInfo.get("filter")); 90 } else { 91 this.filter = Expression.EMPTY_EXPRESSION; 92 } 93 this.maxZoom = layerInfo.getInt("maxzoom", Integer.MAX_VALUE); 94 this.minZoom = layerInfo.getInt("minzoom", Integer.MIN_VALUE); 95 // There is a metadata field (I don't *think* I need it?) 96 // source is only optional with {@link Type#BACKGROUND}. 97 if (this.type == Type.BACKGROUND) { 98 this.source = layerInfo.getString("source", null); 99 } else { 100 this.source = layerInfo.getString("source"); 101 } 102 if (layerInfo.containsKey("paint") && layerInfo.get("paint").getValueType() == JsonValue.ValueType.OBJECT) { 103 final JsonObject paintObject = layerInfo.getJsonObject("paint"); 104 final JsonObject layoutObject = layerInfo.getJsonObject("layout"); 105 // Don't throw exceptions here, since we may just point at the styling 106 switch (type) { 107 case FILL: 108 // area 109 this.paint = parsePaintFill(paintObject); 110 break; 111 case LINE: 112 // way 113 this.paint = parsePaintLine(paintObject); 114 break; 115 case CIRCLE: 116 // point 117 this.paint = parsePaintCircle(paintObject); 118 break; 119 case SYMBOL: 120 // point 121 this.paint = parsePaintSymbol(layoutObject, paintObject); 122 break; 123 case BACKGROUND: 124 // canvas only 125 this.paint = parsePaintBackground(paintObject); 126 break; 127 default: 128 this.paint = EMPTY_STRING; 129 } 130 } else { 131 this.paint = null; 132 } 133 this.sourceLayer = layerInfo.getString("source-layer", null); 134 } 135 136 private static String parsePaintLine(final JsonObject paintObject) { 137 if (!checkVisibility(paintObject)) { 138 return ""; 139 } 140 final StringBuilder sb = new StringBuilder(); 141 // line-blur, default 0 (px) 142 // line-color, default #000000, disabled by line-pattern 143 final String color = paintObject.getString("line-color", "#000000"); 144 if (color != null) { 145 sb.append("color:").append(color).append(SEMI_COLON); 146 } 147 // line-opacity, default 1 (0-1) 148 final JsonNumber opacity = paintObject.getJsonNumber("line-opacity"); 149 if (opacity != null) { 150 sb.append("opacity:").append(opacity.numberValue().doubleValue()).append(SEMI_COLON); 151 } 152 // line-cap, default butt (butt|round|square) 153 final String cap = paintObject.getString("line-cap", "butt"); 154 sb.append("linecap:"); 155 switch (cap) { 156 case "round": 157 case "square": 158 sb.append(cap); 159 break; 160 case "butt": 161 default: 162 sb.append("none"); 163 } 164 165 sb.append(SEMI_COLON); 166 // line-dasharray, array of number >= 0, units in line widths, disabled by line-pattern 167 if (paintObject.containsKey("line-dasharray")) { 168 final JsonArray dashArray = paintObject.getJsonArray("line-dasharray"); 169 sb.append("dashes:"); 170 sb.append(dashArray.stream().filter(JsonNumber.class::isInstance).map(JsonNumber.class::cast) 171 .map(JsonNumber::toString).collect(Collectors.joining(","))); 172 sb.append(SEMI_COLON); 173 } 174 // line-gap-width 175 // line-gradient 176 // line-join 177 // line-miter-limit 178 // line-offset 179 // line-pattern TODO this first, since it disables stuff 180 // line-round-limit 181 // line-sort-key 182 // line-translate 183 // line-translate-anchor 184 // line-width 185 final JsonNumber width = paintObject.getJsonNumber("line-width"); 186 sb.append("width:").append(width == null ? 1 : width.toString()).append(SEMI_COLON); 187 return sb.toString(); 188 } 189 190 private static String parsePaintCircle(final JsonObject paintObject) { 191 if (!checkVisibility(paintObject)) { 192 return EMPTY_STRING; 193 } 194 final StringBuilder sb = new StringBuilder("symbol-shape:circle;"); 195 // circle-blur 196 // circle-color 197 sb.append("symbol-fill-color:").append(paintObject.getString("circle-color", "#000000")).append(SEMI_COLON); 198 // circle-opacity 199 final JsonNumber fillOpacity = paintObject.getJsonNumber("circle-opacity"); 200 sb.append("symbol-fill-opacity:").append(fillOpacity != null ? fillOpacity.numberValue().toString() : "1").append(SEMI_COLON); 201 // circle-pitch-alignment // not 3D 202 // circle-pitch-scale // not 3D 203 // circle-radius 204 final JsonNumber radius = paintObject.getJsonNumber("circle-radius"); 205 sb.append("symbol-size:").append(radius != null ? (2 * radius.numberValue().doubleValue()) : "10").append(SEMI_COLON); 206 // circle-sort-key 207 // circle-stroke-color 208 sb.append("symbol-stroke-color:").append(paintObject.getString("circle-stroke-color", "#000000")).append(SEMI_COLON); 209 // circle-stroke-opacity 210 final JsonNumber strokeOpacity = paintObject.getJsonNumber("circle-stroke-opacity"); 211 sb.append("symbol-stroke-opacity:").append(strokeOpacity != null ? strokeOpacity.numberValue().toString() : "1").append(SEMI_COLON); 212 // circle-stroke-width 213 final JsonNumber strokeWidth = paintObject.getJsonNumber("circle-stroke-width"); 214 sb.append("symbol-stroke-width:").append(strokeWidth != null ? strokeWidth.numberValue().toString() : "0").append(SEMI_COLON); 215 // circle-translate 216 // circle-translate-anchor 217 return sb.toString(); 218 } 219 220 private static String parsePaintSymbol( 221 final JsonObject layoutObject, 222 final JsonObject paintObject) { 223 if (!checkVisibility(paintObject)) { 224 return EMPTY_STRING; 225 } 226 final StringBuilder sb = new StringBuilder(); 227 // icon-allow-overlap 228 // icon-anchor 229 // icon-color 230 // icon-halo-blur 231 // icon-halo-color 232 // icon-halo-width 233 // icon-ignore-placement 234 // icon-image 235 boolean iconImage = false; 236 if (layoutObject.containsKey("icon-image")) { 237 sb.append("icon-image:concat("); 238 Matcher matcher = CURLY_BRACES.matcher(layoutObject.getString("icon-image")); 239 StringBuffer stringBuffer = new StringBuffer(); 240 int previousMatch; 241 if (matcher.lookingAt()) { 242 matcher.appendReplacement(stringBuffer, "tag(\"$2\"), \""); 243 previousMatch = matcher.end(); 244 } else { 245 previousMatch = 0; 246 stringBuffer.append('"'); 247 } 248 while (matcher.find()) { 249 if (matcher.start() == previousMatch) { 250 matcher.appendReplacement(stringBuffer, ",tag(\"$2\")"); 251 } else { 252 matcher.appendReplacement(stringBuffer, "\", tag(\"$2\"), \""); 253 } 254 previousMatch = matcher.end(); 255 } 256 if (matcher.hitEnd()) { 257 stringBuffer.delete(stringBuffer.length() - ", \"".length(), stringBuffer.length()); 258 } else { 259 stringBuffer.append('"'); 260 } 261 matcher.appendTail(stringBuffer); 262 263 sb.append(stringBuffer).append(')').append(SEMI_COLON); 264 iconImage = true; 265 } 266 // icon-keep-upright 267 // icon-offset 268 if (iconImage && layoutObject.containsKey("icon-offset")) { 269 // default [0, 0], right,down == positive, left,up == negative 270 final List<JsonNumber> offset = layoutObject.getJsonArray("icon-offset").getValuesAs(JsonNumber.class); 271 // Assume that the offset must be size 2. Probably not necessary, but docs aren't necessary clear. 272 if (offset.size() == 2) { 273 sb.append("icon-offset-x:").append(offset.get(0).doubleValue()).append(SEMI_COLON) 274 .append("icon-offset-y:").append(offset.get(1).doubleValue()).append(SEMI_COLON); 275 } 276 } 277 // icon-opacity 278 if (iconImage && paintObject.containsKey("icon-opacity")) { 279 final double opacity = layoutObject.getJsonNumber("icon-opacity").doubleValue(); 280 sb.append("icon-opacity:").append(opacity).append(SEMI_COLON); 281 } 282 // icon-optional 283 // icon-padding 284 // icon-pitch-alignment 285 // icon-rotate 286 if (iconImage && layoutObject.containsKey("icon-rotate")) { 287 final double rotation = layoutObject.getJsonNumber("icon-rotate").doubleValue(); 288 sb.append("icon-rotation:").append(rotation).append(SEMI_COLON); 289 } 290 // icon-rotation-alignment 291 // icon-size 292 // icon-text-fit 293 // icon-text-fit-padding 294 // icon-translate 295 // icon-translate-anchor 296 // symbol-avoid-edges 297 // symbol-placement 298 // symbol-sort-key 299 // symbol-spacing 300 // symbol-z-order 301 // text-allow-overlap 302 // text-anchor 303 // text-color 304 if (paintObject.containsKey("text-color")) { 305 sb.append("text-color:").append(paintObject.getString("text-color")).append(SEMI_COLON); 306 } 307 // text-field 308 if (paintObject.containsKey("text-field")) { 309 sb.append("text:") 310 .append(paintObject.getString("text-field").replace("}", EMPTY_STRING).replace("{", EMPTY_STRING)) 311 .append(SEMI_COLON); 312 } 313 // text-font 314 if (paintObject.containsKey("text-font")) { 315 List<String> fonts = paintObject.getJsonArray("text-font").stream().filter(JsonString.class::isInstance) 316 .map(JsonString.class::cast).map(JsonString::getString).collect(Collectors.toList()); 317 Font[] systemFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(); 318 for (String fontString : fonts) { 319 Collection<Font> fontMatches = Stream.of(systemFonts) 320 .filter(font -> Arrays.asList(font.getName(), font.getFontName(), font.getFamily(), font.getPSName()).contains(fontString)) 321 .collect(Collectors.toList()); 322 if (!fontMatches.isEmpty()) { 323 final Font setFont = fontMatches.stream().filter(font -> font.getName().equals(fontString)).findAny() 324 .orElseGet(() -> fontMatches.stream().filter(font -> font.getFontName().equals(fontString)).findAny() 325 .orElseGet(() -> fontMatches.stream().filter(font -> font.getPSName().equals(fontString)).findAny() 326 .orElseGet(() -> fontMatches.stream().filter(font -> font.getFamily().equals(fontString)).findAny().orElse(null)))); 327 if (setFont != null) { 328 sb.append("font-family:\"").append(setFont.getFamily()).append("\"").append(SEMI_COLON); 329 sb.append("font-weight:").append(setFont.isBold() ? "bold" : "normal").append(SEMI_COLON); 330 sb.append("font-style:").append(setFont.isItalic() ? "italic" : "normal").append(SEMI_COLON); 331 break; 332 } 333 } 334 } 335 } 336 // text-halo-blur 337 // text-halo-color 338 if (paintObject.containsKey("text-halo-color")) { 339 sb.append("text-halo-color:").append(paintObject.getString("text-halo-color")).append(SEMI_COLON); 340 } 341 // text-halo-width 342 if (paintObject.containsKey("text-halo-width")) { 343 sb.append("text-halo-radius:").append(paintObject.getString("text-halo-width")).append(SEMI_COLON); 344 } 345 // text-ignore-placement 346 // text-justify 347 // text-keep-upright 348 // text-letter-spacing 349 // text-line-height 350 // text-max-angle 351 // text-max-width 352 // text-offset 353 // text-opacity 354 if (paintObject.containsKey("text-opacity")) { 355 sb.append("text-opacity:").append(paintObject.getJsonNumber("text-opacity").doubleValue()).append(SEMI_COLON); 356 } 357 // text-optional 358 // text-padding 359 // text-pitch-alignment 360 // text-radial-offset 361 // text-rotate 362 // text-rotation-alignment 363 // text-size 364 final JsonNumber textSize = paintObject.getJsonNumber("text-size"); 365 sb.append("font-size:").append(textSize != null ? textSize.numberValue().toString() : "16").append(SEMI_COLON); 366 // text-transform 367 // text-translate 368 // text-translate-anchor 369 // text-variable-anchor 370 // text-writing-mode 371 return sb.toString(); 372 } 373 374 private static String parsePaintBackground(final JsonObject paintObject) { 375 if (!checkVisibility(paintObject)) { 376 return EMPTY_STRING; 377 } 378 final StringBuilder sb = new StringBuilder(20); 379 // background-color 380 final String bgColor = paintObject.getString("background-color", null); 381 if (bgColor != null) { 382 sb.append("fill-color:").append(bgColor).append(SEMI_COLON); 383 } 384 // background-opacity 385 // background-pattern 386 return sb.toString(); 387 } 388 389 private static String parsePaintFill(final JsonObject paintObject) { 390 if (!checkVisibility(paintObject)) { 391 return EMPTY_STRING; 392 } 393 StringBuilder sb = new StringBuilder(); 394 // fill-antialias 395 // fill-color 396 sb.append("fill-color:").append(paintObject.getString("fill-color", "#000000")).append(SEMI_COLON); 397 // fill-opacity 398 final JsonNumber opacity = paintObject.getJsonNumber("fill-opacity"); 399 sb.append("fill-opacity:").append(opacity != null ? opacity.numberValue().toString() : "1").append(SEMI_COLON); 400 // fill-outline-color 401 sb.append("color:").append(paintObject.getString("fill-outline-color", 402 paintObject.getString("fill-color", "#000000"))).append(SEMI_COLON); 403 // fill-pattern 404 // fill-sort-key 405 // fill-translate 406 // fill-translate-anchor 407 return sb.toString(); 408 } 409 410 /** 411 * Check if the layer is displayed 412 * @param paintObject The paint to check 413 * @return {@code true} if the layer should be visible 414 */ 415 private static boolean checkVisibility(final JsonObject paintObject) { 416 return "visible".equals(paintObject.getString("visibility", "visible")); 417 } 418 419 @Override 420 public String toString() { 421 final String zoomSelector; 422 if (this.minZoom == this.maxZoom) { 423 zoomSelector = "|z" + this.minZoom; 424 } else if (this.minZoom > Integer.MIN_VALUE && this.maxZoom == Integer.MAX_VALUE) { 425 zoomSelector = "|z" + this.minZoom + "-"; 426 } else if (this.minZoom == Integer.MIN_VALUE && this.maxZoom < Integer.MAX_VALUE) { 427 zoomSelector = "|z-" + this.maxZoom; 428 } else if (this.minZoom > Integer.MIN_VALUE) { 429 zoomSelector = MessageFormat.format("|z{0}-{1}", this.minZoom, this.maxZoom); 430 } else { 431 zoomSelector = EMPTY_STRING; 432 } 433 final String commonData = zoomSelector + this.filter.toString() + "::" + this.id + "{" + this.paint + "}"; 434 if (this.type == Type.BACKGROUND) { 435 // AFAIK, paint has no zoom levels, and doesn't accept a layer 436 return "canvas{" + this.paint + "}"; 437 } else if (this.type == Type.CIRCLE || this.type == Type.SYMBOL) { 438 return "node" + commonData; 439 } else if (this.type == Type.FILL) { 440 return "area" + commonData; 441 } else if (this.type == Type.LINE) { 442 return "way" + commonData; 443 } 444 return super.toString(); 445 } 446 447 /** 448 * Get the source that this applies to 449 * @return The source name 450 */ 451 public String getSource() { 452 return this.source; 453 } 454 455 /** 456 * Get the layer that this applies to 457 * @return The layer name 458 */ 459 public String getSourceLayer() { 460 return this.sourceLayer; 461 } 462 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --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 new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.image.BufferedImage; 7 import java.io.BufferedReader; 8 import java.io.File; 9 import java.io.FileOutputStream; 10 import java.io.IOException; 11 import java.io.InputStream; 12 import java.nio.charset.StandardCharsets; 13 import java.util.Collections; 14 import java.util.HashMap; 15 import java.util.List; 16 import java.util.Map; 17 import java.util.Objects; 18 import java.util.Optional; 19 import java.util.concurrent.ConcurrentHashMap; 20 import java.util.stream.Collectors; 21 22 import javax.imageio.ImageIO; 23 import javax.json.Json; 24 import javax.json.JsonArray; 25 import javax.json.JsonObject; 26 import javax.json.JsonReader; 27 import javax.json.JsonStructure; 28 import javax.json.JsonValue; 29 30 import org.openstreetmap.josm.data.preferences.JosmBaseDirectories; 31 import org.openstreetmap.josm.gui.MainApplication; 32 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 33 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 34 import org.openstreetmap.josm.io.CachedFile; 35 import org.openstreetmap.josm.tools.Logging; 36 37 /** 38 * Create a mapping for a Mapbox Vector Style 39 * 40 * @author Taylor Smock 41 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/">https://docs.mapbox.com/mapbox-gl-js/style-spec/</a> 42 * @since xxx 43 */ 44 public class MapBoxVectorStyle { 45 46 private static final ConcurrentHashMap<String, MapBoxVectorStyle> STYLE_MAPPING = new ConcurrentHashMap<>(); 47 48 /** 49 * Get a MapBoxVector style for a URL 50 * @param url The url to get 51 * @return The MapBox Vector Style. May be {@code null} if there was an error. 52 */ 53 public static MapBoxVectorStyle getMapBoxVectorStyle(String url) { 54 return STYLE_MAPPING.computeIfAbsent(url, key -> { 55 try (CachedFile style = new CachedFile(url); 56 BufferedReader reader = style.getContentReader(); 57 JsonReader jsonReader = Json.createReader(reader)) { 58 JsonStructure structure = jsonReader.read(); 59 return new MapBoxVectorStyle(structure.asJsonObject()); 60 } catch (IOException e) { 61 Logging.error(e); 62 } 63 // Documentation indicates that this will <i>not</i> be entered into the map, which means that this will be 64 // retried if something goes wrong. 65 return null; 66 }); 67 } 68 69 /** The version for the style specification */ 70 private final int version; 71 /** The optional name for the vector style */ 72 private final String name; 73 /** The optional URL for sprites. This mush be absolute (so it must contain the scheme, authority, and path). */ 74 private final String spriteUrl; 75 /** The optional URL for glyphs. This may have replaceable values in it. */ 76 private final String glyphUrl; 77 /** The required collection of sources with a list of layers that are applicable for that source*/ 78 private final Map<Source, ElemStyles> sources; 79 80 /** 81 * Create a new MapBoxVector style. You should prefer {@link #getMapBoxVectorStyle(String)} 82 * for deduplication purposes. 83 * 84 * @param jsonObject The object to create the style from 85 * @see #getMapBoxVectorStyle(String) 86 */ 87 public MapBoxVectorStyle(JsonObject jsonObject) { 88 // There should be a version specifier. We currently only support version 8. 89 // This can throw an NPE when there is no version number. 90 this.version = jsonObject.getInt("version"); 91 if (this.version == 8) { 92 this.name = jsonObject.getString("name", null); 93 this.spriteUrl = jsonObject.getString("sprite", null); 94 this.glyphUrl = jsonObject.getString("glyphs", null); 95 final List<Source> sources; 96 if (jsonObject.containsKey("sources") && jsonObject.get("sources").getValueType() == JsonValue.ValueType.OBJECT) { 97 final JsonObject sourceObj = jsonObject.getJsonObject("sources"); 98 sources = sourceObj.entrySet().stream().filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT) 99 .map(entry -> new Source(entry.getKey(), entry.getValue().asJsonObject())).collect(Collectors.toList()); 100 } else { 101 sources = Collections.emptyList(); 102 } 103 final List<Layers> layers; 104 if (jsonObject.containsKey("layers") && jsonObject.get("layers").getValueType() == JsonValue.ValueType.ARRAY) { 105 JsonArray lArray = jsonObject.getJsonArray("layers"); 106 layers = lArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast).map(Layers::new) 107 .collect(Collectors.toList()); 108 } else { 109 layers = Collections.emptyList(); 110 } 111 final Map<Optional<Source>, List<Layers>> sourceLayer = layers.stream().collect( 112 Collectors.groupingBy(layer -> sources.stream().filter(source -> source.getName().equals(layer.getSource())) 113 .findFirst())); 114 // Abuse HashMap null (null == default) 115 this.sources = new HashMap<>(); 116 for (Map.Entry<Optional<Source>, List<Layers>> entry : sourceLayer.entrySet()) { 117 final Source source = entry.getKey().orElse(null); 118 final String data = entry.getValue().stream().map(Layers::toString).collect(Collectors.joining()); 119 final String metaData = "meta{title:" + (source == null ? "Generated Style" : 120 source.getName()) + ";version:\"autogenerated\";description:\"auto generated style\";}"; 121 122 // This is the default canvas 123 final String canvas = "canvas{default-points:false;default-lines:false;}"; 124 final MapCSSStyleSource style = new MapCSSStyleSource(metaData + canvas + data); 125 // Save to directory 126 MainApplication.worker.execute(() -> this.save((source == null ? data.hashCode() : source.getName()) + ".mapcss", style)); 127 this.sources.put(source, new ElemStyles(Collections.singleton(style))); 128 } 129 if (this.spriteUrl != null && !this.spriteUrl.trim().isEmpty()) { 130 MainApplication.worker.execute(this::fetchSprites); 131 } 132 } else { 133 throw new IllegalArgumentException(tr("Vector Tile Style Version not understood: version {0} (json: {1})", 134 this.version, jsonObject)); 135 } 136 } 137 138 /** 139 * Fetch sprites. Please note that this is (literally) only png. Unfortunately. 140 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/</a> 141 */ 142 private void fetchSprites() { 143 // HiDPI images first -- if this succeeds, don't bother with the lower resolution (JOSM has no method to switch) 144 try (CachedFile spriteJson = new CachedFile(this.spriteUrl + "@2x.json"); 145 CachedFile spritePng = new CachedFile(this.spriteUrl + "@2x.png")) { 146 if (parseSprites(spriteJson, spritePng)) { 147 return; 148 } 149 } 150 try (CachedFile spriteJson = new CachedFile(this.spriteUrl + ".json"); 151 CachedFile spritePng = new CachedFile(this.spriteUrl + ".png")) { 152 parseSprites(spriteJson, spritePng); 153 } 154 } 155 156 private boolean parseSprites(CachedFile spriteJson, CachedFile spritePng) { 157 /* JSON looks like this: 158 * { "image-name": {"width": width, "height": height, "x": x, "y": y, "pixelRatio": 1 }} 159 * width/height are the dimensions of the image 160 * x -- distance right from top left 161 * y -- distance down from top left 162 * pixelRatio -- this <i>appears</i> to be from the "@2x" (default 1) 163 * content -- [left, top corner, right, bottom corner] 164 * stretchX -- [[from, to], [from, to], ...] 165 * stretchY -- [[from, to], [from, to], ...] 166 */ 167 final JsonObject spriteObject; 168 final BufferedImage spritePngImage; 169 try (BufferedReader spriteJsonBufferedReader = spriteJson.getContentReader(); 170 JsonReader spriteJsonReader = Json.createReader(spriteJsonBufferedReader); 171 InputStream spritePngBufferedReader = spritePng.getInputStream() 172 ) { 173 spriteObject = spriteJsonReader.read().asJsonObject(); 174 spritePngImage = ImageIO.read(spritePngBufferedReader); 175 } catch (IOException e) { 176 Logging.error(e); 177 return false; 178 } 179 for (Map.Entry<String, JsonValue> entry : spriteObject.entrySet()) { 180 final JsonObject info = entry.getValue().asJsonObject(); 181 int width = info.getInt("width"); 182 int height = info.getInt("height"); 183 int x = info.getInt("x"); 184 int y = info.getInt("y"); 185 save(entry.getKey() + ".png", spritePngImage.getSubimage(x, y, width, height)); 186 } 187 return true; 188 } 189 190 private void save(String name, Object object) { 191 final File cache = JosmBaseDirectories.getInstance().getCacheDirectory(true); 192 final File location = new File(cache.getPath(), this.name != null ? this.name : Integer.toString(this.hashCode())); 193 if (!location.exists()) { 194 location.mkdir(); 195 } else if (location.exists() && !location.isDirectory()) { 196 // Don't try to save if the file exists and is not a directory 197 return; 198 } 199 final File toSave = new File(location, name); 200 try (FileOutputStream fileOutputStream = new FileOutputStream(toSave)) { 201 if (object instanceof String) { 202 fileOutputStream.write(((String) object).getBytes(StandardCharsets.UTF_8)); 203 } else if (object instanceof MapCSSStyleSource) { 204 MapCSSStyleSource source = (MapCSSStyleSource) object; 205 try (InputStream inputStream = source.getSourceInputStream()) { 206 int byteVal = inputStream.read(); 207 do { 208 fileOutputStream.write(byteVal); 209 byteVal = inputStream.read(); 210 } while (byteVal > -1); 211 source.url = "file:/" + toSave.getAbsolutePath().replace('\\', '/'); 212 if (source.isLoaded()) { 213 source.loadStyleSource(); 214 } 215 } 216 } else if (object instanceof BufferedImage) { 217 ImageIO.write((BufferedImage) object, "png", toSave); 218 } 219 } catch (IOException e) { 220 Logging.info(e); 221 } 222 } 223 224 /** 225 * Get the generated layer->style mapping 226 * @return The mapping (use to enable/disable a paint style) 227 */ 228 public Map<Source, ElemStyles> getSources() { 229 return this.sources; 230 } 231 232 /** 233 * Get the sprite url for the style 234 * @return The base sprite url 235 */ 236 public String getSpriteUrl() { 237 return this.spriteUrl; 238 } 239 240 @Override 241 public int hashCode() { 242 return Objects.hash(this.name, this.version, this.glyphUrl, this.spriteUrl); 243 } 244 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --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 new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style; 3 4 /** 5 * The scheme used for tiles 6 */ 7 public enum Scheme { 8 /** Standard slippy map scheme */ 9 XYZ, 10 /** OSGeo specification scheme */ 11 TMS 12 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --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 new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style; 3 4 import java.util.ArrayList; 5 import java.util.Collection; 6 import java.util.Collections; 7 import java.util.List; 8 import java.util.Locale; 9 import java.util.stream.Collectors; 10 11 import javax.json.JsonArray; 12 import javax.json.JsonObject; 13 import javax.json.JsonString; 14 import javax.json.JsonValue; 15 16 import org.openstreetmap.josm.data.Bounds; 17 18 /** 19 * A source from a MapBox Vector Style 20 * 21 * @author Taylor Smock 22 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/</a> 23 * @since xxx 24 */ 25 public class Source { 26 /** 27 * WMS servers should contain a "{bbox-epsg-3857}" parameter for the bbox 28 */ 29 private static final String WMS_BBOX = "bbox-epsg-3857"; 30 31 /* Common items */ 32 /** 33 * The name of the source 34 */ 35 private final String name; 36 /** 37 * The type of the source 38 */ 39 private final SourceType sourceType; 40 41 /* Common tiled data */ 42 /** 43 * The minimum zoom supported 44 */ 45 private final int minZoom; 46 /** 47 * The maximum zoom supported 48 */ 49 private final int maxZoom; 50 /** 51 * The tile urls. These usually have replaceable fields. 52 */ 53 private final List<String> tileUrls; 54 55 /* Vector and raster data */ 56 /** 57 * The attribution to display for the user 58 */ 59 private final String attribution; 60 /** 61 * The bounds of the data. We should not request data outside of the bounds 62 */ 63 private final Bounds bounds; 64 /** 65 * The property to use as a feature id. Can be parameterized 66 */ 67 private final String promoteId; 68 /** 69 * The tile scheme 70 */ 71 private final Scheme scheme; 72 /** 73 * {@code true} if the tiles should not be cached 74 */ 75 private final boolean volatileCache; 76 77 /* Raster data */ 78 /** 79 * The tile size 80 */ 81 private final int tileSize; 82 83 /** 84 * Create a new Source object 85 * 86 * @param name The name of the source object 87 * @param data The data to set the source information with 88 */ 89 public Source(final String name, final JsonObject data) { 90 this.name = name; 91 // "type" is required 92 final String type = data.getString("type"); 93 this.sourceType = SourceType.valueOf(type.replace("-", "_").toUpperCase(Locale.ROOT)); 94 // This can also contain SourceType.RASTER_DEM (only needs encoding) 95 if (SourceType.VECTOR == this.sourceType || SourceType.RASTER == this.sourceType) { 96 if (data.containsKey("url")) { 97 // TODO implement https://github.com/mapbox/tilejson-spec 98 throw new UnsupportedOperationException(); 99 } else { 100 this.minZoom = data.getInt("minzoom", 0); 101 this.maxZoom = data.getInt("maxzoom", 22); 102 this.attribution = data.getString("attribution", null); 103 if (data.containsKey("bounds") && data.get("bounds").getValueType() == JsonValue.ValueType.ARRAY) { 104 final JsonArray bJsonArray = data.getJsonArray("bounds"); 105 final double[] bArray = new double[4]; 106 for (int i = 0; i < 4; i++) { 107 bArray[i] = bJsonArray.getJsonNumber(i).doubleValue(); 108 } 109 // The order in the response is 110 // [south-west longitude, south-west latitude, north-east longitude, north-east latitude] 111 this.bounds = new Bounds(bArray[1], bArray[0], bArray[3], bArray[2]); 112 } else { 113 this.bounds = new Bounds(-85.051129, -180, 85.051129, 180); 114 } 115 this.promoteId = data.getString("promoteId", null); 116 this.scheme = Scheme.valueOf(data.getString("scheme", "xyz").toUpperCase(Locale.ROOT)); 117 if (data.containsKey("tiles") && data.get("tiles").getValueType() == JsonValue.ValueType.ARRAY) { 118 this.tileUrls = data.getJsonArray("tiles").stream().filter(JsonString.class::isInstance) 119 .map(JsonString.class::cast).map(JsonString::getString) 120 // Replace bbox-epsg-3857 with bbox (already encased with {}) 121 .map(url -> url.replace(WMS_BBOX, "bbox")).collect(Collectors.toList()); 122 } else { 123 this.tileUrls = Collections.emptyList(); 124 } 125 this.volatileCache = data.getBoolean("volatile", false); 126 this.tileSize = data.getInt("tileSize", 512); 127 } 128 } else { 129 throw new UnsupportedOperationException(); 130 } 131 } 132 133 /** 134 * Get the source name 135 * @return the name 136 */ 137 public String getName() { 138 return name; 139 } 140 141 /** 142 * Get the URLs that can be used to get vector data 143 * 144 * @return The urls 145 */ 146 public List<String> getUrls() { 147 return Collections.unmodifiableList(this.tileUrls); 148 } 149 150 /** 151 * Get the minimum zoom 152 * 153 * @return The min zoom (default {@code 0}) 154 */ 155 public int getMinZoom() { 156 return this.minZoom; 157 } 158 159 /** 160 * Get the max zoom 161 * 162 * @return The max zoom (default {@code 22}) 163 */ 164 public int getMaxZoom() { 165 return this.maxZoom; 166 } 167 168 /** 169 * Get the attribution for this source 170 * 171 * @return The attribution text. May be {@code null}. 172 */ 173 public String getAttributionText() { 174 return this.attribution; 175 } 176 177 @Override 178 public String toString() { 179 Collection<String> parts = new ArrayList<>(1 + this.getUrls().size()); 180 parts.add(this.getName()); 181 parts.addAll(this.getUrls()); 182 return String.join(" ", parts); 183 } 184 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --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 new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style; 3 4 /** 5 * The "source type" for the data (MapBox Vector Style specification) 6 * 7 * @author Taylor Smock 8 * @since xxx 9 */ 10 public enum SourceType { 11 VECTOR, 12 RASTER, 13 RASTER_DEM, 14 GEOJSON, 15 IMAGE, 16 VIDEO 17 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile; 3 4 import java.util.Collection; 5 6 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 7 8 /** 9 * An interface that is used to draw vector tiles, instead of using images 10 * @author Taylor Smock 11 * @since xxx 12 */ 13 public interface VectorTile { 14 /** 15 * Get the layers for this vector tile 16 * @return A collection of layers 17 */ 18 Collection<Layer> getLayers(); 19 20 /** 21 * Get the extent of the tile (in pixels) 22 * @return The tile extent (pixels) 23 */ 24 int getExtent(); 25 } -
src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java b/src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
a b 1637 1637 RenderBenchmarkCollector benchmark = benchmarkFactory.get(); 1638 1638 BBox bbox = bounds.toBBox(); 1639 1639 getSettings(renderVirtualNodes); 1640 1641 1640 try { 1642 1641 if (data.getReadLock().tryLock(1, TimeUnit.SECONDS)) { 1643 1642 try { -
src/org/openstreetmap/josm/data/osm/IPrimitive.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/IPrimitive.java b/src/org/openstreetmap/josm/data/osm/IPrimitive.java
a b 369 369 return getName(); 370 370 } 371 371 372 /** 373 * Get an object to synchronize the style cache on. This <i>should</i> be a field that does not change during paint. 374 * By default, it returns the current object, but should be overriden to avoid some performance issues. 375 * @return A non-{@code null} object to synchronize on when painting 376 */ 377 default Object getStyleCacheSyncObject() { 378 return this; 379 } 380 372 381 /** 373 382 * Replies the display name of a primitive formatted by <code>formatter</code> 374 383 * @param formatter formatter to use -
src/org/openstreetmap/josm/data/osm/IRelationMember.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/IRelationMember.java b/src/org/openstreetmap/josm/data/osm/IRelationMember.java
a b 66 66 * @since 13766 (IRelationMember) 67 67 */ 68 68 P getMember(); 69 70 /** 71 * Returns the relation member as a way. 72 * @return Member as a way 73 * @since xxx 74 */ 75 default IWay<?> getWay() { 76 return (IWay<?>) getMember(); 77 } 69 78 } -
new file src/org/openstreetmap/josm/data/osm/IWaySegment.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/IWaySegment.java b/src/org/openstreetmap/josm/data/osm/IWaySegment.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm; 3 4 import java.awt.geom.Line2D; 5 import java.lang.reflect.Constructor; 6 import java.lang.reflect.InvocationTargetException; 7 import java.util.Arrays; 8 import java.util.Objects; 9 10 import org.openstreetmap.josm.tools.Logging; 11 12 /** 13 * A segment consisting of 2 consecutive nodes out of a way. 14 * @author Taylor Smock 15 * @param <N> The node type 16 * @param <W> The way type 17 * @since xxx 18 */ 19 public class IWaySegment<N extends INode, W extends IWay<N>> implements Comparable<IWaySegment<N, W>> { 20 21 /** 22 * The way. 23 */ 24 public final W way; 25 26 /** 27 * The index of one of the 2 nodes in the way. The other node has the 28 * index <code>lowerIndex + 1</code>. 29 */ 30 public final int lowerIndex; 31 32 /** 33 * Constructs a new {@code IWaySegment}. 34 * @param w The way 35 * @param i The node lower index 36 * @throws IllegalArgumentException in case of invalid index 37 */ 38 public IWaySegment(W w, int i) { 39 way = w; 40 lowerIndex = i; 41 if (i < 0 || i >= w.getNodesCount() - 1) { 42 throw new IllegalArgumentException(toString()); 43 } 44 } 45 46 /** 47 * Returns the first node of the way segment. 48 * @return the first node 49 */ 50 public N getFirstNode() { 51 return way.getNode(lowerIndex); 52 } 53 54 /** 55 * Returns the second (last) node of the way segment. 56 * @return the second node 57 */ 58 public N getSecondNode() { 59 return way.getNode(lowerIndex + 1); 60 } 61 62 /** 63 * Determines and returns the way segment for the given way and node pair. 64 * @param way way 65 * @param first first node 66 * @param second second node 67 * @return way segment 68 * @throws IllegalArgumentException if the node pair is not part of way 69 */ 70 public static <N extends INode, W extends IWay<N>> IWaySegment<N, W> forNodePair(W way, N first, N second) { 71 int endIndex = way.getNodesCount() - 1; 72 while (endIndex > 0) { 73 final int indexOfFirst = way.getNodes().subList(0, endIndex).lastIndexOf(first); 74 if (second.equals(way.getNode(indexOfFirst + 1))) { 75 return new IWaySegment<>(way, indexOfFirst); 76 } 77 endIndex--; 78 } 79 throw new IllegalArgumentException("Node pair is not part of way!"); 80 } 81 82 /** 83 * Returns this way segment as complete way. 84 * @return the way segment as {@code Way} 85 * @throws IllegalAccessException See {@link Constructor#newInstance} 86 * @throws IllegalArgumentException See {@link Constructor#newInstance} 87 * @throws InstantiationException See {@link Constructor#newInstance} 88 * @throws InvocationTargetException See {@link Constructor#newInstance} 89 * @throws NoSuchMethodException See {@link Class#getConstructor} 90 */ 91 public W toWay() 92 throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { 93 // If the number of nodes is 2, then don't bother creating a new way 94 if (this.way.getNodes().size() == 2) { 95 return this.way; 96 } 97 // Since the way determines the generic class, this.way.getClass() is always Class<W>, assuming 98 // that way remains the defining element for the type, and remains final. 99 @SuppressWarnings("unchecked") 100 Class<W> clazz = (Class<W>) this.way.getClass(); 101 Constructor<W> constructor; 102 W w; 103 try { 104 // Check for clone constructor 105 constructor = clazz.getConstructor(clazz); 106 w = constructor.newInstance(this.way); 107 } catch (NoSuchMethodException e) { 108 Logging.trace(e); 109 constructor = clazz.getConstructor(); 110 w = constructor.newInstance(); 111 } 112 113 w.setNodes(Arrays.asList(getFirstNode(), getSecondNode())); 114 return w; 115 } 116 117 @Override 118 public boolean equals(Object o) { 119 if (this == o) return true; 120 if (o == null || getClass() != o.getClass()) return false; 121 IWaySegment<?, ?> that = (IWaySegment<?, ?>) o; 122 return lowerIndex == that.lowerIndex && 123 Objects.equals(way, that.way); 124 } 125 126 @Override 127 public int hashCode() { 128 return Objects.hash(way, lowerIndex); 129 } 130 131 @Override 132 public int compareTo(IWaySegment o) { 133 final W thisWay; 134 final IWay<?> otherWay; 135 try { 136 thisWay = toWay(); 137 otherWay = o == null ? null : o.toWay(); 138 } catch (ReflectiveOperationException e) { 139 Logging.error(e); 140 return -1; 141 } 142 return o == null ? -1 : (equals(o) ? 0 : thisWay.compareTo(otherWay)); 143 } 144 145 /** 146 * Checks whether this segment crosses other segment 147 * 148 * @param s2 The other segment 149 * @return true if both segments crosses 150 */ 151 public boolean intersects(IWaySegment<?, ?> s2) { 152 if (getFirstNode().equals(s2.getFirstNode()) || getSecondNode().equals(s2.getSecondNode()) || 153 getFirstNode().equals(s2.getSecondNode()) || getSecondNode().equals(s2.getFirstNode())) 154 return false; 155 156 return Line2D.linesIntersect( 157 getFirstNode().getEastNorth().east(), getFirstNode().getEastNorth().north(), 158 getSecondNode().getEastNorth().east(), getSecondNode().getEastNorth().north(), 159 s2.getFirstNode().getEastNorth().east(), s2.getFirstNode().getEastNorth().north(), 160 s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north()); 161 } 162 163 /** 164 * Checks whether this segment and another way segment share the same points 165 * @param s2 The other segment 166 * @return true if other way segment is the same or reverse 167 */ 168 public boolean isSimilar(IWaySegment<?, ?> s2) { 169 return (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode())) 170 || (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode())); 171 } 172 173 @Override 174 public String toString() { 175 return "IWaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']'; 176 } 177 } -
src/org/openstreetmap/josm/data/osm/RelationMember.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/RelationMember.java b/src/org/openstreetmap/josm/data/osm/RelationMember.java
a b 57 57 * @return Member as way 58 58 * @since 1937 59 59 */ 60 @Override 60 61 public Way getWay() { 61 62 return (Way) member; 62 63 } -
src/org/openstreetmap/josm/data/osm/WaySegment.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/WaySegment.java b/src/org/openstreetmap/josm/data/osm/WaySegment.java
a b 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.osm; 3 3 4 import java.awt.geom.Line2D;5 import java.util.Objects;6 7 4 /** 8 5 * A segment consisting of 2 consecutive nodes out of a way. 9 6 */ 10 public final class WaySegment implements Comparable<WaySegment> { 11 12 /** 13 * The way. 14 */ 15 public final Way way; 16 17 /** 18 * The index of one of the 2 nodes in the way. The other node has the 19 * index <code>lowerIndex + 1</code>. 20 */ 21 public final int lowerIndex; 7 public final class WaySegment extends IWaySegment<Node, Way> { 22 8 23 9 /** 24 * Constructs a new {@code WaySegment}. 25 * @param w The way 26 * @param i The node lower index 10 * Constructs a new {@code IWaySegment}. 11 * 12 * @param way The way 13 * @param i The node lower index 27 14 * @throws IllegalArgumentException in case of invalid index 28 15 */ 29 public WaySegment(Way w, int i) { 30 way = w; 31 lowerIndex = i; 32 if (i < 0 || i >= w.getNodesCount() - 1) { 33 throw new IllegalArgumentException(toString()); 34 } 35 } 36 37 /** 38 * Returns the first node of the way segment. 39 * @return the first node 40 */ 41 public Node getFirstNode() { 42 return way.getNode(lowerIndex); 16 public WaySegment(Way way, int i) { 17 super(way, i); 43 18 } 44 19 45 20 /** 46 * Returns the second (last) node of the way segment. 47 * @return the second node 48 */ 49 public Node getSecondNode() { 50 return way.getNode(lowerIndex + 1); 51 } 52 53 /** 54 * Determines and returns the way segment for the given way and node pair. 21 * Determines and returns the way segment for the given way and node pair. You should prefer 22 * {@link IWaySegment#forNodePair(IWay, INode, INode)} whenever possible. 23 * 55 24 * @param way way 56 25 * @param first first node 57 26 * @param second second node … … 74 43 * Returns this way segment as complete way. 75 44 * @return the way segment as {@code Way} 76 45 */ 46 @Override 77 47 public Way toWay() { 78 48 Way w = new Way(); 79 49 w.addNode(getFirstNode()); … … 81 51 return w; 82 52 } 83 53 84 @Override85 public boolean equals(Object o) {86 if (this == o) return true;87 if (o == null || getClass() != o.getClass()) return false;88 WaySegment that = (WaySegment) o;89 return lowerIndex == that.lowerIndex &&90 Objects.equals(way, that.way);91 }92 93 @Override94 public int hashCode() {95 return Objects.hash(way, lowerIndex);96 }97 98 @Override99 public int compareTo(WaySegment o) {100 return o == null ? -1 : (equals(o) ? 0 : toWay().compareTo(o.toWay()));101 }102 103 /**104 * Checks whether this segment crosses other segment105 *106 * @param s2 The other segment107 * @return true if both segments crosses108 */109 public boolean intersects(WaySegment s2) {110 if (getFirstNode().equals(s2.getFirstNode()) || getSecondNode().equals(s2.getSecondNode()) ||111 getFirstNode().equals(s2.getSecondNode()) || getSecondNode().equals(s2.getFirstNode()))112 return false;113 114 return Line2D.linesIntersect(115 getFirstNode().getEastNorth().east(), getFirstNode().getEastNorth().north(),116 getSecondNode().getEastNorth().east(), getSecondNode().getEastNorth().north(),117 s2.getFirstNode().getEastNorth().east(), s2.getFirstNode().getEastNorth().north(),118 s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north());119 }120 121 /**122 * Checks whether this segment and another way segment share the same points123 * @param s2 The other segment124 * @return true if other way segment is the same or reverse125 */126 public boolean isSimilar(WaySegment s2) {127 return (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))128 || (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()));129 }130 131 54 @Override 132 55 public String toString() { 133 56 return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']'; -
new file src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import java.util.ArrayList; 5 import java.util.List; 6 7 /** 8 * Parse packed values (only numerical values) 9 * 10 * @author Taylor Smock 11 * @since xxx 12 */ 13 public class ProtoBufPacked { 14 private final byte[] bytes; 15 private final Number[] numbers; 16 private int location; 17 18 /** 19 * Create a new ProtoBufPacked object 20 * 21 * @param bytes The packed bytes 22 */ 23 public ProtoBufPacked(byte[] bytes) { 24 this.location = 0; 25 this.bytes = bytes; 26 List<Number> numbersT = new ArrayList<>(); 27 while (this.location < bytes.length) { 28 numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE)); 29 } 30 31 this.numbers = new Number[numbersT.size()]; 32 for (int i = 0; i < numbersT.size(); i++) { 33 this.numbers[i] = numbersT.get(i); 34 } 35 } 36 37 /** 38 * The number of expected values 39 * 40 * @return The expected values 41 */ 42 public int size() { 43 return this.numbers.length; 44 } 45 46 /** 47 * Get the parsed number array 48 * 49 * @return The number array 50 */ 51 public Number[] getArray() { 52 return this.numbers; 53 } 54 55 private byte[] nextVarInt() { 56 List<Byte> byteList = new ArrayList<>(); 57 while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE) 58 == ProtoBufParser.MOST_SIGNIFICANT_BYTE) { 59 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned) 60 byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE)); 61 } 62 // The last byte doesn't drop the most significant bit 63 byteList.add(this.bytes[this.location++]); 64 byte[] byteArray = new byte[byteList.size()]; 65 for (int i = 0; i < byteList.size(); i++) { 66 byteArray[i] = byteList.get(i); 67 } 68 69 return byteArray; 70 } 71 } -
new file src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import java.io.BufferedInputStream; 5 import java.io.ByteArrayInputStream; 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.util.ArrayList; 9 import java.util.Collection; 10 import java.util.List; 11 12 import org.openstreetmap.josm.tools.Logging; 13 14 /** 15 * A basic Protobuf parser 16 * 17 * @author Taylor Smock 18 * @since xxx 19 */ 20 public class ProtoBufParser implements AutoCloseable { 21 /** 22 * The default byte size (see {@link #VAR_INT_BYTE_SIZE} for var ints) 23 */ 24 public static final byte BYTE_SIZE = 8; 25 /** 26 * The byte size for var ints (since the first byte is just an indicator for if the var int is done) 27 */ 28 public static final byte VAR_INT_BYTE_SIZE = BYTE_SIZE - 1; 29 /** 30 * Used to get the most significant byte 31 */ 32 static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7); 33 // TODO switch to a better parser 34 private final InputStream inputStream; 35 36 /** 37 * Create a new parser 38 * 39 * @param bytes The bytes to parse 40 */ 41 public ProtoBufParser(byte[] bytes) { 42 this(new ByteArrayInputStream(bytes)); 43 } 44 45 /** 46 * Create a new parser 47 * 48 * @param inputStream The InputStream (will be fully read at this time) 49 */ 50 public ProtoBufParser(InputStream inputStream) { 51 if (inputStream.markSupported()) { 52 this.inputStream = inputStream; 53 } else { 54 this.inputStream = new BufferedInputStream(inputStream); 55 } 56 } 57 58 /** 59 * Convert a byte array to a number (little endian) 60 * 61 * @param bytes The bytes to convert 62 * @param byteSize The size of the byte. For var ints, this is 7, for other ints, this is 8. 63 * @return An appropriate {@link Number} class. 64 */ 65 public static Number convertByteArray(byte[] bytes, byte byteSize) { 66 long number = 0; 67 for (int i = 0; i < bytes.length; i++) { 68 // Need to convert to uint64 in order to avoid bit operation from filling in 1's and overflow issues 69 number += Byte.toUnsignedLong(bytes[i]) << (byteSize * i); 70 } 71 return convertLong(number); 72 } 73 74 /** 75 * Convert a long to an appropriate {@link Number} class 76 * 77 * @param number The long to convert 78 * @return A {@link Number} 79 */ 80 public static Number convertLong(long number) { 81 // TODO deal with booleans 82 if (number <= Byte.MAX_VALUE && number >= Byte.MIN_VALUE) { 83 return (byte) number; 84 } else if (number <= Short.MAX_VALUE && number >= Short.MIN_VALUE) { 85 return (short) number; 86 } else if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) { 87 return (int) number; 88 } 89 return number; 90 } 91 92 /** 93 * Decode a zig-zag encoded value 94 * 95 * @param signed The value to decode 96 * @return The decoded value 97 */ 98 public static Number decodeZigZag(Number signed) { 99 final long value = signed.longValue(); 100 return convertLong((value >> 1) ^ -(value & 1)); 101 } 102 103 /** 104 * Encode a number to a zig-zag encode value 105 * 106 * @param signed The number to encode 107 * @return The encoded value 108 */ 109 public static Number encodeZigZag(Number signed) { 110 final long value = signed.longValue(); 111 final int shift = (value > Integer.MAX_VALUE ? Long.BYTES : Integer.BYTES) * 8 - 1; 112 return convertLong((value << 1) ^ (value >>> shift)); 113 } 114 115 @Override 116 public void close() { 117 try { 118 this.inputStream.close(); 119 } catch (IOException e) { 120 Logging.error(e); 121 } 122 } 123 124 /** 125 * Get the "next" WireType 126 * 127 * @return {@link WireType} expected 128 * @throws IOException - if an IO error occurs 129 */ 130 public WireType next() throws IOException { 131 this.inputStream.mark(16); 132 try { 133 return WireType.values()[this.inputStream.read() << 3]; 134 } finally { 135 this.inputStream.reset(); 136 } 137 } 138 139 /** 140 * Get the next byte 141 * 142 * @return The next byte 143 * @throws IOException - if an IO error occurs 144 */ 145 public int nextByte() throws IOException { 146 return this.inputStream.read(); 147 } 148 149 /** 150 * Check if there is more data to read 151 * 152 * @return {@code true} if there is more data to read 153 * @throws IOException - if an IO error occurs 154 */ 155 public boolean hasNext() throws IOException { 156 return this.inputStream.available() > 0; 157 } 158 159 /** 160 * Get the next var int ({@code WireType#VARINT}) 161 * 162 * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum}) 163 * @throws IOException - if an IO error occurs 164 */ 165 public byte[] nextVarInt() throws IOException { 166 List<Byte> byteList = new ArrayList<>(); 167 int currentByte = this.nextByte(); 168 while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE) { 169 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned) 170 byteList.add((byte) (currentByte ^ MOST_SIGNIFICANT_BYTE)); 171 currentByte = this.nextByte(); 172 } 173 // The last byte doesn't drop the most significant bit 174 byteList.add((byte) currentByte); 175 byte[] byteArray = new byte[byteList.size()]; 176 for (int i = 0; i < byteList.size(); i++) { 177 byteArray[i] = byteList.get(i); 178 } 179 180 return byteArray; 181 } 182 183 /** 184 * Get the next 32 bits ({@link WireType#THIRTY_TWO_BIT}) 185 * 186 * @return a byte array of the next 32 bits (4 bytes) 187 * @throws IOException - if an IO error occurs 188 */ 189 public byte[] nextFixed32() throws IOException { 190 // 4 bytes == 32 bits 191 return readNextBytes(4); 192 } 193 194 /** 195 * Get the next 64 bits ({@link WireType#SIXTY_FOUR_BIT}) 196 * 197 * @return a byte array of the next 64 bits (8 bytes) 198 * @throws IOException - if an IO error occurs 199 */ 200 public byte[] nextFixed64() throws IOException { 201 // 8 bytes == 64 bits 202 return readNextBytes(8); 203 } 204 205 /** 206 * Read an arbitrary number of bytes 207 * 208 * @param size The number of bytes to read 209 * @return a byte array of the specified size, filled with bytes read (unsigned) 210 * @throws IOException - if an IO error occurs 211 */ 212 private byte[] readNextBytes(int size) throws IOException { 213 byte[] bytesRead = new byte[size]; 214 for (int i = 0; i < bytesRead.length; i++) { 215 bytesRead[i] = (byte) this.nextByte(); 216 } 217 return bytesRead; 218 } 219 220 /** 221 * Get the next delimited message ({@link WireType#LENGTH_DELIMITED}) 222 * 223 * @return The next length delimited message 224 * @throws IOException - if an IO error occurs 225 */ 226 public byte[] nextLengthDelimited() throws IOException { 227 int length = convertByteArray(this.nextVarInt(), VAR_INT_BYTE_SIZE).intValue(); 228 return readNextBytes(length); 229 } 230 231 /** 232 * Read all records 233 * 234 * @return A collection of all records 235 * @throws IOException - if an IO error occurs 236 */ 237 public Collection<ProtoBufRecord> allRecords() throws IOException { 238 Collection<ProtoBufRecord> records = new ArrayList<>(); 239 while (this.hasNext()) { 240 records.add(new ProtoBufRecord(this)); 241 } 242 return records; 243 } 244 } -
new file src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import java.io.IOException; 5 import java.nio.charset.StandardCharsets; 6 import java.util.stream.Stream; 7 8 import org.openstreetmap.josm.tools.Utils; 9 10 /** 11 * A protobuf record, storing the {@link WireType}, the parsed field number, and the bytes for it. 12 * 13 * @author Taylor Smock 14 * @since xxx 15 */ 16 public class ProtoBufRecord implements AutoCloseable { 17 private static final byte[] EMPTY_BYTES = {}; 18 private final WireType type; 19 private final int field; 20 private byte[] bytes; 21 22 /** 23 * Create a new Protobuf record 24 * 25 * @param parser The parser to use to create the record 26 * @throws IOException - if an IO error occurs 27 */ 28 public ProtoBufRecord(ProtoBufParser parser) throws IOException { 29 Number number = ProtoBufParser.convertByteArray(parser.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE); 30 // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3} 31 this.field = (int) number.longValue() >> 3; 32 // 7 is 111 (so last three bits) 33 byte wireType = (byte) (number.longValue() & 7); 34 this.type = Stream.of(WireType.values()).filter(wType -> wType.getTypeRepresentation() == wireType).findFirst() 35 .orElse(WireType.UNKNOWN); 36 37 if (this.type == WireType.VARINT) { 38 this.bytes = parser.nextVarInt(); 39 } else if (this.type == WireType.SIXTY_FOUR_BIT) { 40 this.bytes = parser.nextFixed64(); 41 } else if (this.type == WireType.THIRTY_TWO_BIT) { 42 this.bytes = parser.nextFixed32(); 43 } else if (this.type == WireType.LENGTH_DELIMITED) { 44 this.bytes = parser.nextLengthDelimited(); 45 } else { 46 this.bytes = EMPTY_BYTES; 47 } 48 } 49 50 /** 51 * Get the field value 52 * 53 * @return The field value 54 */ 55 public int getField() { 56 return this.field; 57 } 58 59 /** 60 * Get the WireType of the data 61 * 62 * @return The {@link WireType} of the data 63 */ 64 public WireType getType() { 65 return this.type; 66 } 67 68 /** 69 * Get the raw bytes for this record 70 * 71 * @return The bytes 72 */ 73 public byte[] getBytes() { 74 return this.bytes; 75 } 76 77 /** 78 * Get the var int ({@code WireType#VARINT}) 79 * 80 * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum}) 81 */ 82 public Number asUnsignedVarInt() { 83 return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE); 84 } 85 86 /** 87 * Get the signed var int ({@code WireType#VARINT}). 88 * These are specially encoded so that they take up less space. 89 * 90 * @return The signed var int ({@code sint32} or {@code sint64}) 91 */ 92 public Number asSignedVarInt() { 93 final Number signed = this.asUnsignedVarInt(); 94 return ProtoBufParser.decodeZigZag(signed); 95 } 96 97 /** 98 * Get as a double ({@link WireType#SIXTY_FOUR_BIT}) 99 * 100 * @return the double 101 */ 102 public double asDouble() { 103 long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue(); 104 return Double.longBitsToDouble(doubleNumber); 105 } 106 107 /** 108 * Get as a float ({@link WireType#THIRTY_TWO_BIT}) 109 * 110 * @return the float 111 */ 112 public float asFloat() { 113 int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue(); 114 return Float.intBitsToFloat(floatNumber); 115 } 116 117 /** 118 * Get as a string ({@link WireType#LENGTH_DELIMITED}) 119 * 120 * @return The string (encoded as {@link StandardCharsets#UTF_8}) 121 */ 122 public String asString() { 123 return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8)); 124 } 125 126 /** 127 * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT}) 128 * 129 * @return a byte array of the 32 bits (4 bytes) 130 */ 131 public byte[] asFixed32() { 132 // TODO verify, or just assume? 133 // 4 bytes == 32 bits 134 return this.bytes; 135 } 136 137 /** 138 * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT}) 139 * 140 * @return a byte array of the 64 bits (8 bytes) 141 */ 142 public byte[] asFixed64() { 143 // TODO verify, or just assume? 144 // 8 bytes == 64 bits 145 return this.bytes; 146 } 147 148 @Override 149 public void close() { 150 this.bytes = null; 151 } 152 } -
new file src/org/openstreetmap/josm/data/protobuf/WireType.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/protobuf/WireType.java b/src/org/openstreetmap/josm/data/protobuf/WireType.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 /** 5 * The WireTypes 6 * 7 * @author Taylor Smock 8 * @since xxx 9 */ 10 public enum WireType { 11 /** 12 * int32, int64, uint32, uint64, sing32, sint64, bool, enum 13 */ 14 VARINT(0), 15 /** 16 * fixed64, sfixed64, double 17 */ 18 SIXTY_FOUR_BIT(1), 19 /** 20 * string, bytes, embedded messages, packed repeated fields 21 */ 22 LENGTH_DELIMITED(2), 23 /** 24 * start groups 25 * 26 * @deprecated Unknown reason. Deprecated since at least 2012. 27 */ 28 @Deprecated 29 START_GROUP(3), 30 /** 31 * end groups 32 * 33 * @deprecated Unknown reason. Deprecated since at least 2012. 34 */ 35 @Deprecated 36 END_GROUP(4), 37 /** 38 * fixed32, sfixed32, float 39 */ 40 THIRTY_TWO_BIT(5), 41 42 /** 43 * For unknown WireTypes 44 */ 45 UNKNOWN(Byte.MAX_VALUE); 46 47 private final byte type; 48 49 WireType(int value) { 50 this.type = (byte) value; 51 } 52 53 /** 54 * Get the type representation (byte form) 55 * 56 * @return The wire type byte representation 57 */ 58 public byte getTypeRepresentation() { 59 return this.type; 60 } 61 } -
new file src/org/openstreetmap/josm/data/vector/DataLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/DataLayer.java b/src/org/openstreetmap/josm/data/vector/DataLayer.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 /** 5 * An interface for objects that are part of a data layer 6 * @param <T> The type used to identify a layer, typically a string 7 */ 8 public interface DataLayer<T> { 9 /** 10 * Get the layer 11 * @return The layer 12 */ 13 T getLayer(); 14 15 /** 16 * Set the layer 17 * @param layer The layer to set 18 * @return {@code true} if the layer was set -- some objects may never change layers. 19 */ 20 default boolean setLayer(T layer) { 21 return layer != null && layer.equals(getLayer()); 22 } 23 } -
new file src/org/openstreetmap/josm/data/vector/VectorDataSet.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import java.awt.geom.Area; 5 import java.awt.geom.Ellipse2D; 6 import java.awt.geom.Path2D; 7 import java.awt.geom.PathIterator; 8 import java.util.ArrayList; 9 import java.util.Arrays; 10 import java.util.Collection; 11 import java.util.Collections; 12 import java.util.HashSet; 13 import java.util.LinkedList; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.Objects; 17 import java.util.Optional; 18 import java.util.Set; 19 import java.util.concurrent.ConcurrentHashMap; 20 import java.util.concurrent.locks.Lock; 21 import java.util.concurrent.locks.ReentrantReadWriteLock; 22 import java.util.function.Predicate; 23 import java.util.function.Supplier; 24 import java.util.stream.Collectors; 25 import java.util.stream.Stream; 26 27 import org.openstreetmap.gui.jmapviewer.Coordinate; 28 import org.openstreetmap.gui.jmapviewer.Tile; 29 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 30 import org.openstreetmap.josm.data.DataSource; 31 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 32 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature; 33 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 34 import org.openstreetmap.josm.data.osm.BBox; 35 import org.openstreetmap.josm.data.osm.DataSelectionListener; 36 import org.openstreetmap.josm.data.osm.DownloadPolicy; 37 import org.openstreetmap.josm.data.osm.HighlightUpdateListener; 38 import org.openstreetmap.josm.data.osm.INode; 39 import org.openstreetmap.josm.data.osm.IPrimitive; 40 import org.openstreetmap.josm.data.osm.IRelation; 41 import org.openstreetmap.josm.data.osm.IWay; 42 import org.openstreetmap.josm.data.osm.OsmData; 43 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 44 import org.openstreetmap.josm.data.osm.PrimitiveId; 45 import org.openstreetmap.josm.data.osm.QuadBucketPrimitiveStore; 46 import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 47 import org.openstreetmap.josm.data.osm.Storage; 48 import org.openstreetmap.josm.data.osm.UniqueIdGenerator; 49 import org.openstreetmap.josm.data.osm.UploadPolicy; 50 import org.openstreetmap.josm.data.osm.WaySegment; 51 import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter; 52 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 53 import org.openstreetmap.josm.tools.Geometry; 54 import org.openstreetmap.josm.tools.ListenerList; 55 import org.openstreetmap.josm.tools.Logging; 56 import org.openstreetmap.josm.tools.SubclassFilteredCollection; 57 58 /** 59 * A data class for Vector Data 60 * 61 * @author Taylor Smock 62 * @since xxx 63 */ 64 public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation> { 65 // Note: In Java 8, computeIfAbsent is blocking for both pre-existing and new values. In Java 9, it is only blocking 66 // for new values (perf increase). See JDK-8161372 for more info. 67 private final Map<Integer, VectorDataStore> dataStoreMap = new ConcurrentHashMap<>(); 68 private final Collection<PrimitiveId> selected = new HashSet<>(); 69 // Both of these listener lists are useless, since they expect OsmPrimitives at this time 70 private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create(); 71 private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create(); 72 private boolean lock = true; 73 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 74 private String name; 75 private short mappaintCacheIdx = 1; 76 77 /** 78 * The distance to consider nodes duplicates -- mostly a memory saving measure. 79 * 0.000_000_1 ~1.2 cm (+- 5.57 mm) 80 * Descriptions from <a href="https://xkcd.com/2170/">https://xkcd.com/2170/</a> 81 * Notes on <a href="https://wiki.openstreetmap.org/wiki/Node">https://wiki.openstreetmap.org/wiki/Node</a> indicate 82 * that IEEE 32-bit floats should not be used at high longitude (0.000_01 precision) 83 */ 84 protected static final float DUPE_NODE_DISTANCE = 0.000_000_1f; 85 86 /** 87 * The current zoom we are getting/adding to 88 */ 89 private int zoom; 90 /** 91 * Default to normal download policy 92 */ 93 private DownloadPolicy downloadPolicy = DownloadPolicy.NORMAL; 94 /** 95 * Default to a blocked upload policy 96 */ 97 private UploadPolicy uploadPolicy = UploadPolicy.BLOCKED; 98 /** 99 * The paint style for this layer 100 */ 101 private ElemStyles styles; 102 103 @Override public Collection<DataSource> getDataSources() { 104 final int currentZoom = this.zoom; 105 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 106 return dataStore.getDataSources(); 107 } 108 109 /** 110 * Add a data source 111 * 112 * @param currentZoom the zoom 113 * @param dataSource The datasource to add at the zoom level 114 */ 115 public void addDataSource(int currentZoom, DataSource dataSource) { 116 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 117 dataStore.addDataSource(dataSource); 118 } 119 120 @Override public void lock() { 121 this.lock = true; 122 } 123 124 @Override public void unlock() { 125 this.lock = false; 126 } 127 128 @Override public boolean isLocked() { 129 return this.lock; 130 } 131 132 @Override public String getVersion() { 133 return "8"; // TODO 134 } 135 136 @Override public String getName() { 137 return this.name; 138 } 139 140 @Override public void setName(String name) { 141 this.name = name; 142 } 143 144 @Override public void addPrimitive(VectorPrimitive primitive) { 145 primitive.setDataSet(this); 146 final int currentZoom = this.zoom; 147 this.tryWrite(() -> { 148 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 149 dataStore.addPrimitive(primitive); 150 }); 151 } 152 153 /** 154 * Remove a primitive from this dataset 155 * 156 * @param primitive The primitive to remove 157 */ 158 protected void removePrimitive(VectorPrimitive primitive) { 159 if (primitive.getDataSet() == this) { 160 final int currentZoom = this.zoom; 161 primitive.setDataSet(null); 162 this.tryWrite(() -> { 163 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 164 dataStore.removePrimitive(primitive); 165 }); 166 } 167 } 168 169 @Override public void clear() { 170 this.tryWrite(this.dataStoreMap::clear); 171 } 172 173 @Override public List<VectorNode> searchNodes(BBox bbox) { 174 return this.tryRead(() -> { 175 final int currentZoom = this.zoom; 176 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 177 return dataStore.getStore().searchNodes(bbox); 178 }).orElseGet(Collections::emptyList); 179 } 180 181 @Override public boolean containsNode(VectorNode vectorNode) { 182 return this.tryRead(() -> { 183 final int currentZoom = this.zoom; 184 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 185 return dataStore.getStore().containsNode(vectorNode); 186 }).orElse(false); 187 } 188 189 @Override public List<VectorWay> searchWays(BBox bbox) { 190 return this.tryRead(() -> { 191 final int currentZoom = this.zoom; 192 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 193 return dataStore.getStore().searchWays(bbox); 194 }).orElseGet(Collections::emptyList); 195 } 196 197 @Override public boolean containsWay(VectorWay vectorWay) { 198 return this.tryRead(() -> { 199 final int currentZoom = this.zoom; 200 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 201 return dataStore.getStore().containsWay(vectorWay); 202 }).orElse(false); 203 } 204 205 @Override public List<VectorRelation> searchRelations(BBox bbox) { 206 return this.tryRead(() -> { 207 final int currentZoom = this.zoom; 208 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 209 return dataStore.getStore().searchRelations(bbox); 210 }).orElseGet(Collections::emptyList); 211 } 212 213 @Override public boolean containsRelation(VectorRelation vectorRelation) { 214 return this.tryRead(() -> { 215 final int currentZoom = this.zoom; 216 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 217 return dataStore.getStore().containsRelation(vectorRelation); 218 }).orElse(false); 219 } 220 221 @Override public VectorPrimitive getPrimitiveById(PrimitiveId primitiveId) { 222 return this.tryRead(() -> { 223 final int currentZoom = this.zoom; 224 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 225 return dataStore.getPrimitivesMap().get(primitiveId); 226 }).orElse(null); 227 } 228 229 @Override public <T extends VectorPrimitive> Collection<T> getPrimitives( 230 Predicate<? super VectorPrimitive> predicate) { 231 // index should be negative, as the datastore doesn't have that zoom level 232 // Prefer the higher zoom level 233 Optional<Collection<T>> optional = this.tryRead(() -> { 234 int currentZoom = this.zoom; 235 if (!this.dataStoreMap.containsKey(currentZoom)) { 236 // index should be negative, as the datastore doesn't have that zoom level 237 final int[] keys = this.dataStoreMap.keySet().stream().mapToInt(Integer::intValue).sorted().toArray(); 238 final int index = 1 - Arrays.binarySearch(keys, currentZoom); 239 // Prefer the higher zoom level 240 if (index < keys.length - 2) { 241 currentZoom = keys[index + 1]; 242 } else if (index > 0 && keys.length > index - 1) { 243 currentZoom = keys[index - 1]; 244 } 245 } 246 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 247 return new SubclassFilteredCollection<>(dataStore.getAllPrimitives(), predicate); 248 }); 249 return optional.orElseGet(Collections::emptyList); 250 } 251 252 @Override public Collection<VectorNode> getNodes() { 253 return this.getPrimitives(VectorNode.class::isInstance); 254 } 255 256 @Override public Collection<VectorWay> getWays() { 257 return this.getPrimitives(VectorWay.class::isInstance); 258 } 259 260 @Override public Collection<VectorRelation> getRelations() { 261 return this.getPrimitives(VectorRelation.class::isInstance); 262 } 263 264 @Override public DownloadPolicy getDownloadPolicy() { 265 return this.downloadPolicy; 266 } 267 268 @Override public void setDownloadPolicy(DownloadPolicy downloadPolicy) { 269 this.downloadPolicy = downloadPolicy; 270 } 271 272 @Override public UploadPolicy getUploadPolicy() { 273 return this.uploadPolicy; 274 } 275 276 @Override public void setUploadPolicy(UploadPolicy uploadPolicy) { 277 this.uploadPolicy = uploadPolicy; 278 } 279 280 @Override public Lock getReadLock() { 281 return this.readWriteLock.readLock(); 282 } 283 284 @Override public Collection<WaySegment> getHighlightedVirtualNodes() { 285 // TODO? 286 return Collections.emptyList(); 287 } 288 289 @Override public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) { 290 // TODO? 291 } 292 293 @Override public Collection<WaySegment> getHighlightedWaySegments() { 294 // TODO? 295 return Collections.emptyList(); 296 } 297 298 @Override public void setHighlightedWaySegments(Collection<WaySegment> waySegments) { 299 // TODO? 300 } 301 302 @Override public void addHighlightUpdateListener(HighlightUpdateListener listener) { 303 this.highlightUpdateListenerListenerList.addListener(listener); 304 } 305 306 @Override public void removeHighlightUpdateListener(HighlightUpdateListener listener) { 307 this.highlightUpdateListenerListenerList.removeListener(listener); 308 } 309 310 @Override public Collection<VectorPrimitive> getAllSelected() { 311 final int currentZoom = this.zoom; 312 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 313 return dataStore.getAllPrimitives().stream() 314 .filter(primitive -> this.selected.contains(primitive.getPrimitiveId())).collect( 315 Collectors.toList()); 316 } 317 318 @Override public boolean selectionEmpty() { 319 return this.selected.isEmpty(); 320 } 321 322 @Override public boolean isSelected(VectorPrimitive osm) { 323 return this.selected.contains(osm.getPrimitiveId()); 324 } 325 326 @Override public void toggleSelected(Collection<? extends PrimitiveId> osm) { 327 this.toggleSelectedImpl(osm.stream()); 328 } 329 330 @Override public void toggleSelected(PrimitiveId... osm) { 331 this.toggleSelectedImpl(Stream.of(osm)); 332 } 333 334 private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) { 335 osm.forEach(primitiveId -> { 336 if (this.selected.contains(primitiveId)) { 337 this.selected.remove(primitiveId); 338 } else { 339 this.selected.add(primitiveId); 340 } 341 }); 342 } 343 344 @Override public void setSelected(Collection<? extends PrimitiveId> selection) { 345 this.setSelectedImpl(selection.stream()); 346 } 347 348 @Override public void setSelected(PrimitiveId... osm) { 349 this.setSelectedImpl(Stream.of(osm)); 350 } 351 352 private void setSelectedImpl(Stream<? extends PrimitiveId> osm) { 353 this.selected.clear(); 354 osm.forEach(this.selected::add); 355 } 356 357 @Override public void addSelected(Collection<? extends PrimitiveId> selection) { 358 this.addSelectedImpl(selection.stream()); 359 } 360 361 @Override public void addSelected(PrimitiveId... osm) { 362 this.addSelectedImpl(Stream.of(osm)); 363 } 364 365 private void addSelectedImpl(Stream<? extends PrimitiveId> osm) { 366 osm.forEach(this.selected::add); 367 } 368 369 @Override public void clearSelection(PrimitiveId... osm) { 370 this.clearSelectionImpl(Stream.of(osm)); 371 } 372 373 @Override public void clearSelection(Collection<? extends PrimitiveId> list) { 374 this.clearSelectionImpl(list.stream()); 375 } 376 377 @Override public void clearSelection() { 378 this.clearSelectionImpl(new ArrayList<>(this.selected).stream()); 379 } 380 381 private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) { 382 osm.forEach(this.selected::remove); 383 } 384 385 @Override public void addSelectionListener(DataSelectionListener listener) { 386 this.dataSelectionListenerListenerList.addListener(listener); 387 } 388 389 @Override public void removeSelectionListener(DataSelectionListener listener) { 390 this.dataSelectionListenerListenerList.removeListener(listener); 391 } 392 393 public short getMappaintCacheIndex() { 394 return this.mappaintCacheIdx; 395 } 396 397 @Override public void clearMappaintCache() { 398 this.mappaintCacheIdx++; 399 } 400 401 public void setZoom(int zoom) { 402 if (zoom == this.zoom) { 403 return; // Do nothing -- zoom isn't actually changing 404 } 405 this.tryWrite(() -> { 406 this.zoom = zoom; 407 this.clearMappaintCache(); 408 final int[] nearestZoom = new int[] {-1, -1, -1}; 409 nearestZoom[0] = zoom; 410 // Create a new list to avoid concurrent modification issues 411 final int[] keys = new ArrayList<>(this.dataStoreMap.keySet()).stream().filter(Objects::nonNull) 412 .mapToInt(Integer::intValue).sorted().toArray(); 413 final int index; 414 if (this.dataStoreMap.containsKey(zoom)) { 415 index = Arrays.binarySearch(keys, zoom); 416 } else { 417 // (-(insertion point) - 1) = return -> insertion point = -(return + 1) 418 index = -(Arrays.binarySearch(keys, zoom) + 1); 419 } 420 if (index > 0) { 421 nearestZoom[1] = keys[index - 1]; 422 } 423 if (index < keys.length - 2) { 424 nearestZoom[2] = keys[index + 1]; 425 } 426 // Clear zoom levels not immediately above/below the current zoom level (attempt to save some memory) 427 //IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key)).forEach(this.dataStoreMap::remove); 428 }); 429 } 430 431 public int getZoom() { 432 return this.zoom; 433 } 434 435 /** 436 * Add tile data to this dataset 437 * @param tile The tile to add 438 * @param <T> The tile type 439 */ 440 public <T extends Tile & VectorTile> void addTileData(T tile) { 441 this.tryWrite(() -> { 442 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(tile.getZoom(), VectorDataStore::new); 443 dataStore.addTile(tile); 444 }); 445 } 446 447 /** 448 * Try to read something (here to avoid boilerplate) 449 * 450 * @param supplier The reading function 451 * @param <T> The return type 452 * @return The optional return 453 */ 454 private <T> Optional<T> tryRead(Supplier<T> supplier) { 455 try { 456 this.readWriteLock.readLock().lockInterruptibly(); 457 return Optional.ofNullable(supplier.get()); 458 } catch (InterruptedException e) { 459 Logging.error(e); 460 Thread.currentThread().interrupt(); 461 } finally { 462 this.readWriteLock.readLock().unlock(); 463 } 464 return Optional.empty(); 465 } 466 467 /** 468 * Try to write something (here to avoid boilerplate) 469 * 470 * @param runnable The writing function 471 */ 472 private void tryWrite(Runnable runnable) { 473 try { 474 this.readWriteLock.writeLock().lockInterruptibly(); 475 runnable.run(); 476 } catch (InterruptedException e) { 477 Logging.error(e); 478 Thread.currentThread().interrupt(); 479 } finally { 480 if (this.readWriteLock.isWriteLockedByCurrentThread()) { 481 this.readWriteLock.writeLock().unlock(); 482 } 483 } 484 } 485 486 /** 487 * Get the styles for this layer 488 * 489 * @return The styles 490 */ 491 public ElemStyles getStyles() { 492 return this.styles; 493 } 494 495 /** 496 * Set the styles for this layer 497 * @param styles The styles to set for this layer 498 */ 499 public void setStyles(Collection<ElemStyles> styles) { 500 if (styles.size() == 1) { 501 this.styles = styles.iterator().next(); 502 } else if (!styles.isEmpty()) { 503 this.styles = new ElemStyles(styles.stream().flatMap(style -> style.getStyleSources().stream()).collect(Collectors.toList())); 504 } else { 505 this.styles = null; 506 } 507 } 508 509 /** 510 * This literally only exists to make {@link QuadBucketPrimitiveStore#removePrimitive} public 511 * 512 * @param <N> The node type 513 * @param <W> The way type 514 * @param <R> The relation type 515 */ 516 private static class LocalQuadBucketPrimitiveStore<N extends INode, W extends IWay<N>, R extends IRelation<?>> 517 extends QuadBucketPrimitiveStore<N, W, R> { 518 // Allow us to remove primitives 519 @Override 520 public void removePrimitive(IPrimitive primitive) { 521 if ((primitive instanceof IRelation && this.containsRelation((R) primitive)) 522 || (primitive instanceof IWay && this.containsWay((W) primitive)) 523 || (primitive instanceof INode && this.containsNode((N) primitive))) { 524 super.removePrimitive(primitive); 525 } 526 } 527 } 528 529 private static class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> { 530 protected final int zoom; 531 protected final LocalQuadBucketPrimitiveStore<N, W, R> store = new LocalQuadBucketPrimitiveStore<>(); 532 protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true); 533 protected final Set<Tile> addedTiles = new HashSet<>(); 534 protected final Map<PrimitiveId, O> primitivesMap = allPrimitives 535 .foreignKey(new Storage.PrimitiveIdHash()); 536 protected final Collection<DataSource> dataSources = new LinkedList<>(); 537 538 DataStore(int zoom) { 539 this.zoom = zoom; 540 } 541 542 public int getZoom() { 543 return this.zoom; 544 } 545 546 public QuadBucketPrimitiveStore<N, W, R> getStore() { 547 return this.store; 548 } 549 550 public Storage<O> getAllPrimitives() { 551 return this.allPrimitives; 552 } 553 554 public Map<PrimitiveId, O> getPrimitivesMap() { 555 return this.primitivesMap; 556 } 557 558 public Collection<DataSource> getDataSources() { 559 return Collections.unmodifiableCollection(dataSources); 560 } 561 562 /** 563 * Add a datasource to this data set 564 * @param dataSource The data soure to add 565 */ 566 public void addDataSource(DataSource dataSource) { 567 this.dataSources.add(dataSource); 568 } 569 570 /** 571 * Add a primitive to this dataset 572 * @param primitive The primitive to remove 573 */ 574 protected void removePrimitive(O primitive) { 575 this.store.removePrimitive(primitive); 576 this.allPrimitives.remove(primitive); 577 this.primitivesMap.remove(primitive.getPrimitiveId()); 578 } 579 580 /** 581 * Add a primitive to this dataset 582 * @param primitive The primitive to add 583 */ 584 protected void addPrimitive(O primitive) { 585 this.store.addPrimitive(primitive); 586 this.allPrimitives.add(primitive); 587 this.primitivesMap.put(primitive.getPrimitiveId(), primitive); 588 } 589 } 590 591 private class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> { 592 private static final String JOSM_MERGE_TYPE_KEY = "josm_merge_type"; 593 594 VectorDataStore(int zoom) { 595 super(zoom); 596 } 597 598 @Override 599 protected void addPrimitive(VectorPrimitive primitive) { 600 primitive.setDataSet(VectorDataSet.this); 601 // The field is uint64, so we can use negative numbers to indicate that it is a "generated" object (e.g., nodes for ways) 602 if (primitive.getUniqueId() == 0) { 603 final UniqueIdGenerator generator = primitive.getIdGenerator(); 604 long id; 605 do { 606 id = generator.generateUniqueId(); 607 } while (this.primitivesMap.containsKey(new SimplePrimitiveId(id, primitive.getType()))); 608 primitive.setId(primitive.getIdGenerator().generateUniqueId()); 609 } 610 if (primitive instanceof VectorRelation && !primitive.isMultipolygon()) { 611 primitive = mergeWays((VectorRelation) primitive); 612 } 613 final VectorPrimitive alreadyAdded = this.primitivesMap.get(primitive.getPrimitiveId()); 614 final VectorRelation mergedRelation = (VectorRelation) this.primitivesMap 615 .get(new SimplePrimitiveId(primitive.getPrimitiveId().getUniqueId(), 616 OsmPrimitiveType.RELATION)); 617 if (alreadyAdded == null || alreadyAdded.equals(primitive)) { 618 super.addPrimitive(primitive); 619 } else if (mergedRelation != null && mergedRelation.get(JOSM_MERGE_TYPE_KEY) != null) { 620 mergedRelation.addRelationMember(new VectorRelationMember("", primitive)); 621 super.addPrimitive(primitive); 622 // Check that all primitives can be merged 623 if (mergedRelation.getMemberPrimitivesList().stream().allMatch(IWay.class::isInstance)) { 624 // This pretty much does the "right" thing 625 this.mergeWays(mergedRelation); 626 } else if (!(primitive instanceof IWay)) { 627 // Can't merge, ever (one of the childs is a node/relation) 628 mergedRelation.remove(JOSM_MERGE_TYPE_KEY); 629 } 630 } else if (mergedRelation != null && primitive instanceof IRelation) { 631 // Just add to the relation 632 ((VectorRelation) primitive).getMembers().forEach(mergedRelation::addRelationMember); 633 } else if (alreadyAdded instanceof VectorWay && primitive instanceof VectorWay) { 634 final VectorRelation temporaryRelation = 635 mergedRelation == null ? new VectorRelation(primitive.getLayer()) : mergedRelation; 636 if (mergedRelation == null) { 637 temporaryRelation.put(JOSM_MERGE_TYPE_KEY, "merge"); 638 temporaryRelation.addRelationMember(new VectorRelationMember("", alreadyAdded)); 639 } 640 temporaryRelation.addRelationMember(new VectorRelationMember("", primitive)); 641 temporaryRelation.setDataSet(VectorDataSet.this); 642 super.addPrimitive(primitive); 643 super.addPrimitive(temporaryRelation); 644 } 645 } 646 647 private VectorPrimitive mergeWays(VectorRelation relation) { 648 List<VectorRelationMember> members = RelationSorter.sortMembersByConnectivity(relation.getMembers()); 649 Collection<VectorWay> relationWayList = relation.getMemberPrimitivesList().stream() 650 .filter(VectorWay.class::isInstance) 651 .map(VectorWay.class::cast).collect(Collectors.toCollection(ArrayList::new)); 652 // Only support way-only relations 653 if (relationWayList.size() != relation.getMemberPrimitivesList().size()) { 654 return relation; 655 } 656 List<VectorWay> wayList = new ArrayList<>(relation.getMembersCount()); 657 // Assume that the order may not be correct, worst case O(n), best case O(n/2) 658 // Assume that the ways were drawn in order 659 final int maxIteration = relationWayList.size(); 660 int iteration = 0; 661 while (iteration < maxIteration && wayList.size() < relationWayList.size()) { 662 for (VectorWay way : relationWayList) { 663 if (wayList.isEmpty()) { 664 wayList.add(way); 665 continue; 666 } 667 // Check first/last ways 668 if (canMergeWays(wayList.get(0), way, false)) { 669 wayList.add(0, way); 670 } else if (canMergeWays(wayList.get(wayList.size() - 1), way, false)) { 671 wayList.add(way); 672 } 673 } 674 iteration++; 675 relationWayList.removeIf(wayList::contains); 676 } 677 if (!relationWayList.isEmpty()) { 678 return relation; 679 } 680 // Merge ways 681 List<VectorNode> nodes = new ArrayList<>(); 682 for (VectorWay way : wayList) { 683 for (VectorNode node : way.getNodes()) { 684 if (nodes.isEmpty() || !Objects.equals(nodes.get(nodes.size() - 1), node)) { 685 nodes.add(node); 686 } 687 } 688 } 689 VectorWay way = wayList.get(0); 690 way.setNodes(nodes); 691 wayList.remove(way); 692 wayList.forEach(this::removePrimitive); 693 this.removePrimitive(relation); 694 return way; 695 } 696 697 private <N extends INode, W extends IWay<N>> boolean canMergeWays(W old, W toAdd, boolean allowReverse) { 698 final List<N> nodes = new ArrayList<>(old.getNodes()); 699 boolean added = true; 700 if (allowReverse && old.firstNode().equals(toAdd.firstNode())) { 701 // old <-|-> new becomes old ->|-> new 702 Collections.reverse(nodes); 703 nodes.addAll(toAdd.getNodes()); 704 } else if (old.firstNode().equals(toAdd.lastNode())) { 705 // old <-|<- new, so we prepend the new nodes in order 706 nodes.addAll(0, toAdd.getNodes()); 707 } else if (old.lastNode().equals(toAdd.firstNode())) { 708 // old ->|-> new, we just add it 709 nodes.addAll(toAdd.getNodes()); 710 } else if (allowReverse && old.lastNode().equals(toAdd.lastNode())) { 711 // old ->|<- new, we need to reverse new 712 final List<N> toAddNodes = new ArrayList<>(toAdd.getNodes()); 713 Collections.reverse(toAddNodes); 714 nodes.addAll(toAddNodes); 715 } else { 716 added = false; 717 } 718 if (added) { 719 // This is (technically) always correct 720 old.setNodes(nodes); 721 } 722 return added; 723 } 724 725 private synchronized <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer, 726 Collection<VectorPrimitive> featureObjects, int x, int y) { 727 final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile); 728 final int layerExtent = layer.getExtent() * 2; 729 final ICoordinate lowerRight = tile.getTileSource() 730 .tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom()); 731 final ICoordinate coords = new Coordinate( 732 upperLeft.getLat() - (upperLeft.getLat() - lowerRight.getLat()) * y / layerExtent, 733 upperLeft.getLon() + (lowerRight.getLon() - upperLeft.getLon()) * x / layerExtent); 734 final Collection<VectorNode> nodes = this.store 735 .searchNodes(new BBox(coords.getLon(), coords.getLat(), DUPE_NODE_DISTANCE)); 736 final VectorNode node; 737 if (!nodes.isEmpty()) { 738 final VectorNode first = nodes.iterator().next(); 739 if (first.isDisabled() || !first.isVisible()) { 740 // Only replace nodes that are not visible 741 node = new VectorNode(layer.getName()); 742 node.setCoor(node.getCoor()); 743 first.getReferrers(true).forEach(primitive -> { 744 if (primitive instanceof VectorWay) { 745 List<VectorNode> nodeList = new ArrayList<>(((VectorWay) primitive).getNodes()); 746 nodeList.replaceAll(vnode -> vnode.equals(first) ? node : vnode); 747 ((VectorWay) primitive).setNodes(nodeList); 748 } else if (primitive instanceof VectorRelation) { 749 List<VectorRelationMember> members = new ArrayList<>(((VectorRelation) primitive).getMembers()); 750 members.replaceAll(member -> 751 member.getMember().equals(first) ? new VectorRelationMember(member.getRole(), node): member); 752 ((VectorRelation) primitive).setMembers(members); 753 } 754 }); 755 this.removePrimitive(first); 756 } else { 757 node = first; 758 } 759 } else { 760 node = new VectorNode(layer.getName()); 761 } 762 node.setCoor(coords); 763 featureObjects.add(node); 764 return node; 765 } 766 767 private <T extends Tile & VectorTile> List<VectorWay> pathToWay(T tile, Layer layer, 768 Collection<VectorPrimitive> featureObjects, Path2D shape) { 769 final PathIterator pathIterator = shape.getPathIterator(null); 770 final List<VectorWay> ways = pathIteratorToObjects(tile, layer, featureObjects, pathIterator).stream() 771 .filter(VectorWay.class::isInstance).map(VectorWay.class::cast).collect( 772 Collectors.toList()); 773 // These nodes technically do not exist, so we shouldn't show them 774 ways.stream().flatMap(way -> way.getNodes().stream()) 775 .filter(prim -> !prim.isTagged() && prim.getReferrers(true).size() == 1 && prim.getId() <= 0) 776 .forEach(prim -> { 777 prim.setDisabled(true); 778 prim.setVisible(false); 779 }); 780 return ways; 781 } 782 783 private <T extends Tile & VectorTile> List<VectorPrimitive> pathIteratorToObjects(T tile, Layer layer, 784 Collection<VectorPrimitive> featureObjects, PathIterator pathIterator) { 785 final List<VectorNode> nodes = new ArrayList<>(); 786 final double[] coords = new double[6]; 787 final List<VectorPrimitive> ways = new ArrayList<>(); 788 do { 789 final int type = pathIterator.currentSegment(coords); 790 pathIterator.next(); 791 if ((PathIterator.SEG_MOVETO == type || PathIterator.SEG_CLOSE == type) && !nodes.isEmpty()) { 792 if (PathIterator.SEG_CLOSE == type) { 793 nodes.add(nodes.get(0)); 794 } 795 // New line 796 if (!nodes.isEmpty()) { 797 final VectorWay way = new VectorWay(layer.getName()); 798 way.setNodes(nodes); 799 featureObjects.add(way); 800 ways.add(way); 801 } 802 nodes.clear(); 803 } 804 if (PathIterator.SEG_MOVETO == type || PathIterator.SEG_LINETO == type) { 805 final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1]); 806 nodes.add(node); 807 } else if (PathIterator.SEG_CLOSE != type) { 808 // Vector Tiles only have MoveTo, LineTo, and ClosePath. Anything else is not supported at this time. 809 throw new UnsupportedOperationException(); 810 } 811 } while (!pathIterator.isDone()); 812 if (!nodes.isEmpty()) { 813 final VectorWay way = new VectorWay(layer.getName()); 814 way.setNodes(nodes); 815 featureObjects.add(way); 816 ways.add(way); 817 } 818 return ways; 819 } 820 821 private <T extends Tile & VectorTile> VectorRelation areaToRelation(T tile, Layer layer, 822 Collection<VectorPrimitive> featureObjects, Area area) { 823 final PathIterator pathIterator = area.getPathIterator(null); 824 final List<VectorPrimitive> members = pathIteratorToObjects(tile, layer, featureObjects, pathIterator); 825 VectorRelation vectorRelation = new VectorRelation(layer.getName()); 826 for (VectorPrimitive member : members) { 827 final String role; 828 if (member instanceof VectorWay && ((VectorWay) member).isClosed()) { 829 role = Geometry.isClockwise(((VectorWay) member).getNodes()) ? "outer" : "inner"; 830 } else { 831 role = ""; 832 } 833 vectorRelation.addRelationMember(new VectorRelationMember(role, member)); 834 } 835 return vectorRelation; 836 } 837 838 /** 839 * Add a tile to this data store 840 * @param tile The tile to add 841 * @param <T> The tile type 842 */ 843 public synchronized <T extends Tile & VectorTile> void addTile(T tile) { 844 Optional<Tile> previous = this.addedTiles.stream() 845 .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny(); 846 // Check if we have already added the tile (just to save processing time) 847 if (!previous.isPresent() || (!previous.get().isLoaded() && !previous.get().isLoading())) { 848 previous.ifPresent(this.addedTiles::remove); 849 this.addedTiles.add(tile); 850 for (Layer layer : tile.getLayers()) { 851 layer.getGeometry().forEach(geometry -> { 852 List<VectorPrimitive> featureObjects = new ArrayList<>(); 853 List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>(); 854 geometry.getShapes().forEach(shape -> { 855 final VectorPrimitive primitive; 856 if (shape instanceof Ellipse2D) { 857 primitive = pointToNode(tile, layer, featureObjects, 858 (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY()); 859 } else if (shape instanceof Path2D) { 860 primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape).stream().findFirst() 861 .orElse(null); 862 } else if (shape instanceof Area) { 863 primitive = areaToRelation(tile, layer, featureObjects, (Area) shape); 864 primitive.put("type", "multipolygon"); 865 } else { 866 // We shouldn't hit this, but just in case 867 throw new UnsupportedOperationException(); 868 } 869 primaryFeatureObjects.add(primitive); 870 }); 871 final VectorPrimitive primitive; 872 if (primaryFeatureObjects.size() == 1) { 873 primitive = primaryFeatureObjects.get(0); 874 if (primitive instanceof IRelation && !primitive.isMultipolygon()) { 875 primitive.put(JOSM_MERGE_TYPE_KEY, "merge"); 876 } 877 } else if (!primaryFeatureObjects.isEmpty()) { 878 VectorRelation relation = new VectorRelation(layer.getName()); 879 primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim)) 880 .forEach(relation::addRelationMember); 881 primitive = relation; 882 } else { 883 return; 884 } 885 Feature feature = geometry.getFeature(); 886 primitive.setId(feature.getId()); 887 feature.getTags().forEach(primitive::put); 888 featureObjects.forEach(this::addPrimitive); 889 primaryFeatureObjects.forEach(this::addPrimitive); 890 this.addPrimitive(primitive); 891 }); 892 } 893 } 894 } 895 } 896 } -
new file src/org/openstreetmap/josm/data/vector/VectorNode.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorNode.java b/src/org/openstreetmap/josm/data/vector/VectorNode.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import java.util.List; 5 6 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 7 import org.openstreetmap.josm.data.coor.EastNorth; 8 import org.openstreetmap.josm.data.coor.LatLon; 9 import org.openstreetmap.josm.data.osm.BBox; 10 import org.openstreetmap.josm.data.osm.INode; 11 import org.openstreetmap.josm.data.osm.IPrimitive; 12 import org.openstreetmap.josm.data.osm.IWay; 13 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 14 import org.openstreetmap.josm.data.osm.UniqueIdGenerator; 15 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 16 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 17 18 /** 19 * The "Node" type of a vector layer 20 * 21 * @since xxx 22 */ 23 public class VectorNode extends VectorPrimitive implements INode { 24 private static final UniqueIdGenerator ID_GENERATOR = new UniqueIdGenerator(); 25 private double lon = Double.NaN; 26 private double lat = Double.NaN; 27 28 /** 29 * Create a new vector node 30 * @param layer The layer for the vector node 31 */ 32 public VectorNode(String layer) { 33 super(layer); 34 } 35 36 @Override public double lon() { 37 return this.lon; 38 } 39 40 @Override public double lat() { 41 return this.lat; 42 } 43 44 @Override public UniqueIdGenerator getIdGenerator() { 45 return ID_GENERATOR; 46 } 47 48 @Override public LatLon getCoor() { 49 return new LatLon(this.lat, this.lon); 50 } 51 52 @Override public void setCoor(LatLon coordinates) { 53 this.lat = coordinates.lat(); 54 this.lon = coordinates.lon(); 55 } 56 57 /** 58 * Set the coordinates of this node 59 * 60 * @param coordinates The coordinates to set 61 * @see #setCoor(LatLon) 62 */ 63 public void setCoor(ICoordinate coordinates) { 64 this.lat = coordinates.getLat(); 65 this.lon = coordinates.getLon(); 66 } 67 68 @Override public void setEastNorth(EastNorth eastNorth) { 69 final LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth); 70 this.lat = ll.lat(); 71 this.lon = ll.lon(); 72 } 73 74 @Override public boolean isReferredByWays(int n) { 75 // Count only referrers that are members of the same dataset (primitive can have some fake references, for example 76 // when way is cloned 77 List<? extends IPrimitive> referrers = super.getReferrers(); 78 if (referrers == null || referrers.isEmpty()) 79 return false; 80 if (referrers instanceof IPrimitive) 81 return n <= 1 && referrers instanceof IWay && ((IPrimitive) referrers).getDataSet() == getDataSet(); 82 else { 83 int counter = 0; 84 for (IPrimitive o : referrers) { 85 if (getDataSet() == o.getDataSet() && o instanceof IWay && ++counter >= n) 86 return true; 87 } 88 return false; 89 } 90 } 91 92 @Override public void accept(PrimitiveVisitor visitor) { 93 visitor.visit(this); 94 } 95 96 @Override public BBox getBBox() { 97 return new BBox(this.lon, this.lat); 98 } 99 100 @Override public OsmPrimitiveType getType() { 101 return OsmPrimitiveType.NODE; 102 } 103 } -
new file src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import java.util.Arrays; 5 import java.util.List; 6 import java.util.Map; 7 import java.util.function.Consumer; 8 import java.util.stream.Collectors; 9 import java.util.stream.IntStream; 10 import java.util.stream.Stream; 11 12 import org.openstreetmap.josm.data.osm.AbstractPrimitive; 13 import org.openstreetmap.josm.data.osm.IPrimitive; 14 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 15 import org.openstreetmap.josm.gui.mappaint.StyleCache; 16 import org.openstreetmap.josm.tools.Utils; 17 18 /** 19 * The base class for Vector primitives 20 * @author Taylor Smock 21 * @since xxx 22 */ 23 public abstract class VectorPrimitive extends AbstractPrimitive implements DataLayer<String> { 24 private VectorDataSet dataSet; 25 private boolean highlighted; 26 private StyleCache mappaintStyle; 27 private final String layer; 28 29 /** 30 * Create a primitive for a specific vector layer 31 * @param layer The layer for the primitive 32 */ 33 public VectorPrimitive(String layer) { 34 this.layer = layer; 35 } 36 37 @Override protected void keysChangedImpl(Map<String, String> originalKeys) { 38 clearCachedStyle(); 39 if (dataSet != null) { 40 for (IPrimitive ref : getReferrers()) { 41 ref.clearCachedStyle(); 42 } 43 } 44 } 45 46 @Override public boolean isHighlighted() { 47 return this.highlighted; 48 } 49 50 @Override public void setHighlighted(boolean highlighted) { 51 this.highlighted = highlighted; 52 } 53 54 @Override public boolean isTagged() { 55 return !this.getInterestingTags().isEmpty(); 56 } 57 58 @Override public boolean isAnnotated() { 59 return this.getInterestingTags().size() - this.getKeys().size() > 0; 60 } 61 62 @Override 63 public VectorDataSet getDataSet() { 64 return this.dataSet; 65 } 66 67 protected void setDataSet(VectorDataSet dataSet) { 68 this.dataSet = dataSet; 69 } 70 71 /*---------- 72 * MAPPAINT 73 *--------*/ 74 private short mappaintCacheIdx; 75 76 @Override 77 public final StyleCache getCachedStyle() { 78 return mappaintStyle; 79 } 80 81 @Override 82 public final void setCachedStyle(StyleCache mappaintStyle) { 83 this.mappaintStyle = mappaintStyle; 84 } 85 86 @Override 87 public final boolean isCachedStyleUpToDate() { 88 return mappaintStyle != null && mappaintCacheIdx == dataSet.getMappaintCacheIndex(); 89 } 90 91 @Override 92 public final void declareCachedStyleUpToDate() { 93 this.mappaintCacheIdx = dataSet.getMappaintCacheIndex(); 94 } 95 96 @Override public boolean hasDirectionKeys() { 97 return false; 98 } 99 100 @Override public boolean reversedDirection() { 101 return false; 102 } 103 104 /*------------ 105 * Referrers 106 ------------*/ 107 // Largely the same as OsmPrimitive, OsmPrimitive not modified at this time to avoid breaking binary compatibility 108 109 private Object referrers; 110 111 @Override 112 public final List<VectorPrimitive> getReferrers(boolean allowWithoutDataset) { 113 return referrers(allowWithoutDataset, VectorPrimitive.class) 114 .collect(Collectors.toList()); 115 } 116 117 /** 118 * Add new referrer. If referrer is already included then no action is taken 119 * @param referrer The referrer to add 120 */ 121 protected void addReferrer(IPrimitive referrer) { 122 if (referrers == null) { 123 referrers = referrer; 124 } else if (referrers instanceof IPrimitive) { 125 if (referrers != referrer) { 126 referrers = new IPrimitive[] {(IPrimitive) referrers, referrer}; 127 } 128 } else { 129 for (IPrimitive primitive:(IPrimitive[]) referrers) { 130 if (primitive == referrer) 131 return; 132 } 133 referrers = Utils.addInArrayCopy((IPrimitive[]) referrers, referrer); 134 } 135 } 136 137 /** 138 * Remove referrer. No action is taken if referrer is not registered 139 * @param referrer The referrer to remove 140 */ 141 protected void removeReferrer(IPrimitive referrer) { 142 if (referrers instanceof IPrimitive) { 143 if (referrers == referrer) { 144 referrers = null; 145 } 146 } else if (referrers instanceof IPrimitive[]) { 147 IPrimitive[] orig = (IPrimitive[]) referrers; 148 int idx = IntStream.range(0, orig.length) 149 .filter(i -> orig[i] == referrer) 150 .findFirst().orElse(-1); 151 if (idx == -1) 152 return; 153 154 if (orig.length == 2) { 155 referrers = orig[1-idx]; // idx is either 0 or 1, take the other 156 } else { // downsize the array 157 IPrimitive[] smaller = new IPrimitive[orig.length-1]; 158 System.arraycopy(orig, 0, smaller, 0, idx); 159 System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx); 160 referrers = smaller; 161 } 162 } 163 } 164 165 private <T extends IPrimitive> Stream<T> referrers(boolean allowWithoutDataset, Class<T> filter) { 166 // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example 167 // when way is cloned 168 169 if (dataSet == null && allowWithoutDataset) { 170 return Stream.empty(); 171 } 172 if (referrers == null) { 173 return Stream.empty(); 174 } 175 final Stream<IPrimitive> stream = referrers instanceof IPrimitive // NOPMD 176 ? Stream.of((IPrimitive) referrers) 177 : Arrays.stream((IPrimitive[]) referrers); 178 return stream 179 .filter(p -> p.getDataSet() == dataSet) 180 .filter(filter::isInstance) 181 .map(filter::cast); 182 } 183 184 /** 185 * Gets all primitives in the current dataset that reference this primitive. 186 * @param filter restrict primitives to subclasses 187 * @param <T> type of primitives 188 * @return the referrers as Stream 189 */ 190 public final <T extends IPrimitive> Stream<T> referrers(Class<T> filter) { 191 return referrers(false, filter); 192 } 193 194 @Override 195 public void visitReferrers(PrimitiveVisitor visitor) { 196 if (visitor != null) 197 doVisitReferrers(o -> o.accept(visitor)); 198 } 199 200 private void doVisitReferrers(Consumer<IPrimitive> visitor) { 201 if (this.referrers instanceof IPrimitive) { 202 IPrimitive ref = (IPrimitive) this.referrers; 203 if (ref.getDataSet() == dataSet) { 204 visitor.accept(ref); 205 } 206 } else if (this.referrers instanceof IPrimitive[]) { 207 IPrimitive[] refs = (IPrimitive[]) this.referrers; 208 for (IPrimitive ref: refs) { 209 if (ref.getDataSet() == dataSet) { 210 visitor.accept(ref); 211 } 212 } 213 } 214 } 215 216 /** 217 * Set the id of the object 218 * @param id The id 219 */ 220 protected void setId(long id) { 221 this.id = id; 222 } 223 224 /** 225 * Make this object disabled 226 * @param disabled {@code true} to disable the object 227 */ 228 public void setDisabled(boolean disabled) { 229 this.updateFlags(FLAG_DISABLED, disabled); 230 } 231 232 /** 233 * Make this object visible 234 * @param visible {@code true} to make this object visible (default) 235 */ 236 @Override 237 public void setVisible(boolean visible) { 238 this.updateFlags(FLAG_VISIBLE, visible); 239 } 240 241 /************************** 242 * Data layer information * 243 **************************/ 244 @Override 245 public String getLayer() { 246 return this.layer; 247 } 248 } -
new file src/org/openstreetmap/josm/data/vector/VectorRelation.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelation.java b/src/org/openstreetmap/josm/data/vector/VectorRelation.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import java.util.ArrayList; 5 import java.util.Collections; 6 import java.util.List; 7 8 import org.openstreetmap.josm.data.osm.BBox; 9 import org.openstreetmap.josm.data.osm.IPrimitive; 10 import org.openstreetmap.josm.data.osm.IRelation; 11 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 12 import org.openstreetmap.josm.data.osm.UniqueIdGenerator; 13 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 14 15 /** 16 * The "Relation" type for vectors 17 * 18 * @author Taylor Smock 19 * @since xxx 20 */ 21 public class VectorRelation extends VectorPrimitive implements IRelation<VectorRelationMember> { 22 private static final UniqueIdGenerator RELATION_ID_GENERATOR = new UniqueIdGenerator(); 23 private final List<VectorRelationMember> members = new ArrayList<>(); 24 private BBox cachedBBox; 25 26 /** 27 * Create a new relation for a layer 28 * @param layer The layer the relation will belong to 29 */ 30 public VectorRelation(String layer) { 31 super(layer); 32 } 33 34 @Override public UniqueIdGenerator getIdGenerator() { 35 return RELATION_ID_GENERATOR; 36 } 37 38 @Override public void accept(PrimitiveVisitor visitor) { 39 visitor.visit(this); 40 } 41 42 @Override public BBox getBBox() { 43 if (cachedBBox == null) { 44 cachedBBox = new BBox(); 45 for (IPrimitive member : this.getMemberPrimitivesList()) { 46 cachedBBox.add(member.getBBox()); 47 } 48 } 49 return cachedBBox; 50 } 51 52 protected void addRelationMember(VectorRelationMember member) { 53 this.members.add(member); 54 member.getMember().addReferrer(this); 55 cachedBBox = null; 56 } 57 58 /** 59 * Remove the first instance of a member from the relation 60 * 61 * @param member The member to remove 62 */ 63 protected void removeRelationMember(VectorRelationMember member) { 64 this.members.remove(member); 65 if (!this.members.contains(member)) { 66 member.getMember().removeReferrer(this); 67 } 68 } 69 70 @Override public int getMembersCount() { 71 return this.members.size(); 72 } 73 74 @Override public VectorRelationMember getMember(int index) { 75 return this.members.get(index); 76 } 77 78 @Override public List<VectorRelationMember> getMembers() { 79 return Collections.unmodifiableList(this.members); 80 } 81 82 @Override public void setMembers(List<VectorRelationMember> members) { 83 this.members.clear(); 84 this.members.addAll(members); 85 } 86 87 @Override public long getMemberId(int idx) { 88 return this.getMember(idx).getMember().getId(); 89 } 90 91 @Override public String getRole(int idx) { 92 return this.getMember(idx).getRole(); 93 } 94 95 @Override public OsmPrimitiveType getMemberType(int idx) { 96 return this.getMember(idx).getType(); 97 } 98 99 @Override public OsmPrimitiveType getType() { 100 return this.getMembers().stream().map(VectorRelationMember::getType) 101 .allMatch(OsmPrimitiveType.CLOSEDWAY::equals) ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION; 102 } 103 } -
new file src/org/openstreetmap/josm/data/vector/VectorRelationMember.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java b/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import java.util.Optional; 5 6 import org.openstreetmap.josm.data.osm.INode; 7 import org.openstreetmap.josm.data.osm.IRelation; 8 import org.openstreetmap.josm.data.osm.IRelationMember; 9 import org.openstreetmap.josm.data.osm.IWay; 10 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 11 import org.openstreetmap.josm.tools.CheckParameterUtil; 12 13 /** 14 * Relation members for a Vector Relation 15 */ 16 public class VectorRelationMember implements IRelationMember<VectorPrimitive> { 17 private final String role; 18 private final VectorPrimitive member; 19 20 /** 21 * Create a new relation member 22 * @param role The role of the member 23 * @param member The member primitive 24 */ 25 public VectorRelationMember(String role, VectorPrimitive member) { 26 CheckParameterUtil.ensureParameterNotNull(member, "member"); 27 this.role = Optional.ofNullable(role).orElse("").intern(); 28 this.member = member; 29 } 30 31 @Override public String getRole() { 32 return this.role; 33 } 34 35 @Override public boolean isNode() { 36 return this.member instanceof INode; 37 } 38 39 @Override public boolean isWay() { 40 return this.member instanceof IWay; 41 } 42 43 @Override public boolean isRelation() { 44 return this.member instanceof IRelation; 45 } 46 47 @Override public VectorPrimitive getMember() { 48 return this.member; 49 } 50 51 @Override public long getUniqueId() { 52 return this.member.getId(); 53 } 54 55 @Override public OsmPrimitiveType getType() { 56 return this.member.getType(); 57 } 58 59 @Override public boolean isNew() { 60 return this.member.isNew(); 61 } 62 } -
new file src/org/openstreetmap/josm/data/vector/VectorWay.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorWay.java b/src/org/openstreetmap/josm/data/vector/VectorWay.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import java.util.ArrayList; 5 import java.util.Collections; 6 import java.util.List; 7 import java.util.stream.Collectors; 8 9 import org.openstreetmap.josm.data.osm.BBox; 10 import org.openstreetmap.josm.data.osm.INode; 11 import org.openstreetmap.josm.data.osm.IWay; 12 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 13 import org.openstreetmap.josm.data.osm.UniqueIdGenerator; 14 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 15 16 /** 17 * The "Way" type for a Vector layer 18 * 19 * @author Taylor Smock 20 * @since xxx 21 */ 22 public class VectorWay extends VectorPrimitive implements IWay<VectorNode> { 23 private static final UniqueIdGenerator WAY_GENERATOR = new UniqueIdGenerator(); 24 private final List<VectorNode> nodes = new ArrayList<>(); 25 private BBox cachedBBox; 26 27 /** 28 * Create a new way for a layer 29 * @param layer The layer for the way 30 */ 31 public VectorWay(String layer) { 32 super(layer); 33 } 34 35 @Override public UniqueIdGenerator getIdGenerator() { 36 return WAY_GENERATOR; 37 } 38 39 @Override public void accept(PrimitiveVisitor visitor) { 40 visitor.visit(this); 41 } 42 43 @Override public BBox getBBox() { 44 if (cachedBBox == null) { 45 cachedBBox = new BBox(); 46 for (INode node : this.getNodes()) { 47 cachedBBox.add(node.getBBox()); 48 } 49 } 50 return cachedBBox; 51 } 52 53 @Override public int getNodesCount() { 54 return this.getNodes().size(); 55 } 56 57 @Override public VectorNode getNode(int index) { 58 return this.getNodes().get(index); 59 } 60 61 @Override public List<VectorNode> getNodes() { 62 return Collections.unmodifiableList(this.nodes); 63 } 64 65 @Override public void setNodes(List<VectorNode> nodes) { 66 this.nodes.forEach(node -> node.removeReferrer(this)); 67 this.nodes.clear(); 68 nodes.forEach(node -> node.addReferrer(this)); 69 this.nodes.addAll(nodes); 70 this.cachedBBox = null; 71 } 72 73 @Override public List<Long> getNodeIds() { 74 return this.getNodes().stream().map(VectorNode::getId).collect(Collectors.toList()); 75 } 76 77 @Override public long getNodeId(int idx) { 78 return this.getNodes().get(idx).getId(); 79 } 80 81 @Override public boolean isClosed() { 82 return this.firstNode() != null && this.firstNode().equals(this.lastNode()); 83 } 84 85 @Override public VectorNode firstNode() { 86 if (this.nodes.isEmpty()) { 87 return null; 88 } 89 return this.getNode(0); 90 } 91 92 @Override public VectorNode lastNode() { 93 if (this.nodes.isEmpty()) { 94 return null; 95 } 96 return this.getNode(this.getNodesCount() - 1); 97 } 98 99 @Override public boolean isFirstLastNode(INode n) { 100 if (this.nodes.isEmpty()) { 101 return false; 102 } 103 return this.firstNode().equals(n) || this.lastNode().equals(n); 104 } 105 106 @Override public boolean isInnerNode(INode n) { 107 if (this.nodes.isEmpty()) { 108 return false; 109 } 110 return !this.firstNode().equals(n) && !this.lastNode().equals(n) && this.nodes.stream() 111 .anyMatch(vectorNode -> vectorNode.equals(n)); 112 } 113 114 @Override public OsmPrimitiveType getType() { 115 return this.isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY; 116 } 117 } -
src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java
a b 10 10 import java.util.TreeMap; 11 11 import java.util.TreeSet; 12 12 13 import org.openstreetmap.josm.data.osm.Node; 14 import org.openstreetmap.josm.data.osm.RelationMember; 15 import org.openstreetmap.josm.data.osm.Way; 13 import org.openstreetmap.josm.data.osm.INode; 14 import org.openstreetmap.josm.data.osm.IPrimitive; 15 import org.openstreetmap.josm.data.osm.IRelationMember; 16 import org.openstreetmap.josm.data.osm.IWay; 16 17 17 18 /** 18 19 * Auxiliary class for relation sorting. … … 26 27 * (that are shared by other members). 27 28 * 28 29 * @author Christiaan Welvaart <cjw@time4t.net> 29 * @since 1785 30 * @param <T> The type of {@link IRelationMember} 31 * @since 1785, xxx (generics) 30 32 */ 31 public class RelationNodeMap {33 public class RelationNodeMap<T extends IRelationMember<? extends IPrimitive>> { 32 34 33 35 private static final String ROLE_BACKWARD = "backward"; 34 36 35 37 private static class NodesWays { 36 public final Map< Node, Set<Integer>> nodes = new TreeMap<>();37 public final Map<Integer, Set< Node>> ways = new TreeMap<>();38 public final Map<INode, Set<Integer>> nodes = new TreeMap<>(); 39 public final Map<Integer, Set<INode>> ways = new TreeMap<>(); 38 40 public final boolean oneWay; 39 41 40 42 NodesWays(boolean oneWay) { … … 56 58 * Used to keep track of what members are done. 57 59 */ 58 60 private final Set<Integer> remaining = new TreeSet<>(); 59 private final Map<Integer, Set< Node>> remainingOneway = new TreeMap<>();61 private final Map<Integer, Set<INode>> remainingOneway = new TreeMap<>(); 60 62 61 63 /** 62 64 * All members that are incomplete or not a way … … 67 69 * Gets the start node of the member, respecting the direction role. 68 70 * @param m The relation member. 69 71 * @return <code>null</code> if the member is no way, the node otherwise. 72 * @since xxx (generics) 70 73 */ 71 public static Node firstOnewayNode(RelationMemberm) {74 public static INode firstOnewayNode(IRelationMember<?> m) { 72 75 if (!m.isWay()) return null; 73 76 if (ROLE_BACKWARD.equals(m.getRole())) { 74 77 return m.getWay().lastNode(); … … 81 84 * @param m The relation member. 82 85 * @return <code>null</code> if the member is no way, the node otherwise. 83 86 */ 84 public static Node lastOnewayNode(RelationMemberm) {87 public static INode lastOnewayNode(IRelationMember<?> m) { 85 88 if (!m.isWay()) return null; 86 89 if (ROLE_BACKWARD.equals(m.getRole())) { 87 90 return m.getWay().firstNode(); … … 89 92 return m.getWay().lastNode(); 90 93 } 91 94 92 RelationNodeMap(List< RelationMember> members) {95 RelationNodeMap(List<T> members) { 93 96 for (int i = 0; i < members.size(); ++i) { 94 RelationMemberm = members.get(i);97 T m = members.get(i); 95 98 if (m.getMember().isIncomplete() || !m.isWay() || m.getWay().getNodesCount() < 2) { 96 99 notSortable.add(i); 97 100 continue; 98 101 } 99 102 100 Wayw = m.getWay();103 IWay<?> w = m.getWay(); 101 104 if (RelationSortUtils.roundaboutType(w) != NONE) { 102 for ( Node nd : w.getNodes()) {105 for (INode nd : w.getNodes()) { 103 106 addPair(nd, i); 104 107 } 105 108 } else if (RelationSortUtils.isOneway(m)) { … … 118 121 remaining.addAll(map.ways.keySet()); 119 122 } 120 123 121 private void addPair( Node n, int i) {124 private void addPair(INode n, int i) { 122 125 map.nodes.computeIfAbsent(n, k -> new TreeSet<>()).add(i); 123 126 map.ways.computeIfAbsent(i, k -> new TreeSet<>()).add(n); 124 127 } 125 128 126 private void addNodeWayMap( Node n, int i) {129 private void addNodeWayMap(INode n, int i) { 127 130 onewayMap.nodes.computeIfAbsent(n, k -> new TreeSet<>()).add(i); 128 131 } 129 132 130 private void addWayNodeMap( Node n, int i) {133 private void addWayNodeMap(INode n, int i) { 131 134 onewayMap.ways.computeIfAbsent(i, k -> new TreeSet<>()).add(n); 132 135 } 133 136 134 private void addNodeWayMapReverse( Node n, int i) {137 private void addNodeWayMapReverse(INode n, int i) { 135 138 onewayReverseMap.nodes.computeIfAbsent(n, k -> new TreeSet<>()).add(i); 136 139 } 137 140 138 private void addWayNodeMapReverse( Node n, int i) {141 private void addWayNodeMapReverse(INode n, int i) { 139 142 onewayReverseMap.ways.computeIfAbsent(i, k -> new TreeSet<>()).add(n); 140 143 } 141 144 142 private void addRemainingForward( Node n, int i) {145 private void addRemainingForward(INode n, int i) { 143 146 remainingOneway.computeIfAbsent(i, k -> new TreeSet<>()).add(n); 144 147 } 145 148 146 149 private Integer firstOneway; 147 private Node lastOnewayNode;148 private Node firstCircular;150 private INode lastOnewayNode; 151 private INode firstCircular; 149 152 150 153 /** 151 154 * Return a relation member that is linked to the member 'i', but has not been popped yet. … … 158 161 if (firstOneway != null) return popForwardOnewayPart(way); 159 162 160 163 if (map.ways.containsKey(way)) { 161 for ( Node n : map.ways.get(way)) {164 for (INode n : map.ways.get(way)) { 162 165 Integer i = deleteAndGetAdjacentNode(map, n); 163 166 if (i != null) return i; 164 167 … … 176 179 177 180 private Integer popForwardOnewayPart(Integer way) { 178 181 if (onewayMap.ways.containsKey(way)) { 179 Node exitNode = onewayMap.ways.get(way).iterator().next();182 INode exitNode = onewayMap.ways.get(way).iterator().next(); 180 183 181 184 if (checkIfEndOfLoopReached(exitNode)) { 182 185 lastOnewayNode = exitNode; … … 201 204 // Check if the given node can be the end of the loop (i.e. it has 202 205 // an outgoing bidirectional or multiple outgoing oneways, or we 203 206 // looped back to our first circular node) 204 private boolean checkIfEndOfLoopReached( Node n) {207 private boolean checkIfEndOfLoopReached(INode n) { 205 208 return map.nodes.containsKey(n) 206 209 || (onewayMap.nodes.containsKey(n) && (onewayMap.nodes.get(n).size() > 1)) 207 210 || ((firstCircular != null) && (firstCircular == n)); … … 209 212 210 213 private Integer popBackwardOnewayPart(int way) { 211 214 if (lastOnewayNode != null) { 212 Set< Node> nodes = new TreeSet<>();215 Set<INode> nodes = new TreeSet<>(); 213 216 if (onewayReverseMap.ways.containsKey(way)) { 214 217 nodes.addAll(onewayReverseMap.ways.get(way)); 215 218 } 216 219 if (map.ways.containsKey(way)) { 217 220 nodes.addAll(map.ways.get(way)); 218 221 } 219 for ( Node n : nodes) {222 for (INode n : nodes) { 220 223 if (n == lastOnewayNode) { //if oneway part ends 221 224 firstOneway = null; 222 225 lastOnewayNode = null; … … 247 250 * @param n node 248 251 * @return node next to n 249 252 */ 250 private Integer deleteAndGetAdjacentNode(NodesWays nw, Node n) {253 private Integer deleteAndGetAdjacentNode(NodesWays nw, INode n) { 251 254 Integer j = findAdjacentWay(nw, n); 252 255 if (j == null) return null; 253 256 deleteWayNode(nw, j, n); 254 257 return j; 255 258 } 256 259 257 private static Integer findAdjacentWay(NodesWays nw, Node n) {260 private static Integer findAdjacentWay(NodesWays nw, INode n) { 258 261 Set<Integer> adj = nw.nodes.get(n); 259 262 if (adj == null || adj.isEmpty()) return null; 260 263 return adj.iterator().next(); 261 264 } 262 265 263 private void deleteWayNode(NodesWays nw, Integer way, Node n) {266 private void deleteWayNode(NodesWays nw, Integer way, INode n) { 264 267 if (nw.oneWay) { 265 268 doneOneway(way); 266 269 } else { … … 285 288 286 289 if (remainingOneway.isEmpty()) return null; 287 290 for (Integer i : remainingOneway.keySet()) { //find oneway, which is connected to more than one way (is between two oneway loops) 288 for ( Node n : onewayReverseMap.ways.get(i)) {291 for (INode n : onewayReverseMap.ways.get(i)) { 289 292 if (onewayReverseMap.nodes.containsKey(n) && onewayReverseMap.nodes.get(n).size() > 1) { 290 293 doneOneway(i); 291 294 firstCircular = n; … … 305 308 * @param i member key 306 309 */ 307 310 private void doneOneway(Integer i) { 308 Set< Node> nodesForward = remainingOneway.get(i);309 for ( Node n : nodesForward) {311 Set<INode> nodesForward = remainingOneway.get(i); 312 for (INode n : nodesForward) { 310 313 if (onewayMap.nodes.containsKey(n)) { 311 314 onewayMap.nodes.get(n).remove(i); 312 315 } … … 319 322 320 323 private void done(Integer i) { 321 324 remaining.remove(i); 322 Set< Node> nodes = map.ways.get(i);323 for ( Node n : nodes) {325 Set<INode> nodes = map.ways.get(i); 326 for (INode n : nodes) { 324 327 boolean result = map.nodes.get(n).remove(i); 325 328 if (!result) throw new AssertionError(); 326 329 } -
src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java
a b 15 15 import java.util.stream.Collectors; 16 16 17 17 import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 18 import org.openstreetmap.josm.data.osm.IPrimitive; 19 import org.openstreetmap.josm.data.osm.IRelationMember; 18 20 import org.openstreetmap.josm.data.osm.OsmPrimitive; 19 21 import org.openstreetmap.josm.data.osm.Relation; 20 22 import org.openstreetmap.josm.data.osm.RelationMember; … … 194 196 * Sorts a list of members by connectivity 195 197 * @param defaultMembers The members to sort 196 198 * @return A sorted list of the same members 199 * @since xxx (signature change, generics) 197 200 */ 198 public static List<RelationMember> sortMembersByConnectivity(List<RelationMember> defaultMembers) { 201 public static <T extends IRelationMember<? extends IPrimitive>> List<T> sortMembersByConnectivity(List<T> defaultMembers) { 202 List<T> newMembers; 199 203 200 List<RelationMember> newMembers; 201 202 RelationNodeMap map = new RelationNodeMap(defaultMembers); 204 RelationNodeMap<T> map = new RelationNodeMap<>(defaultMembers); 203 205 // List of groups of linked members 204 206 // 205 207 List<LinkedList<Integer>> allGroups = new ArrayList<>(); -
src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java b/src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java
a b 6 6 import static org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction.ROUNDABOUT_RIGHT; 7 7 8 8 import org.openstreetmap.josm.data.coor.EastNorth; 9 import org.openstreetmap.josm.data.osm. Node;10 import org.openstreetmap.josm.data.osm. RelationMember;11 import org.openstreetmap.josm.data.osm. Way;9 import org.openstreetmap.josm.data.osm.INode; 10 import org.openstreetmap.josm.data.osm.IRelationMember; 11 import org.openstreetmap.josm.data.osm.IWay; 12 12 import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType.Direction; 13 13 14 14 /** … … 24 24 * determine, if the way i is a roundabout and if yes, what type of roundabout 25 25 * @param member relation member 26 26 * @return roundabout type 27 * @since xxx (generics) 27 28 */ 28 static Direction roundaboutType( RelationMembermember) {29 static Direction roundaboutType(IRelationMember<?> member) { 29 30 if (member == null || !member.isWay()) return NONE; 30 return roundaboutType( member.getWay());31 return roundaboutType((IWay<?>) member.getWay()); 31 32 } 32 33 33 static Direction roundaboutType(Way w) { 34 /** 35 * Check if a way is a roundabout type 36 * @param w The way to check 37 * @param <W> The way type 38 * @return The roundabout type 39 * @since xxx (generics) 40 */ 41 static <W extends IWay<?>> Direction roundaboutType(W w) { 34 42 if (w != null && w.hasTag("junction", "circular", "roundabout")) { 35 43 int nodesCount = w.getNodesCount(); 36 44 if (nodesCount > 2 && nodesCount < 200) { 37 Node n1 = w.getNode(0);38 Node n2 = w.getNode(1);39 Node n3 = w.getNode(2);45 INode n1 = w.getNode(0); 46 INode n2 = w.getNode(1); 47 INode n3 = w.getNode(2); 40 48 if (n1 != null && n2 != null && n3 != null && w.isClosed()) { 41 49 /** do some simple determinant / cross product test on the first 3 nodes 42 50 to see, if the roundabout goes clock wise or ccw */ … … 54 62 return NONE; 55 63 } 56 64 57 static boolean isBackward(final RelationMembermember) {65 static boolean isBackward(final IRelationMember<?> member) { 58 66 return "backward".equals(member.getRole()); 59 67 } 60 68 61 static boolean isForward(final RelationMembermember) {69 static boolean isForward(final IRelationMember<?> member) { 62 70 return "forward".equals(member.getRole()); 63 71 } 64 72 65 static boolean isOneway(final RelationMembermember) {73 static boolean isOneway(final IRelationMember<?> member) { 66 74 return isForward(member) || isBackward(member); 67 75 } 68 76 } -
new file src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.Component; 7 import java.awt.Graphics2D; 8 import java.awt.event.ActionEvent; 9 import java.util.ArrayList; 10 import java.util.Arrays; 11 import java.util.Collection; 12 import java.util.Collections; 13 import java.util.HashMap; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.Objects; 17 import java.util.function.BooleanSupplier; 18 import java.util.function.Consumer; 19 import java.util.stream.Collectors; 20 21 import javax.swing.AbstractAction; 22 import javax.swing.Action; 23 import javax.swing.JCheckBoxMenuItem; 24 import javax.swing.JMenuItem; 25 26 import org.openstreetmap.gui.jmapviewer.Tile; 27 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 28 import org.openstreetmap.josm.data.Bounds; 29 import org.openstreetmap.josm.data.imagery.ImageryInfo; 30 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 31 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile; 32 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile; 33 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.TileListener; 34 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader; 35 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource; 36 import org.openstreetmap.josm.data.osm.DataSet; 37 import org.openstreetmap.josm.data.osm.Node; 38 import org.openstreetmap.josm.data.osm.OsmPrimitive; 39 import org.openstreetmap.josm.data.osm.Relation; 40 import org.openstreetmap.josm.data.osm.RelationMember; 41 import org.openstreetmap.josm.data.osm.Way; 42 import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer; 43 import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory; 44 import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; 45 import org.openstreetmap.josm.data.vector.VectorDataSet; 46 import org.openstreetmap.josm.data.vector.VectorNode; 47 import org.openstreetmap.josm.data.vector.VectorRelation; 48 import org.openstreetmap.josm.data.vector.VectorWay; 49 import org.openstreetmap.josm.gui.MainApplication; 50 import org.openstreetmap.josm.gui.MapView; 51 import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer; 52 import org.openstreetmap.josm.gui.layer.LayerManager; 53 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 54 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 55 import org.openstreetmap.josm.gui.mappaint.StyleSource; 56 57 /** 58 * A layer for MapBox Vector Tiles 59 * @author Taylor Smock 60 * @since xxx 61 */ 62 public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> implements TileListener { 63 private static final String CACHE_REGION_NAME = "MVT"; 64 private final Map<String, Boolean> layerNames = new HashMap<>(); 65 private final VectorDataSet dataSet = new VectorDataSet(); 66 67 /** 68 * Creates an instance of an MVT layer 69 * 70 * @param info ImageryInfo describing the layer 71 */ 72 public MVTLayer(ImageryInfo info) { 73 super(info); 74 } 75 76 @Override 77 protected Class<? extends TileLoader> getTileLoaderClass() { 78 return MapBoxVectorCachedTileLoader.class; 79 } 80 81 @Override 82 protected String getCacheName() { 83 return CACHE_REGION_NAME; 84 } 85 86 @Override 87 public Collection<String> getNativeProjections() { 88 // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857 89 // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}"). 90 return Collections.singleton(MVTFile.DEFAULT_PROJECTION); 91 } 92 93 @Override public void paint(Graphics2D g, MapView mv, Bounds box) { 94 this.dataSet.setZoom(this.getZoomLevel()); 95 AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, false); 96 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress() 97 || !OsmDataLayer.PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get()); 98 // Set the painter to use our custom style sheet 99 if (painter instanceof StyledMapRenderer && this.dataSet.getStyles() != null) { 100 ((StyledMapRenderer) painter).setStyles(this.dataSet.getStyles()); 101 } 102 painter.render(this.dataSet, false, box); 103 } 104 105 @Override 106 protected MapboxVectorTileSource getTileSource() { 107 MapboxVectorTileSource source = new MapboxVectorTileSource(this.info); 108 this.info.setAttribution(source); 109 if (source.getStyleSource() != null) { 110 List<ElemStyles> styles = source.getStyleSource().getSources().entrySet().stream() 111 .filter(entry -> entry.getKey() == null || entry.getKey().getUrls().contains(source.getBaseUrl())) 112 .map(Map.Entry::getValue).collect(Collectors.toList()); 113 // load the style sources 114 styles.stream().map(ElemStyles::getStyleSources).flatMap(Collection::stream).forEach(StyleSource::loadStyleSource); 115 this.dataSet.setStyles(styles); 116 this.setName(source.getName()); 117 } 118 return source; 119 } 120 121 @Override 122 public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) { 123 final MVTTile tile = new MVTTile(source, x, y, zoom); 124 tile.addTileLoaderFinisher(this); 125 return tile; 126 } 127 128 @Override 129 public Action[] getMenuEntries() { 130 ArrayList<Action> actions = new ArrayList<>(Arrays.asList(super.getMenuEntries())); 131 // Add separator between Info and the layers 132 actions.add(SeparatorLayerAction.INSTANCE); 133 for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) { 134 actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true), 135 layer -> { 136 layerNames.compute(layer, (key, value) -> Boolean.FALSE.equals(value)); 137 this.invalidate(); 138 })); 139 } 140 // Add separator between layers and convert action 141 actions.add(SeparatorLayerAction.INSTANCE); 142 actions.add(new ConvertLayerAction(this)); 143 return actions.toArray(new Action[0]); 144 } 145 146 /** 147 * Get the data set for this layer 148 */ 149 public VectorDataSet getData() { 150 return this.dataSet; 151 } 152 153 private static class ConvertLayerAction extends AbstractAction implements LayerAction { 154 private final MVTLayer layer; 155 156 ConvertLayerAction(MVTLayer layer) { 157 this.layer = layer; 158 } 159 160 @Override public void actionPerformed(ActionEvent e) { 161 LayerManager manager = MainApplication.getLayerManager(); 162 VectorDataSet dataSet = layer.getData(); 163 DataSet osmData = new DataSet(); 164 // Add nodes first, map is to ensure we can map new nodes to vector nodes 165 Map<VectorNode, Node> nodeMap = new HashMap<>(dataSet.getNodes().size()); 166 for (VectorNode vectorNode : dataSet.getNodes()) { 167 Node newNode = new Node(vectorNode.getCoor()); 168 if (vectorNode.isTagged()) { 169 vectorNode.getInterestingTags().forEach(newNode::put); 170 } 171 nodeMap.put(vectorNode, newNode); 172 } 173 // Add ways next 174 Map<VectorWay, Way> wayMap = new HashMap<>(dataSet.getWays().size()); 175 for (VectorWay vectorWay : dataSet.getWays()) { 176 Way newWay = new Way(); 177 List<Node> nodes = vectorWay.getNodes().stream().map(nodeMap::get).filter(Objects::nonNull).collect(Collectors.toList()); 178 newWay.setNodes(nodes); 179 if (vectorWay.isTagged()) { 180 vectorWay.getInterestingTags().forEach(newWay::put); 181 } 182 wayMap.put(vectorWay, newWay); 183 } 184 185 // Finally, add Relations 186 Map<VectorRelation, Relation> relationMap = new HashMap<>(dataSet.getRelations().size()); 187 for (VectorRelation vectorRelation : dataSet.getRelations()) { 188 Relation relation = new Relation(); 189 if (vectorRelation.isTagged()) { 190 vectorRelation.getInterestingTags().forEach(relation::put); 191 } 192 List<RelationMember> members = vectorRelation.getMembers().stream().map(member -> { 193 final OsmPrimitive primitive; 194 if (member.getMember() instanceof VectorNode) { 195 primitive = nodeMap.get(member.getMember()); 196 } else if (member.getMember() instanceof VectorWay) { 197 primitive = wayMap.get(member.getMember()); 198 } else if (member.getMember() instanceof VectorRelation) { 199 // Hopefully, relations are encountered in order... 200 primitive = relationMap.get(member.getMember()); 201 } else { 202 primitive = null; 203 } 204 if (primitive == null) return null; 205 return new RelationMember(member.getRole(), primitive); 206 }).filter(Objects::nonNull).collect(Collectors.toList()); 207 relation.setMembers(members); 208 relationMap.put(vectorRelation, relation); 209 } 210 try { 211 osmData.beginUpdate(); 212 nodeMap.values().forEach(osmData::addPrimitive); 213 wayMap.values().forEach(osmData::addPrimitive); 214 relationMap.values().forEach(osmData::addPrimitive); 215 } finally { 216 osmData.endUpdate(); 217 } 218 manager.addLayer(new OsmDataLayer(osmData, this.layer.getName(), null)); 219 manager.removeLayer(this.layer); 220 } 221 222 @Override public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) { 223 return layers.stream().allMatch(MVTLayer.class::isInstance); 224 } 225 226 @Override public Component createMenuComponent() { 227 JMenuItem menuItem = new JMenuItem(tr("Convert to OSM Data")); 228 menuItem.addActionListener(this); 229 return menuItem; 230 } 231 } 232 233 private static class EnableLayerAction extends AbstractAction implements LayerAction { 234 private final String layer; 235 private final Consumer<String> consumer; 236 private final BooleanSupplier state; 237 238 EnableLayerAction(String layer, BooleanSupplier state, Consumer<String> consumer) { 239 super(tr("Toggle layer {0}", layer)); 240 this.layer = layer; 241 this.consumer = consumer; 242 this.state = state; 243 } 244 245 @Override 246 public void actionPerformed(ActionEvent e) { 247 consumer.accept(layer); 248 } 249 250 @Override 251 public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) { 252 return layers.stream().allMatch(MVTLayer.class::isInstance); 253 } 254 255 @Override 256 public Component createMenuComponent() { 257 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 258 item.setSelected(this.state.getAsBoolean()); 259 return item; 260 } 261 } 262 263 @Override 264 public void finishedLoading(MVTTile tile) { 265 for (Layer layer : tile.getLayers()) { 266 this.layerNames.putIfAbsent(layer.getName(), true); 267 } 268 this.dataSet.addTileData(tile); 269 } 270 } -
src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
a b 87 87 import org.openstreetmap.josm.data.imagery.OffsetBookmark; 88 88 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 89 89 import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 90 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 90 91 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 91 92 import org.openstreetmap.josm.data.preferences.BooleanProperty; 92 93 import org.openstreetmap.josm.data.preferences.IntegerProperty; … … 110 111 import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction; 111 112 import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction; 112 113 import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction; 114 import org.openstreetmap.josm.gui.layer.imagery.MVTLayer; 113 115 import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile; 114 116 import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction; 115 117 import org.openstreetmap.josm.gui.layer.imagery.TileAnchor; … … 890 892 if (coordinateConverter.requiresReprojection()) { 891 893 tile = new ReprojectionTile(tileSource, x, y, zoom); 892 894 } else { 893 tile = newTile(tileSource, x, y, zoom);895 tile = createTile(tileSource, x, y, zoom); 894 896 } 895 897 tileCache.addTile(tile); 896 898 } … … 1043 1045 img = getLoadedTileImage(tile); 1044 1046 anchorImage = getAnchor(tile, img); 1045 1047 } 1046 if (img == null || anchorImage == null ) {1048 if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) { 1047 1049 miss = true; 1048 1050 } 1049 1051 } … … 1052 1054 return; 1053 1055 } 1054 1056 1055 img = applyImageProcessors(img); 1057 if (img != null) { 1058 img = applyImageProcessors(img); 1059 } 1056 1060 1057 1061 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile); 1058 1062 synchronized (paintMutex) { … … 1864 1868 1865 1869 for (int x = minX; x <= maxX; x++) { 1866 1870 for (int y = minY; y <= maxY; y++) { 1867 requestedTiles.add( newTile(tileSource, x, y, currentZoomLevel));1871 requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel)); 1868 1872 } 1869 1873 } 1870 1874 } … … 1970 1974 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1971 1975 } 1972 1976 1977 /** 1978 * Create a new tile. Added to allow use of custom {@link Tile} objects. 1979 * 1980 * @param source Tile source 1981 * @param x X coordinate 1982 * @param y Y coordinate 1983 * @param zoom Zoom level 1984 * @return The new {@link Tile} 1985 * @since xxx 1986 */ 1987 public Tile createTile(T source, int x, int y, int zoom) { 1988 return new Tile(source, x, y, zoom); 1989 } 1990 1973 1991 @Override 1974 1992 public synchronized void destroy() { 1975 1993 super.destroy(); … … 1990 2008 allocateCacheMemory(); 1991 2009 if (memory != null) { 1992 2010 doPaint(graphics); 2011 if (AbstractTileSourceLayer.this instanceof MVTLayer) { 2012 AbstractTileSourceLayer.this.paint(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getMapView() 2013 .getRealBounds()); 2014 } 1993 2015 } else { 1994 2016 Graphics g = graphics.getDefaultGraphics(); 1995 2017 Color oldColor = g.getColor(); -
src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java b/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
a b 37 37 import org.openstreetmap.josm.gui.MapView; 38 38 import org.openstreetmap.josm.gui.MenuScroller; 39 39 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings; 40 import org.openstreetmap.josm.gui.layer.imagery.MVTLayer; 40 41 import org.openstreetmap.josm.gui.widgets.UrlLabel; 41 42 import org.openstreetmap.josm.tools.GBC; 42 43 import org.openstreetmap.josm.tools.ImageProcessor; … … 168 169 case BING: 169 170 case SCANEX: 170 171 return new TMSLayer(info); 172 case MVT: 173 return new MVTLayer(info); 171 174 default: 172 175 throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType())); 173 176 } -
src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java b/src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java
a b 85 85 Config.getPref().addPreferenceChangeListener(this); 86 86 } 87 87 88 /** 89 * Constructs a new {@code ElemStyles} with specific style sources. This does not listen to preference changes, 90 * and therefore should only be used with layers that have specific drawing requirements. 91 * 92 * @param sources The style sources (these cannot be added to, or removed from) 93 * @since xxx 94 */ 95 public ElemStyles(Collection<StyleSource> sources) { 96 this.styleSources.addAll(sources); 97 } 98 88 99 /** 89 100 * Clear the style cache for all primitives of all DataSets. 90 101 */ … … 151 162 * @since 13810 (signature) 152 163 */ 153 164 public Pair<StyleElementList, Range> getStyleCacheWithRange(IPrimitive osm, double scale, NavigatableComponent nc) { 154 if (!osm.isCachedStyleUpToDate() || scale <= 0) { 155 osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE); 156 } else { 157 Pair<StyleElementList, Range> lst = osm.getCachedStyle().getWithRange(scale, osm.isSelected()); 158 if (lst.a != null) 159 return lst; 160 } 161 Pair<StyleElementList, Range> p = getImpl(osm, scale, nc); 162 if (osm instanceof INode && isDefaultNodes()) { 163 if (p.a.isEmpty()) { 164 if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 165 p.a = DefaultStyles.DEFAULT_NODE_STYLELIST_TEXT; 166 } else { 167 p.a = DefaultStyles.DEFAULT_NODE_STYLELIST; 168 } 169 } else { 170 boolean hasNonModifier = false; 171 boolean hasText = false; 172 for (StyleElement s : p.a) { 173 if (s instanceof BoxTextElement) { 174 hasText = true; 175 } else { 176 if (!s.isModifier) { 177 hasNonModifier = true; 178 } 179 } 180 } 181 if (!hasNonModifier) { 182 p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_ELEMSTYLE); 183 if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 184 p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_TEXT_ELEMSTYLE); 185 } 186 } 187 } 188 } else if (osm instanceof IWay && isDefaultLines()) { 189 boolean hasProperLineStyle = false; 190 for (StyleElement s : p.a) { 191 if (s.isProperLineStyle()) { 192 hasProperLineStyle = true; 193 break; 194 } 195 } 196 if (!hasProperLineStyle) { 197 LineElement line = LineElement.UNTAGGED_WAY; 198 for (StyleElement element : p.a) { 199 if (element instanceof AreaElement) { 200 line = LineElement.createSimpleLineStyle(((AreaElement) element).color, true); 201 break; 202 } 203 } 204 p.a = new StyleElementList(p.a, line); 205 } 206 } 207 StyleCache style = osm.getCachedStyle() != null ? osm.getCachedStyle() : StyleCache.EMPTY_STYLECACHE; 208 try { 209 osm.setCachedStyle(style.put(p.a, p.b, osm.isSelected())); 210 } catch (RangeViolatedError e) { 211 throw new AssertionError("Range violated: " + e.getMessage() 212 + " (object: " + osm.getPrimitiveId() + ", current style: "+osm.getCachedStyle() 213 + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e); 214 } 215 osm.declareCachedStyleUpToDate(); 216 return p; 165 synchronized (osm.getStyleCacheSyncObject()) { 166 if (!osm.isCachedStyleUpToDate() || scale <= 0) { 167 osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE); 168 } else { 169 Pair<StyleElementList, Range> lst = osm.getCachedStyle().getWithRange(scale, osm.isSelected()); 170 if (lst.a != null) 171 return lst; 172 } 173 Pair<StyleElementList, Range> p = getImpl(osm, scale, nc); 174 if (osm instanceof INode && isDefaultNodes()) { 175 if (p.a.isEmpty()) { 176 if (TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 177 p.a = DefaultStyles.DEFAULT_NODE_STYLELIST_TEXT; 178 } else { 179 p.a = DefaultStyles.DEFAULT_NODE_STYLELIST; 180 } 181 } else { 182 boolean hasNonModifier = false; 183 boolean hasText = false; 184 for (StyleElement s : p.a) { 185 if (s instanceof BoxTextElement) { 186 hasText = true; 187 } else { 188 if (!s.isModifier) { 189 hasNonModifier = true; 190 } 191 } 192 } 193 if (!hasNonModifier) { 194 p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_ELEMSTYLE); 195 if (!hasText && TextLabel.AUTO_LABEL_COMPOSITION_STRATEGY.compose(osm) != null) { 196 p.a = new StyleElementList(p.a, DefaultStyles.SIMPLE_NODE_TEXT_ELEMSTYLE); 197 } 198 } 199 } 200 } else if (osm instanceof IWay && isDefaultLines()) { 201 boolean hasProperLineStyle = false; 202 for (StyleElement s : p.a) { 203 if (s.isProperLineStyle()) { 204 hasProperLineStyle = true; 205 break; 206 } 207 } 208 if (!hasProperLineStyle) { 209 LineElement line = LineElement.UNTAGGED_WAY; 210 for (StyleElement element : p.a) { 211 if (element instanceof AreaElement) { 212 line = LineElement.createSimpleLineStyle(((AreaElement) element).color, true); 213 break; 214 } 215 } 216 p.a = new StyleElementList(p.a, line); 217 } 218 } 219 StyleCache style = osm.getCachedStyle() != null ? osm.getCachedStyle() : StyleCache.EMPTY_STYLECACHE; 220 try { 221 osm.setCachedStyle(style.put(p.a, p.b, osm.isSelected())); 222 } catch (RangeViolatedError e) { 223 throw new AssertionError("Range violated: " + e.getMessage() 224 + " (object: " + osm.getPrimitiveId() + ", current style: " + osm.getCachedStyle() 225 + ", scale: " + scale + ", new stylelist: " + p.a + ", new range: " + p.b + ')', e); 226 } 227 osm.declareCachedStyleUpToDate(); 228 return p; 229 } 217 230 } 218 231 219 232 /** … … 241 254 * @param nc navigable component 242 255 * @return pair containing style list and range 243 256 */ 244 privatePair<StyleElementList, Range> getImpl(IPrimitive osm, double scale, NavigatableComponent nc) {257 Pair<StyleElementList, Range> getImpl(IPrimitive osm, double scale, NavigatableComponent nc) { 245 258 if (osm instanceof INode) 246 259 return generateStyles(osm, scale, false); 247 260 else if (osm instanceof IWay) { … … 376 389 * @since 13810 (signature) 377 390 */ 378 391 public Pair<StyleElementList, Range> generateStyles(IPrimitive osm, double scale, boolean pretendWayIsClosed) { 379 380 392 List<StyleElement> sl = new ArrayList<>(); 381 393 MultiCascade mc = new MultiCascade(); 382 394 Environment env = new Environment(osm, mc, null, null); -
new file src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.preferences.imagery; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.event.KeyAdapter; 7 import java.awt.event.KeyEvent; 8 import java.util.Arrays; 9 10 import javax.swing.JLabel; 11 12 import org.openstreetmap.josm.data.imagery.ImageryInfo; 13 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 14 import org.openstreetmap.josm.gui.widgets.JosmTextArea; 15 import org.openstreetmap.josm.gui.widgets.JosmTextField; 16 import org.openstreetmap.josm.tools.GBC; 17 import org.openstreetmap.josm.tools.Utils; 18 19 /** 20 * A panel for adding MapBox Vector Tile layers 21 * @author Taylor Smock 22 * @since xxx 23 */ 24 public class AddMVTLayerPanel extends AddImageryPanel { 25 private final JosmTextField mvtZoom = new JosmTextField(); 26 private final JosmTextArea mvtUrl = new JosmTextArea(3, 40).transferFocusOnTab(); 27 28 /** 29 * Constructs a new {@code AddMVTLayerPanel}. 30 */ 31 public AddMVTLayerPanel() { 32 33 add(new JLabel(tr("{0} Make sure OSM has the permission to use this service", "1.")), GBC.eol()); 34 add(new JLabel(tr("{0} Enter URL (may be a style sheet url)", "2.")), GBC.eol()); 35 add(new JLabel("<html>" + Utils.joinAsHtmlUnorderedList(Arrays.asList( 36 tr("{0} is replaced by tile zoom level, also supported:<br>" + 37 "offsets to the zoom level: {1} or {2}<br>" + 38 "reversed zoom level: {3}", "{zoom}", "{zoom+1}", "{zoom-1}", "{19-zoom}"), 39 tr("{0} is replaced by X-coordinate of the tile", "{x}"), 40 tr("{0} is replaced by Y-coordinate of the tile", "{y}"), 41 tr("{0} is replaced by a random selection from the given comma separated list, e.g. {1}", "{switch:...}", "{switch:a,b,c}") 42 )) + "</html>"), GBC.eol().fill()); 43 44 final KeyAdapter keyAdapter = new KeyAdapter() { 45 @Override 46 public void keyReleased(KeyEvent e) { 47 mvtUrl.setText(buildMvtUrl()); 48 } 49 }; 50 51 add(rawUrl, GBC.eop().fill()); 52 rawUrl.setLineWrap(true); 53 rawUrl.addKeyListener(keyAdapter); 54 55 add(new JLabel(tr("{0} Enter maximum zoom (optional)", "3.")), GBC.eol()); 56 mvtZoom.addKeyListener(keyAdapter); 57 add(mvtZoom, GBC.eop().fill()); 58 59 add(new JLabel(tr("{0} Edit generated {1} URL (optional)", "4.", "MVT")), GBC.eol()); 60 add(mvtUrl, GBC.eop().fill()); 61 mvtUrl.setLineWrap(true); 62 63 add(new JLabel(tr("{0} Enter name for this layer", "5.")), GBC.eol()); 64 add(name, GBC.eop().fill()); 65 66 registerValidableComponent(mvtUrl); 67 } 68 69 private String buildMvtUrl() { 70 StringBuilder a = new StringBuilder("mvt"); 71 String z = sanitize(mvtZoom.getText()); 72 if (!z.isEmpty()) { 73 a.append('[').append(z).append(']'); 74 } 75 a.append(':').append(sanitize(getImageryRawUrl(), ImageryType.MVT)); 76 return a.toString(); 77 } 78 79 @Override 80 public ImageryInfo getImageryInfo() { 81 final ImageryInfo generated = new ImageryInfo(getImageryName(), getMvtUrl()); 82 generated.setImageryType(ImageryType.MVT); 83 return generated; 84 } 85 86 protected final String getMvtUrl() { 87 return sanitize(mvtUrl.getText()); 88 } 89 90 @Override 91 protected boolean isImageryValid() { 92 return !getImageryName().isEmpty() && !getMvtUrl().isEmpty(); 93 } 94 } -
src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
a b 312 312 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMS)); 313 313 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.TMS)); 314 314 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMTS)); 315 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.MVT)); 315 316 activeToolbar.add(remove); 316 317 activePanel.add(activeToolbar, BorderLayout.EAST); 317 318 add(activePanel, GBC.eol().fill(GridBagConstraints.BOTH).weight(2.0, 0.4).insets(5, 0, 0, 5)); … … 439 440 break; 440 441 case WMTS: 441 442 icon = /* ICON(dialogs/) */ "add_wmts"; 443 break; 444 case MVT: 445 icon = /* ICON(dialogs/) */ "add_mvt"; 442 446 break; 443 447 default: 444 448 break; … … 460 464 case WMTS: 461 465 p = new AddWMTSLayerPanel(); 462 466 break; 467 case MVT: 468 p = new AddMVTLayerPanel(); 469 break; 463 470 default: 464 471 throw new IllegalStateException("Type " + type + " not supported"); 465 472 } … … 741 748 private static boolean confirmEulaAcceptance(PreferenceTabbedPane gui, String eulaUrl) { 742 749 URL url; 743 750 try { 744 url = new URL(eulaUrl.replaceAll("\\{lang \\}", LanguageInfo.getWikiLanguagePrefix()));751 url = new URL(eulaUrl.replaceAll("\\{lang}", LanguageInfo.getWikiLanguagePrefix())); 745 752 JosmEditorPane htmlPane; 746 753 try { 747 754 htmlPane = new JosmEditorPane(url); … … 749 756 Logging.trace(e1); 750 757 // give a second chance with a default Locale 'en' 751 758 try { 752 url = new URL(eulaUrl.replaceAll("\\{lang \\}", ""));759 url = new URL(eulaUrl.replaceAll("\\{lang}", "")); 753 760 htmlPane = new JosmEditorPane(url); 754 761 } catch (IOException e2) { 755 762 Logging.debug(e2); -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --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 new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertNotNull; 6 import static org.junit.jupiter.api.Assertions.assertTrue; 7 8 9 import java.nio.file.Paths; 10 import java.text.MessageFormat; 11 import java.util.Collection; 12 import java.util.Map; 13 import java.util.Objects; 14 import java.util.Optional; 15 import java.util.stream.Collectors; 16 17 import org.openstreetmap.josm.TestUtils; 18 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 19 import org.openstreetmap.josm.gui.mappaint.Keyword; 20 import org.openstreetmap.josm.gui.mappaint.StyleSource; 21 import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction; 22 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule; 23 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 24 import org.openstreetmap.josm.testutils.JOSMTestRules; 25 import org.openstreetmap.josm.tools.ColorHelper; 26 27 import org.junit.jupiter.api.Test; 28 import org.junit.jupiter.api.extension.RegisterExtension; 29 30 /** 31 * Test class for {@link MapBoxVectorStyle} 32 * @author Taylor Smock 33 */ 34 public class MapBoxVectorStyleTest { 35 // Needed for osm primitives (we really just need to initialize the config) 36 // OSM primitives are called when we load style sources 37 @RegisterExtension 38 JOSMTestRules rules = new JOSMTestRules(); 39 40 @Test 41 void testMapillaryStyle() { 42 final String file = Paths.get("file:", TestUtils.getTestDataRoot(), "mapillary.json").toString(); 43 final MapBoxVectorStyle style = MapBoxVectorStyle.getMapBoxVectorStyle(file); 44 assertNotNull(style); 45 // There are three "sources" in the mapillary.json file 46 assertEquals(3, style.getSources().size()); 47 final ElemStyles mapillarySource = style.getSources().entrySet().stream() 48 .filter(source -> "mapillary-source".equals(source.getKey().getName())).map( 49 Map.Entry::getValue).findAny().orElse(null); 50 assertNotNull(mapillarySource); 51 mapillarySource.getStyleSources().forEach(StyleSource::loadStyleSource); 52 assertEquals(1, mapillarySource.getStyleSources().size()); 53 final MapCSSStyleSource mapillaryCssSource = (MapCSSStyleSource) mapillarySource.getStyleSources().get(0); 54 assertTrue(mapillaryCssSource.getErrors().isEmpty()); 55 final MapCSSRule mapillaryOverview = getRule(mapillaryCssSource, "node", "mapillary-overview"); 56 assertNotNull(mapillaryOverview); 57 assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-shape", new Keyword("circle")); 58 assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-color", ColorHelper.html2color("#05CB63")); 59 assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-opacity", 0.6f); 60 assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-size", 4.0f); 61 } 62 63 /** 64 * Check that an instruction is in a collection of instructions, and return it 65 * @param instructions The instructions to search 66 * @param key The key to look for 67 * @param value The expected value for the key 68 */ 69 private void assertInInstructions(Collection<Instruction> instructions, String key, Object value) { 70 // In JOSM, all Instruction objects are AssignmentInstruction objects 71 Collection<Instruction.AssignmentInstruction> instructionKeys = instructions.stream().filter(Instruction.AssignmentInstruction.class::isInstance).map( 72 Instruction.AssignmentInstruction.class::cast).filter(instruction -> Objects.equals(key, instruction.key)).collect( 73 Collectors.toList()); 74 Optional<Instruction.AssignmentInstruction> instructionOptional = instructionKeys.stream().filter(instruction -> Objects.equals(value, instruction.val)).findAny(); 75 assertTrue(instructionOptional.isPresent(), MessageFormat 76 .format("Expected {0}, but got {1}", value, instructionOptional.orElse(instructionKeys.stream().findAny().orElseThrow(() -> new AssertionError("No instruction with "+key+" found"))).val)); 77 } 78 79 private static MapCSSRule getRule(MapCSSStyleSource source, String base, String subpart) { 80 return source.rules.stream().filter(rule -> rule.selectors.stream().filter(selector -> base.equals(selector.getBase()) && subpart.equals(selector.getSubpart().getId(null))).count() > 0).findAny().orElse(null); 81 } 82 } -
new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertNotNull; 6 import static org.junit.jupiter.api.Assertions.fail; 7 8 9 import java.awt.Shape; 10 import java.awt.geom.Ellipse2D; 11 import java.awt.geom.PathIterator; 12 import java.io.File; 13 import java.io.IOException; 14 import java.io.InputStream; 15 import java.nio.file.Paths; 16 import java.text.MessageFormat; 17 import java.util.ArrayList; 18 import java.util.Collection; 19 import java.util.List; 20 import java.util.stream.Collectors; 21 22 import org.openstreetmap.josm.TestUtils; 23 import org.openstreetmap.josm.data.coor.LatLon; 24 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature; 25 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Geometry; 26 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 27 import org.openstreetmap.josm.data.osm.BBox; 28 import org.openstreetmap.josm.data.osm.DataSet; 29 import org.openstreetmap.josm.data.osm.Node; 30 import org.openstreetmap.josm.data.osm.OsmPrimitive; 31 import org.openstreetmap.josm.data.osm.Relation; 32 import org.openstreetmap.josm.data.osm.RelationMember; 33 import org.openstreetmap.josm.data.osm.Way; 34 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 35 import org.openstreetmap.josm.io.Compression; 36 import org.openstreetmap.josm.testutils.JOSMTestRules; 37 38 import org.junit.jupiter.api.Test; 39 import org.junit.jupiter.api.extension.RegisterExtension; 40 41 /** 42 * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord} 43 * @author Taylor Smock 44 * @since xxx 45 */ 46 class ProtoBufTest { 47 @RegisterExtension 48 JOSMTestRules josmTestRules = new JOSMTestRules().preferences(); 49 50 /** 51 * Test simple message. 52 * Check that a simple message is readable 53 * @throws IOException - if an IO error occurs 54 */ 55 @Test 56 void testSimpleMessage() throws IOException { 57 ProtoBufParser parser = new ProtoBufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01}); 58 ProtoBufRecord record = new ProtoBufRecord(parser); 59 assertEquals(WireType.VARINT, record.getType()); 60 assertEquals(150, record.asUnsignedVarInt().intValue()); 61 } 62 63 /** 64 * Test reading tile from Mapillary ( 14/3251/6258 ) 65 * @throws IOException if there is a problem reading the file 66 */ 67 @Test 68 void testRead_14_3251_6258() throws IOException { 69 File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "6258.mvt").toFile(); 70 InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile); 71 Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords(); 72 assertEquals(2, records.size()); 73 List<Layer> layers = new ArrayList<>(); 74 for (ProtoBufRecord record : records) { 75 if (record.getField() == Layer.LAYER_FIELD) { 76 layers.add(new Layer(record.getBytes())); 77 } else { 78 fail(MessageFormat.format("Invalid field {0}", record.getField())); 79 } 80 } 81 Layer mapillarySequences = layers.get(0); 82 Layer mapillaryPictures = layers.get(1); 83 assertEquals("mapillary-sequences", mapillarySequences.getName()); 84 assertEquals("mapillary-images", mapillaryPictures.getName()); 85 assertEquals(2048, mapillarySequences.getExtent()); 86 assertEquals(2048, mapillaryPictures.getExtent()); 87 88 assertEquals(1, mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).count()); 89 Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 241083111).findAny().orElse(null); 90 assertEquals("jgxkXqVFM4jepMG3vP5Q9A", testSequence.getTags().get("key")); 91 assertEquals("C15Ul6qVMfQFlzRcmQCLcA", testSequence.getTags().get("ikey")); 92 assertEquals("x0hTY8cakpy0m3ui1GaG1A", testSequence.getTags().get("userkey")); 93 assertEquals(Long.valueOf(1565196718638L), Long.valueOf(testSequence.getTags().get("captured_at"))); 94 assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano"))); 95 } 96 97 /** 98 * Test reading tile from OpenInfraMap ( 16/13014/25030 ) 99 * @throws IOException if there is a problem reading the file 100 */ 101 @Test 102 void testRead_16_13014_25030() throws IOException { 103 // TODO finish 104 File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "16", "13014", "25030.pbf").toFile(); 105 InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile); 106 Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords(); 107 List<Layer> layers = new ArrayList<>(); 108 for (ProtoBufRecord record : records) { 109 if (record.getField() == Layer.LAYER_FIELD) { 110 layers.add(new Layer(record.getBytes())); 111 } else { 112 fail(MessageFormat.format("Invalid field {0}", record.getField())); 113 } 114 } 115 assertEquals(19, layers.size()); 116 List<Layer> dataLayers = layers.stream().filter(layer -> !layer.getFeatures().isEmpty()).collect(Collectors.toList()); 117 // power_plant, power_plant_point, power_generator, power_heatmap_solar, and power_generator_area 118 assertEquals(5, dataLayers.size()); 119 } 120 121 @Test 122 void testRead_17_26028_50060() throws IOException { 123 File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "17", "26028", "50060.pbf").toFile(); 124 InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile); 125 Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords(); 126 List<Layer> layers = new ArrayList<>(); 127 for (ProtoBufRecord record : records) { 128 if (record.getField() == Layer.LAYER_FIELD) { 129 layers.add(new Layer(record.getBytes())); 130 } else { 131 fail(MessageFormat.format("Invalid field {0}", record.getField())); 132 } 133 } 134 assertEquals(19, layers.size()); 135 List<Layer> dataLayers = layers.stream().filter(layer -> !layer.getFeatures().isEmpty()).collect(Collectors.toList()); 136 // power_plant, power_plant_point, power_generator, power_heatmap_solar, and power_generator_area 137 assertEquals(5, dataLayers.size()); 138 139 // power_generator_area was rendered incorrectly 140 final Layer powerGeneratorArea = dataLayers.stream().filter(layer -> "power_generator_area".equals(layer.getName())).findAny().orElse(null); 141 assertNotNull(powerGeneratorArea); 142 final int extent = powerGeneratorArea.getExtent(); 143 // 17/26028/50060 bounds 144 final BBox tileExtent = new BBox(new LatLon(39.068246, -108.511959), new LatLon(39.070381, -108.509219)); 145 final DataSet ds = new DataSet(); 146 for (Geometry feature : powerGeneratorArea.getGeometry()) { 147 final Collection<OsmPrimitive> primitives = feature.getShapes().stream().flatMap(shape -> convertShape(tileExtent, extent, shape).stream()).collect(Collectors.toList()); 148 primitives.forEach(ds::addPrimitive); 149 final OsmPrimitive toTag; 150 if (primitives.size() > 1) { 151 final Relation relation = new Relation(); 152 primitives.forEach(prim -> relation.addMember(new RelationMember("", prim))); 153 ds.addPrimitive(relation); 154 toTag = relation; 155 } else { 156 toTag = primitives.iterator().next(); 157 } 158 feature.getFeature().getTags().forEach((key, value) -> toTag.put(key, value)); 159 } 160 final Way one = new Way(); 161 one.addNode(new Node(new LatLon(39.0687509, -108.5100816))); 162 one.addNode(new Node(new LatLon(39.0687509, -108.5095751))); 163 one.addNode(new Node(new LatLon(39.0687169, -108.5095751))); 164 one.addNode(new Node(new LatLon(39.0687169, -108.5100816))); 165 one.addNode(one.getNode(0)); 166 one.setOsmId(666293899, 2); 167 final BBox searchBBox = one.getBBox(); 168 searchBBox.addPrimitive(one, 0.001); 169 final Collection<Node> searchedNodes = ds.searchNodes(searchBBox); 170 OsmDataLayer testLayer = new OsmDataLayer(ds, "", null); 171 testLayer.autosave(new File("/tmp/test.osm")); 172 assertEquals(4, searchedNodes.size()); 173 } 174 175 /** 176 * Convert a latlon to a relative latlon for the bbox 177 * @param tileExtent The tile extent 178 * @param toConvert The shape 179 * @return An OSM primitive representing the shape 180 */ 181 private static Collection<OsmPrimitive> convertShape(BBox tileExtent, int extent, Shape toConvert) { 182 final List<Node> nodes = new ArrayList<>(); 183 final List<Way> ways = new ArrayList<>(); 184 final List<Relation> relations = new ArrayList<>(); 185 final PathIterator iterator = toConvert.getPathIterator(null); 186 final List<Node> wayNodes = new ArrayList<>(); 187 while (!iterator.isDone()) { 188 final double[] coords = new double[6]; 189 final int type = iterator.currentSegment(coords); 190 if (type == PathIterator.SEG_MOVETO || type == PathIterator.SEG_LINETO) { 191 final Node node = convertPointToNode(tileExtent, extent, coords[0], coords[1]); 192 nodes.add(node); 193 if (type == PathIterator.SEG_MOVETO && wayNodes.size() > 1) { 194 final Way way = new Way(); 195 way.setNodes(wayNodes); 196 ways.add(way); 197 wayNodes.clear(); 198 } else if (type == PathIterator.SEG_MOVETO) { 199 wayNodes.clear(); 200 } 201 wayNodes.add(node); 202 } else if (type == PathIterator.SEG_CLOSE) { 203 wayNodes.add(wayNodes.get(0)); 204 final Way way = new Way(); 205 way.setNodes(wayNodes); 206 ways.add(way); 207 wayNodes.clear(); 208 } 209 iterator.next(); 210 } 211 212 final Collection<OsmPrimitive> primitives = new ArrayList<>(nodes); 213 primitives.addAll(ways); 214 primitives.addAll(relations); 215 return primitives; 216 } 217 218 private static Node convertPointToNode(BBox tileExtent, int extent, double x, double y) { 219 final double latDiff = tileExtent.getTopLeftLat() - tileExtent.getBottomRightLat(); 220 final double lonDiff = tileExtent.getBottomRightLon() - tileExtent.getTopLeftLon(); 221 final double lat = tileExtent.getTopLeftLat() - y * latDiff / extent; 222 final double lon = tileExtent.getTopLeftLon() - x * lonDiff / extent; 223 return new Node(new LatLon(lat, lon)); 224 } 225 226 227 // TODO remove temporary tests or indicate that they are from the vector-tile-js library (BSD-3) 228 @Test 229 void test_14_8801_5371() throws IOException { 230 File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "tmp", "14-8801-5371.vector.pbf").toFile(); 231 InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile); 232 Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords(); 233 List<Layer> layers = new ArrayList<>(); 234 for (ProtoBufRecord record : records) { 235 if (record.getField() == Layer.LAYER_FIELD) { 236 layers.add(new Layer(record.getBytes())); 237 } else { 238 fail(MessageFormat.format("Invalid field {0}", record.getField())); 239 } 240 } 241 assertEquals(20, layers.size()); 242 Geometry park = layers.stream().filter(layer -> "poi_label".equals(layer.getName())).flatMap(layer -> layer.getGeometry().stream()).filter(g -> g.getFeature().getId() == 3000003150561L).findAny().orElse(null); 243 assertEquals("Mauerpark", park.getFeature().getTags().get("name")); 244 assertEquals("Park", park.getFeature().getTags().get("type")); 245 246 Ellipse2D parkShape = (Ellipse2D) park.getShapes().iterator().next(); 247 assertEquals(3898, parkShape.getCenterX()); 248 assertEquals(1731, parkShape.getCenterY()); 249 250 Geometry road = layers.stream().filter(layer -> "road".equals(layer.getName())).flatMap(layer -> layer.getGeometry().stream()).skip(656).findFirst().orElse(null); 251 PathIterator roadIterator = road.getShapes().iterator().next().getPathIterator(null); 252 double[] coords = new double[6]; 253 assertEquals(PathIterator.SEG_MOVETO, roadIterator.currentSegment(coords)); 254 assertEquals(1988, coords[0]); 255 assertEquals(306, coords[1]); 256 roadIterator.next(); 257 assertEquals(PathIterator.SEG_LINETO, roadIterator.currentSegment(coords)); 258 assertEquals(1808, coords[0]); 259 assertEquals(321, coords[1]); 260 roadIterator.next(); 261 assertEquals(PathIterator.SEG_LINETO, roadIterator.currentSegment(coords)); 262 assertEquals(1506, coords[0]); 263 assertEquals(347, coords[1]); 264 } 265 266 @Test 267 void testSingletonMultiPoint() throws IOException { 268 File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "tmp", "singleton-multi-point.pbf").toFile(); 269 InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile); 270 Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords(); 271 List<Layer> layers = new ArrayList<>(); 272 for (ProtoBufRecord record : records) { 273 if (record.getField() == Layer.LAYER_FIELD) { 274 layers.add(new Layer(record.getBytes())); 275 } else { 276 fail(MessageFormat.format("Invalid field {0}", record.getField())); 277 } 278 } 279 assertEquals(1, layers.size()); 280 assertEquals(1, layers.get(0).getGeometry().size()); 281 Ellipse2D shape = (Ellipse2D) layers.get(0).getGeometry().iterator().next().getShapes().iterator().next(); 282 assertEquals(2059, shape.getCenterX()); 283 assertEquals(2071, shape.getCenterY()); 284 } 285 286 @Test 287 void testReadVarInt() { 288 assertEquals(ProtoBufParser.convertLong(0), bytesToVarInt(0x0)); 289 assertEquals(ProtoBufParser.convertLong(1), bytesToVarInt(0x1)); 290 assertEquals(ProtoBufParser.convertLong(127), bytesToVarInt(0x7f)); 291 // This should b 0xff 0xff 0xff 0xff 0x07, but we drop the leading bit when reading to a byte array 292 Number actual = bytesToVarInt(0x7f, 0x7f, 0x7f, 0x7f, 0x07); 293 assertEquals(ProtoBufParser.convertLong(Integer.MAX_VALUE), actual, 294 MessageFormat.format("Expected {0} but got {1}", Integer.toBinaryString(Integer.MAX_VALUE), 295 Long.toBinaryString(actual.longValue()))); 296 } 297 298 @Test 299 void testZigZag() { 300 assertEquals(0, ProtoBufParser.decodeZigZag(0).intValue()); 301 assertEquals(-1, ProtoBufParser.decodeZigZag(1).intValue()); 302 assertEquals(1, ProtoBufParser.decodeZigZag(2).intValue()); 303 assertEquals(-2, ProtoBufParser.decodeZigZag(3).intValue()); 304 } 305 306 private Number bytesToVarInt(int... bytes) { 307 byte[] byteArray = new byte[bytes.length]; 308 for (int i = 0; i < bytes.length; i++) { 309 byteArray[i] = (byte) bytes[i]; 310 } 311 return ProtoBufParser.convertByteArray(byteArray, ProtoBufParser.VAR_INT_BYTE_SIZE); 312 } 313 } -
new file test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java b/test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertTrue; 6 7 8 import java.nio.file.Paths; 9 import java.text.MessageFormat; 10 import java.util.ArrayList; 11 import java.util.Collection; 12 import java.util.Collections; 13 import java.util.HashSet; 14 import java.util.List; 15 import java.util.Map; 16 import java.util.stream.Collectors; 17 18 import org.openstreetmap.josm.TestUtils; 19 import org.openstreetmap.josm.data.imagery.ImageryInfo; 20 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile; 21 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader; 22 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource; 23 import org.openstreetmap.josm.gui.layer.imagery.MVTLayer; 24 import org.openstreetmap.josm.testutils.JOSMTestRules; 25 26 import org.awaitility.Awaitility; 27 import org.awaitility.Durations; 28 import org.junit.jupiter.api.BeforeEach; 29 import org.junit.jupiter.api.Test; 30 import org.junit.jupiter.api.extension.RegisterExtension; 31 32 /** 33 * A test for {@link VectorDataSet} 34 */ 35 class VectorDataSetTest { 36 /** 37 * Make some methods available for this test class 38 */ 39 private static class MVTLayerMock extends MVTLayer { 40 private final Collection<MVTTile> finishedLoading = new HashSet<>(); 41 42 MVTLayerMock(ImageryInfo info) { 43 super(info); 44 } 45 46 @Override 47 protected MapboxVectorTileSource getTileSource() { 48 return super.getTileSource(); 49 } 50 51 protected MapBoxVectorCachedTileLoader getTileLoader() { 52 if (this.tileLoader == null) { 53 this.tileLoader = this.getTileLoaderFactory().makeTileLoader(this, Collections.emptyMap(), 7200); 54 } 55 if (this.tileLoader instanceof MapBoxVectorCachedTileLoader) { 56 return (MapBoxVectorCachedTileLoader) this.tileLoader; 57 } 58 return null; 59 } 60 61 @Override 62 public void finishedLoading(MVTTile tile) { 63 super.finishedLoading(tile); 64 this.finishedLoading.add(tile); 65 } 66 67 public Collection<MVTTile> finishedLoading() { 68 return this.finishedLoading; 69 } 70 } 71 72 @RegisterExtension JOSMTestRules rule = new JOSMTestRules().projection(); 73 74 /** 75 * Load arbitrary tiles 76 * @param layer The layer to add the tiles to 77 * @param tiles The tiles to load ([z, x, y, z, x, y, ...]) -- must be divisible by three 78 */ 79 private static void loadTile(MVTLayerMock layer, int... tiles) { 80 if (tiles.length % 3 != 0 || tiles.length == 0) { 81 throw new IllegalArgumentException("Tiles come with a {z}, {x}, and {y} component"); 82 } 83 final MapboxVectorTileSource tileSource = layer.getTileSource(); 84 MapBoxVectorCachedTileLoader tileLoader = layer.getTileLoader(); 85 Collection<MVTTile> tilesCollection = new ArrayList<>(); 86 for (int i = 0; i < tiles.length / 3; i++) { 87 final MVTTile tile = (MVTTile) layer.createTile(tileSource, tiles[3 * i + 1], tiles[3 * i + 2], tiles[3 * i]); 88 tileLoader.createTileLoaderJob(tile).submit(); 89 tilesCollection.add(tile); 90 } 91 Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> layer.finishedLoading().size() == tilesCollection 92 .size()); 93 } 94 95 private MVTLayerMock layer; 96 97 @BeforeEach 98 void setup() { 99 // Create the preconditions for the test 100 final ImageryInfo info = new ImageryInfo(); 101 info.setName("en", "Test info"); 102 info.setUrl("file:/" + Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "{z}", "{x}", "{y}.mvt")); 103 layer = new MVTLayerMock(info); 104 } 105 106 @Test 107 void testNodeDeduplication() { 108 final VectorDataSet dataSet = this.layer.getData(); 109 assertTrue(dataSet.allPrimitives().isEmpty()); 110 111 // Set the zoom to 14, as that is the tile we are checking 112 dataSet.setZoom(14); 113 loadTile(this.layer, 14, 3248, 6258); 114 115 // Actual test 116 // With Mapillary, only ends of ways should be untagged 117 // There are 55 actual "nodes" in the data with two nodes for the ends of the way. 118 assertEquals(57, dataSet.getNodes().size()); 119 assertEquals(1, dataSet.getWays().size()); 120 assertEquals(0, dataSet.getRelations().size()); 121 } 122 123 @Test 124 void testWayDeduplicationSimple() { 125 final VectorDataSet dataSet = this.layer.getData(); 126 assertTrue(dataSet.allPrimitives().isEmpty()); 127 128 // Set the zoom to 14, as that is the tile we are checking 129 dataSet.setZoom(14); 130 // Load tiles that are next to each other 131 loadTile(this.layer, 14, 3248, 6258, 14, 3248, 6257); 132 133 Map<Long, List<VectorWay>> wayGroups = dataSet.getWays().stream() 134 .collect(Collectors.groupingBy(VectorWay::getId)); 135 wayGroups.forEach((id, ways) -> assertEquals(1, ways.size(), MessageFormat.format("{0} was not deduplicated", id))); 136 } 137 }