/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2017, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.mbstyle.parse; import org.geotools.mbstyle.layer.MBLayer; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; import java.util.*; import static org.geotools.mbstyle.function.ZoomLevelFunction.EPSG_3857_O_SCALE; /** * This class provides the ability to find all the zoom levels within a MapBox Style and returns a reduced list * of only the layers and properties containing Base and Stops values. * * @author David Vick (Boundless) */ public class MBObjectStops { JSONParser parser = new JSONParser(); List<Long> layerZoomLevels; public boolean hasStops = false; public List<Long> stops = new ArrayList<>(); public List<MBLayer> layersForStop = new ArrayList<>(); public List<long[]> ranges = new ArrayList<>(); /** * Data structure for pre-processing a MBLayer determining whether the layer contains property and zoom functions * and if so, getting the distinct stops for each, building a list of MBLayers (one for each stop) and setting * ranges that are used to set min/max scale denominators for each MBLayer. * @param layer */ public MBObjectStops(MBLayer layer) { try { if (layer.getPaint() != null) { hasStops = getStops(layer.getPaint()); } if (layer.getLayout() != null && !hasStops) { hasStops = getStops(layer.getLayout()); } if (hasStops) { stops = getStopLevels(layer); layersForStop = getLayerStyleForStops(layer, stops); ranges = getStopLevelRanges(stops); } } catch (ParseException e) { System.out.println(e.getLocalizedMessage()); } } /** * Gets the current stop of the layer. This would be the bottom of the range i.e 0 for {0, 20} * @param layer * @return */ public long getCurrentStop(MBLayer layer) { long stop = getStop(layer); return stop; } /** * Finds all the stops within the layer and returns a sorted distinct list * @param mbLayer * @return */ List<Long> getStopLevels(MBLayer mbLayer) { Set<Long> distinctValues = new HashSet<>(); List<Long> zoomLevels = new ArrayList<>(); layerZoomLevels = new ArrayList<>(); if (mbLayer.getPaint() != null) { findStopLevels(mbLayer.getPaint(), layerZoomLevels); } if (mbLayer.getLayout() != null) { findStopLevels(mbLayer.getLayout(), layerZoomLevels); } distinctValues.addAll(layerZoomLevels); zoomLevels.addAll(distinctValues); Collections.sort(zoomLevels); return zoomLevels; } /** * This method creates a copy of the incoming layer to be used for creating the unique layer for each stop. * @param layer * @param layerStops * @return * @throws ParseException */ List<MBLayer> getLayerStyleForStops(MBLayer layer, List<Long> layerStops) throws ParseException { List<MBLayer> layers = new ArrayList<>(); for (int i = 0; i < layerStops.size(); i ++) { JSONObject obj = (JSONObject) parser.parse(layer.getJson().toJSONString()); MBLayer workingLayer = MBLayer.create(obj); Long maxZoom = layerStops.get(layerStops.size() - 1); Long current = layerStops.get(i); long[] range = {0, 0}; if (current < maxZoom) { range[0] = current; range[1] = layerStops.get(i + 1); } else if ( current == maxZoom) { range[0] = current; range[1] = maxZoom; } layers.add(createLayerStopStyle(workingLayer, range)); } return layers; } /** * Accepts the list of stops for a layer and builds a list of ranges for each stop. * @param stops * @return */ List<long[]> getStopLevelRanges(List<Long> stops) { List<long[]> ranges = new ArrayList<>(); for (int i = 0; i < stops.size(); i++) { Long maxZoom = stops.get(stops.size() - 1); Long current = stops.get(i); long[] range = {0, 0}; if (current < maxZoom) { range[0] = current; range[1] = stops.get(i + 1); } else if (current == maxZoom) { range[0] = current; range[1] = -1; } ranges.add(range); } return ranges; } boolean getStops(JSONObject jsonObject) { return containsStops(jsonObject); } /** * Finds the distinct range for the current stop. * @param stop * @param ranges * @return */ public long[] getRangeForStop(Long stop, List<long[]> ranges) { long[] rangeForStopLevel = {0,0}; for (int i = 0; i < ranges.size(); i++) { if (ranges.get(i)[0] == stop) { rangeForStopLevel = ranges.get(i); } } return rangeForStopLevel; } /** * Take a web mercator zoom level, and return the equivalent scale denominator (at the equator). * * Converting to a scale denominator at the equator is consistent with the conversion elsewhere in GeoTools, e.g., in the GeoTools YSLD * ZoomContextFinder. * * @param zoomLevel The zoom level * @return The equivalent scale denominator (at the equator) */ public static Double zoomLevelToScaleDenominator(Long zoomLevel) { return EPSG_3857_O_SCALE / Math.pow(2, zoomLevel); } public long getStop(MBLayer layer) { return stop(layer); } boolean containsStops(JSONObject jsonObject) { Boolean hasStops = false; Set<?> keySet = jsonObject.keySet(); Iterator<?> keys = keySet.iterator(); while (keys.hasNext()) { String key = (String) keys.next(); if (jsonObject.get(key) instanceof JSONObject) { JSONObject child = (JSONObject) jsonObject.get(key); if (child.containsKey("stops") && ((JSONArray)((JSONArray)child.get("stops")).get(0)).get(0) instanceof JSONObject) { hasStops = true; } } } return hasStops; } /** * Accepts a distinct MBLayer and finds the stop for this layer. * @param layer * @return */ long stop (MBLayer layer) { long s = 0; if (layer.getPaint() != null) { s = findStop(layer.getPaint(), s); } if (layer.getLayout() != null) { s = findStop(layer.getLayout(), s); } return s; } /** * This method reduces the Layer to just what is needed for the given range. * @param layer * @param range * @return */ MBLayer createLayerStopStyle(MBLayer layer, long[] range) { if (layer.getPaint() != null ) { reduceJsonForRange(layer.getPaint(), range); } if (layer.getLayout() != null) { reduceJsonForRange(layer.getLayout(), range); } return layer; } /** * Iterates over the JSONObject looking for the stops for the given stop. * @param jsonObject * @param layerStop * @return */ long findStop(JSONObject jsonObject, long layerStop) { Set<?> keySet = jsonObject.keySet(); Iterator<?> keys = keySet.iterator(); while (keys.hasNext()) { String key = (String)keys.next(); if (jsonObject.get(key) instanceof JSONObject) { JSONObject child = (JSONObject) jsonObject.get(key); if (child.containsKey("stops")) { JSONArray stops = (JSONArray) child.get("stops"); for (int i = 0; i < stops.size(); i++) { JSONArray stop = (JSONArray) stops.get(i); if (stop.get(0) instanceof Long) { layerStop = (Long)stop.get(0); } } } } } return layerStop; } /** * Reduces the JSON to just the required values for the given range. * @param jsonObject * @param range * @return */ JSONObject reduceJsonForRange(JSONObject jsonObject, long[] range) { Set<?> keySet = jsonObject.keySet(); Iterator<?> keys = keySet.iterator(); List<String> keyToRemove = new ArrayList<>(); while (keys.hasNext()) { String key = (String)keys.next(); if (jsonObject.get(key) instanceof JSONObject) { JSONObject child = (JSONObject) jsonObject.get(key); if (child.containsKey("stops")) { List<JSONArray> objectsToEdit = new ArrayList<>(); List<Object> objectsToRemove = new ArrayList<>(); JSONArray stops = (JSONArray) child.get("stops"); for (int i = 0; i < stops.size(); i++) { JSONArray stop = (JSONArray) stops.get(i); if (stop.get(0) instanceof JSONObject) { if (((Long)((JSONObject) stop.get(0)).get("zoom")).longValue() == range[0]) { objectsToEdit.add((JSONArray) stops.get(i)); } else { objectsToRemove.add(stops.get(i)); } } } for (Object o : objectsToRemove) { stops.remove(o); } for (JSONArray o : objectsToEdit) { JSONArray stopsArray = new JSONArray(); stopsArray.add(0, ((JSONObject) o.get(0)).get("value")); stopsArray.add(1, o.get(1)); stops.remove(o); stops.add(stopsArray); } } if (((JSONArray)child.get("stops")).size() == 0) { keyToRemove.add(key); } } } for (String key : keyToRemove) { jsonObject.remove(key); } return jsonObject; } /** * Iterates over the JSONObject finding the stops and adding them to the list. * @param jsonObject * @param layerZoomLevels * @return */ List<Long> findStopLevels(JSONObject jsonObject, List<Long> layerZoomLevels) { Set<?> keySet = jsonObject.keySet(); Iterator<?> keys = keySet.iterator(); while (keys.hasNext()) { String key = (String)keys.next(); if (jsonObject.get(key) instanceof JSONObject) { JSONObject child = (JSONObject) jsonObject.get(key); if (child.containsKey("stops")) { JSONArray stops = (JSONArray) child.get("stops"); for (int i = 0; i < stops.size(); i++) { JSONArray stop = (JSONArray) stops.get(i); if (stop.get(0) instanceof Long) { layerZoomLevels.add(((Long)stop.get(0))); } if (stop.get(0) instanceof JSONObject) { layerZoomLevels.add((Long)((JSONObject) stop.get(0)).get("zoom")); } } } } } return layerZoomLevels; } }