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.io.ByteArrayOutputStream;
|
---|
7 | import java.io.IOException;
|
---|
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.Objects;
|
---|
16 | import java.util.function.Function;
|
---|
17 | import java.util.stream.Collectors;
|
---|
18 |
|
---|
19 | import org.openstreetmap.josm.data.protobuf.ProtobufParser;
|
---|
20 | import org.openstreetmap.josm.data.protobuf.ProtobufRecord;
|
---|
21 | import org.openstreetmap.josm.tools.Destroyable;
|
---|
22 |
|
---|
23 | /**
|
---|
24 | * A Mapbox Vector Tile Layer
|
---|
25 | * @author Taylor Smock
|
---|
26 | * @since 17862
|
---|
27 | */
|
---|
28 | public final class Layer implements Destroyable {
|
---|
29 | private static final class ValueFields<T> {
|
---|
30 | static final ValueFields<String> STRING = new ValueFields<>(1, ProtobufRecord::asString);
|
---|
31 | static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtobufRecord::asFloat);
|
---|
32 | static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtobufRecord::asDouble);
|
---|
33 | static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtobufRecord::asUnsignedVarInt);
|
---|
34 | // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE})
|
---|
35 | static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtobufRecord::asUnsignedVarInt);
|
---|
36 | static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtobufRecord::asSignedVarInt);
|
---|
37 | static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0);
|
---|
38 |
|
---|
39 | /**
|
---|
40 | * A collection of methods to map a record to a type
|
---|
41 | */
|
---|
42 | public static final Collection<ValueFields<?>> MAPPERS =
|
---|
43 | Collections.unmodifiableList(Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL));
|
---|
44 |
|
---|
45 | private final byte field;
|
---|
46 | private final Function<ProtobufRecord, T> conversion;
|
---|
47 | private ValueFields(int field, Function<ProtobufRecord, T> conversion) {
|
---|
48 | this.field = (byte) field;
|
---|
49 | this.conversion = conversion;
|
---|
50 | }
|
---|
51 |
|
---|
52 | /**
|
---|
53 | * Get the field identifier for the value
|
---|
54 | * @return The identifier
|
---|
55 | */
|
---|
56 | public byte getField() {
|
---|
57 | return this.field;
|
---|
58 | }
|
---|
59 |
|
---|
60 | /**
|
---|
61 | * Convert a protobuf record to a value
|
---|
62 | * @param protobufRecord The record to convert
|
---|
63 | * @return the converted value
|
---|
64 | */
|
---|
65 | public T convertValue(ProtobufRecord protobufRecord) {
|
---|
66 | return this.conversion.apply(protobufRecord);
|
---|
67 | }
|
---|
68 | }
|
---|
69 |
|
---|
70 | /** The field value for a layer (in {@link ProtobufRecord#getField}) */
|
---|
71 | public static final byte LAYER_FIELD = 3;
|
---|
72 | private static final byte VERSION_FIELD = 15;
|
---|
73 | private static final byte NAME_FIELD = 1;
|
---|
74 | private static final byte FEATURE_FIELD = 2;
|
---|
75 | private static final byte KEY_FIELD = 3;
|
---|
76 | private static final byte VALUE_FIELD = 4;
|
---|
77 | private static final byte EXTENT_FIELD = 5;
|
---|
78 | /** The default extent for a vector tile */
|
---|
79 | static final int DEFAULT_EXTENT = 4096;
|
---|
80 | private static final byte DEFAULT_VERSION = 1;
|
---|
81 | /** This is <i>technically</i> an integer, but there are currently only two major versions (1, 2). Required. */
|
---|
82 | private final byte version;
|
---|
83 | /** A unique name for the layer. This <i>must</i> be unique on a per-tile basis. Required. */
|
---|
84 | private final String name;
|
---|
85 |
|
---|
86 | /** The extent of the tile, typically 4096. Required. */
|
---|
87 | private final int extent;
|
---|
88 |
|
---|
89 | /** A list of unique keys. Order is important. Optional. */
|
---|
90 | private final List<String> keyList = new ArrayList<>();
|
---|
91 | /** A list of unique values. Order is important. Optional. */
|
---|
92 | private final List<Object> valueList = new ArrayList<>();
|
---|
93 | /** The actual features of this layer in this tile */
|
---|
94 | private final List<Feature> featureCollection;
|
---|
95 |
|
---|
96 | /**
|
---|
97 | * Create a layer from a collection of records
|
---|
98 | * @param records The records to convert to a layer
|
---|
99 | * @throws IOException - if an IO error occurs
|
---|
100 | */
|
---|
101 | @SuppressWarnings("PMD.CloseResource") // The resources _are_ closed after use; it just isn't detect with PMD 7.2.x.
|
---|
102 | public Layer(Collection<ProtobufRecord> records) throws IOException {
|
---|
103 | // Do the unique required fields first
|
---|
104 | Map<Integer, List<ProtobufRecord>> sorted = new HashMap<>(records.size());
|
---|
105 | byte tVersion = DEFAULT_VERSION;
|
---|
106 | String tName = null;
|
---|
107 | int tExtent = DEFAULT_EXTENT;
|
---|
108 | final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(4);
|
---|
109 | for (ProtobufRecord protobufRecord : records) {
|
---|
110 | if (protobufRecord.getField() == VERSION_FIELD) {
|
---|
111 | tVersion = protobufRecord.asUnsignedVarInt().byteValue();
|
---|
112 | // Per spec, we cannot continue past this until we have checked the version number
|
---|
113 | if (tVersion != 1 && tVersion != 2) {
|
---|
114 | throw new IllegalArgumentException(tr("We do not understand version {0} of the vector tile specification", tVersion));
|
---|
115 | }
|
---|
116 | } else if (protobufRecord.getField() == NAME_FIELD) {
|
---|
117 | tName = protobufRecord.asString();
|
---|
118 | } else if (protobufRecord.getField() == EXTENT_FIELD) {
|
---|
119 | tExtent = protobufRecord.asUnsignedVarInt().intValue();
|
---|
120 | } else if (protobufRecord.getField() == KEY_FIELD) {
|
---|
121 | this.keyList.add(protobufRecord.asString());
|
---|
122 | } else if (protobufRecord.getField() == VALUE_FIELD) {
|
---|
123 | parseValueRecord(byteArrayOutputStream, protobufRecord);
|
---|
124 | } else {
|
---|
125 | sorted.computeIfAbsent(protobufRecord.getField(), i -> new ArrayList<>(records.size())).add(protobufRecord);
|
---|
126 | }
|
---|
127 | }
|
---|
128 | this.version = tVersion;
|
---|
129 | if (tName == null) {
|
---|
130 | throw new IllegalArgumentException(tr("Vector tile layers must have a layer name"));
|
---|
131 | }
|
---|
132 | this.name = tName;
|
---|
133 | this.extent = tExtent;
|
---|
134 |
|
---|
135 | this.featureCollection = new ArrayList<>(sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList()).size());
|
---|
136 | for (ProtobufRecord protobufRecord : sorted.getOrDefault((int) FEATURE_FIELD, Collections.emptyList())) {
|
---|
137 | this.featureCollection.add(new Feature(this, protobufRecord));
|
---|
138 | }
|
---|
139 | // Cleanup bytes (for memory)
|
---|
140 | for (ProtobufRecord protobufRecord : records) { // NOSONAR -- this shouldn't be combined since this is a cleanup loop.
|
---|
141 | protobufRecord.close();
|
---|
142 | }
|
---|
143 | }
|
---|
144 |
|
---|
145 | private void parseValueRecord(ByteArrayOutputStream byteArrayOutputStream, ProtobufRecord protobufRecord)
|
---|
146 | throws IOException {
|
---|
147 | try (ProtobufParser parser = new ProtobufParser(protobufRecord.getBytes())) {
|
---|
148 | ProtobufRecord protobufRecord2 = new ProtobufRecord(byteArrayOutputStream, parser);
|
---|
149 | int field = protobufRecord2.getField();
|
---|
150 | int valueListSize = this.valueList.size();
|
---|
151 | for (Layer.ValueFields<?> mapper : ValueFields.MAPPERS) {
|
---|
152 | if (mapper.getField() == field) {
|
---|
153 | this.valueList.add(mapper.convertValue(protobufRecord2));
|
---|
154 | break;
|
---|
155 | }
|
---|
156 | }
|
---|
157 | if (valueListSize == this.valueList.size()) {
|
---|
158 | throw new IllegalArgumentException(tr("Unknown field in vector tile layer value ({0})", field));
|
---|
159 | }
|
---|
160 | }
|
---|
161 | }
|
---|
162 |
|
---|
163 | /**
|
---|
164 | * Get all the records from a array of bytes
|
---|
165 | * @param bytes The byte information
|
---|
166 | * @return All the protobuf records
|
---|
167 | * @throws IOException If there was an error reading the bytes (unlikely)
|
---|
168 | */
|
---|
169 | private static Collection<ProtobufRecord> getAllRecords(byte[] bytes) throws IOException {
|
---|
170 | try (ProtobufParser parser = new ProtobufParser(bytes)) {
|
---|
171 | return parser.allRecords();
|
---|
172 | }
|
---|
173 | }
|
---|
174 |
|
---|
175 | /**
|
---|
176 | * Create a new layer
|
---|
177 | * @param bytes The bytes that the layer comes from
|
---|
178 | * @throws IOException - if an IO error occurs
|
---|
179 | */
|
---|
180 | public Layer(byte[] bytes) throws IOException {
|
---|
181 | this(getAllRecords(bytes));
|
---|
182 | }
|
---|
183 |
|
---|
184 | /**
|
---|
185 | * Get the extent of the tile
|
---|
186 | * @return The layer extent
|
---|
187 | */
|
---|
188 | public int getExtent() {
|
---|
189 | return this.extent;
|
---|
190 | }
|
---|
191 |
|
---|
192 | /**
|
---|
193 | * Get the feature on this layer
|
---|
194 | * @return the features
|
---|
195 | */
|
---|
196 | public Collection<Feature> getFeatures() {
|
---|
197 | return Collections.unmodifiableCollection(this.featureCollection);
|
---|
198 | }
|
---|
199 |
|
---|
200 | /**
|
---|
201 | * Get the geometry for this layer
|
---|
202 | * @return The geometry
|
---|
203 | */
|
---|
204 | public Collection<Geometry> getGeometry() {
|
---|
205 | return getFeatures().stream().map(Feature::getGeometryObject).collect(Collectors.toList());
|
---|
206 | }
|
---|
207 |
|
---|
208 | /**
|
---|
209 | * Get a specified key
|
---|
210 | * @param index The index in the key list
|
---|
211 | * @return The actual key
|
---|
212 | */
|
---|
213 | public String getKey(int index) {
|
---|
214 | return this.keyList.get(index);
|
---|
215 | }
|
---|
216 |
|
---|
217 | /**
|
---|
218 | * Get the name of the layer
|
---|
219 | * @return The layer name
|
---|
220 | */
|
---|
221 | public String getName() {
|
---|
222 | return this.name;
|
---|
223 | }
|
---|
224 |
|
---|
225 | /**
|
---|
226 | * Get a specified value
|
---|
227 | * @param index The index in the value list
|
---|
228 | * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value.
|
---|
229 | */
|
---|
230 | public Object getValue(int index) {
|
---|
231 | return this.valueList.get(index);
|
---|
232 | }
|
---|
233 |
|
---|
234 | /**
|
---|
235 | * Get the Mapbox Vector Tile version specification for this layer
|
---|
236 | * @return The version of the Mapbox Vector Tile specification
|
---|
237 | */
|
---|
238 | public byte getVersion() {
|
---|
239 | return this.version;
|
---|
240 | }
|
---|
241 |
|
---|
242 | @Override
|
---|
243 | public void destroy() {
|
---|
244 | this.featureCollection.clear();
|
---|
245 | this.keyList.clear();
|
---|
246 | this.valueList.clear();
|
---|
247 | }
|
---|
248 |
|
---|
249 | @Override
|
---|
250 | public boolean equals(Object other) {
|
---|
251 | if (other instanceof Layer) {
|
---|
252 | Layer o = (Layer) other;
|
---|
253 | return this.extent == o.extent
|
---|
254 | && this.version == o.version
|
---|
255 | && Objects.equals(this.name, o.name)
|
---|
256 | && Objects.equals(this.featureCollection, o.featureCollection)
|
---|
257 | && Objects.equals(this.keyList, o.keyList)
|
---|
258 | && Objects.equals(this.valueList, o.valueList);
|
---|
259 | }
|
---|
260 | return false;
|
---|
261 | }
|
---|
262 |
|
---|
263 | @Override
|
---|
264 | public int hashCode() {
|
---|
265 | return Objects.hash(this.name, this.version, this.extent, this.featureCollection, this.keyList, this.valueList);
|
---|
266 | }
|
---|
267 | }
|
---|