Ticket #17177: 17177.9.patch
| File 17177.9.patch, 370.7 KB (added by , 5 years ago) |
|---|
-
new file resources/images/dialogs/add_mvt.svg
diff --git resources/images/dialogs/add_mvt.svg resources/images/dialogs/add_mvt.svg new file mode 100644 index 000000000..eeae80f10
- + 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
diff --git src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java src/org/openstreetmap/josm/data/cache/JCSCachedTileLoaderJob.java index a8561a771..eeac761c6 100644
1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.cache; 3 3 4 import java.io.File; 4 5 import java.io.FileNotFoundException; 5 6 import java.io.IOException; 7 import java.io.InputStream; 6 8 import java.net.HttpURLConnection; 7 9 import java.net.URL; 10 import java.nio.file.Files; 8 11 import java.security.SecureRandom; 9 12 import java.util.Collections; 10 13 import java.util.List; … … import java.util.concurrent.ThreadPoolExecutor; 17 20 import java.util.concurrent.TimeUnit; 18 21 import java.util.regex.Matcher; 19 22 20 import org.apache.commons.jcs3.access.behavior.ICacheAccess;21 import org.apache.commons.jcs3.engine.behavior.ICacheElement;22 23 import org.openstreetmap.josm.data.cache.ICachedLoaderListener.LoadResult; 23 24 import org.openstreetmap.josm.data.imagery.TileJobOptions; 24 25 import org.openstreetmap.josm.data.preferences.IntegerProperty; … … import org.openstreetmap.josm.tools.HttpClient; 27 28 import org.openstreetmap.josm.tools.Logging; 28 29 import org.openstreetmap.josm.tools.Utils; 29 30 31 import org.apache.commons.compress.utils.IOUtils; 32 import org.apache.commons.jcs3.access.behavior.ICacheAccess; 33 import org.apache.commons.jcs3.engine.behavior.ICacheElement; 34 30 35 /** 31 36 * Generic loader for HTTP based tiles. Uses custom attribute, to check, if entry has expired 32 37 * according to HTTP headers sent with tile. If so, it tries to verify using Etags … … public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 294 299 if (attributes == null) { 295 300 attributes = new CacheEntryAttributes(); 296 301 } 302 final URL url = this.getUrlNoException(); 303 if (url == null) { 304 return false; 305 } 306 307 if (url.getProtocol().contains("http")) { 308 return loadObjectHttp(); 309 } 310 if (url.getProtocol().contains("file")) { 311 return loadObjectFile(url); 312 } 313 314 return false; 315 } 316 317 private boolean loadObjectFile(URL url) { 318 String fileName = url.toExternalForm(); 319 File file = new File(fileName.substring("file:/".length() - 1)); 320 if (!file.exists()) { 321 file = new File(fileName.substring("file://".length() - 1)); 322 } 323 try (InputStream fileInputStream = Files.newInputStream(file.toPath())) { 324 cacheData = createCacheEntry(IOUtils.toByteArray(fileInputStream)); 325 cache.put(getCacheKey(), cacheData, attributes); 326 return true; 327 } catch (IOException e) { 328 Logging.error(e); 329 attributes.setError(e); 330 attributes.setException(e); 331 } 332 return false; 333 } 334 335 /** 336 * @return true if object was successfully downloaded via http, false, if there was a loading failure 337 */ 338 private boolean loadObjectHttp() { 297 339 try { 298 340 // if we have object in cache, and host doesn't support If-Modified-Since nor If-None-Match 299 341 // then just use HEAD request and check returned values … … public abstract class JCSCachedTileLoaderJob<K, V extends CacheEntry> implements 553 595 try { 554 596 return getUrl(); 555 597 } catch (IOException e) { 598 Logging.trace(e); 556 599 return null; 557 600 } 558 601 } -
src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
diff --git src/org/openstreetmap/josm/data/imagery/ImageryInfo.java src/org/openstreetmap/josm/data/imagery/ImageryInfo.java index 07cabc76a..32b1055ed 100644
public class ImageryInfo extends 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 … … public class ImageryInfo extends 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; … … public class ImageryInfo extends 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
diff --git src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java index 41c69e377..358e2d810 100644
import java.net.URL; 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; … … import java.util.concurrent.TimeUnit; 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; … … import org.openstreetmap.josm.data.cache.CacheEntry; 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 * … … public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, Buffe 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; … … public class TMSCachedTileLoaderJob extends JCSCachedTileLoaderJob<String, Buffe 318 322 private boolean tryLoadTileImage(CacheEntry object) throws IOException { 319 323 if (object != null) { 320 324 byte[] content = object.getContent(); 321 if (content.length > 0 ) {325 if (content.length > 0 || tile instanceof VectorTile) { 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/VectorTile.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java new file mode 100644 index 000000000..692f3ea8c
- + 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 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java new file mode 100644 index 000000000..05ffcf945
- + 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
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java new file mode 100644 index 000000000..5213bf0e8
- + 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
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java new file mode 100644 index 000000000..df194cc00
- + 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 private Geometry geometryObject; 45 46 /** 47 * Create a new Feature 48 * 49 * @param layer The layer the feature is part of (required for tags) 50 * @param record The record to create the feature from 51 * @throws IOException - if an IO error occurs 52 */ 53 public Feature(Layer layer, ProtoBufRecord record) throws IOException { 54 long tId = 0; 55 GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN; 56 String key = null; 57 try (ProtoBufParser parser = new ProtoBufParser(record.getBytes())) { 58 while (parser.hasNext()) { 59 try (ProtoBufRecord next = new ProtoBufRecord(parser)) { 60 if (next.getField() == TAG_FIELD) { 61 if (tags == null) { 62 tags = new TagMap(); 63 } 64 // This is packed in v1 and v2 65 ProtoBufPacked packed = new ProtoBufPacked(next.getBytes()); 66 for (Number number : packed.getArray()) { 67 key = parseTagValue(key, layer, number); 68 } 69 } else if (next.getField() == GEOMETRY_FIELD) { 70 // This is packed in v1 and v2 71 ProtoBufPacked packed = new ProtoBufPacked(next.getBytes()); 72 CommandInteger currentCommand = null; 73 for (Number number : packed.getArray()) { 74 if (currentCommand != null && currentCommand.hasAllExpectedParameters()) { 75 currentCommand = null; 76 } 77 if (currentCommand == null) { 78 currentCommand = new CommandInteger(number.intValue()); 79 this.geometry.add(currentCommand); 80 } else { 81 currentCommand.addParameter(ProtoBufParser.decodeZigZag(number)); 82 } 83 } 84 // TODO fallback to non-packed 85 } else if (next.getField() == GEOMETRY_TYPE_FIELD) { 86 geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()]; 87 } else if (next.getField() == ID_FIELD) { 88 tId = next.asUnsignedVarInt().longValue(); 89 } 90 } 91 } 92 } 93 this.id = tId; 94 this.geometryType = geometryTypeTemp; 95 record.close(); 96 } 97 98 /** 99 * Parse a tag value 100 * 101 * @param key The current key (or {@code null}, if {@code null}, the returned value will be the new key) 102 * @param layer The layer with key/value information 103 * @param number The number to get the value from 104 * @return The new key (if {@code null}, then a value was parsed and added to tags) 105 */ 106 private String parseTagValue(String key, Layer layer, Number number) { 107 if (key == null) { 108 key = layer.getKey(number.intValue()); 109 } else { 110 Object value = layer.getValue(number.intValue()); 111 if (value instanceof Double || value instanceof Float) { 112 // reset grouping if the instance is a singleton 113 final NumberFormat numberFormat = NumberFormat.getNumberInstance(); 114 final boolean grouping = numberFormat.isGroupingUsed(); 115 try { 116 numberFormat.setGroupingUsed(false); 117 this.tags.put(key, numberFormat.format(value)); 118 } finally { 119 numberFormat.setGroupingUsed(grouping); 120 } 121 } else { 122 this.tags.put(key, Utils.intern(value.toString())); 123 } 124 key = null; 125 } 126 return key; 127 } 128 129 /** 130 * Get the geometry instructions 131 * 132 * @return The geometry 133 */ 134 public List<CommandInteger> getGeometry() { 135 return this.geometry; 136 } 137 138 /** 139 * Get the geometry type 140 * 141 * @return The {@link GeometryTypes} 142 */ 143 public GeometryTypes getGeometryType() { 144 return this.geometryType; 145 } 146 147 /** 148 * Get the id of the object 149 * 150 * @return The unique id in the layer, or 0. 151 */ 152 public long getId() { 153 return this.id; 154 } 155 156 /** 157 * Get the tags 158 * 159 * @return A tag map 160 */ 161 public TagMap getTags() { 162 return this.tags; 163 } 164 165 /** 166 * Get the an object with shapes for the geometry 167 * @return An object with usable geometry information 168 */ 169 public Geometry getGeometryObject() { 170 if (this.geometryObject == null) { 171 this.geometryObject = new Geometry(this.getGeometryType(), this.getGeometry()); 172 } 173 return this.geometryObject; 174 } 175 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java new file mode 100644 index 000000000..c612c7e83
- + 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.ArrayList; 11 import java.util.Collection; 12 import java.util.Collections; 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 final Collection<Shape> shapes = new ArrayList<>(); 22 23 /** 24 * Create a {@link Geometry} for a {@link Feature} 25 * @param geometryType The type of geometry 26 * @param commands The commands used to create the geometry 27 */ 28 public Geometry(GeometryTypes geometryType, List<CommandInteger> commands) { 29 if (geometryType == GeometryTypes.POINT) { 30 for (CommandInteger command : commands) { 31 final short[] operations = command.getOperations(); 32 // Each MoveTo command is a new point 33 if (command.getType() == Command.MoveTo && operations.length % 2 == 0 && operations.length > 0) { 34 for (int i = 0; i < operations.length / 2; i++) { 35 // Just using Ellipse2D since it extends Shape 36 shapes.add(new Ellipse2D.Float(operations[2 * i], 37 operations[2 * i + 1], 0, 0)); 38 } 39 } else { 40 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 41 } 42 } 43 } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) { 44 Path2D.Float line = null; 45 Area area = null; 46 // MVT uses delta encoding. Each feature starts at (0, 0). 47 double x = 0; 48 double y = 0; 49 // Area is used to determine the inner/outer of a polygon 50 double areaAreaSq = 0; 51 for (CommandInteger command : commands) { 52 final short[] operations = command.getOperations(); 53 // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior 54 if (command.getType() == Command.MoveTo && operations.length == 2) { 55 areaAreaSq = 0; 56 x += operations[0]; 57 y += operations[1]; 58 line = new Path2D.Float(); 59 line.moveTo(x, y); 60 shapes.add(line); 61 } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) { 62 for (int i = 0; i < operations.length / 2; i++) { 63 final double lx = x; 64 final double ly = y; 65 x += operations[2 * i]; 66 y += operations[2 * i + 1]; 67 areaAreaSq += lx * y - x * ly; 68 line.lineTo(x, y); 69 } 70 // ClosePath should only be used with Polygon geometry 71 } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) { 72 shapes.remove(line); 73 // new Area() closes the line if it isn't already closed 74 if (area == null) { 75 area = new Area(); 76 shapes.add(area); 77 } 78 79 Area nArea = new Area(line); 80 // SonarLint thinks that this is never > 0. It can be. 81 if (areaAreaSq > 0) { 82 area.add(nArea); 83 } else if (areaAreaSq < 0) { 84 area.exclusiveOr(nArea); 85 } else { 86 throw new IllegalArgumentException(tr("{0} cannot have zero area", geometryType)); 87 } 88 } else { 89 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 90 } 91 } 92 } 93 } 94 95 /** 96 * Get the shapes to draw this geometry with 97 * @return A collection of shapes 98 */ 99 public Collection<Shape> getShapes() { 100 return Collections.unmodifiableCollection(this.shapes); 101 } 102 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java new file mode 100644 index 000000000..0dc29c6a6
- + 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, 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, 15 /** May be a line or a multiline geometry. Each line {@link Command#MoveTo} and one or more {@link Command#LineTo}. */ 16 LINESTRING, 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; 20 21 /** 22 * Rings used by {@link GeometryTypes#POLYGON} 23 * @author Taylor Smock 24 */ 25 public enum Ring { 26 /** A ring that goes in the clockwise direction */ 27 ExteriorRing, 28 /** A ring that goes in the anti-clockwise direction */ 29 InteriorRing 30 } 31 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java new file mode 100644 index 000000000..d1186ad3f
- + 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
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java new file mode 100644 index 000000000..09851e8c7
- + 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 final 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 94 /** 95 * Create a layer from a collection of records 96 * @param records The records to convert to a layer 97 * @throws IOException - if an IO error occurs 98 */ 99 public Layer(Collection<ProtoBufRecord> records) throws IOException { 100 // Do the unique required fields first 101 Map<Integer, List<ProtoBufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtoBufRecord::getField)); 102 this.version = sorted.getOrDefault((int) VERSION_FIELD, Collections.emptyList()).parallelStream() 103 .map(ProtoBufRecord::asUnsignedVarInt).map(Number::byteValue).findFirst().orElse(DEFAULT_VERSION); 104 // Per spec, we cannot continue past this until we have checked the version number 105 if (this.version != 1 && this.version != 2) { 106 throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version)); 107 } 108 this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst() 109 .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name"))); 110 this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asSignedVarInt) 111 .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT); 112 113 sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString) 114 .forEachOrdered(this.keyList::add); 115 sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::getBytes) 116 .map(ProtoBufParser::new).map(parser1 -> { 117 try { 118 return new ProtoBufRecord(parser1); 119 } catch (IOException e) { 120 Logging.error(e); 121 return null; 122 } 123 }) 124 .filter(Objects::nonNull) 125 .map(value -> ValueFields.MAPPERS.parallelStream() 126 .filter(v -> v.getField() == value.getField()) 127 .map(v -> v.convertValue(value)).findFirst() 128 .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField())))) 129 .forEachOrdered(this.valueList::add); 130 Collection<IOException> exceptions = new HashSet<>(0); 131 this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> { 132 try { 133 return new Feature(this, feature); 134 } catch (IOException e) { 135 exceptions.add(e); 136 } 137 return null; 138 }).collect(Collectors.toList()); 139 if (!exceptions.isEmpty()) { 140 throw exceptions.iterator().next(); 141 } 142 // Cleanup bytes (for memory) 143 for (ProtoBufRecord record : records) { 144 record.close(); 145 } 146 } 147 148 /** 149 * Get all the records from a array of bytes 150 * @param bytes The byte information 151 * @return All the protobuf records 152 * @throws IOException If there was an error reading the bytes (unlikely) 153 */ 154 private static Collection<ProtoBufRecord> getAllRecords(byte[] bytes) throws IOException { 155 try (ProtoBufParser parser = new ProtoBufParser(bytes)) { 156 return parser.allRecords(); 157 } 158 } 159 160 /** 161 * Create a new layer 162 * @param bytes The bytes that the layer comes from 163 * @throws IOException - if an IO error occurs 164 */ 165 public Layer(byte[] bytes) throws IOException { 166 this(getAllRecords(bytes)); 167 } 168 169 /** 170 * Get the extent of the tile 171 * @return The layer extent 172 */ 173 public int getExtent() { 174 return this.extent; 175 } 176 177 /** 178 * Get the feature on this layer 179 * @return the features 180 */ 181 public Collection<Feature> getFeatures() { 182 return Collections.unmodifiableCollection(this.featureCollection); 183 } 184 185 /** 186 * Get the geometry for this layer 187 * @return The geometry 188 */ 189 public Collection<Geometry> getGeometry() { 190 return getFeatures().stream().map(Feature::getGeometryObject).collect(Collectors.toList()); 191 } 192 193 /** 194 * Get a specified key 195 * @param index The index in the key list 196 * @return The actual key 197 */ 198 public String getKey(int index) { 199 return this.keyList.get(index); 200 } 201 202 /** 203 * Get the name of the layer 204 * @return The layer name 205 */ 206 public String getName() { 207 return this.name; 208 } 209 210 /** 211 * Get a specified value 212 * @param index The index in the value list 213 * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value. 214 */ 215 public Object getValue(int index) { 216 return this.valueList.get(index); 217 } 218 219 /** 220 * Get the MapBox Vector Tile version specification for this layer 221 * @return The version of the MapBox Vector Tile specification 222 */ 223 public byte getVersion() { 224 return this.version; 225 } 226 227 @Override 228 public boolean equals(Object other) { 229 if (other instanceof Layer) { 230 Layer o = (Layer) other; 231 return this.extent == o.extent 232 && this.version == o.version 233 && Objects.equals(this.name, o.name) 234 && Objects.equals(this.featureCollection, o.featureCollection) 235 && Objects.equals(this.keyList, o.keyList) 236 && Objects.equals(this.valueList, o.valueList); 237 } 238 return false; 239 } 240 241 @Override 242 public int hashCode() { 243 return Objects.hash(this.name, this.version, this.extent, this.featureCollection, this.keyList, this.valueList); 244 } 245 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java new file mode 100644 index 000000000..84ac8ae89
- + 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
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java new file mode 100644 index 000000000..5d1d781dd
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.awt.image.BufferedImage; 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.stream.Collectors; 11 12 import org.openstreetmap.gui.jmapviewer.Tile; 13 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 14 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 15 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 16 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 17 import org.openstreetmap.josm.tools.ListenerList; 18 import org.openstreetmap.josm.tools.Logging; 19 20 /** 21 * A class for MapBox Vector Tiles 22 * 23 * @author Taylor Smock 24 * @since xxx 25 */ 26 public class MVTTile extends Tile implements VectorTile { 27 private final ListenerList<TileListener> listenerList = ListenerList.create(); 28 private Collection<Layer> layers; 29 private int extent = Layer.DEFAULT_EXTENT; 30 static final BufferedImage CLEAR_LOADED = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR); 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 loadImage(final InputStream inputStream) throws IOException { 45 if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) { 46 this.initLoading(); 47 ProtoBufParser parser = new ProtoBufParser(inputStream); 48 Collection<ProtoBufRecord> protoBufRecords = parser.allRecords(); 49 this.layers = new HashSet<>(); 50 this.layers = protoBufRecords.stream().map(record -> { 51 Layer mvtLayer = null; 52 if (record.getField() == Layer.LAYER_FIELD) { 53 try (ProtoBufParser tParser = new ProtoBufParser(record.getBytes())) { 54 mvtLayer = new Layer(tParser.allRecords()); 55 } catch (IOException e) { 56 Logging.error(e); 57 } finally { 58 // Cleanup bytes 59 record.close(); 60 } 61 } 62 return mvtLayer; 63 }).collect(Collectors.toCollection(HashSet::new)); 64 this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT); 65 this.finishLoading(); 66 this.listenerList.fireEvent(event -> event.finishedLoading(this)); 67 // Ensure that we don't keep the loading image around 68 this.image = CLEAR_LOADED; 69 } 70 } 71 72 @Override 73 public Collection<Layer> getLayers() { 74 return this.layers; 75 } 76 77 @Override 78 public int getExtent() { 79 return this.extent; 80 } 81 82 /** 83 * Add a tile loader finisher listener 84 * 85 * @param listener The listener to add 86 */ 87 public void addTileLoaderFinisher(TileListener listener) { 88 // Add as weak listeners since we don't want to keep unnecessary references. 89 this.listenerList.addWeakListener(listener); 90 } 91 92 /** 93 * A class that can be notified that a tile has finished loading 94 * 95 * @author Taylor Smock 96 */ 97 public interface TileListener { 98 /** 99 * Called when the MVTTile is finished loading 100 * 101 * @param tile The tile that finished loading 102 */ 103 void finishedLoading(MVTTile tile); 104 } 105 106 /** 107 * A class used to set the layers that an MVTTile will show. 108 * 109 * @author Taylor Smock 110 */ 111 public interface LayerShower { 112 /** 113 * Get a list of layers to show 114 * 115 * @return A list of layer names 116 */ 117 List<String> layersToShow(); 118 } 119 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java new file mode 100644 index 000000000..bf1b368d9
- + 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
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java new file mode 100644 index 000000000..748172f5f
- + 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
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java new file mode 100644 index 000000000..413c7b32b
- + 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.List; 8 import java.util.Objects; 9 import java.util.stream.Collectors; 10 11 import javax.json.Json; 12 import javax.json.JsonException; 13 import javax.json.JsonReader; 14 15 import org.openstreetmap.josm.data.imagery.ImageryInfo; 16 import org.openstreetmap.josm.data.imagery.JosmTemplatedTMSTileSource; 17 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapBoxVectorStyle; 18 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.Source; 19 import org.openstreetmap.josm.gui.ExtendedDialog; 20 import org.openstreetmap.josm.gui.MainApplication; 21 import org.openstreetmap.josm.gui.util.GuiHelper; 22 import org.openstreetmap.josm.gui.widgets.JosmComboBox; 23 import org.openstreetmap.josm.io.CachedFile; 24 import org.openstreetmap.josm.tools.Logging; 25 26 /** 27 * Tile Source handling for Mapbox Vector Tile sources 28 * @author Taylor Smock 29 * @since xxx 30 */ 31 public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource { 32 private final MapBoxVectorStyle styleSource; 33 34 /** 35 * Create a new {@link MapboxVectorTileSource} from an {@link ImageryInfo} 36 * @param info The info to create the source from 37 */ 38 public MapboxVectorTileSource(ImageryInfo info) { 39 super(info); 40 MapBoxVectorStyle mapBoxVectorStyle = null; 41 try (CachedFile style = new CachedFile(info.getUrl()); 42 InputStream inputStream = style.getInputStream(); 43 JsonReader reader = Json.createReader(inputStream)) { 44 reader.readObject(); 45 // OK, we have a stylesheet 46 mapBoxVectorStyle = MapBoxVectorStyle.getMapBoxVectorStyle(info.getUrl()); 47 } catch (IOException | JsonException e) { 48 Logging.trace(e); 49 } 50 this.styleSource = mapBoxVectorStyle; 51 if (this.styleSource != null) { 52 final Source source; 53 List<Source> sources = this.styleSource.getSources().keySet().stream().filter(Objects::nonNull) 54 .collect(Collectors.toList()); 55 if (sources.size() == 1) { 56 source = sources.get(0); 57 } else if (!sources.isEmpty()) { 58 // Ask user what source they want. 59 source = GuiHelper.runInEDTAndWaitAndReturn(() -> { 60 ExtendedDialog dialog = new ExtendedDialog(MainApplication.getMainFrame(), 61 tr("Select Vector Tile Layers"), tr("Add layers")); 62 JosmComboBox<Source> comboBox = new JosmComboBox<>(sources.toArray(new Source[0])); 63 comboBox.setSelectedIndex(0); 64 dialog.setContent(comboBox); 65 dialog.showDialog(); 66 return (Source) comboBox.getSelectedItem(); 67 }); 68 } else { 69 // Umm. What happened? We probably have an invalid style source. 70 throw new InvalidMapboxVectorTileException(tr("Cannot understand style source: {0}", info.getUrl())); 71 } 72 if (source != null) { 73 this.name = name + ": " + source.getName(); 74 // There can technically be multiple URL's for this field; unfortunately, JOSM can only handle one right now. 75 this.baseUrl = source.getUrls().get(0); 76 this.minZoom = source.getMinZoom(); 77 this.maxZoom = source.getMaxZoom(); 78 if (source.getAttributionText() != null) { 79 this.setAttributionText(source.getAttributionText()); 80 } 81 } 82 } 83 } 84 85 /** 86 * Get the style source for this Vector Tile source 87 * @return The source to use for styling 88 */ 89 public MapBoxVectorStyle getStyleSource() { 90 return this.styleSource; 91 } 92 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Expression.java new file mode 100644 index 000000000..a7f677755
- + 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.Objects; 6 import java.util.stream.Collectors; 7 8 import javax.json.JsonArray; 9 import javax.json.JsonObject; 10 import javax.json.JsonString; 11 import javax.json.JsonValue; 12 13 /** 14 * A MapBox vector style expression (immutable) 15 * @author Taylor Smock 16 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/">https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/</a> 17 * @since xxx 18 */ 19 public final class Expression { 20 /** An empty expression to use */ 21 public static final Expression EMPTY_EXPRESSION = new Expression(JsonValue.NULL); 22 private static final String EMPTY_STRING = ""; 23 24 private final String mapcssFilterExpression; 25 26 /** 27 * Create a new filter expression. <i>Please note that this currently only supports basic comparators!</i> 28 * @param value The value to parse 29 */ 30 public Expression(JsonValue value) { 31 if (value.getValueType() == JsonValue.ValueType.ARRAY) { 32 final JsonArray array = value.asJsonArray(); 33 if (!array.isEmpty() && array.get(0).getValueType() == JsonValue.ValueType.STRING) { 34 if ("==".equals(array.getString(0))) { 35 // The mapcss equivalent of == is = (for the most part) 36 this.mapcssFilterExpression = convertToString(array.get(1)) + "=" + convertToString(array.get(2)); 37 } else if (Arrays.asList("<=", ">=", ">", "<", "!=").contains(array.getString(0))) { 38 this.mapcssFilterExpression = convertToString(array.get(1)) + array.getString(0) + convertToString(array.get(2)); 39 } else { 40 this.mapcssFilterExpression = EMPTY_STRING; 41 } 42 } else { 43 this.mapcssFilterExpression = EMPTY_STRING; 44 } 45 } else { 46 this.mapcssFilterExpression = EMPTY_STRING; 47 } 48 } 49 50 /** 51 * Convert a value to a string 52 * @param value The value to convert 53 * @return A string 54 */ 55 private static String convertToString(JsonValue value) { 56 switch (value.getValueType()) { 57 case STRING: 58 return ((JsonString) value).getString(); 59 case FALSE: 60 return Boolean.FALSE.toString(); 61 case TRUE: 62 return Boolean.TRUE.toString(); 63 case NUMBER: 64 return value.toString(); 65 case ARRAY: 66 return '[' 67 + ((JsonArray) value).stream().map(Expression::convertToString).collect(Collectors.joining(",")) 68 + ']'; 69 case OBJECT: 70 return '{' 71 + ((JsonObject) value).entrySet().stream() 72 .map(entry -> entry.getKey() + ":" + convertToString(entry.getValue())).collect( 73 Collectors.joining(",")) 74 + '}'; 75 case NULL: 76 default: 77 return EMPTY_STRING; 78 } 79 } 80 81 @Override 82 public String toString() { 83 return !EMPTY_STRING.equals(this.mapcssFilterExpression) ? '[' + this.mapcssFilterExpression + ']' : EMPTY_STRING; 84 } 85 86 @Override 87 public boolean equals(Object other) { 88 if (other instanceof Expression) { 89 Expression o = (Expression) other; 90 return Objects.equals(this.mapcssFilterExpression, o.mapcssFilterExpression); 91 } 92 return false; 93 } 94 95 @Override 96 public int hashCode() { 97 return Objects.hash(this.mapcssFilterExpression); 98 } 99 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Layers.java new file mode 100644 index 000000000..9488c3d19
- + 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.Objects; 12 import java.util.regex.Matcher; 13 import java.util.regex.Pattern; 14 import java.util.stream.Collectors; 15 import java.util.stream.Stream; 16 17 import javax.json.JsonArray; 18 import javax.json.JsonNumber; 19 import javax.json.JsonObject; 20 import javax.json.JsonString; 21 import javax.json.JsonValue; 22 23 /** 24 * MapBox style layers 25 * @author Taylor Smock 26 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/">https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/</a> 27 * @since xxx 28 */ 29 public class Layers { 30 /** 31 * The layer type. This affects the rendering. 32 * @author Taylor Smock 33 * @since xxx 34 */ 35 enum Type { 36 /** Filled polygon with an (optional) border */ 37 FILL, 38 /** A line */ 39 LINE, 40 /** A symbol */ 41 SYMBOL, 42 /** A circle */ 43 CIRCLE, 44 /** A heatmap */ 45 HEATMAP, 46 /** A 3D polygon extrusion */ 47 FILL_EXTRUSION, 48 /** Raster */ 49 RASTER, 50 /** Hillshade data */ 51 HILLSHADE, 52 /** A background color or pattern */ 53 BACKGROUND, 54 /** The fallback layer */ 55 SKY 56 } 57 58 private static final String EMPTY_STRING = ""; 59 private static final char SEMI_COLON = ';'; 60 private static final Pattern CURLY_BRACES = Pattern.compile("(\\{(.*?)})"); 61 62 /** A required unique layer name */ 63 private final String id; 64 /** The required type */ 65 private final Type type; 66 /** An optional expression */ 67 private final Expression filter; 68 /** The max zoom for the layer */ 69 private final int maxZoom; 70 /** The min zoom for the layer */ 71 private final int minZoom; 72 73 /** Default paint properties for this layer */ 74 private final String paint; 75 76 /** A source description to be used with this layer. Required for everything <i>but</i> {@link Type#BACKGROUND} */ 77 private final String source; 78 /** Layer to use from the vector tile source. Only allowed with {@link SourceType#VECTOR}. */ 79 private final String sourceLayer; 80 /** The id for the style -- used for image paths */ 81 private final String styleId; 82 /** 83 * Create a layer object 84 * @param layerInfo The info to use to create the layer 85 */ 86 public Layers(final JsonObject layerInfo) { 87 this (null, layerInfo); 88 } 89 90 /** 91 * Create a layer object 92 * @param styleId The id for the style (image paths require this) 93 * @param layerInfo The info to use to create the layer 94 */ 95 public Layers(final String styleId, final JsonObject layerInfo) { 96 this.id = layerInfo.getString("id"); 97 this.styleId = styleId; 98 this.type = Type.valueOf(layerInfo.getString("type").replace("-", "_").toUpperCase(Locale.ROOT)); 99 if (layerInfo.containsKey("filter")) { 100 this.filter = new Expression(layerInfo.get("filter")); 101 } else { 102 this.filter = Expression.EMPTY_EXPRESSION; 103 } 104 this.maxZoom = layerInfo.getInt("maxzoom", Integer.MAX_VALUE); 105 this.minZoom = layerInfo.getInt("minzoom", Integer.MIN_VALUE); 106 // There is a metadata field (I don't *think* I need it?) 107 // source is only optional with {@link Type#BACKGROUND}. 108 if (this.type == Type.BACKGROUND) { 109 this.source = layerInfo.getString("source", null); 110 } else { 111 this.source = layerInfo.getString("source"); 112 } 113 if (layerInfo.containsKey("paint") && layerInfo.get("paint").getValueType() == JsonValue.ValueType.OBJECT) { 114 final JsonObject paintObject = layerInfo.getJsonObject("paint"); 115 final JsonObject layoutObject = layerInfo.getOrDefault("layout", JsonValue.EMPTY_JSON_OBJECT).asJsonObject(); 116 // Don't throw exceptions here, since we may just point at the styling 117 if ("visible".equalsIgnoreCase(layoutObject.getString("visibility", "visible"))) { 118 switch (type) { 119 case FILL: 120 // area 121 this.paint = parsePaintFill(paintObject); 122 break; 123 case LINE: 124 // way 125 this.paint = parsePaintLine(layoutObject, paintObject); 126 break; 127 case CIRCLE: 128 // point 129 this.paint = parsePaintCircle(paintObject); 130 break; 131 case SYMBOL: 132 // point 133 this.paint = parsePaintSymbol(layoutObject, paintObject); 134 break; 135 case BACKGROUND: 136 // canvas only 137 this.paint = parsePaintBackground(paintObject); 138 break; 139 default: 140 this.paint = EMPTY_STRING; 141 } 142 } else { 143 this.paint = EMPTY_STRING; 144 } 145 } else { 146 this.paint = EMPTY_STRING; 147 } 148 this.sourceLayer = layerInfo.getString("source-layer", null); 149 } 150 151 /** 152 * Get the filter for this layer 153 * @return The filter 154 */ 155 public Expression getFilter() { 156 return this.filter; 157 } 158 159 /** 160 * Get the unique id for this layer 161 * @return The unique id 162 */ 163 public String getId() { 164 return this.id; 165 } 166 167 /** 168 * Get the type of this layer 169 * @return The layer type 170 */ 171 public Type getType() { 172 return this.type; 173 } 174 175 private static String parsePaintLine(final JsonObject layoutObject, final JsonObject paintObject) { 176 final StringBuilder sb = new StringBuilder(36); 177 // line-blur, default 0 (px) 178 // line-color, default #000000, disabled by line-pattern 179 final String color = paintObject.getString("line-color", "#000000"); 180 sb.append("color:").append(color).append(SEMI_COLON); 181 // line-opacity, default 1 (0-1) 182 final JsonNumber opacity = paintObject.getJsonNumber("line-opacity"); 183 if (opacity != null) { 184 sb.append("opacity:").append(opacity.numberValue().doubleValue()).append(SEMI_COLON); 185 } 186 // line-cap, default butt (butt|round|square) 187 final String cap = layoutObject.getString("line-cap", "butt"); 188 sb.append("linecap:"); 189 switch (cap) { 190 case "round": 191 case "square": 192 sb.append(cap); 193 break; 194 case "butt": 195 default: 196 sb.append("none"); 197 } 198 199 sb.append(SEMI_COLON); 200 // line-dasharray, array of number >= 0, units in line widths, disabled by line-pattern 201 if (paintObject.containsKey("line-dasharray")) { 202 final JsonArray dashArray = paintObject.getJsonArray("line-dasharray"); 203 sb.append("dashes:"); 204 sb.append(dashArray.stream().filter(JsonNumber.class::isInstance).map(JsonNumber.class::cast) 205 .map(JsonNumber::toString).collect(Collectors.joining(","))); 206 sb.append(SEMI_COLON); 207 } 208 // line-gap-width 209 // line-gradient 210 // line-join 211 // line-miter-limit 212 // line-offset 213 // line-pattern TODO this first, since it disables stuff 214 // line-round-limit 215 // line-sort-key 216 // line-translate 217 // line-translate-anchor 218 // line-width 219 final JsonNumber width = paintObject.getJsonNumber("line-width"); 220 sb.append("width:").append(width == null ? 1 : width.toString()).append(SEMI_COLON); 221 return sb.toString(); 222 } 223 224 private static String parsePaintCircle(final JsonObject paintObject) { 225 final StringBuilder sb = new StringBuilder(150).append("symbol-shape:circle;") 226 // circle-blur 227 // circle-color 228 .append("symbol-fill-color:").append(paintObject.getString("circle-color", "#000000")).append(SEMI_COLON); 229 // circle-opacity 230 final JsonNumber fillOpacity = paintObject.getJsonNumber("circle-opacity"); 231 sb.append("symbol-fill-opacity:").append(fillOpacity != null ? fillOpacity.numberValue().toString() : "1").append(SEMI_COLON); 232 // circle-pitch-alignment // not 3D 233 // circle-pitch-scale // not 3D 234 // circle-radius 235 final JsonNumber radius = paintObject.getJsonNumber("circle-radius"); 236 sb.append("symbol-size:").append(radius != null ? (2 * radius.numberValue().doubleValue()) : "10").append(SEMI_COLON) 237 // circle-sort-key 238 // circle-stroke-color 239 .append("symbol-stroke-color:").append(paintObject.getString("circle-stroke-color", "#000000")).append(SEMI_COLON); 240 // circle-stroke-opacity 241 final JsonNumber strokeOpacity = paintObject.getJsonNumber("circle-stroke-opacity"); 242 sb.append("symbol-stroke-opacity:").append(strokeOpacity != null ? strokeOpacity.numberValue().toString() : "1").append(SEMI_COLON); 243 // circle-stroke-width 244 final JsonNumber strokeWidth = paintObject.getJsonNumber("circle-stroke-width"); 245 sb.append("symbol-stroke-width:").append(strokeWidth != null ? strokeWidth.numberValue().toString() : "0").append(SEMI_COLON); 246 // circle-translate 247 // circle-translate-anchor 248 return sb.toString(); 249 } 250 251 private String parsePaintSymbol( 252 final JsonObject layoutObject, 253 final JsonObject paintObject) { 254 final StringBuilder sb = new StringBuilder(); 255 // icon-allow-overlap 256 // icon-anchor 257 // icon-color 258 // icon-halo-blur 259 // icon-halo-color 260 // icon-halo-width 261 // icon-ignore-placement 262 // icon-image 263 boolean iconImage = false; 264 if (layoutObject.containsKey("icon-image")) { 265 sb.append("icon-image:concat("); 266 if (this.styleId != null && !this.styleId.trim().isEmpty()) { 267 sb.append('"').append(this.styleId).append('/').append("\","); 268 } 269 Matcher matcher = CURLY_BRACES.matcher(layoutObject.getString("icon-image")); 270 StringBuffer stringBuffer = new StringBuffer(); 271 int previousMatch; 272 if (matcher.lookingAt()) { 273 matcher.appendReplacement(stringBuffer, "tag(\"$2\"),\""); 274 previousMatch = matcher.end(); 275 } else { 276 previousMatch = 0; 277 stringBuffer.append('"'); 278 } 279 while (matcher.find()) { 280 if (matcher.start() == previousMatch) { 281 matcher.appendReplacement(stringBuffer, ",tag(\"$2\")"); 282 } else { 283 matcher.appendReplacement(stringBuffer, "\",tag(\"$2\"),\""); 284 } 285 previousMatch = matcher.end(); 286 } 287 if (matcher.hitEnd() && stringBuffer.toString().endsWith(",\"")) { 288 stringBuffer.delete(stringBuffer.length() - ",\"".length(), stringBuffer.length()); 289 } else if (!matcher.hitEnd()) { 290 stringBuffer.append('"'); 291 } 292 StringBuffer tail = new StringBuffer(); 293 matcher.appendTail(tail); 294 if (tail.length() > 0) { 295 String current = stringBuffer.toString(); 296 if (!"\"".equals(current) && !current.endsWith(",\"")) { 297 stringBuffer.append(",\""); 298 } 299 stringBuffer.append(tail); 300 stringBuffer.append('"'); 301 } 302 303 sb.append(stringBuffer).append(')').append(SEMI_COLON); 304 iconImage = true; 305 } 306 // icon-keep-upright 307 // icon-offset 308 if (iconImage && layoutObject.containsKey("icon-offset")) { 309 // default [0, 0], right,down == positive, left,up == negative 310 final List<JsonNumber> offset = layoutObject.getJsonArray("icon-offset").getValuesAs(JsonNumber.class); 311 // Assume that the offset must be size 2. Probably not necessary, but docs aren't necessary clear. 312 if (offset.size() == 2) { 313 sb.append("icon-offset-x:").append(offset.get(0).doubleValue()).append(SEMI_COLON) 314 .append("icon-offset-y:").append(offset.get(1).doubleValue()).append(SEMI_COLON); 315 } 316 } 317 // icon-opacity 318 if (iconImage && paintObject.containsKey("icon-opacity")) { 319 final double opacity = paintObject.getJsonNumber("icon-opacity").doubleValue(); 320 sb.append("icon-opacity:").append(opacity).append(SEMI_COLON); 321 } 322 // icon-optional 323 // icon-padding 324 // icon-pitch-alignment 325 // icon-rotate 326 if (iconImage && layoutObject.containsKey("icon-rotate")) { 327 final double rotation = layoutObject.getJsonNumber("icon-rotate").doubleValue(); 328 sb.append("icon-rotation:").append(rotation).append(SEMI_COLON); 329 } 330 // icon-rotation-alignment 331 // icon-size 332 // icon-text-fit 333 // icon-text-fit-padding 334 // icon-translate 335 // icon-translate-anchor 336 // symbol-avoid-edges 337 // symbol-placement 338 // symbol-sort-key 339 // symbol-spacing 340 // symbol-z-order 341 // text-allow-overlap 342 // text-anchor 343 // text-color 344 if (paintObject.containsKey("text-color")) { 345 sb.append("text-color:").append(paintObject.getString("text-color")).append(SEMI_COLON); 346 } 347 // text-field 348 if (layoutObject.containsKey("text-field")) { 349 sb.append("text:") 350 .append(layoutObject.getString("text-field").replace("}", EMPTY_STRING).replace("{", EMPTY_STRING)) 351 .append(SEMI_COLON); 352 } 353 // text-font 354 if (layoutObject.containsKey("text-font")) { 355 List<String> fonts = layoutObject.getJsonArray("text-font").stream().filter(JsonString.class::isInstance) 356 .map(JsonString.class::cast).map(JsonString::getString).collect(Collectors.toList()); 357 Font[] systemFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(); 358 for (String fontString : fonts) { 359 Collection<Font> fontMatches = Stream.of(systemFonts) 360 .filter(font -> Arrays.asList(font.getName(), font.getFontName(), font.getFamily(), font.getPSName()).contains(fontString)) 361 .collect(Collectors.toList()); 362 if (!fontMatches.isEmpty()) { 363 final Font setFont = fontMatches.stream().filter(font -> font.getName().equals(fontString)).findAny() 364 .orElseGet(() -> fontMatches.stream().filter(font -> font.getFontName().equals(fontString)).findAny() 365 .orElseGet(() -> fontMatches.stream().filter(font -> font.getPSName().equals(fontString)).findAny() 366 .orElseGet(() -> fontMatches.stream().filter(font -> font.getFamily().equals(fontString)).findAny().orElse(null)))); 367 if (setFont != null) { 368 sb.append("font-family:\"").append(setFont.getFamily()).append('"').append(SEMI_COLON); 369 sb.append("font-weight:").append(setFont.isBold() ? "bold" : "normal").append(SEMI_COLON); 370 sb.append("font-style:").append(setFont.isItalic() ? "italic" : "normal").append(SEMI_COLON); 371 break; 372 } 373 } 374 } 375 } 376 // text-halo-blur 377 // text-halo-color 378 if (paintObject.containsKey("text-halo-color")) { 379 sb.append("text-halo-color:").append(paintObject.getString("text-halo-color")).append(SEMI_COLON); 380 } 381 // text-halo-width 382 if (paintObject.containsKey("text-halo-width")) { 383 sb.append("text-halo-radius:").append(paintObject.getJsonNumber("text-halo-width").intValue()).append(SEMI_COLON); 384 } 385 // text-ignore-placement 386 // text-justify 387 // text-keep-upright 388 // text-letter-spacing 389 // text-line-height 390 // text-max-angle 391 // text-max-width 392 // text-offset 393 // text-opacity 394 if (paintObject.containsKey("text-opacity")) { 395 sb.append("text-opacity:").append(paintObject.getJsonNumber("text-opacity").doubleValue()).append(SEMI_COLON); 396 } 397 // text-optional 398 // text-padding 399 // text-pitch-alignment 400 // text-radial-offset 401 // text-rotate 402 // text-rotation-alignment 403 // text-size 404 final JsonNumber textSize = layoutObject.getJsonNumber("text-size"); 405 sb.append("font-size:").append(textSize != null ? textSize.numberValue().toString() : "16").append(SEMI_COLON); 406 // text-transform 407 // text-translate 408 // text-translate-anchor 409 // text-variable-anchor 410 // text-writing-mode 411 return sb.toString(); 412 } 413 414 private static String parsePaintBackground(final JsonObject paintObject) { 415 final StringBuilder sb = new StringBuilder(20); 416 // background-color 417 final String bgColor = paintObject.getString("background-color", null); 418 if (bgColor != null) { 419 sb.append("fill-color:").append(bgColor).append(SEMI_COLON); 420 } 421 // background-opacity 422 // background-pattern 423 return sb.toString(); 424 } 425 426 private static String parsePaintFill(final JsonObject paintObject) { 427 StringBuilder sb = new StringBuilder(50) 428 // fill-antialias 429 // fill-color 430 .append("fill-color:").append(paintObject.getString("fill-color", "#000000")).append(SEMI_COLON); 431 // fill-opacity 432 final JsonNumber opacity = paintObject.getJsonNumber("fill-opacity"); 433 sb.append("fill-opacity:").append(opacity != null ? opacity.numberValue().toString() : "1").append(SEMI_COLON) 434 // fill-outline-color 435 .append("color:").append(paintObject.getString("fill-outline-color", 436 paintObject.getString("fill-color", "#000000"))).append(SEMI_COLON); 437 // fill-pattern 438 // fill-sort-key 439 // fill-translate 440 // fill-translate-anchor 441 return sb.toString(); 442 } 443 444 /** 445 * Converts this layer object to a mapcss entry string (to be parsed later) 446 * @return The mapcss entry (string form) 447 */ 448 @Override 449 public String toString() { 450 if (this.filter.toString().isEmpty() && this.paint.isEmpty()) { 451 return EMPTY_STRING; 452 } else if (this.type == Type.BACKGROUND) { 453 // AFAIK, paint has no zoom levels, and doesn't accept a layer 454 return "canvas{" + this.paint + "}"; 455 } 456 457 final String zoomSelector; 458 if (this.minZoom == this.maxZoom) { 459 zoomSelector = "|z" + this.minZoom; 460 } else if (this.minZoom > Integer.MIN_VALUE && this.maxZoom == Integer.MAX_VALUE) { 461 zoomSelector = "|z" + this.minZoom + "-"; 462 } else if (this.minZoom == Integer.MIN_VALUE && this.maxZoom < Integer.MAX_VALUE) { 463 zoomSelector = "|z-" + this.maxZoom; 464 } else if (this.minZoom > Integer.MIN_VALUE) { 465 zoomSelector = MessageFormat.format("|z{0}-{1}", this.minZoom, this.maxZoom); 466 } else { 467 zoomSelector = EMPTY_STRING; 468 } 469 final String commonData = zoomSelector + this.filter.toString() + "::" + this.id + "{" + this.paint + "}"; 470 471 if (this.type == Type.CIRCLE || this.type == Type.SYMBOL) { 472 return "node" + commonData; 473 } else if (this.type == Type.FILL) { 474 return "area" + commonData; 475 } else if (this.type == Type.LINE) { 476 return "way" + commonData; 477 } 478 return super.toString(); 479 } 480 481 /** 482 * Get the source that this applies to 483 * @return The source name 484 */ 485 public String getSource() { 486 return this.source; 487 } 488 489 /** 490 * Get the layer that this applies to 491 * @return The layer name 492 */ 493 public String getSourceLayer() { 494 return this.sourceLayer; 495 } 496 497 @Override 498 public boolean equals(Object other) { 499 if (other != null && this.getClass() == other.getClass()) { 500 Layers o = (Layers) other; 501 return this.type == o.type 502 && this.minZoom == o.minZoom 503 && this.maxZoom == o.maxZoom 504 && Objects.equals(this.id, o.id) 505 && Objects.equals(this.styleId, o.styleId) 506 && Objects.equals(this.sourceLayer, o.sourceLayer) 507 && Objects.equals(this.source, o.source) 508 && Objects.equals(this.filter, o.filter) 509 && Objects.equals(this.paint, o.paint); 510 } 511 return false; 512 } 513 514 @Override 515 public int hashCode() { 516 return Objects.hash(this.type, this.minZoom, this.maxZoom, this.id, this.styleId, this.sourceLayer, this.source, 517 this.filter, this.paint); 518 } 519 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyle.java new file mode 100644 index 000000000..746913042
- + 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; 7 import java.awt.image.BufferedImage; 8 import java.io.BufferedReader; 9 import java.io.File; 10 import java.io.IOException; 11 import java.io.InputStream; 12 import java.io.OutputStream; 13 import java.nio.charset.StandardCharsets; 14 import java.nio.file.Files; 15 import java.util.Collections; 16 import java.util.LinkedHashMap; 17 import java.util.List; 18 import java.util.Map; 19 import java.util.Objects; 20 import java.util.Optional; 21 import java.util.concurrent.ConcurrentHashMap; 22 import java.util.stream.Collectors; 23 24 import javax.imageio.ImageIO; 25 import javax.json.Json; 26 import javax.json.JsonArray; 27 import javax.json.JsonObject; 28 import javax.json.JsonReader; 29 import javax.json.JsonStructure; 30 import javax.json.JsonValue; 31 32 import org.openstreetmap.josm.data.preferences.JosmBaseDirectories; 33 import org.openstreetmap.josm.gui.MainApplication; 34 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 35 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 36 import org.openstreetmap.josm.io.CachedFile; 37 import org.openstreetmap.josm.spi.preferences.Config; 38 import org.openstreetmap.josm.tools.Logging; 39 40 /** 41 * Create a mapping for a Mapbox Vector Style 42 * 43 * @author Taylor Smock 44 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/">https://docs.mapbox.com/mapbox-gl-js/style-spec/</a> 45 * @since xxx 46 */ 47 public class MapBoxVectorStyle { 48 49 private static final ConcurrentHashMap<String, MapBoxVectorStyle> STYLE_MAPPING = new ConcurrentHashMap<>(); 50 51 /** 52 * Get a MapBoxVector style for a URL 53 * @param url The url to get 54 * @return The MapBox Vector Style. May be {@code null} if there was an error. 55 */ 56 public static MapBoxVectorStyle getMapBoxVectorStyle(String url) { 57 return STYLE_MAPPING.computeIfAbsent(url, key -> { 58 try (CachedFile style = new CachedFile(url); 59 BufferedReader reader = style.getContentReader(); 60 JsonReader jsonReader = Json.createReader(reader)) { 61 JsonStructure structure = jsonReader.read(); 62 return new MapBoxVectorStyle(structure.asJsonObject()); 63 } catch (IOException e) { 64 Logging.error(e); 65 } 66 // Documentation indicates that this will <i>not</i> be entered into the map, which means that this will be 67 // retried if something goes wrong. 68 return null; 69 }); 70 } 71 72 /** The version for the style specification */ 73 private final int version; 74 /** The optional name for the vector style */ 75 private final String name; 76 /** The optional URL for sprites. This mush be absolute (so it must contain the scheme, authority, and path). */ 77 private final String spriteUrl; 78 /** The optional URL for glyphs. This may have replaceable values in it. */ 79 private final String glyphUrl; 80 /** The required collection of sources with a list of layers that are applicable for that source*/ 81 private final Map<Source, ElemStyles> sources; 82 83 /** 84 * Create a new MapBoxVector style. You should prefer {@link #getMapBoxVectorStyle(String)} 85 * for deduplication purposes. 86 * 87 * @param jsonObject The object to create the style from 88 * @see #getMapBoxVectorStyle(String) 89 */ 90 public MapBoxVectorStyle(JsonObject jsonObject) { 91 // There should be a version specifier. We currently only support version 8. 92 // This can throw an NPE when there is no version number. 93 this.version = jsonObject.getInt("version"); 94 if (this.version == 8) { 95 this.name = jsonObject.getString("name", null); 96 String id = jsonObject.getString("id", this.name); 97 this.spriteUrl = jsonObject.getString("sprite", null); 98 this.glyphUrl = jsonObject.getString("glyphs", null); 99 final List<Source> sourceList; 100 if (jsonObject.containsKey("sources") && jsonObject.get("sources").getValueType() == JsonValue.ValueType.OBJECT) { 101 final JsonObject sourceObj = jsonObject.getJsonObject("sources"); 102 sourceList = sourceObj.entrySet().stream().filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT) 103 .map(entry -> new Source(entry.getKey(), entry.getValue().asJsonObject())).collect(Collectors.toList()); 104 } else { 105 sourceList = Collections.emptyList(); 106 } 107 final List<Layers> layers; 108 if (jsonObject.containsKey("layers") && jsonObject.get("layers").getValueType() == JsonValue.ValueType.ARRAY) { 109 JsonArray lArray = jsonObject.getJsonArray("layers"); 110 layers = lArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast).map(obj -> new Layers(id, obj)) 111 .collect(Collectors.toList()); 112 } else { 113 layers = Collections.emptyList(); 114 } 115 final Map<Optional<Source>, List<Layers>> sourceLayer = layers.stream().collect( 116 Collectors.groupingBy(layer -> sourceList.stream().filter(source -> source.getName().equals(layer.getSource())) 117 .findFirst(), LinkedHashMap::new, Collectors.toList())); 118 // Abuse HashMap null (null == default) 119 this.sources = new LinkedHashMap<>(); 120 for (Map.Entry<Optional<Source>, List<Layers>> entry : sourceLayer.entrySet()) { 121 final Source source = entry.getKey().orElse(null); 122 final String data = entry.getValue().stream().map(Layers::toString).collect(Collectors.joining()); 123 final String metaData = "meta{title:" + (source == null ? "Generated Style" : 124 source.getName()) + ";version:\"autogenerated\";description:\"auto generated style\";}"; 125 126 // This is the default canvas 127 final String canvas = "canvas{default-points:false;default-lines:false;}"; 128 final MapCSSStyleSource style = new MapCSSStyleSource(metaData + canvas + data); 129 // Save to directory 130 MainApplication.worker.execute(() -> this.save((source == null ? data.hashCode() : source.getName()) + ".mapcss", style)); 131 this.sources.put(source, new ElemStyles(Collections.singleton(style))); 132 } 133 if (this.spriteUrl != null && !this.spriteUrl.trim().isEmpty()) { 134 MainApplication.worker.execute(this::fetchSprites); 135 } 136 } else { 137 throw new IllegalArgumentException(tr("Vector Tile Style Version not understood: version {0} (json: {1})", 138 this.version, jsonObject)); 139 } 140 } 141 142 /** 143 * Fetch sprites. Please note that this is (literally) only png. Unfortunately. 144 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/</a> 145 */ 146 private void fetchSprites() { 147 // HiDPI images first -- if this succeeds, don't bother with the lower resolution (JOSM has no method to switch) 148 try (CachedFile spriteJson = new CachedFile(this.spriteUrl + "@2x.json"); 149 CachedFile spritePng = new CachedFile(this.spriteUrl + "@2x.png")) { 150 if (parseSprites(spriteJson, spritePng)) { 151 return; 152 } 153 } 154 try (CachedFile spriteJson = new CachedFile(this.spriteUrl + ".json"); 155 CachedFile spritePng = new CachedFile(this.spriteUrl + ".png")) { 156 parseSprites(spriteJson, spritePng); 157 } 158 } 159 160 private boolean parseSprites(CachedFile spriteJson, CachedFile spritePng) { 161 /* JSON looks like this: 162 * { "image-name": {"width": width, "height": height, "x": x, "y": y, "pixelRatio": 1 }} 163 * width/height are the dimensions of the image 164 * x -- distance right from top left 165 * y -- distance down from top left 166 * pixelRatio -- this <i>appears</i> to be from the "@2x" (default 1) 167 * content -- [left, top corner, right, bottom corner] 168 * stretchX -- [[from, to], [from, to], ...] 169 * stretchY -- [[from, to], [from, to], ...] 170 */ 171 final JsonObject spriteObject; 172 final BufferedImage spritePngImage; 173 try (BufferedReader spriteJsonBufferedReader = spriteJson.getContentReader(); 174 JsonReader spriteJsonReader = Json.createReader(spriteJsonBufferedReader); 175 InputStream spritePngBufferedReader = spritePng.getInputStream() 176 ) { 177 spriteObject = spriteJsonReader.read().asJsonObject(); 178 spritePngImage = ImageIO.read(spritePngBufferedReader); 179 } catch (IOException e) { 180 Logging.error(e); 181 return false; 182 } 183 for (Map.Entry<String, JsonValue> entry : spriteObject.entrySet()) { 184 final JsonObject info = entry.getValue().asJsonObject(); 185 int width = info.getInt("width"); 186 int height = info.getInt("height"); 187 int x = info.getInt("x"); 188 int y = info.getInt("y"); 189 save(entry.getKey() + ".png", spritePngImage.getSubimage(x, y, width, height)); 190 } 191 return true; 192 } 193 194 private void save(String name, Object object) { 195 final File cache; 196 if (object instanceof Image) { 197 // Images have a specific location where they are looked for 198 cache = new File(Config.getDirs().getUserDataDirectory(true), "images"); 199 } else { 200 cache = JosmBaseDirectories.getInstance().getCacheDirectory(true); 201 } 202 final File location = new File(cache, this.name != null ? this.name : Integer.toString(this.hashCode())); 203 if ((!location.exists() && !location.mkdirs()) || (location.exists() && !location.isDirectory())) { 204 // Don't try to save if the file exists and is not a directory or we couldn't create it 205 return; 206 } 207 final File toSave = new File(location, name); 208 try (OutputStream fileOutputStream = Files.newOutputStream(toSave.toPath())) { 209 if (object instanceof String) { 210 fileOutputStream.write(((String) object).getBytes(StandardCharsets.UTF_8)); 211 } else if (object instanceof MapCSSStyleSource) { 212 MapCSSStyleSource source = (MapCSSStyleSource) object; 213 try (InputStream inputStream = source.getSourceInputStream()) { 214 int byteVal = inputStream.read(); 215 do { 216 fileOutputStream.write(byteVal); 217 byteVal = inputStream.read(); 218 } while (byteVal > -1); 219 source.url = "file:/" + toSave.getAbsolutePath().replace('\\', '/'); 220 if (source.isLoaded()) { 221 source.loadStyleSource(); 222 } 223 } 224 } else if (object instanceof BufferedImage) { 225 // This directory is checked first when getting images 226 ImageIO.write((BufferedImage) object, "png", toSave); 227 } 228 } catch (IOException e) { 229 Logging.info(e); 230 } 231 } 232 233 /** 234 * Get the generated layer->style mapping 235 * @return The mapping (use to enable/disable a paint style) 236 */ 237 public Map<Source, ElemStyles> getSources() { 238 return this.sources; 239 } 240 241 /** 242 * Get the sprite url for the style 243 * @return The base sprite url 244 */ 245 public String getSpriteUrl() { 246 return this.spriteUrl; 247 } 248 249 @Override 250 public boolean equals(Object other) { 251 if (other != null && other.getClass() == this.getClass()) { 252 MapBoxVectorStyle o = (MapBoxVectorStyle) other; 253 return this.version == o.version 254 && Objects.equals(this.name, o.name) 255 && Objects.equals(this.glyphUrl, o.glyphUrl) 256 && Objects.equals(this.spriteUrl, o.spriteUrl) 257 && Objects.equals(this.sources, o.sources); 258 } 259 return false; 260 } 261 262 @Override 263 public int hashCode() { 264 return Objects.hash(this.name, this.version, this.glyphUrl, this.spriteUrl, this.sources); 265 } 266 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Scheme.java new file mode 100644 index 000000000..e8583b940
- + 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
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/Source.java new file mode 100644 index 000000000..dc7c62d62
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style; 3 4 import java.text.MessageFormat; 5 import java.util.ArrayList; 6 import java.util.Arrays; 7 import java.util.Collection; 8 import java.util.Collections; 9 import java.util.List; 10 import java.util.Locale; 11 import java.util.Objects; 12 import java.util.function.IntFunction; 13 14 import javax.json.JsonArray; 15 import javax.json.JsonObject; 16 import javax.json.JsonString; 17 import javax.json.JsonValue; 18 19 import org.openstreetmap.josm.data.Bounds; 20 21 /** 22 * A source from a MapBox Vector Style 23 * 24 * @author Taylor Smock 25 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/</a> 26 * @since xxx 27 */ 28 public class Source { 29 /** 30 * A common function for zoom constraints 31 */ 32 private static class ZoomBoundFunction implements IntFunction<Integer> { 33 private final int min; 34 private final int max; 35 /** 36 * Create a new bound for zooms 37 * @param min The min zoom 38 * @param max The max zoom 39 */ 40 ZoomBoundFunction(int min, int max) { 41 this.min = min; 42 this.max = max; 43 } 44 45 @Override public Integer apply(int value) { 46 return Math.max(min, Math.min(value, max)); 47 } 48 } 49 50 /** 51 * WMS servers should contain a "{bbox-epsg-3857}" parameter for the bbox 52 */ 53 private static final String WMS_BBOX = "bbox-epsg-3857"; 54 55 private static final String[] NO_URLS = new String[0]; 56 57 /** 58 * Constrain the min/max zooms to be between 0 and 30, as per tilejson spec 59 */ 60 private static final IntFunction<Integer> ZOOM_BOUND_FUNCTION = new ZoomBoundFunction(0, 30); 61 62 /* Common items */ 63 /** 64 * The name of the source 65 */ 66 private final String name; 67 /** 68 * The type of the source 69 */ 70 private final SourceType sourceType; 71 72 /* Common tiled data */ 73 /** 74 * The minimum zoom supported 75 */ 76 private final int minZoom; 77 /** 78 * The maximum zoom supported 79 */ 80 private final int maxZoom; 81 /** 82 * The tile urls. These usually have replaceable fields. 83 */ 84 private final String[] tileUrls; 85 86 /* Vector and raster data */ 87 /** 88 * The attribution to display for the user 89 */ 90 private final String attribution; 91 /** 92 * The bounds of the data. We should not request data outside of the bounds 93 */ 94 private final Bounds bounds; 95 /** 96 * The property to use as a feature id. Can be parameterized 97 */ 98 private final String promoteId; 99 /** 100 * The tile scheme 101 */ 102 private final Scheme scheme; 103 /** 104 * {@code true} if the tiles should not be cached 105 */ 106 private final boolean volatileCache; 107 108 /* Raster data */ 109 /** 110 * The tile size 111 */ 112 private final int tileSize; 113 114 /** 115 * Create a new Source object 116 * 117 * @param name The name of the source object 118 * @param data The data to set the source information with 119 */ 120 public Source(final String name, final JsonObject data) { 121 Objects.requireNonNull(name, "Name cannot be null"); 122 Objects.requireNonNull(data, "Data cannot be null"); 123 this.name = name; 124 // "type" is required (so throw an NPE if it doesn't exist) 125 final String type = data.getString("type"); 126 this.sourceType = SourceType.valueOf(type.replace("-", "_").toUpperCase(Locale.ROOT)); 127 // This can also contain SourceType.RASTER_DEM (only needs encoding) 128 if (SourceType.VECTOR == this.sourceType || SourceType.RASTER == this.sourceType) { 129 if (data.containsKey("url")) { 130 // TODO implement https://github.com/mapbox/tilejson-spec 131 throw new UnsupportedOperationException(); 132 } else { 133 this.minZoom = ZOOM_BOUND_FUNCTION.apply(data.getInt("minzoom", 0)); 134 this.maxZoom = ZOOM_BOUND_FUNCTION.apply(data.getInt("maxzoom", 22)); 135 this.attribution = data.getString("attribution", null); 136 if (data.containsKey("bounds") && data.get("bounds").getValueType() == JsonValue.ValueType.ARRAY) { 137 final JsonArray bJsonArray = data.getJsonArray("bounds"); 138 if (bJsonArray.size() != 4) { 139 throw new IllegalArgumentException(MessageFormat.format("bounds must have four values, but has {0}", bJsonArray.size())); 140 } 141 final double[] bArray = new double[bJsonArray.size()]; 142 for (int i = 0; i < bJsonArray.size(); i++) { 143 bArray[i] = bJsonArray.getJsonNumber(i).doubleValue(); 144 } 145 // The order in the response is 146 // [south-west longitude, south-west latitude, north-east longitude, north-east latitude] 147 this.bounds = new Bounds(bArray[1], bArray[0], bArray[3], bArray[2]); 148 } else { 149 // Don't use a static instance for bounds, as it is not a immutable class 150 this.bounds = new Bounds(-85.051129, -180, 85.051129, 180); 151 } 152 this.promoteId = data.getString("promoteId", null); 153 this.scheme = Scheme.valueOf(data.getString("scheme", "xyz").toUpperCase(Locale.ROOT)); 154 if (data.containsKey("tiles") && data.get("tiles").getValueType() == JsonValue.ValueType.ARRAY) { 155 this.tileUrls = data.getJsonArray("tiles").stream().filter(JsonString.class::isInstance) 156 .map(JsonString.class::cast).map(JsonString::getString) 157 // Replace bbox-epsg-3857 with bbox (already encased with {}) 158 .map(url -> url.replace(WMS_BBOX, "bbox")).toArray(String[]::new); 159 } else { 160 this.tileUrls = NO_URLS; 161 } 162 this.volatileCache = data.getBoolean("volatile", false); 163 this.tileSize = data.getInt("tileSize", 512); 164 } 165 } else { 166 throw new UnsupportedOperationException(); 167 } 168 } 169 170 /** 171 * Get the bounds for this source 172 * @return The bounds where this source can be used 173 */ 174 public Bounds getBounds() { 175 return this.bounds; 176 } 177 178 /** 179 * Get the source name 180 * @return the name 181 */ 182 public String getName() { 183 return name; 184 } 185 186 /** 187 * Get the URLs that can be used to get vector data 188 * 189 * @return The urls 190 */ 191 public List<String> getUrls() { 192 return Collections.unmodifiableList(Arrays.asList(this.tileUrls)); 193 } 194 195 /** 196 * Get the minimum zoom 197 * 198 * @return The min zoom (default {@code 0}) 199 */ 200 public int getMinZoom() { 201 return this.minZoom; 202 } 203 204 /** 205 * Get the max zoom 206 * 207 * @return The max zoom (default {@code 22}) 208 */ 209 public int getMaxZoom() { 210 return this.maxZoom; 211 } 212 213 /** 214 * Get the attribution for this source 215 * 216 * @return The attribution text. May be {@code null}. 217 */ 218 public String getAttributionText() { 219 return this.attribution; 220 } 221 222 @Override 223 public String toString() { 224 Collection<String> parts = new ArrayList<>(1 + this.getUrls().size()); 225 parts.add(this.getName()); 226 parts.addAll(this.getUrls()); 227 return String.join(" ", parts); 228 } 229 230 @Override 231 public boolean equals(Object other) { 232 if (other != null && this.getClass() == other.getClass()) { 233 Source o = (Source) other; 234 return Objects.equals(this.name, o.name) 235 && this.sourceType == o.sourceType 236 && this.minZoom == o.minZoom 237 && this.maxZoom == o.maxZoom 238 && Objects.equals(this.attribution, o.attribution) 239 && Objects.equals(this.promoteId, o.promoteId) 240 && this.scheme == o.scheme 241 && this.volatileCache == o.volatileCache 242 && this.tileSize == o.tileSize 243 && Objects.equals(this.bounds, o.bounds) 244 && Objects.deepEquals(this.tileUrls, o.tileUrls); 245 } 246 return false; 247 } 248 249 @Override 250 public int hashCode() { 251 return Objects.hash(this.name, this.sourceType, this.minZoom, this.maxZoom, this.attribution, this.promoteId, 252 this.scheme, this.volatileCache, this.tileSize, this.bounds, Arrays.hashCode(this.tileUrls)); 253 } 254 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java
diff --git src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceType.java new file mode 100644 index 000000000..a086289d6
- + 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 } -
src/org/openstreetmap/josm/data/osm/IPrimitive.java
diff --git src/org/openstreetmap/josm/data/osm/IPrimitive.java src/org/openstreetmap/josm/data/osm/IPrimitive.java index cdabcd1b6..34e2ca0be 100644
public interface IPrimitive extends IQuadBucketType, Tagged, PrimitiveId, Stylab 391 391 return getName(); 392 392 } 393 393 394 /** 395 * Get an object to synchronize the style cache on. This <i>should</i> be a field that does not change during paint. 396 * By default, it returns the current object, but should be overriden to avoid some performance issues. 397 * @return A non-{@code null} object to synchronize on when painting 398 */ 399 default Object getStyleCacheSyncObject() { 400 return this; 401 } 402 394 403 /** 395 404 * Replies the display name of a primitive formatted by <code>formatter</code> 396 405 * @param formatter formatter to use -
src/org/openstreetmap/josm/data/osm/IRelationMember.java
diff --git src/org/openstreetmap/josm/data/osm/IRelationMember.java src/org/openstreetmap/josm/data/osm/IRelationMember.java index c2803e38d..69091056d 100644
public interface IRelationMember<P extends IPrimitive> extends PrimitiveId { 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
diff --git src/org/openstreetmap/josm/data/osm/IWaySegment.java src/org/openstreetmap/josm/data/osm/IWaySegment.java new file mode 100644 index 000000000..0735935de
- + 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
diff --git src/org/openstreetmap/josm/data/osm/RelationMember.java src/org/openstreetmap/josm/data/osm/RelationMember.java index fc62c71f3..5add40403 100644
public class RelationMember implements IRelationMember<OsmPrimitive> { 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
diff --git src/org/openstreetmap/josm/data/osm/WaySegment.java src/org/openstreetmap/josm/data/osm/WaySegment.java index 2ca1cc379..302f82842 100644
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 } 16 public WaySegment(Way way, int i) { 17 super(way, i); 35 18 } 36 19 37 20 /** 38 * Returns the first node of the way segment. 39 * @return the first node 40 */ 41 public Node getFirstNode() { 42 return way.getNode(lowerIndex); 43 } 44 45 /** 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 … … public final class WaySegment implements Comparable<WaySegment> { 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()); … … public final class WaySegment implements Comparable<WaySegment> { 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 + ']'; -
src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java
diff --git src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java src/org/openstreetmap/josm/data/osm/visitor/paint/StyledMapRenderer.java index 038374233..03ba0e5b2 100644
import java.util.Objects; 36 36 import java.util.Optional; 37 37 import java.util.concurrent.ForkJoinPool; 38 38 import java.util.concurrent.TimeUnit; 39 import java.util.concurrent.locks.Lock; 39 40 import java.util.function.BiConsumer; 40 41 import java.util.function.Consumer; 41 42 import java.util.function.Supplier; … … public class StyledMapRenderer extends AbstractMapRenderer { 1637 1638 RenderBenchmarkCollector benchmark = benchmarkFactory.get(); 1638 1639 BBox bbox = bounds.toBBox(); 1639 1640 getSettings(renderVirtualNodes); 1640 1641 1641 try { 1642 if (data.getReadLock().tryLock(1, TimeUnit.SECONDS)) { 1642 Lock readLock = data.getReadLock(); 1643 if (readLock.tryLock(1, TimeUnit.SECONDS)) { 1643 1644 try { 1644 1645 paintWithLock(data, renderVirtualNodes, benchmark, bbox); 1645 1646 } finally { 1646 data.getReadLock().unlock();1647 readLock.unlock(); 1647 1648 } 1648 1649 } else { 1649 1650 Logging.warn("Cannot paint layer {0}: It is locked."); -
new file src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
diff --git src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java new file mode 100644 index 000000000..109f8915a
- + 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 * Get the parsed number array 39 * 40 * @return The number array 41 */ 42 public Number[] getArray() { 43 return this.numbers; 44 } 45 46 private byte[] nextVarInt() { 47 List<Byte> byteList = new ArrayList<>(); 48 while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE) 49 == ProtoBufParser.MOST_SIGNIFICANT_BYTE) { 50 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned) 51 byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE)); 52 } 53 // The last byte doesn't drop the most significant bit 54 byteList.add(this.bytes[this.location++]); 55 byte[] byteArray = new byte[byteList.size()]; 56 for (int i = 0; i < byteList.size(); i++) { 57 byteArray[i] = byteList.get(i); 58 } 59 60 return byteArray; 61 } 62 } -
new file src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
diff --git src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java new file mode 100644 index 000000000..18059e339
- + 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 /** 34 * Convert a byte array to a number (little endian) 35 * 36 * @param bytes The bytes to convert 37 * @param byteSize The size of the byte. For var ints, this is 7, for other ints, this is 8. 38 * @return An appropriate {@link Number} class. 39 */ 40 public static Number convertByteArray(byte[] bytes, byte byteSize) { 41 long number = 0; 42 for (int i = 0; i < bytes.length; i++) { 43 // Need to convert to uint64 in order to avoid bit operation from filling in 1's and overflow issues 44 number += Byte.toUnsignedLong(bytes[i]) << (byteSize * i); 45 } 46 return convertLong(number); 47 } 48 49 /** 50 * Convert a long to an appropriate {@link Number} class 51 * 52 * @param number The long to convert 53 * @return A {@link Number} 54 */ 55 public static Number convertLong(long number) { 56 // TODO deal with booleans 57 if (number <= Byte.MAX_VALUE && number >= Byte.MIN_VALUE) { 58 return (byte) number; 59 } else if (number <= Short.MAX_VALUE && number >= Short.MIN_VALUE) { 60 return (short) number; 61 } else if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) { 62 return (int) number; 63 } 64 return number; 65 } 66 67 /** 68 * Decode a zig-zag encoded value 69 * 70 * @param signed The value to decode 71 * @return The decoded value 72 */ 73 public static Number decodeZigZag(Number signed) { 74 final long value = signed.longValue(); 75 return convertLong((value >> 1) ^ -(value & 1)); 76 } 77 78 /** 79 * Encode a number to a zig-zag encode value 80 * 81 * @param signed The number to encode 82 * @return The encoded value 83 */ 84 public static Number encodeZigZag(Number signed) { 85 final long value = signed.longValue(); 86 // This boundary condition could be >= or <= or both. Tests indicate that it doesn't actually matter. 87 // The only difference would be the number type returned, except it is always converted to the most basic type. 88 final int shift = (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE ? Long.BYTES : Integer.BYTES) * 8 - 1; 89 return convertLong((value << 1) ^ (value >> shift)); 90 } 91 92 private final InputStream inputStream; 93 94 /** 95 * Create a new parser 96 * 97 * @param bytes The bytes to parse 98 */ 99 public ProtoBufParser(byte[] bytes) { 100 this(new ByteArrayInputStream(bytes)); 101 } 102 103 /** 104 * Create a new parser 105 * 106 * @param inputStream The InputStream (will be fully read at this time) 107 */ 108 public ProtoBufParser(InputStream inputStream) { 109 if (inputStream.markSupported()) { 110 this.inputStream = inputStream; 111 } else { 112 this.inputStream = new BufferedInputStream(inputStream); 113 } 114 } 115 116 /** 117 * Read all records 118 * 119 * @return A collection of all records 120 * @throws IOException - if an IO error occurs 121 */ 122 public Collection<ProtoBufRecord> allRecords() throws IOException { 123 Collection<ProtoBufRecord> records = new ArrayList<>(); 124 while (this.hasNext()) { 125 records.add(new ProtoBufRecord(this)); 126 } 127 return records; 128 } 129 130 @Override 131 public void close() { 132 try { 133 this.inputStream.close(); 134 } catch (IOException e) { 135 Logging.error(e); 136 } 137 } 138 139 /** 140 * Check if there is more data to read 141 * 142 * @return {@code true} if there is more data to read 143 * @throws IOException - if an IO error occurs 144 */ 145 public boolean hasNext() throws IOException { 146 return this.inputStream.available() > 0; 147 } 148 149 /** 150 * Get the "next" WireType 151 * 152 * @return {@link WireType} expected 153 * @throws IOException - if an IO error occurs 154 */ 155 public WireType next() throws IOException { 156 this.inputStream.mark(16); 157 try { 158 return WireType.values()[this.inputStream.read() << 3]; 159 } finally { 160 this.inputStream.reset(); 161 } 162 } 163 164 /** 165 * Get the next byte 166 * 167 * @return The next byte 168 * @throws IOException - if an IO error occurs 169 */ 170 public int nextByte() throws IOException { 171 return this.inputStream.read(); 172 } 173 174 /** 175 * Get the next 32 bits ({@link WireType#THIRTY_TWO_BIT}) 176 * 177 * @return a byte array of the next 32 bits (4 bytes) 178 * @throws IOException - if an IO error occurs 179 */ 180 public byte[] nextFixed32() throws IOException { 181 // 4 bytes == 32 bits 182 return readNextBytes(4); 183 } 184 185 /** 186 * Get the next 64 bits ({@link WireType#SIXTY_FOUR_BIT}) 187 * 188 * @return a byte array of the next 64 bits (8 bytes) 189 * @throws IOException - if an IO error occurs 190 */ 191 public byte[] nextFixed64() throws IOException { 192 // 8 bytes == 64 bits 193 return readNextBytes(8); 194 } 195 196 /** 197 * Get the next delimited message ({@link WireType#LENGTH_DELIMITED}) 198 * 199 * @return The next length delimited message 200 * @throws IOException - if an IO error occurs 201 */ 202 public byte[] nextLengthDelimited() throws IOException { 203 int length = convertByteArray(this.nextVarInt(), VAR_INT_BYTE_SIZE).intValue(); 204 return readNextBytes(length); 205 } 206 207 /** 208 * Get the next var int ({@code WireType#VARINT}) 209 * 210 * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum}) 211 * @throws IOException - if an IO error occurs 212 */ 213 public byte[] nextVarInt() throws IOException { 214 List<Byte> byteList = new ArrayList<>(); 215 int currentByte = this.nextByte(); 216 while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE && currentByte > 0) { 217 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned) 218 byteList.add((byte) (currentByte ^ MOST_SIGNIFICANT_BYTE)); 219 currentByte = this.nextByte(); 220 } 221 // The last byte doesn't drop the most significant bit 222 byteList.add((byte) currentByte); 223 byte[] byteArray = new byte[byteList.size()]; 224 for (int i = 0; i < byteList.size(); i++) { 225 byteArray[i] = byteList.get(i); 226 } 227 228 return byteArray; 229 } 230 231 /** 232 * Read an arbitrary number of bytes 233 * 234 * @param size The number of bytes to read 235 * @return a byte array of the specified size, filled with bytes read (unsigned) 236 * @throws IOException - if an IO error occurs 237 */ 238 private byte[] readNextBytes(int size) throws IOException { 239 byte[] bytesRead = new byte[size]; 240 for (int i = 0; i < bytesRead.length; i++) { 241 bytesRead[i] = (byte) this.nextByte(); 242 } 243 return bytesRead; 244 } 245 } -
new file src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
diff --git src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java new file mode 100644 index 000000000..1eb5d38a6
- + 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 as a double ({@link WireType#SIXTY_FOUR_BIT}) 52 * 53 * @return the double 54 */ 55 public double asDouble() { 56 long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue(); 57 return Double.longBitsToDouble(doubleNumber); 58 } 59 60 /** 61 * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT}) 62 * 63 * @return a byte array of the 32 bits (4 bytes) 64 */ 65 public byte[] asFixed32() { 66 // TODO verify, or just assume? 67 // 4 bytes == 32 bits 68 return this.bytes; 69 } 70 71 /** 72 * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT}) 73 * 74 * @return a byte array of the 64 bits (8 bytes) 75 */ 76 public byte[] asFixed64() { 77 // TODO verify, or just assume? 78 // 8 bytes == 64 bits 79 return this.bytes; 80 } 81 82 /** 83 * Get as a float ({@link WireType#THIRTY_TWO_BIT}) 84 * 85 * @return the float 86 */ 87 public float asFloat() { 88 int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue(); 89 return Float.intBitsToFloat(floatNumber); 90 } 91 92 /** 93 * Get the signed var int ({@code WireType#VARINT}). 94 * These are specially encoded so that they take up less space. 95 * 96 * @return The signed var int ({@code sint32} or {@code sint64}) 97 */ 98 public Number asSignedVarInt() { 99 final Number signed = this.asUnsignedVarInt(); 100 return ProtoBufParser.decodeZigZag(signed); 101 } 102 103 /** 104 * Get as a string ({@link WireType#LENGTH_DELIMITED}) 105 * 106 * @return The string (encoded as {@link StandardCharsets#UTF_8}) 107 */ 108 public String asString() { 109 return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8)); 110 } 111 112 /** 113 * Get the var int ({@code WireType#VARINT}) 114 * 115 * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum}) 116 */ 117 public Number asUnsignedVarInt() { 118 return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE); 119 } 120 121 @Override 122 public void close() { 123 this.bytes = null; 124 } 125 126 /** 127 * Get the raw bytes for this record 128 * 129 * @return The bytes 130 */ 131 public byte[] getBytes() { 132 return this.bytes; 133 } 134 135 /** 136 * Get the field value 137 * 138 * @return The field value 139 */ 140 public int getField() { 141 return this.field; 142 } 143 144 /** 145 * Get the WireType of the data 146 * 147 * @return The {@link WireType} of the data 148 */ 149 public WireType getType() { 150 return this.type; 151 } 152 } -
new file src/org/openstreetmap/josm/data/protobuf/WireType.java
diff --git src/org/openstreetmap/josm/data/protobuf/WireType.java src/org/openstreetmap/josm/data/protobuf/WireType.java new file mode 100644 index 000000000..41edc8e4f
- + 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
diff --git src/org/openstreetmap/josm/data/vector/DataLayer.java src/org/openstreetmap/josm/data/vector/DataLayer.java new file mode 100644 index 000000000..9052e5b1a
- + 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/DataStore.java
diff --git src/org/openstreetmap/josm/data/vector/DataStore.java src/org/openstreetmap/josm/data/vector/DataStore.java new file mode 100644 index 000000000..9de044f62
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import java.util.Collection; 5 import java.util.Collections; 6 import java.util.HashMap; 7 import java.util.HashSet; 8 import java.util.LinkedList; 9 import java.util.Map; 10 import java.util.Set; 11 import java.util.concurrent.locks.ReentrantReadWriteLock; 12 13 import org.openstreetmap.gui.jmapviewer.Tile; 14 import org.openstreetmap.josm.data.DataSource; 15 import org.openstreetmap.josm.data.osm.DataSet; 16 import org.openstreetmap.josm.data.osm.INode; 17 import org.openstreetmap.josm.data.osm.IPrimitive; 18 import org.openstreetmap.josm.data.osm.IRelation; 19 import org.openstreetmap.josm.data.osm.IWay; 20 import org.openstreetmap.josm.data.osm.PrimitiveId; 21 import org.openstreetmap.josm.data.osm.QuadBucketPrimitiveStore; 22 import org.openstreetmap.josm.data.osm.Storage; 23 24 /** 25 * A class that stores data (essentially a simple {@link DataSet}) 26 * @author Taylor Smock 27 * @since xxx 28 */ 29 class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> { 30 /** 31 * This literally only exists to make {@link QuadBucketPrimitiveStore#removePrimitive} public 32 * 33 * @param <N> The node type 34 * @param <W> The way type 35 * @param <R> The relation type 36 */ 37 static class LocalQuadBucketPrimitiveStore<N extends INode, W extends IWay<N>, R extends IRelation<?>> 38 extends QuadBucketPrimitiveStore<N, W, R> { 39 // Allow us to remove primitives (protected in {@link QuadBucketPrimitiveStore}) 40 @Override 41 public void removePrimitive(IPrimitive primitive) { 42 super.removePrimitive(primitive); 43 } 44 } 45 46 protected final int zoom; 47 protected final LocalQuadBucketPrimitiveStore<N, W, R> store = new LocalQuadBucketPrimitiveStore<>(); 48 protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true); 49 protected final Set<Tile> addedTiles = new HashSet<>(); 50 protected final Map<PrimitiveId, O> primitivesMap = allPrimitives 51 .foreignKey(new Storage.PrimitiveIdHash()); 52 protected final Collection<DataSource> dataSources = new LinkedList<>(); 53 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 54 55 DataStore(int zoom) { 56 this.zoom = zoom; 57 } 58 59 public int getZoom() { 60 return this.zoom; 61 } 62 63 public QuadBucketPrimitiveStore<N, W, R> getStore() { 64 return this.store; 65 } 66 67 public Storage<O> getAllPrimitives() { 68 return this.allPrimitives; 69 } 70 71 public Map<PrimitiveId, O> getPrimitivesMap() { 72 if (this.readWriteLock.isWriteLocked()) { 73 return new HashMap<>(this.primitivesMap); 74 } 75 return this.primitivesMap; 76 } 77 78 public Collection<DataSource> getDataSources() { 79 return Collections.unmodifiableCollection(dataSources); 80 } 81 82 /** 83 * Add a datasource to this data set 84 * @param dataSource The data soure to add 85 */ 86 public void addDataSource(DataSource dataSource) { 87 this.dataSources.add(dataSource); 88 } 89 90 /** 91 * Add a primitive to this dataset 92 * @param primitive The primitive to remove 93 */ 94 @SuppressWarnings("squid:S2445") 95 protected void removePrimitive(O primitive) { 96 if (primitive == null) { 97 return; 98 } 99 // This is deliberate -- attempting to remove the primitive twice causes issues 100 synchronized (primitive) { 101 if (this.allPrimitives.contains(primitive)) { 102 this.store.removePrimitive(primitive); 103 this.allPrimitives.remove(primitive); 104 this.primitivesMap.remove(primitive.getPrimitiveId()); 105 } 106 } 107 } 108 109 /** 110 * Add a primitive to this dataset 111 * @param primitive The primitive to add 112 */ 113 protected void addPrimitive(O primitive) { 114 this.store.addPrimitive(primitive); 115 this.allPrimitives.add(primitive); 116 this.primitivesMap.put(primitive.getPrimitiveId(), primitive); 117 } 118 119 /** 120 * Get the read/write lock for this dataset 121 * @return The read/write lock 122 */ 123 protected ReentrantReadWriteLock getReadWriteLock() { 124 return this.readWriteLock; 125 } 126 } -
new file src/org/openstreetmap/josm/data/vector/VectorDataSet.java
diff --git src/org/openstreetmap/josm/data/vector/VectorDataSet.java src/org/openstreetmap/josm/data/vector/VectorDataSet.java new file mode 100644 index 000000000..dfa9334a3
- + 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.Arrays; 6 import java.util.Collection; 7 import java.util.Collections; 8 import java.util.HashSet; 9 import java.util.List; 10 import java.util.Map; 11 import java.util.Objects; 12 import java.util.Optional; 13 import java.util.concurrent.ConcurrentHashMap; 14 import java.util.concurrent.locks.Lock; 15 import java.util.concurrent.locks.ReentrantReadWriteLock; 16 import java.util.function.Predicate; 17 import java.util.function.Supplier; 18 import java.util.stream.Collectors; 19 import java.util.stream.IntStream; 20 import java.util.stream.Stream; 21 22 import org.openstreetmap.gui.jmapviewer.Tile; 23 import org.openstreetmap.josm.data.DataSource; 24 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 25 import org.openstreetmap.josm.data.osm.BBox; 26 import org.openstreetmap.josm.data.osm.DataSelectionListener; 27 import org.openstreetmap.josm.data.osm.DownloadPolicy; 28 import org.openstreetmap.josm.data.osm.HighlightUpdateListener; 29 import org.openstreetmap.josm.data.osm.OsmData; 30 import org.openstreetmap.josm.data.osm.PrimitiveId; 31 import org.openstreetmap.josm.data.osm.UploadPolicy; 32 import org.openstreetmap.josm.data.osm.WaySegment; 33 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 34 import org.openstreetmap.josm.tools.ListenerList; 35 import org.openstreetmap.josm.tools.Logging; 36 import org.openstreetmap.josm.tools.SubclassFilteredCollection; 37 38 /** 39 * A data class for Vector Data 40 * 41 * @author Taylor Smock 42 * @since xxx 43 */ 44 public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation> { 45 // Note: In Java 8, computeIfAbsent is blocking for both pre-existing and new values. In Java 9, it is only blocking 46 // for new values (perf increase). See JDK-8161372 for more info. 47 private final Map<Integer, VectorDataStore> dataStoreMap = new ConcurrentHashMap<>(); 48 private final Collection<PrimitiveId> selected = new HashSet<>(); 49 // Both of these listener lists are useless, since they expect OsmPrimitives at this time 50 private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create(); 51 private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create(); 52 private boolean lock = true; 53 private String name; 54 private short mappaintCacheIdx = 1; 55 56 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 57 58 /** 59 * The distance to consider nodes duplicates -- mostly a memory saving measure. 60 * 0.000_000_1 ~1.2 cm (+- 5.57 mm) 61 * Descriptions from <a href="https://xkcd.com/2170/">https://xkcd.com/2170/</a> 62 * Notes on <a href="https://wiki.openstreetmap.org/wiki/Node">https://wiki.openstreetmap.org/wiki/Node</a> indicate 63 * that IEEE 32-bit floats should not be used at high longitude (0.000_01 precision) 64 */ 65 protected static final float DUPE_NODE_DISTANCE = 0.000_000_1f; 66 67 /** 68 * The current zoom we are getting/adding to 69 */ 70 private int zoom; 71 /** 72 * Default to normal download policy 73 */ 74 private DownloadPolicy downloadPolicy = DownloadPolicy.NORMAL; 75 /** 76 * Default to a blocked upload policy 77 */ 78 private UploadPolicy uploadPolicy = UploadPolicy.BLOCKED; 79 /** 80 * The paint style for this layer 81 */ 82 private ElemStyles styles; 83 84 @Override 85 public Collection<DataSource> getDataSources() { 86 final int currentZoom = this.zoom; 87 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom)); 88 return dataStore.getDataSources(); 89 } 90 91 /** 92 * Add a data source 93 * 94 * @param currentZoom the zoom 95 * @param dataSource The datasource to add at the zoom level 96 */ 97 public void addDataSource(int currentZoom, DataSource dataSource) { 98 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom)); 99 dataStore.addDataSource(dataSource); 100 } 101 102 @Override 103 public void lock() { 104 this.lock = true; 105 } 106 107 @Override 108 public void unlock() { 109 this.lock = false; 110 } 111 112 @Override 113 public boolean isLocked() { 114 return this.lock; 115 } 116 117 @Override 118 public String getVersion() { 119 return "8"; // TODO get this dynamically. Not critical, as this is currently the _only_ version. 120 } 121 122 @Override 123 public String getName() { 124 return this.name; 125 } 126 127 @Override 128 public void setName(String name) { 129 this.name = name; 130 } 131 132 @Override 133 public void addPrimitive(VectorPrimitive primitive) { 134 primitive.setDataSet(this); 135 final int currentZoom = this.zoom; 136 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom)); 137 tryWrite(dataStore, () -> dataStore.addPrimitive(primitive)); 138 } 139 140 /** 141 * Remove a primitive from this dataset 142 * 143 * @param primitive The primitive to remove 144 */ 145 protected void removePrimitive(VectorPrimitive primitive) { 146 if (primitive.getDataSet() == this) { 147 primitive.setDataSet(null); 148 this.dataStoreMap.values() 149 .forEach(vectorDataStore -> tryWrite(vectorDataStore, () -> vectorDataStore.removePrimitive(primitive))); 150 } 151 } 152 153 @Override 154 public void clear() { 155 synchronized (this.dataStoreMap) { 156 this.dataStoreMap.clear(); 157 } 158 } 159 160 @Override 161 public List<VectorNode> searchNodes(BBox bbox) { 162 return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchNodes(bbox)) 163 .orElseGet(Collections::emptyList); 164 } 165 166 @Override 167 public boolean containsNode(VectorNode vectorNode) { 168 return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsNode(vectorNode)).orElse(false); 169 } 170 171 @Override 172 public List<VectorWay> searchWays(BBox bbox) { 173 return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchWays(bbox)) 174 .orElseGet(Collections::emptyList); 175 } 176 177 @Override 178 public boolean containsWay(VectorWay vectorWay) { 179 return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsWay(vectorWay)).orElse(false); 180 } 181 182 @Override 183 public List<VectorRelation> searchRelations(BBox bbox) { 184 return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.searchRelations(bbox)) 185 .orElseGet(Collections::emptyList); 186 } 187 188 @Override 189 public boolean containsRelation(VectorRelation vectorRelation) { 190 return this.getBestZoomDataStore().map(VectorDataStore::getStore).map(store -> store.containsRelation(vectorRelation)).orElse(false); 191 } 192 193 @Override 194 public VectorPrimitive getPrimitiveById(PrimitiveId primitiveId) { 195 return this.getBestZoomDataStore().map(VectorDataStore::getPrimitivesMap).map(m -> m .get(primitiveId)).orElse(null); 196 } 197 198 // The last return statement is "unchecked", even though it is literally the same as the previous return, except 199 // as an optional. 200 @SuppressWarnings("unchecked") 201 @Override 202 public <T extends VectorPrimitive> Collection<T> getPrimitives( 203 Predicate<? super VectorPrimitive> predicate) { 204 final VectorDataStore dataStore = this.getBestZoomDataStore().orElse(null); 205 if (dataStore == null) { 206 return Collections.emptyList(); 207 } 208 209 if (dataStore.getReadWriteLock().isWriteLocked()) { 210 return new SubclassFilteredCollection<>(new HashSet<>(dataStore.getAllPrimitives()), predicate); 211 } 212 return (Collection<T>) tryRead(dataStore, () -> new SubclassFilteredCollection<>(dataStore.getAllPrimitives(), predicate)) 213 // Throw an NPE if we don't have a collection (this should never happen, so if it does, _something_ is wrong) 214 .orElseThrow(NullPointerException::new); 215 } 216 217 @Override 218 public Collection<VectorNode> getNodes() { 219 return this.getPrimitives(VectorNode.class::isInstance); 220 } 221 222 @Override 223 public Collection<VectorWay> getWays() { 224 return this.getPrimitives(VectorWay.class::isInstance); 225 } 226 227 @Override 228 public Collection<VectorRelation> getRelations() { 229 return this.getPrimitives(VectorRelation.class::isInstance); 230 } 231 232 @Override 233 public DownloadPolicy getDownloadPolicy() { 234 return this.downloadPolicy; 235 } 236 237 @Override 238 public void setDownloadPolicy(DownloadPolicy downloadPolicy) { 239 this.downloadPolicy = downloadPolicy; 240 } 241 242 @Override 243 public UploadPolicy getUploadPolicy() { 244 return this.uploadPolicy; 245 } 246 247 @Override 248 public void setUploadPolicy(UploadPolicy uploadPolicy) { 249 this.uploadPolicy = uploadPolicy; 250 } 251 252 /** 253 * Get the current Read/Write lock 254 * @implNote This changes based off of zoom level. Please do not use this in a finally block 255 * @return The current read/write lock 256 */ 257 @Override 258 public Lock getReadLock() { 259 return getBestZoomDataStore().map(VectorDataStore::getReadWriteLock).map(ReentrantReadWriteLock::readLock) 260 .orElse(this.readWriteLock.readLock()); 261 } 262 263 @Override 264 public Collection<WaySegment> getHighlightedVirtualNodes() { 265 // TODO? This requires a change to WaySegment so that it isn't Way/Node specific 266 return Collections.emptyList(); 267 } 268 269 @Override 270 public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) { 271 // TODO? This requires a change to WaySegment so that it isn't Way/Node specific 272 } 273 274 @Override 275 public Collection<WaySegment> getHighlightedWaySegments() { 276 // TODO? This requires a change to WaySegment so that it isn't Way/Node specific 277 return Collections.emptyList(); 278 } 279 280 @Override 281 public void setHighlightedWaySegments(Collection<WaySegment> waySegments) { 282 // TODO? This requires a change to WaySegment so that it isn't Way/Node specific 283 } 284 285 @Override 286 public void addHighlightUpdateListener(HighlightUpdateListener listener) { 287 this.highlightUpdateListenerListenerList.addListener(listener); 288 } 289 290 @Override 291 public void removeHighlightUpdateListener(HighlightUpdateListener listener) { 292 this.highlightUpdateListenerListenerList.removeListener(listener); 293 } 294 295 @Override 296 public Collection<VectorPrimitive> getAllSelected() { 297 final Optional<VectorDataStore> dataStore = this.getBestZoomDataStore(); 298 return dataStore.map(vectorDataStore -> vectorDataStore.getAllPrimitives().stream() 299 .filter(primitive -> this.selected.contains(primitive.getPrimitiveId())) 300 .collect(Collectors.toList())).orElse(Collections.emptyList()); 301 } 302 303 /** 304 * Get the best zoom datastore 305 * @return A datastore with data, or {@code null} if no good datastore exists. 306 */ 307 private Optional<VectorDataStore> getBestZoomDataStore() { 308 final int currentZoom = this.zoom; 309 if (this.dataStoreMap.containsKey(currentZoom)) { 310 return Optional.of(this.dataStoreMap.get(currentZoom)); 311 } 312 // Check up to two zooms higher (may cause perf hit) 313 for (int tZoom = currentZoom + 1; tZoom < currentZoom + 3; tZoom++) { 314 if (this.dataStoreMap.containsKey(tZoom)) { 315 return Optional.of(this.dataStoreMap.get(tZoom)); 316 } 317 } 318 // Return *any* lower zoom data (shouldn't cause a perf hit...) 319 for (int tZoom = currentZoom - 1; tZoom >= 0; tZoom--) { 320 if (this.dataStoreMap.containsKey(tZoom)) { 321 return Optional.of(this.dataStoreMap.get(tZoom)); 322 } 323 } 324 // Check higher level zooms. May cause perf issues if selected datastore has a lot of data. 325 for (int tZoom = currentZoom + 3; tZoom < 34; tZoom++) { 326 if (this.dataStoreMap.containsKey(tZoom)) { 327 return Optional.of(this.dataStoreMap.get(tZoom)); 328 } 329 } 330 return Optional.empty(); 331 } 332 333 @Override 334 public boolean selectionEmpty() { 335 return this.selected.isEmpty(); 336 } 337 338 @Override 339 public boolean isSelected(VectorPrimitive osm) { 340 return this.selected.contains(osm.getPrimitiveId()); 341 } 342 343 @Override 344 public void toggleSelected(Collection<? extends PrimitiveId> osm) { 345 this.toggleSelectedImpl(osm.stream()); 346 } 347 348 @Override 349 public void toggleSelected(PrimitiveId... osm) { 350 this.toggleSelectedImpl(Stream.of(osm)); 351 } 352 353 private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) { 354 osm.forEach(primitiveId -> { 355 if (this.selected.contains(primitiveId)) { 356 this.selected.remove(primitiveId); 357 } else { 358 this.selected.add(primitiveId); 359 } 360 }); 361 } 362 363 @Override 364 public void setSelected(Collection<? extends PrimitiveId> selection) { 365 this.setSelectedImpl(selection.stream()); 366 } 367 368 @Override 369 public void setSelected(PrimitiveId... osm) { 370 this.setSelectedImpl(Stream.of(osm)); 371 } 372 373 private void setSelectedImpl(Stream<? extends PrimitiveId> osm) { 374 this.selected.clear(); 375 osm.forEach(this.selected::add); 376 } 377 378 @Override 379 public void addSelected(Collection<? extends PrimitiveId> selection) { 380 this.addSelectedImpl(selection.stream()); 381 } 382 383 @Override 384 public void addSelected(PrimitiveId... osm) { 385 this.addSelectedImpl(Stream.of(osm)); 386 } 387 388 private void addSelectedImpl(Stream<? extends PrimitiveId> osm) { 389 osm.forEach(this.selected::add); 390 } 391 392 @Override 393 public void clearSelection(PrimitiveId... osm) { 394 this.clearSelectionImpl(Stream.of(osm)); 395 } 396 397 @Override 398 public void clearSelection(Collection<? extends PrimitiveId> list) { 399 this.clearSelectionImpl(list.stream()); 400 } 401 402 @Override 403 public void clearSelection() { 404 this.clearSelectionImpl(new ArrayList<>(this.selected).stream()); 405 } 406 407 private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) { 408 osm.forEach(this.selected::remove); 409 } 410 411 @Override 412 public void addSelectionListener(DataSelectionListener listener) { 413 this.dataSelectionListenerListenerList.addListener(listener); 414 } 415 416 @Override 417 public void removeSelectionListener(DataSelectionListener listener) { 418 this.dataSelectionListenerListenerList.removeListener(listener); 419 } 420 421 public short getMappaintCacheIndex() { 422 return this.mappaintCacheIdx; 423 } 424 425 @Override 426 public void clearMappaintCache() { 427 this.mappaintCacheIdx++; 428 } 429 430 public void setZoom(int zoom) { 431 if (zoom == this.zoom) { 432 return; // Do nothing -- zoom isn't actually changing 433 } 434 this.zoom = zoom; 435 this.clearMappaintCache(); 436 final int[] nearestZoom = {-1, -1, -1, -1}; 437 nearestZoom[0] = zoom; 438 // Create a new list to avoid concurrent modification issues 439 synchronized (this.dataStoreMap) { 440 final int[] keys = new ArrayList<>(this.dataStoreMap.keySet()).stream().filter(Objects::nonNull) 441 .mapToInt(Integer::intValue).sorted().toArray(); 442 final int index; 443 if (this.dataStoreMap.containsKey(zoom)) { 444 index = Arrays.binarySearch(keys, zoom); 445 } else { 446 // (-(insertion point) - 1) = return -> insertion point = -(return + 1) 447 index = -(Arrays.binarySearch(keys, zoom) + 1); 448 } 449 if (index > 0) { 450 nearestZoom[1] = keys[index - 1]; 451 } 452 if (index < keys.length - 2) { 453 nearestZoom[2] = keys[index + 1]; 454 } 455 456 nearestZoom[3] = this.getBestZoomDataStore().map(VectorDataStore::getZoom).orElse(-1); 457 IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key)) 458 .mapToObj(this.dataStoreMap::get).forEach(VectorDataStore::destroy); 459 IntStream.of(keys).filter(key -> IntStream.of(nearestZoom).noneMatch(zoomKey -> zoomKey == key)) 460 .forEach(this.dataStoreMap::remove); 461 } 462 } 463 464 public int getZoom() { 465 return this.zoom; 466 } 467 468 /** 469 * Add tile data to this dataset 470 * @param tile The tile to add 471 * @param <T> The tile type 472 */ 473 public <T extends Tile & VectorTile> void addTileData(T tile) { 474 final int currentZoom = tile.getZoom(); 475 // computeIfAbsent should be thread safe (ConcurrentHashMap indicates it is, anyway) 476 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, tZoom -> new VectorDataStore(this, tZoom)); 477 tryWrite(dataStore, () -> dataStore.addTile(tile)); 478 } 479 480 /** 481 * Try to read something (here to avoid boilerplate) 482 * 483 * @param supplier The reading function 484 * @param <T> The return type 485 * @return The optional return 486 */ 487 private static <T> Optional<T> tryRead(VectorDataStore dataStore, Supplier<T> supplier) { 488 try { 489 dataStore.getReadWriteLock().readLock().lockInterruptibly(); 490 return Optional.ofNullable(supplier.get()); 491 } catch (InterruptedException e) { 492 Logging.error(e); 493 Thread.currentThread().interrupt(); 494 } finally { 495 dataStore.getReadWriteLock().readLock().unlock(); 496 } 497 return Optional.empty(); 498 } 499 500 /** 501 * Try to write something (here to avoid boilerplate) 502 * 503 * @param runnable The writing function 504 */ 505 private static void tryWrite(VectorDataStore dataStore, Runnable runnable) { 506 try { 507 dataStore.getReadWriteLock().writeLock().lockInterruptibly(); 508 runnable.run(); 509 } catch (InterruptedException e) { 510 Logging.error(e); 511 Thread.currentThread().interrupt(); 512 } finally { 513 if (dataStore.getReadWriteLock().isWriteLockedByCurrentThread()) { 514 dataStore.getReadWriteLock().writeLock().unlock(); 515 } 516 } 517 } 518 519 /** 520 * Get the styles for this layer 521 * 522 * @return The styles 523 */ 524 public ElemStyles getStyles() { 525 return this.styles; 526 } 527 528 /** 529 * Set the styles for this layer 530 * @param styles The styles to set for this layer 531 */ 532 public void setStyles(Collection<ElemStyles> styles) { 533 if (styles.size() == 1) { 534 this.styles = styles.iterator().next(); 535 } else if (!styles.isEmpty()) { 536 this.styles = new ElemStyles(styles.stream().flatMap(style -> style.getStyleSources().stream()).collect(Collectors.toList())); 537 } else { 538 this.styles = null; 539 } 540 } 541 } -
new file src/org/openstreetmap/josm/data/vector/VectorDataStore.java
diff --git src/org/openstreetmap/josm/data/vector/VectorDataStore.java src/org/openstreetmap/josm/data/vector/VectorDataStore.java new file mode 100644 index 000000000..f486651b6
- + 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.Collection; 10 import java.util.Collections; 11 import java.util.List; 12 import java.util.Objects; 13 import java.util.Optional; 14 import java.util.stream.Collectors; 15 16 import org.openstreetmap.gui.jmapviewer.Coordinate; 17 import org.openstreetmap.gui.jmapviewer.Tile; 18 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 19 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 20 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 21 import org.openstreetmap.josm.data.osm.BBox; 22 import org.openstreetmap.josm.data.osm.INode; 23 import org.openstreetmap.josm.data.osm.IRelation; 24 import org.openstreetmap.josm.data.osm.IWay; 25 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 26 import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 27 import org.openstreetmap.josm.data.osm.UniqueIdGenerator; 28 import org.openstreetmap.josm.gui.dialogs.relation.sort.RelationSorter; 29 import org.openstreetmap.josm.tools.Destroyable; 30 import org.openstreetmap.josm.tools.Geometry; 31 32 /** 33 * A data store for Vector Data sets 34 * @author Taylor Smock 35 * @since xxx 36 */ 37 class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> implements Destroyable { 38 private static final String JOSM_MERGE_TYPE_KEY = "josm_merge_type"; 39 private final VectorDataSet dataSet; 40 41 VectorDataStore(VectorDataSet dataSet, int zoom) { 42 super(zoom); 43 this.dataSet = dataSet; 44 } 45 46 @Override 47 protected void addPrimitive(VectorPrimitive primitive) { 48 primitive.setDataSet(this.dataSet); 49 // The field is uint64, so we can use negative numbers to indicate that it is a "generated" object (e.g., nodes for ways) 50 if (primitive.getUniqueId() == 0) { 51 final UniqueIdGenerator generator = primitive.getIdGenerator(); 52 long id; 53 do { 54 id = generator.generateUniqueId(); 55 } while (this.primitivesMap.containsKey(new SimplePrimitiveId(id, primitive.getType()))); 56 primitive.setId(primitive.getIdGenerator().generateUniqueId()); 57 } 58 if (primitive instanceof VectorRelation && !primitive.isMultipolygon()) { 59 primitive = mergeWays((VectorRelation) primitive); 60 } 61 final VectorPrimitive alreadyAdded = this.primitivesMap.get(primitive.getPrimitiveId()); 62 final VectorRelation mergedRelation = (VectorRelation) this.primitivesMap 63 .get(new SimplePrimitiveId(primitive.getPrimitiveId().getUniqueId(), 64 OsmPrimitiveType.RELATION)); 65 if (alreadyAdded == null || alreadyAdded.equals(primitive)) { 66 super.addPrimitive(primitive); 67 } else if (mergedRelation != null && mergedRelation.get(JOSM_MERGE_TYPE_KEY) != null) { 68 mergedRelation.addRelationMember(new VectorRelationMember("", primitive)); 69 super.addPrimitive(primitive); 70 // Check that all primitives can be merged 71 if (mergedRelation.getMemberPrimitivesList().stream().allMatch(IWay.class::isInstance)) { 72 // This pretty much does the "right" thing 73 this.mergeWays(mergedRelation); 74 } else if (!(primitive instanceof IWay)) { 75 // Can't merge, ever (one of the childs is a node/relation) 76 mergedRelation.remove(JOSM_MERGE_TYPE_KEY); 77 } 78 } else if (mergedRelation != null && primitive instanceof IRelation) { 79 // Just add to the relation 80 ((VectorRelation) primitive).getMembers().forEach(mergedRelation::addRelationMember); 81 } else if (alreadyAdded instanceof VectorWay && primitive instanceof VectorWay) { 82 final VectorRelation temporaryRelation = 83 mergedRelation == null ? new VectorRelation(primitive.getLayer()) : mergedRelation; 84 if (mergedRelation == null) { 85 temporaryRelation.put(JOSM_MERGE_TYPE_KEY, "merge"); 86 temporaryRelation.addRelationMember(new VectorRelationMember("", alreadyAdded)); 87 } 88 temporaryRelation.addRelationMember(new VectorRelationMember("", primitive)); 89 temporaryRelation.setDataSet(this.dataSet); 90 super.addPrimitive(primitive); 91 super.addPrimitive(temporaryRelation); 92 } 93 } 94 95 private VectorPrimitive mergeWays(VectorRelation relation) { 96 List<VectorRelationMember> members = RelationSorter.sortMembersByConnectivity(relation.getMembers()); 97 Collection<VectorWay> relationWayList = members.stream().map(VectorRelationMember::getMember) 98 .filter(VectorWay.class::isInstance) 99 .map(VectorWay.class::cast).collect(Collectors.toCollection(ArrayList::new)); 100 // Only support way-only relations 101 if (relationWayList.size() != relation.getMemberPrimitivesList().size()) { 102 return relation; 103 } 104 List<VectorWay> wayList = new ArrayList<>(relation.getMembersCount()); 105 // Assume that the order may not be correct, worst case O(n), best case O(n/2) 106 // Assume that the ways were drawn in order 107 final int maxIteration = relationWayList.size(); 108 int iteration = 0; 109 while (iteration < maxIteration && wayList.size() < relationWayList.size()) { 110 for (VectorWay way : relationWayList) { 111 if (wayList.isEmpty()) { 112 wayList.add(way); 113 continue; 114 } 115 // Check first/last ways (last first, since the list *should* be sorted) 116 if (canMergeWays(wayList.get(wayList.size() - 1), way, false)) { 117 wayList.add(way); 118 } else if (canMergeWays(wayList.get(0), way, false)) { 119 wayList.add(0, way); 120 } 121 } 122 iteration++; 123 relationWayList.removeIf(wayList::contains); 124 } 125 if (!relationWayList.isEmpty()) { 126 return relation; 127 } 128 // Merge ways 129 List<VectorNode> nodes = new ArrayList<>(); 130 for (VectorWay way : wayList) { 131 for (VectorNode node : way.getNodes()) { 132 if (nodes.isEmpty() || !Objects.equals(nodes.get(nodes.size() - 1), node)) { 133 nodes.add(node); 134 } 135 } 136 } 137 VectorWay way = wayList.get(0); 138 way.setNodes(nodes); 139 wayList.remove(way); 140 wayList.forEach(this::removePrimitive); 141 this.removePrimitive(relation); 142 return way; 143 } 144 145 private static <N extends INode, W extends IWay<N>> boolean canMergeWays(W old, W toAdd, boolean allowReverse) { 146 final List<N> nodes = new ArrayList<>(old.getNodes()); 147 boolean added = true; 148 if (allowReverse && old.firstNode().equals(toAdd.firstNode())) { 149 // old <-|-> new becomes old ->|-> new 150 Collections.reverse(nodes); 151 nodes.addAll(toAdd.getNodes()); 152 } else if (old.firstNode().equals(toAdd.lastNode())) { 153 // old <-|<- new, so we prepend the new nodes in order 154 nodes.addAll(0, toAdd.getNodes()); 155 } else if (old.lastNode().equals(toAdd.firstNode())) { 156 // old ->|-> new, we just add it 157 nodes.addAll(toAdd.getNodes()); 158 } else if (allowReverse && old.lastNode().equals(toAdd.lastNode())) { 159 // old ->|<- new, we need to reverse new 160 final List<N> toAddNodes = new ArrayList<>(toAdd.getNodes()); 161 Collections.reverse(toAddNodes); 162 nodes.addAll(toAddNodes); 163 } else { 164 added = false; 165 } 166 if (added) { 167 // This is (technically) always correct 168 old.setNodes(nodes); 169 } 170 return added; 171 } 172 173 private synchronized <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer, 174 Collection<VectorPrimitive> featureObjects, int x, int y) { 175 final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile); 176 final int layerExtent = layer.getExtent() * 2; 177 final ICoordinate lowerRight = tile.getTileSource() 178 .tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom()); 179 final ICoordinate coords = new Coordinate( 180 upperLeft.getLat() - (upperLeft.getLat() - lowerRight.getLat()) * y / layerExtent, 181 upperLeft.getLon() + (lowerRight.getLon() - upperLeft.getLon()) * x / layerExtent); 182 final Collection<VectorNode> nodes = this.store 183 .searchNodes(new BBox(coords.getLon(), coords.getLat(), VectorDataSet.DUPE_NODE_DISTANCE)); 184 final VectorNode node; 185 if (!nodes.isEmpty()) { 186 final VectorNode first = nodes.iterator().next(); 187 if (first.isDisabled() || !first.isVisible()) { 188 // Only replace nodes that are not visible 189 node = new VectorNode(layer.getName()); 190 node.setCoor(node.getCoor()); 191 first.getReferrers(true).forEach(primitive -> { 192 if (primitive instanceof VectorWay) { 193 List<VectorNode> nodeList = new ArrayList<>(((VectorWay) primitive).getNodes()); 194 nodeList.replaceAll(vnode -> vnode.equals(first) ? node : vnode); 195 ((VectorWay) primitive).setNodes(nodeList); 196 } else if (primitive instanceof VectorRelation) { 197 List<VectorRelationMember> members = new ArrayList<>(((VectorRelation) primitive).getMembers()); 198 members.replaceAll(member -> 199 member.getMember().equals(first) ? new VectorRelationMember(member.getRole(), node) : member); 200 ((VectorRelation) primitive).setMembers(members); 201 } 202 }); 203 this.removePrimitive(first); 204 } else { 205 node = first; 206 } 207 } else { 208 node = new VectorNode(layer.getName()); 209 } 210 node.setCoor(coords); 211 featureObjects.add(node); 212 return node; 213 } 214 215 private <T extends Tile & VectorTile> List<VectorWay> pathToWay(T tile, Layer layer, 216 Collection<VectorPrimitive> featureObjects, Path2D shape) { 217 final PathIterator pathIterator = shape.getPathIterator(null); 218 final List<VectorWay> ways = pathIteratorToObjects(tile, layer, featureObjects, pathIterator).stream() 219 .filter(VectorWay.class::isInstance).map(VectorWay.class::cast).collect( 220 Collectors.toList()); 221 // These nodes technically do not exist, so we shouldn't show them 222 ways.stream().flatMap(way -> way.getNodes().stream()) 223 .filter(prim -> !prim.isTagged() && prim.getReferrers(true).size() == 1 && prim.getId() <= 0) 224 .forEach(prim -> { 225 prim.setDisabled(true); 226 prim.setVisible(false); 227 }); 228 return ways; 229 } 230 231 private <T extends Tile & VectorTile> List<VectorPrimitive> pathIteratorToObjects(T tile, Layer layer, 232 Collection<VectorPrimitive> featureObjects, PathIterator pathIterator) { 233 final List<VectorNode> nodes = new ArrayList<>(); 234 final double[] coords = new double[6]; 235 final List<VectorPrimitive> ways = new ArrayList<>(); 236 do { 237 final int type = pathIterator.currentSegment(coords); 238 pathIterator.next(); 239 if ((PathIterator.SEG_MOVETO == type || PathIterator.SEG_CLOSE == type) && !nodes.isEmpty()) { 240 if (PathIterator.SEG_CLOSE == type) { 241 nodes.add(nodes.get(0)); 242 } 243 // New line 244 if (!nodes.isEmpty()) { 245 final VectorWay way = new VectorWay(layer.getName()); 246 way.setNodes(nodes); 247 featureObjects.add(way); 248 ways.add(way); 249 } 250 nodes.clear(); 251 } 252 if (PathIterator.SEG_MOVETO == type || PathIterator.SEG_LINETO == type) { 253 final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1]); 254 nodes.add(node); 255 } else if (PathIterator.SEG_CLOSE != type) { 256 // Vector Tiles only have MoveTo, LineTo, and ClosePath. Anything else is not supported at this time. 257 throw new UnsupportedOperationException(); 258 } 259 } while (!pathIterator.isDone()); 260 if (!nodes.isEmpty()) { 261 final VectorWay way = new VectorWay(layer.getName()); 262 way.setNodes(nodes); 263 featureObjects.add(way); 264 ways.add(way); 265 } 266 return ways; 267 } 268 269 private <T extends Tile & VectorTile> VectorRelation areaToRelation(T tile, Layer layer, 270 Collection<VectorPrimitive> featureObjects, Area area) { 271 final PathIterator pathIterator = area.getPathIterator(null); 272 final List<VectorPrimitive> members = pathIteratorToObjects(tile, layer, featureObjects, pathIterator); 273 VectorRelation vectorRelation = new VectorRelation(layer.getName()); 274 for (VectorPrimitive member : members) { 275 final String role; 276 if (member instanceof VectorWay && ((VectorWay) member).isClosed()) { 277 role = Geometry.isClockwise(((VectorWay) member).getNodes()) ? "outer" : "inner"; 278 } else { 279 role = ""; 280 } 281 vectorRelation.addRelationMember(new VectorRelationMember(role, member)); 282 } 283 return vectorRelation; 284 } 285 286 /** 287 * Add a tile to this data store 288 * @param tile The tile to add 289 * @param <T> The tile type 290 */ 291 public synchronized <T extends Tile & VectorTile> void addTile(T tile) { 292 Optional<Tile> previous = this.addedTiles.stream() 293 .filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny(); 294 // Check if we have already added the tile (just to save processing time) 295 if (!previous.isPresent() || (!previous.get().isLoaded() && !previous.get().isLoading())) { 296 previous.ifPresent(this.addedTiles::remove); 297 this.addedTiles.add(tile); 298 for (Layer layer : tile.getLayers()) { 299 layer.getFeatures().forEach(feature -> { 300 org.openstreetmap.josm.data.imagery.vectortile.mapbox.Geometry geometry = feature 301 .getGeometryObject(); 302 List<VectorPrimitive> featureObjects = new ArrayList<>(); 303 List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>(); 304 geometry.getShapes().forEach(shape -> { 305 final VectorPrimitive primitive; 306 if (shape instanceof Ellipse2D) { 307 primitive = pointToNode(tile, layer, featureObjects, 308 (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY()); 309 } else if (shape instanceof Path2D) { 310 primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape).stream().findFirst() 311 .orElse(null); 312 } else if (shape instanceof Area) { 313 primitive = areaToRelation(tile, layer, featureObjects, (Area) shape); 314 primitive.put("type", "multipolygon"); 315 } else { 316 // We shouldn't hit this, but just in case 317 throw new UnsupportedOperationException(); 318 } 319 primaryFeatureObjects.add(primitive); 320 }); 321 final VectorPrimitive primitive; 322 if (primaryFeatureObjects.size() == 1) { 323 primitive = primaryFeatureObjects.get(0); 324 if (primitive instanceof IRelation && !primitive.isMultipolygon()) { 325 primitive.put(JOSM_MERGE_TYPE_KEY, "merge"); 326 } 327 } else if (!primaryFeatureObjects.isEmpty()) { 328 VectorRelation relation = new VectorRelation(layer.getName()); 329 primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim)) 330 .forEach(relation::addRelationMember); 331 primitive = relation; 332 } else { 333 return; 334 } 335 primitive.setId(feature.getId()); 336 feature.getTags().forEach(primitive::put); 337 featureObjects.forEach(this::addPrimitive); 338 primaryFeatureObjects.forEach(this::addPrimitive); 339 this.addPrimitive(primitive); 340 }); 341 } 342 } 343 } 344 345 @Override 346 public void destroy() { 347 this.addedTiles.forEach(tile -> tile.setLoaded(false)); 348 this.addedTiles.forEach(tile -> tile.setImage(null)); 349 this.addedTiles.clear(); 350 this.store.clear(); 351 this.allPrimitives.clear(); 352 this.primitivesMap.clear(); 353 } 354 } -
new file src/org/openstreetmap/josm/data/vector/VectorNode.java
diff --git src/org/openstreetmap/josm/data/vector/VectorNode.java src/org/openstreetmap/josm/data/vector/VectorNode.java new file mode 100644 index 000000000..60aecd8ff
- + 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 37 public double lon() { 38 return this.lon; 39 } 40 41 @Override 42 public double lat() { 43 return this.lat; 44 } 45 46 @Override 47 public UniqueIdGenerator getIdGenerator() { 48 return ID_GENERATOR; 49 } 50 51 @Override 52 public LatLon getCoor() { 53 return new LatLon(this.lat, this.lon); 54 } 55 56 @Override 57 public void setCoor(LatLon coordinates) { 58 this.lat = coordinates.lat(); 59 this.lon = coordinates.lon(); 60 } 61 62 /** 63 * Set the coordinates of this node 64 * 65 * @param coordinates The coordinates to set 66 * @see #setCoor(LatLon) 67 */ 68 public void setCoor(ICoordinate coordinates) { 69 this.lat = coordinates.getLat(); 70 this.lon = coordinates.getLon(); 71 } 72 73 @Override 74 public void setEastNorth(EastNorth eastNorth) { 75 final LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth); 76 this.lat = ll.lat(); 77 this.lon = ll.lon(); 78 } 79 80 @Override 81 public boolean isReferredByWays(int n) { 82 // Count only referrers that are members of the same dataset (primitive can have some fake references, for example 83 // when way is cloned 84 List<? extends IPrimitive> referrers = super.getReferrers(); 85 if (referrers == null || referrers.isEmpty()) 86 return false; 87 if (referrers instanceof IPrimitive) 88 return n <= 1 && referrers instanceof IWay && ((IPrimitive) referrers).getDataSet() == getDataSet(); 89 else { 90 int counter = 0; 91 for (IPrimitive o : referrers) { 92 if (getDataSet() == o.getDataSet() && o instanceof IWay && ++counter >= n) 93 return true; 94 } 95 return false; 96 } 97 } 98 99 @Override 100 public void accept(PrimitiveVisitor visitor) { 101 visitor.visit(this); 102 } 103 104 @Override 105 public BBox getBBox() { 106 return new BBox(this.lon, this.lat); 107 } 108 109 @Override 110 public OsmPrimitiveType getType() { 111 return OsmPrimitiveType.NODE; 112 } 113 } -
new file src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
diff --git src/org/openstreetmap/josm/data/vector/VectorPrimitive.java src/org/openstreetmap/josm/data/vector/VectorPrimitive.java new file mode 100644 index 000000000..17b5bef6f
- + 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 protected VectorPrimitive(String layer) { 34 this.layer = layer; 35 this.id = getIdGenerator().generateUniqueId(); 36 } 37 38 @Override 39 protected void keysChangedImpl(Map<String, String> originalKeys) { 40 clearCachedStyle(); 41 if (dataSet != null) { 42 for (IPrimitive ref : getReferrers()) { 43 ref.clearCachedStyle(); 44 } 45 } 46 } 47 48 @Override 49 public boolean isHighlighted() { 50 return this.highlighted; 51 } 52 53 @Override 54 public void setHighlighted(boolean highlighted) { 55 this.highlighted = highlighted; 56 } 57 58 @Override 59 public boolean isTagged() { 60 return !this.getInterestingTags().isEmpty(); 61 } 62 63 @Override 64 public boolean isAnnotated() { 65 return this.getInterestingTags().size() - this.getKeys().size() > 0; 66 } 67 68 @Override 69 public VectorDataSet getDataSet() { 70 return this.dataSet; 71 } 72 73 protected void setDataSet(VectorDataSet dataSet) { 74 this.dataSet = dataSet; 75 } 76 77 /*---------- 78 * MAPPAINT 79 *--------*/ 80 private short mappaintCacheIdx; 81 82 @Override 83 public final StyleCache getCachedStyle() { 84 return mappaintStyle; 85 } 86 87 @Override 88 public final void setCachedStyle(StyleCache mappaintStyle) { 89 this.mappaintStyle = mappaintStyle; 90 } 91 92 @Override 93 public final boolean isCachedStyleUpToDate() { 94 return mappaintStyle != null && mappaintCacheIdx == dataSet.getMappaintCacheIndex(); 95 } 96 97 @Override 98 public final void declareCachedStyleUpToDate() { 99 this.mappaintCacheIdx = dataSet.getMappaintCacheIndex(); 100 } 101 102 @Override 103 public boolean hasDirectionKeys() { 104 return false; 105 } 106 107 @Override 108 public boolean reversedDirection() { 109 return false; 110 } 111 112 /*------------ 113 * Referrers 114 ------------*/ 115 // Largely the same as OsmPrimitive, OsmPrimitive not modified at this time to avoid breaking binary compatibility 116 117 private Object referrers; 118 119 @Override 120 public final List<VectorPrimitive> getReferrers(boolean allowWithoutDataset) { 121 return referrers(allowWithoutDataset, VectorPrimitive.class) 122 .collect(Collectors.toList()); 123 } 124 125 /** 126 * Add new referrer. If referrer is already included then no action is taken 127 * @param referrer The referrer to add 128 */ 129 protected void addReferrer(IPrimitive referrer) { 130 if (referrers == null) { 131 referrers = referrer; 132 } else if (referrers instanceof IPrimitive) { 133 if (referrers != referrer) { 134 referrers = new IPrimitive[] {(IPrimitive) referrers, referrer}; 135 } 136 } else { 137 for (IPrimitive primitive:(IPrimitive[]) referrers) { 138 if (primitive == referrer) 139 return; 140 } 141 referrers = Utils.addInArrayCopy((IPrimitive[]) referrers, referrer); 142 } 143 } 144 145 /** 146 * Remove referrer. No action is taken if referrer is not registered 147 * @param referrer The referrer to remove 148 */ 149 protected void removeReferrer(IPrimitive referrer) { 150 if (referrers instanceof IPrimitive) { 151 if (referrers == referrer) { 152 referrers = null; 153 } 154 } else if (referrers instanceof IPrimitive[]) { 155 IPrimitive[] orig = (IPrimitive[]) referrers; 156 int idx = IntStream.range(0, orig.length) 157 .filter(i -> orig[i] == referrer) 158 .findFirst().orElse(-1); 159 if (idx == -1) 160 return; 161 162 if (orig.length == 2) { 163 referrers = orig[1-idx]; // idx is either 0 or 1, take the other 164 } else { // downsize the array 165 IPrimitive[] smaller = new IPrimitive[orig.length-1]; 166 System.arraycopy(orig, 0, smaller, 0, idx); 167 System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx); 168 referrers = smaller; 169 } 170 } 171 } 172 173 private <T extends IPrimitive> Stream<T> referrers(boolean allowWithoutDataset, Class<T> filter) { 174 // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example 175 // when way is cloned 176 177 if (dataSet == null && !allowWithoutDataset) { 178 return Stream.empty(); 179 } 180 if (referrers == null) { 181 return Stream.empty(); 182 } 183 final Stream<IPrimitive> stream = referrers instanceof IPrimitive // NOPMD 184 ? Stream.of((IPrimitive) referrers) 185 : Arrays.stream((IPrimitive[]) referrers); 186 return stream 187 .filter(p -> p.getDataSet() == dataSet) 188 .filter(filter::isInstance) 189 .map(filter::cast); 190 } 191 192 /** 193 * Gets all primitives in the current dataset that reference this primitive. 194 * @param filter restrict primitives to subclasses 195 * @param <T> type of primitives 196 * @return the referrers as Stream 197 */ 198 public final <T extends IPrimitive> Stream<T> referrers(Class<T> filter) { 199 return referrers(false, filter); 200 } 201 202 @Override 203 public void visitReferrers(PrimitiveVisitor visitor) { 204 if (visitor != null) 205 doVisitReferrers(o -> o.accept(visitor)); 206 } 207 208 private void doVisitReferrers(Consumer<IPrimitive> visitor) { 209 if (this.referrers instanceof IPrimitive) { 210 IPrimitive ref = (IPrimitive) this.referrers; 211 if (ref.getDataSet() == dataSet) { 212 visitor.accept(ref); 213 } 214 } else if (this.referrers instanceof IPrimitive[]) { 215 IPrimitive[] refs = (IPrimitive[]) this.referrers; 216 for (IPrimitive ref: refs) { 217 if (ref.getDataSet() == dataSet) { 218 visitor.accept(ref); 219 } 220 } 221 } 222 } 223 224 /** 225 * Set the id of the object 226 * @param id The id 227 */ 228 protected void setId(long id) { 229 this.id = id; 230 } 231 232 /** 233 * Make this object disabled 234 * @param disabled {@code true} to disable the object 235 */ 236 public void setDisabled(boolean disabled) { 237 this.updateFlags(FLAG_DISABLED, disabled); 238 } 239 240 /** 241 * Make this object visible 242 * @param visible {@code true} to make this object visible (default) 243 */ 244 @Override 245 public void setVisible(boolean visible) { 246 this.updateFlags(FLAG_VISIBLE, visible); 247 } 248 249 /************************** 250 * Data layer information * 251 **************************/ 252 @Override 253 public String getLayer() { 254 return this.layer; 255 } 256 } -
new file src/org/openstreetmap/josm/data/vector/VectorRelation.java
diff --git src/org/openstreetmap/josm/data/vector/VectorRelation.java src/org/openstreetmap/josm/data/vector/VectorRelation.java new file mode 100644 index 000000000..0deb57e57
- + 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 35 public UniqueIdGenerator getIdGenerator() { 36 return RELATION_ID_GENERATOR; 37 } 38 39 @Override 40 public void accept(PrimitiveVisitor visitor) { 41 visitor.visit(this); 42 } 43 44 @Override 45 public BBox getBBox() { 46 if (cachedBBox == null) { 47 cachedBBox = new BBox(); 48 for (IPrimitive member : this.getMemberPrimitivesList()) { 49 cachedBBox.add(member.getBBox()); 50 } 51 } 52 return cachedBBox; 53 } 54 55 protected void addRelationMember(VectorRelationMember member) { 56 this.members.add(member); 57 member.getMember().addReferrer(this); 58 cachedBBox = null; 59 } 60 61 /** 62 * Remove the first instance of a member from the relation 63 * 64 * @param member The member to remove 65 */ 66 protected void removeRelationMember(VectorRelationMember member) { 67 this.members.remove(member); 68 if (!this.members.contains(member)) { 69 member.getMember().removeReferrer(this); 70 } 71 } 72 73 @Override 74 public int getMembersCount() { 75 return this.members.size(); 76 } 77 78 @Override 79 public VectorRelationMember getMember(int index) { 80 return this.members.get(index); 81 } 82 83 @Override 84 public List<VectorRelationMember> getMembers() { 85 return Collections.unmodifiableList(this.members); 86 } 87 88 @Override 89 public void setMembers(List<VectorRelationMember> members) { 90 this.members.clear(); 91 this.members.addAll(members); 92 } 93 94 @Override 95 public long getMemberId(int idx) { 96 return this.getMember(idx).getMember().getId(); 97 } 98 99 @Override 100 public String getRole(int idx) { 101 return this.getMember(idx).getRole(); 102 } 103 104 @Override 105 public OsmPrimitiveType getMemberType(int idx) { 106 return this.getMember(idx).getType(); 107 } 108 109 @Override 110 public OsmPrimitiveType getType() { 111 return this.getMembers().stream().map(VectorRelationMember::getType) 112 .allMatch(OsmPrimitiveType.CLOSEDWAY::equals) ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION; 113 } 114 } -
new file src/org/openstreetmap/josm/data/vector/VectorRelationMember.java
diff --git src/org/openstreetmap/josm/data/vector/VectorRelationMember.java src/org/openstreetmap/josm/data/vector/VectorRelationMember.java new file mode 100644 index 000000000..56d6dfe77
- + 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 32 public String getRole() { 33 return this.role; 34 } 35 36 @Override 37 public boolean isNode() { 38 return this.member instanceof INode; 39 } 40 41 @Override 42 public boolean isWay() { 43 return this.member instanceof IWay; 44 } 45 46 @Override 47 public boolean isRelation() { 48 return this.member instanceof IRelation; 49 } 50 51 @Override 52 public VectorPrimitive getMember() { 53 return this.member; 54 } 55 56 @Override 57 public long getUniqueId() { 58 return this.member.getId(); 59 } 60 61 @Override 62 public OsmPrimitiveType getType() { 63 return this.member.getType(); 64 } 65 66 @Override 67 public boolean isNew() { 68 return this.member.isNew(); 69 } 70 } -
new file src/org/openstreetmap/josm/data/vector/VectorWay.java
diff --git src/org/openstreetmap/josm/data/vector/VectorWay.java src/org/openstreetmap/josm/data/vector/VectorWay.java new file mode 100644 index 000000000..582fca2d4
- + 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 36 public UniqueIdGenerator getIdGenerator() { 37 return WAY_GENERATOR; 38 } 39 40 @Override 41 public void accept(PrimitiveVisitor visitor) { 42 visitor.visit(this); 43 } 44 45 @Override 46 public BBox getBBox() { 47 if (cachedBBox == null) { 48 cachedBBox = new BBox(); 49 for (INode node : this.getNodes()) { 50 cachedBBox.add(node.getBBox()); 51 } 52 } 53 return cachedBBox; 54 } 55 56 @Override 57 public int getNodesCount() { 58 return this.getNodes().size(); 59 } 60 61 @Override 62 public VectorNode getNode(int index) { 63 return this.getNodes().get(index); 64 } 65 66 @Override 67 public List<VectorNode> getNodes() { 68 return Collections.unmodifiableList(this.nodes); 69 } 70 71 @Override 72 public void setNodes(List<VectorNode> nodes) { 73 this.nodes.forEach(node -> node.removeReferrer(this)); 74 this.nodes.clear(); 75 nodes.forEach(node -> node.addReferrer(this)); 76 this.nodes.addAll(nodes); 77 this.cachedBBox = null; 78 } 79 80 @Override 81 public List<Long> getNodeIds() { 82 return this.getNodes().stream().map(VectorNode::getId).collect(Collectors.toList()); 83 } 84 85 @Override 86 public long getNodeId(int idx) { 87 return this.getNodes().get(idx).getId(); 88 } 89 90 @Override 91 public boolean isClosed() { 92 return this.firstNode() != null && this.firstNode().equals(this.lastNode()); 93 } 94 95 @Override 96 public VectorNode firstNode() { 97 if (this.nodes.isEmpty()) { 98 return null; 99 } 100 return this.getNode(0); 101 } 102 103 @Override 104 public VectorNode lastNode() { 105 if (this.nodes.isEmpty()) { 106 return null; 107 } 108 return this.getNode(this.getNodesCount() - 1); 109 } 110 111 @Override 112 public boolean isFirstLastNode(INode n) { 113 if (this.nodes.isEmpty()) { 114 return false; 115 } 116 return this.firstNode().equals(n) || this.lastNode().equals(n); 117 } 118 119 @Override 120 public boolean isInnerNode(INode n) { 121 if (this.nodes.isEmpty()) { 122 return false; 123 } 124 return !this.firstNode().equals(n) && !this.lastNode().equals(n) && this.nodes.stream() 125 .anyMatch(vectorNode -> vectorNode.equals(n)); 126 } 127 128 @Override 129 public OsmPrimitiveType getType() { 130 return this.isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY; 131 } 132 } -
src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java
diff --git src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationNodeMap.java index 0ac990275..1b768e6fb 100644
import java.util.Set; 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. … … import org.openstreetmap.josm.data.osm.Way; 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) { … … public class RelationNodeMap { 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 … … public class RelationNodeMap { 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(); … … public class RelationNodeMap { 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(); … … public class RelationNodeMap { 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)) { … … public class RelationNodeMap { 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. … … public class RelationNodeMap { 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 … … public class RelationNodeMap { 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; … … public class RelationNodeMap { 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)); … … public class RelationNodeMap { 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; … … public class RelationNodeMap { 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 { … … public class RelationNodeMap { 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; … … public class RelationNodeMap { 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 } … … public class RelationNodeMap { 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/RelationSortUtils.java
diff --git src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSortUtils.java index d7457f7f1..70023011d 100644
import static org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType 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 /** … … final class RelationSortUtils { 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 */ … … final class RelationSortUtils { 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 } -
src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java
diff --git src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java src/org/openstreetmap/josm/gui/dialogs/relation/sort/RelationSorter.java index 12713094a..34d6bdf4f 100644
import java.util.Objects; 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; … … public class RelationSorter { 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/layer/AbstractTileSourceLayer.java
diff --git src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java index f00dd956f..049951fcd 100644
import org.openstreetmap.josm.data.imagery.ImageryInfo; 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; … … import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChan 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; … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 889 891 if (coordinateConverter.requiresReprojection()) { 890 892 tile = new ReprojectionTile(tileSource, x, y, zoom); 891 893 } else { 892 tile = newTile(tileSource, x, y, zoom);894 tile = createTile(tileSource, x, y, zoom); 893 895 } 894 896 tileCache.addTile(tile); 895 897 } … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1042 1044 img = getLoadedTileImage(tile); 1043 1045 anchorImage = getAnchor(tile, img); 1044 1046 } 1045 if (img == null || anchorImage == null ) {1047 if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) { 1046 1048 miss = true; 1047 1049 } 1048 1050 } … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1051 1053 return; 1052 1054 } 1053 1055 1054 img = applyImageProcessors(img); 1056 if (img != null) { 1057 img = applyImageProcessors(img); 1058 } 1055 1059 1056 1060 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile); 1057 1061 synchronized (paintMutex) { … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1863 1867 1864 1868 for (int x = minX; x <= maxX; x++) { 1865 1869 for (int y = minY; y <= maxY; y++) { 1866 requestedTiles.add( newTile(tileSource, x, y, currentZoomLevel));1870 requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel)); 1867 1871 } 1868 1872 } 1869 1873 } … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1969 1973 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1970 1974 } 1971 1975 1976 /** 1977 * Create a new tile. Added to allow use of custom {@link Tile} objects. 1978 * 1979 * @param source Tile source 1980 * @param x X coordinate 1981 * @param y Y coordinate 1982 * @param zoom Zoom level 1983 * @return The new {@link Tile} 1984 * @since xxx 1985 */ 1986 public Tile createTile(T source, int x, int y, int zoom) { 1987 return new Tile(source, x, y, zoom); 1988 } 1989 1972 1990 @Override 1973 1991 public synchronized void destroy() { 1974 1992 super.destroy(); … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1989 2007 allocateCacheMemory(); 1990 2008 if (memory != null) { 1991 2009 doPaint(graphics); 2010 if (AbstractTileSourceLayer.this instanceof MVTLayer) { 2011 AbstractTileSourceLayer.this.paint(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getMapView() 2012 .getRealBounds()); 2013 } 1992 2014 } else { 1993 2015 Graphics g = graphics.getDefaultGraphics(); 1994 2016 Color oldColor = g.getColor(); -
src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
diff --git src/org/openstreetmap/josm/gui/layer/ImageryLayer.java src/org/openstreetmap/josm/gui/layer/ImageryLayer.java index 6c902b92a..933933da3 100644
import org.openstreetmap.josm.gui.MainApplication; 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; … … public abstract class ImageryLayer extends Layer { 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 } -
new file src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
diff --git src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java new file mode 100644 index 000000000..aa335f7b0
- + 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.VectorPrimitive; 48 import org.openstreetmap.josm.data.vector.VectorRelation; 49 import org.openstreetmap.josm.data.vector.VectorWay; 50 import org.openstreetmap.josm.gui.MainApplication; 51 import org.openstreetmap.josm.gui.MapView; 52 import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer; 53 import org.openstreetmap.josm.gui.layer.LayerManager; 54 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 55 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 56 import org.openstreetmap.josm.gui.mappaint.StyleSource; 57 58 /** 59 * A layer for MapBox Vector Tiles 60 * @author Taylor Smock 61 * @since xxx 62 */ 63 public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> implements TileListener { 64 private static final String CACHE_REGION_NAME = "MVT"; 65 // Just to avoid allocating a bunch of 0 length action arrays 66 private static final Action[] EMPTY_ACTIONS = new Action[0]; 67 private final Map<String, Boolean> layerNames = new HashMap<>(); 68 private final VectorDataSet dataSet = new VectorDataSet(); 69 70 /** 71 * Creates an instance of an MVT layer 72 * 73 * @param info ImageryInfo describing the layer 74 */ 75 public MVTLayer(ImageryInfo info) { 76 super(info); 77 } 78 79 @Override 80 protected Class<? extends TileLoader> getTileLoaderClass() { 81 return MapBoxVectorCachedTileLoader.class; 82 } 83 84 @Override 85 protected String getCacheName() { 86 return CACHE_REGION_NAME; 87 } 88 89 @Override 90 public Collection<String> getNativeProjections() { 91 // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857 92 // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}"). 93 return Collections.singleton(MVTFile.DEFAULT_PROJECTION); 94 } 95 96 @Override 97 public void paint(Graphics2D g, MapView mv, Bounds box) { 98 this.dataSet.setZoom(this.getZoomLevel()); 99 AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, false); 100 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress() 101 || !OsmDataLayer.PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get()); 102 // Set the painter to use our custom style sheet 103 if (painter instanceof StyledMapRenderer && this.dataSet.getStyles() != null) { 104 ((StyledMapRenderer) painter).setStyles(this.dataSet.getStyles()); 105 } 106 painter.render(this.dataSet, false, box); 107 } 108 109 @Override 110 protected MapboxVectorTileSource getTileSource() { 111 MapboxVectorTileSource source = new MapboxVectorTileSource(this.info); 112 this.info.setAttribution(source); 113 if (source.getStyleSource() != null) { 114 List<ElemStyles> styles = source.getStyleSource().getSources().entrySet().stream() 115 .filter(entry -> entry.getKey() == null || entry.getKey().getUrls().contains(source.getBaseUrl())) 116 .map(Map.Entry::getValue).collect(Collectors.toList()); 117 // load the style sources 118 styles.stream().map(ElemStyles::getStyleSources).flatMap(Collection::stream).forEach(StyleSource::loadStyleSource); 119 this.dataSet.setStyles(styles); 120 this.setName(source.getName()); 121 } 122 return source; 123 } 124 125 @Override 126 public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) { 127 final MVTTile tile = new MVTTile(source, x, y, zoom); 128 tile.addTileLoaderFinisher(this); 129 return tile; 130 } 131 132 @Override 133 public Action[] getMenuEntries() { 134 ArrayList<Action> actions = new ArrayList<>(Arrays.asList(super.getMenuEntries())); 135 // Add separator between Info and the layers 136 actions.add(SeparatorLayerAction.INSTANCE); 137 for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) { 138 actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true), 139 layer -> { 140 layerNames.compute(layer, (key, value) -> Boolean.FALSE.equals(value)); 141 this.invalidate(); 142 })); 143 } 144 // Add separator between layers and convert action 145 actions.add(SeparatorLayerAction.INSTANCE); 146 actions.add(new ConvertLayerAction(this)); 147 return actions.toArray(EMPTY_ACTIONS); 148 } 149 150 /** 151 * Get the data set for this layer 152 */ 153 public VectorDataSet getData() { 154 return this.dataSet; 155 } 156 157 private static class ConvertLayerAction extends AbstractAction implements LayerAction { 158 private final MVTLayer layer; 159 160 ConvertLayerAction(MVTLayer layer) { 161 this.layer = layer; 162 } 163 164 @Override 165 public void actionPerformed(ActionEvent e) { 166 LayerManager manager = MainApplication.getLayerManager(); 167 VectorDataSet dataSet = layer.getData(); 168 DataSet osmData = new DataSet(); 169 // Add nodes first, map is to ensure we can map new nodes to vector nodes 170 Map<VectorNode, Node> nodeMap = new HashMap<>(dataSet.getNodes().size()); 171 for (VectorNode vectorNode : dataSet.getNodes()) { 172 Node newNode = new Node(vectorNode.getCoor()); 173 if (vectorNode.isTagged()) { 174 vectorNode.getInterestingTags().forEach(newNode::put); 175 } 176 nodeMap.put(vectorNode, newNode); 177 } 178 // Add ways next 179 Map<VectorWay, Way> wayMap = new HashMap<>(dataSet.getWays().size()); 180 for (VectorWay vectorWay : dataSet.getWays()) { 181 Way newWay = new Way(); 182 List<Node> nodes = vectorWay.getNodes().stream().map(nodeMap::get).filter(Objects::nonNull).collect(Collectors.toList()); 183 newWay.setNodes(nodes); 184 if (vectorWay.isTagged()) { 185 vectorWay.getInterestingTags().forEach(newWay::put); 186 } 187 wayMap.put(vectorWay, newWay); 188 } 189 190 // Finally, add Relations 191 Map<VectorRelation, Relation> relationMap = new HashMap<>(dataSet.getRelations().size()); 192 for (VectorRelation vectorRelation : dataSet.getRelations()) { 193 Relation relation = new Relation(); 194 if (vectorRelation.isTagged()) { 195 vectorRelation.getInterestingTags().forEach(relation::put); 196 } 197 List<RelationMember> members = vectorRelation.getMembers().stream().map(member -> { 198 final OsmPrimitive primitive; 199 final VectorPrimitive vectorPrimitive = member.getMember(); 200 if (vectorPrimitive instanceof VectorNode) { 201 primitive = nodeMap.get(vectorPrimitive); 202 } else if (vectorPrimitive instanceof VectorWay) { 203 primitive = wayMap.get(vectorPrimitive); 204 } else if (vectorPrimitive instanceof VectorRelation) { 205 // Hopefully, relations are encountered in order... 206 primitive = relationMap.get(vectorPrimitive); 207 } else { 208 primitive = null; 209 } 210 if (primitive == null) return null; 211 return new RelationMember(member.getRole(), primitive); 212 }).filter(Objects::nonNull).collect(Collectors.toList()); 213 relation.setMembers(members); 214 relationMap.put(vectorRelation, relation); 215 } 216 try { 217 osmData.beginUpdate(); 218 nodeMap.values().forEach(osmData::addPrimitive); 219 wayMap.values().forEach(osmData::addPrimitive); 220 relationMap.values().forEach(osmData::addPrimitive); 221 } finally { 222 osmData.endUpdate(); 223 } 224 manager.addLayer(new OsmDataLayer(osmData, this.layer.getName(), null)); 225 manager.removeLayer(this.layer); 226 } 227 228 @Override 229 public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) { 230 return layers.stream().allMatch(MVTLayer.class::isInstance); 231 } 232 233 @Override 234 public Component createMenuComponent() { 235 JMenuItem menuItem = new JMenuItem(tr("Convert to OSM Data")); 236 menuItem.addActionListener(this); 237 return menuItem; 238 } 239 } 240 241 private static class EnableLayerAction extends AbstractAction implements LayerAction { 242 private final String layer; 243 private final Consumer<String> consumer; 244 private final BooleanSupplier state; 245 246 EnableLayerAction(String layer, BooleanSupplier state, Consumer<String> consumer) { 247 super(tr("Toggle layer {0}", layer)); 248 this.layer = layer; 249 this.consumer = consumer; 250 this.state = state; 251 } 252 253 @Override 254 public void actionPerformed(ActionEvent e) { 255 consumer.accept(layer); 256 } 257 258 @Override 259 public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) { 260 return layers.stream().allMatch(MVTLayer.class::isInstance); 261 } 262 263 @Override 264 public Component createMenuComponent() { 265 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 266 item.setSelected(this.state.getAsBoolean()); 267 return item; 268 } 269 } 270 271 @Override 272 public void finishedLoading(MVTTile tile) { 273 for (Layer layer : tile.getLayers()) { 274 this.layerNames.putIfAbsent(layer.getName(), true); 275 } 276 this.dataSet.addTileData(tile); 277 } 278 } -
src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java
diff --git src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java src/org/openstreetmap/josm/gui/mappaint/ElemStyles.java index fef095ea1..970635d96 100644
public class ElemStyles implements PreferenceChangedListener { 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 */ … … public class ElemStyles implements PreferenceChangedListener { 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 } 165 synchronized (osm.getStyleCacheSyncObject()) { 166 if (!osm.isCachedStyleUpToDate() || scale <= 0) { 167 osm.setCachedStyle(StyleCache.EMPTY_STYLECACHE); 169 168 } else { 170 boolean hasNonModifier = false; 171 boolean hasText = false; 172 for (StyleElement s : p.a) { 173 if (s instanceof BoxTextElement) { 174 hasText = true; 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; 175 178 } else { 176 if (!s.isModifier) { 177 hasNonModifier = true; 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 } 178 191 } 179 192 } 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);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 } 185 198 } 186 199 } 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); 200 } else if (osm instanceof IWay && isDefaultLines()) { 201 boolean hasProperLineStyle = false; 202 for (StyleElement s : p.a) { 203 if (s.isProperLineStyle()) { 204 hasProperLineStyle = true; 201 205 break; 202 206 } 203 207 } 204 p.a = new StyleElementList(p.a, line); 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 } 205 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; 206 229 } 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;217 230 } 218 231 219 232 /** … … public class ElemStyles implements PreferenceChangedListener { 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); -
src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java
diff --git src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java index f1e05a5f9..a19916e5b 100644
public final class ConditionFactory { 875 875 } 876 876 return e.osm.isSelected(); 877 877 } 878 879 /** 880 * Check if the object is highlighted (i.e., is hovered over) 881 * @param e The MapCSS environment 882 * @return {@code true} if the object is highlighted 883 * @see IPrimitive#isHighlighted 884 * @since xxx 885 */ 886 static boolean highlighted(Environment e) { // NO_UCD (unused code) 887 return e.osm.isHighlighted(); 888 } 878 889 } 879 890 880 891 /** -
new file src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
diff --git src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java new file mode 100644 index 000000000..99bbd058d
- + 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
diff --git src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java index d1d0ed096..cb78f9bda 100644
public class ImageryProvidersPanel extends JPanel { 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)); … … public class ImageryProvidersPanel extends JPanel { 440 441 case WMTS: 441 442 icon = /* ICON(dialogs/) */ "add_wmts"; 442 443 break; 444 case MVT: 445 icon = /* ICON(dialogs/) */ "add_mvt"; 446 break; 443 447 default: 444 448 break; 445 449 } … … public class ImageryProvidersPanel extends JPanel { 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 } … … public class ImageryProvidersPanel extends JPanel { 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); … … public class ImageryProvidersPanel extends JPanel { 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/data/mapillary.json
diff --git test/data/mapillary.json test/data/mapillary.json new file mode 100644 index 000000000..0f6f9483d
- + 1 { 2 "version":8, 3 "name":"Mapillary", 4 "owner":"Mapillary", 5 "id":"mapillary", 6 "sources":{ 7 "mapillary-source":{ 8 "type":"vector", 9 "tiles":[ 10 "https://tiles3.mapillary.com/v0.1/{z}/{x}/{y}.mvt" 11 ], 12 "maxzoom":14 13 }, 14 "mapillary-features-source": { 15 "maxzoom": 20, 16 "minzoom": 14, 17 "tiles": [ "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_&layers=points&per_page=1000" ], 18 "type": "vector" 19 }, 20 "mapillary-traffic-signs-source": { 21 "maxzoom": 20, 22 "minzoom": 14, 23 "tiles": [ "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_&layers=trafficsigns&per_page=1000" ], 24 "type": "vector" 25 } 26 }, 27 "layers":[ 28 { 29 "filter": [ "==", "pano", 1 ], 30 "id": "mapillary-panos", 31 "type": "circle", 32 "source": "mapillary-source", 33 "source-layer": "mapillary-images", 34 "minzoom": 17, 35 "paint": { 36 "circle-color": "#05CB63", 37 "circle-opacity": 0.5, 38 "circle-radius": 18 39 } 40 }, 41 { 42 "id": "mapillary-dots", 43 "type": "circle", 44 "source": "mapillary-source", 45 "source-layer": "mapillary-images", 46 "interactive": true, 47 "minzoom": 14, 48 "paint": { 49 "circle-color": "#05CB63", 50 "circle-radius": 6 51 } 52 }, 53 { 54 "id": "mapillary-lines", 55 "type": "line", 56 "source": "mapillary-source", 57 "source-layer": "mapillary-sequences", 58 "minzoom": 6, 59 "paint": { 60 "line-color": "#05CB63", 61 "line-width": 2 62 } 63 }, 64 { 65 "id": "mapillary-overview", 66 "type": "circle", 67 "source": "mapillary-source", 68 "source-layer": "mapillary-sequence-overview", 69 "maxzoom": 6, 70 "paint": { 71 "circle-radius": 4, 72 "circle-opacity": 0.6, 73 "circle-color": "#05CB63" 74 } 75 }, 76 { 77 "id": "mapillary-features", 78 "type": "symbol", 79 "source": "mapillary-features-source", 80 "source-layer": "mapillary-map-features", 81 "interactive": true, 82 "minzoom": 14, 83 "layout": { 84 "icon-image": "{value}", 85 "icon-allow-overlap": true, 86 "symbol-avoid-edges": true 87 }, 88 "paint": { 89 "text-color": "#fff", 90 "text-halo-color": "#000" 91 } 92 }, 93 { 94 "id": "mapillary-traffic-signs", 95 "type": "symbol", 96 "source": "mapillary-traffic-signs-source", 97 "source-layer": "mapillary-map-features", 98 "interactive": true, 99 "minzoom": 14, 100 "layout": { 101 "icon-image": "{value}", 102 "icon-allow-overlap": true, 103 "symbol-avoid-edges": true 104 }, 105 "paint": { 106 "text-color": "#fff", 107 "text-halo-color": "#000" 108 } 109 } 110 ] 111 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java
diff --git test/data/pbf/mapillary/14/3248/6258.mvt test/data/pbf/mapillary/14/3248/6258.mvt new file mode 100644 index 000000000..ff6a462a3 Binary files /dev/null and test/data/pbf/mapillary/14/3248/6258.mvt differ diff --git test/data/pbf/mapillary/14/3249/6258.mvt test/data/pbf/mapillary/14/3249/6258.mvt new file mode 100644 index 000000000..c5278577e Binary files /dev/null and test/data/pbf/mapillary/14/3249/6258.mvt differ diff --git test/data/pbf/openinframap/17/26028/50060.pbf test/data/pbf/openinframap/17/26028/50060.pbf new file mode 100644 index 000000000..358270e1b Binary files /dev/null and test/data/pbf/openinframap/17/26028/50060.pbf differ diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/FeatureTest.java new file mode 100644 index 000000000..5468fe649
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 5 import static org.junit.jupiter.api.Assertions.assertEquals; 6 import static org.junit.jupiter.api.Assertions.assertNotNull; 7 import static org.junit.jupiter.api.Assertions.assertSame; 8 import static org.junit.jupiter.api.Assertions.assertTrue; 9 10 11 import static org.openstreetmap.josm.data.imagery.vectortile.mapbox.LayerTest.getSimpleFeatureLayerBytes; 12 import static org.openstreetmap.josm.data.imagery.vectortile.mapbox.LayerTest.getLayer; 13 14 import java.text.NumberFormat; 15 import java.util.Arrays; 16 17 import org.junit.jupiter.api.Test; 18 19 /** 20 * Test class for {@link Feature} 21 */ 22 class FeatureTest { 23 /** 24 * This can be used to replace bytes 11-14 (inclusive) in {@link LayerTest#simpleFeatureLayerBytes}. 25 */ 26 private final byte[] nonPackedTags = new byte[] {0x10, 0x00, 0x10, 0x00}; 27 28 @Test 29 void testCreation() { 30 testCreation(getSimpleFeatureLayerBytes()); 31 } 32 33 @Test 34 void testCreationUnpacked() { 35 byte[] copyBytes = getSimpleFeatureLayerBytes(); 36 System.arraycopy(nonPackedTags, 0, copyBytes, 13, nonPackedTags.length); 37 testCreation(copyBytes); 38 } 39 40 @Test 41 void testCreationTrueToFalse() { 42 byte[] copyBytes = getSimpleFeatureLayerBytes(); 43 copyBytes[copyBytes.length - 1] = 0x00; // set value=false 44 Layer layer = assertDoesNotThrow(() -> getLayer(copyBytes)); 45 assertSame(Boolean.FALSE, layer.getValue(0)); 46 } 47 48 @Test 49 void testNumberGrouping() { 50 // This is the float we are adding 51 // 49 74 24 00 == 1_000_000f 52 // 3f 80 00 00 == 1f 53 byte[] newBytes = new byte[] {0x22, 0x09, 0x15, 0x00, 0x24, 0x74, 0x49}; 54 byte[] copyBytes = Arrays.copyOf(getSimpleFeatureLayerBytes(), getSimpleFeatureLayerBytes().length + newBytes.length - 4); 55 // Change last few bytes 56 System.arraycopy(newBytes, 0, copyBytes, 25, newBytes.length); 57 // Update the length of the record 58 copyBytes[1] = (byte) (copyBytes[1] + newBytes.length - 4); 59 final NumberFormat numberFormat = NumberFormat.getNumberInstance(); 60 final boolean numberFormatGroupingUsed = numberFormat.isGroupingUsed(); 61 // Sanity check 62 Layer layer; 63 try { 64 numberFormat.setGroupingUsed(true); 65 layer = assertDoesNotThrow(() -> getLayer(copyBytes)); 66 assertTrue(numberFormat.isGroupingUsed()); 67 } finally { 68 numberFormat.setGroupingUsed(numberFormatGroupingUsed); 69 } 70 assertEquals(1, layer.getFeatures().size()); 71 assertEquals("t", layer.getName()); 72 assertEquals(2, layer.getVersion()); 73 assertEquals("a", layer.getKey(0)); 74 assertEquals(1_000_000f, ((Number) layer.getValue(0)).floatValue(), 0.00001); 75 76 // Feature check 77 Feature feature = layer.getFeatures().iterator().next(); 78 checkDefaultGeometry(feature); 79 assertEquals("1000000", feature.getTags().get("a")); 80 } 81 82 private void testCreation(byte[] bytes) { 83 Layer layer = assertDoesNotThrow(() -> getLayer(bytes)); 84 // Sanity check the layer 85 assertEquals(1, layer.getFeatures().size()); 86 assertEquals("t", layer.getName()); 87 assertEquals(2, layer.getVersion()); 88 assertEquals("a", layer.getKey(0)); 89 assertSame(Boolean.TRUE, layer.getValue(0)); 90 91 // OK. Get the feature. 92 Feature feature = layer.getFeatures().iterator().next(); 93 94 checkDefaultTags(feature); 95 96 // Check id (should be the default of 0) 97 assertEquals(1, feature.getId()); 98 99 checkDefaultGeometry(feature); 100 } 101 102 private void checkDefaultTags(Feature feature) { 103 // Check tags 104 assertEquals(1, feature.getTags().size()); 105 assertTrue(feature.getTags().containsKey("a")); 106 // We are converting to a tag map (Map<String, String>), so "true" 107 assertEquals("true", feature.getTags().get("a")); 108 } 109 110 private void checkDefaultGeometry(Feature feature) { 111 // Check the geometry 112 assertEquals(GeometryTypes.POINT, feature.getGeometryType()); 113 assertEquals(1, feature.getGeometry().size()); 114 CommandInteger geometry = feature.getGeometry().get(0); 115 assertEquals(Command.MoveTo, geometry.getType()); 116 assertEquals(2, geometry.getOperations().length); 117 assertEquals(25, geometry.getOperations()[0]); 118 assertEquals(17, geometry.getOperations()[1]); 119 assertNotNull(feature.getGeometryObject()); 120 assertEquals(feature.getGeometryObject(), feature.getGeometryObject()); 121 } 122 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java
diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTest.java new file mode 100644 index 000000000..175d64cd7
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertFalse; 6 import static org.junit.jupiter.api.Assertions.assertThrows; 7 import static org.junit.jupiter.api.Assertions.assertTrue; 8 9 10 import java.awt.geom.Area; 11 import java.awt.geom.Ellipse2D; 12 import java.awt.geom.Path2D; 13 import java.awt.geom.PathIterator; 14 import java.awt.geom.Point2D; 15 import java.util.ArrayList; 16 import java.util.Arrays; 17 import java.util.Collections; 18 import java.util.List; 19 20 import org.junit.jupiter.api.Test; 21 22 /** 23 * Test class for {@link Geometry} 24 * @author Taylor Smock 25 * @since xxx 26 */ 27 class GeometryTest { 28 /** 29 * Create a command integer fairly easily 30 * @param command The command type (see {@link Command}) 31 * @param parameters The parameters for the command 32 * @return A command integer 33 */ 34 private static CommandInteger createCommandInteger(int command, int... parameters) { 35 CommandInteger commandInteger = new CommandInteger(command); 36 if (parameters != null) { 37 for (int parameter : parameters) { 38 commandInteger.addParameter(parameter); 39 } 40 } 41 return commandInteger; 42 } 43 44 /** 45 * Check the current 46 * @param pathIterator The path to check 47 * @param expected The expected coords 48 */ 49 private static void checkCurrentSegmentAndIncrement(PathIterator pathIterator, float... expected) { 50 float[] coords = new float[6]; 51 int type = pathIterator.currentSegment(coords); 52 pathIterator.next(); 53 for (int i = 0; i < expected.length; i++) { 54 assertEquals(expected[i], coords[i]); 55 } 56 if (Arrays.asList(PathIterator.SEG_MOVETO, PathIterator.SEG_LINETO).contains(type)) { 57 assertEquals(2, expected.length, "You should check both x and y coordinates"); 58 } else if (PathIterator.SEG_QUADTO == type) { 59 assertEquals(4, expected.length, "You should check all x and y coordinates"); 60 } else if (PathIterator.SEG_CUBICTO == type) { 61 assertEquals(6, expected.length, "You should check all x and y coordinates"); 62 } else if (PathIterator.SEG_CLOSE == type) { 63 assertEquals(0, expected.length, "CloseTo has no expected coordinates to check"); 64 } 65 } 66 67 @Test 68 void testBadGeometry() { 69 IllegalArgumentException badPointException = assertThrows(IllegalArgumentException.class, 70 () -> new Geometry(GeometryTypes.POINT, Collections.singletonList(createCommandInteger(1)))); 71 assertEquals("POINT with 0 arguments is not understood", badPointException.getMessage()); 72 IllegalArgumentException badLineException = assertThrows(IllegalArgumentException.class, 73 () -> new Geometry(GeometryTypes.LINESTRING, Collections.singletonList(createCommandInteger(15)))); 74 assertEquals("LINESTRING with 0 arguments is not understood", badLineException.getMessage()); 75 } 76 77 @Test 78 void testPoint() { 79 CommandInteger moveTo = createCommandInteger(9, 17, 34); 80 Geometry geometry = new Geometry(GeometryTypes.POINT, Collections.singletonList(moveTo)); 81 assertEquals(1, geometry.getShapes().size()); 82 Ellipse2D shape = (Ellipse2D) geometry.getShapes().iterator().next(); 83 assertEquals(17, shape.getCenterX()); 84 assertEquals(34, shape.getCenterY()); 85 } 86 87 @Test 88 void testLine() { 89 CommandInteger moveTo = createCommandInteger(9, 2, 2); 90 CommandInteger lineTo = createCommandInteger(18, 0, 8, 8, 0); 91 Geometry geometry = new Geometry(GeometryTypes.LINESTRING, Arrays.asList(moveTo, lineTo)); 92 assertEquals(1, geometry.getShapes().size()); 93 Path2D path = (Path2D) geometry.getShapes().iterator().next(); 94 PathIterator pathIterator = path.getPathIterator(null); 95 checkCurrentSegmentAndIncrement(pathIterator, 2, 2); 96 checkCurrentSegmentAndIncrement(pathIterator, 2, 10); 97 checkCurrentSegmentAndIncrement(pathIterator, 10, 10); 98 assertTrue(pathIterator.isDone()); 99 } 100 101 @Test 102 void testPolygon() { 103 List<CommandInteger> commands = new ArrayList<>(3); 104 commands.add(createCommandInteger(9, 3, 6)); 105 commands.add(createCommandInteger(18, 5, 6, 12, 22)); 106 commands.add(createCommandInteger(15)); 107 108 Geometry geometry = new Geometry(GeometryTypes.POLYGON, commands); 109 assertEquals(1, geometry.getShapes().size()); 110 111 Area area = (Area) geometry.getShapes().iterator().next(); 112 PathIterator pathIterator = area.getPathIterator(null); 113 checkCurrentSegmentAndIncrement(pathIterator, 3, 6); 114 // This is somewhat unexpected, and may change based off of JVM implementations 115 // But for whatever reason, Java flips the inner coordinates in this case. 116 checkCurrentSegmentAndIncrement(pathIterator, 20, 34); 117 checkCurrentSegmentAndIncrement(pathIterator, 8, 12); 118 checkCurrentSegmentAndIncrement(pathIterator, 3, 6); 119 checkCurrentSegmentAndIncrement(pathIterator); 120 assertTrue(pathIterator.isDone()); 121 } 122 123 @Test 124 void testBadPolygon() { 125 /* 126 * "Linear rings MUST be geometric objects that have no anomalous geometric points, 127 * such as self-intersection or self-tangency. The position of the cursor before 128 * calling the ClosePath command of a linear ring SHALL NOT repeat the same position 129 * as the first point in the linear ring as this would create a zero-length line 130 * segment. A linear ring SHOULD NOT have an area calculated by the surveyor's 131 * formula equal to zero, as this would signify a ring with anomalous geometric points." 132 */ 133 List<CommandInteger> commands = new ArrayList<>(3); 134 commands.add(createCommandInteger(9, 0, 0)); 135 commands.add(createCommandInteger(18, 0, 0)); 136 commands.add(createCommandInteger(15)); 137 IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new Geometry(GeometryTypes.POLYGON, commands)); 138 assertEquals("POLYGON cannot have zero area", exception.getMessage()); 139 } 140 141 @Test 142 void testMultiPolygon() { 143 List<CommandInteger> commands = new ArrayList<>(10); 144 // Polygon 1 145 commands.add(createCommandInteger(9, 0, 0)); 146 commands.add(createCommandInteger(26, 10, 0, 0, 10, -10, 0)); 147 commands.add(createCommandInteger(15)); 148 // Polygon 2 outer 149 commands.add(createCommandInteger(9, 11, 1)); 150 commands.add(createCommandInteger(26, 9, 0, 0, 9, -9, 0)); 151 commands.add(createCommandInteger(15)); 152 // Polygon 2 inner 153 commands.add(createCommandInteger(9, 2, -7)); 154 commands.add(createCommandInteger(26, 0, 4, 4, 0, 0, -4)); 155 commands.add(createCommandInteger(15)); 156 157 Geometry geometry = new Geometry(GeometryTypes.POLYGON, commands); 158 assertEquals(1, geometry.getShapes().size()); 159 Area area = (Area) geometry.getShapes().iterator().next(); 160 assertFalse(area.isSingular()); 161 PathIterator pathIterator = area.getPathIterator(null); 162 assertEquals(PathIterator.WIND_NON_ZERO, pathIterator.getWindingRule()); 163 assertTrue(area.contains(new Point2D.Float(5, 5))); 164 assertTrue(area.contains(new Point2D.Float(12, 12))); 165 assertFalse(area.contains(new Point2D.Float(15, 15))); 166 assertFalse(area.contains(new Point2D.Float(10, 11))); 167 assertFalse(area.contains(new Point2D.Float(-1, -1))); 168 } 169 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java
diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypesTest.java new file mode 100644 index 000000000..da0f9b3c7
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.fail; 6 7 8 import org.openstreetmap.josm.TestUtils; 9 10 import org.junit.jupiter.api.Test; 11 import org.junit.jupiter.params.ParameterizedTest; 12 import org.junit.jupiter.params.provider.EnumSource; 13 14 /** 15 * Test class for {@link GeometryTypes} 16 * @author Taylor Smock 17 * @since xxx 18 */ 19 class GeometryTypesTest { 20 @Test 21 void testNaiveEnumTest() { 22 TestUtils.superficialEnumCodeCoverage(GeometryTypes.class); 23 TestUtils.superficialEnumCodeCoverage(GeometryTypes.Ring.class); 24 } 25 26 @ParameterizedTest 27 @EnumSource(GeometryTypes.class) 28 void testExpectedIds(GeometryTypes type) { 29 // Ensure that users can get the type from the ordinal 30 // See https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto#L8 31 // for the expected values 32 final int expectedId; 33 if (type == GeometryTypes.UNKNOWN) { 34 expectedId = 0; 35 } else if (type == GeometryTypes.POINT) { 36 expectedId = 1; 37 } else if (type == GeometryTypes.LINESTRING) { 38 expectedId = 2; 39 } else if (type == GeometryTypes.POLYGON) { 40 expectedId = 3; 41 } else { 42 fail("Unknown geometry type, see vector tile spec"); 43 expectedId = Integer.MIN_VALUE; 44 } 45 assertEquals(expectedId, type.ordinal()); 46 } 47 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java
diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/LayerTest.java new file mode 100644 index 000000000..fc3ba9c27
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 5 import static org.junit.jupiter.api.Assertions.assertEquals; 6 import static org.junit.jupiter.api.Assertions.assertThrows; 7 8 import java.io.FileInputStream; 9 import java.io.IOException; 10 import java.util.Arrays; 11 import java.util.List; 12 13 import org.openstreetmap.josm.TestUtils; 14 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 15 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 16 17 import nl.jqno.equalsverifier.EqualsVerifier; 18 import org.junit.jupiter.api.Test; 19 20 /** 21 * Test class for {@link Layer} 22 */ 23 public class LayerTest { 24 /** 25 * This looks something like this (if it were json). Note that some keys could be repeated, 26 * and so could be better represented as an array. Specifically, "features", "key", and "value". 27 * "layer": { 28 * "name": "t", 29 * "version": 2, 30 * "features": { 31 * "type": "POINT", 32 * "tags": [0, 0], 33 * "geometry": [9, 50, 34] 34 * }, 35 * "key": "a", 36 * "value": true 37 * } 38 * 39 * WARNING: DO NOT MODIFY THIS ARRAY DIRECTLY -- it could contaminate other tests 40 */ 41 private static final byte[] simpleFeatureLayerBytes = new byte[] { 42 0x1a, 0x1b, // layer, 27 bytes for the rest 43 0x0a, 0x01, 0x74, // name=t 44 0x78, 0x02, // version=2 45 0x12, 0x0d, // features, 11 bytes 46 0x08, 0x01, // id=1 47 0x18, 0x01, // type=POINT 48 0x12, 0x02, 0x00, 0x00, // tags=[0, 0] (packed). Non-packed would be [0x10, 0x00, 0x10, 0x00] 49 0x22, 0x03, 0x09, 0x32, 0x22, // geometry=[9, 50, 34] 50 0x1a, 0x01, 0x61, // key=a 51 0x22, 0x02, 0x38, 0x01, // value=true (boolean) 52 }; 53 54 /** 55 * Gets a copy of {@link #simpleFeatureLayerBytes} so that a test doesn't accidentally change the bytes 56 * @return An array that can be modified. 57 */ 58 static byte[] getSimpleFeatureLayerBytes() { 59 return Arrays.copyOf(simpleFeatureLayerBytes, simpleFeatureLayerBytes.length); 60 } 61 62 /** 63 * Create a layer from bytes 64 * @param bytes The bytes that make up the layer 65 * @return The generated layer 66 * @throws IOException If something happened (should never trigger) 67 */ 68 static Layer getLayer(byte[] bytes) throws IOException { 69 List<ProtoBufRecord> records = (List<ProtoBufRecord>) new ProtoBufParser(bytes).allRecords(); 70 assertEquals(1, records.size()); 71 return new Layer(new ProtoBufParser(records.get(0).getBytes()).allRecords()); 72 } 73 74 @Test 75 void testLayerCreation() throws IOException { 76 List<ProtoBufRecord> layers = (List<ProtoBufRecord>) new ProtoBufParser(new FileInputStream(TestUtils.getTestDataRoot() 77 + "pbf/mapillary/14/3249/6258.mvt")).allRecords(); 78 Layer sequenceLayer = new Layer(layers.get(0).getBytes()); 79 assertEquals("mapillary-sequences", sequenceLayer.getName()); 80 assertEquals(1, sequenceLayer.getFeatures().size()); 81 assertEquals(1, sequenceLayer.getGeometry().size()); 82 assertEquals(2048, sequenceLayer.getExtent()); 83 assertEquals(1, sequenceLayer.getVersion()); 84 85 Layer imageLayer = new Layer(layers.get(1).getBytes()); 86 assertEquals("mapillary-images", imageLayer.getName()); 87 assertEquals(116, imageLayer.getFeatures().size()); 88 assertEquals(116, imageLayer.getGeometry().size()); 89 assertEquals(2048, imageLayer.getExtent()); 90 assertEquals(1, imageLayer.getVersion()); 91 } 92 93 @Test 94 void testLayerEqualsHashCode() throws IOException { 95 List<ProtoBufRecord> layers = (List<ProtoBufRecord>) new ProtoBufParser(new FileInputStream(TestUtils.getTestDataRoot() 96 + "pbf/mapillary/14/3249/6258.mvt")).allRecords(); 97 EqualsVerifier.forClass(Layer.class).withPrefabValues(byte[].class, layers.get(0).getBytes(), layers.get(1).getBytes()) 98 .verify(); 99 } 100 101 @Test 102 void testVersionsNumbers() { 103 byte[] copyByte = getSimpleFeatureLayerBytes(); 104 assertEquals(2, assertDoesNotThrow(() -> getLayer(copyByte)).getVersion()); 105 copyByte[6] = 1; 106 assertEquals(1, assertDoesNotThrow(() -> getLayer(copyByte)).getVersion()); 107 copyByte[6] = 0; 108 IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte)); 109 assertEquals("We do not understand version 0 of the vector tile specification", exception.getMessage()); 110 copyByte[6] = 3; 111 exception = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte)); 112 assertEquals("We do not understand version 3 of the vector tile specification", exception.getMessage()); 113 // Remove version number (AKA change it to some unknown field). Default is version=1. 114 copyByte[5] = 0x18; 115 assertEquals(1, assertDoesNotThrow(() -> getLayer(copyByte)).getVersion()); 116 } 117 118 @Test 119 void testLayerName() throws IOException { 120 byte[] copyByte = getSimpleFeatureLayerBytes(); 121 Layer layer = getLayer(copyByte); 122 assertEquals("t", layer.getName()); 123 copyByte[2] = 0x1a; // name=t -> ? 124 Exception noNameException = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte)); 125 assertEquals("Vector tile layers must have a layer name", noNameException.getMessage()); 126 } 127 128 @Test 129 void testUnknownField() { 130 byte[] copyByte = getSimpleFeatureLayerBytes(); 131 copyByte[27] = 0x78; 132 Exception unknownField = assertThrows(IllegalArgumentException.class, () -> getLayer(copyByte)); 133 assertEquals("Unknown field in vector tile layer value (15)", unknownField.getMessage()); 134 } 135 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java
diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTileTest.java new file mode 100644 index 000000000..66e4ea781
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertNull; 6 7 import java.awt.image.BufferedImage; 8 import java.util.Collections; 9 import java.util.stream.Stream; 10 11 import org.openstreetmap.gui.jmapviewer.Tile; 12 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 13 import org.openstreetmap.josm.TestUtils; 14 import org.openstreetmap.josm.data.cache.JCSCacheManager; 15 import org.openstreetmap.josm.data.imagery.ImageryInfo; 16 import org.openstreetmap.josm.data.imagery.TileJobOptions; 17 import org.openstreetmap.josm.testutils.JOSMTestRules; 18 19 import org.awaitility.Awaitility; 20 import org.awaitility.Durations; 21 import org.junit.jupiter.api.BeforeEach; 22 import org.junit.jupiter.api.extension.RegisterExtension; 23 import org.junit.jupiter.params.ParameterizedTest; 24 import org.junit.jupiter.params.provider.Arguments; 25 import org.junit.jupiter.params.provider.MethodSource; 26 27 /** 28 * Test class for {@link MVTTile} 29 */ 30 public class MVTTileTest { 31 private MapboxVectorTileSource tileSource; 32 private MapBoxVectorCachedTileLoader loader; 33 @RegisterExtension 34 JOSMTestRules rule = new JOSMTestRules(); 35 @BeforeEach 36 void setup() { 37 tileSource = new MapboxVectorTileSource(new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot() 38 + "pbf/mapillary/{z}/{x}/{y}.mvt")); 39 loader = new MapBoxVectorCachedTileLoader(null, 40 JCSCacheManager.getCache("testMapillaryCache"), new TileJobOptions(1, 1, Collections 41 .emptyMap(), 3600)); 42 } 43 44 /** 45 * Provide arguments for {@link #testMVTTile(BufferedImage, Boolean)} 46 * @return The arguments to use 47 */ 48 private static Stream<Arguments> testMVTTile() { 49 return Stream.of( 50 Arguments.of(null, Boolean.TRUE), 51 Arguments.of(Tile.LOADING_IMAGE, Boolean.TRUE), 52 Arguments.of(Tile.ERROR_IMAGE, Boolean.TRUE), 53 Arguments.of(new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_GRAY), Boolean.FALSE) 54 ); 55 } 56 57 @ParameterizedTest 58 @MethodSource("testMVTTile") 59 void testMVTTile(BufferedImage image, Boolean isLoaded) { 60 MVTTile tile = new MVTTile(tileSource, 3249, 6258, 14); 61 tile.setImage(image); 62 assertEquals(image, tile.getImage()); 63 64 TileJob job = loader.createTileLoaderJob(tile); 65 job.submit(); 66 Awaitility.await().atMost(Durations.ONE_SECOND).until(tile::isLoaded); 67 if (isLoaded) { 68 Awaitility.await().atMost(Durations.ONE_SECOND).until(() -> tile.getLayers() != null && tile.getLayers().size() > 1); 69 assertEquals(2, tile.getLayers().size()); 70 // The test Mapillary tiles have 2048 instead of 4096 for their extent. This *may* change 71 // in future Mapillary tiles, so if the test PBF files are updated, beware. 72 assertEquals(2048, tile.getExtent()); 73 // Ensure that we have the clear image set, such that the tile doesn't add to the dataset again 74 // and we don't have a loading image 75 assertEquals(MVTTile.CLEAR_LOADED, tile.getImage()); 76 } else { 77 assertNull(tile.getLayers()); 78 assertEquals(image, tile.getImage()); 79 } 80 } 81 82 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java
diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSourceTest.java new file mode 100644 index 000000000..5b9f16842
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 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.assertNull; 7 8 9 import java.util.stream.Stream; 10 11 import org.junit.jupiter.api.extension.RegisterExtension; 12 import org.openstreetmap.josm.TestUtils; 13 import org.openstreetmap.josm.data.imagery.ImageryInfo; 14 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.MapBoxVectorStyle; 15 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.style.Source; 16 import org.openstreetmap.josm.gui.ExtendedDialog; 17 import org.openstreetmap.josm.gui.widgets.JosmComboBox; 18 import org.openstreetmap.josm.testutils.JOSMTestRules; 19 import org.openstreetmap.josm.testutils.mockers.ExtendedDialogMocker; 20 21 import org.junit.jupiter.api.Test; 22 import org.junit.jupiter.params.ParameterizedTest; 23 import org.junit.jupiter.params.provider.Arguments; 24 import org.junit.jupiter.params.provider.MethodSource; 25 26 /** 27 * Test class for {@link MapboxVectorTileSource} 28 * @author Taylor Smock 29 * @since xxx 30 */ 31 class MapboxVectorTileSourceTest { 32 @RegisterExtension 33 JOSMTestRules rule = new JOSMTestRules(); 34 private static class SelectLayerDialogMocker extends ExtendedDialogMocker { 35 int index; 36 @Override 37 protected void act(final ExtendedDialog instance) { 38 ((JosmComboBox<?>) this.getContent(instance)).setSelectedIndex(index); 39 } 40 41 @Override 42 protected String getString(final ExtendedDialog instance) { 43 return String.join(";", ((Source) ((JosmComboBox<?>) this.getContent(instance)).getSelectedItem()).getUrls()); 44 } 45 } 46 47 @Test 48 void testNoStyle() { 49 MapboxVectorTileSource tileSource = new MapboxVectorTileSource( 50 new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot() + "pbf/mapillary/{z}/{x}/{y}.mvt")); 51 assertNull(tileSource.getStyleSource()); 52 } 53 54 private static Stream<Arguments> testMapillaryStyle() { 55 return Stream.of(Arguments.of(0, "Test Mapillary: mapillary-source", "https://tiles3.mapillary.com/v0.1/{z}/{x}/{y}.mvt"), 56 Arguments.of(1, "Test Mapillary: mapillary-features-source", 57 "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_" 58 + "&layers=points&per_page=1000"), 59 Arguments.of(2, "Test Mapillary: mapillary-traffic-signs-source", 60 "https://a.mapillary.com/v3/map_features?tile={z}/{x}/{y}&client_id=_apiKey_" 61 + "&layers=trafficsigns&per_page=1000")); 62 } 63 64 @ParameterizedTest 65 @MethodSource("testMapillaryStyle") 66 void testMapillaryStyle(Integer index, String expected, String dialogMockerText) { 67 TestUtils.assumeWorkingJMockit(); 68 SelectLayerDialogMocker extendedDialogMocker = new SelectLayerDialogMocker(); 69 extendedDialogMocker.index = index; 70 extendedDialogMocker.getMockResultMap().put(dialogMockerText, "Add layers"); 71 MapboxVectorTileSource tileSource = new MapboxVectorTileSource( 72 new ImageryInfo("Test Mapillary", "file:/" + TestUtils.getTestDataRoot() + "mapillary.json")); 73 MapBoxVectorStyle styleSource = tileSource.getStyleSource(); 74 assertNotNull(styleSource); 75 assertEquals(expected, tileSource.toString()); 76 } 77 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java
diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/ExpressionTest.java new file mode 100644 index 000000000..0130da35e
- + 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 6 7 import javax.json.Json; 8 import javax.json.JsonValue; 9 10 import nl.jqno.equalsverifier.EqualsVerifier; 11 import org.junit.jupiter.api.Test; 12 13 /** 14 * Test class for {@link Expression} 15 * @author Taylor Smock 16 * @since xxx 17 */ 18 class ExpressionTest { 19 @Test 20 void testInvalidJson() { 21 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.NULL)); 22 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.FALSE)); 23 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.TRUE)); 24 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.EMPTY_JSON_OBJECT)); 25 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(JsonValue.EMPTY_JSON_ARRAY)); 26 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createObjectBuilder().add("bad", "value").build())); 27 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createValue(1))); 28 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createValue(1.0))); 29 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createValue("bad string"))); 30 } 31 32 @Test 33 void testBasicExpressions() { 34 // "filter": [ "==|>=|<=|<|>", "key", "value" ] 35 assertEquals("[key=value]", new Expression(Json.createArrayBuilder().add("==").add("key").add("value").build()).toString()); 36 assertEquals("[key>=true]", new Expression(Json.createArrayBuilder().add(">=").add("key").add(true).build()).toString()); 37 assertEquals("[key<=false]", new Expression(Json.createArrayBuilder().add("<=").add("key").add(false).build()).toString()); 38 assertEquals("[key<1]", new Expression(Json.createArrayBuilder().add("<").add("key").add(1).build()).toString()); 39 assertEquals("[key>2.5]", new Expression(Json.createArrayBuilder().add(">").add("key").add(2.5).build()).toString()); 40 // Test bad expression 41 assertEquals(Expression.EMPTY_EXPRESSION, new Expression(Json.createArrayBuilder().add(">>").add("key").add("value").build())); 42 43 // Test expressions with a subarray and object. This is expected to fail when properly supported, so it should be fixed. 44 assertEquals("[key=[{bad:value}]]", new Expression(Json.createArrayBuilder().add("==").add("key").add( 45 Json.createArrayBuilder().add(Json.createObjectBuilder().add("bad", "value"))).build()).toString()); 46 assertEquals("[key=]", new Expression(Json.createArrayBuilder().add("==").add("key").add(JsonValue.NULL).build()).toString()); 47 } 48 49 @Test 50 void testEquals() { 51 EqualsVerifier.forClass(Expression.class).verify(); 52 } 53 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java
diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/LayersTest.java new file mode 100644 index 000000000..28b09b950
- + 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.assertNull; 6 import static org.junit.jupiter.api.Assertions.assertSame; 7 import static org.junit.jupiter.api.Assertions.assertThrows; 8 9 import java.text.MessageFormat; 10 import java.util.Locale; 11 12 import javax.json.Json; 13 import javax.json.JsonObject; 14 import javax.json.JsonValue; 15 16 import nl.jqno.equalsverifier.EqualsVerifier; 17 import org.junit.jupiter.api.Test; 18 19 /** 20 * Test class for {@link Layers}. 21 * @implNote Tests will fail when support is added for new styling information. 22 * All current (2021-03-31) properties are checked for in some form or another. 23 * @author Taylor Smock 24 * @since xxx 25 */ 26 class LayersTest { 27 @Test 28 void testBackground() { 29 // Test an empty background layer 30 Layers emptyBackgroundLayer = new Layers(Json.createObjectBuilder() 31 .add("type", Layers.Type.BACKGROUND.name()) 32 .add("id", "Empty Background").build()); 33 assertEquals("Empty Background", emptyBackgroundLayer.getId()); 34 assertEquals(Layers.Type.BACKGROUND, emptyBackgroundLayer.getType()); 35 assertNull(emptyBackgroundLayer.getSource()); 36 assertSame(Expression.EMPTY_EXPRESSION, emptyBackgroundLayer.getFilter()); 37 assertEquals("", emptyBackgroundLayer.toString()); 38 39 // Test a background layer with some styling information 40 JsonObject allProperties = Json.createObjectBuilder() 41 .add("background-color", "#fff000") // fill-color:#fff000; 42 .add("background-opacity", 0.5) // No good mapping for JOSM yet 43 .add("background-pattern", "null") // This should be an image, not implemented 44 .build(); 45 Layers backgroundLayer = new Layers(Json.createObjectBuilder() 46 .add("id", "Background layer") 47 .add("type", Layers.Type.BACKGROUND.name().toLowerCase(Locale.ROOT)) 48 .add("paint", allProperties) 49 .build()); 50 assertEquals("canvas{fill-color:#fff000;}", backgroundLayer.toString()); 51 52 // Test a background layer with some styling information, but invisible 53 Layers invisibleBackgroundLayer = new Layers(Json.createObjectBuilder() 54 .add("id", "Background layer") 55 .add("type", Layers.Type.BACKGROUND.name().toLowerCase(Locale.ROOT)) 56 .add("layout", Json.createObjectBuilder().add("visibility", "none").build()) 57 .add("paint", allProperties).build()); 58 assertEquals("", invisibleBackgroundLayer.toString()); 59 } 60 61 @Test 62 void testFill() { 63 // Test a layer without a source (should fail) 64 assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder() 65 .add("type", Layers.Type.FILL.name()) 66 .add("id", "Empty Fill").build())); 67 68 // Test an empty fill layer 69 Layers emptyFillLayer = new Layers(Json.createObjectBuilder() 70 .add("type", Layers.Type.FILL.name()) 71 .add("id", "Empty Fill") 72 .add("source", "Random source").build()); 73 assertEquals("Empty Fill", emptyFillLayer.getId()); 74 assertEquals("Random source", emptyFillLayer.getSource()); 75 assertEquals("", emptyFillLayer.toString()); 76 77 // Test a fully implemented fill layer 78 JsonObject allLayoutProperties = Json.createObjectBuilder() 79 .add("fill-sort-key", 5) 80 .add("visibility", "visible") 81 .build(); 82 JsonObject allPaintProperties = Json.createObjectBuilder() 83 .add("fill-antialias", false) 84 .add("fill-color", "#fff000") // fill-color:#fff000 85 .add("fill-opacity", 0.5) // fill-opacity:0.5 86 .add("fill-outline-color", "#ffff00") // fill-color:#ffff00 (defaults to fill-color) 87 .add("fill-pattern", JsonValue.NULL) // disables fill-outline-color and fill-color 88 .add("fill-translate", Json.createArrayBuilder().add(5).add(5)) 89 .add("fill-translate-anchor", "viewport") // requires fill-translate 90 .build(); 91 92 Layers fullFillLayer = new Layers(Json.createObjectBuilder() 93 .add("type", Layers.Type.FILL.toString()) 94 .add("id", "random-layer-id") 95 .add("source", "Random source") 96 .add("layout", allLayoutProperties) 97 .add("paint", allPaintProperties) 98 .build()); 99 assertEquals("random-layer-id", fullFillLayer.getId()); 100 assertEquals(Layers.Type.FILL, fullFillLayer.getType()); 101 assertEquals("area::random-layer-id{fill-color:#fff000;fill-opacity:0.5;color:#ffff00;}", fullFillLayer.toString()); 102 103 // Test a fully implemented fill layer (invisible) 104 Layers fullFillInvisibleLayer = new Layers(Json.createObjectBuilder() 105 .add("type", Layers.Type.FILL.toString()) 106 .add("id", "random-layer-id") 107 .add("source", "Random source") 108 .add("layout", Json.createObjectBuilder(allLayoutProperties) 109 .add("visibility", "none")) 110 .add("paint", allPaintProperties) 111 .build()); 112 assertEquals("random-layer-id", fullFillInvisibleLayer.getId()); 113 assertEquals(Layers.Type.FILL, fullFillInvisibleLayer.getType()); 114 assertEquals("", fullFillInvisibleLayer.toString()); 115 } 116 117 @Test 118 void testLine() { 119 // Test a layer without a source (should fail) 120 assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder() 121 .add("type", Layers.Type.LINE.name()) 122 .add("id", "Empty Line").build())); 123 124 JsonObject allLayoutProperties = Json.createObjectBuilder() 125 .add("line-cap", "round") // linecap:round; 126 .add("line-join", "bevel") 127 .add("line-miter-limit", 65) 128 .add("line-round-limit", 1.5) 129 .add("line-sort-key", 3) 130 .add("visibility", "visible") 131 .build(); 132 JsonObject allPaintProperties = Json.createObjectBuilder() 133 .add("line-blur", 5) 134 .add("line-color", "#fff000") // color:#fff000; 135 .add("line-dasharray", Json.createArrayBuilder().add(1).add(5).add(1)) // dashes:1,5,1; 136 .add("line-gap-width", 6) 137 .add("line-gradient", "#ffff00") // disabled by line-dasharray/line-pattern, source must be "geojson" 138 .add("line-offset", 12) 139 .add("line-opacity", 0.5) // opacity:0.5; 140 .add("line-pattern", JsonValue.NULL) 141 .add("line-translate", Json.createArrayBuilder().add(-1).add(-2)) 142 .add("line-translate-anchor", "viewport") 143 .add("line-width", 22) // width:22; 144 .build(); 145 146 // Test fully defined line 147 Layers fullLineLayer = new Layers(Json.createObjectBuilder() 148 .add("type", Layers.Type.LINE.name().toLowerCase(Locale.ROOT)) 149 .add("id", "random-layer-id") 150 .add("source", "Random source") 151 .add("layout", allLayoutProperties) 152 .add("paint", allPaintProperties) 153 .build()); 154 assertEquals("random-layer-id", fullLineLayer.getId()); 155 assertEquals(Layers.Type.LINE, fullLineLayer.getType()); 156 assertEquals("way::random-layer-id{color:#fff000;opacity:0.5;linecap:round;dashes:1,5,1;width:22;}", fullLineLayer.toString()); 157 158 // Test invisible line 159 Layers fullLineInvisibleLayer = new Layers(Json.createObjectBuilder() 160 .add("type", Layers.Type.LINE.name().toLowerCase(Locale.ROOT)) 161 .add("id", "random-layer-id") 162 .add("source", "Random source") 163 .add("layout", Json.createObjectBuilder(allLayoutProperties) 164 .add("visibility", "none")) 165 .add("paint", allPaintProperties) 166 .build()); 167 assertEquals("random-layer-id", fullLineInvisibleLayer.getId()); 168 assertEquals(Layers.Type.LINE, fullLineInvisibleLayer.getType()); 169 assertEquals("", fullLineInvisibleLayer.toString()); 170 } 171 172 @Test 173 void testSymbol() { 174 // Test a layer without a source (should fail) 175 assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder() 176 .add("type", Layers.Type.SYMBOL.name()) 177 .add("id", "Empty Symbol").build())); 178 179 JsonObject allPaintProperties = Json.createObjectBuilder() 180 .add("icon-color", "#fff000") // also requires sdf icons 181 .add("icon-halo-blur", 5) 182 .add("icon-halo-color", "#ffff00") 183 .add("icon-halo-width", 6) 184 .add("icon-opacity", 0.5) // icon-opacity:0.5; 185 .add("icon-translate", Json.createArrayBuilder().add(11).add(12)) 186 .add("icon-translate-anchor", "viewport") // also requires icon-translate 187 .add("text-color", "#fffff0") // text-color:#fffff0; 188 .add("text-halo-blur", 15) 189 .add("text-halo-color", "#ffffff") // text-halo-color:#ffffff; 190 .add("text-halo-width", 16) // text-halo-radius:16; 191 .add("text-opacity", 0.6) // text-opacity:0.6; 192 .add("text-translate", Json.createArrayBuilder().add(26).add(27)) 193 .add("text-translate-anchor", "viewport") 194 .build(); 195 JsonObject allLayoutProperties = Json.createObjectBuilder() 196 .add("icon-allow-overlap", true) 197 .add("icon-anchor", "left") 198 .add("icon-ignore-placement", true) 199 .add("icon-image", "random-image") // icon-image:concat(\"random-image\"); 200 .add("icon-keep-upright", true) // also requires icon-rotation-alignment=map and symbol-placement=line|line-center 201 .add("icon-offset", Json.createArrayBuilder().add(2).add(3)) // icon-offset-x:2.0;icon-offset-y:3.0; 202 .add("icon-optional", true) // also requires text-field 203 .add("icon-padding", 4) 204 .add("icon-pitch-alignment", "viewport") 205 .add("icon-rotate", 30) // icon-rotation:30.0; 206 .add("icon-rotation-alignment", "map") 207 .add("icon-size", 2) 208 .add("icon-text-fit", "width") // also requires text-field 209 .add("icon-text-fit-padding", Json.createArrayBuilder().add(7).add(8).add(9).add(10)) 210 .add("symbol-avoid-edges", true) 211 .add("symbol-placement", "line") 212 .add("symbol-sort-key", 13) 213 .add("symbol-spacing", 14) // requires symbol-placement=line 214 .add("symbol-z-order", "source") 215 .add("text-allow-overlap", true) // requires text-field 216 .add("text-anchor", "left") // requires text-field, disabled by text-variable-anchor 217 .add("text-field", "something") // text:something; 218 .add("text-font", Json.createArrayBuilder().add("SansSerif")) // DroidSans isn't always available in an IDE 219 .add("text-ignore-placement", true) 220 .add("text-justify", "left") 221 .add("text-keep-upright", false) 222 .add("text-letter-spacing", 17) 223 .add("text-line-height", 1.3) 224 .add("text-max-angle", 18) 225 .add("text-max-width", 19) 226 .add("text-offset", Json.createArrayBuilder().add(20).add(21)) 227 .add("text-optional", true) 228 .add("text-padding", 22) 229 .add("text-pitch-alignment", "viewport") 230 .add("text-radial-offset", 23) 231 .add("text-rotate", 24) 232 .add("text-rotation-alignment", "viewport") 233 .add("text-size", 25) // font-size:25; 234 .add("text-transform", "uppercase") 235 .add("text-variable-anchor", "left") 236 .add("text-writing-mode", "vertical") 237 .add("visibility", "visible").build(); 238 239 // Test fully defined symbol 240 Layers fullLineLayer = new Layers(Json.createObjectBuilder() 241 .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT)) 242 .add("id", "random-layer-id") 243 .add("source", "Random source") 244 .add("layout", allLayoutProperties) 245 .add("paint", allPaintProperties) 246 .build()); 247 assertEquals("random-layer-id", fullLineLayer.getId()); 248 assertEquals(Layers.Type.SYMBOL, fullLineLayer.getType()); 249 assertEquals("node::random-layer-id{icon-image:concat(\"random-image\");icon-offset-x:2.0;icon-offset-y:3.0;" 250 + "icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";font-weight:normal;" 251 + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}", fullLineLayer.toString()); 252 253 // Test an invisible symbol 254 Layers fullLineInvisibleLayer = new Layers(Json.createObjectBuilder() 255 .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT)) 256 .add("id", "random-layer-id") 257 .add("source", "Random source") 258 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none")) 259 .add("paint", allPaintProperties) 260 .build()); 261 assertEquals("random-layer-id", fullLineInvisibleLayer.getId()); 262 assertEquals(Layers.Type.SYMBOL, fullLineInvisibleLayer.getType()); 263 assertEquals("", fullLineInvisibleLayer.toString()); 264 265 // Test with placeholders in icon-image 266 Layers fullOneIconImagePlaceholderLineLayer = new Layers(Json.createObjectBuilder() 267 .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT)) 268 .add("id", "random-layer-id") 269 .add("source", "Random source") 270 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("icon-image", "{value}")) 271 .add("paint", allPaintProperties) 272 .build()); 273 assertEquals("node::random-layer-id{icon-image:concat(tag(\"value\"));icon-offset-x:2.0;icon-offset-y:3.0;" 274 + "icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";font-weight:normal;" 275 + "font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}", 276 fullOneIconImagePlaceholderLineLayer.toString()); 277 278 // Test with placeholders in icon-image 279 Layers fullOneIconImagePlaceholderExtraLineLayer = new Layers(Json.createObjectBuilder() 280 .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT)) 281 .add("id", "random-layer-id") 282 .add("source", "Random source") 283 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("icon-image", "something/{value}/random")) 284 .add("paint", allPaintProperties) 285 .build()); 286 assertEquals("node::random-layer-id{icon-image:concat(\"something/\",tag(\"value\"),\"/random\");icon-offset-x:2.0;" 287 + "icon-offset-y:3.0;icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;font-family:\"SansSerif\";" 288 + "font-weight:normal;font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;text-opacity:0.6;font-size:25;}", 289 fullOneIconImagePlaceholderExtraLineLayer.toString()); 290 291 // Test with placeholders in icon-image 292 Layers fullTwoIconImagePlaceholderExtraLineLayer = new Layers(Json.createObjectBuilder() 293 .add("type", Layers.Type.SYMBOL.name().toLowerCase(Locale.ROOT)) 294 .add("id", "random-layer-id") 295 .add("source", "Random source") 296 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("icon-image", "something/{value}/random/{value2}")) 297 .add("paint", allPaintProperties) 298 .build()); 299 assertEquals("node::random-layer-id{icon-image:concat(\"something/\",tag(\"value\"),\"/random/\",tag(\"value2\"));" 300 + "icon-offset-x:2.0;icon-offset-y:3.0;icon-opacity:0.5;icon-rotation:30.0;text-color:#fffff0;text:something;" 301 + "font-family:\"SansSerif\";font-weight:normal;font-style:normal;text-halo-color:#ffffff;text-halo-radius:16;" 302 + "text-opacity:0.6;font-size:25;}", fullTwoIconImagePlaceholderExtraLineLayer.toString()); 303 } 304 305 @Test 306 void testRaster() { 307 // Test a layer without a source (should fail) 308 assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder() 309 .add("type", Layers.Type.RASTER.name()) 310 .add("id", "Empty Raster").build())); 311 312 JsonObject allPaintProperties = Json.createObjectBuilder() 313 .add("raster-brightness-max", 0.5) 314 .add("raster-brightness-min", 0.6) 315 .add("raster-contrast", 0.7) 316 .add("raster-fade-duration", 1) 317 .add("raster-hue-rotate", 2) 318 .add("raster-opacity", 0.7) 319 .add("raster-resampling", "nearest") 320 .add("raster-saturation", 0.8) 321 .build(); 322 JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build(); 323 324 // Test fully defined raster 325 Layers fullRaster = new Layers(Json.createObjectBuilder() 326 .add("id", "test-raster") 327 .add("type", Layers.Type.RASTER.name().toLowerCase(Locale.ROOT)) 328 .add("source", "Random source") 329 .add("layout", allLayoutProperties) 330 .add("paint", allPaintProperties) 331 .build()); 332 assertEquals(Layers.Type.RASTER, fullRaster.getType()); 333 assertEquals("test-raster", fullRaster.getId()); 334 assertEquals("Random source", fullRaster.getSource()); 335 assertEquals("", fullRaster.toString()); 336 337 // Test fully defined invisible raster 338 Layers fullInvisibleRaster = new Layers(Json.createObjectBuilder() 339 .add("id", "test-raster") 340 .add("type", Layers.Type.RASTER.name().toLowerCase(Locale.ROOT)) 341 .add("source", "Random source") 342 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none")) 343 .add("paint", allPaintProperties) 344 .build()); 345 assertEquals("", fullInvisibleRaster.toString()); 346 } 347 348 @Test 349 void testCircle() { 350 // Test a layer without a source (should fail) 351 assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder() 352 .add("type", Layers.Type.CIRCLE.name()) 353 .add("id", "Empty Circle").build())); 354 355 JsonObject allPaintProperties = Json.createObjectBuilder() 356 .add("circle-blur", 1) 357 .add("circle-color", "#fff000") // symbol-fill-color:#fff000; 358 .add("circle-opacity", 0.5) // symbol-fill-opacity:0.5; 359 .add("circle-pitch-alignment", "map") 360 .add("circle-pitch-scale", "viewport") 361 .add("circle-radius", 2) // symbol-size:4.0; (we use width) 362 .add("circle-stroke-color", "#ffff00") // symbol-stroke-color:#ffff00; 363 .add("circle-stroke-opacity", 0.6) // symbol-stroke-opacity:0.6; 364 .add("circle-stroke-width", 5) // symbol-stroke-width:5.0; 365 .add("circle-translate", Json.createArrayBuilder().add(3).add(4)) 366 .add("circle-translate-anchor", "viewport") 367 .build(); 368 JsonObject allLayoutProperties = Json.createObjectBuilder() 369 .add("circle-sort-key", 3) 370 .add("visibility", "visible") 371 .build(); 372 373 Layers fullCircleLayer = new Layers(Json.createObjectBuilder() 374 .add("id", "Full circle layer") 375 .add("type", Layers.Type.CIRCLE.name().toLowerCase(Locale.ROOT)) 376 .add("source", "Random source") 377 .add("layout", allLayoutProperties) 378 .add("paint", allPaintProperties) 379 .build()); 380 assertEquals(Layers.Type.CIRCLE, fullCircleLayer.getType()); 381 assertEquals("Full circle layer", fullCircleLayer.getId()); 382 assertEquals("Random source", fullCircleLayer.getSource()); 383 assertEquals("node::Full circle layer{symbol-shape:circle;symbol-fill-color:#fff000;symbol-fill-opacity:0.5;" 384 + "symbol-size:4.0;symbol-stroke-color:#ffff00;symbol-stroke-opacity:0.6;symbol-stroke-width:5;}", fullCircleLayer.toString()); 385 386 Layers fullCircleInvisibleLayer = new Layers(Json.createObjectBuilder() 387 .add("id", "Full circle layer") 388 .add("type", Layers.Type.CIRCLE.name().toLowerCase(Locale.ROOT)) 389 .add("source", "Random source") 390 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none")) 391 .add("paint", allPaintProperties) 392 .build()); 393 assertEquals(Layers.Type.CIRCLE, fullCircleInvisibleLayer.getType()); 394 assertEquals("Full circle layer", fullCircleInvisibleLayer.getId()); 395 assertEquals("Random source", fullCircleInvisibleLayer.getSource()); 396 assertEquals("", fullCircleInvisibleLayer.toString()); 397 } 398 399 @Test 400 void testFillExtrusion() { 401 // Test a layer without a source (should fail) 402 assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder() 403 .add("type", Layers.Type.FILL_EXTRUSION.name()) 404 .add("id", "Empty Fill Extrusion").build())); 405 406 JsonObject allPaintProperties = Json.createObjectBuilder() 407 .add("fill-extrusion-base", 1) 408 .add("fill-extrusion-color", "#fff000") 409 .add("fill-extrusion-height", 2) 410 .add("fill-extrusion-opacity", 0.5) 411 .add("fill-extrusion-pattern", "something-random") 412 .add("fill-extrusion-translate", Json.createArrayBuilder().add(3).add(4)) 413 .add("fill-extrusion-translate-anchor", "viewport") 414 .add("fill-extrusion-vertical-gradient", false) 415 .build(); 416 JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build(); 417 418 Layers fullFillLayer = new Layers(Json.createObjectBuilder() 419 .add("id", "Fill Extrusion") 420 .add("type", Layers.Type.FILL_EXTRUSION.name().toLowerCase(Locale.ROOT)) 421 .add("source", "Random source") 422 .add("layout", allLayoutProperties) 423 .add("paint", allPaintProperties) 424 .build()); 425 assertEquals("", fullFillLayer.toString()); 426 assertEquals(Layers.Type.FILL_EXTRUSION, fullFillLayer.getType()); 427 Layers fullFillInvisibleLayer = new Layers(Json.createObjectBuilder() 428 .add("id", "Fill Extrusion") 429 .add("type", Layers.Type.FILL_EXTRUSION.name().toLowerCase(Locale.ROOT)) 430 .add("source", "Random source") 431 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none")) 432 .add("paint", allPaintProperties) 433 .build()); 434 assertEquals("", fullFillInvisibleLayer.toString()); 435 assertEquals(Layers.Type.FILL_EXTRUSION, fullFillInvisibleLayer.getType()); 436 } 437 438 @Test 439 void testHeatmap() { 440 // Test a layer without a source (should fail) 441 assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder() 442 .add("type", Layers.Type.HEATMAP.name()) 443 .add("id", "Empty Heatmap").build())); 444 445 JsonObject allPaintProperties = Json.createObjectBuilder() 446 .add("heatmap-color", "#fff000") // This will probably be a gradient of some type 447 .add("heatmap-intensity", 0.5) 448 .add("heatmap-opacity", 0.6) 449 .add("heatmap-radius", 1) // This is in pixels 450 .add("heatmap-weight", 0.7) 451 .build(); 452 JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build(); 453 454 Layers fullHeatmapLayer = new Layers(Json.createObjectBuilder() 455 .add("id", "Full heatmap") 456 .add("type", Layers.Type.HEATMAP.name().toLowerCase(Locale.ROOT)) 457 .add("source", "Random source") 458 .add("paint", allPaintProperties) 459 .add("layout", allLayoutProperties) 460 .build()); 461 assertEquals(Layers.Type.HEATMAP, fullHeatmapLayer.getType()); 462 assertEquals("", fullHeatmapLayer.toString()); 463 464 Layers fullHeatmapInvisibleLayer = new Layers(Json.createObjectBuilder() 465 .add("id", "Full heatmap") 466 .add("type", Layers.Type.HEATMAP.name().toLowerCase(Locale.ROOT)) 467 .add("source", "Random source") 468 .add("paint", allPaintProperties) 469 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none")) 470 .build()); 471 assertEquals(Layers.Type.HEATMAP, fullHeatmapInvisibleLayer.getType()); 472 assertEquals("", fullHeatmapInvisibleLayer.toString()); 473 } 474 475 @Test 476 void testHillshade() { 477 // Test a layer without a source (should fail) 478 assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder() 479 .add("type", Layers.Type.HILLSHADE.name()) 480 .add("id", "Empty Hillshade").build())); 481 482 JsonObject allPaintProperties = Json.createObjectBuilder() 483 .add("hillshade-accent-color", "#fff000") 484 .add("hillshade-exaggeration", 0.6) 485 .add("hillshade-highlight-color", "#ffff00") 486 .add("hillshade-illumination-anchor", "map") 487 .add("hillshade-illumination-direction", 90) 488 .add("hillshade-shadow-color", "#fffff0") 489 .build(); 490 JsonObject allLayoutProperties = Json.createObjectBuilder() 491 .add("visibility", "visible") 492 .build(); 493 494 Layers fullHillshadeLayer = new Layers(Json.createObjectBuilder() 495 .add("id", "Hillshade") 496 .add("type", Layers.Type.HILLSHADE.toString().toLowerCase(Locale.ROOT)) 497 .add("source", "Random source") 498 .add("paint", allPaintProperties) 499 .add("layout", allLayoutProperties) 500 .build()); 501 assertEquals(Layers.Type.HILLSHADE, fullHillshadeLayer.getType()); 502 assertEquals("", fullHillshadeLayer.toString()); 503 504 Layers fullHillshadeInvisibleLayer = new Layers(Json.createObjectBuilder() 505 .add("id", "Hillshade") 506 .add("type", Layers.Type.HILLSHADE.toString().toLowerCase(Locale.ROOT)) 507 .add("source", "Random source") 508 .add("paint", allPaintProperties) 509 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none")) 510 .build()); 511 assertEquals(Layers.Type.HILLSHADE, fullHillshadeInvisibleLayer.getType()); 512 assertEquals("", fullHillshadeInvisibleLayer.toString()); 513 } 514 515 @Test 516 void testSky() { 517 // Test a layer without a source (should fail) 518 assertThrows(NullPointerException.class, () -> new Layers(Json.createObjectBuilder() 519 .add("type", Layers.Type.SKY.name()) 520 .add("id", "Empty Sky").build())); 521 522 JsonObject allPaintProperties = Json.createObjectBuilder() 523 .add("sky-atmosphere-color", "red") 524 .add("sky-atmosphere-halo-color", "yellow") 525 // 360180 is apparently included in this? Or it might be a formatting issue in the docs. 526 .add("sky-atmosphere-sun", Json.createArrayBuilder().add(0, 360180)) 527 .add("sky-atmosphere-sun-intensity", 99) 528 .add("sky-gradient", "#fff000") 529 .add("sky-gradient-center", Json.createArrayBuilder().add(0).add(360180)) // see note on 360180 above 530 .add("sky-gradient-radius", 1) 531 .add("sky-opacity", 0.5) 532 .add("sky-type", "gradient") 533 .build(); 534 JsonObject allLayoutProperties = Json.createObjectBuilder().add("visibility", "visible").build(); 535 536 Layers fullSkyLayer = new Layers(Json.createObjectBuilder() 537 .add("id", "Sky") 538 .add("type", Layers.Type.SKY.name().toLowerCase(Locale.ROOT)) 539 .add("source", "Random source") 540 .add("paint", allPaintProperties) 541 .add("layout", allLayoutProperties) 542 .build()); 543 assertEquals(Layers.Type.SKY, fullSkyLayer.getType()); 544 assertEquals("", fullSkyLayer.toString()); 545 546 Layers fullSkyInvisibleLayer = new Layers(Json.createObjectBuilder() 547 .add("id", "Sky") 548 .add("type", Layers.Type.SKY.name().toLowerCase(Locale.ROOT)) 549 .add("source", "Random source") 550 .add("paint", allPaintProperties) 551 .add("layout", Json.createObjectBuilder(allLayoutProperties).add("visibility", "none")) 552 .build()); 553 assertEquals(Layers.Type.SKY, fullSkyInvisibleLayer.getType()); 554 assertEquals("", fullSkyInvisibleLayer.toString()); 555 } 556 557 @Test 558 void testZoomLevels() { 559 JsonObject baseInformation = Json.createObjectBuilder() 560 .add("id", "dots") 561 .add("type", "CiRcLe") 562 .add("source", "osm-source") 563 .add("source-layer", "osm-images") 564 .add("paint", Json.createObjectBuilder() 565 .add("circle-color", "#fff000") 566 .add("circle-radius", 6) 567 ).build(); 568 Layers noZoomLayer = new Layers(baseInformation); 569 String baseString = "node{0}::dots'{symbol-shape:circle;symbol-fill-color:#fff000;symbol-fill-opacity:1;" 570 + "symbol-size:12.0;symbol-stroke-color:#000000;symbol-stroke-opacity:1;symbol-stroke-width:0;}'"; 571 assertEquals("osm-images", noZoomLayer.getSourceLayer()); 572 assertEquals(MessageFormat.format(baseString, ""), noZoomLayer.toString()); 573 574 Layers minZoomLayer = new Layers(Json.createObjectBuilder(baseInformation) 575 .add("minzoom", 0) 576 .build()); 577 assertEquals(MessageFormat.format(baseString, "|z0-"), minZoomLayer.toString()); 578 579 Layers maxZoomLayer = new Layers(Json.createObjectBuilder(baseInformation) 580 .add("maxzoom", 24) 581 .build()); 582 assertEquals(MessageFormat.format(baseString, "|z-24"), maxZoomLayer.toString()); 583 584 Layers minMaxZoomLayer = new Layers(Json.createObjectBuilder(baseInformation) 585 .add("minzoom", 1) 586 .add("maxzoom", 2) 587 .build()); 588 assertEquals(MessageFormat.format(baseString, "|z1-2"), minMaxZoomLayer.toString()); 589 590 Layers sameMinMaxZoomLayer = new Layers(Json.createObjectBuilder(baseInformation) 591 .add("minzoom", 2) 592 .add("maxzoom", 2) 593 .build()); 594 assertEquals(MessageFormat.format(baseString, "|z2"), sameMinMaxZoomLayer.toString()); 595 } 596 597 @Test 598 void testEquals() { 599 EqualsVerifier.forClass(Layers.class).usingGetClass().verify(); 600 } 601 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java
diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapBoxVectorStyleTest.java new file mode 100644 index 000000000..1fcb7bfe8
- + 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.assertDoesNotThrow; 5 import static org.junit.jupiter.api.Assertions.assertEquals; 6 import static org.junit.jupiter.api.Assertions.assertFalse; 7 import static org.junit.jupiter.api.Assertions.assertNotNull; 8 import static org.junit.jupiter.api.Assertions.assertThrows; 9 import static org.junit.jupiter.api.Assertions.assertTrue; 10 import static org.junit.jupiter.api.Assertions.fail; 11 12 13 import java.awt.Color; 14 import java.awt.Graphics2D; 15 import java.awt.image.BufferedImage; 16 import java.io.ByteArrayInputStream; 17 import java.io.File; 18 import java.io.FileOutputStream; 19 import java.io.IOException; 20 import java.nio.charset.StandardCharsets; 21 import java.nio.file.Paths; 22 import java.text.MessageFormat; 23 import java.util.ArrayList; 24 import java.util.Collection; 25 import java.util.Map; 26 import java.util.Objects; 27 import java.util.Optional; 28 import java.util.concurrent.atomic.AtomicBoolean; 29 import java.util.stream.Collectors; 30 31 import javax.imageio.ImageIO; 32 import javax.json.Json; 33 import javax.json.JsonObject; 34 import javax.json.JsonObjectBuilder; 35 import javax.json.JsonReader; 36 import javax.json.JsonStructure; 37 import javax.json.JsonValue; 38 39 import org.openstreetmap.josm.TestUtils; 40 import org.openstreetmap.josm.gui.MainApplication; 41 import org.openstreetmap.josm.gui.mappaint.ElemStyles; 42 import org.openstreetmap.josm.gui.mappaint.Keyword; 43 import org.openstreetmap.josm.gui.mappaint.StyleSource; 44 import org.openstreetmap.josm.gui.mappaint.mapcss.Instruction; 45 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSRule; 46 import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; 47 import org.openstreetmap.josm.testutils.JOSMTestRules; 48 import org.openstreetmap.josm.tools.ColorHelper; 49 import org.openstreetmap.josm.tools.ImageProvider; 50 51 import nl.jqno.equalsverifier.EqualsVerifier; 52 import org.awaitility.Awaitility; 53 import org.awaitility.Durations; 54 import org.junit.jupiter.api.Test; 55 import org.junit.jupiter.api.extension.RegisterExtension; 56 import org.junit.jupiter.api.io.TempDir; 57 58 /** 59 * Test class for {@link MapBoxVectorStyle} 60 * @author Taylor Smock 61 */ 62 public class MapBoxVectorStyleTest { 63 /** Used to store sprite files (specifically, sprite{,@2x}.{png,json}) */ 64 @TempDir 65 File spritesDirectory; 66 67 // Needed for osm primitives (we really just need to initialize the config) 68 // OSM primitives are called when we load style sources 69 @RegisterExtension 70 JOSMTestRules rules = new JOSMTestRules(); 71 72 /** The base information */ 73 private static final String BASE_STYLE = "'{'\"version\":8,\"name\":\"test style\",\"owner\":\"josm test\",\"id\":\"{0}\",{1}'}'"; 74 /** Source 1 */ 75 private static final String SOURCE1 = "\"source1\":{\"type\":\"vector\",\"tiles\":[\"https://example.org/{z}/{x}/{y}.mvt\"]}"; 76 /** Layer 1 */ 77 private static final String LAYER1 = "{\"id\":\"layer1\",\"type\":\"circle\",\"source\":\"source1\",\"source-layer\":\"nodes\"}"; 78 /** Source 2 */ 79 private static final String SOURCE2 = "\"source2\":{\"type\":\"vector\",\"tiles\":[\"https://example.org/{z}2/{x}/{y}.mvt\"]}"; 80 /** Layer 2 */ 81 private static final String LAYER2 = "{\"id\":\"layer2\",\"type\":\"circle\",\"source\":\"source2\",\"source-layer\":\"nodes\"}"; 82 83 /** 84 * Check that the version matches the supported style version(s). Currently, only version 8 exists and is (partially) 85 * supported. 86 */ 87 @Test 88 void testVersionChecks() { 89 assertThrows(NullPointerException.class, () -> new MapBoxVectorStyle(JsonValue.EMPTY_JSON_OBJECT)); 90 IllegalArgumentException badVersion = assertThrows(IllegalArgumentException.class, 91 () -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 7).build())); 92 assertEquals("Vector Tile Style Version not understood: version 7 (json: {\"version\":7})", badVersion.getMessage()); 93 badVersion = assertThrows(IllegalArgumentException.class, 94 () -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 9).build())); 95 assertEquals("Vector Tile Style Version not understood: version 9 (json: {\"version\":9})", badVersion.getMessage()); 96 assertDoesNotThrow(() -> new MapBoxVectorStyle(Json.createObjectBuilder().add("version", 8).build())); 97 } 98 99 @Test 100 void testSources() { 101 // Check with an invalid sources list 102 assertTrue(new MapBoxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty()); 103 Map<Source, ElemStyles> sources = new MapBoxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test", 104 MessageFormat.format("\"sources\":'{'{0},{1},\"source3\":[\"bad source\"]'}',\"layers\":[{2},{3},{4}]", 105 SOURCE1, SOURCE2, LAYER1, LAYER2, LAYER2.replace('2', '3'))))).getSources(); 106 assertEquals(3, sources.size()); 107 assertTrue(sources.containsKey(null)); // This is due to there being no source3 layer 108 sources.remove(null); // Avoid null checks later 109 assertTrue(sources.keySet().stream().map(Source::getName).anyMatch("source1"::equals)); 110 assertTrue(sources.keySet().stream().map(Source::getName).anyMatch("source2"::equals)); 111 assertTrue(sources.keySet().stream().map(Source::getName).noneMatch("source3"::equals)); 112 } 113 114 @Test 115 void testSavedFiles() { 116 assertTrue(new MapBoxVectorStyle(getJson(JsonObject.class, "{\"version\":8,\"sources\":[\"s1\",\"s2\"]}")).getSources().isEmpty()); 117 Map<Source, ElemStyles> sources = new MapBoxVectorStyle(getJson(JsonObject.class, MessageFormat.format(BASE_STYLE, "test", 118 MessageFormat.format("\"sources\":'{'{0},{1}'}',\"layers\":[{2},{3}]", SOURCE1, SOURCE2, LAYER1, LAYER2)))).getSources(); 119 assertEquals(2, sources.size()); 120 // For various reasons, the map _must_ be reliably ordered in the order of encounter 121 Source source1 = sources.keySet().iterator().next(); 122 Source source2 = sources.keySet().stream().skip(1).findFirst().orElseGet(() -> fail("No second source")); 123 assertEquals("source1", source1.getName()); 124 assertEquals("source2", source2.getName()); 125 126 // Check that the files have been saved. Ideally, we would check that they haven't been 127 // saved earlier, since this is in a different thread. Unfortunately, that is a _race condition_. 128 MapCSSStyleSource styleSource1 = (MapCSSStyleSource) sources.get(source1).getStyleSources().get(0); 129 MapCSSStyleSource styleSource2 = (MapCSSStyleSource) sources.get(source2).getStyleSources().get(0); 130 131 AtomicBoolean saveFinished = new AtomicBoolean(); 132 MainApplication.worker.execute(() -> saveFinished.set(true)); 133 Awaitility.await().atMost(Durations.ONE_SECOND).until(saveFinished::get); 134 135 assertTrue(styleSource1.url.endsWith("source1.mapcss")); 136 assertTrue(styleSource2.url.endsWith("source2.mapcss")); 137 138 MapCSSStyleSource mapCSSStyleSource1 = new MapCSSStyleSource(styleSource1.url, styleSource1.name, styleSource1.title); 139 MapCSSStyleSource mapCSSStyleSource2 = new MapCSSStyleSource(styleSource2.url, styleSource2.name, styleSource2.title); 140 141 assertEquals(styleSource1, mapCSSStyleSource1); 142 assertEquals(styleSource2, mapCSSStyleSource2); 143 } 144 145 @Test 146 void testSprites() throws IOException { 147 generateSprites(false); 148 // Ensure that we fall back to 1x sprites 149 assertTrue(new File(this.spritesDirectory, "sprite.png").exists()); 150 assertFalse(new File(this.spritesDirectory, "sprite@2x.png").exists()); 151 assertTrue(new File(this.spritesDirectory, "sprite.json").exists()); 152 assertFalse(new File(this.spritesDirectory, "sprite@2x.json").exists()); 153 154 checkImages(false); 155 156 generateSprites(true); 157 checkImages(true); 158 } 159 160 private void checkImages(boolean hiDpi) { 161 // Ensure that we don't have images saved in the ImageProvider cache 162 ImageProvider.clearCache(); 163 int hiDpiScalar = hiDpi ? 2 : 1; 164 String spritePath = new File(this.spritesDirectory, "sprite").getPath(); 165 MapBoxVectorStyle style = new MapBoxVectorStyle(getJson(JsonObject.class, 166 MessageFormat.format(BASE_STYLE, "sprite_test", "\"sprite\":\"file:/" + spritePath + "\""))); 167 assertEquals("file:/" + spritePath, style.getSpriteUrl()); 168 169 AtomicBoolean saveFinished = new AtomicBoolean(); 170 MainApplication.worker.execute(() -> saveFinished.set(true)); 171 Awaitility.await().atMost(Durations.ONE_SECOND).until(saveFinished::get); 172 173 int scalar = 28; // 255 / 9 (could be 4, but this was a nicer number) 174 for (int x = 0; x < 3; x++) { 175 for (int y = 0; y < 3; y++) { 176 // Expected color 177 Color color = new Color(scalar * x, scalar * y, scalar * x * y); 178 int finalX = x; 179 int finalY = y; 180 BufferedImage image = (BufferedImage) assertDoesNotThrow( 181 () -> ImageProvider.get(new File("test style", MessageFormat.format("({0},{1})", finalX, finalY)).getPath())) 182 .getImage(); 183 assertEquals(3 * hiDpiScalar, image.getWidth(null)); 184 assertEquals(3 * hiDpiScalar, image.getHeight(null)); 185 for (int x2 = 0; x2 < image.getWidth(null); x2++) { 186 for (int y2 = 0; y2 < image.getHeight(null); y2++) { 187 assertEquals(color.getRGB(), image.getRGB(x2, y2)); 188 } 189 } 190 } 191 } 192 } 193 194 private void generateSprites(boolean hiDpi) throws IOException { 195 // Create a 3x3 grid of 3x3 or 6x6 pixel squares (depends upon the dpi setting) 196 int hiDpiScale = hiDpi ? 2 : 1; 197 BufferedImage nineByNine = new BufferedImage(hiDpiScale * 9, hiDpiScale * 9, BufferedImage.TYPE_4BYTE_ABGR); 198 int scalar = 28; // 255 / 9 (could be 4, but this was a nicer number) 199 Graphics2D g = nineByNine.createGraphics(); 200 JsonObjectBuilder json = Json.createObjectBuilder(); 201 for (int x = 0; x < 3; x++) { 202 for (int y = 0; y < 3; y++) { 203 Color color = new Color(scalar * x, scalar * y, scalar * x * y); 204 g.setColor(color); 205 g.drawRect(3 * hiDpiScale * x, 3 * hiDpiScale * y, 3 * hiDpiScale, 3 * hiDpiScale); 206 g.fillRect(3 * hiDpiScale * x, 3 * hiDpiScale * y, 3 * hiDpiScale, 3 * hiDpiScale); 207 208 JsonObjectBuilder sprite = Json.createObjectBuilder(); 209 sprite.add("height", hiDpiScale * 3); 210 sprite.add("pixelRatio", hiDpiScale); 211 sprite.add("width", hiDpiScale * 3); 212 sprite.add("x", 3 * hiDpiScale * x); 213 sprite.add("y", 3 * hiDpiScale * y); 214 215 json.add(MessageFormat.format("({0},{1})", x, y), sprite); 216 } 217 } 218 String imageName = hiDpi ? "sprite@2x.png" : "sprite.png"; 219 ImageIO.write(nineByNine, "png", new File(this.spritesDirectory, imageName)); 220 String jsonName = hiDpi ? "sprite@2x.json" : "sprite.json"; 221 File jsonFile = new File(this.spritesDirectory, jsonName); 222 try (FileOutputStream fileOutputStream = new FileOutputStream(jsonFile)) { 223 fileOutputStream.write(json.build().toString().getBytes(StandardCharsets.UTF_8)); 224 } 225 } 226 227 private static <T extends JsonStructure> T getJson(Class<T> clazz, String json) { 228 try (JsonReader reader = Json.createReader(new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)))) { 229 JsonStructure structure = reader.read(); 230 if (clazz.isAssignableFrom(structure.getClass())) { 231 return clazz.cast(structure); 232 } 233 } 234 fail("Could not cast to expected class"); 235 throw new IllegalArgumentException(); 236 } 237 238 @Test 239 void testMapillaryStyle() { 240 final String file = Paths.get("file:", TestUtils.getTestDataRoot(), "mapillary.json").toString(); 241 final MapBoxVectorStyle style = MapBoxVectorStyle.getMapBoxVectorStyle(file); 242 assertNotNull(style); 243 // There are three "sources" in the mapillary.json file 244 assertEquals(3, style.getSources().size()); 245 final ElemStyles mapillarySource = style.getSources().entrySet().stream() 246 .filter(source -> "mapillary-source".equals(source.getKey().getName())).map( 247 Map.Entry::getValue).findAny().orElse(null); 248 assertNotNull(mapillarySource); 249 mapillarySource.getStyleSources().forEach(StyleSource::loadStyleSource); 250 assertEquals(1, mapillarySource.getStyleSources().size()); 251 final MapCSSStyleSource mapillaryCssSource = (MapCSSStyleSource) mapillarySource.getStyleSources().get(0); 252 assertTrue(mapillaryCssSource.getErrors().isEmpty()); 253 final MapCSSRule mapillaryOverview = getRule(mapillaryCssSource, "node", "mapillary-overview"); 254 assertNotNull(mapillaryOverview); 255 assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-shape", new Keyword("circle")); 256 assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-color", ColorHelper.html2color("#05CB63")); 257 assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-fill-opacity", 0.6f); 258 // Docs indicate that symbol-size is total width, while we are translating from a radius. So 2 * 4 = 8. 259 assertInInstructions(mapillaryOverview.declaration.instructions, "symbol-size", 8.0f); 260 } 261 262 @Test 263 void testEqualsContract() { 264 // We need to "load" the style sources to avoid the verifier from thinking they are equal 265 StyleSource canvas = new MapCSSStyleSource("meta{title:\"canvas\";}canvas{default-points:false;}"); 266 StyleSource node = new MapCSSStyleSource("meta{title:\"node\";}node{text:ref;}"); 267 node.loadStyleSource(); 268 canvas.loadStyleSource(); 269 EqualsVerifier.forClass(MapBoxVectorStyle.class) 270 .withPrefabValues(ImageProvider.class, new ImageProvider("cancel"), new ImageProvider("ok")) 271 .withPrefabValues(StyleSource.class, canvas, node) 272 .usingGetClass().verify(); 273 } 274 275 /** 276 * Check that an instruction is in a collection of instructions, and return it 277 * @param instructions The instructions to search 278 * @param key The key to look for 279 * @param value The expected value for the key 280 */ 281 private void assertInInstructions(Collection<Instruction> instructions, String key, Object value) { 282 // In JOSM, all Instruction objects are AssignmentInstruction objects 283 Collection<Instruction.AssignmentInstruction> instructionKeys = instructions.stream() 284 .filter(Instruction.AssignmentInstruction.class::isInstance) 285 .map(Instruction.AssignmentInstruction.class::cast).filter(instruction -> Objects.equals(key, instruction.key)) 286 .collect(Collectors.toList()); 287 Optional<Instruction.AssignmentInstruction> instructionOptional = instructionKeys.stream() 288 .filter(instruction -> Objects.equals(value, instruction.val)).findAny(); 289 assertTrue(instructionOptional.isPresent(), MessageFormat 290 .format("Expected {0}, but got {1}", value, instructionOptional.orElse(instructionKeys.stream().findAny() 291 .orElseThrow(() -> new AssertionError("No instruction with "+key+" found"))).val)); 292 } 293 294 private static MapCSSRule getRule(MapCSSStyleSource source, String base, String subpart) { 295 // We need to do a new arraylist just to avoid the occasional ConcurrentModificationException 296 return new ArrayList<>(source.rules).stream().filter(rule -> rule.selectors.stream() 297 .anyMatch(selector -> base.equals(selector.getBase()) && subpart.equals(selector.getSubpart().getId(null)))) 298 .findAny().orElse(null); 299 } 300 } -
new file test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java
diff --git test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java test/unit/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/SourceTest.java new file mode 100644 index 000000000..500b5f8b5
- + 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.assertDoesNotThrow; 5 import static org.junit.jupiter.api.Assertions.assertEquals; 6 import static org.junit.jupiter.api.Assertions.assertNull; 7 import static org.junit.jupiter.api.Assertions.assertThrows; 8 import static org.junit.jupiter.api.Assertions.assertTrue; 9 10 11 import java.util.Locale; 12 import java.util.stream.Collectors; 13 import java.util.stream.Stream; 14 15 import javax.json.Json; 16 import javax.json.JsonObject; 17 import javax.json.JsonValue; 18 19 import org.openstreetmap.josm.data.Bounds; 20 21 import nl.jqno.equalsverifier.EqualsVerifier; 22 import org.junit.jupiter.api.Test; 23 24 /** 25 * Test class for {@link Source} 26 * @author Taylor Smock 27 * @since xxx 28 */ 29 public class SourceTest { 30 @Test 31 void testEquals() { 32 EqualsVerifier.forClass(Source.class).usingGetClass().verify(); 33 } 34 35 @Test 36 void testSimpleSources() { 37 final JsonObject emptyObject = Json.createObjectBuilder().build(); 38 assertThrows(NullPointerException.class, () -> new Source("Test source", emptyObject)); 39 40 final JsonObject badTypeValue = Json.createObjectBuilder().add("type", "bad type value").build(); 41 assertThrows(IllegalArgumentException.class, () -> new Source("Test source", badTypeValue)); 42 43 // Only SourceType.{VECTOR,RASTER} are supported 44 final SourceType[] supported = new SourceType[] {SourceType.VECTOR, SourceType.RASTER}; 45 for (SourceType type : supported) { 46 final JsonObject goodSourceType = Json.createObjectBuilder().add("type", type.toString().toLowerCase(Locale.ROOT)).build(); 47 Source source = assertDoesNotThrow(() -> new Source(type.name(), goodSourceType)); 48 // Check defaults 49 assertEquals(0, source.getMinZoom()); 50 assertEquals(22, source.getMaxZoom()); 51 assertEquals(type.name(), source.getName()); 52 assertNull(source.getAttributionText()); 53 assertTrue(source.getUrls().isEmpty()); 54 assertEquals(new Bounds(-85.051129, -180, 85.051129, 180), source.getBounds()); 55 } 56 57 // Check that unsupported types throw 58 for (SourceType type : Stream.of(SourceType.values()).filter(t -> Stream.of(supported).noneMatch(t::equals)).collect( 59 Collectors.toList())) { 60 final JsonObject goodSourceType = Json.createObjectBuilder().add("type", type.toString().toLowerCase(Locale.ROOT)).build(); 61 assertThrows(UnsupportedOperationException.class, () -> new Source(type.name(), goodSourceType)); 62 } 63 } 64 65 @Test 66 void testTileJsonSpec() { 67 // This isn't currently implemented, so it should throw. Mostly here to remind implementor to add tests... 68 final JsonObject tileJsonSpec = Json.createObjectBuilder() 69 .add("type", SourceType.VECTOR.name()).add("url", "some-random-url.com") 70 .build(); 71 assertThrows(UnsupportedOperationException.class, () -> new Source("Test TileJson", tileJsonSpec)); 72 } 73 74 @Test 75 void testBounds() { 76 // Check a "good" bounds 77 final JsonObject tileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("bounds", 78 Json.createArrayBuilder().add(-1).add(-2).add(3).add(4)).build(); 79 Source source = new Source("Test Bounds[-1, -2, 3, 4]", tileJsonSpec); 80 assertEquals(new Bounds(-2, -1, 4, 3), source.getBounds()); 81 82 // Check "bad" bounds 83 final JsonObject tileJsonSpecShort = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("bounds", 84 Json.createArrayBuilder().add(-1).add(-2).add(3)).build(); 85 IllegalArgumentException badLengthException = assertThrows(IllegalArgumentException.class, 86 () -> new Source("Test Bounds[-1, -2, 3]", tileJsonSpecShort)); 87 assertEquals("bounds must have four values, but has 3", badLengthException.getMessage()); 88 89 final JsonObject tileJsonSpecLong = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("bounds", 90 Json.createArrayBuilder().add(-1).add(-2).add(3).add(4).add(5)).build(); 91 badLengthException = assertThrows(IllegalArgumentException.class, () -> new Source("Test Bounds[-1, -2, 3, 4, 5]", tileJsonSpecLong)); 92 assertEquals("bounds must have four values, but has 5", badLengthException.getMessage()); 93 } 94 95 @Test 96 void testTiles() { 97 // No url 98 final JsonObject tileJsonSpecEmpty = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles", 99 JsonValue.NULL).build(); 100 Source source = new Source("Test Tile[]", tileJsonSpecEmpty); 101 assertTrue(source.getUrls().isEmpty()); 102 103 // Create a tile URL 104 final JsonObject tileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles", 105 Json.createArrayBuilder().add("https://example.org/{bbox-epsg-3857}")).build(); 106 source = new Source("Test Tile[https://example.org/{bbox-epsg-3857}]", tileJsonSpec); 107 assertEquals(1, source.getUrls().size()); 108 // Make certain that {bbox-epsg-3857} is replaced with the JOSM equivalent 109 assertEquals("https://example.org/{bbox}", source.getUrls().get(0)); 110 111 // Check with invalid data 112 final JsonObject tileJsonSpecBad = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles", 113 Json.createArrayBuilder().add(1).add("https://example.org/{bbox-epsg-3857}").add(false).add(Json.createArrayBuilder().add("hello")) 114 .add(Json.createObjectBuilder().add("bad", "array"))).build(); 115 source = new Source("Test Tile[1, https://example.org/{bbox-epsg-3857}, false, [\"hello\"], {\"bad\": \"array\"}]", tileJsonSpecBad); 116 assertEquals(1, source.getUrls().size()); 117 // Make certain that {bbox-epsg-3857} is replaced with the JOSM equivalent 118 assertEquals("https://example.org/{bbox}", source.getUrls().get(0)); 119 } 120 121 @Test 122 void testZoom() { 123 // Min zoom 124 final JsonObject minZoom5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("minzoom", 125 5).build(); 126 Source source = new Source("Test Zoom[minzoom=5]", minZoom5); 127 assertEquals(5, source.getMinZoom()); 128 assertEquals(22, source.getMaxZoom()); 129 130 // Negative min zoom 131 final JsonObject minZoomNeg1 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("minzoom", 132 -1).build(); 133 source = new Source("Test Zoom[minzoom=-1]", minZoomNeg1); 134 assertEquals(0, source.getMinZoom()); 135 assertEquals(22, source.getMaxZoom()); 136 137 // Max zoom 138 final JsonObject maxZoom5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom", 139 5).build(); 140 source = new Source("Test Zoom[maxzoom=5]", maxZoom5); 141 assertEquals(0, source.getMinZoom()); 142 assertEquals(5, source.getMaxZoom()); 143 144 // Big Max zoom 145 final JsonObject maxZoom31 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom", 146 31).build(); 147 source = new Source("Test Zoom[maxzoom=31]", maxZoom31); 148 assertEquals(0, source.getMinZoom()); 149 assertEquals(30, source.getMaxZoom()); 150 151 // Negative max zoom 152 final JsonObject maxZoomNeg5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom", 153 -5).build(); 154 source = new Source("Test Zoom[maxzoom=-5]", maxZoomNeg5); 155 assertEquals(0, source.getMinZoom()); 156 assertEquals(0, source.getMaxZoom()); 157 158 // Min max zoom 159 final JsonObject minZoom1MaxZoom5 = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("maxzoom", 160 5).add("minzoom", 1).build(); 161 source = new Source("Test Zoom[minzoom=1,maxzoom=5]", minZoom1MaxZoom5); 162 assertEquals(1, source.getMinZoom()); 163 assertEquals(5, source.getMaxZoom()); 164 } 165 166 @Test 167 void testToString() { 168 // Simple (no urls) 169 final JsonObject noTileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).build(); 170 Source source = new Source("Test String[]", noTileJsonSpec); 171 assertEquals("Test String[]", source.toString()); 172 173 // With one url 174 final JsonObject tileJsonSpec = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()).add("tiles", 175 Json.createArrayBuilder().add("https://example.org/{bbox-epsg-3857}")).build(); 176 source = new Source("Test String[https://example.org/{bbox-epsg-3857}]", tileJsonSpec); 177 assertEquals("Test String[https://example.org/{bbox-epsg-3857}] https://example.org/{bbox}", source.toString()); 178 179 // With two URLs 180 final JsonObject tileJsonSpecMultiple = Json.createObjectBuilder().add("type", SourceType.VECTOR.name()) 181 .add("tiles", Json.createArrayBuilder() 182 .add("https://example.org/{bbox-epsg-3857}") 183 .add("https://example.com/{bbox-epsg-3857}")).build(); 184 source = new Source("Test String[https://example.org/{bbox-epsg-3857},https://example.com/{bbox-epsg-3857}]", tileJsonSpecMultiple); 185 assertEquals("Test String[https://example.org/{bbox-epsg-3857},https://example.com/{bbox-epsg-3857}] https://example.org/{bbox} " 186 + "https://example.com/{bbox}", source.toString()); 187 } 188 } -
new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java
diff --git test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufParserTest.java new file mode 100644 index 000000000..bdfdf86b7
- + 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 6 import org.junit.jupiter.api.Test; 7 8 /** 9 * Test class for {@link ProtoBufParser} 10 * @author Taylor Smock 11 * @since xxx 12 */ 13 class ProtoBufParserTest { 14 /** 15 * Check that we are appropriately converting values to the "smallest" type 16 */ 17 @Test 18 void testConvertLong() { 19 // No casting due to auto conversions 20 assertEquals(Byte.MAX_VALUE, ProtoBufParser.convertLong(Byte.MAX_VALUE)); 21 assertEquals(Byte.MIN_VALUE, ProtoBufParser.convertLong(Byte.MIN_VALUE)); 22 assertEquals(Short.MIN_VALUE, ProtoBufParser.convertLong(Short.MIN_VALUE)); 23 assertEquals(Short.MAX_VALUE, ProtoBufParser.convertLong(Short.MAX_VALUE)); 24 assertEquals(Integer.MAX_VALUE, ProtoBufParser.convertLong(Integer.MAX_VALUE)); 25 assertEquals(Integer.MIN_VALUE, ProtoBufParser.convertLong(Integer.MIN_VALUE)); 26 assertEquals(Long.MIN_VALUE, ProtoBufParser.convertLong(Long.MIN_VALUE)); 27 assertEquals(Long.MAX_VALUE, ProtoBufParser.convertLong(Long.MAX_VALUE)); 28 } 29 30 /** 31 * Check that zig zags are appropriately encoded. 32 */ 33 @Test 34 void testEncodeZigZag() { 35 assertEquals(0, ProtoBufParser.encodeZigZag(0).byteValue()); 36 assertEquals(1, ProtoBufParser.encodeZigZag(-1).byteValue()); 37 assertEquals(2, ProtoBufParser.encodeZigZag(1).byteValue()); 38 assertEquals(3, ProtoBufParser.encodeZigZag(-2).byteValue()); 39 assertEquals(254, ProtoBufParser.encodeZigZag(Byte.MAX_VALUE).shortValue()); 40 assertEquals(255, ProtoBufParser.encodeZigZag(Byte.MIN_VALUE).shortValue()); 41 assertEquals(65_534, ProtoBufParser.encodeZigZag(Short.MAX_VALUE).intValue()); 42 assertEquals(65_535, ProtoBufParser.encodeZigZag(Short.MIN_VALUE).intValue()); 43 // These integers check a possible boundary condition (the boundary between using the 32/64 bit encoding methods) 44 assertEquals(4_294_967_292L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE - 1).longValue()); 45 assertEquals(4_294_967_293L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE + 1).longValue()); 46 assertEquals(4_294_967_294L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE).longValue()); 47 assertEquals(4_294_967_295L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE).longValue()); 48 assertEquals(4_294_967_296L, ProtoBufParser.encodeZigZag(Integer.MAX_VALUE + 1L).longValue()); 49 assertEquals(4_294_967_297L, ProtoBufParser.encodeZigZag(Integer.MIN_VALUE - 1L).longValue()); 50 } 51 } -
new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java
diff --git test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufRecordTest.java new file mode 100644 index 000000000..d0e204c6a
- + 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 6 7 import java.io.IOException; 8 9 import org.junit.jupiter.api.Test; 10 11 /** 12 * Test class for specific {@link ProtoBufRecord} functionality 13 */ 14 class ProtoBufRecordTest { 15 @Test 16 void testFixed32() throws IOException { 17 ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f})); 18 ProtoBufRecord thirtyTwoBit = new ProtoBufRecord(parser); 19 assertEquals(WireType.THIRTY_TWO_BIT, thirtyTwoBit.getType()); 20 assertEquals(1f, thirtyTwoBit.asFloat()); 21 } 22 23 @Test 24 void testUnknown() throws IOException { 25 ProtoBufParser parser = new ProtoBufParser(ProtoBufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f})); 26 ProtoBufRecord unknown = new ProtoBufRecord(parser); 27 assertEquals(WireType.UNKNOWN, unknown.getType()); 28 assertEquals(0, unknown.getBytes().length); 29 } 30 } -
new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
diff --git test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java new file mode 100644 index 000000000..043481efe
- + 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 import java.awt.geom.Ellipse2D; 9 import java.io.ByteArrayInputStream; 10 import java.io.File; 11 import java.io.IOException; 12 import java.io.InputStream; 13 import java.nio.file.Paths; 14 import java.text.MessageFormat; 15 import java.util.ArrayList; 16 import java.util.Collection; 17 import java.util.List; 18 import java.util.stream.Collectors; 19 20 import org.openstreetmap.josm.TestUtils; 21 import org.openstreetmap.josm.data.coor.LatLon; 22 import org.openstreetmap.josm.data.imagery.ImageryInfo; 23 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature; 24 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 25 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile; 26 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource; 27 import org.openstreetmap.josm.data.osm.BBox; 28 import org.openstreetmap.josm.data.osm.Node; 29 import org.openstreetmap.josm.data.osm.Way; 30 import org.openstreetmap.josm.data.vector.VectorDataSet; 31 import org.openstreetmap.josm.data.vector.VectorNode; 32 import org.openstreetmap.josm.data.vector.VectorWay; 33 import org.openstreetmap.josm.io.Compression; 34 import org.openstreetmap.josm.testutils.JOSMTestRules; 35 36 import org.junit.jupiter.api.Test; 37 import org.junit.jupiter.api.extension.RegisterExtension; 38 39 /** 40 * Test class for {@link ProtoBufParser} and {@link ProtoBufRecord} 41 * 42 * @author Taylor Smock 43 * @since xxx 44 */ 45 class ProtoBufTest { 46 /** 47 * Convert an int array into a byte array 48 * @param intArray The int array to convert (NOTE: numbers must be below 255) 49 * @return A byte array that can be used 50 */ 51 static byte[] toByteArray(int[] intArray) { 52 byte[] byteArray = new byte[intArray.length]; 53 for (int i = 0; i < intArray.length; i++) { 54 if (intArray[i] > Byte.MAX_VALUE - Byte.MIN_VALUE) { 55 throw new IllegalArgumentException(); 56 } 57 byteArray[i] = Integer.valueOf(intArray[i]).byteValue(); 58 } 59 return byteArray; 60 } 61 62 @RegisterExtension 63 JOSMTestRules josmTestRules = new JOSMTestRules().preferences(); 64 65 private Number bytesToVarInt(int... bytes) { 66 byte[] byteArray = new byte[bytes.length]; 67 for (int i = 0; i < bytes.length; i++) { 68 byteArray[i] = (byte) bytes[i]; 69 } 70 return ProtoBufParser.convertByteArray(byteArray, ProtoBufParser.VAR_INT_BYTE_SIZE); 71 } 72 73 /** 74 * Test reading tile from Mapillary ( 14/3248/6258 ) 75 * 76 * @throws IOException if there is a problem reading the file 77 */ 78 @Test 79 void testRead_14_3248_6258() throws IOException { 80 File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "14", "3248", "6258.mvt").toFile(); 81 InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile); 82 Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords(); 83 assertEquals(2, records.size()); 84 List<Layer> layers = new ArrayList<>(); 85 for (ProtoBufRecord record : records) { 86 if (record.getField() == Layer.LAYER_FIELD) { 87 layers.add(new Layer(record.getBytes())); 88 } else { 89 fail(MessageFormat.format("Invalid field {0}", record.getField())); 90 } 91 } 92 Layer mapillarySequences = layers.get(0); 93 Layer mapillaryPictures = layers.get(1); 94 assertEquals("mapillary-sequences", mapillarySequences.getName()); 95 assertEquals("mapillary-images", mapillaryPictures.getName()); 96 assertEquals(2048, mapillarySequences.getExtent()); 97 assertEquals(2048, mapillaryPictures.getExtent()); 98 99 assertEquals(1, 100 mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 233760500).count()); 101 Feature testSequence = mapillarySequences.getFeatures().stream().filter(feature -> feature.getId() == 233760500) 102 .findAny().orElse(null); 103 assertEquals("dpudn262yz6aitu33zh7bl", testSequence.getTags().get("key")); 104 assertEquals("clnaw3kpokIAe_CsN5Qmiw", testSequence.getTags().get("ikey")); 105 assertEquals("B1iNjH4Ohn25cRAGPhetfw", testSequence.getTags().get("userkey")); 106 assertEquals(Long.valueOf(1557535457401L), Long.valueOf(testSequence.getTags().get("captured_at"))); 107 assertEquals(0, Integer.parseInt(testSequence.getTags().get("pano"))); 108 } 109 110 @Test 111 void testRead_17_26028_50060() throws IOException { 112 File vectorTile = Paths.get(TestUtils.getTestDataRoot(), "pbf", "openinframap", "17", "26028", "50060.pbf") 113 .toFile(); 114 InputStream inputStream = Compression.getUncompressedFileInputStream(vectorTile); 115 Collection<ProtoBufRecord> records = new ProtoBufParser(inputStream).allRecords(); 116 List<Layer> layers = new ArrayList<>(); 117 for (ProtoBufRecord record : records) { 118 if (record.getField() == Layer.LAYER_FIELD) { 119 layers.add(new Layer(record.getBytes())); 120 } else { 121 fail(MessageFormat.format("Invalid field {0}", record.getField())); 122 } 123 } 124 assertEquals(19, layers.size()); 125 List<Layer> dataLayers = layers.stream().filter(layer -> !layer.getFeatures().isEmpty()) 126 .collect(Collectors.toList()); 127 // power_plant, power_plant_point, power_generator, power_heatmap_solar, and power_generator_area 128 assertEquals(5, dataLayers.size()); 129 130 // power_generator_area was rendered incorrectly 131 final Layer powerGeneratorArea = dataLayers.stream() 132 .filter(layer -> "power_generator_area".equals(layer.getName())).findAny().orElse(null); 133 assertNotNull(powerGeneratorArea); 134 final int extent = powerGeneratorArea.getExtent(); 135 // 17/26028/50060 bounds 136 VectorDataSet vectorDataSet = new VectorDataSet(); 137 MVTTile vectorTile1 = new MVTTile(new MapboxVectorTileSource(new ImageryInfo("Test info", "example.org")), 138 26028, 50060, 17); 139 vectorTile1.loadImage(Compression.getUncompressedFileInputStream(vectorTile)); 140 vectorDataSet.addTileData(vectorTile1); 141 vectorDataSet.setZoom(17); 142 final Way one = new Way(); 143 one.addNode(new Node(new LatLon(39.0687509, -108.5100816))); 144 one.addNode(new Node(new LatLon(39.0687509, -108.5095751))); 145 one.addNode(new Node(new LatLon(39.0687169, -108.5095751))); 146 one.addNode(new Node(new LatLon(39.0687169, -108.5100816))); 147 one.addNode(one.getNode(0)); 148 one.setOsmId(666293899, 2); 149 final BBox searchBBox = one.getBBox(); 150 searchBBox.addPrimitive(one, 0.00001); 151 final Collection<VectorNode> searchedNodes = vectorDataSet.searchNodes(searchBBox); 152 final Collection<VectorWay> searchedWays = vectorDataSet.searchWays(searchBBox); 153 assertEquals(4, searchedNodes.size()); 154 } 155 156 @Test 157 void testReadVarInt() { 158 assertEquals(ProtoBufParser.convertLong(0), bytesToVarInt(0x0)); 159 assertEquals(ProtoBufParser.convertLong(1), bytesToVarInt(0x1)); 160 assertEquals(ProtoBufParser.convertLong(127), bytesToVarInt(0x7f)); 161 // This should b 0xff 0xff 0xff 0xff 0x07, but we drop the leading bit when reading to a byte array 162 Number actual = bytesToVarInt(0x7f, 0x7f, 0x7f, 0x7f, 0x07); 163 assertEquals(ProtoBufParser.convertLong(Integer.MAX_VALUE), actual, 164 MessageFormat.format("Expected {0} but got {1}", Integer.toBinaryString(Integer.MAX_VALUE), 165 Long.toBinaryString(actual.longValue()))); 166 } 167 168 /** 169 * Test simple message. 170 * Check that a simple message is readable 171 * 172 * @throws IOException - if an IO error occurs 173 */ 174 @Test 175 void testSimpleMessage() throws IOException { 176 ProtoBufParser parser = new ProtoBufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01}); 177 ProtoBufRecord record = new ProtoBufRecord(parser); 178 assertEquals(WireType.VARINT, record.getType()); 179 assertEquals(150, record.asUnsignedVarInt().intValue()); 180 } 181 182 @Test 183 void testSingletonMultiPoint() throws IOException { 184 Collection<ProtoBufRecord> records = new ProtoBufParser(new ByteArrayInputStream(toByteArray( 185 new int[] {0x1a, 0x2c, 0x78, 0x02, 0x0a, 0x03, 0x74, 0x6d, 0x70, 0x28, 0x80, 0x20, 0x1a, 0x04, 0x6e, 186 0x61, 0x6d, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x54, 0x65, 0x73, 0x74, 0x20, 0x6e, 0x61, 0x6d, 0x65, 187 0x12, 0x0d, 0x18, 0x01, 0x12, 0x02, 0x00, 0x00, 0x22, 0x05, 0x09, 0xe0, 0x3e, 0x84, 0x27}))) 188 .allRecords(); 189 List<Layer> layers = new ArrayList<>(); 190 for (ProtoBufRecord record : records) { 191 if (record.getField() == Layer.LAYER_FIELD) { 192 layers.add(new Layer(record.getBytes())); 193 } else { 194 fail(MessageFormat.format("Invalid field {0}", record.getField())); 195 } 196 } 197 assertEquals(1, layers.size()); 198 assertEquals(1, layers.get(0).getGeometry().size()); 199 Ellipse2D shape = (Ellipse2D) layers.get(0).getGeometry().iterator().next().getShapes().iterator().next(); 200 assertEquals(4016, shape.getCenterX()); 201 assertEquals(2498, shape.getCenterY()); 202 } 203 204 @Test 205 void testZigZag() { 206 assertEquals(0, ProtoBufParser.decodeZigZag(0).intValue()); 207 assertEquals(-1, ProtoBufParser.decodeZigZag(1).intValue()); 208 assertEquals(1, ProtoBufParser.decodeZigZag(2).intValue()); 209 assertEquals(-2, ProtoBufParser.decodeZigZag(3).intValue()); 210 } 211 } -
new file test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java
diff --git test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java test/unit/org/openstreetmap/josm/data/vector/VectorDataSetTest.java new file mode 100644 index 000000000..38ab53ad2
- + 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 73 JOSMTestRules rule = new JOSMTestRules().projection(); 74 75 /** 76 * Load arbitrary tiles 77 * @param layer The layer to add the tiles to 78 * @param tiles The tiles to load ([z, x, y, z, x, y, ...]) -- must be divisible by three 79 */ 80 private static void loadTile(MVTLayerMock layer, int... tiles) { 81 if (tiles.length % 3 != 0 || tiles.length == 0) { 82 throw new IllegalArgumentException("Tiles come with a {z}, {x}, and {y} component"); 83 } 84 final MapboxVectorTileSource tileSource = layer.getTileSource(); 85 MapBoxVectorCachedTileLoader tileLoader = layer.getTileLoader(); 86 Collection<MVTTile> tilesCollection = new ArrayList<>(); 87 for (int i = 0; i < tiles.length / 3; i++) { 88 final MVTTile tile = (MVTTile) layer.createTile(tileSource, tiles[3 * i + 1], tiles[3 * i + 2], tiles[3 * i]); 89 tileLoader.createTileLoaderJob(tile).submit(); 90 tilesCollection.add(tile); 91 } 92 Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> layer.finishedLoading().size() == tilesCollection 93 .size()); 94 } 95 96 private MVTLayerMock layer; 97 98 @BeforeEach 99 void setup() { 100 // Create the preconditions for the test 101 final ImageryInfo info = new ImageryInfo(); 102 info.setName("en", "Test info"); 103 info.setUrl("file:/" + Paths.get(TestUtils.getTestDataRoot(), "pbf", "mapillary", "{z}", "{x}", "{y}.mvt")); 104 layer = new MVTLayerMock(info); 105 } 106 107 @Test 108 void testNodeDeduplication() { 109 final VectorDataSet dataSet = this.layer.getData(); 110 assertTrue(dataSet.allPrimitives().isEmpty()); 111 112 // Set the zoom to 14, as that is the tile we are checking 113 dataSet.setZoom(14); 114 loadTile(this.layer, 14, 3248, 6258); 115 116 // There _does_ appear to be some kind of race condition though 117 Awaitility.await().atMost(Durations.FIVE_SECONDS).until(() -> dataSet.getNodes().size() > 50); 118 // Actual test 119 // With Mapillary, only ends of ways should be untagged 120 // There are 55 actual "nodes" in the data with two nodes for the ends of the way. 121 // One of the end nodes is a duplicate of an actual node. 122 assertEquals(56, dataSet.getNodes().size()); 123 assertEquals(1, dataSet.getWays().size()); 124 assertEquals(0, dataSet.getRelations().size()); 125 } 126 127 @Test 128 void testWayDeduplicationSimple() { 129 final VectorDataSet dataSet = this.layer.getData(); 130 assertTrue(dataSet.allPrimitives().isEmpty()); 131 132 // Set the zoom to 14, as that is the tile we are checking 133 dataSet.setZoom(14); 134 // Load tiles that are next to each other 135 loadTile(this.layer, 14, 3248, 6258, 14, 3249, 6258); 136 137 Map<Long, List<VectorWay>> wayGroups = dataSet.getWays().stream() 138 .collect(Collectors.groupingBy(VectorWay::getId)); 139 wayGroups.forEach((id, ways) -> assertEquals(1, ways.size(), MessageFormat.format("{0} was not deduplicated", id))); 140 } 141 } -
new file test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java
diff --git test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java test/unit/org/openstreetmap/josm/data/vector/VectorNodeTest.java new file mode 100644 index 000000000..834b46c4d
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import org.junit.jupiter.api.Test; 5 import org.junit.jupiter.api.extension.RegisterExtension; 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.IRelation; 12 import org.openstreetmap.josm.data.osm.IWay; 13 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 14 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 15 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 16 import org.openstreetmap.josm.testutils.JOSMTestRules; 17 18 import java.util.ArrayList; 19 import java.util.Collections; 20 import java.util.List; 21 22 import static org.junit.jupiter.api.Assertions.assertEquals; 23 import static org.junit.jupiter.api.Assertions.assertFalse; 24 import static org.junit.jupiter.api.Assertions.assertNotNull; 25 import static org.junit.jupiter.api.Assertions.assertSame; 26 import static org.junit.jupiter.api.Assertions.assertTrue; 27 import static org.junit.jupiter.api.Assertions.fail; 28 29 /** 30 * Test class for {@link VectorNode} 31 * @author Taylor Smock 32 * @since xxx 33 */ 34 class VectorNodeTest { 35 @RegisterExtension 36 JOSMTestRules rule = new JOSMTestRules().projection(); 37 38 @Test 39 void testLatLon() { 40 VectorNode node = new VectorNode("test"); 41 assertTrue(Double.isNaN(node.lat())); 42 assertTrue(Double.isNaN(node.lon())); 43 LatLon testLatLon = new LatLon(50, -40); 44 node.setCoor(testLatLon); 45 assertEquals(50, node.lat()); 46 assertEquals(-40, node.lon()); 47 assertEquals(testLatLon, node.getCoor()); 48 } 49 50 @Test 51 void testSetEastNorth() { 52 VectorNode node = new VectorNode("test"); 53 LatLon latLon = new LatLon(-1, 5); 54 EastNorth eastNorth = ProjectionRegistry.getProjection().latlon2eastNorth(latLon); 55 node.setEastNorth(eastNorth); 56 assertEquals(-1, node.lat(), 0.0000000001); 57 assertEquals(5, node.lon(), 0.0000000001); 58 } 59 60 @Test 61 void testICoordinate() { 62 VectorNode node = new VectorNode("test"); 63 assertTrue(Double.isNaN(node.lat())); 64 assertTrue(Double.isNaN(node.lon())); 65 ICoordinate coord = new ICoordinate() { 66 @Override 67 public double getLat() { 68 return 5; 69 } 70 71 @Override 72 public void setLat(double lat) { 73 // No op 74 } 75 76 @Override 77 public double getLon() { 78 return -1; 79 } 80 81 @Override 82 public void setLon(double lon) { 83 // no op 84 } 85 }; 86 node.setCoor(coord); 87 assertEquals(5, node.lat()); 88 assertEquals(-1, node.lon()); 89 } 90 91 @Test 92 void testUniqueIdGenerator() { 93 VectorNode node1 = new VectorNode("test"); 94 VectorNode node2 = new VectorNode("test2"); 95 assertSame(node1.getIdGenerator(), node2.getIdGenerator()); 96 assertNotNull(node1.getIdGenerator()); 97 } 98 99 @Test 100 void testNode() { 101 assertEquals(OsmPrimitiveType.NODE, new VectorNode("test").getType()); 102 } 103 104 @Test 105 void testBBox() { 106 VectorNode node = new VectorNode("test"); 107 node.setCoor(new LatLon(5, -1)); 108 assertTrue(node.getBBox().bboxIsFunctionallyEqual(new BBox(-1, 5), 0d)); 109 } 110 111 @Test 112 void testVisitor() { 113 List<VectorNode> visited = new ArrayList<>(); 114 VectorNode node = new VectorNode("test"); 115 node.accept(new PrimitiveVisitor() { 116 @Override 117 public void visit(INode n) { 118 visited.add((VectorNode) n); 119 } 120 121 @Override 122 public void visit(IWay<?> w) { 123 fail("Way should not have been visited"); 124 } 125 126 @Override 127 public void visit(IRelation<?> r) { 128 fail("Relation should not have been visited"); 129 } 130 }); 131 132 assertEquals(1, visited.size()); 133 assertSame(node, visited.get(0)); 134 } 135 136 @Test 137 void testIsReferredToByWays() { 138 VectorWay way = new VectorWay("test"); 139 VectorNode node = new VectorNode("test"); 140 assertFalse(node.isReferredByWays(1)); 141 assertTrue(node.getReferrers(true).isEmpty()); 142 way.setNodes(Collections.singletonList(node)); 143 assertEquals(1, node.getReferrers(true).size()); 144 assertSame(way, node.getReferrers(true).get(0)); 145 // No dataset yet 146 assertFalse(node.isReferredByWays(1)); 147 VectorDataSet dataSet = new VectorDataSet(); 148 dataSet.addPrimitive(way); 149 dataSet.addPrimitive(node); 150 assertTrue(node.isReferredByWays(1)); 151 assertFalse(node.isReferredByWays(2)); 152 } 153 } -
new file test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java
diff --git test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java test/unit/org/openstreetmap/josm/data/vector/VectorRelationTest.java new file mode 100644 index 000000000..941143b25
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import org.junit.jupiter.api.Test; 5 import org.junit.jupiter.api.extension.RegisterExtension; 6 import org.openstreetmap.josm.testutils.JOSMTestRules; 7 8 import java.util.Arrays; 9 10 import static org.junit.jupiter.api.Assertions.assertEquals; 11 import static org.junit.jupiter.api.Assertions.assertFalse; 12 import static org.junit.jupiter.api.Assertions.assertSame; 13 import static org.junit.jupiter.api.Assertions.assertThrows; 14 import static org.junit.jupiter.api.Assertions.assertTrue; 15 16 /** 17 * Test class for {@link VectorRelation} 18 * @author Taylor Smock 19 * @since xxx 20 */ 21 class VectorRelationTest { 22 @RegisterExtension 23 JOSMTestRules rule = new JOSMTestRules(); 24 25 @Test 26 void testMembers() { 27 VectorNode node1 = new VectorNode("test"); 28 VectorNode node2 = new VectorNode("test"); 29 VectorWay way1 = new VectorWay("test"); 30 way1.setNodes(Arrays.asList(node1, node2)); 31 VectorRelationMember member1 = new VectorRelationMember("randomRole", node1); 32 VectorRelationMember member2 = new VectorRelationMember("role2", way1); 33 assertSame(node1, member1.getMember()); 34 assertSame(node1.getType(), member1.getType()); 35 assertEquals("randomRole", member1.getRole()); 36 assertSame(node1.getId(), member1.getUniqueId()); 37 // Not a way. 38 assertThrows(ClassCastException.class, member1::getWay); 39 40 assertTrue(member1.isNode()); 41 assertFalse(member1.isWay()); 42 assertFalse(member2.isNode()); 43 assertTrue(member2.isWay()); 44 } 45 } -
new file test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java
diff --git test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java test/unit/org/openstreetmap/josm/data/vector/VectorWayTest.java new file mode 100644 index 000000000..db2367e6b
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.vector; 3 4 import org.junit.jupiter.api.Test; 5 import org.openstreetmap.josm.data.coor.LatLon; 6 import org.openstreetmap.josm.data.osm.BBox; 7 import org.openstreetmap.josm.data.osm.INode; 8 import org.openstreetmap.josm.data.osm.IRelation; 9 import org.openstreetmap.josm.data.osm.IWay; 10 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 11 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 12 13 import java.util.ArrayList; 14 import java.util.Arrays; 15 import java.util.Collections; 16 import java.util.List; 17 18 import static org.junit.jupiter.api.Assertions.assertEquals; 19 import static org.junit.jupiter.api.Assertions.assertFalse; 20 import static org.junit.jupiter.api.Assertions.assertNull; 21 import static org.junit.jupiter.api.Assertions.assertSame; 22 import static org.junit.jupiter.api.Assertions.assertTrue; 23 import static org.junit.jupiter.api.Assertions.fail; 24 25 /** 26 * Test class for {@link VectorWay} 27 * @author Taylor Smock 28 * @since xxx 29 */ 30 class VectorWayTest { 31 @Test 32 void testBBox() { 33 VectorNode node1 = new VectorNode("test"); 34 VectorWay way = new VectorWay("test"); 35 way.setNodes(Collections.singletonList(node1)); 36 node1.setCoor(new LatLon(-5, 1)); 37 assertTrue(node1.getBBox().bboxIsFunctionallyEqual(way.getBBox(), 0.0)); 38 39 VectorNode node2 = new VectorNode("test"); 40 node2.setCoor(new LatLon(-10, 2)); 41 42 way.setNodes(Arrays.asList(node1, node2)); 43 assertTrue(way.getBBox().bboxIsFunctionallyEqual(new BBox(2, -10, 1, -5), 0.0)); 44 } 45 46 @Test 47 void testIdGenerator() { 48 assertSame(new VectorWay("test").getIdGenerator(), new VectorWay("test").getIdGenerator()); 49 } 50 51 @Test 52 void testNodes() { 53 VectorNode node1 = new VectorNode("test"); 54 VectorNode node2 = new VectorNode("test"); 55 VectorNode node3 = new VectorNode("test"); 56 node1.setId(1); 57 node2.setId(2); 58 node3.setId(3); 59 VectorWay way = new VectorWay("test"); 60 assertNull(way.firstNode()); 61 assertNull(way.lastNode()); 62 assertFalse(way.isClosed()); 63 assertFalse(way.isFirstLastNode(node1)); 64 assertFalse(way.isInnerNode(node2)); 65 way.setNodes(Arrays.asList(node1, node2, node3)); 66 assertEquals(3, way.getNodesCount()); 67 assertEquals(node1, way.getNode(0)); 68 assertEquals(node2, way.getNode(1)); 69 assertEquals(node3, way.getNode(2)); 70 assertTrue(way.isFirstLastNode(node1)); 71 assertTrue(way.isFirstLastNode(node3)); 72 assertFalse(way.isFirstLastNode(node2)); 73 assertTrue(way.isInnerNode(node2)); 74 assertFalse(way.isInnerNode(node1)); 75 assertFalse(way.isInnerNode(node3)); 76 77 assertEquals(1, way.getNodeIds().get(0)); 78 assertEquals(2, way.getNodeIds().get(1)); 79 assertEquals(3, way.getNodeIds().get(2)); 80 assertEquals(1, way.getNodeId(0)); 81 assertEquals(2, way.getNodeId(1)); 82 assertEquals(3, way.getNodeId(2)); 83 84 assertFalse(way.isClosed()); 85 assertEquals(OsmPrimitiveType.WAY, way.getType()); 86 List<VectorNode> nodes = new ArrayList<>(way.getNodes()); 87 nodes.add(nodes.get(0)); 88 way.setNodes(nodes); 89 assertTrue(way.isClosed()); 90 assertEquals(OsmPrimitiveType.CLOSEDWAY, way.getType()); 91 } 92 93 @Test 94 void testAccept() { 95 VectorWay way = new VectorWay("test"); 96 List<VectorWay> visited = new ArrayList<>(1); 97 way.accept(new PrimitiveVisitor() { 98 @Override 99 public void visit(INode n) { 100 fail("No nodes should be visited"); 101 } 102 103 @Override 104 public void visit(IWay<?> w) { 105 visited.add((VectorWay) w); 106 } 107 108 @Override 109 public void visit(IRelation<?> r) { 110 fail("No relations should be visited"); 111 } 112 }); 113 114 assertEquals(1, visited.size()); 115 assertSame(way, visited.get(0)); 116 } 117 }
