001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
003import static org.openstreetmap.josm.tools.I18n.tr;
004
005import java.io.IOException;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.HashSet;
011import java.util.List;
012import java.util.Map;
013import java.util.Objects;
014import java.util.function.Function;
015import java.util.stream.Collectors;
016
017import org.openstreetmap.josm.data.protobuf.ProtobufParser;
018import org.openstreetmap.josm.data.protobuf.ProtobufRecord;
019import org.openstreetmap.josm.tools.Destroyable;
020import org.openstreetmap.josm.tools.Logging;
021
022/**
023 * A Mapbox Vector Tile Layer
024 * @author Taylor Smock
025 * @since 17862
026 */
027public final class Layer implements Destroyable {
028    private static final class ValueFields<T> {
029        static final ValueFields<String> STRING = new ValueFields<>(1, ProtobufRecord::asString);
030        static final ValueFields<Float> FLOAT = new ValueFields<>(2, ProtobufRecord::asFloat);
031        static final ValueFields<Double> DOUBLE = new ValueFields<>(3, ProtobufRecord::asDouble);
032        static final ValueFields<Number> INT64 = new ValueFields<>(4, ProtobufRecord::asUnsignedVarInt);
033        // This may have issues if there are actual uint_values (i.e., more than {@link Long#MAX_VALUE})
034        static final ValueFields<Number> UINT64 = new ValueFields<>(5, ProtobufRecord::asUnsignedVarInt);
035        static final ValueFields<Number> SINT64 = new ValueFields<>(6, ProtobufRecord::asSignedVarInt);
036        static final ValueFields<Boolean> BOOL = new ValueFields<>(7, r -> r.asUnsignedVarInt().longValue() != 0);
037
038        /**
039         * A collection of methods to map a record to a type
040         */
041        public static final Collection<ValueFields<?>> MAPPERS =
042          Collections.unmodifiableList(Arrays.asList(STRING, FLOAT, DOUBLE, INT64, UINT64, SINT64, BOOL));
043
044        private final byte field;
045        private final Function<ProtobufRecord, T> conversion;
046        private ValueFields(int field, Function<ProtobufRecord, T> conversion) {
047            this.field = (byte) field;
048            this.conversion = conversion;
049        }
050
051        /**
052         * Get the field identifier for the value
053         * @return The identifier
054         */
055        public byte getField() {
056            return this.field;
057        }
058
059        /**
060         * Convert a protobuf record to a value
061         * @param protobufRecord The record to convert
062         * @return the converted value
063         */
064        public T convertValue(ProtobufRecord protobufRecord) {
065            return this.conversion.apply(protobufRecord);
066        }
067    }
068
069    /** The field value for a layer (in {@link ProtobufRecord#getField}) */
070    public static final byte LAYER_FIELD = 3;
071    private static final byte VERSION_FIELD = 15;
072    private static final byte NAME_FIELD = 1;
073    private static final byte FEATURE_FIELD = 2;
074    private static final byte KEY_FIELD = 3;
075    private static final byte VALUE_FIELD = 4;
076    private static final byte EXTENT_FIELD = 5;
077    /** The default extent for a vector tile */
078    static final int DEFAULT_EXTENT = 4096;
079    private static final byte DEFAULT_VERSION = 1;
080    /** This is <i>technically</i> an integer, but there are currently only two major versions (1, 2). Required. */
081    private final byte version;
082    /** A unique name for the layer. This <i>must</i> be unique on a per-tile basis. Required. */
083    private final String name;
084
085    /** The extent of the tile, typically 4096. Required. */
086    private final int extent;
087
088    /** A list of unique keys. Order is important. Optional. */
089    private final List<String> keyList = new ArrayList<>();
090    /** A list of unique values. Order is important. Optional. */
091    private final List<Object> valueList = new ArrayList<>();
092    /** The actual features of this layer in this tile */
093    private final List<Feature> featureCollection;
094
095    /**
096     * Create a layer from a collection of records
097     * @param records The records to convert to a layer
098     * @throws IOException - if an IO error occurs
099     */
100    public Layer(Collection<ProtobufRecord> records) throws IOException {
101        // 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);
137            }
138            return null;
139        }).collect(Collectors.toList());
140        if (!exceptions.isEmpty()) {
141            throw exceptions.iterator().next();
142        }
143        // Cleanup bytes (for memory)
144        for (ProtobufRecord record : records) {
145            record.close();
146        }
147    }
148
149    /**
150     * Get all the records from a array of bytes
151     * @param bytes The byte information
152     * @return All the protobuf records
153     * @throws IOException If there was an error reading the bytes (unlikely)
154     */
155    private static Collection<ProtobufRecord> getAllRecords(byte[] bytes) throws IOException {
156        try (ProtobufParser parser = new ProtobufParser(bytes)) {
157            return parser.allRecords();
158        }
159    }
160
161    /**
162     * Create a new layer
163     * @param bytes The bytes that the layer comes from
164     * @throws IOException - if an IO error occurs
165     */
166    public Layer(byte[] bytes) throws IOException {
167        this(getAllRecords(bytes));
168    }
169
170    /**
171     * Get the extent of the tile
172     * @return The layer extent
173     */
174    public int getExtent() {
175        return this.extent;
176    }
177
178    /**
179     * Get the feature on this layer
180     * @return the features
181     */
182    public Collection<Feature> getFeatures() {
183        return Collections.unmodifiableCollection(this.featureCollection);
184    }
185
186    /**
187     * Get the geometry for this layer
188     * @return The geometry
189     */
190    public Collection<Geometry> getGeometry() {
191        return getFeatures().stream().map(Feature::getGeometryObject).collect(Collectors.toList());
192    }
193
194    /**
195     * Get a specified key
196     * @param index The index in the key list
197     * @return The actual key
198     */
199    public String getKey(int index) {
200        return this.keyList.get(index);
201    }
202
203    /**
204     * Get the name of the layer
205     * @return The layer name
206     */
207    public String getName() {
208        return this.name;
209    }
210
211    /**
212     * Get a specified value
213     * @param index The index in the value list
214     * @return The actual value. This can be a {@link String}, {@link Boolean}, {@link Integer}, or {@link Float} value.
215     */
216    public Object getValue(int index) {
217        return this.valueList.get(index);
218    }
219
220    /**
221     * Get the Mapbox Vector Tile version specification for this layer
222     * @return The version of the Mapbox Vector Tile specification
223     */
224    public byte getVersion() {
225        return this.version;
226    }
227
228    @Override
229    public void destroy() {
230        this.featureCollection.clear();
231        this.keyList.clear();
232        this.valueList.clear();
233    }
234
235    @Override
236    public boolean equals(Object other) {
237        if (other instanceof Layer) {
238            Layer o = (Layer) other;
239            return this.extent == o.extent
240              && this.version == o.version
241              && Objects.equals(this.name, o.name)
242              && Objects.equals(this.featureCollection, o.featureCollection)
243              && Objects.equals(this.keyList, o.keyList)
244              && Objects.equals(this.valueList, o.valueList);
245        }
246        return false;
247    }
248
249    @Override
250    public int hashCode() {
251        return Objects.hash(this.name, this.version, this.extent, this.featureCollection, this.keyList, this.valueList);
252    }
253}