001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.imagery.vectortile.mapbox;
003
004import java.io.IOException;
005import java.text.NumberFormat;
006import java.util.ArrayList;
007import java.util.List;
008import java.util.Locale;
009
010import org.openstreetmap.josm.data.osm.TagMap;
011import org.openstreetmap.josm.data.protobuf.ProtobufPacked;
012import org.openstreetmap.josm.data.protobuf.ProtobufParser;
013import org.openstreetmap.josm.data.protobuf.ProtobufRecord;
014import org.openstreetmap.josm.tools.Utils;
015
016/**
017 * A Feature for a {@link Layer}
018 *
019 * @author Taylor Smock
020 * @since 17862
021 */
022public class Feature {
023    private static final byte ID_FIELD = 1;
024    private static final byte TAG_FIELD = 2;
025    private static final byte GEOMETRY_TYPE_FIELD = 3;
026    private static final byte GEOMETRY_FIELD = 4;
027    /**
028     * The geometry of the feature. Required.
029     */
030    private final List<CommandInteger> geometry = new ArrayList<>();
031
032    /**
033     * The geometry type of the feature. Required.
034     */
035    private final GeometryTypes geometryType;
036    /**
037     * The id of the feature. Optional.
038     */
039    // Technically, uint64
040    private final long id;
041    /**
042     * The tags of the feature. Optional.
043     */
044    private TagMap tags;
045    private Geometry geometryObject;
046
047    /**
048     * Create a new Feature
049     *
050     * @param layer  The layer the feature is part of (required for tags)
051     * @param record The record to create the feature from
052     * @throws IOException - if an IO error occurs
053     */
054    public Feature(Layer layer, ProtobufRecord record) throws IOException {
055        long tId = 0;
056        GeometryTypes geometryTypeTemp = GeometryTypes.UNKNOWN;
057        String key = null;
058        try (ProtobufParser parser = new ProtobufParser(record.getBytes())) {
059            while (parser.hasNext()) {
060                try (ProtobufRecord next = new ProtobufRecord(parser)) {
061                    if (next.getField() == TAG_FIELD) {
062                        if (tags == null) {
063                            tags = new TagMap();
064                        }
065                        // This is packed in v1 and v2
066                        ProtobufPacked packed = new ProtobufPacked(next.getBytes());
067                        for (Number number : packed.getArray()) {
068                            key = parseTagValue(key, layer, number);
069                        }
070                    } else if (next.getField() == GEOMETRY_FIELD) {
071                        // This is packed in v1 and v2
072                        ProtobufPacked packed = new ProtobufPacked(next.getBytes());
073                        CommandInteger currentCommand = null;
074                        for (Number number : packed.getArray()) {
075                            if (currentCommand != null && currentCommand.hasAllExpectedParameters()) {
076                                currentCommand = null;
077                            }
078                            if (currentCommand == null) {
079                                currentCommand = new CommandInteger(number.intValue());
080                                this.geometry.add(currentCommand);
081                            } else {
082                                currentCommand.addParameter(ProtobufParser.decodeZigZag(number));
083                            }
084                        }
085                        // TODO fallback to non-packed
086                    } else if (next.getField() == GEOMETRY_TYPE_FIELD) {
087                        geometryTypeTemp = GeometryTypes.values()[next.asUnsignedVarInt().intValue()];
088                    } else if (next.getField() == ID_FIELD) {
089                        tId = next.asUnsignedVarInt().longValue();
090                    }
091                }
092            }
093        }
094        this.id = tId;
095        this.geometryType = geometryTypeTemp;
096        record.close();
097    }
098
099    /**
100     * Parse a tag value
101     *
102     * @param key    The current key (or {@code null}, if {@code null}, the returned value will be the new key)
103     * @param layer  The layer with key/value information
104     * @param number The number to get the value from
105     * @return The new key (if {@code null}, then a value was parsed and added to tags)
106     */
107    private String parseTagValue(String key, Layer layer, Number number) {
108        if (key == null) {
109            key = layer.getKey(number.intValue());
110        } else {
111            Object value = layer.getValue(number.intValue());
112            if (value instanceof Double || value instanceof Float) {
113                // reset grouping if the instance is a singleton
114                final NumberFormat numberFormat = NumberFormat.getNumberInstance(Locale.ROOT);
115                final boolean grouping = numberFormat.isGroupingUsed();
116                try {
117                    numberFormat.setGroupingUsed(false);
118                    this.tags.put(key, numberFormat.format(value));
119                } finally {
120                    numberFormat.setGroupingUsed(grouping);
121                }
122            } else {
123                this.tags.put(key, Utils.intern(value.toString()));
124            }
125            key = null;
126        }
127        return key;
128    }
129
130    /**
131     * Get the geometry instructions
132     *
133     * @return The geometry
134     */
135    public List<CommandInteger> getGeometry() {
136        return this.geometry;
137    }
138
139    /**
140     * Get the geometry type
141     *
142     * @return The {@link GeometryTypes}
143     */
144    public GeometryTypes getGeometryType() {
145        return this.geometryType;
146    }
147
148    /**
149     * Get the id of the object
150     *
151     * @return The unique id in the layer, or 0.
152     */
153    public long getId() {
154        return this.id;
155    }
156
157    /**
158     * Get the tags
159     *
160     * @return A tag map
161     */
162    public TagMap getTags() {
163        return this.tags;
164    }
165
166    /**
167     * Get the an object with shapes for the geometry
168     * @return An object with usable geometry information
169     * @throws IllegalArgumentException if the geometry object cannot be created because arguments are not understood
170     *                                  or the shoelace formula returns 0 for a polygon ring.
171     */
172    public Geometry getGeometryObject() {
173        if (this.geometryObject == null) {
174            this.geometryObject = new Geometry(this.getGeometryType(), this.getGeometry());
175        }
176        return this.geometryObject;
177    }
178
179    @Override
180    public String toString() {
181        return "Feature [geometry=" + geometry + ", "
182                + "geometryType=" + geometryType + ", id=" + id + ", "
183                + (tags != null ? "tags=" + tags + ", " : "")
184                + (geometryObject != null ? "geometryObject=" + geometryObject : "") + ']';
185    }
186}