/*
* Copyright (c) 2014 Oculus Info Inc. http://www.oculusinfo.com/
*
* Released under the MIT License.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.oculusinfo.binning.metadata;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.oculusinfo.binning.TilePyramid;
import com.oculusinfo.binning.impl.AOITilePyramid;
import com.oculusinfo.binning.impl.WebMercatorTilePyramid;
import com.oculusinfo.binning.util.JsonUtilities;
import com.oculusinfo.factory.util.Pair;
/**
* This is a wrapper around a JSON object containing a layer's metadata. It is
* intended to encapsulate, properly type, and simplify access to the metadata.
*
* This version is intended to be immutable. The user is capable of retrieving
* the raw JSON and altering it, of course, but they are explicitly not supposed
* to.
*
* @author nkronenfeld
*/
public class PyramidMetaData {
private static final Logger LOGGER = Logger.getLogger(PyramidMetaData.class.getName());
private JSONObject _metaData;
public PyramidMetaData (JSONObject metaData) throws JSONException {
_metaData = metaData;
PyramidMetaDataVersionMutator.updateMetaData(_metaData,
PyramidMetaDataVersionMutator.CURRENT_VERSION);
}
public PyramidMetaData (String metaData) throws JSONException {
_metaData = new JSONObject(metaData);
PyramidMetaDataVersionMutator.updateMetaData(_metaData,
PyramidMetaDataVersionMutator.CURRENT_VERSION);
}
public PyramidMetaData (String name,
String description,
Integer tileSizeX,
Integer tileSizeY,
String scheme,
String projection,
List<Integer> zoomLevels,
Rectangle2D bounds,
Collection<Pair<Integer, String>> levelMins,
Collection<Pair<Integer, String>> levelMaxes)
throws JSONException
{
_metaData = new JSONObject();
_metaData.put("version", PyramidMetaDataVersionMutator.CURRENT_VERSION);
if (null != name)
_metaData.put("name", name);
if (null != description)
_metaData.put("description", description);
if (null != tileSizeX)
_metaData.put("tilesizex", tileSizeX);
if (null != tileSizeY)
_metaData.put("tilesizey", tileSizeY);
if (null != scheme)
_metaData.put("scheme", scheme);
if (null != projection)
_metaData.put("projection", projection);
if (null != zoomLevels)
_metaData.put("zoomlevels", zoomLevels);
if (null != bounds) {
JSONArray metaDataBounds = new JSONArray();
metaDataBounds.put(bounds.getMinX());
metaDataBounds.put(bounds.getMinY());
metaDataBounds.put(bounds.getMaxX());
metaDataBounds.put(bounds.getMaxY());
_metaData.put("bounds", metaDataBounds);
}
if (null != levelMins || null != levelMaxes) {
JSONObject metaMeta = new JSONObject();
_metaData.put("meta", metaMeta);
if (null != levelMins) {
JSONObject levelMinMap = new JSONObject();
metaMeta.put("levelMinimums", levelMinMap);
for (Pair<Integer, String> entry: levelMins) {
levelMinMap.put(""+entry.getFirst(), entry.getSecond());
}
}
if (null != levelMaxes) {
JSONObject levelMaxMap = new JSONObject();
metaMeta.put("levelMaximums", levelMaxMap);
for (Pair<Integer, String> entry: levelMaxes) {
levelMaxMap.put(""+entry.getFirst(), entry.getSecond());
}
}
}
}
/**
* Sometimes one just needs access to the raw json object - such as when one
* is constructing another such object. This method provides said access.
*
* @return The raw JSON object describing the metadata of our layer.
*/
public JSONObject getRawData () {
return _metaData;
}
/**
* Get the user-readable name of the tile pyramid described by this metadata
* object.
*/
public String getName () {
return _metaData.optString("name", "unknown");
}
/**
* Get the user-readable description of the tile pyramid described by this
* metadata object.
*/
public String getDescription () {
return _metaData.optString("description", "unknown");
}
/**
* Get the size of tiles in the tile pyramid described by this metadata
* object, in bins. This is deprecated in favor of {@link #getTileSizeX()}
* and {@link #getTileSizeY()}.
*/
@Deprecated
public int getTileSize () {
return _metaData.optInt("tilesize", 256);
}
/**
* Get the horizontal size of tiles in the tile pyramid described by this
* metadata object, in bins.
*/
public int getTileSizeX () {
return _metaData.optInt("tilesizex", _metaData.optInt("tilesize", 256));
}
/**
* Get the vertical size of tiles in the tile pyramid described by this
* metadata object, in bins.
*/
public int getTileSizeY () {
return _metaData.optInt("tilesizey", _metaData.optInt("tilesize", 256));
}
/**
* Get the tile index scheem used for the tile pyramid described by this
* metadata object. At the moment, the only (and default) supported value is
* "TMS".
*/
public String getScheme () {
return _metaData.optString("scheme", "TMS");
}
/**
* Get the name of the projection used for the tile pyramid described by
* this metadata object.
*/
public String getProjection () {
return _metaData.optString("projection", "unprojected");
}
/**
* Get the minimum zoom level supported by the tile pyramid described by
* this metadata object.
*/
public int getMinZoom () {
int minZoom = 0;
JSONArray levels = _metaData.optJSONArray("zoomlevels");
if (null != levels && levels.length() > 0) {
try {
minZoom = levels.getInt(0);
} catch (JSONException e) {
LOGGER.log(Level.WARNING, "Bad minimum zoom level in metadata", e);
}
}
return minZoom;
}
/**
* Get the maximum zoom level supported by the tile pyramid described by
* this metadata object.
*/
public int getMaxZoom () {
int maxZoom = 0;
JSONArray levels = _metaData.optJSONArray("zoomlevels");
if (null != levels && levels.length() > 0) {
try {
maxZoom = levels.getInt(levels.length()-1);
} catch (JSONException e) {
LOGGER.log(Level.WARNING, "Bad maximum zoom level in metadata", e);
}
}
return maxZoom;
}
/**
* Get all valid zoom levels in the tile pyramid described by this metadata
* object.
*/
public List<Integer> getValidZoomLevels () {
List<Integer> levels = new ArrayList<>();
JSONArray storedLevels = _metaData.optJSONArray("zoomlevels");
if (null != storedLevels) {
for (int i=0; i<storedLevels.length(); ++i) {
try {
levels.add(storedLevels.getInt(i));
} catch (JSONException e) {
LOGGER.log(Level.WARNING, "Bad zoom level in metadata: "+i, e);
}
}
}
return levels;
}
/**
* Adds levels to the list of valid zoom levels in the tile pyramid
* described by this metadata object.
*/
public void addValidZoomLevels (Collection<Integer> newLevels) throws JSONException {
List<Integer> currentLevels = getValidZoomLevels();
Set<Integer> allLevels = new HashSet<>(currentLevels);
allLevels.addAll(newLevels);
List<Integer> sortedLevels = new ArrayList<>(allLevels);
Collections.sort(sortedLevels);
_metaData.put("zoomlevels", sortedLevels);
}
/**
* Get a list of all levels described by the metadata of the given tile
* pyramid.
*
* @return A list of levels. This should never be null, but may be empty if
* an error is encountered getting the level information.
*/
/*
public List<Integer> getLevels () {
int minZoom = getMinZoom();
int maxZoom = getMaxZoom();
List<Integer> levels = new ArrayList<>();
for (int i=minZoom; i<=maxZoom; ++i) levels.add(i);
return levels;
}
*/
/**
* Get the bounds of the area covered by the tile pyramid described by this
* metadata object, in raw data coordinates.
*/
public Rectangle2D getBounds () {
JSONArray metaDataBounds = _metaData.optJSONArray("bounds");
if (null == metaDataBounds) return null;
if (metaDataBounds.length()<4) return null;
double minx = metaDataBounds.optDouble(0);
double miny = metaDataBounds.optDouble(1);
double maxx = metaDataBounds.optDouble(2);
double maxy = metaDataBounds.optDouble(3);
return new Rectangle2D.Double(minx, miny, maxx-minx, maxy-miny);
}
/**
* Get the tiling projection used to create the tile pyramid described by
* this metadata object.
*/
public TilePyramid getTilePyramid () {
try {
String projection = _metaData.getString("projection");
if ("EPSG:4326".equals(projection)) {
JSONArray bounds = _metaData.getJSONArray("bounds");
double xMin = bounds.getDouble(0);
double yMin = bounds.getDouble(1);
double xMax = bounds.getDouble(2);
double yMax = bounds.getDouble(3);
return new AOITilePyramid(xMin, yMin, xMax, yMax);
} else if ("EPSG:900913".equals(projection) || "EPSG:3857".equals(projection)) {
return new WebMercatorTilePyramid();
}
} catch (JSONException e) {
LOGGER.log(Level.WARNING, "Bad projection data in tile pyramid", e);
}
return null;
}
public String getCustomMetaData (String... path) {
JSONObject metaInfo = _metaData.optJSONObject("meta");
if (null == metaInfo) return null;
int index = 0;
while (null != metaInfo && index < path.length-1) {
metaInfo = metaInfo.optJSONObject(path[index]);
index++;
}
if (null == metaInfo) return null;
Object result = metaInfo.opt(path[path.length-1]);
if (null == result) return null;
return result.toString();
}
public void setCustomMetaData(Object value, String... path) throws JSONException {
if (!_metaData.has("meta"))
_metaData.put("meta", new JSONObject());
JSONObject metaInfo = _metaData.getJSONObject("meta");
int index = 0;
while (index < path.length-1) {
if (!metaInfo.has(path[index]))
metaInfo.put(path[index], new JSONObject());
metaInfo = metaInfo.getJSONObject(path[index]);
index++;
}
metaInfo.put(path[path.length-1], value);
}
public void setCustomMetaData (JSONObject metadata) throws JSONException {
if (!_metaData.has("meta"))
_metaData.put("meta", new JSONObject());
JSONObject metaInfo = _metaData.getJSONObject("meta");
JsonUtilities.overlayInPlace(metaInfo, JsonUtilities.expandKeysInPlace(metadata));
}
public Map<String, Object> getAllCustomMetaData () {
Object metaInfo = _metaData.opt("meta");
if (null == metaInfo)
return null;
Map<String, Object> customMetaData = new HashMap<String, Object>();
if (metaInfo instanceof JSONObject) {
addFieldsToMap("", (JSONObject) metaInfo, customMetaData);
} else if (metaInfo instanceof JSONArray) {
addFieldsToMap("", (JSONArray) metaInfo, customMetaData);
}
return customMetaData;
}
private void addFieldsToMap (String prefix, JSONObject info,
Map<String, Object> map) {
String[] keys = JSONObject.getNames(info);
for (String key: keys) {
Object value = info.opt(key);
if (value instanceof JSONObject) {
addFieldsToMap(prefix+key+".", (JSONObject) value, map);
} else if (value instanceof JSONArray) {
addFieldsToMap(prefix+key+".", (JSONArray) value, map);
} else if (null != value) {
map.put(prefix+key, value);
}
}
}
private void addFieldsToMap (String prefix, JSONArray info,
Map<String, Object> map) {
int length = info.length();
for (int i=0; i<length; ++i) {
Object value = info.opt(i);
if (value instanceof JSONObject) {
addFieldsToMap(prefix+i+".", (JSONObject) value, map);
} else if (value instanceof JSONArray) {
addFieldsToMap(prefix+i+".", (JSONArray) value, map);
} else if (null != value) {
map.put(prefix+i, value);
}
}
}
//
// private JSONObject getLevelExtremaObject (boolean max) {
// JSONObject metaInfo;
// try {
// metaInfo = _metaData.getJSONObject("meta");
// } catch (JSONException e) {
// LOGGER.log(Level.WARNING,
// "Missing meta object in pyramid metadata.");
// return null;
// }
//
// JSONObject extrema;
// try {
// if (max) extrema = metaInfo.getJSONObject("levelMaximums");
// else extrema = metaInfo.getJSONObject("levelMinimums");
// } catch (JSONException e1) {
// try {
// // Some older tiles use this instead - included for backwards
// // compatibility. No real users should ever reach this.
// if (max) extrema = metaInfo.getJSONObject("levelMaxFreq");
// else extrema = metaInfo.getJSONObject("levelMinFreq");
// } catch (JSONException e2) {
// LOGGER.log(Level.WARNING,
// "Missing level bounds in pyramid metadata.");
// return null;
// }
// }
// return extrema;
// }
//
// private Map<Integer, String> getLevelExtrema (boolean max) {
// JSONObject extrema = getLevelExtremaObject(max);
//
// Map<Integer, String> byLevel = new HashMap<Integer, String>();
// if (null != extrema) {
// for (Iterator<?> i = extrema.keys(); i.hasNext();) {
// Object rawKey = i.next();
// try {
// int key = Integer.parseInt(rawKey.toString());
// String value = extrema.getString(rawKey.toString());
// byLevel.put(key, value);
// } catch (NumberFormatException e) {
// LOGGER.log(Level.WARNING, "Unparsable level "+rawKey+".");
// } catch (JSONException e) {
// LOGGER.log(Level.WARNING, "Error reading level maximum value for level "+rawKey+".");
// }
// }
// }
// return byLevel;
// }
//
// public String getLevelExtremum (boolean max, int level) {
// JSONObject extrema = getLevelExtremaObject(max);
//
// if (null != extrema) {
// for (Iterator<?> i = extrema.keys(); i.hasNext();) {
// Object rawKey = i.next();
// try {
// int key = Integer.parseInt(rawKey.toString());
// if (key == level) {
// return extrema.getString(rawKey.toString());
// }
// } catch (NumberFormatException e) {
// LOGGER.log(Level.WARNING, "Unparsable level " + rawKey
// + ".");
// } catch (JSONException e) {
// LOGGER.log(Level.WARNING,
// "Error reading level maximum value for level "
// + rawKey + ".");
// }
// }
// }
// return null;
// }
//
// /**
// * Get a map, indexed by level, of the maximum value for each level
// *
// * @return A map from level to maximum value of that level. May be empty,
// * but should never be null.
// */
//
// public Map<Integer, String> getLevelMaximums () {
// return getLevelExtrema(true);
// }
//
// /**
// * Get a map, indexed by level, of the minimum value for each level
// *
// * @return A map from level to minimum value of that level. May be empty,
// * but should never be null.
// */
//
// public Map<Integer, String> getLevelMinimums () {
// return getLevelExtrema(false);
// }
//
// /**
// * Get the maximum value of a given level
// *
// * @param level The level of interest
// *
// * @return The maximum value of that level, or null if no maximum is
// * properly specified for that level.
// */
//
// public String getLevelMaximum (int level) {
// return getLevelExtremum(true, level);
// }
//
// /**
// * Get the minimum value of a given level
// *
// * @param level The level of interest
// *
// * @return The minimum value of that level, or null if no maximum is
// * properly specified for that level.
// */
//
// public String getLevelMinimum (int level) {
// return getLevelExtremum(false, level);
// }
@Override
public String toString () {
return _metaData.toString();
}
}