source: josm/trunk/src/org/openstreetmap/josm/data/imagery/vectortile/mapbox/style/MapboxVectorStyle.java@ 17862

Last change on this file since 17862 was 17862, checked in by simon04, 3 years ago

fix #17177 - Add support for Mapbox Vector Tile (patch by taylor.smock)

Signed-off-by: Taylor Smock <tsmock@…>

File size: 12.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.imagery.vectortile.mapbox.style;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Image;
7import java.awt.image.BufferedImage;
8import java.io.BufferedReader;
9import java.io.File;
10import java.io.IOException;
11import java.io.InputStream;
12import java.io.OutputStream;
13import java.nio.charset.StandardCharsets;
14import java.nio.file.Files;
15import java.util.Collections;
16import java.util.LinkedHashMap;
17import java.util.List;
18import java.util.Map;
19import java.util.Objects;
20import java.util.Optional;
21import java.util.concurrent.ConcurrentHashMap;
22import java.util.stream.Collectors;
23
24import javax.imageio.ImageIO;
25import javax.json.Json;
26import javax.json.JsonArray;
27import javax.json.JsonObject;
28import javax.json.JsonReader;
29import javax.json.JsonStructure;
30import javax.json.JsonValue;
31
32import org.openstreetmap.josm.data.imagery.vectortile.mapbox.InvalidMapboxVectorTileException;
33import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
34import org.openstreetmap.josm.gui.MainApplication;
35import org.openstreetmap.josm.gui.mappaint.ElemStyles;
36import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
37import org.openstreetmap.josm.io.CachedFile;
38import org.openstreetmap.josm.spi.preferences.Config;
39import org.openstreetmap.josm.tools.Logging;
40
41/**
42 * Create a mapping for a Mapbox Vector Style
43 *
44 * @author Taylor Smock
45 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/">https://docs.mapbox.com/mapbox-gl-js/style-spec/</a>
46 * @since xxx
47 */
48public class MapboxVectorStyle {
49
50 private static final ConcurrentHashMap<String, MapboxVectorStyle> STYLE_MAPPING = new ConcurrentHashMap<>();
51
52 /**
53 * Get a MapboxVector style for a URL
54 * @param url The url to get
55 * @return The Mapbox Vector Style. May be {@code null} if there was an error.
56 */
57 public static MapboxVectorStyle getMapboxVectorStyle(String url) {
58 return STYLE_MAPPING.computeIfAbsent(url, key -> {
59 try (CachedFile style = new CachedFile(url);
60 BufferedReader reader = style.getContentReader();
61 JsonReader jsonReader = Json.createReader(reader)) {
62 JsonStructure structure = jsonReader.read();
63 return new MapboxVectorStyle(structure.asJsonObject());
64 } catch (IOException e) {
65 Logging.error(e);
66 }
67 // Documentation indicates that this will <i>not</i> be entered into the map, which means that this will be
68 // retried if something goes wrong.
69 return null;
70 });
71 }
72
73 /** The version for the style specification */
74 private final int version;
75 /** The optional name for the vector style */
76 private final String name;
77 /** The optional URL for sprites. This mush be absolute (so it must contain the scheme, authority, and path). */
78 private final String spriteUrl;
79 /** The optional URL for glyphs. This may have replaceable values in it. */
80 private final String glyphUrl;
81 /** The required collection of sources with a list of layers that are applicable for that source*/
82 private final Map<Source, ElemStyles> sources;
83
84 /**
85 * Create a new MapboxVector style. You should prefer {@link #getMapboxVectorStyle(String)}
86 * for deduplication purposes.
87 *
88 * @param jsonObject The object to create the style from
89 * @see #getMapboxVectorStyle(String)
90 */
91 public MapboxVectorStyle(JsonObject jsonObject) {
92 // There should be a version specifier. We currently only support version 8.
93 // This can throw an NPE when there is no version number.
94 this.version = jsonObject.getInt("version");
95 if (this.version == 8) {
96 this.name = jsonObject.getString("name", null);
97 String id = jsonObject.getString("id", this.name);
98 this.spriteUrl = jsonObject.getString("sprite", null);
99 this.glyphUrl = jsonObject.getString("glyphs", null);
100 final List<Source> sourceList;
101 if (jsonObject.containsKey("sources") && jsonObject.get("sources").getValueType() == JsonValue.ValueType.OBJECT) {
102 final JsonObject sourceObj = jsonObject.getJsonObject("sources");
103 sourceList = sourceObj.entrySet().stream().filter(entry -> entry.getValue().getValueType() == JsonValue.ValueType.OBJECT)
104 .map(entry -> {
105 try {
106 return new Source(entry.getKey(), entry.getValue().asJsonObject());
107 } catch (InvalidMapboxVectorTileException e) {
108 Logging.error(e);
109 // Reraise if not a known exception
110 if (!"TileJson not yet supported".equals(e.getMessage())) {
111 throw e;
112 }
113 }
114 return null;
115 }).filter(Objects::nonNull).collect(Collectors.toList());
116 } else {
117 sourceList = Collections.emptyList();
118 }
119 final List<Layers> layers;
120 if (jsonObject.containsKey("layers") && jsonObject.get("layers").getValueType() == JsonValue.ValueType.ARRAY) {
121 JsonArray lArray = jsonObject.getJsonArray("layers");
122 layers = lArray.stream().filter(JsonObject.class::isInstance).map(JsonObject.class::cast).map(obj -> new Layers(id, obj))
123 .collect(Collectors.toList());
124 } else {
125 layers = Collections.emptyList();
126 }
127 final Map<Optional<Source>, List<Layers>> sourceLayer = layers.stream().collect(
128 Collectors.groupingBy(layer -> sourceList.stream().filter(source -> source.getName().equals(layer.getSource()))
129 .findFirst(), LinkedHashMap::new, Collectors.toList()));
130 // Abuse HashMap null (null == default)
131 this.sources = new LinkedHashMap<>();
132 for (Map.Entry<Optional<Source>, List<Layers>> entry : sourceLayer.entrySet()) {
133 final Source source = entry.getKey().orElse(null);
134 final String data = entry.getValue().stream().map(Layers::toString).collect(Collectors.joining());
135 final String metaData = "meta{title:" + (source == null ? "Generated Style" :
136 source.getName()) + ";version:\"autogenerated\";description:\"auto generated style\";}";
137
138 // This is the default canvas
139 final String canvas = "canvas{default-points:false;default-lines:false;}";
140 final MapCSSStyleSource style = new MapCSSStyleSource(metaData + canvas + data);
141 // Save to directory
142 MainApplication.worker.execute(() -> this.save((source == null ? data.hashCode() : source.getName()) + ".mapcss", style));
143 this.sources.put(source, new ElemStyles(Collections.singleton(style)));
144 }
145 if (this.spriteUrl != null && !this.spriteUrl.trim().isEmpty()) {
146 MainApplication.worker.execute(this::fetchSprites);
147 }
148 } else {
149 throw new IllegalArgumentException(tr("Vector Tile Style Version not understood: version {0} (json: {1})",
150 this.version, jsonObject));
151 }
152 }
153
154 /**
155 * Fetch sprites. Please note that this is (literally) only png. Unfortunately.
156 * @see <a href="https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/">https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/</a>
157 */
158 private void fetchSprites() {
159 // HiDPI images first -- if this succeeds, don't bother with the lower resolution (JOSM has no method to switch)
160 try (CachedFile spriteJson = new CachedFile(this.spriteUrl + "@2x.json");
161 CachedFile spritePng = new CachedFile(this.spriteUrl + "@2x.png")) {
162 if (parseSprites(spriteJson, spritePng)) {
163 return;
164 }
165 }
166 try (CachedFile spriteJson = new CachedFile(this.spriteUrl + ".json");
167 CachedFile spritePng = new CachedFile(this.spriteUrl + ".png")) {
168 parseSprites(spriteJson, spritePng);
169 }
170 }
171
172 private boolean parseSprites(CachedFile spriteJson, CachedFile spritePng) {
173 /* JSON looks like this:
174 * { "image-name": {"width": width, "height": height, "x": x, "y": y, "pixelRatio": 1 }}
175 * width/height are the dimensions of the image
176 * x -- distance right from top left
177 * y -- distance down from top left
178 * pixelRatio -- this <i>appears</i> to be from the "@2x" (default 1)
179 * content -- [left, top corner, right, bottom corner]
180 * stretchX -- [[from, to], [from, to], ...]
181 * stretchY -- [[from, to], [from, to], ...]
182 */
183 final JsonObject spriteObject;
184 final BufferedImage spritePngImage;
185 try (BufferedReader spriteJsonBufferedReader = spriteJson.getContentReader();
186 JsonReader spriteJsonReader = Json.createReader(spriteJsonBufferedReader);
187 InputStream spritePngBufferedReader = spritePng.getInputStream()
188 ) {
189 spriteObject = spriteJsonReader.read().asJsonObject();
190 spritePngImage = ImageIO.read(spritePngBufferedReader);
191 } catch (IOException e) {
192 Logging.error(e);
193 return false;
194 }
195 for (Map.Entry<String, JsonValue> entry : spriteObject.entrySet()) {
196 final JsonObject info = entry.getValue().asJsonObject();
197 int width = info.getInt("width");
198 int height = info.getInt("height");
199 int x = info.getInt("x");
200 int y = info.getInt("y");
201 save(entry.getKey() + ".png", spritePngImage.getSubimage(x, y, width, height));
202 }
203 return true;
204 }
205
206 private void save(String name, Object object) {
207 final File cache;
208 if (object instanceof Image) {
209 // Images have a specific location where they are looked for
210 cache = new File(Config.getDirs().getUserDataDirectory(true), "images");
211 } else {
212 cache = JosmBaseDirectories.getInstance().getCacheDirectory(true);
213 }
214 final File location = new File(cache, this.name != null ? this.name : Integer.toString(this.hashCode()));
215 if ((!location.exists() && !location.mkdirs()) || (location.exists() && !location.isDirectory())) {
216 // Don't try to save if the file exists and is not a directory or we couldn't create it
217 return;
218 }
219 final File toSave = new File(location, name);
220 try (OutputStream fileOutputStream = Files.newOutputStream(toSave.toPath())) {
221 if (object instanceof String) {
222 fileOutputStream.write(((String) object).getBytes(StandardCharsets.UTF_8));
223 } else if (object instanceof MapCSSStyleSource) {
224 MapCSSStyleSource source = (MapCSSStyleSource) object;
225 try (InputStream inputStream = source.getSourceInputStream()) {
226 int byteVal = inputStream.read();
227 do {
228 fileOutputStream.write(byteVal);
229 byteVal = inputStream.read();
230 } while (byteVal > -1);
231 source.url = "file:/" + toSave.getAbsolutePath().replace('\\', '/');
232 if (source.isLoaded()) {
233 source.loadStyleSource();
234 }
235 }
236 } else if (object instanceof BufferedImage) {
237 // This directory is checked first when getting images
238 ImageIO.write((BufferedImage) object, "png", toSave);
239 }
240 } catch (IOException e) {
241 Logging.info(e);
242 }
243 }
244
245 /**
246 * Get the generated layer->style mapping
247 * @return The mapping (use to enable/disable a paint style)
248 */
249 public Map<Source, ElemStyles> getSources() {
250 return this.sources;
251 }
252
253 /**
254 * Get the sprite url for the style
255 * @return The base sprite url
256 */
257 public String getSpriteUrl() {
258 return this.spriteUrl;
259 }
260
261 @Override
262 public boolean equals(Object other) {
263 if (other != null && other.getClass() == this.getClass()) {
264 MapboxVectorStyle o = (MapboxVectorStyle) other;
265 return this.version == o.version
266 && Objects.equals(this.name, o.name)
267 && Objects.equals(this.glyphUrl, o.glyphUrl)
268 && Objects.equals(this.spriteUrl, o.spriteUrl)
269 && Objects.equals(this.sources, o.sources);
270 }
271 return false;
272 }
273
274 @Override
275 public int hashCode() {
276 return Objects.hash(this.name, this.version, this.glyphUrl, this.spriteUrl, this.sources);
277 }
278}
Note: See TracBrowser for help on using the repository browser.