Ticket #17177: 17177.4.patch
File 17177.4.patch, 170.9 KB (added by , 4 years ago) |
---|
-
new file resources/images/dialogs/add_mvt.svg
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/resources/images/dialogs/add_mvt.svg b/resources/images/dialogs/add_mvt.svg new file mode 100644
- + 1 <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 <svg 3 xmlns:dc="http://purl.org/dc/elements/1.1/" 4 xmlns:cc="http://creativecommons.org/ns#" 5 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 6 xmlns: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/imagery/ImageryInfo.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java b/src/org/openstreetmap/josm/data/imagery/ImageryInfo.java
a b 61 61 /** A WMS endpoint entry only stores the WMS server info, without layer, which are chosen later by the user. **/ 62 62 WMS_ENDPOINT("wms_endpoint"), 63 63 /** WMTS stores GetCapabilities URL. Does not store any information about the layer **/ 64 WMTS("wmts"); 64 WMTS("wmts"), 65 /** MapBox Vector Tiles entry*/ 66 MVT("mvt"); 65 67 66 68 private final String typeString; 67 69 … … 654 656 defaultMaxZoom = 0; 655 657 defaultMinZoom = 0; 656 658 for (ImageryType type : ImageryType.values()) { 657 Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+) \\])?:(.*)").matcher(url);659 Matcher m = Pattern.compile(type.getTypeString()+"(?:\\[(?:(\\d+)[,-])?(\\d+)])?:(.*)").matcher(url); 658 660 if (m.matches()) { 659 661 this.url = m.group(3); 660 662 this.sourceType = type; … … 669 671 } 670 672 671 673 if (serverProjections.isEmpty()) { 672 Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\) \\}.*").matcher(url.toUpperCase(Locale.ENGLISH));674 Matcher m = Pattern.compile(".*\\{PROJ\\(([^)}]+)\\)}.*").matcher(url.toUpperCase(Locale.ENGLISH)); 673 675 if (m.matches()) { 674 676 setServerProjections(Arrays.asList(m.group(1).split(",", -1))); 675 677 } -
src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/TMSCachedTileLoaderJob.java
a b 10 10 import java.nio.charset.StandardCharsets; 11 11 import java.util.HashSet; 12 12 import java.util.List; 13 import java.util.Locale; 13 14 import java.util.Map; 14 15 import java.util.Map.Entry; 15 16 import java.util.Optional; … … 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()) { -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Command.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 /** 5 * Command integers for Mapbox Vector Tiles 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public enum Command { 10 /** 11 * For {@link GeometryTypes#POINT}, each {@link #MoveTo} is a new point. 12 * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #MoveTo} is a new geometry of the same type. 13 */ 14 MoveTo((byte) 1, (byte) 2), 15 /** 16 * While not explicitly prohibited for {@link GeometryTypes#POINT}, it should be ignored. 17 * For {@link GeometryTypes#LINESTRING} and {@link GeometryTypes#POLYGON}, each {@link #LineTo} extends that geometry. 18 */ 19 LineTo((byte) 2, (byte) 2), 20 /** 21 * This is only explicitly valid for {@link GeometryTypes#POLYGON}. It closes the {@link GeometryTypes#POLYGON}. 22 */ 23 ClosePath((byte) 7, (byte) 0); 24 25 private final byte id; 26 private final byte parameters; 27 28 Command(byte id, byte parameters) { 29 this.id = id; 30 this.parameters = parameters; 31 } 32 33 /** 34 * Get the command id 35 * @return The id 36 */ 37 public byte getId() { 38 return this.id; 39 } 40 41 /** 42 * Get the number of parameters 43 * @return The number of parameters 44 */ 45 public byte getParameterNumber() { 46 return this.parameters; 47 } 48 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/CommandInteger.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.stream.Stream; 5 6 /** 7 * An indicator for a command to be executed 8 * @author Taylor Smock 9 * @since xxx 10 */ 11 public class CommandInteger { 12 private final Command type; 13 private final short[] parameters; 14 private int added; 15 16 /** 17 * Create a new command 18 * @param command the command (treated as an unsigned int) 19 */ 20 public CommandInteger(final int command) { 21 // Technically, the int is unsigned, but it is easier to work with the long 22 final long unsigned = Integer.toUnsignedLong(command); 23 this.type = Stream.of(Command.values()).filter(e -> e.getId() == (unsigned & 0x7)).findAny() 24 .orElseThrow(InvalidMapboxVectorTileException::new); 25 // This is safe, since we are shifting right 3 when we converted an int to a long (for unsigned). 26 // So we <i>cannot</i> lose anything. 27 final int operationsInt = (int) (unsigned >> 3); 28 this.parameters = new short[operationsInt * this.type.getParameterNumber()]; 29 } 30 31 /** 32 * Add a parameter 33 * @param parameterInteger The parameter to add (converted to {@link short}). 34 */ 35 public void addParameter(Number parameterInteger) { 36 this.parameters[added++] = parameterInteger.shortValue(); 37 } 38 39 /** 40 * Get the operations for the command 41 * @return The operations 42 */ 43 public short[] getOperations() { 44 return this.parameters; 45 } 46 47 /** 48 * Get the command type 49 * @return the command type 50 */ 51 public Command getType() { 52 return this.type; 53 } 54 55 /** 56 * Get the expected parameter length 57 * @return The expected parameter size 58 */ 59 public boolean hasAllExpectedParameters() { 60 return this.added >= this.parameters.length; 61 } 62 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.io.IOException; 5 import java.text.NumberFormat; 6 import java.util.ArrayList; 7 import java.util.List; 8 9 import org.openstreetmap.josm.data.osm.TagMap; 10 import org.openstreetmap.josm.data.protobuf.ProtoBufPacked; 11 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 12 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 13 import org.openstreetmap.josm.tools.Utils; 14 15 /** 16 * A Feature for a {@link Layer} 17 * @author Taylor Smock 18 * @since xxx 19 */ 20 public class Feature { 21 private static final byte ID_FIELD = 1; 22 private static final byte TAG_FIELD = 2; 23 private static final byte GEOMETRY_TYPE_FIELD = 3; 24 private static final byte GEOMETRY_FIELD = 4; 25 /** The geometry of the feature. Required. */ 26 private final List<CommandInteger> geometry = new ArrayList<>(); 27 28 /** The geometry type of the feature. Required. */ 29 private final GeometryTypes geometryType; 30 31 /** The tags of the feature. Optional. */ 32 private TagMap tags; 33 34 /** The id of the feature. Optional. */ 35 // Technically, uint64 36 private final long id; 37 38 /** 39 * Create a new Feature 40 * @param layer The layer the feature is part of (required for tags) 41 * @param record The record to create the feature from 42 * @throws IOException - if an IO error occurs 43 */ 44 public Feature(Layer layer, ProtoBufRecord record) throws IOException { 45 long tId = 0; 46 GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN; 47 String key = null; 48 try (ProtoBufParser parser = new ProtoBufParser(record.getBytes())) { 49 while (parser.hasNext()) { 50 try (ProtoBufRecord next = new ProtoBufRecord(parser)) { 51 if (next.getField() == TAG_FIELD) { 52 if (tags == null) { 53 tags = new TagMap(); 54 } 55 // This is packed in v1 and v2 56 ProtoBufPacked packed = new ProtoBufPacked(next.getBytes()); 57 for (Number number : packed.getArray()) { 58 key = parseTagValue(key, layer, number); 59 } 60 } else if (next.getField() == GEOMETRY_FIELD) { 61 // This is packed in v1 and v2 62 ProtoBufPacked packed = new ProtoBufPacked(next.getBytes()); 63 CommandInteger currentCommand = null; 64 for (Number number : packed.getArray()) { 65 if (currentCommand != null && currentCommand.hasAllExpectedParameters()) { 66 currentCommand = null; 67 } 68 if (currentCommand == null) { 69 currentCommand = new CommandInteger(number.intValue()); 70 this.geometry.add(currentCommand); 71 } else { 72 currentCommand.addParameter(ParameterInteger.decode(number.intValue())); 73 } 74 } 75 // TODO fallback to non-packed 76 } else if (next.getField() == GEOMETRY_TYPE_FIELD) { 77 geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()]; 78 } else if (next.getField() == ID_FIELD) { 79 tId = next.asUnsignedVarInt().longValue(); 80 } 81 } 82 } 83 } 84 this.id = tId; 85 this.geometryType = geometryTypeTemp; 86 record.close(); 87 } 88 89 /** 90 * Parse a tag value 91 * @param key The current key (or {@code null}, if {@code null}, the returned value will be the new key) 92 * @param layer The layer with key/value information 93 * @param number The number to get the value from 94 * @return The new key (if {@code null}, then a value was parsed and added to tags) 95 */ 96 private String parseTagValue(String key, Layer layer, Number number) { 97 if (key == null) { 98 key = layer.getKey(number.intValue()); 99 } else { 100 Object value = layer.getValue(number.intValue()); 101 if (value instanceof Double || value instanceof Float) { 102 // reset grouping if the instance is a singleton 103 final NumberFormat numberFormat = NumberFormat.getNumberInstance(); 104 final boolean grouping = numberFormat.isGroupingUsed(); 105 try { 106 numberFormat.setGroupingUsed(false); 107 this.tags.put(key, numberFormat.format(value)); 108 } finally { 109 numberFormat.setGroupingUsed(grouping); 110 } 111 } else { 112 this.tags.put(key, Utils.intern(value.toString())); 113 } 114 key = null; 115 } 116 return key; 117 } 118 119 /** 120 * Get the geometry instructions 121 * @return The geometry 122 */ 123 public List<CommandInteger> getGeometry() { 124 return this.geometry; 125 } 126 127 /** 128 * Get the geometry type 129 * @return The {@link GeometryTypes} 130 */ 131 public GeometryTypes getGeometryType() { 132 return this.geometryType; 133 } 134 135 /** 136 * Get the id of the object 137 * @return The unique id in the layer, or 0. 138 */ 139 public long getId() { 140 return this.id; 141 } 142 143 /** 144 * Get the tags 145 * @return A tag map 146 */ 147 public TagMap getTags() { 148 return this.tags; 149 } 150 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.Shape; 7 import java.awt.geom.Area; 8 import java.awt.geom.Ellipse2D; 9 import java.awt.geom.Path2D; 10 import java.awt.geom.Point2D; 11 import java.util.Collection; 12 import java.util.Collections; 13 import java.util.HashSet; 14 import java.util.List; 15 16 /** 17 * A class to generate geometry for a vector tile 18 * @author Taylor Smock 19 * @since xxx 20 */ 21 public class Geometry { 22 private static final byte CIRCLE_SIZE = 0; 23 final Collection<Shape> shapes = new HashSet<>(); 24 private final Feature feature; 25 26 /** 27 * Create a {@link Geometry} for a {@link Feature} 28 * @param feature the {@link Feature} for the geometry 29 */ 30 public Geometry(final Feature feature) { 31 this.feature = feature; 32 final GeometryTypes geometryType = this.feature.getGeometryType(); 33 final List<CommandInteger> commands = this.feature.getGeometry(); 34 final byte circleSize = CIRCLE_SIZE; 35 if (geometryType == GeometryTypes.POINT) { 36 for (CommandInteger command : commands) { 37 final short[] operations = command.getOperations(); 38 // Each MoveTo command is a new point 39 if (command.getType() == Command.MoveTo && operations.length % 2 == 0) { 40 for (int i = 0; i < operations.length / 2; i++) { 41 // move left/up by 1/2 circleSize, so that the circle is centered 42 shapes.add(new Ellipse2D.Float(operations[2 * i] - circleSize / 2f, 43 operations[2 * i + 1] - circleSize / 2f, circleSize, circleSize)); 44 } 45 } else { 46 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 47 } 48 } 49 } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) { 50 Path2D.Float line = null; 51 Area area = null; 52 // MVT uses delta encoding. Each feature starts at (0, 0). 53 double x = 0; 54 double y = 0; 55 // Area is used to determine the inner/outer of a polygon 56 double areaAreaSq = 0; 57 for (CommandInteger command : commands) { 58 final short[] operations = command.getOperations(); 59 // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior 60 if (command.getType() == Command.MoveTo && operations.length == 2) { 61 areaAreaSq = 0; 62 x += operations[0]; 63 y += operations[1]; 64 line = new Path2D.Float(); 65 line.moveTo(x, y); 66 shapes.add(line); 67 } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) { 68 for (int i = 0; i < operations.length / 2; i++) { 69 final double lx = x; 70 final double ly = y; 71 x += operations[2 * i]; 72 y += operations[2 * i + 1]; 73 areaAreaSq += lx * y - x * ly; 74 line.lineTo(x, y); 75 } 76 // ClosePath should only be used with Polygon geometry 77 } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) { 78 // ClosePath specifically does not change the cursor position 79 line.closePath(); 80 line.setWindingRule(Path2D.WIND_NON_ZERO); 81 shapes.remove(line); 82 if (area == null) { 83 area = new Area(line); 84 shapes.add(area); 85 } else { 86 Area nArea = new Area(line); 87 // SonarLint thinks that this is never > 0. It can be. 88 if (areaAreaSq > 0) { 89 area.add(nArea); 90 } else { 91 area.exclusiveOr(nArea); 92 } 93 } 94 } else { 95 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 96 } 97 } 98 } 99 } 100 101 /** 102 * Get the feature for this geometry 103 * @return The feature 104 */ 105 public Feature getFeature() { 106 return this.feature; 107 } 108 109 /** 110 * Get the shapes to draw this geometry with 111 * @return A collection of shapes 112 */ 113 public Collection<Shape> getShapes() { 114 return Collections.unmodifiableCollection(this.shapes); 115 } 116 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 /** 5 * Geometry types used by Mapbox Vector Tiles 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public enum GeometryTypes { 10 /** May be ignored */ 11 UNKNOWN((byte) 0), 12 /** May be a point or a multipoint geometry. Uses <i>only</i> {@link Command#MoveTo}. Multiple {@link Command#MoveTo} 13 * indicates that it is a multi-point object. */ 14 POINT((byte) 1), 15 /** May be a line or a multiline geometry. Each line {@link Command#MoveTo} and one or more {@link Command#LineTo}. */ 16 LINESTRING((byte) 2), 17 /** May be a polygon or a multipolygon. Each ring uses a {@link Command#MoveTo}, one or more {@link Command#LineTo}, 18 * and one {@link Command#ClosePath} command. See {@link Ring}s. */ 19 POLYGON((byte) 3); 20 21 private final byte id; 22 GeometryTypes(byte id) { 23 this.id = id; 24 } 25 26 /** 27 * Get the id for the geometry type 28 * @return The id 29 */ 30 public byte getId() { 31 return this.id; 32 } 33 34 /** 35 * Rings used by {@link GeometryTypes#POLYGON} 36 * @author Taylor Smock 37 */ 38 public enum Ring { 39 /** A ring that goes in the clockwise direction */ 40 ExteriorRing, 41 /** A ring that goes in the anti-clockwise direction */ 42 InteriorRing 43 } 44 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/InvalidMapboxVectorTileException.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 /** 5 * Thrown when a mapbox vector tile does not match specifications. 6 * 7 * @author Taylor Smock 8 * @since xxx 9 */ 10 public class InvalidMapboxVectorTileException extends RuntimeException { 11 /** 12 * Create a default {@link InvalidMapboxVectorTileException}. 13 */ 14 public InvalidMapboxVectorTileException() { 15 super(); 16 } 17 18 /** 19 * Create a new {@link InvalidMapboxVectorTile} exception with a message 20 * @param message The message 21 */ 22 public InvalidMapboxVectorTileException(final String message) { 23 super(message); 24 } 25 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 import java.io.IOException; 6 import java.util.ArrayList; 7 import java.util.Arrays; 8 import java.util.Collection; 9 import java.util.Collections; 10 import java.util.HashSet; 11 import java.util.List; 12 import java.util.Map; 13 import java.util.Objects; 14 import java.util.function.Function; 15 import java.util.stream.Collectors; 16 17 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 18 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 19 import org.openstreetmap.josm.tools.Logging; 20 21 /** 22 * A Mapbox Vector Tile Layer 23 * @author Taylor Smock 24 * @since xxx 25 */ 26 public class Layer { 27 private static final class ValueFields<T> { 28 static final ValueFields<String> STRING = new ValueFields<>(1, ProtoBufRecord::asString); 29 static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtoBufRecord::asFloat); 30 static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtoBufRecord::asDouble); 31 static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtoBufRecord::asUnsignedVarInt); 32 // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE}) 33 static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtoBufRecord::asUnsignedVarInt); 34 static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtoBufRecord::asSignedVarInt); 35 static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0); 36 37 public static final Collection<ValueFields<?>> MAPPERS = Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL); 38 39 private final byte field; 40 private final Function<ProtoBufRecord, T> conversion; 41 private ValueFields(int field, Function<ProtoBufRecord, T> conversion) { 42 this.field = (byte) field; 43 this.conversion = conversion; 44 } 45 46 /** 47 * Get the field identifier for the value 48 * @return The identifier 49 */ 50 public byte getField() { 51 return this.field; 52 } 53 54 /** 55 * Convert a protobuf record to a value 56 * @param protobufRecord The record to convert 57 * @return the converted value 58 */ 59 public T convertValue(ProtoBufRecord protobufRecord) { 60 return this.conversion.apply(protobufRecord); 61 } 62 } 63 64 /** The field value for a layer (in {@link ProtoBufRecord#getField}) */ 65 public static final byte LAYER_FIELD = 3; 66 private static final byte VERSION_FIELD = 15; 67 private static final byte NAME_FIELD = 1; 68 private static final byte FEATURE_FIELD = 2; 69 private static final byte KEY_FIELD = 3; 70 private static final byte VALUE_FIELD = 4; 71 private static final byte EXTENT_FIELD = 5; 72 /** The default extent for a vector tile */ 73 static final int DEFAULT_EXTENT = 4096; 74 private static final byte DEFAULT_VERSION = 1; 75 /** This is <i>technically</i> an integer, but there are currently only two major versions (1, 2). Required. */ 76 private final byte version; 77 /** A unique name for the layer. This <i>must</i> be unique on a per-tile basis. Required. */ 78 private final String name; 79 80 /** The extent of the tile, typically 4096. Required. */ 81 private final int extent; 82 83 /** A list of unique keys. Order is important. Optional. */ 84 private final List<String> keyList = new ArrayList<>(); 85 /** A list of unique values. Order is important. Optional. */ 86 private final List<Object> valueList = new ArrayList<>(); 87 /** The actual features of this layer in this tile */ 88 private final List<Feature> featureCollection; 89 /** The shapes to use to draw this layer */ 90 private final List<Geometry> geometryCollection; 91 92 /** 93 * Create a layer from a collection of records 94 * @param records The records to convert to a layer 95 * @throws IOException - if an IO error occurs 96 */ 97 public Layer(Collection<ProtoBufRecord> records) throws IOException { 98 // Do the unique required fields first 99 Map<Integer, List<ProtoBufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtoBufRecord::getField)); 100 this.version = sorted.get((int) VERSION_FIELD).parallelStream().map(ProtoBufRecord::asUnsignedVarInt).map(Number::byteValue) 101 .findFirst().orElse(DEFAULT_VERSION); 102 // Per spec, we cannot continue past this until we have checked the version number 103 if (this.version != 1 && this.version != 2) { 104 throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version)); 105 } 106 this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString).findFirst() 107 .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name"))); 108 this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asSignedVarInt) 109 .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT); 110 111 sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::asString) 112 .forEachOrdered(this.keyList::add); 113 sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtoBufRecord::getBytes) 114 .map(ProtoBufParser::new).map(parser1 -> { 115 try { 116 return new ProtoBufRecord(parser1); 117 } catch (IOException e) { 118 return null; 119 } 120 }) 121 .filter(Objects::nonNull) 122 .map(value -> ValueFields.MAPPERS.parallelStream() 123 .filter(v -> v.getField() == value.getField()) 124 .map(v -> v.convertValue(value)).findFirst() 125 .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField())))) 126 .forEachOrdered(this.valueList::add); 127 Collection<IOException> exceptions = new HashSet<>(0); 128 this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> { 129 try { 130 return new Feature(this, feature); 131 } catch (IOException e) { 132 exceptions.add(e); 133 } 134 return null; 135 }).collect(Collectors.toList()); 136 this.geometryCollection = this.featureCollection.stream().map(Geometry::new).collect(Collectors.toList()); 137 if (!exceptions.isEmpty()) { 138 throw exceptions.iterator().next(); 139 } 140 // Cleanup bytes (for memory) 141 for (ProtoBufRecord record : records) { 142 try { 143 record.close(); 144 } catch (Exception e) { 145 Logging.error(e); 146 } 147 } 148 } 149 150 /** 151 * Create a new layer 152 * @param bytes The bytes that the layer comes from 153 * @throws IOException - if an IO error occurs 154 */ 155 public Layer(byte[] bytes) throws IOException { 156 this(new ProtoBufParser(bytes).allRecords()); 157 } 158 159 /** 160 * Get the extent of the tile 161 * @return The layer extent 162 */ 163 public int getExtent() { 164 return this.extent; 165 } 166 167 /** 168 * Get the feature on this layer 169 * @return the features 170 */ 171 public Collection<Feature> getFeatures() { 172 return Collections.unmodifiableCollection(this.featureCollection); 173 } 174 175 /** 176 * Get the geometry for this layer 177 * @return The geometry 178 */ 179 public Collection<Geometry> getGeometry() { 180 return Collections.unmodifiableCollection(this.geometryCollection); 181 } 182 183 /** 184 * Get a specified key 185 * @param index The index in the key list 186 * @return The actual key 187 */ 188 public String getKey(int index) { 189 return this.keyList.get(index); 190 } 191 192 /** 193 * Get the name of the layer 194 * @return The layer name 195 */ 196 public String getName() { 197 return this.name; 198 } 199 200 /** 201 * Get a specified value 202 * @param index The index in the value list 203 * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value. 204 */ 205 public Object getValue(int index) { 206 return this.valueList.get(index); 207 } 208 209 /** 210 * Get the MapBox Vector Tile version specification for this layer 211 * @return The version of the MapBox Vector Tile specification 212 */ 213 public byte getVersion() { 214 return this.version; 215 } 216 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoader.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.concurrent.ThreadPoolExecutor; 5 6 import org.apache.commons.jcs3.access.behavior.ICacheAccess; 7 import org.openstreetmap.gui.jmapviewer.Tile; 8 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 9 import org.openstreetmap.gui.jmapviewer.interfaces.TileJob; 10 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 11 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 12 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 13 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 14 import org.openstreetmap.josm.data.cache.JCSCachedTileLoaderJob; 15 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 16 import org.openstreetmap.josm.data.imagery.TileJobOptions; 17 import org.openstreetmap.josm.data.preferences.IntegerProperty; 18 import org.openstreetmap.josm.tools.CheckParameterUtil; 19 20 /** 21 * A TileLoader class for MVT tiles 22 * @author Taylor Smock 23 * @since xxx 24 */ 25 public class MapBoxVectorCachedTileLoader implements TileLoader, CachedTileLoader { 26 protected final ICacheAccess<String, BufferedImageCacheEntry> cache; 27 protected final TileLoaderListener listener; 28 protected final TileJobOptions options; 29 private static final IntegerProperty THREAD_LIMIT = 30 new IntegerProperty("imagery.vector.mvtloader.maxjobs", TMSCachedTileLoader.THREAD_LIMIT.getDefaultValue()); 31 private static final ThreadPoolExecutor DEFAULT_DOWNLOAD_JOB_DISPATCHER = 32 TMSCachedTileLoader.getNewThreadPoolExecutor("MVT-downloader-%d", THREAD_LIMIT.get()); 33 34 /** 35 * Constructor 36 * @param listener called when tile loading has finished 37 * @param cache of the cache 38 * @param options tile job options 39 */ 40 public MapBoxVectorCachedTileLoader(TileLoaderListener listener, ICacheAccess<String, BufferedImageCacheEntry> cache, 41 TileJobOptions options) { 42 CheckParameterUtil.ensureParameterNotNull(cache, "cache"); 43 this.cache = cache; 44 this.options = options; 45 this.listener = listener; 46 } 47 48 @Override 49 public void clearCache(TileSource source) { 50 this.cache.remove(source.getName() + ':'); 51 } 52 53 @Override 54 public TileJob createTileLoaderJob(Tile tile) { 55 return new MapBoxVectorCachedTileLoaderJob( 56 listener, 57 tile, 58 cache, 59 options, 60 getDownloadExecutor()); 61 } 62 63 @Override 64 public void cancelOutstandingTasks() { 65 final ThreadPoolExecutor executor = getDownloadExecutor(); 66 executor.getQueue().stream().filter(executor::remove).filter(MapBoxVectorCachedTileLoaderJob.class::isInstance) 67 .map(MapBoxVectorCachedTileLoaderJob.class::cast).forEach(JCSCachedTileLoaderJob::handleJobCancellation); 68 } 69 70 @Override 71 public boolean hasOutstandingTasks() { 72 return getDownloadExecutor().getTaskCount() > getDownloadExecutor().getCompletedTaskCount(); 73 } 74 75 private ThreadPoolExecutor getDownloadExecutor() { 76 return DEFAULT_DOWNLOAD_JOB_DISPATCHER; 77 } 78 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapBoxVectorCachedTileLoaderJob.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.concurrent.ThreadPoolExecutor; 5 6 import org.apache.commons.jcs3.access.behavior.ICacheAccess; 7 import org.openstreetmap.gui.jmapviewer.Tile; 8 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 9 import org.openstreetmap.josm.data.cache.BufferedImageCacheEntry; 10 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoaderJob; 11 import org.openstreetmap.josm.data.imagery.TileJobOptions; 12 13 /** 14 * Bridge to JCS cache for MVT tiles 15 * @author Taylor Smock 16 * @since xxx 17 */ 18 public class MapBoxVectorCachedTileLoaderJob extends TMSCachedTileLoaderJob { 19 20 public MapBoxVectorCachedTileLoaderJob(TileLoaderListener listener, Tile tile, 21 ICacheAccess<String, BufferedImageCacheEntry> cache, TileJobOptions options, 22 ThreadPoolExecutor downloadExecutor) { 23 super(listener, tile, cache, options, downloadExecutor); 24 } 25 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MapboxVectorTileSource.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import org.openstreetmap.josm.data.imagery.ImageryInfo; 5 import org.openstreetmap.josm.data.imagery.JosmTemplatedTMSTileSource; 6 import org.openstreetmap.josm.data.projection.Projection; 7 8 /** 9 * Tile Source handling for Mapbox Vector Tile sources 10 * @author Taylor Smock 11 * @since xxx 12 */ 13 public class MapboxVectorTileSource extends JosmTemplatedTMSTileSource { 14 public MapboxVectorTileSource(ImageryInfo info) { 15 super(info); 16 } 17 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTFile.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.util.Arrays; 5 import java.util.List; 6 7 /** 8 * Items that MAY be used to figure out if a file or server response MAY BE a Mapbox Vector Tile 9 * @author Taylor Smock 10 * @since xxx 11 */ 12 public final class MVTFile { 13 /** 14 * Extensions for Mapbox Vector Tiles. 15 * This is a SHOULD, <i>not</i> a MUST. 16 */ 17 public static final List<String> EXTENSION = Arrays.asList("mvt"); 18 19 /** 20 * mimetypes for Mapbox Vector Tiles 21 * This is a SHOULD, <i>not</i> a MUST. 22 */ 23 public static final List<String> MIMETYPE = Arrays.asList("application/vnd.mapbox-vector-tile"); 24 25 /** 26 * The default projection. This is Web Mercator, per specification. 27 */ 28 public static final String DEFAULT_PROJECTION = "EPSG:3857"; 29 30 private MVTFile() { 31 // Hide the constructor 32 } 33 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 import java.awt.Color; 5 import java.awt.Graphics; 6 import java.awt.Graphics2D; 7 import java.awt.Shape; 8 import java.awt.Stroke; 9 import java.awt.geom.AffineTransform; 10 import java.awt.geom.Area; 11 import java.awt.geom.Ellipse2D; 12 import java.awt.geom.Path2D; 13 import java.awt.image.BufferedImage; 14 import java.awt.image.ImageObserver; 15 import java.io.IOException; 16 import java.io.InputStream; 17 import java.util.ArrayList; 18 import java.util.Collection; 19 import java.util.HashSet; 20 import java.util.List; 21 import java.util.Objects; 22 import java.util.function.UnaryOperator; 23 import java.util.stream.Collectors; 24 25 import org.openstreetmap.gui.jmapviewer.Tile; 26 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 27 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 28 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 29 import org.openstreetmap.josm.data.protobuf.ProtoBufParser; 30 import org.openstreetmap.josm.data.protobuf.ProtoBufRecord; 31 import org.openstreetmap.josm.data.vector.VectorDataSet; 32 import org.openstreetmap.josm.tools.ListenerList; 33 import org.openstreetmap.josm.tools.Logging; 34 35 /** 36 * A class for MapBox Vector Tiles 37 * @author Taylor Smock 38 * @since xxx 39 */ 40 public class MVTTile extends Tile implements VectorTile { 41 private Collection<Layer> layers; 42 private int extent = Layer.DEFAULT_EXTENT; 43 private final ListenerList<TileListener> listenerList = ListenerList.create(); 44 private LayerShower layerShower; 45 46 public MVTTile(TileSource source, int xtile, int ytile, int zoom) { 47 super(source, xtile, ytile, zoom); 48 } 49 50 @Override 51 public void paint(final Graphics g, final int x, final int y) { 52 this.paint(g, x, y, 256, 256); 53 } 54 55 @Override 56 public void paint(Graphics g, int x, int y, int width, int height, int zoom, ImageObserver observer) { 57 if (!(g instanceof Graphics2D) || this.layers == null) { 58 if (getImage() != null) { 59 g.drawImage(image, x, y, width, height, observer); 60 } 61 return; 62 } 63 final Graphics2D graphics = (Graphics2D) g; 64 graphics.setColor(Color.GREEN); 65 final AffineTransform originalTransform = graphics.getTransform(); 66 final Stroke originalStroke = graphics.getStroke(); 67 try { 68 graphics.translate(x, y); 69 // TODO figure out HiDPI (maybe GuiSizesHelper?) 70 // 131072 seems to be the magic number for my screens. This needs to be investigated more. 71 final double scale = width / (double) (32768); 72 // The scaleTransform is separate to avoid wide lines at high zoom (e.g., when vector tiles go to z14, but 73 // we are currently at z20, the graphics.scale function makes everything big. Unfortunately, this creates 74 // a new shape object. 75 final UnaryOperator<Shape> scaleShape; 76 if (scale > 1) { 77 final AffineTransform scaleTransform = AffineTransform.getScaleInstance(scale, scale); 78 scaleShape = scaleTransform::createTransformedShape; 79 } else { 80 // Cannot use Objects.identity() since it isn't a UnaryOperator 81 scaleShape = s -> s; 82 graphics.scale(scale, scale); 83 } 84 final Color transparentYellow = new Color(Color.YELLOW.getRed(), Color.YELLOW.getGreen(), Color.YELLOW.getBlue(), 120); 85 Collection<Layer> layersToShow = new ArrayList<>(); 86 if (this.layerShower != null) { 87 this.layerShower.layersToShow().stream().map(layer -> this.layers.stream().filter(l -> Objects.equals(layer, l.getName())).findAny().orElse(null)).filter(Objects::nonNull).forEach(layersToShow::add); 88 } else { 89 layersToShow.addAll(this.layers); 90 } 91 for (Layer layer : layersToShow) { 92 layer.getGeometry().forEach(shapes -> { 93 for (Shape shape : shapes.getShapes()) { 94 final Shape scaledShape = scaleShape.apply(shape); 95 if (shape instanceof Ellipse2D) { 96 graphics.setColor(Color.GREEN); 97 } else if (shape instanceof Path2D) { 98 graphics.setColor(Color.RED); 99 } else if (shape instanceof Area) { 100 graphics.setColor(transparentYellow); 101 graphics.fill(scaledShape); 102 graphics.setColor(Color.YELLOW); 103 } 104 graphics.draw(scaledShape); 105 } 106 }); 107 } 108 } finally { 109 graphics.setTransform(originalTransform); 110 graphics.setStroke(originalStroke); 111 } 112 } 113 114 @Override 115 public void loadImage(final InputStream inputStream) throws IOException { 116 if (this.image == null || this.image == Tile.LOADING_IMAGE || this.image == Tile.ERROR_IMAGE) { 117 this.initLoading(); 118 ProtoBufParser parser = new ProtoBufParser(inputStream); 119 Collection<ProtoBufRecord> protoBufRecords = parser.allRecords(); 120 this.layers = new HashSet<>(); 121 this.layers = protoBufRecords.stream().map(record -> { 122 Layer mvtLayer = null; 123 if (record.getField() == Layer.LAYER_FIELD) { 124 try (ProtoBufParser tParser = new ProtoBufParser(record.getBytes())) { 125 mvtLayer = new Layer(tParser.allRecords()); 126 } catch (IOException e) { 127 Logging.error(e); 128 } finally { 129 // Cleanup bytes 130 record.close(); 131 } 132 } 133 return mvtLayer; 134 }).collect(Collectors.toCollection(HashSet::new)); 135 this.extent = layers.stream().map(Layer::getExtent).max(Integer::compare).orElse(Layer.DEFAULT_EXTENT); 136 BufferedImage bufferedImage = new BufferedImage(this.extent, this.extent, BufferedImage.TYPE_4BYTE_ABGR); 137 Graphics2D graphics = bufferedImage.createGraphics(); 138 this.paint(graphics, 0, 0); 139 140 // TODO figure out a better way to free memory 141 final int maxSize = 256; 142 final BufferedImage resized = new BufferedImage(maxSize, maxSize, bufferedImage.getType()); 143 resized.getGraphics().drawImage(bufferedImage, 0, 0, resized.getWidth(), resized.getHeight(), null); 144 this.image = resized; 145 this.finishLoading(); 146 Collection<TileListener> fired = new ArrayList<>(); 147 this.listenerList.fireEvent(event -> { 148 event.finishedLoading(this); 149 fired.add(event); 150 }); 151 // We shouldn't keep object references around once we no longer need them 152 fired.forEach(this.listenerList::removeListener); 153 } 154 } 155 156 @Override 157 public Collection<Layer> getLayers() { 158 return this.layers; 159 } 160 161 @Override 162 public int getExtent() { 163 return this.extent; 164 } 165 166 /** 167 * Set the object that will determine what layers are shown 168 * @param shower The class that will determine the layers to paint 169 */ 170 public void setLayerShower(LayerShower shower) { 171 this.layerShower = shower; 172 } 173 174 175 /** 176 * A class that can be notified that a tile has finished loading 177 * @author Taylor Smock 178 * 179 */ 180 public static interface TileListener { 181 /** 182 * Called when the MVTTile is finished loading 183 * @param tile The tile that finished loading 184 */ 185 void finishedLoading(final MVTTile tile); 186 } 187 188 /** 189 * A class used to set the layers that an MVTTile will show. 190 * @author Taylor Smock 191 * 192 */ 193 public static interface LayerShower { 194 /** 195 * Get a list of layers to show 196 * @return A list of layer names 197 */ 198 List<String> layersToShow(); 199 } 200 201 /** 202 * Add a tile loader finisher listener 203 * @param listener The listener to add 204 */ 205 public void addTileLoaderFinisher(TileListener listener) { 206 // Add as weak listeners since we don't want to keep unnecessary references. 207 this.listenerList.addWeakListener(listener); 208 } 209 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/ParameterInteger.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 4 /** 5 * The parameters that follow the {@link CommandInteger}. 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public final class ParameterInteger { 10 private ParameterInteger() { 11 // Hide constructor 12 } 13 14 /** 15 * Get the value for this ParameterInteger 16 * @param value The zig-zag and delta encoded value to decode 17 * @return The decoded integer value 18 */ 19 public static int decode(int value) { 20 return ((value >> 1) ^ -(value & 1)); 21 } 22 } -
new file src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/VectorTile.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.imagery.vectortile; 3 4 import java.awt.Graphics; 5 import java.awt.image.ImageObserver; 6 import java.util.Collection; 7 8 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 9 import org.openstreetmap.josm.data.vector.VectorDataSet; 10 11 /** 12 * An interface that is used to draw vector tiles, instead of using images 13 * @author Taylor Smock 14 * @since xxx 15 */ 16 public interface VectorTile { 17 /** 18 * Paints the vector tile on the {@link Graphics} <code>g</code> at the 19 * position <code>x</code>/<code>y</code>. 20 * 21 * @param g the Graphics object 22 * @param x x-coordinate in <code>g</code> 23 * @param y y-coordinate in <code>g</code> 24 */ 25 @Deprecated 26 void paint(Graphics g, int x, int y); 27 28 /** 29 * Paints the vector tile on the {@link Graphics} <code>g</code> at the 30 * position <code>x</code>/<code>y</code>. 31 * @param g the Graphics object 32 * @param x x-coordinate in <code>g</code> 33 * @param y y-coordinate in <code>g</code> 34 * @param width width that tile should have 35 * @param height height that tile should have 36 * @param observer The paint observer. May be {@code null}. 37 * @param zoom The current zoom level 38 */ 39 @Deprecated 40 void paint(Graphics g, int x, int y, int width, int height, int zoom, ImageObserver observer); 41 42 /** 43 * Get the layers for this vector tile 44 * @return A collection of layers 45 */ 46 Collection<Layer> getLayers(); 47 48 /** 49 * Get the extent of the tile (in pixels) 50 * @return The tile extent (pixels) 51 */ 52 int getExtent(); 53 } -
new file src/org/openstreetmap/josm/data/osm/IWaySegment.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/IWaySegment.java b/src/org/openstreetmap/josm/data/osm/IWaySegment.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.osm; 3 4 import java.awt.geom.Line2D; 5 import java.util.Arrays; 6 import java.util.Objects; 7 8 import org.openstreetmap.josm.tools.Logging; 9 10 /** 11 * A segment consisting of 2 consecutive nodes out of a way. 12 */ 13 public class IWaySegment<N extends INode, W extends IWay<N>> implements Comparable<IWaySegment> { 14 15 /** 16 * The way. 17 */ 18 public final W way; 19 20 /** 21 * The index of one of the 2 nodes in the way. The other node has the 22 * index <code>lowerIndex + 1</code>. 23 */ 24 public final int lowerIndex; 25 26 /** 27 * Constructs a new {@code IWaySegment}. 28 * @param w The way 29 * @param i The node lower index 30 * @throws IllegalArgumentException in case of invalid index 31 */ 32 public IWaySegment(W w, int i) { 33 way = w; 34 lowerIndex = i; 35 if (i < 0 || i >= w.getNodesCount() - 1) { 36 throw new IllegalArgumentException(toString()); 37 } 38 } 39 40 /** 41 * Returns the first node of the way segment. 42 * @return the first node 43 */ 44 public N getFirstNode() { 45 return way.getNode(lowerIndex); 46 } 47 48 /** 49 * Returns the second (last) node of the way segment. 50 * @return the second node 51 */ 52 public N getSecondNode() { 53 return way.getNode(lowerIndex + 1); 54 } 55 56 /** 57 * Determines and returns the way segment for the given way and node pair. 58 * @param way way 59 * @param first first node 60 * @param second second node 61 * @return way segment 62 * @throws IllegalArgumentException if the node pair is not part of way 63 */ 64 public static <N extends INode, W extends IWay<N>> IWaySegment<N, W> forNodePair(W way, N first, N second) { 65 int endIndex = way.getNodesCount() - 1; 66 while (endIndex > 0) { 67 final int indexOfFirst = way.getNodes().subList(0, endIndex).lastIndexOf(first); 68 if (second.equals(way.getNode(indexOfFirst + 1))) { 69 return new IWaySegment<>(way, indexOfFirst); 70 } 71 endIndex--; 72 } 73 throw new IllegalArgumentException("Node pair is not part of way!"); 74 } 75 76 /** 77 * Returns this way segment as complete way. 78 * @return the way segment as {@code Way} 79 */ 80 public W toWay() throws IllegalAccessException, InstantiationException { 81 W w = (W) this.way.getClass().newInstance(); 82 w.setNodes(Arrays.asList(getFirstNode(), getSecondNode())); 83 return w; 84 } 85 86 @Override 87 public boolean equals(Object o) { 88 if (this == o) return true; 89 if (o == null || getClass() != o.getClass()) return false; 90 IWaySegment that = (IWaySegment) o; 91 return lowerIndex == that.lowerIndex && 92 Objects.equals(way, that.way); 93 } 94 95 @Override 96 public int hashCode() { 97 return Objects.hash(way, lowerIndex); 98 } 99 100 @Override 101 public int compareTo(IWaySegment o) { 102 final W thisWay; 103 final IWay otherWay; 104 try { 105 thisWay = toWay(); 106 otherWay = o == null ? null : o.toWay(); 107 } catch (IllegalAccessException | InstantiationException e) { 108 Logging.error(e); 109 return -1; 110 } 111 return o == null ? -1 : (equals(o) ? 0 : thisWay.compareTo(otherWay)); 112 } 113 114 /** 115 * Checks whether this segment crosses other segment 116 * 117 * @param s2 The other segment 118 * @return true if both segments crosses 119 */ 120 public boolean intersects(IWaySegment s2) { 121 if (getFirstNode().equals(s2.getFirstNode()) || getSecondNode().equals(s2.getSecondNode()) || 122 getFirstNode().equals(s2.getSecondNode()) || getSecondNode().equals(s2.getFirstNode())) 123 return false; 124 125 return Line2D.linesIntersect( 126 getFirstNode().getEastNorth().east(), getFirstNode().getEastNorth().north(), 127 getSecondNode().getEastNorth().east(), getSecondNode().getEastNorth().north(), 128 s2.getFirstNode().getEastNorth().east(), s2.getFirstNode().getEastNorth().north(), 129 s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north()); 130 } 131 132 /** 133 * Checks whether this segment and another way segment share the same points 134 * @param s2 The other segment 135 * @return true if other way segment is the same or reverse 136 */ 137 public boolean isSimilar(IWaySegment s2) { 138 return (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode())) 139 || (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode())); 140 } 141 142 @Override 143 public String toString() { 144 return "IWaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']'; 145 } 146 } -
src/org/openstreetmap/josm/data/osm/WaySegment.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/osm/WaySegment.java b/src/org/openstreetmap/josm/data/osm/WaySegment.java
a b 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.osm; 3 3 4 import java.awt.geom.Line2D;5 import java.util.Objects;6 7 4 /** 8 5 * A segment consisting of 2 consecutive nodes out of a way. 9 6 */ 10 public final class WaySegment implements Comparable<WaySegment> { 11 12 /** 13 * The way. 14 */ 15 public final Way way; 16 17 /** 18 * The index of one of the 2 nodes in the way. The other node has the 19 * index <code>lowerIndex + 1</code>. 20 */ 21 public final int lowerIndex; 7 public final class WaySegment extends IWaySegment<Node, Way> { 22 8 23 9 /** 24 * Constructs a new {@code WaySegment}. 25 * @param w The way 26 * @param i The node lower index 10 * Constructs a new {@code IWaySegment}. 11 * 12 * @param way The way 13 * @param i The node lower index 27 14 * @throws IllegalArgumentException in case of invalid index 28 15 */ 29 public WaySegment(Way w, int i) { 30 way = w; 31 lowerIndex = i; 32 if (i < 0 || i >= w.getNodesCount() - 1) { 33 throw new IllegalArgumentException(toString()); 34 } 35 } 36 37 /** 38 * Returns the first node of the way segment. 39 * @return the first node 40 */ 41 public Node getFirstNode() { 42 return way.getNode(lowerIndex); 16 public WaySegment(Way way, int i) { 17 super(way, i); 43 18 } 44 19 45 20 /** 46 * Returns the second (last) node of the way segment. 47 * @return the second node 48 */ 49 public Node getSecondNode() { 50 return way.getNode(lowerIndex + 1); 51 } 52 53 /** 54 * Determines and returns the way segment for the given way and node pair. 21 * Determines and returns the way segment for the given way and node pair. You should prefer 22 * {@link IWaySegment#forNodePair(IWay, INode, INode)} whenever possible. 23 * 55 24 * @param way way 56 25 * @param first first node 57 26 * @param second second node … … 74 43 * Returns this way segment as complete way. 75 44 * @return the way segment as {@code Way} 76 45 */ 46 @Override 77 47 public Way toWay() { 78 48 Way w = new Way(); 79 49 w.addNode(getFirstNode()); … … 81 51 return w; 82 52 } 83 53 84 @Override85 public boolean equals(Object o) {86 if (this == o) return true;87 if (o == null || getClass() != o.getClass()) return false;88 WaySegment that = (WaySegment) o;89 return lowerIndex == that.lowerIndex &&90 Objects.equals(way, that.way);91 }92 93 @Override94 public int hashCode() {95 return Objects.hash(way, lowerIndex);96 }97 98 @Override99 public int compareTo(WaySegment o) {100 return o == null ? -1 : (equals(o) ? 0 : toWay().compareTo(o.toWay()));101 }102 103 /**104 * Checks whether this segment crosses other segment105 *106 * @param s2 The other segment107 * @return true if both segments crosses108 */109 public boolean intersects(WaySegment s2) {110 if (getFirstNode().equals(s2.getFirstNode()) || getSecondNode().equals(s2.getSecondNode()) ||111 getFirstNode().equals(s2.getSecondNode()) || getSecondNode().equals(s2.getFirstNode()))112 return false;113 114 return Line2D.linesIntersect(115 getFirstNode().getEastNorth().east(), getFirstNode().getEastNorth().north(),116 getSecondNode().getEastNorth().east(), getSecondNode().getEastNorth().north(),117 s2.getFirstNode().getEastNorth().east(), s2.getFirstNode().getEastNorth().north(),118 s2.getSecondNode().getEastNorth().east(), s2.getSecondNode().getEastNorth().north());119 }120 121 /**122 * Checks whether this segment and another way segment share the same points123 * @param s2 The other segment124 * @return true if other way segment is the same or reverse125 */126 public boolean isSimilar(WaySegment s2) {127 return (getFirstNode().equals(s2.getFirstNode()) && getSecondNode().equals(s2.getSecondNode()))128 || (getFirstNode().equals(s2.getSecondNode()) && getSecondNode().equals(s2.getFirstNode()));129 }130 131 54 @Override 132 55 public String toString() { 133 56 return "WaySegment [way=" + way.getUniqueId() + ", lowerIndex=" + lowerIndex + ']'; -
new file src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufPacked.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import java.util.ArrayList; 5 import java.util.List; 6 7 /** 8 * Parse packed values (only numerical values) 9 * @author Taylor Smock 10 * @since xxx 11 */ 12 public class ProtoBufPacked { 13 private final byte[] bytes; 14 private int location; 15 private final Number[] numbers; 16 /** 17 * Create a new ProtoBufPacked object 18 * @param bytes The packed bytes 19 */ 20 public ProtoBufPacked(byte[] bytes) { 21 this.location = 0; 22 this.bytes = bytes; 23 List<Number> numbersT = new ArrayList<>(); 24 while (this.location < bytes.length) { 25 numbersT.add(ProtoBufParser.convertByteArray(this.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE)); 26 } 27 28 this.numbers = new Number[numbersT.size()]; 29 for (int i = 0; i < numbersT.size(); i++) { 30 this.numbers[i] = numbersT.get(i); 31 } 32 } 33 34 /** 35 * The number of expected values 36 * @return The expected values 37 */ 38 public int size() { 39 return this.numbers.length; 40 } 41 42 /** 43 * Get the parsed number array 44 * @return The number array 45 */ 46 public Number[] getArray() { 47 return this.numbers; 48 } 49 50 private byte[] nextVarInt() { 51 List<Byte> byteList = new ArrayList<>(); 52 while ((this.bytes[this.location] & ProtoBufParser.MOST_SIGNIFICANT_BYTE) == ProtoBufParser.MOST_SIGNIFICANT_BYTE) { 53 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned) 54 byteList.add((byte) (this.bytes[this.location++] ^ ProtoBufParser.MOST_SIGNIFICANT_BYTE)); 55 } 56 // The last byte doesn't drop the most significant bit 57 byteList.add(this.bytes[this.location++]); 58 byte[] byteArray = new byte[byteList.size()]; 59 for (int i = 0; i < byteList.size(); i++) { 60 byteArray[i] = byteList.get(i); 61 } 62 63 return byteArray; 64 } 65 } -
new file src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufParser.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import java.io.BufferedInputStream; 5 import java.io.ByteArrayInputStream; 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.util.ArrayList; 9 import java.util.Collection; 10 import java.util.List; 11 12 import org.openstreetmap.josm.tools.Logging; 13 14 /** 15 * A basic Protobuf parser 16 * @author Taylor Smock 17 * @since xxx 18 */ 19 public class ProtoBufParser implements AutoCloseable { 20 /** 21 * Used to get the most significant byte 22 */ 23 static final byte MOST_SIGNIFICANT_BYTE = (byte) (1 << 7); 24 /** The default byte size (see {@link #VAR_INT_BYTE_SIZE} for var ints) */ 25 public static final byte BYTE_SIZE = 8; 26 /** The byte size for var ints (since the first byte is just an indicator for if the var int is done) */ 27 public static final byte VAR_INT_BYTE_SIZE = BYTE_SIZE - 1; 28 // TODO switch to a better parser 29 private final InputStream inputStream; 30 /** 31 * Create a new parser 32 * @param bytes The bytes to parse 33 */ 34 public ProtoBufParser(byte[] bytes) { 35 this(new ByteArrayInputStream(bytes)); 36 } 37 38 /** 39 * Create a new parser 40 * @param inputStream The InputStream (will be fully read at this time) 41 */ 42 public ProtoBufParser(InputStream inputStream) { 43 if (inputStream.markSupported()) { 44 this.inputStream = inputStream; 45 } else { 46 this.inputStream = new BufferedInputStream(inputStream); 47 } 48 } 49 50 @Override 51 public void close() { 52 try { 53 this.inputStream.close(); 54 } catch (IOException e) { 55 Logging.error(e); 56 } 57 } 58 59 /** 60 * Get the "next" WireType 61 * @return {@link WireType} expected 62 * @throws IOException - if an IO error occurs 63 */ 64 public WireType next() throws IOException { 65 this.inputStream.mark(16); 66 try { 67 return WireType.values()[this.inputStream.read() << 3]; 68 } finally { 69 this.inputStream.reset(); 70 } 71 } 72 73 /** 74 * Get the next byte 75 * @return The next byte 76 * @throws IOException - if an IO error occurs 77 */ 78 public int nextByte() throws IOException { 79 return this.inputStream.read(); 80 } 81 82 /** 83 * Check if there is more data to read 84 * @return {@code true} if there is more data to read 85 * @throws IOException - if an IO error occurs 86 */ 87 public boolean hasNext() throws IOException { 88 return this.inputStream.available() > 0; 89 } 90 91 /** 92 * Get the next var int ({@code WireType#VARINT}) 93 * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum}) 94 * @throws IOException - if an IO error occurs 95 */ 96 public byte[] nextVarInt() throws IOException { 97 List<Byte> byteList = new ArrayList<>(); 98 int currentByte = this.nextByte(); 99 while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE) { 100 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned) 101 byteList.add((byte) (currentByte ^ MOST_SIGNIFICANT_BYTE)); 102 currentByte = this.nextByte(); 103 } 104 // The last byte doesn't drop the most significant bit 105 byteList.add((byte) currentByte); 106 byte[] byteArray = new byte[byteList.size()]; 107 for (int i = 0; i < byteList.size(); i++) { 108 byteArray[i] = byteList.get(i); 109 } 110 111 return byteArray; 112 } 113 114 /** 115 * Get the next 32 bits ({@link WireType#THIRTY_TWO_BIT}) 116 * @return a byte array of the next 32 bits (4 bytes) 117 * @throws IOException - if an IO error occurs 118 */ 119 public byte[] nextFixed32() throws IOException { 120 // 4 bytes == 32 bits 121 return readNextBytes(4); 122 } 123 124 /** 125 * Get the next 64 bits ({@link WireType#SIXTY_FOUR_BIT}) 126 * @return a byte array of the next 64 bits (8 bytes) 127 * @throws IOException - if an IO error occurs 128 */ 129 public byte[] nextFixed64() throws IOException { 130 // 8 bytes == 64 bits 131 return readNextBytes(8); 132 } 133 134 /** 135 * Read an arbitrary number of bytes 136 * @param size The number of bytes to read 137 * @return a byte array of the specified size, filled with bytes read (unsigned) 138 * @throws IOException - if an IO error occurs 139 */ 140 private byte[] readNextBytes(int size) throws IOException { 141 byte[] bytesRead = new byte[size]; 142 for (int i = 0; i < bytesRead.length; i++) { 143 bytesRead[i] = (byte) this.nextByte(); 144 } 145 return bytesRead; 146 } 147 148 /** 149 * Get the next delimited message ({@link WireType#LENGTH_DELIMITED}) 150 * @return The next length delimited message 151 * @throws IOException - if an IO error occurs 152 */ 153 public byte[] nextLengthDelimited() throws IOException { 154 int length = convertByteArray(this.nextVarInt(), VAR_INT_BYTE_SIZE).intValue(); 155 return readNextBytes(length); 156 } 157 158 /** 159 * Convert a byte array to a number (little endian) 160 * @param bytes The bytes to convert 161 * @param byteSize The size of the byte. For var ints, this is 7, for other ints, this is 8. 162 * @return An appropriate {@link Number} class. 163 */ 164 public static Number convertByteArray(byte[] bytes, byte byteSize) { 165 long number = 0; 166 for (int i = 0; i < bytes.length; i++) { 167 // Need to convert to uint64 in order to avoid bit operation from filling in 1's and overflow issues 168 number += Byte.toUnsignedLong(bytes[i]) << (byteSize * i); 169 } 170 return convertLong(number); 171 } 172 173 /** 174 * Convert a long to an appropriate {@link Number} class 175 * @param number The long to convert 176 * @return A {@link Number} 177 */ 178 public static Number convertLong(long number) { 179 // TODO deal with booleans 180 if (number <= Byte.MAX_VALUE && number >= Byte.MIN_VALUE) { 181 return (byte) number; 182 } else if (number <= Short.MAX_VALUE && number >= Short.MIN_VALUE) { 183 return (short) number; 184 } else if (number <= Integer.MAX_VALUE && number >= Integer.MIN_VALUE) { 185 return (int) number; 186 } 187 return number; 188 } 189 190 /** 191 * Read all records 192 * @return A collection of all records 193 * @throws IOException - if an IO error occurs 194 */ 195 public Collection<ProtoBufRecord> allRecords() throws IOException { 196 Collection<ProtoBufRecord> records = new ArrayList<>(); 197 while (this.hasNext()) { 198 records.add(new ProtoBufRecord(this)); 199 } 200 return records; 201 } 202 203 /** 204 * Decode a zig-zag encoded value 205 * @param signed The value to decode 206 * @return The decoded value 207 */ 208 public static Number decodeZigZag(Number signed) { 209 final long value = signed.longValue(); 210 return convertLong((value >> 1) ^ (-(value & 1))); 211 } 212 213 /** 214 * Encode a number to a zig-zag encode value 215 * @param signed The number to encode 216 * @return The encoded value 217 */ 218 public static Number encodeZigZag(Number signed) { 219 final long value = signed.longValue(); 220 final int shift = (value > Integer.MAX_VALUE ? Long.BYTES : Integer.BYTES) * 8 - 1; 221 return convertLong((value << 1) ^ (value >>> shift)); 222 } 223 } -
new file src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java b/src/org/openstreetmap/josm/data/protobuf/ProtoBufRecord.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import java.io.IOException; 5 import java.nio.charset.StandardCharsets; 6 import java.util.stream.Stream; 7 8 import org.openstreetmap.josm.tools.Utils; 9 10 /** 11 * A protobuf record, storing the {@link WireType}, the parsed field number, and the bytes for it. 12 * @author Taylor Smock 13 * @since xxx 14 */ 15 public class ProtoBufRecord implements AutoCloseable { 16 private static final byte[] EMPTY_BYTES = {}; 17 private final WireType type; 18 private final int field; 19 private byte[] bytes; 20 21 /** 22 * Create a new Protobuf record 23 * @param parser The parser to use to create the record 24 * @throws IOException - if an IO error occurs 25 */ 26 public ProtoBufRecord(ProtoBufParser parser) throws IOException { 27 Number number = ProtoBufParser.convertByteArray(parser.nextVarInt(), ProtoBufParser.VAR_INT_BYTE_SIZE); 28 // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3} 29 this.field = (int) number.longValue() >> 3; 30 // 7 is 111 (so last three bits) 31 byte wireType = (byte) (number.longValue() & 7); 32 this.type = Stream.of(WireType.values()).filter(wType -> wType.getTypeRepresentation() == wireType).findFirst().orElse(WireType.UNKNOWN); 33 34 if (this.type == WireType.VARINT) { 35 this.bytes = parser.nextVarInt(); 36 } else if (this.type == WireType.SIXTY_FOUR_BIT) { 37 this.bytes = parser.nextFixed64(); 38 } else if (this.type == WireType.THIRTY_TWO_BIT) { 39 this.bytes = parser.nextFixed32(); 40 } else if (this.type == WireType.LENGTH_DELIMITED) { 41 this.bytes = parser.nextLengthDelimited(); 42 } else { 43 this.bytes = EMPTY_BYTES; 44 } 45 } 46 47 /** 48 * Get the field value 49 * @return The field value 50 */ 51 public int getField() { 52 return this.field; 53 } 54 55 /** 56 * Get the WireType of the data 57 * @return The {@link WireType} of the data 58 */ 59 public WireType getType() { 60 return this.type; 61 } 62 63 /** 64 * Get the raw bytes for this record 65 * @return The bytes 66 */ 67 public byte[] getBytes() { 68 return this.bytes; 69 } 70 71 /** 72 * Get the var int ({@code WireType#VARINT}) 73 * @return The var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum}) 74 */ 75 public Number asUnsignedVarInt() { 76 return ProtoBufParser.convertByteArray(this.bytes, ProtoBufParser.VAR_INT_BYTE_SIZE); 77 } 78 79 /** 80 * Get the signed var int ({@code WireType#VARINT}). 81 * These are specially encoded so that they take up less space. 82 * 83 * @return The signed var int ({@code sint32} or {@code sint64}) 84 */ 85 public Number asSignedVarInt() { 86 final Number signed = this.asUnsignedVarInt(); 87 return ProtoBufParser.decodeZigZag(signed); 88 } 89 90 /** 91 * Get as a double ({@link WireType#SIXTY_FOUR_BIT}) 92 * @return the double 93 */ 94 public double asDouble() { 95 long doubleNumber = ProtoBufParser.convertByteArray(asFixed64(), ProtoBufParser.BYTE_SIZE).longValue(); 96 return Double.longBitsToDouble(doubleNumber); 97 } 98 99 /** 100 * Get as a float ({@link WireType#THIRTY_TWO_BIT}) 101 * @return the float 102 */ 103 public float asFloat() { 104 int floatNumber = ProtoBufParser.convertByteArray(asFixed32(), ProtoBufParser.BYTE_SIZE).intValue(); 105 return Float.intBitsToFloat(floatNumber); 106 } 107 108 /** 109 * Get as a string ({@link WireType#LENGTH_DELIMITED}) 110 * @return The string (encoded as {@link StandardCharsets#UTF_8}) 111 */ 112 public String asString() { 113 return Utils.intern(new String(this.bytes, StandardCharsets.UTF_8)); 114 } 115 116 /** 117 * Get as 32 bits ({@link WireType#THIRTY_TWO_BIT}) 118 * @return a byte array of the 32 bits (4 bytes) 119 */ 120 public byte[] asFixed32() { 121 // TODO verify, or just assume? 122 // 4 bytes == 32 bits 123 return this.bytes; 124 } 125 126 /** 127 * Get as 64 bits ({@link WireType#SIXTY_FOUR_BIT}) 128 * @return a byte array of the 64 bits (8 bytes) 129 */ 130 public byte[] asFixed64() { 131 // TODO verify, or just assume? 132 // 8 bytes == 64 bits 133 return this.bytes; 134 } 135 136 @Override 137 public void close() { 138 this.bytes = null; 139 } 140 } -
new file src/org/openstreetmap/josm/data/protobuf/WireType.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/protobuf/WireType.java b/src/org/openstreetmap/josm/data/protobuf/WireType.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 /** 5 * The WireTypes 6 * @author Taylor Smock 7 * @since xxx 8 */ 9 public enum WireType { 10 /** int32, int64, uint32, uint64, sing32, sint64, bool, enum */ 11 VARINT(0), 12 /** fixed64, sfixed64, double */ 13 SIXTY_FOUR_BIT(1), 14 /** string, bytes, embedded messages, packed repeated fields */ 15 LENGTH_DELIMITED(2), 16 /** 17 * start groups 18 * @deprecated Unknown reason. Deprecated since at least 2012. 19 */ 20 @Deprecated 21 START_GROUP(3), 22 /** 23 * end groups 24 * @deprecated Unknown reason. Deprecated since at least 2012. 25 */ 26 @Deprecated 27 END_GROUP(4), 28 /** fixed32, sfixed32, float */ 29 THIRTY_TWO_BIT(5), 30 31 /** For unknown WireTypes */ 32 UNKNOWN(Byte.MAX_VALUE); 33 34 private final byte type; 35 WireType(int value) { 36 this.type = (byte) value; 37 } 38 39 /** 40 * Get the type representation (byte form) 41 * @return The wire type byte representation 42 */ 43 public byte getTypeRepresentation() { 44 return this.type; 45 } 46 } -
new file src/org/openstreetmap/josm/data/vector/VectorDataSet.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataSet.java b/src/org/openstreetmap/josm/data/vector/VectorDataSet.java new file mode 100644
- + 1 package org.openstreetmap.josm.data.vector; 2 3 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 import java.awt.Color; 6 import java.awt.geom.Area; 7 import java.awt.geom.Ellipse2D; 8 import java.awt.geom.Path2D; 9 import java.awt.geom.PathIterator; 10 import java.awt.geom.Point2D; 11 import java.util.ArrayList; 12 import java.util.Arrays; 13 import java.util.Collection; 14 import java.util.Collections; 15 import java.util.HashMap; 16 import java.util.HashSet; 17 import java.util.LinkedList; 18 import java.util.List; 19 import java.util.Map; 20 import java.util.Optional; 21 import java.util.Set; 22 import java.util.concurrent.CopyOnWriteArrayList; 23 import java.util.concurrent.locks.Lock; 24 import java.util.concurrent.locks.ReentrantReadWriteLock; 25 import java.util.function.Predicate; 26 import java.util.stream.Collectors; 27 import java.util.stream.Stream; 28 29 import org.openstreetmap.gui.jmapviewer.Coordinate; 30 import org.openstreetmap.gui.jmapviewer.Tile; 31 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 32 import org.openstreetmap.josm.data.DataSource; 33 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 34 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature; 35 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 36 import org.openstreetmap.josm.data.osm.BBox; 37 import org.openstreetmap.josm.data.osm.DataSelectionListener; 38 import org.openstreetmap.josm.data.osm.DownloadPolicy; 39 import org.openstreetmap.josm.data.osm.HighlightUpdateListener; 40 import org.openstreetmap.josm.data.osm.INode; 41 import org.openstreetmap.josm.data.osm.IPrimitive; 42 import org.openstreetmap.josm.data.osm.IRelation; 43 import org.openstreetmap.josm.data.osm.IWay; 44 import org.openstreetmap.josm.data.osm.OsmData; 45 import org.openstreetmap.josm.data.osm.PrimitiveId; 46 import org.openstreetmap.josm.data.osm.QuadBucketPrimitiveStore; 47 import org.openstreetmap.josm.data.osm.Storage; 48 import org.openstreetmap.josm.data.osm.UploadPolicy; 49 import org.openstreetmap.josm.data.osm.WaySegment; 50 import org.openstreetmap.josm.data.osm.event.DataSetListener; 51 import org.openstreetmap.josm.tools.Geometry; 52 import org.openstreetmap.josm.tools.ListenerList; 53 import org.openstreetmap.josm.tools.Logging; 54 import org.openstreetmap.josm.tools.SubclassFilteredCollection; 55 56 import sun.reflect.generics.reflectiveObjects.NotImplementedException; 57 58 /** 59 * A data class for Vector Data 60 * @author Taylor Smock 61 * @since xxx 62 */ 63 public class VectorDataSet implements OsmData<VectorPrimitive, VectorNode, VectorWay, VectorRelation> { 64 private final Map<Integer, VectorDataStore> dataStoreMap = new HashMap<>(); 65 private final CopyOnWriteArrayList<DataSetListener> listeners = new CopyOnWriteArrayList<>(); 66 private final Collection<PrimitiveId> selected = new HashSet<>(); 67 // Both of these listener lists are useless, since they expect OsmPrimitives at this time 68 private final ListenerList<HighlightUpdateListener> highlightUpdateListenerListenerList = ListenerList.create(); 69 private final ListenerList<DataSelectionListener> dataSelectionListenerListenerList = ListenerList.create(); 70 private boolean lock = true; 71 private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 72 private String name; 73 /** The current zoom we are getting/adding to */ 74 private int zoom; 75 /** Default to normal download policy */ 76 private DownloadPolicy downloadPolicy = DownloadPolicy.NORMAL; 77 /** Default to a blocked upload policy */ 78 private UploadPolicy uploadPolicy = UploadPolicy.BLOCKED; 79 80 @Override public Collection<DataSource> getDataSources() { 81 final int currentZoom = this.zoom; 82 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 83 return dataStore.getDataSources(); 84 } 85 86 /** 87 * Add a data source 88 * @param currentZoom the zoom 89 * @param dataSource The datasource to add at the zoom level 90 */ 91 public void addDataSource(int currentZoom, DataSource dataSource) { 92 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 93 dataStore.addDataSource(dataSource); 94 } 95 96 @Override public void lock() { 97 this.lock = true; 98 } 99 100 @Override public void unlock() { 101 this.lock = false; 102 } 103 104 @Override public boolean isLocked() { 105 return this.lock; 106 } 107 108 @Override public String getVersion() { 109 return "8"; // TODO 110 } 111 112 @Override public String getName() { 113 return this.name; 114 } 115 116 @Override public void setName(String name) { 117 this.name = name; 118 } 119 120 @Override public void addPrimitive(VectorPrimitive primitive) { 121 final int currentZoom = this.zoom; 122 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 123 if (!dataStore.getPrimitivesMap().containsKey(primitive.getPrimitiveId())) { 124 dataStore.getPrimitivesMap().put(primitive.getPrimitiveId(), primitive); 125 dataStore.getAllPrimitives().add(primitive); 126 } 127 } 128 129 @Override public void clear() { 130 this.dataStoreMap.clear(); 131 } 132 133 @Override public List<VectorNode> searchNodes(BBox bbox) { 134 final int currentZoom = this.zoom; 135 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 136 return dataStore.getStore().searchNodes(bbox); 137 } 138 139 @Override public boolean containsNode(VectorNode vectorNode) { 140 final int currentZoom = this.zoom; 141 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 142 return dataStore.getStore().containsNode(vectorNode); 143 } 144 145 @Override public List<VectorWay> searchWays(BBox bbox) { 146 final int currentZoom = this.zoom; 147 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 148 return dataStore.getStore().searchWays(bbox); 149 } 150 151 @Override public boolean containsWay(VectorWay vectorWay) { 152 final int currentZoom = this.zoom; 153 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 154 return dataStore.getStore().containsWay(vectorWay); 155 } 156 157 @Override public List<VectorRelation> searchRelations(BBox bbox) { 158 final int currentZoom = this.zoom; 159 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 160 return dataStore.getStore().searchRelations(bbox); 161 } 162 163 @Override public boolean containsRelation(VectorRelation vectorRelation) { 164 final int currentZoom = this.zoom; 165 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 166 return dataStore.getStore().containsRelation(vectorRelation); 167 } 168 169 @Override public VectorPrimitive getPrimitiveById(PrimitiveId primitiveId) { 170 final int currentZoom = this.zoom; 171 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 172 return dataStore.getPrimitivesMap().get(primitiveId); 173 } 174 175 @Override public <T extends VectorPrimitive> Collection<T> getPrimitives( 176 Predicate<? super VectorPrimitive> predicate) { 177 final int currentZoom = this.zoom; 178 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 179 return new SubclassFilteredCollection<>(dataStore.getAllPrimitives(), predicate); 180 } 181 182 @Override public Collection<VectorNode> getNodes() { 183 return this.getPrimitives(VectorNode.class::isInstance); 184 } 185 186 @Override public Collection<VectorWay> getWays() { 187 return this.getPrimitives(VectorWay.class::isInstance); 188 } 189 190 @Override public Collection<VectorRelation> getRelations() { 191 return this.getPrimitives(VectorRelation.class::isInstance); 192 } 193 194 @Override public DownloadPolicy getDownloadPolicy() { 195 return this.downloadPolicy; 196 } 197 198 @Override public void setDownloadPolicy(DownloadPolicy downloadPolicy) { 199 this.downloadPolicy = downloadPolicy; 200 } 201 202 @Override public UploadPolicy getUploadPolicy() { 203 return this.uploadPolicy; 204 } 205 206 @Override public void setUploadPolicy(UploadPolicy uploadPolicy) { 207 this.uploadPolicy = uploadPolicy; 208 } 209 210 @Override public Lock getReadLock() { 211 return this.readWriteLock.readLock(); 212 } 213 214 @Override public Collection<WaySegment> getHighlightedVirtualNodes() { 215 // TODO? 216 return Collections.emptyList(); 217 } 218 219 @Override public void setHighlightedVirtualNodes(Collection<WaySegment> waySegments) { 220 // TODO? 221 } 222 223 @Override public Collection<WaySegment> getHighlightedWaySegments() { 224 // TODO? 225 return Collections.emptyList(); 226 } 227 228 @Override public void setHighlightedWaySegments(Collection<WaySegment> waySegments) { 229 // TODO? 230 } 231 232 @Override public void addHighlightUpdateListener(HighlightUpdateListener listener) { 233 this.highlightUpdateListenerListenerList.addListener(listener); 234 } 235 236 @Override public void removeHighlightUpdateListener(HighlightUpdateListener listener) { 237 this.highlightUpdateListenerListenerList.removeListener(listener); 238 } 239 240 @Override public Collection<VectorPrimitive> getAllSelected() { 241 final int currentZoom = this.zoom; 242 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(currentZoom, VectorDataStore::new); 243 return dataStore.getAllPrimitives().stream().filter(primitive -> this.selected.contains(primitive.getPrimitiveId())).collect( 244 Collectors.toList()); 245 } 246 247 @Override public boolean selectionEmpty() { 248 return this.selected.isEmpty(); 249 } 250 251 @Override public boolean isSelected(VectorPrimitive osm) { 252 return this.selected.contains(osm.getPrimitiveId()); 253 } 254 255 @Override public void toggleSelected(Collection<? extends PrimitiveId> osm) { 256 this.toggleSelectedImpl(osm.stream()); 257 } 258 259 @Override public void toggleSelected(PrimitiveId... osm) { 260 this.toggleSelectedImpl(Stream.of(osm)); 261 } 262 263 private void toggleSelectedImpl(Stream<? extends PrimitiveId> osm) { 264 osm.forEach(primitiveId -> { 265 if (this.selected.contains(primitiveId)) { 266 this.selected.remove(primitiveId); 267 } else { 268 this.selected.add(primitiveId); 269 } 270 }); 271 } 272 273 @Override public void setSelected(Collection<? extends PrimitiveId> selection) { 274 this.setSelectedImpl(selection.stream()); 275 } 276 277 @Override public void setSelected(PrimitiveId... osm) { 278 this.setSelectedImpl(Stream.of(osm)); 279 } 280 281 private void setSelectedImpl(Stream<? extends PrimitiveId> osm) { 282 this.selected.clear(); 283 osm.forEach(this.selected::add); 284 } 285 286 @Override public void addSelected(Collection<? extends PrimitiveId> selection) { 287 this.addSelectedImpl(selection.stream()); 288 } 289 290 @Override public void addSelected(PrimitiveId... osm) { 291 this.addSelectedImpl(Stream.of(osm)); 292 } 293 294 private void addSelectedImpl(Stream<? extends PrimitiveId> osm) { 295 osm.forEach(this.selected::add); 296 } 297 298 @Override public void clearSelection(PrimitiveId... osm) { 299 this.clearSelectionImpl(Stream.of(osm)); 300 } 301 302 @Override public void clearSelection(Collection<? extends PrimitiveId> list) { 303 this.clearSelectionImpl(list.stream()); 304 } 305 306 @Override public void clearSelection() { 307 this.clearSelectionImpl(new ArrayList<>(this.selected).stream()); 308 } 309 310 private void clearSelectionImpl(Stream<? extends PrimitiveId> osm) { 311 osm.forEach(this.selected::remove); 312 } 313 314 @Override public void addSelectionListener(DataSelectionListener listener) { 315 this.dataSelectionListenerListenerList.addListener(listener); 316 } 317 318 @Override public void removeSelectionListener(DataSelectionListener listener) { 319 this.dataSelectionListenerListenerList.removeListener(listener); 320 } 321 322 @Override public void clearMappaintCache() { 323 // TODO? 324 } 325 326 public void setZoom(int zoom) { 327 try { 328 this.readWriteLock.writeLock().lockInterruptibly(); 329 this.zoom = zoom; 330 } catch (InterruptedException e) { 331 Logging.error(e); 332 Thread.currentThread().interrupt(); 333 } finally { 334 if (this.readWriteLock.isWriteLockedByCurrentThread()) { 335 this.readWriteLock.writeLock().unlock(); 336 } 337 } 338 } 339 340 public int getZoom() { 341 return this.zoom; 342 } 343 344 public <T extends Tile & VectorTile> void addTileData(T tile) { 345 ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile); 346 ICoordinate lowerRight = tile.getTileSource().xyToLatLon(tile.getExtent(), tile.getExtent(), tile.getZoom()); 347 final VectorDataStore dataStore = this.dataStoreMap.computeIfAbsent(tile.getZoom(), VectorDataStore::new); 348 try { 349 this.readWriteLock.writeLock().lockInterruptibly(); 350 dataStore.addTile(tile); 351 } catch (InterruptedException e) { 352 Logging.error(e); 353 Thread.currentThread().interrupt(); 354 } finally { 355 if (this.readWriteLock.isWriteLockedByCurrentThread()) { 356 this.readWriteLock.writeLock().unlock(); 357 } 358 } 359 } 360 361 private static class DataStore<O extends IPrimitive, N extends INode, W extends IWay<N>, R extends IRelation<?>> { 362 protected final int zoom; 363 protected final QuadBucketPrimitiveStore<N, W, R> store = new QuadBucketPrimitiveStore<>(); 364 protected final Storage<O> allPrimitives = new Storage<>(new Storage.PrimitiveIdHash(), true); 365 protected final Set<Tile> addedTiles = new HashSet<>(); 366 protected final Map<PrimitiveId, O> primitivesMap = allPrimitives 367 .foreignKey(new Storage.PrimitiveIdHash()); 368 protected final Collection<DataSource> dataSources = new LinkedList<>(); 369 370 public DataStore(int zoom) { 371 this.zoom = zoom; 372 } 373 374 public int getZoom() { 375 return this.zoom; 376 } 377 378 public QuadBucketPrimitiveStore<N, W, R> getStore() { 379 return this.store; 380 } 381 382 public Storage<O> getAllPrimitives() { 383 return this.allPrimitives; 384 } 385 386 public Map<PrimitiveId, O> getPrimitivesMap() { 387 return this.primitivesMap; 388 } 389 390 public Collection<DataSource> getDataSources() { 391 return Collections.unmodifiableCollection(dataSources); 392 } 393 394 public void addDataSource(DataSource dataSource) { 395 this.dataSources.add(dataSource); 396 } 397 398 protected void addPrimitive(O primitive) { 399 this.store.addPrimitive(primitive); 400 this.allPrimitives.add(primitive); 401 this.primitivesMap.put(primitive.getPrimitiveId(), primitive); 402 } 403 } 404 405 private static class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, VectorWay, VectorRelation> { 406 public VectorDataStore(int zoom) { 407 super(zoom); 408 } 409 410 private <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer, Collection<VectorPrimitive> featureObjects, int x, int y) { 411 final ICoordinate upperLeft = tile.getTileSource().tileXYToLatLon(tile); 412 final int layerExtent = layer.getExtent() * 2; 413 final ICoordinate lowerRight = tile.getTileSource().tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom()); 414 final ICoordinate coords = new Coordinate(upperLeft.getLat() - (upperLeft.getLat() - lowerRight.getLat()) * y / layerExtent, 415 upperLeft.getLon() + (lowerRight.getLon() - upperLeft.getLon()) * x / layerExtent); 416 final Collection<VectorNode> nodes = this.store.searchNodes(new BBox(coords.getLon(), coords.getLat())); 417 if (!nodes.isEmpty()) { 418 return nodes.iterator().next(); 419 } 420 final VectorNode node = new VectorNode(layer.getName()); 421 node.setCoor(coords); 422 featureObjects.add(node); 423 return node; 424 } 425 426 private <T extends Tile & VectorTile> List<VectorWay> pathToWay(T tile, Layer layer, Collection<VectorPrimitive> featureObjects, Path2D shape) { 427 final PathIterator pathIterator = shape.getPathIterator(null); 428 return pathIteratorToObjects(tile, layer, featureObjects, pathIterator).stream().filter(VectorWay.class::isInstance).map(VectorWay.class::cast).collect( 429 Collectors.toList()); 430 } 431 432 private <T extends Tile & VectorTile> List<VectorPrimitive> pathIteratorToObjects(T tile, Layer layer, Collection<VectorPrimitive> featureObjects, PathIterator pathIterator) { 433 final List<VectorNode> nodes = new ArrayList<>(); 434 final double[] coords = new double[6]; 435 final List<VectorPrimitive> ways = new ArrayList<>(); 436 do { 437 final int type = pathIterator.currentSegment(coords); 438 pathIterator.next(); 439 if ((PathIterator.SEG_MOVETO == type || PathIterator.SEG_CLOSE == type) && !nodes.isEmpty()) { 440 if (PathIterator.SEG_CLOSE == type) { 441 nodes.add(nodes.get(0)); 442 } 443 // New line 444 if (!nodes.isEmpty()) { 445 final VectorWay way = new VectorWay(layer.getName()); 446 way.setNodes(nodes); 447 featureObjects.add(way); 448 ways.add(way); 449 } 450 nodes.clear(); 451 } 452 if (PathIterator.SEG_MOVETO == type || PathIterator.SEG_LINETO == type) { 453 final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1]); 454 nodes.add(node); 455 } else if (PathIterator.SEG_CLOSE != type) { 456 // Vector Tiles only have MoveTo, LineTo, and ClosePath. Anything else is not supported at this time. 457 throw new NotImplementedException(); 458 } 459 } while (!pathIterator.isDone()); 460 if (!nodes.isEmpty()) { 461 final VectorWay way = new VectorWay(layer.getName()); 462 way.setNodes(nodes); 463 featureObjects.add(way); 464 ways.add(way); 465 } 466 return ways; 467 } 468 469 private <T extends Tile & VectorTile> VectorRelation areaToRelation(T tile, Layer layer, Collection<VectorPrimitive> featureObjects, Area area) { 470 final PathIterator pathIterator = area.getPathIterator(null); 471 final List<VectorPrimitive> members = pathIteratorToObjects(tile, layer, featureObjects, pathIterator); 472 VectorRelation vectorRelation = new VectorRelation(layer.getName()); 473 for (VectorPrimitive member : members) { 474 final String role; 475 if (member instanceof VectorWay && ((VectorWay) member).isClosed()) { 476 role = Geometry.isClockwise(((VectorWay) member).getNodes()) ? "outer" : "inner"; 477 } else { 478 role = ""; 479 } 480 vectorRelation.addRelationMember(new VectorRelationMember(role, member)); 481 } 482 return vectorRelation; 483 } 484 485 public synchronized <T extends Tile & VectorTile> void addTile(T tile) { 486 Optional<Tile> previous = this.addedTiles.stream().filter(t -> t.getTileXY().equals(tile.getTileXY()) && t.getZoom() == tile.getZoom()).findAny(); 487 // Check if we have already added the tile (just to save processing time) 488 if (!previous.isPresent() || !previous.get().isLoaded() && !previous.get().isLoading()) { 489 if (previous.isPresent()) { 490 this.addedTiles.remove(previous.get()); 491 } 492 this.addedTiles.add(tile); 493 for (Layer layer : tile.getLayers()) { 494 layer.getGeometry().forEach(geometry -> { 495 Collection<VectorPrimitive> primitives = new ArrayList<>(geometry.getShapes().size()); 496 List<VectorPrimitive> featureObjects = new ArrayList<>(); 497 List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>(); 498 geometry.getShapes().forEach(shape -> { 499 final VectorPrimitive primitive; 500 if (shape instanceof Ellipse2D) { 501 primitive = pointToNode(tile, layer, featureObjects, (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY()); 502 } else if (shape instanceof Path2D) { 503 primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape).stream().findFirst().orElse(null); 504 } else if (shape instanceof Area) { 505 primitive = areaToRelation(tile, layer, featureObjects, (Area) shape); 506 } else { 507 // We shouldn't hit this, but just in case 508 throw new NotImplementedException(); 509 } 510 primaryFeatureObjects.add(primitive); 511 }); 512 final VectorPrimitive primitive; 513 if (primaryFeatureObjects.size() == 1) { 514 primitive = primaryFeatureObjects.get(0); 515 if (primitive instanceof IRelation) { 516 // This should always be a multipolygon 517 primitive.put("type", "multipolygon"); 518 } 519 } else if (!primaryFeatureObjects.isEmpty()) { 520 VectorRelation relation = new VectorRelation(layer.getName()); 521 primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim)).forEach(relation::addRelationMember); 522 primitive = relation; 523 } else { 524 return; 525 } 526 Feature feature = geometry.getFeature(); 527 primitive.setId(feature.getId()); 528 feature.getTags().forEach(primitive::put); 529 featureObjects.forEach(this::addPrimitive); 530 primaryFeatureObjects.forEach(this::addPrimitive); 531 this.addPrimitive(primitive); 532 }); 533 } 534 } 535 } 536 } 537 } -
new file src/org/openstreetmap/josm/data/vector/VectorNode.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorNode.java b/src/org/openstreetmap/josm/data/vector/VectorNode.java new file mode 100644
- + 1 package org.openstreetmap.josm.data.vector; 2 3 import java.util.List; 4 5 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 6 import org.openstreetmap.josm.data.coor.EastNorth; 7 import org.openstreetmap.josm.data.coor.LatLon; 8 import org.openstreetmap.josm.data.osm.BBox; 9 import org.openstreetmap.josm.data.osm.INode; 10 import org.openstreetmap.josm.data.osm.IPrimitive; 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 import org.openstreetmap.josm.data.projection.ProjectionRegistry; 16 17 /** 18 * The "Node" type of a vector layer 19 * @since xxx 20 */ 21 public class VectorNode extends VectorPrimitive implements INode { 22 private static final UniqueIdGenerator ID_GENERATOR = new UniqueIdGenerator(); 23 private double lon = Double.NaN; 24 private double lat = Double.NaN; 25 26 public VectorNode(String layer) { 27 super(layer); 28 } 29 30 @Override public double lon() { 31 return this.lon; 32 } 33 34 @Override public double lat() { 35 return this.lat; 36 } 37 38 @Override public UniqueIdGenerator getIdGenerator() { 39 return ID_GENERATOR; 40 } 41 42 @Override public LatLon getCoor() { 43 return new LatLon(this.lat, this.lon); 44 } 45 46 @Override public void setCoor(LatLon coor) { 47 this.lat = coor.lat(); 48 this.lon = coor.lon(); 49 } 50 51 /** 52 * Set the coordinates of this node 53 * @param coordinates The coordinates to set 54 * @see #setCoor(LatLon) 55 */ 56 public void setCoor(ICoordinate coordinates) { 57 this.lat = coordinates.getLat(); 58 this.lon = coordinates.getLon(); 59 } 60 61 @Override public void setEastNorth(EastNorth eastNorth) { 62 final LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth); 63 this.lat = ll.lat(); 64 this.lon = ll.lon(); 65 } 66 67 @Override public boolean isReferredByWays(int n) { 68 // Count only referrers that are members of the same dataset (primitive can have some fake references, for example 69 // when way is cloned 70 List<? extends IPrimitive> referrers = super.getReferrers(); 71 if (referrers == null || referrers.isEmpty()) return false; 72 if (referrers instanceof IPrimitive) 73 return n <= 1 && referrers instanceof IWay && ((IPrimitive) referrers).getDataSet() == getDataSet(); 74 else { 75 int counter = 0; 76 for (IPrimitive o : referrers) { 77 if (getDataSet() == o.getDataSet() && o instanceof IWay && ++counter >= n) 78 return true; 79 } 80 return false; 81 } 82 } 83 84 @Override public void accept(PrimitiveVisitor visitor) { 85 visitor.visit(this); 86 } 87 88 @Override public BBox getBBox() { 89 return new BBox(this.lon, this.lat); 90 } 91 92 @Override public OsmPrimitiveType getType() { 93 return OsmPrimitiveType.NODE; 94 } 95 } -
new file src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java new file mode 100644
- + 1 package org.openstreetmap.josm.data.vector; 2 3 import java.util.Arrays; 4 import java.util.List; 5 import java.util.Map; 6 import java.util.function.Consumer; 7 import java.util.stream.Collectors; 8 import java.util.stream.IntStream; 9 import java.util.stream.Stream; 10 11 import org.openstreetmap.josm.data.osm.AbstractPrimitive; 12 import org.openstreetmap.josm.data.osm.IPrimitive; 13 import org.openstreetmap.josm.data.osm.NameFormatter; 14 import org.openstreetmap.josm.data.osm.OsmPrimitive; 15 import org.openstreetmap.josm.data.osm.TagMap; 16 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 17 import org.openstreetmap.josm.gui.mappaint.StyleCache; 18 import org.openstreetmap.josm.tools.Utils; 19 20 /** 21 * The base class for Vector primitives 22 * @author Taylor Smock 23 * @since xxx 24 */ 25 public abstract class VectorPrimitive extends AbstractPrimitive { 26 private VectorDataSet dataSet; 27 private TagMap tags; 28 private boolean highlighted; 29 private StyleCache mappaintStyle; 30 private boolean mappaintStyleUpToDate; 31 private final String layer; 32 33 public VectorPrimitive(String layer) { 34 this.layer = layer; 35 } 36 37 @Override protected void keysChangedImpl(Map<String, String> originalKeys) { 38 clearCachedStyle(); 39 if (dataSet != null) { 40 for (IPrimitive ref : getReferrers()) { 41 ref.clearCachedStyle(); 42 } 43 } 44 } 45 46 @Override public boolean isHighlighted() { 47 return this.highlighted; 48 } 49 50 @Override public void setHighlighted(boolean highlighted) { 51 this.highlighted = highlighted; 52 } 53 54 @Override public boolean isTagged() { 55 return !this.getInterestingTags().isEmpty(); 56 } 57 58 @Override public boolean isAnnotated() { 59 return this.getInterestingTags().size() - this.tags.size() > 0; 60 } 61 62 @Override 63 public VectorDataSet getDataSet() { 64 return this.dataSet; 65 } 66 67 protected void setDataSet(VectorDataSet dataSet) { 68 this.dataSet = dataSet; 69 } 70 71 @Override public StyleCache getCachedStyle() { 72 return this.mappaintStyle; 73 } 74 75 @Override public void setCachedStyle(StyleCache mappaintStyle) { 76 this.mappaintStyle = mappaintStyle; 77 if (mappaintStyle != null) { 78 this.declareCachedStyleUpToDate(); 79 } else { 80 this.mappaintStyleUpToDate = false; 81 } 82 } 83 84 @Override public boolean isCachedStyleUpToDate() { 85 return this.mappaintStyleUpToDate; 86 } 87 88 @Override public void declareCachedStyleUpToDate() { 89 this.mappaintStyleUpToDate = true; 90 this.clearCachedStyle(); 91 } 92 93 @Override public boolean hasDirectionKeys() { 94 return false; 95 } 96 97 @Override public boolean reversedDirection() { 98 return false; 99 } 100 101 /*------------ 102 * Referrers 103 ------------*/ 104 // Largely the same as OsmPrimitive, OsmPrimitive not modified at this time to avoid breaking binary compatibility 105 106 private Object referrers; 107 108 @Override 109 public final List<OsmPrimitive> getReferrers(boolean allowWithoutDataset) { 110 return referrers(allowWithoutDataset, OsmPrimitive.class) 111 .collect(Collectors.toList()); 112 } 113 114 /** 115 * Add new referrer. If referrer is already included then no action is taken 116 * @param referrer The referrer to add 117 */ 118 protected void addReferrer(IPrimitive referrer) { 119 if (referrers == null) { 120 referrers = referrer; 121 } else if (referrers instanceof IPrimitive) { 122 if (referrers != referrer) { 123 referrers = new IPrimitive[] {(IPrimitive) referrers, referrer}; 124 } 125 } else { 126 for (IPrimitive primitive:(IPrimitive[]) referrers) { 127 if (primitive == referrer) 128 return; 129 } 130 referrers = Utils.addInArrayCopy((IPrimitive[]) referrers, referrer); 131 } 132 } 133 134 /** 135 * Remove referrer. No action is taken if referrer is not registered 136 * @param referrer The referrer to remove 137 */ 138 protected void removeReferrer(IPrimitive referrer) { 139 if (referrers instanceof IPrimitive) { 140 if (referrers == referrer) { 141 referrers = null; 142 } 143 } else if (referrers instanceof IPrimitive[]) { 144 IPrimitive[] orig = (IPrimitive[]) referrers; 145 int idx = IntStream.range(0, orig.length) 146 .filter(i -> orig[i] == referrer) 147 .findFirst().orElse(-1); 148 if (idx == -1) 149 return; 150 151 if (orig.length == 2) { 152 referrers = orig[1-idx]; // idx is either 0 or 1, take the other 153 } else { // downsize the array 154 IPrimitive[] smaller = new IPrimitive[orig.length-1]; 155 System.arraycopy(orig, 0, smaller, 0, idx); 156 System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx); 157 referrers = smaller; 158 } 159 } 160 } 161 162 private <T extends IPrimitive> Stream<T> referrers(boolean allowWithoutDataset, Class<T> filter) { 163 // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example 164 // when way is cloned 165 166 if (dataSet == null && allowWithoutDataset) { 167 return Stream.empty(); 168 } 169 if (referrers == null) { 170 return Stream.empty(); 171 } 172 final Stream<IPrimitive> stream = referrers instanceof IPrimitive // NOPMD 173 ? Stream.of((IPrimitive) referrers) 174 : Arrays.stream((IPrimitive[]) referrers); 175 return stream 176 .filter(p -> p.getDataSet() == dataSet) 177 .filter(filter::isInstance) 178 .map(filter::cast); 179 } 180 181 /** 182 * Gets all primitives in the current dataset that reference this primitive. 183 * @param filter restrict primitives to subclasses 184 * @param <T> type of primitives 185 * @return the referrers as Stream 186 */ 187 public final <T extends IPrimitive> Stream<T> referrers(Class<T> filter) { 188 return referrers(false, filter); 189 } 190 191 @Override 192 public void visitReferrers(PrimitiveVisitor visitor) { 193 if (visitor != null) 194 doVisitReferrers(o -> o.accept(visitor)); 195 } 196 197 private void doVisitReferrers(Consumer<IPrimitive> visitor) { 198 if (this.referrers == null) 199 return; 200 else if (this.referrers instanceof IPrimitive) { 201 IPrimitive ref = (IPrimitive) this.referrers; 202 if (ref.getDataSet() == dataSet) { 203 visitor.accept(ref); 204 } 205 } else if (this.referrers instanceof IPrimitive[]) { 206 IPrimitive[] refs = (IPrimitive[]) this.referrers; 207 for (IPrimitive ref: refs) { 208 if (ref.getDataSet() == dataSet) { 209 visitor.accept(ref); 210 } 211 } 212 } 213 } 214 215 /** 216 * Set the id of the object 217 * @param id The id 218 */ 219 protected void setId(long id) { 220 this.id = id; 221 } 222 } -
new file src/org/openstreetmap/josm/data/vector/VectorRelation.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelation.java b/src/org/openstreetmap/josm/data/vector/VectorRelation.java new file mode 100644
- + 1 package org.openstreetmap.josm.data.vector; 2 3 import java.util.ArrayList; 4 import java.util.Collections; 5 import java.util.List; 6 7 import org.openstreetmap.josm.data.osm.BBox; 8 import org.openstreetmap.josm.data.osm.IPrimitive; 9 import org.openstreetmap.josm.data.osm.IRelation; 10 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 11 import org.openstreetmap.josm.data.osm.UniqueIdGenerator; 12 import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 13 14 /** 15 * The "Relation" type for vectors 16 * @author Taylor Smock 17 * @since xxx 18 */ 19 public class VectorRelation extends VectorPrimitive implements IRelation<VectorRelationMember> { 20 private static final UniqueIdGenerator RELATION_ID_GENERATOR = new UniqueIdGenerator(); 21 private final List<VectorRelationMember> members = new ArrayList<>(); 22 public VectorRelation(String layer) { 23 super(layer); 24 } 25 @Override public UniqueIdGenerator getIdGenerator() { 26 return RELATION_ID_GENERATOR; 27 } 28 29 @Override public void accept(PrimitiveVisitor visitor) { 30 visitor.visit(this); 31 } 32 33 @Override public BBox getBBox() { 34 final BBox bbox = new BBox(); 35 for (IPrimitive member : this.getMemberPrimitivesList()) { 36 bbox.add(member.getBBox()); 37 } 38 return bbox; 39 } 40 41 protected void addRelationMember(VectorRelationMember member) { 42 this.members.add(member); 43 } 44 45 @Override public int getMembersCount() { 46 return this.members.size(); 47 } 48 49 @Override public VectorRelationMember getMember(int index) { 50 return this.members.get(index); 51 } 52 53 @Override public List<VectorRelationMember> getMembers() { 54 return Collections.unmodifiableList(this.members); 55 } 56 57 @Override public void setMembers(List<VectorRelationMember> members) { 58 this.members.clear(); 59 this.members.addAll(members); 60 } 61 62 @Override public long getMemberId(int idx) { 63 return this.getMember(idx).getMember().getId(); 64 } 65 66 @Override public String getRole(int idx) { 67 return this.getMember(idx).getRole(); 68 } 69 70 @Override public OsmPrimitiveType getMemberType(int idx) { 71 return this.getMember(idx).getType(); 72 } 73 74 @Override public OsmPrimitiveType getType() { 75 return this.getMembers().stream().map(VectorRelationMember::getType).allMatch(OsmPrimitiveType.CLOSEDWAY::equals) ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION; 76 } 77 } -
new file src/org/openstreetmap/josm/data/vector/VectorRelationMember.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java b/src/org/openstreetmap/josm/data/vector/VectorRelationMember.java new file mode 100644
- + 1 package org.openstreetmap.josm.data.vector; 2 3 import java.util.Optional; 4 5 import org.openstreetmap.josm.data.osm.INode; 6 import org.openstreetmap.josm.data.osm.IRelation; 7 import org.openstreetmap.josm.data.osm.IRelationMember; 8 import org.openstreetmap.josm.data.osm.IWay; 9 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 10 import org.openstreetmap.josm.tools.CheckParameterUtil; 11 12 public class VectorRelationMember implements IRelationMember<VectorPrimitive> { 13 private final String role; 14 private final VectorPrimitive member; 15 public VectorRelationMember(String role, VectorPrimitive member) { 16 CheckParameterUtil.ensureParameterNotNull(member, "member"); 17 this.role = Optional.ofNullable(role).orElse("").intern(); 18 this.member = member; 19 } 20 @Override public String getRole() { 21 return this.role; 22 } 23 24 @Override public boolean isNode() { 25 return this.member instanceof INode; 26 } 27 28 @Override public boolean isWay() { 29 return this.member instanceof IWay; 30 } 31 32 @Override public boolean isRelation() { 33 return this.member instanceof IRelation; 34 } 35 36 @Override public VectorPrimitive getMember() { 37 return this.member; 38 } 39 40 @Override public long getUniqueId() { 41 return this.member.getId(); 42 } 43 44 @Override public OsmPrimitiveType getType() { 45 return this.member.getType(); 46 } 47 48 @Override public boolean isNew() { 49 return this.member.isNew(); 50 } 51 } -
new file src/org/openstreetmap/josm/data/vector/VectorWay.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/vector/VectorWay.java b/src/org/openstreetmap/josm/data/vector/VectorWay.java new file mode 100644
- + 1 package org.openstreetmap.josm.data.vector; 2 3 import java.util.ArrayList; 4 import java.util.Collections; 5 import java.util.List; 6 import java.util.stream.Collectors; 7 8 import org.openstreetmap.josm.data.osm.BBox; 9 import org.openstreetmap.josm.data.osm.INode; 10 import org.openstreetmap.josm.data.osm.IWay; 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 "Way" type for a Vector layer 17 * @author Taylor Smock 18 * @since xxx 19 */ 20 public class VectorWay extends VectorPrimitive implements IWay<VectorNode> { 21 private static final UniqueIdGenerator WAY_GENERATOR = new UniqueIdGenerator(); 22 private final List<VectorNode> nodes = new ArrayList<>(); 23 public VectorWay(String layer) { 24 super(layer); 25 } 26 @Override public UniqueIdGenerator getIdGenerator() { 27 return WAY_GENERATOR; 28 } 29 30 @Override public void accept(PrimitiveVisitor visitor) { 31 visitor.visit(this); 32 } 33 34 @Override public BBox getBBox() { 35 final BBox bbox = new BBox(); 36 for (INode node : this.getNodes()) { 37 bbox.add(node.getBBox()); 38 } 39 return bbox; 40 } 41 42 @Override public int getNodesCount() { 43 return this.getNodes().size(); 44 } 45 46 @Override public VectorNode getNode(int index) { 47 return this.getNodes().get(index); 48 } 49 50 @Override public List<VectorNode> getNodes() { 51 return Collections.unmodifiableList(this.nodes); 52 } 53 54 @Override public List<Long> getNodeIds() { 55 return this.getNodes().stream().map(VectorNode::getId).collect(Collectors.toList()); 56 } 57 58 @Override public long getNodeId(int idx) { 59 return this.getNodes().get(idx).getId(); 60 } 61 62 @Override public void setNodes(List<VectorNode> nodes) { 63 this.nodes.clear(); 64 this.nodes.addAll(nodes); 65 } 66 67 @Override public boolean isClosed() { 68 return this.firstNode() != null && this.firstNode().equals(this.lastNode()); 69 } 70 71 @Override public VectorNode firstNode() { 72 if (this.nodes.isEmpty()) { 73 return null; 74 } 75 return this.getNode(0); 76 } 77 78 @Override public VectorNode lastNode() { 79 if (this.nodes.isEmpty()) { 80 return null; 81 } 82 return this.getNode(this.getNodesCount() - 1); 83 } 84 85 @Override public boolean isFirstLastNode(INode n) { 86 if (this.nodes.isEmpty()) { 87 return false; 88 } 89 return this.firstNode().equals(n) || this.lastNode().equals(n); 90 } 91 92 @Override public boolean isInnerNode(INode n) { 93 if (this.nodes.isEmpty()) { 94 return false; 95 } 96 return !this.firstNode().equals(n) && !this.lastNode().equals(n) && this.nodes.stream().anyMatch(vectorNode -> vectorNode.equals(n)); 97 } 98 99 @Override public OsmPrimitiveType getType() { 100 return this.isClosed() ? OsmPrimitiveType.CLOSEDWAY : OsmPrimitiveType.WAY; 101 } 102 } -
src/org/openstreetmap/josm/gui/io/importexport/ImageImporter.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/io/importexport/ImageImporter.java b/src/org/openstreetmap/josm/gui/io/importexport/ImageImporter.java
a b 8 8 import java.util.ArrayList; 9 9 import java.util.Arrays; 10 10 import java.util.Collections; 11 import java.util.EnumSet; 11 12 import java.util.HashSet; 12 13 import java.util.List; 13 14 import java.util.Set; 15 import java.util.regex.Matcher; 16 import java.util.regex.Pattern; 14 17 import java.util.stream.Collectors; 15 18 16 19 import javax.imageio.ImageIO; … … 19 22 import org.openstreetmap.josm.gui.layer.GpxLayer; 20 23 import org.openstreetmap.josm.gui.layer.geoimage.GeoImageLayer; 21 24 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 25 import org.openstreetmap.josm.io.CachedFile; 22 26 import org.openstreetmap.josm.io.IllegalDataException; 23 27 24 28 /** … … 26 30 * @since 17548 27 31 */ 28 32 public class ImageImporter extends FileImporter { 33 34 /** Check if the filename starts with a borked path ({@link java.io.File#File} drops consecutive {@code /} characters). */ 35 private static final Pattern URL_START_BAD = Pattern.compile("^(https?:/)([^/].*)$"); 36 /** Check for the beginning of a "good" url */ 37 private static final Pattern URL_START_GOOD = Pattern.compile("^https?://.*$"); 38 29 39 private GpxLayer gpx; 30 40 31 41 /** … … 90 100 try { 91 101 List<File> files = new ArrayList<>(); 92 102 Set<String> visitedDirs = new HashSet<>(); 93 addRecursiveFiles( files, visitedDirs, sel, progressMonitor.createSubTaskMonitor(1, true));103 addRecursiveFiles(this.options, files, visitedDirs, sel, progressMonitor.createSubTaskMonitor(1, true)); 94 104 95 105 if (progressMonitor.isCanceled()) 96 106 return; … … 106 116 107 117 static void addRecursiveFiles(List<File> files, Set<String> visitedDirs, List<File> sel, ProgressMonitor progressMonitor) 108 118 throws IOException { 119 addRecursiveFiles(EnumSet.noneOf(Options.class), files, visitedDirs, sel, progressMonitor); 120 } 121 122 static void addRecursiveFiles(Set<Options> options, List<File> files, Set<String> visitedDirs, List<File> sel, 123 ProgressMonitor progressMonitor) throws IOException { 109 124 110 125 if (progressMonitor.isCanceled()) 111 126 return; … … 117 132 if (visitedDirs.add(f.getCanonicalPath())) { // Do not loop over symlinks 118 133 File[] dirFiles = f.listFiles(); // Can be null for some strange directories (like lost+found) 119 134 if (dirFiles != null) { 120 addRecursiveFiles(files, visitedDirs, Arrays.asList(dirFiles), progressMonitor.createSubTaskMonitor(1, true)); 135 addRecursiveFiles(options, files, visitedDirs, Arrays.asList(dirFiles), 136 progressMonitor.createSubTaskMonitor(1, true)); 121 137 } 122 138 } else { 123 139 progressMonitor.worked(1); 124 140 } 125 141 } else { 126 if (FILE_FILTER.accept(f)) { 142 /* Check if the path is a web path, and if so, ensure that it is "correct" */ 143 final String path = f.getPath(); 144 Matcher matcherBad = URL_START_BAD.matcher(path); 145 final String realPath; 146 if (matcherBad.matches()) { 147 realPath = matcherBad.replaceFirst(matcherBad.group(1) + "/" + matcherBad.group(2)); 148 } else { 149 realPath = path; 150 } 151 if (URL_START_GOOD.matcher(realPath).matches() && FILE_FILTER.accept(f) 152 && options.contains(Options.ALLOW_WEB_RESOURCES)) { 153 try (CachedFile cachedFile = new CachedFile(realPath)) { 154 files.add(cachedFile.getFile()); 155 } 156 } else if (FILE_FILTER.accept(f)) { 127 157 files.add(f); 128 158 } 129 159 progressMonitor.worked(1); -
new file src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java b/src/org/openstreetmap/josm/gui/layer/imagery/MVTLayer.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 import java.awt.Component; 6 import java.awt.Graphics2D; 7 import java.awt.event.ActionEvent; 8 import java.util.ArrayList; 9 import java.util.Arrays; 10 import java.util.Collection; 11 import java.util.Collections; 12 import java.util.HashMap; 13 import java.util.List; 14 import java.util.Map; 15 import java.util.function.BooleanSupplier; 16 import java.util.function.Consumer; 17 import java.util.stream.Collectors; 18 19 import javax.swing.AbstractAction; 20 import javax.swing.Action; 21 import javax.swing.JCheckBoxMenuItem; 22 23 import org.openstreetmap.gui.jmapviewer.Tile; 24 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 25 import org.openstreetmap.josm.data.Bounds; 26 import org.openstreetmap.josm.data.imagery.ImageryInfo; 27 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; 28 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTFile; 29 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile; 30 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.LayerShower; 31 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MVTTile.TileListener; 32 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapBoxVectorCachedTileLoader; 33 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.MapboxVectorTileSource; 34 import org.openstreetmap.josm.data.osm.visitor.paint.AbstractMapRenderer; 35 import org.openstreetmap.josm.data.osm.visitor.paint.MapRendererFactory; 36 import org.openstreetmap.josm.data.vector.VectorDataSet; 37 import org.openstreetmap.josm.gui.MainApplication; 38 import org.openstreetmap.josm.gui.MapView; 39 import org.openstreetmap.josm.gui.NavigatableComponent; 40 import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer; 41 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 42 import org.openstreetmap.josm.spi.preferences.Config; 43 44 /** 45 * A layer for MapBox Vector Tiles 46 * @author Taylor Smock 47 * @since xxx 48 */ 49 public class MVTLayer extends AbstractCachedTileSourceLayer<MapboxVectorTileSource> implements LayerShower, TileListener, 50 NavigatableComponent.ZoomChangeListener { 51 private static final String CACHE_REGION_NAME = "MVT"; 52 private final Map<String, Boolean> layerNames = new HashMap<>(); 53 private final VectorDataSet dataSet = new VectorDataSet(); 54 55 /** 56 * Creates an instance of an MVT layer 57 * 58 * @param info ImageryInfo describing the layer 59 */ 60 public MVTLayer(ImageryInfo info) { 61 super(info); 62 NavigatableComponent.addZoomChangeListener(this); 63 } 64 65 @Override 66 protected Class<? extends TileLoader> getTileLoaderClass() { 67 return MapBoxVectorCachedTileLoader.class; 68 } 69 70 @Override 71 protected String getCacheName() { 72 return CACHE_REGION_NAME; 73 } 74 75 @Override 76 public Collection<String> getNativeProjections() { 77 // MapBox Vector Tiles <i>specifically</i> only support EPSG:3857 78 // ("it is exclusively geared towards square pixel tiles in {link to EPSG:3857}"). 79 return Collections.singleton(MVTFile.DEFAULT_PROJECTION); 80 } 81 82 @Override public void paint(Graphics2D g, MapView mv, Bounds box) { 83 this.dataSet.setZoom(this.getZoomLevel()); 84 boolean active = mv.getLayerManager().getActiveLayer() == this; 85 boolean inactive = !active && Config.getPref().getBoolean("draw.data.inactive_color", true); 86 boolean virtual = !inactive && mv.isVirtualNodesEnabled(); 87 AbstractMapRenderer painter = MapRendererFactory.getInstance().createActiveRenderer(g, mv, inactive); 88 painter.enableSlowOperations(mv.getMapMover() == null || !mv.getMapMover().movementInProgress() 89 || !OsmDataLayer.PROPERTY_HIDE_LABELS_WHILE_DRAGGING.get()); 90 painter.render(this.dataSet, virtual, box); 91 } 92 93 @Override 94 protected MapboxVectorTileSource getTileSource() { 95 MapboxVectorTileSource source = new MapboxVectorTileSource(this.info); 96 this.info.setAttribution(source); 97 return source; 98 } 99 100 @Override 101 public Tile createTile(MapboxVectorTileSource source, int x, int y, int zoom) { 102 final MVTTile tile = new MVTTile(source, x, y, zoom); 103 tile.setLayerShower(this); 104 tile.addTileLoaderFinisher(this); 105 return tile; 106 } 107 108 @Override 109 public Action[] getMenuEntries() { 110 ArrayList<Action> actions = new ArrayList<>(Arrays.asList(super.getMenuEntries())); 111 // Add separator between Info and the layers 112 actions.add(SeparatorLayerAction.INSTANCE); 113 for (Map.Entry<String, Boolean> layerConfig : layerNames.entrySet()) { 114 actions.add(new EnableLayerAction(layerConfig.getKey(), () -> layerNames.computeIfAbsent(layerConfig.getKey(), key -> true), 115 layer -> {layerNames.compute(layer, (key, value) -> !value); this.invalidate(); })); 116 } 117 return actions.toArray(new Action[0]); 118 } 119 120 private static class EnableLayerAction extends AbstractAction implements LayerAction { 121 private final String layer; 122 private final Consumer<String> consumer; 123 private final BooleanSupplier state; 124 public EnableLayerAction(String layer, BooleanSupplier state, Consumer<String> consumer) { 125 super(tr("Toggle layer {0}", layer)); 126 this.layer = layer; 127 this.consumer = consumer; 128 this.state = state; 129 } 130 @Override 131 public void actionPerformed(ActionEvent e) { 132 consumer.accept(layer); 133 } 134 @Override 135 public boolean supportLayers(List<org.openstreetmap.josm.gui.layer.Layer> layers) { 136 return layers.stream().allMatch(MVTLayer.class::isInstance); 137 } 138 @Override 139 public Component createMenuComponent() { 140 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this); 141 item.setSelected(this.state.getAsBoolean()); 142 return item; 143 } 144 } 145 146 @Override 147 public void finishedLoading(MVTTile tile) { 148 for (Layer layer : tile.getLayers()) { 149 this.layerNames.putIfAbsent(layer.getName(), true); 150 } 151 this.dataSet.addTileData(tile); 152 } 153 154 @Override 155 public List<String> layersToShow() { 156 return this.layerNames.entrySet().stream().filter(Map.Entry::getValue).map(Map.Entry::getKey).collect(Collectors.toList()); 157 } 158 } -
src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
a b 87 87 import org.openstreetmap.josm.data.imagery.OffsetBookmark; 88 88 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 89 89 import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 90 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 90 91 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 91 92 import org.openstreetmap.josm.data.preferences.BooleanProperty; 92 93 import org.openstreetmap.josm.data.preferences.IntegerProperty; … … 110 111 import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction; 111 112 import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction; 112 113 import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction; 114 import org.openstreetmap.josm.gui.layer.imagery.MVTLayer; 113 115 import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile; 114 116 import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction; 115 117 import org.openstreetmap.josm.gui.layer.imagery.TileAnchor; … … 890 892 if (coordinateConverter.requiresReprojection()) { 891 893 tile = new ReprojectionTile(tileSource, x, y, zoom); 892 894 } else { 893 tile = newTile(tileSource, x, y, zoom);895 tile = createTile(tileSource, x, y, zoom); 894 896 } 895 897 tileCache.addTile(tile); 896 898 } … … 1029 1031 } 1030 1032 } 1031 1033 1034 /** 1035 * Draw a vector tile on screen. 1036 * @param g the Graphics2D 1037 * @param tile the vector tile 1038 * @param anchorImage tile anchor in image coordinates 1039 * @param anchorScreen tile anchor in screen coordinates 1040 * @param clip clipping region in screen coordinates (can be null) 1041 */ 1042 private void drawVectorTileInside(Graphics2D g, VectorTile tile, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) { 1043 AffineTransform imageToScreen = anchorImage.convert(anchorScreen); 1044 Point2D screen0 = imageToScreen.transform(new Point2D.Double(0, 0), null); 1045 Point2D screen1 = imageToScreen.transform(new Point2D.Double( 1046 tile.getExtent(), tile.getExtent()), null); 1047 1048 Shape oldClip = null; 1049 if (clip != null) { 1050 oldClip = g.getClip(); 1051 g.clip(clip); 1052 } 1053 tile.paint(g, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()), 1054 (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()), 1055 (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this.currentZoomLevel, this); 1056 if (clip != null) { 1057 g.setClip(oldClip); 1058 } 1059 } 1060 1032 1061 private List<Tile> paintTileImages(Graphics2D g, TileSet ts) { 1033 1062 Object paintMutex = new Object(); 1034 1063 List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>()); … … 1043 1072 img = getLoadedTileImage(tile); 1044 1073 anchorImage = getAnchor(tile, img); 1045 1074 } 1046 if (img == null || anchorImage == null ) {1075 if (img == null || anchorImage == null || (tile instanceof VectorTile && !tile.isLoaded())) { 1047 1076 miss = true; 1048 1077 } 1049 1078 } … … 1052 1081 return; 1053 1082 } 1054 1083 1055 img = applyImageProcessors(img); 1084 if (img != null) { 1085 img = applyImageProcessors(img); 1086 } 1056 1087 1057 1088 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile); 1058 1089 synchronized (paintMutex) { 1059 1090 //cannot paint in parallel 1060 drawImageInside(g, img, anchorImage, anchorScreen, null); 1091 if (tile instanceof VectorTile) { 1092 // drawVectorTileInside(g, (VectorTile) tile, anchorImage, anchorScreen, null); TODO 1093 } else { 1094 drawImageInside(g, img, anchorImage, anchorScreen, null); 1095 } 1061 1096 } 1062 1097 MapView mapView = MainApplication.getMap().mapView; 1063 1098 if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) { … … 1864 1899 1865 1900 for (int x = minX; x <= maxX; x++) { 1866 1901 for (int y = minY; y <= maxY; y++) { 1867 requestedTiles.add( newTile(tileSource, x, y, currentZoomLevel));1902 requestedTiles.add(createTile(tileSource, x, y, currentZoomLevel)); 1868 1903 } 1869 1904 } 1870 1905 } … … 1970 2005 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1971 2006 } 1972 2007 2008 /** 2009 * Create a new tile. Added to allow use of custom {@link Tile} objects. 2010 * 2011 * @param source Tile source 2012 * @param x X coordinate 2013 * @param y Y coordinate 2014 * @param zoom Zoom level 2015 * @return The new {@link Tile} 2016 * @since xxx 2017 */ 2018 public Tile createTile(T source, int x, int y, int zoom) { 2019 return new Tile(source, x, y, zoom); 2020 } 2021 1973 2022 @Override 1974 2023 public synchronized void destroy() { 1975 2024 super.destroy(); … … 1990 2039 allocateCacheMemory(); 1991 2040 if (memory != null) { 1992 2041 doPaint(graphics); 2042 if (AbstractTileSourceLayer.this instanceof MVTLayer) { 2043 AbstractTileSourceLayer.this.paint(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getMapView() 2044 .getRealBounds()); 2045 } 1993 2046 } else { 1994 2047 Graphics g = graphics.getDefaultGraphics(); 1995 2048 Color oldColor = g.getColor(); -
src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java b/src/org/openstreetmap/josm/gui/layer/ImageryLayer.java
a b 37 37 import org.openstreetmap.josm.gui.MapView; 38 38 import org.openstreetmap.josm.gui.MenuScroller; 39 39 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings; 40 import org.openstreetmap.josm.gui.layer.imagery.MVTLayer; 40 41 import org.openstreetmap.josm.gui.widgets.UrlLabel; 41 42 import org.openstreetmap.josm.tools.GBC; 42 43 import org.openstreetmap.josm.tools.ImageProcessor; … … 168 169 case BING: 169 170 case SCANEX: 170 171 return new TMSLayer(info); 172 case MVT: 173 return new MVTLayer(info); 171 174 default: 172 175 throw new AssertionError(tr("Unsupported imagery type: {0}", info.getImageryType())); 173 176 } -
new file src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/AddMVTLayerPanel.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.preferences.imagery; 3 import org.openstreetmap.josm.data.imagery.ImageryInfo; 4 import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 5 import org.openstreetmap.josm.gui.widgets.JosmTextArea; 6 import org.openstreetmap.josm.gui.widgets.JosmTextField; 7 import org.openstreetmap.josm.tools.GBC; 8 import org.openstreetmap.josm.tools.Utils; 9 10 import javax.swing.JLabel; 11 import java.awt.event.KeyAdapter; 12 import java.awt.event.KeyEvent; 13 import java.util.Arrays; 14 15 import static org.openstreetmap.josm.tools.I18n.tr; 16 17 /** 18 * A panel for adding MapBox Vector Tile layers 19 * @author Taylor Smock 20 * @since xxx 21 */ 22 public class AddMVTLayerPanel extends AddImageryPanel { 23 private final JosmTextField mvtZoom = new JosmTextField(); 24 private final JosmTextArea mvtUrl = new JosmTextArea(3, 40).transferFocusOnTab(); 25 26 /** 27 * Constructs a new {@code AddMVTLayerPanel}. 28 */ 29 public AddMVTLayerPanel() { 30 31 add(new JLabel(tr("{0} Make sure OSM has the permission to use this service", "1.")), GBC.eol()); 32 add(new JLabel(tr("{0} Enter URL", "2.")), GBC.eol()); 33 add(new JLabel("<html>" + Utils.joinAsHtmlUnorderedList(Arrays.asList( 34 tr("{0} is replaced by tile zoom level, also supported:<br>" + 35 "offsets to the zoom level: {1} or {2}<br>" + 36 "reversed zoom level: {3}", "{zoom}", "{zoom+1}", "{zoom-1}", "{19-zoom}"), 37 tr("{0} is replaced by X-coordinate of the tile", "{x}"), 38 tr("{0} is replaced by Y-coordinate of the tile", "{y}"), 39 tr("{0} is replaced by a random selection from the given comma separated list, e.g. {1}", "{switch:...}", "{switch:a,b,c}") 40 )) + "</html>"), GBC.eol().fill()); 41 42 final KeyAdapter keyAdapter = new KeyAdapter() { 43 @Override 44 public void keyReleased(KeyEvent e) { 45 mvtUrl.setText(buildMvtUrl()); 46 } 47 }; 48 49 add(rawUrl, GBC.eop().fill()); 50 rawUrl.setLineWrap(true); 51 rawUrl.addKeyListener(keyAdapter); 52 53 add(new JLabel(tr("{0} Enter maximum zoom (optional)", "3.")), GBC.eol()); 54 mvtZoom.addKeyListener(keyAdapter); 55 add(mvtZoom, GBC.eop().fill()); 56 57 add(new JLabel(tr("{0} Edit generated {1} URL (optional)", "4.", "MVT")), GBC.eol()); 58 add(mvtUrl, GBC.eop().fill()); 59 mvtUrl.setLineWrap(true); 60 61 add(new JLabel(tr("{0} Enter name for this layer", "5.")), GBC.eol()); 62 add(name, GBC.eop().fill()); 63 64 registerValidableComponent(mvtUrl); 65 } 66 67 private String buildMvtUrl() { 68 StringBuilder a = new StringBuilder("mvt"); 69 String z = sanitize(mvtZoom.getText()); 70 if (!z.isEmpty()) { 71 a.append('[').append(z).append(']'); 72 } 73 a.append(':').append(sanitize(getImageryRawUrl(), ImageryType.MVT)); 74 return a.toString(); 75 } 76 77 @Override 78 public ImageryInfo getImageryInfo() { 79 ImageryInfo generated = new ImageryInfo(getImageryName(), getMvtUrl()); 80 generated.setImageryType(ImageryType.MVT); 81 return generated; 82 } 83 84 protected final String getMvtUrl() { 85 return sanitize(mvtUrl.getText()); 86 } 87 88 @Override 89 protected boolean isImageryValid() { 90 return !getImageryName().isEmpty() && !getMvtUrl().isEmpty(); 91 } 92 } -
src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java b/src/org/openstreetmap/josm/gui/preferences/imagery/ImageryProvidersPanel.java
a b 312 312 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMS)); 313 313 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.TMS)); 314 314 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.WMTS)); 315 activeToolbar.add(new NewEntryAction(ImageryInfo.ImageryType.MVT)); 315 316 activeToolbar.add(remove); 316 317 activePanel.add(activeToolbar, BorderLayout.EAST); 317 318 add(activePanel, GBC.eol().fill(GridBagConstraints.BOTH).weight(2.0, 0.4).insets(5, 0, 0, 5)); … … 439 440 break; 440 441 case WMTS: 441 442 icon = /* ICON(dialogs/) */ "add_wmts"; 443 break; 444 case MVT: 445 icon = /* ICON(dialogs/) */ "add_mvt"; 442 446 break; 443 447 default: 444 448 break; … … 460 464 case WMTS: 461 465 p = new AddWMTSLayerPanel(); 462 466 break; 467 case MVT: 468 p = new AddMVTLayerPanel(); 469 break; 463 470 default: 464 471 throw new IllegalStateException("Type " + type + " not supported"); 465 472 } … … 741 748 private static boolean confirmEulaAcceptance(PreferenceTabbedPane gui, String eulaUrl) { 742 749 URL url; 743 750 try { 744 url = new URL(eulaUrl.replaceAll("\\{lang \\}", LanguageInfo.getWikiLanguagePrefix()));751 url = new URL(eulaUrl.replaceAll("\\{lang}", LanguageInfo.getWikiLanguagePrefix())); 745 752 JosmEditorPane htmlPane; 746 753 try { 747 754 htmlPane = new JosmEditorPane(url); … … 749 756 Logging.trace(e1); 750 757 // give a second chance with a default Locale 'en' 751 758 try { 752 url = new URL(eulaUrl.replaceAll("\\{lang \\}", ""));759 url = new URL(eulaUrl.replaceAll("\\{lang}", "")); 753 760 htmlPane = new JosmEditorPane(url); 754 761 } catch (IOException e2) { 755 762 Logging.debug(e2); -
new file test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtoBufTest.java new file mode 100644
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.data.protobuf; 3 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 import static org.junit.jupiter.api.Assertions.assertNotNull; 6 import static org.junit.jupiter.api.Assertions.fail; 7 8 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, MessageFormat.format("Expected {0} but got {1}", Integer.toBinaryString(Integer.MAX_VALUE), Long.toBinaryString(actual.longValue()))); 292 } 293 294 @Test 295 void testZigZag() { 296 assertEquals(0, ProtoBufParser.decodeZigZag(0).intValue()); 297 assertEquals(-1, ProtoBufParser.decodeZigZag(1).intValue()); 298 assertEquals(1, ProtoBufParser.decodeZigZag(2).intValue()); 299 assertEquals(-2, ProtoBufParser.decodeZigZag(3).intValue()); 300 } 301 302 private Number bytesToVarInt(int... bytes) { 303 byte[] byteArray = new byte[bytes.length]; 304 for (int i = 0; i < bytes.length; i++) { 305 byteArray[i] = (byte) bytes[i]; 306 } 307 return ProtoBufParser.convertByteArray(byteArray, ProtoBufParser.VAR_INT_BYTE_SIZE); 308 } 309 }