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}