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