Ticket #22032: 22032.patch
| File 22032.patch, 55.9 KB (added by , 4 years ago) |
|---|
-
src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java
diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Feature.java index eb4ea72c4d..3d44b4ad17 100644
a b 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 3 4 import java.io.ByteArrayOutputStream; 4 5 import java.io.IOException; 5 6 import java.text.NumberFormat; 6 7 import java.util.ArrayList; … … public class Feature { 26 27 private static final byte GEOMETRY_FIELD = 4; 27 28 /** 28 29 * The number format instance to use (using a static instance gets rid of quite o few allocations) 29 * Doing this reduced the allocations of {@link #parseTagValue(String, Layer, Number )} from 22.79% of parent to30 * Doing this reduced the allocations of {@link #parseTagValue(String, Layer, Number, List)} from 22.79% of parent to 30 31 * 12.2% of parent. 31 32 */ 32 33 private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ROOT); 34 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 33 35 /** 34 36 * The geometry of the feature. Required. 35 37 */ … … public class Feature { 47 49 /** 48 50 * The tags of the feature. Optional. 49 51 */ 50 private TagMap tags;52 private final TagMap tags; 51 53 private Geometry geometryObject; 52 54 53 55 /** … … public class Feature { 61 63 long tId = 0; 62 64 GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN; 63 65 String key = null; 66 // Use a list where we can grow capacity easily (TagMap will do an array copy every time a tag is added) 67 // This lets us avoid most array copies (i.e., this should only happen if some software decided it would be 68 // a good idea to have multiple tag fields). 69 // By avoiding array copies in TagMap, Feature#init goes from 339 MB to 188 MB. 70 ArrayList<String> tagList = null; 64 71 try (ProtobufParser parser = new ProtobufParser(record.getBytes())) { 72 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4); 65 73 while (parser.hasNext()) { 66 try (ProtobufRecord next = new ProtobufRecord( parser)) {74 try (ProtobufRecord next = new ProtobufRecord(byteArrayOutputStream, parser)) { 67 75 if (next.getField() == TAG_FIELD) { 68 if (tags == null) {69 tags = new TagMap();70 }71 76 // This is packed in v1 and v2 72 ProtobufPacked packed = new ProtobufPacked(next.getBytes()); 77 ProtobufPacked packed = new ProtobufPacked(byteArrayOutputStream, next.getBytes()); 78 if (tagList == null) { 79 tagList = new ArrayList<>(packed.getArray().length); 80 } else { 81 tagList.ensureCapacity(tagList.size() + packed.getArray().length); 82 } 73 83 for (Number number : packed.getArray()) { 74 key = parseTagValue(key, layer, number );84 key = parseTagValue(key, layer, number, tagList); 75 85 } 76 86 } else if (next.getField() == GEOMETRY_FIELD) { 77 87 // This is packed in v1 and v2 78 ProtobufPacked packed = new ProtobufPacked( next.getBytes());88 ProtobufPacked packed = new ProtobufPacked(byteArrayOutputStream, next.getBytes()); 79 89 CommandInteger currentCommand = null; 80 90 for (Number number : packed.getArray()) { 81 91 if (currentCommand != null && currentCommand.hasAllExpectedParameters()) { … … public class Feature { 90 100 } 91 101 // TODO fallback to non-packed 92 102 } else if (next.getField() == GEOMETRY_TYPE_FIELD) { 93 geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()]; 103 // by using getAllValues, we avoid 12.4 MB allocations 104 geometryTypeTemp = GeometryTypes.getAllValues()[next.asUnsignedVarInt().intValue()]; 94 105 } else if (next.getField() == ID_FIELD) { 95 106 tId = next.asUnsignedVarInt().longValue(); 96 107 } … … public class Feature { 100 111 this.id = tId; 101 112 this.geometryType = geometryTypeTemp; 102 113 record.close(); 114 if (tagList != null && !tagList.isEmpty()) { 115 this.tags = new TagMap(tagList.toArray(EMPTY_STRING_ARRAY)); 116 } else { 117 this.tags = null; 118 } 103 119 } 104 120 105 121 /** … … public class Feature { 108 124 * @param key The current key (or {@code null}, if {@code null}, the returned value will be the new key) 109 125 * @param layer The layer with key/value information 110 126 * @param number The number to get the value from 127 * @param tagList The list to add the new value to 111 128 * @return The new key (if {@code null}, then a value was parsed and added to tags) 112 129 */ 113 private String parseTagValue(String key, Layer layer, Number number ) {130 private String parseTagValue(String key, Layer layer, Number number, List<String> tagList) { 114 131 if (key == null) { 115 132 key = layer.getKey(number.intValue()); 116 133 } else { 134 tagList.add(key); 117 135 Object value = layer.getValue(number.intValue()); 118 136 if (value instanceof Double || value instanceof Float) { 119 137 // reset grouping if the instance is a singleton … … public class Feature { 121 139 final boolean grouping = NUMBER_FORMAT.isGroupingUsed(); 122 140 try { 123 141 NUMBER_FORMAT.setGroupingUsed(false); 124 t his.tags.put(key, NUMBER_FORMAT.format(value));142 tagList.add(Utils.intern(NUMBER_FORMAT.format(value))); 125 143 } finally { 126 144 NUMBER_FORMAT.setGroupingUsed(grouping); 127 145 } 128 146 } else { 129 t his.tags.put(key,Utils.intern(value.toString()));147 tagList.add(Utils.intern(value.toString())); 130 148 } 131 149 key = null; 132 150 } -
src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java
diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Geometry.java index 87aff165f7..0e5976c100 100644
a b import java.util.List; 18 18 * @since 17862 19 19 */ 20 20 public class Geometry { 21 final Collection<Shape> shapes = new ArrayList<>();21 final Collection<Shape> shapes; 22 22 23 23 /** 24 24 * Create a {@link Geometry} for a {@link Feature} … … public class Geometry { 28 28 */ 29 29 public Geometry(GeometryTypes geometryType, List<CommandInteger> commands) { 30 30 if (geometryType == GeometryTypes.POINT) { 31 for (CommandInteger command : commands) { 32 final short[] operations = command.getOperations(); 33 // Each MoveTo command is a new point 34 if (command.getType() == Command.MoveTo && operations.length % 2 == 0 && operations.length > 0) { 35 for (int i = 0; i < operations.length / 2; i++) { 36 // Just using Ellipse2D since it extends Shape 37 shapes.add(new Ellipse2D.Float(operations[2 * i], operations[2 * i + 1], 0, 0)); 38 } 39 } else { 40 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 31 // This gets rid of most of the expensive array copies from ArrayList#grow 32 shapes = new ArrayList<>(commands.size()); 33 initializePoints(geometryType, commands); 34 } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) { 35 // This gets rid of most of the expensive array copies from ArrayList#grow 36 shapes = new ArrayList<>(1); 37 initializeWayGeometry(geometryType, commands); 38 } else { 39 shapes = Collections.emptyList(); 40 } 41 } 42 43 /** 44 * Initialize point geometry 45 * @param geometryType The geometry type (used for logging) 46 * @param commands The commands to use to create the geometry 47 */ 48 private void initializePoints(GeometryTypes geometryType, List<CommandInteger> commands) { 49 for (CommandInteger command : commands) { 50 final short[] operations = command.getOperations(); 51 // Each MoveTo command is a new point 52 if (command.getType() == Command.MoveTo && operations.length % 2 == 0 && operations.length > 0) { 53 for (int i = 0; i < operations.length / 2; i++) { 54 // Just using Ellipse2D since it extends Shape 55 shapes.add(new Ellipse2D.Float(operations[2 * i], operations[2 * i + 1], 0, 0)); 41 56 } 57 } else { 58 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 42 59 } 43 } else if (geometryType == GeometryTypes.LINESTRING || geometryType == GeometryTypes.POLYGON) { 44 Path2D.Float line = null; 45 Area area = null; 46 // MVT uses delta encoding. Each feature starts at (0, 0). 47 int x = 0; 48 int y = 0; 49 // Area is used to determine the inner/outer of a polygon 50 final int maxArraySize = commands.stream().filter(command -> command.getType() != Command.ClosePath) 51 .mapToInt(command -> command.getOperations().length).sum(); 52 final List<Integer> xArray = new ArrayList<>(maxArraySize); 53 final List<Integer> yArray = new ArrayList<>(maxArraySize); 54 for (CommandInteger command : commands) { 55 final short[] operations = command.getOperations(); 56 // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior 57 if (command.getType() == Command.MoveTo && operations.length == 2) { 58 x += operations[0]; 59 y += operations[1]; 60 line = new Path2D.Float(); 61 line.moveTo(x, y); 60 } 61 } 62 63 /** 64 * Initialize way geometry 65 * @param geometryType The geometry type 66 * @param commands The commands to use to create the geometry 67 */ 68 private void initializeWayGeometry(GeometryTypes geometryType, List<CommandInteger> commands) { 69 Path2D.Float line = null; 70 Area area = null; 71 // MVT uses delta encoding. Each feature starts at (0, 0). 72 int x = 0; 73 int y = 0; 74 // Area is used to determine the inner/outer of a polygon 75 final int maxArraySize = commands.stream().filter(command -> command.getType() != Command.ClosePath) 76 .mapToInt(command -> command.getOperations().length).sum(); 77 final List<Integer> xArray = new ArrayList<>(maxArraySize); 78 final List<Integer> yArray = new ArrayList<>(maxArraySize); 79 for (CommandInteger command : commands) { 80 final short[] operations = command.getOperations(); 81 // Technically, there is no reason why there can be multiple MoveTo operations in one command, but that is undefined behavior 82 if (command.getType() == Command.MoveTo && operations.length == 2) { 83 x += operations[0]; 84 y += operations[1]; 85 // Avoid fairly expensive Arrays.copyOf calls 86 line = new Path2D.Float(Path2D.WIND_NON_ZERO, commands.size()); 87 line.moveTo(x, y); 88 xArray.add(x); 89 yArray.add(y); 90 shapes.add(line); 91 } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) { 92 for (int i = 0; i < operations.length / 2; i++) { 93 x += operations[2 * i]; 94 y += operations[2 * i + 1]; 62 95 xArray.add(x); 63 96 yArray.add(y); 64 shapes.add(line); 65 } else if (command.getType() == Command.LineTo && operations.length % 2 == 0 && line != null) { 66 for (int i = 0; i < operations.length / 2; i++) { 67 x += operations[2 * i]; 68 y += operations[2 * i + 1]; 69 xArray.add(x); 70 yArray.add(y); 71 line.lineTo(x, y); 72 } 97 line.lineTo(x, y); 98 } 73 99 // ClosePath should only be used with Polygon geometry 74 } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) {75 shapes.remove(line);76 // new Area() closes the line if it isn't already closed77 if (area == null) {78 area = new Area();79 shapes.add(area);80 }100 } else if (geometryType == GeometryTypes.POLYGON && command.getType() == Command.ClosePath && line != null) { 101 shapes.remove(line); 102 // new Area() closes the line if it isn't already closed 103 if (area == null) { 104 area = new Area(); 105 shapes.add(area); 106 } 81 107 82 final double areaAreaSq = calculateSurveyorsArea(xArray.stream().mapToInt(i -> i).toArray(), 83 yArray.stream().mapToInt(i -> i).toArray()); 84 Area nArea = new Area(line); 85 // SonarLint thinks that this is never > 0. It can be. 86 if (areaAreaSq > 0) { 87 area.add(nArea); 88 } else if (areaAreaSq < 0) { 89 area.exclusiveOr(nArea); 90 } else { 91 throw new IllegalArgumentException(tr("{0} cannot have zero area", geometryType)); 92 } 93 xArray.clear(); 94 yArray.clear(); 108 final double areaAreaSq = calculateSurveyorsArea(xArray.stream().mapToInt(i -> i).toArray(), 109 yArray.stream().mapToInt(i -> i).toArray()); 110 Area nArea = new Area(line); 111 // SonarLint thinks that this is never > 0. It can be. 112 if (areaAreaSq > 0) { 113 area.add(nArea); 114 } else if (areaAreaSq < 0) { 115 area.exclusiveOr(nArea); 95 116 } else { 96 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length));117 throw new IllegalArgumentException(tr("{0} cannot have zero area", geometryType)); 97 118 } 119 xArray.clear(); 120 yArray.clear(); 121 } else { 122 throw new IllegalArgumentException(tr("{0} with {1} arguments is not understood", geometryType, operations.length)); 98 123 } 99 124 } 100 125 } -
src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java
diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/GeometryTypes.java index 2341207bc5..4694ddde1c 100644
a b public enum GeometryTypes { 18 18 * and one {@link Command#ClosePath} command. See {@link Ring}s. */ 19 19 POLYGON; 20 20 21 private static final GeometryTypes[] CACHED_VALUES = values(); 21 22 /** 22 23 * Rings used by {@link GeometryTypes#POLYGON} 23 24 * @author Taylor Smock … … public enum GeometryTypes { 28 29 /** A ring that goes in the anti-clockwise direction */ 29 30 InteriorRing 30 31 } 32 33 /** 34 * A replacement for {@link #values()} which can be used when there are no changes to the underlying array. 35 * This is useful for avoiding unnecessary allocations. 36 * @return A cached array from {@link #values()}. Do not modify. 37 */ 38 static GeometryTypes[] getAllValues() { 39 return CACHED_VALUES; 40 } 31 41 } -
src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java
diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/Layer.java index ce5680c531..1eb0e5a423 100644
a b 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.imagery.vectortile.mapbox; 3 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 4 5 6 import java.io.ByteArrayOutputStream; 5 7 import java.io.IOException; 6 8 import java.util.ArrayList; 7 9 import java.util.Arrays; 8 10 import java.util.Collection; 9 11 import java.util.Collections; 10 import java.util.Hash Set;12 import java.util.HashMap; 11 13 import java.util.List; 12 14 import java.util.Map; 13 15 import java.util.Objects; … … import java.util.stream.Collectors; 17 19 import org.openstreetmap.josm.data.protobuf.ProtobufParser; 18 20 import org.openstreetmap.josm.data.protobuf.ProtobufRecord; 19 21 import org.openstreetmap.josm.tools.Destroyable; 20 import org.openstreetmap.josm.tools.Logging;21 22 22 23 /** 23 24 * A Mapbox Vector Tile Layer … … public final class Layer implements Destroyable { 99 100 */ 100 101 public Layer(Collection<ProtobufRecord> records) throws IOException { 101 102 // Do the unique required fields first 102 Map<Integer, List<ProtobufRecord>> sorted = records.stream().collect(Collectors.groupingBy(ProtobufRecord::getField)); 103 this.version = sorted.getOrDefault((int) VERSION_FIELD, Collections.emptyList()).parallelStream() 104 .map(ProtobufRecord::asUnsignedVarInt).map(Number::byteValue).findFirst().orElse(DEFAULT_VERSION); 105 // Per spec, we cannot continue past this until we have checked the version number 106 if (this.version != 1 && this.version != 2) { 107 throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", this.version)); 108 } 109 this.name = sorted.getOrDefault((int) NAME_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::asString).findFirst() 110 .orElseThrow(() -> new IllegalArgumentException(tr("Vector tile layers must have a layer name"))); 111 this.extent = sorted.getOrDefault((int) EXTENT_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::asUnsignedVarInt) 112 .map(Number::intValue).findAny().orElse(DEFAULT_EXTENT); 113 114 sorted.getOrDefault((int) KEY_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::asString) 115 .forEachOrdered(this.keyList::add); 116 sorted.getOrDefault((int) VALUE_FIELD, Collections.emptyList()).parallelStream().map(ProtobufRecord::getBytes) 117 .map(ProtobufParser::new).map(parser1 -> { 118 try { 119 return new ProtobufRecord(parser1); 120 } catch (IOException e) { 121 Logging.error(e); 122 return null; 123 } 124 }) 125 .filter(Objects::nonNull) 126 .map(value -> ValueFields.MAPPERS.parallelStream() 127 .filter(v -> v.getField() == value.getField()) 128 .map(v -> v.convertValue(value)).findFirst() 129 .orElseThrow(() -> new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", value.getField())))) 130 .forEachOrdered(this.valueList::add); 131 Collection<IOException> exceptions = new HashSet<>(0); 132 this.featureCollection = sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).parallelStream().map(feature -> { 133 try { 134 return new Feature(this, feature); 135 } catch (IOException e) { 136 exceptions.add(e); 103 Map<Integer, List<ProtobufRecord>> sorted = new HashMap<>(records.size()); 104 byte tVersion = DEFAULT_VERSION; 105 String tName = null; 106 int tExtent = DEFAULT_EXTENT; 107 final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4); 108 for (ProtobufRecord protobufRecord : records) { 109 if (protobufRecord.getField() == VERSION_FIELD) { 110 tVersion = protobufRecord.asUnsignedVarInt().byteValue(); 111 // Per spec, we cannot continue past this until we have checked the version number 112 if (tVersion != 1 && tVersion != 2) { 113 throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", tVersion)); 114 } 115 } else if (protobufRecord.getField() == NAME_FIELD) { 116 tName = protobufRecord.asString(); 117 } else if (protobufRecord.getField() == EXTENT_FIELD) { 118 tExtent = protobufRecord.asUnsignedVarInt().intValue(); 119 } else if (protobufRecord.getField() == KEY_FIELD) { 120 this.keyList.add(protobufRecord.asString()); 121 } else if (protobufRecord.getField() == VALUE_FIELD) { 122 parseValueRecord(byteArrayOutputStream, protobufRecord); 123 } else { 124 sorted.computeIfAbsent(protobufRecord.getField(), i -> new ArrayList<>(records.size())).add(protobufRecord); 137 125 } 138 return null; 139 }).collect(Collectors.toList()); 140 if (!exceptions.isEmpty()) { 141 throw exceptions.iterator().next(); 126 } 127 this.version = tVersion; 128 if (tName == null) { 129 throw new IllegalArgumentException(tr("Vector tile layers must have a layer name")); 130 } 131 this.name = tName; 132 this.extent = tExtent; 133 134 this.featureCollection = new ArrayList<>(sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).size()); 135 for (ProtobufRecord protobufRecord : sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList())) { 136 this.featureCollection.add(new Feature(this, protobufRecord)); 142 137 } 143 138 // Cleanup bytes (for memory) 144 for (ProtobufRecord record : records) { 145 record.close(); 139 for (ProtobufRecord protobufRecord : records) { 140 protobufRecord.close(); 141 } 142 } 143 144 private void parseValueRecord(ByteArrayOutputStream byteArrayOutputStream, ProtobufRecord protobufRecord) 145 throws IOException { 146 try (ProtobufParser parser = new ProtobufParser(protobufRecord.getBytes())) { 147 ProtobufRecord protobufRecord2 = new ProtobufRecord(byteArrayOutputStream, parser); 148 int field = protobufRecord2.getField(); 149 int valueListSize = this.valueList.size(); 150 for (Layer.ValueFields<?> mapper : ValueFields.MAPPERS) { 151 if (mapper.getField() == field) { 152 this.valueList.add(mapper.convertValue(protobufRecord2)); 153 break; 154 } 155 } 156 if (valueListSize == this.valueList.size()) { 157 throw new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", field)); 158 } 146 159 } 147 160 } 148 161 -
src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java
diff --git a/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java b/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/MVTTile.java index 5aa200d603..148e37a30d 100644
a b import java.util.Collection; 8 8 import java.util.HashSet; 9 9 import java.util.List; 10 10 import java.util.Objects; 11 import java.util.stream.Collectors;12 11 13 12 import org.openstreetmap.gui.jmapviewer.Tile; 14 13 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; … … public class MVTTile extends Tile implements VectorTile, IQuadBucketType { 53 52 this.initLoading(); 54 53 ProtobufParser parser = new ProtobufParser(inputStream); 55 54 Collection<ProtobufRecord> protobufRecords = parser.allRecords(); 56 this.layers = new HashSet<>(); 57 this.layers = protobufRecords.stream().map(protoBufRecord -> { 58 Layer mvtLayer = null; 55 this.layers = new HashSet<>(protobufRecords.size()); 56 for (ProtobufRecord protoBufRecord : protobufRecords) { 59 57 if (protoBufRecord.getField() == Layer.LAYER_FIELD) { 60 58 try (ProtobufParser tParser = new ProtobufParser(protoBufRecord.getBytes())) { 61 mvtLayer = new Layer(tParser.allRecords());59 this.layers.add(new Layer(tParser.allRecords())); 62 60 } catch (IOException e) { 63 61 Logging.error(e); 64 62 } finally { … … public class MVTTile extends Tile implements VectorTile, IQuadBucketType { 66 64 protoBufRecord.close(); 67 65 } 68 66 } 69 return mvtLayer; 70 }).collect(Collectors.toCollection(HashSet::new)); 67 } 68 this.layers = new HashSet<>(this.layers); 69 71 70 this.extent = layers.stream().filter(Objects::nonNull).mapToInt(Layer::getExtent).max().orElse(Layer.DEFAULT_EXTENT); 72 71 if (this.getData() != null) { 73 72 this.finishLoading(); -
src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java
diff --git a/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java b/src/org/openstreetmap/josm/data/osm/AbstractPrimitive.java index b4b1e1e339..09556dd3c3 100644
a b import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 6 import java.text.MessageFormat; 7 7 import java.time.Instant; 8 import java.util.ArrayList; 8 9 import java.util.Arrays; 9 10 import java.util.Collection; 10 11 import java.util.Collections; … … public abstract class AbstractPrimitive implements IPrimitive, IFilterablePrimit 611 612 } 612 613 } 613 614 615 @Override 616 public void putAll(Map<String, String> tags) { 617 if (tags == null || tags.isEmpty()) { 618 return; 619 } 620 // Defensive copy of keys 621 String[] newKeys = keys; 622 Map<String, String> originalKeys = getKeys(); 623 List<Map.Entry<String, String>> tagsToAdd = new ArrayList<>(tags.size()); 624 for (Map.Entry<String, String> tag : tags.entrySet()) { 625 if (!Utils.isBlank(tag.getKey())) { 626 int keyIndex = indexOfKey(newKeys, tag.getKey()); 627 // Realistically, we will not hit the newKeys == null branch. If it is null, keyIndex is always < 1 628 if (keyIndex < 0 || newKeys == null) { 629 tagsToAdd.add(tag); 630 } else { 631 newKeys[keyIndex + 1] = tag.getValue(); 632 } 633 } 634 } 635 if (!tagsToAdd.isEmpty()) { 636 int index = newKeys != null ? newKeys.length : 0; 637 newKeys = newKeys != null ? Arrays.copyOf(newKeys, newKeys.length + 2 * tagsToAdd.size()) : new String[2 * tagsToAdd.size()]; 638 for (Map.Entry<String, String> tag : tagsToAdd) { 639 newKeys[index++] = tag.getKey(); 640 newKeys[index++] = tag.getValue(); 641 } 642 keys = newKeys; 643 } 644 keysChangedImpl(originalKeys); 645 } 646 614 647 /** 615 648 * Scans a key/value array for a given key. 616 649 * @param keys The key array. It is not modified. It may be null to indicate an empty array. -
src/org/openstreetmap/josm/data/osm/Tagged.java
diff --git a/src/org/openstreetmap/josm/data/osm/Tagged.java b/src/org/openstreetmap/josm/data/osm/Tagged.java index 067fc804c4..faadcd0bd9 100644
a b public interface Tagged { 68 68 put(tag.getKey(), tag.getValue()); 69 69 } 70 70 71 /** 72 * Add all key/value pairs. This <i>may</i> be more performant than {@link #put}, depending upon the implementation. 73 * By default, this calls {@link #put} for each map entry. 74 * @param tags The tag map to add 75 * @since xxx 76 */ 77 default void putAll(Map<String, String> tags) { 78 for (Map.Entry<String, String> entry : tags.entrySet()) { 79 put(entry.getKey(), entry.getValue()); 80 } 81 } 82 71 83 /** 72 84 * Replies the value of the given key; null, if there is no value for this key 73 85 * -
src/org/openstreetmap/josm/data/protobuf/ProtobufPacked.java
diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtobufPacked.java b/src/org/openstreetmap/josm/data/protobuf/ProtobufPacked.java index c4c331c2cc..1b494514f0 100644
a b import java.util.List; 12 12 * @since 17862 13 13 */ 14 14 public class ProtobufPacked { 15 private static final Number[] NO_NUMBERS = new Number[0]; 15 16 private final byte[] bytes; 16 17 private final Number[] numbers; 17 18 private int location; … … public class ProtobufPacked { 19 20 /** 20 21 * Create a new ProtobufPacked object 21 22 * 23 * @param byteArrayOutputStream A reusable ByteArrayOutputStream (helps to reduce memory allocations) 22 24 * @param bytes The packed bytes 23 25 */ 24 public ProtobufPacked( byte[] bytes) {26 public ProtobufPacked(ByteArrayOutputStream byteArrayOutputStream, byte[] bytes) { 25 27 this.location = 0; 26 28 this.bytes = bytes; 27 List<Number> numbersT = new ArrayList<>(); 29 30 // By creating a list of size bytes.length, we avoid 36 MB of allocations from list growth. This initialization 31 // only adds 3.7 MB to the ArrayList#init calls. Note that the real-world test case (Mapillary vector tiles) 32 // primarily created Shorts. 33 List<Number> numbersT = new ArrayList<>(bytes.length); 28 34 // By reusing a ByteArrayOutputStream, we can reduce allocations in nextVarInt from 230 MB to 74 MB. 29 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4);30 35 while (this.location < bytes.length) { 31 36 numbersT.add(ProtobufParser.convertByteArray(this.nextVarInt(byteArrayOutputStream), ProtobufParser.VAR_INT_BYTE_SIZE)); 32 byteArrayOutputStream.reset();33 37 } 34 38 35 this.numbers = new Number[numbersT.size()]; 36 for (int i = 0; i < numbersT.size(); i++) { 37 this.numbers[i] = numbersT.get(i); 38 } 39 this.numbers = numbersT.toArray(NO_NUMBERS); 39 40 } 40 41 41 42 /** … … public class ProtobufPacked { 50 51 private byte[] nextVarInt(final ByteArrayOutputStream byteArrayOutputStream) { 51 52 // In a real world test, the largest List<Byte> seen had 3 elements. Use 4 to avoid most new array allocations. 52 53 // Memory allocations went from 368 MB to 280 MB by using an initial array allocation. When using a 53 // ByteArrayOutputStream, it went down to 230 MB. 54 // ByteArrayOutputStream, it went down to 230 MB. By further reusing the ByteArrayOutputStream between method 55 // calls, it went down further to 73 MB. 54 56 while ((this.bytes[this.location] & ProtobufParser.MOST_SIGNIFICANT_BYTE) 55 57 == ProtobufParser.MOST_SIGNIFICANT_BYTE) { 56 58 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned) … … public class ProtobufPacked { 58 60 } 59 61 // The last byte doesn't drop the most significant bit 60 62 byteArrayOutputStream.write(this.bytes[this.location++]); 61 return byteArrayOutputStream.toByteArray(); 63 try { 64 return byteArrayOutputStream.toByteArray(); 65 } finally { 66 byteArrayOutputStream.reset(); 67 } 62 68 } 63 69 } -
src/org/openstreetmap/josm/data/protobuf/ProtobufParser.java
diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtobufParser.java b/src/org/openstreetmap/josm/data/protobuf/ProtobufParser.java index dd2553223f..60d4fb29aa 100644
a b public class ProtobufParser implements AutoCloseable { 121 121 */ 122 122 public Collection<ProtobufRecord> allRecords() throws IOException { 123 123 Collection<ProtobufRecord> records = new ArrayList<>(); 124 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4); 124 125 while (this.hasNext()) { 125 records.add(new ProtobufRecord( this));126 records.add(new ProtobufRecord(byteArrayOutputStream, this)); 126 127 } 127 128 return records; 128 129 } … … public class ProtobufParser implements AutoCloseable { 196 197 /** 197 198 * Get the next delimited message ({@link WireType#LENGTH_DELIMITED}) 198 199 * 200 * @param byteArrayOutputStream A reusable stream to write bytes to. This can significantly reduce the allocations 201 * (150 MB to 95 MB in a test area). 199 202 * @return The next length delimited message 200 203 * @throws IOException - if an IO error occurs 201 204 */ 202 public byte[] nextLengthDelimited( ) throws IOException {203 int length = convertByteArray(this.nextVarInt( ), VAR_INT_BYTE_SIZE).intValue();205 public byte[] nextLengthDelimited(ByteArrayOutputStream byteArrayOutputStream) throws IOException { 206 int length = convertByteArray(this.nextVarInt(byteArrayOutputStream), VAR_INT_BYTE_SIZE).intValue(); 204 207 return readNextBytes(length); 205 208 } 206 209 207 210 /** 208 211 * Get the next var int ({@code WireType#VARINT}) 209 212 * 213 * @param byteArrayOutputStream A reusable stream to write bytes to. This can significantly reduce the allocations 214 * (150 MB to 95 MB in a test area). 210 215 * @return The next var int ({@code int32}, {@code int64}, {@code uint32}, {@code uint64}, {@code bool}, {@code enum}) 211 216 * @throws IOException - if an IO error occurs 212 217 */ 213 public byte[] nextVarInt( ) throws IOException {218 public byte[] nextVarInt(ByteArrayOutputStream byteArrayOutputStream) throws IOException { 214 219 // Using this reduces the allocations from 150 MB to 95 MB. 215 final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4);216 220 int currentByte = this.nextByte(); 217 221 while ((byte) (currentByte & MOST_SIGNIFICANT_BYTE) == MOST_SIGNIFICANT_BYTE && currentByte > 0) { 218 222 // Get rid of the leading bit (shift left 1, then shift right 1 unsigned) … … public class ProtobufParser implements AutoCloseable { 221 225 } 222 226 // The last byte doesn't drop the most significant bit 223 227 byteArrayOutputStream.write(currentByte); 224 return byteArrayOutputStream.toByteArray(); 228 try { 229 return byteArrayOutputStream.toByteArray(); 230 } finally { 231 byteArrayOutputStream.reset(); 232 } 225 233 } 226 234 227 235 /** -
src/org/openstreetmap/josm/data/protobuf/ProtobufRecord.java
diff --git a/src/org/openstreetmap/josm/data/protobuf/ProtobufRecord.java b/src/org/openstreetmap/josm/data/protobuf/ProtobufRecord.java index 6c9784215f..1a679100ea 100644
a b 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.protobuf; 3 3 4 import java.io.ByteArrayOutputStream; 4 5 import java.io.IOException; 5 6 import java.nio.charset.StandardCharsets; 6 7 … … public class ProtobufRecord implements AutoCloseable { 21 22 /** 22 23 * Create a new Protobuf record 23 24 * 25 * @param byteArrayOutputStream A reusable ByteArrayOutputStream to avoid unnecessary allocations 24 26 * @param parser The parser to use to create the record 25 27 * @throws IOException - if an IO error occurs 26 28 */ 27 public ProtobufRecord( ProtobufParser parser) throws IOException {28 Number number = ProtobufParser.convertByteArray(parser.nextVarInt( ), ProtobufParser.VAR_INT_BYTE_SIZE);29 public ProtobufRecord(ByteArrayOutputStream byteArrayOutputStream, ProtobufParser parser) throws IOException { 30 Number number = ProtobufParser.convertByteArray(parser.nextVarInt(byteArrayOutputStream), ProtobufParser.VAR_INT_BYTE_SIZE); 29 31 // I don't foresee having field numbers > {@code Integer#MAX_VALUE >> 3} 30 32 this.field = (int) number.longValue() >> 3; 31 33 // 7 is 111 (so last three bits) … … public class ProtobufRecord implements AutoCloseable { 42 44 this.type = tType; 43 45 44 46 if (this.type == WireType.VARINT) { 45 this.bytes = parser.nextVarInt( );47 this.bytes = parser.nextVarInt(byteArrayOutputStream); 46 48 } else if (this.type == WireType.SIXTY_FOUR_BIT) { 47 49 this.bytes = parser.nextFixed64(); 48 50 } else if (this.type == WireType.THIRTY_TWO_BIT) { 49 51 this.bytes = parser.nextFixed32(); 50 52 } else if (this.type == WireType.LENGTH_DELIMITED) { 51 this.bytes = parser.nextLengthDelimited( );53 this.bytes = parser.nextLengthDelimited(byteArrayOutputStream); 52 54 } else { 53 55 this.bytes = EMPTY_BYTES; 54 56 } -
src/org/openstreetmap/josm/data/vector/VectorDataStore.java
diff --git a/src/org/openstreetmap/josm/data/vector/VectorDataStore.java b/src/org/openstreetmap/josm/data/vector/VectorDataStore.java index d339627e9b..44584737d4 100644
a b import java.awt.geom.PathIterator; 12 12 import java.util.ArrayList; 13 13 import java.util.Collection; 14 14 import java.util.Collections; 15 import java.util.HashMap; 15 16 import java.util.List; 17 import java.util.Map; 16 18 import java.util.Objects; 17 19 18 import org.openstreetmap.gui.jmapviewer.Coordinate;19 20 import org.openstreetmap.gui.jmapviewer.Tile; 20 21 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 21 22 import org.openstreetmap.josm.data.IQuadBucketType; 23 import org.openstreetmap.josm.data.coor.ILatLon; 24 import org.openstreetmap.josm.data.coor.LatLon; 22 25 import org.openstreetmap.josm.data.imagery.vectortile.VectorTile; 23 26 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Feature; 24 27 import org.openstreetmap.josm.data.imagery.vectortile.mapbox.Layer; … … import org.openstreetmap.josm.tools.Destroyable; 35 38 import org.openstreetmap.josm.tools.Geometry; 36 39 import org.openstreetmap.josm.tools.JosmRuntimeException; 37 40 import org.openstreetmap.josm.tools.Logging; 41 import org.openstreetmap.josm.tools.Utils; 38 42 39 43 /** 40 44 * A data store for Vector Data sets … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 156 160 } 157 161 158 162 private synchronized <T extends Tile & VectorTile> VectorNode pointToNode(T tile, Layer layer, 159 Collection<VectorPrimitive> featureObjects, int x, int y ) {163 Collection<VectorPrimitive> featureObjects, int x, int y, final Map<ILatLon, VectorNode> nodeMap) { 160 164 final BBox tileBbox; 161 165 if (tile instanceof IQuadBucketType) { 162 166 tileBbox = ((IQuadBucketType) tile).getBBox(); … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 168 172 tileBbox = new BBox(upperLeft.getLon(), upperLeft.getLat(), lowerRight.getLon(), lowerRight.getLat()); 169 173 } 170 174 final int layerExtent = layer.getExtent(); 171 final ICoordinate coords = new Coordinate(175 final LatLon coords = new LatLon( 172 176 tileBbox.getMaxLat() - (tileBbox.getMaxLat() - tileBbox.getMinLat()) * y / layerExtent, 173 177 tileBbox.getMinLon() + (tileBbox.getMaxLon() - tileBbox.getMinLon()) * x / layerExtent 174 178 ); 179 if (nodeMap.containsKey(coords)) { 180 return nodeMap.get(coords); 181 } 175 182 final Collection<VectorNode> nodes = this.store 176 .searchNodes(new BBox(coords. getLon(), coords.getLat(), VectorDataSet.DUPE_NODE_DISTANCE));183 .searchNodes(new BBox(coords.lon(), coords.lat(), VectorDataSet.DUPE_NODE_DISTANCE)); 177 184 final VectorNode node; 178 185 if (!nodes.isEmpty()) { 179 186 final VectorNode first = nodes.iterator().next(); … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 202 209 } 203 210 node.setCoor(coords); 204 211 featureObjects.add(node); 212 nodeMap.put(node.getCoor(), node); 205 213 return node; 206 214 } 207 215 208 216 private <T extends Tile & VectorTile> List<VectorWay> pathToWay(T tile, Layer layer, 209 Collection<VectorPrimitive> featureObjects, Path2D shape ) {217 Collection<VectorPrimitive> featureObjects, Path2D shape, Map<ILatLon, VectorNode> nodeMap) { 210 218 final PathIterator pathIterator = shape.getPathIterator(null); 211 final List<VectorWay> ways = pathIteratorToObjects(tile, layer, featureObjects, pathIterator).stream()212 .filter(VectorWay.class::isInstance).map(VectorWay.class::cast).collect(toList());219 final List<VectorWay> ways = new ArrayList<>( 220 Utils.filteredCollection(pathIteratorToObjects(tile, layer, featureObjects, pathIterator, nodeMap), VectorWay.class)); 213 221 // These nodes technically do not exist, so we shouldn't show them 214 ways.stream().flatMap(way -> way.getNodes().stream()) 215 .filter(prim -> !prim.isTagged() && prim.getReferrers(true).size() == 1 && prim.getId() <= 0) 216 .forEach(prim -> { 217 prim.setDisabled(true); 218 prim.setVisible(false); 219 }); 222 for (VectorWay way : ways) { 223 for (VectorNode node : way.getNodes()) { 224 if (!node.hasKeys() && node.getReferrers(true).size() == 1 && node.getId() <= 0) { 225 node.setDisabled(true); 226 node.setVisible(false); 227 } 228 } 229 } 220 230 return ways; 221 231 } 222 232 223 233 private <T extends Tile & VectorTile> List<VectorPrimitive> pathIteratorToObjects(T tile, Layer layer, 224 Collection<VectorPrimitive> featureObjects, PathIterator pathIterator ) {234 Collection<VectorPrimitive> featureObjects, PathIterator pathIterator, Map<ILatLon, VectorNode> nodeMap) { 225 235 final List<VectorNode> nodes = new ArrayList<>(); 226 236 final double[] coords = new double[6]; 227 237 final List<VectorPrimitive> ways = new ArrayList<>(); … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 242 252 nodes.clear(); 243 253 } 244 254 if (PathIterator.SEG_MOVETO == type || PathIterator.SEG_LINETO == type) { 245 final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1] );255 final VectorNode node = pointToNode(tile, layer, featureObjects, (int) coords[0], (int) coords[1], nodeMap); 246 256 nodes.add(node); 247 257 } else if (PathIterator.SEG_CLOSE != type) { 248 258 // Vector Tiles only have MoveTo, LineTo, and ClosePath. Anything else is not supported at this time. … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 259 269 } 260 270 261 271 private <T extends Tile & VectorTile> VectorRelation areaToRelation(T tile, Layer layer, 262 Collection<VectorPrimitive> featureObjects, Area area ) {272 Collection<VectorPrimitive> featureObjects, Area area, Map<ILatLon, VectorNode> nodeMap) { 263 273 VectorRelation vectorRelation = new VectorRelation(layer.getName()); 264 for (VectorPrimitive member : pathIteratorToObjects(tile, layer, featureObjects, area.getPathIterator(null) )) {274 for (VectorPrimitive member : pathIteratorToObjects(tile, layer, featureObjects, area.getPathIterator(null), nodeMap)) { 265 275 final String role; 266 276 if (member instanceof VectorWay && ((VectorWay) member).isClosed()) { 267 277 role = Geometry.isClockwise(((VectorWay) member).getNodes()) ? "outer" : "inner"; … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 279 289 * @param <T> The tile type 280 290 */ 281 291 public <T extends Tile & VectorTile> void addDataTile(T tile) { 292 // Using a map reduces the cost of addFeatureData from 2,715,158,632 bytes to 235,042,184 bytes (-91.3%) 293 // This was somewhat variant, with some runs being closer to ~560 MB (still -80%). 294 final Map<ILatLon, VectorNode> nodeMap = new HashMap<>(); 282 295 for (Layer layer : tile.getLayers()) { 283 296 for (Feature feature : layer.getFeatures()) { 284 297 try { 285 addFeatureData(tile, layer, feature );298 addFeatureData(tile, layer, feature, nodeMap); 286 299 } catch (IllegalArgumentException e) { 287 300 Logging.error("Cannot add vector data for feature {0} of tile {1}: {2}", feature, tile, e.getMessage()); 288 301 Logging.error(e); … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 298 311 .findAny().orElse(null))); 299 312 } 300 313 301 private <T extends Tile & VectorTile> void addFeatureData(T tile, Layer layer, Feature feature) { 302 List<VectorPrimitive> featureObjects = new ArrayList<>(); 303 List<VectorPrimitive> primaryFeatureObjects = feature.getGeometryObject().getShapes().stream() 304 .map(shape -> shapeToPrimaryFeatureObject(tile, layer, shape, featureObjects)).collect(toList()); 314 private <T extends Tile & VectorTile> void addFeatureData(T tile, Layer layer, Feature feature, Map<ILatLon, VectorNode> nodeMap) { 315 // This will typically be larger than primaryFeatureObjects, but this at least avoids quite a few ArrayList#grow calls 316 List<VectorPrimitive> featureObjects = new ArrayList<>(feature.getGeometryObject().getShapes().size()); 317 List<VectorPrimitive> primaryFeatureObjects = new ArrayList<>(featureObjects.size()); 318 for (Shape shape : feature.getGeometryObject().getShapes()) { 319 primaryFeatureObjects.add(shapeToPrimaryFeatureObject(tile, layer, shape, featureObjects, nodeMap)); 320 } 305 321 final VectorPrimitive primitive; 306 322 if (primaryFeatureObjects.size() == 1) { 307 323 primitive = primaryFeatureObjects.get(0); … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 310 326 } 311 327 } else if (!primaryFeatureObjects.isEmpty()) { 312 328 VectorRelation relation = new VectorRelation(layer.getName()); 313 primaryFeatureObjects.stream().map(prim -> new VectorRelationMember("", prim)) 314 .forEach(relation::addRelationMember); 329 List<VectorRelationMember> members = new ArrayList<>(primaryFeatureObjects.size()); 330 for (VectorPrimitive prim : primaryFeatureObjects) { 331 members.add(new VectorRelationMember("", prim)); 332 } 333 relation.setMembers(members); 315 334 primitive = relation; 316 335 } else { 317 336 return; … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 325 344 primitive.setId(primitive.getIdGenerator().generateUniqueId()); 326 345 } 327 346 if (feature.getTags() != null) { 328 feature.getTags().forEach(primitive::put); 347 primitive.putAll(feature.getTags()); 348 } 349 for (VectorPrimitive prim : featureObjects) { 350 this.addPrimitive(prim); 351 } 352 for (VectorPrimitive prim : primaryFeatureObjects) { 353 this.addPrimitive(prim); 329 354 } 330 featureObjects.forEach(this::addPrimitive);331 primaryFeatureObjects.forEach(this::addPrimitive);332 355 try { 333 356 this.addPrimitive(primitive); 334 357 } catch (JosmRuntimeException e) { … … public class VectorDataStore extends DataStore<VectorPrimitive, VectorNode, Vect 338 361 } 339 362 340 363 private <T extends Tile & VectorTile> VectorPrimitive shapeToPrimaryFeatureObject( 341 T tile, Layer layer, Shape shape, List<VectorPrimitive> featureObjects ) {364 T tile, Layer layer, Shape shape, List<VectorPrimitive> featureObjects, Map<ILatLon, VectorNode> nodeMap) { 342 365 final VectorPrimitive primitive; 343 366 if (shape instanceof Ellipse2D) { 344 367 primitive = pointToNode(tile, layer, featureObjects, 345 (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY() );368 (int) ((Ellipse2D) shape).getCenterX(), (int) ((Ellipse2D) shape).getCenterY(), nodeMap); 346 369 } else if (shape instanceof Path2D) { 347 primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape ).stream().findFirst().orElse(null);370 primitive = pathToWay(tile, layer, featureObjects, (Path2D) shape, nodeMap).stream().findFirst().orElse(null); 348 371 } else if (shape instanceof Area) { 349 primitive = areaToRelation(tile, layer, featureObjects, (Area) shape );372 primitive = areaToRelation(tile, layer, featureObjects, (Area) shape, nodeMap); 350 373 primitive.put(RELATION_TYPE, MULTIPOLYGON_TYPE); 351 374 } else { 352 375 // We shouldn't hit this, but just in case -
src/org/openstreetmap/josm/data/vector/VectorNode.java
diff --git a/src/org/openstreetmap/josm/data/vector/VectorNode.java b/src/org/openstreetmap/josm/data/vector/VectorNode.java index a406d775b2..3e215a9477 100644
a b public class VectorNode extends VectorPrimitive implements INode { 25 25 private static final UniqueIdGenerator ID_GENERATOR = new UniqueIdGenerator(); 26 26 private double lon = Double.NaN; 27 27 private double lat = Double.NaN; 28 private BBox cachedBbox; 28 29 29 30 /** 30 31 * Create a new vector node … … public class VectorNode extends VectorPrimitive implements INode { 56 57 57 58 @Override 58 59 public void setCoor(LatLon coordinates) { 59 this.lat = coordinates.lat(); 60 this.lon = coordinates.lon(); 60 setCoor(coordinates.lat(), coordinates.lon()); 61 61 } 62 62 63 63 /** … … public class VectorNode extends VectorPrimitive implements INode { 67 67 * @see #setCoor(LatLon) 68 68 */ 69 69 public void setCoor(ICoordinate coordinates) { 70 this.lat = coordinates.getLat(); 71 this.lon = coordinates.getLon(); 70 setCoor(coordinates.getLat(), coordinates.getLon()); 72 71 } 73 72 74 73 @Override 75 74 public void setEastNorth(EastNorth eastNorth) { 76 75 final LatLon ll = ProjectionRegistry.getProjection().eastNorth2latlon(eastNorth); 77 this.lat = ll.lat(); 78 this.lon = ll.lon(); 76 setCoor(ll); 77 } 78 79 private void setCoor(final double lat, final double lon) { 80 this.lat = lat; 81 this.lon = lon; 82 cachedBbox = null; 79 83 } 80 84 81 85 @Override … … public class VectorNode extends VectorPrimitive implements INode { 104 108 105 109 @Override 106 110 public BBox getBBox() { 107 return new BBox(this.lon, this.lat).toImmutable(); 111 // Don't make immutable -- we don't care (not a cached bbox), and making it immutable doubles the cost of this call. 112 // This is important since QuadBuckets#search ends up calling getBBox() repeatedly when doing a search. 113 // This really only matters during the construction of the Vector layers. 114 if (cachedBbox == null) { 115 updateCachedBBox(); 116 } 117 return cachedBbox; 118 } 119 120 private synchronized void updateCachedBBox() { 121 if (cachedBbox == null) { 122 cachedBbox = new BBox(this.lon, this.lat).toImmutable(); 123 } 108 124 } 109 125 110 126 @Override -
src/org/openstreetmap/josm/data/vector/VectorPrimitive.java
diff --git a/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java b/src/org/openstreetmap/josm/data/vector/VectorPrimitive.java index 6e1fdb47d8..403abf3c32 100644
a b 2 2 package org.openstreetmap.josm.data.vector; 3 3 4 4 import java.util.Arrays; 5 import java.util.Collections; 5 6 import java.util.List; 6 7 import java.util.Map; 7 8 import java.util.function.Consumer; … … public abstract class VectorPrimitive extends AbstractPrimitive implements DataL 117 118 118 119 @Override 119 120 public final List<VectorPrimitive> getReferrers(boolean allowWithoutDataset) { 121 if (this.referrers == null) { 122 return Collections.emptyList(); 123 } else if (this.referrers instanceof VectorPrimitive) { 124 return Collections.singletonList((VectorPrimitive) this.referrers); 125 } 120 126 return referrers(allowWithoutDataset, VectorPrimitive.class) 121 127 .collect(Collectors.toList()); 122 128 } -
src/org/openstreetmap/josm/data/vector/VectorRelation.java
diff --git a/src/org/openstreetmap/josm/data/vector/VectorRelation.java b/src/org/openstreetmap/josm/data/vector/VectorRelation.java index 60ce48d2bb..36079097bd 100644
a b public class VectorRelation extends VectorPrimitive implements IRelation<VectorR 90 90 public void setMembers(List<VectorRelationMember> members) { 91 91 this.members.clear(); 92 92 this.members.addAll(members); 93 for (VectorRelationMember member : members) { 94 member.getMember().addReferrer(this); 95 } 96 cachedBBox = null; 93 97 } 94 98 95 99 @Override -
test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java
diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufRecordTest.java index f99aa7e2e0..1182151287 100644
a b package org.openstreetmap.josm.data.protobuf; 3 3 4 4 import static org.junit.jupiter.api.Assertions.assertEquals; 5 5 6 6 import java.io.ByteArrayOutputStream; 7 7 import java.io.IOException; 8 8 9 9 import org.junit.jupiter.api.Test; … … class ProtobufRecordTest { 15 15 @Test 16 16 void testFixed32() throws IOException { 17 17 ProtobufParser parser = new ProtobufParser(ProtobufTest.toByteArray(new int[] {0x0d, 0x00, 0x00, 0x80, 0x3f})); 18 ProtobufRecord thirtyTwoBit = new ProtobufRecord( parser);18 ProtobufRecord thirtyTwoBit = new ProtobufRecord(new ByteArrayOutputStream(), parser); 19 19 assertEquals(WireType.THIRTY_TWO_BIT, thirtyTwoBit.getType()); 20 20 assertEquals(1f, thirtyTwoBit.asFloat()); 21 21 } … … class ProtobufRecordTest { 23 23 @Test 24 24 void testUnknown() throws IOException { 25 25 ProtobufParser parser = new ProtobufParser(ProtobufTest.toByteArray(new int[] {0x0f, 0x00, 0x00, 0x80, 0x3f})); 26 ProtobufRecord unknown = new ProtobufRecord( parser);26 ProtobufRecord unknown = new ProtobufRecord(new ByteArrayOutputStream(), parser); 27 27 assertEquals(WireType.UNKNOWN, unknown.getType()); 28 28 assertEquals(0, unknown.getBytes().length); 29 29 } -
test/unit/org/openstreetmap/josm/data/protobuf/ProtobufTest.java
diff --git a/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufTest.java b/test/unit/org/openstreetmap/josm/data/protobuf/ProtobufTest.java index 0844c6ebd9..7237e40c1c 100644
a b import static org.junit.jupiter.api.Assertions.fail; 7 7 8 8 import java.awt.geom.Ellipse2D; 9 9 import java.io.ByteArrayInputStream; 10 import java.io.ByteArrayOutputStream; 10 11 import java.io.File; 11 12 import java.io.IOException; 12 13 import java.io.InputStream; … … class ProtobufTest { 171 172 @Test 172 173 void testSimpleMessage() throws IOException { 173 174 ProtobufParser parser = new ProtobufParser(new byte[] {(byte) 0x08, (byte) 0x96, (byte) 0x01}); 174 ProtobufRecord record = new ProtobufRecord( parser);175 ProtobufRecord record = new ProtobufRecord(new ByteArrayOutputStream(), parser); 175 176 assertEquals(WireType.VARINT, record.getType()); 176 177 assertEquals(150, record.asUnsignedVarInt().intValue()); 177 178 }
