/*
* 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.MapboxTestUtils;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.ParseException;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.hamcrest.Matchers;
import java.awt.Color;
import java.io.IOException;
import java.util.*;
import static org.geotools.mbstyle.parse.MBStyleTestUtils.categories;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;
public class FunctionParseTest {
Map<String, JSONObject> testLayersById = new HashMap<>();
@Before
public void setUp() throws IOException, ParseException {
JSONObject jsonObject = MapboxTestUtils.parseTestStyle("functionParseTest.json");
JSONArray layers = (JSONArray) jsonObject.get("layers");
for (Object o : layers) {
JSONObject layer = (JSONObject) o;
testLayersById.put((String) layer.get("id"), layer);
}
}
// ---- ZOOM FUNCTIONS ---------------------------------------------------------
/**
* Verify that a linear color zoom function can be parsed correctly.
*/
@Test
public void testParseLinearColorFunction() throws IOException, ParseException {
JSONObject layer = testLayersById.get("function1");
Optional<JSONObject> o = traverse(layer, JSONObject.class, "paint", "line-color");
JSONObject j = o.get();
MBFunction fn = new MBFunction(j);
// The function type is not specified in the JSON, so getType should return null.
assertNull(fn.getType());
// But the default fallback function type for color returns is 'exponential':
assertEquals(MBFunction.FunctionType.EXPONENTIAL, fn.getTypeWithDefault(Color.class));
assertEquals(EnumSet.of(MBFunction.FunctionCategory.ZOOM), fn.category());
assertEquals(1, fn.getBase().intValue());
assertNull(fn.getProperty());
assertNotNull(fn.getStops());
assertEquals(2, fn.getStops().size());
}
/**
* Verify that an exponential (color) zoom function can be parsed correctly.
*/
@Test
public void testParseExpColorFunction() throws IOException, ParseException {
JSONObject layer = testLayersById.get("function2");
Optional<JSONObject> o = traverse(layer, JSONObject.class, "paint", "line-color");
JSONObject j = o.get();
MBFunction fn = new MBFunction(j);
// The function type is not specified in the JSON, so getType should return null.
assertNull(fn.getType());
// But the default fallback function type for color returns is 'exponential':
assertEquals(MBFunction.FunctionType.EXPONENTIAL, fn.getTypeWithDefault(Color.class));
assertEquals(EnumSet.of(MBFunction.FunctionCategory.ZOOM), fn.category());
assertEquals(1.75, fn.getBase().doubleValue(), .00001);
assertNull(fn.getProperty());
assertNotNull(fn.getStops());
assertEquals(3, fn.getStops().size());
}
/**
* Verify that an exponential (number) zoom function can be parsed correctly.
*/
@Test
public void testParseExpRadiusFunction() throws IOException, ParseException {
JSONObject layer = testLayersById.get("function3");
Optional<JSONObject> o = traverse(layer, JSONObject.class, "paint", "circle-radius");
JSONObject j = o.get();
MBFunction fn = new MBFunction(j);
// The function type is not specified in the JSON, so getType should return null.
assertNull(fn.getType());
// But the default fallback function type for number returns is 'exponential':
assertEquals(MBFunction.FunctionType.EXPONENTIAL, fn.getTypeWithDefault(Number.class));
assertEquals(EnumSet.of(MBFunction.FunctionCategory.ZOOM), fn.category());
assertEquals(1.5, fn.getBase().doubleValue(), .00001);
assertNull(fn.getProperty());
assertNotNull(fn.getStops());
assertEquals(3, fn.getStops().size());
}
// ---- PROPERTY AND ZOOM FUNCTIONS ---------------------------------------------------------
/**
* Verify that a linear zoom-and-property function can be parsed correctly.
*/
@Test
public void testParsePropertyZoomFunction() throws IOException, ParseException {
JSONObject layer = testLayersById.get("function4");
Optional<JSONObject> o = traverse(layer, JSONObject.class, "paint", "circle-radius");
JSONObject j = o.get();
MBFunction fn = new MBFunction(j);
// The function type is not specified in the JSON, so getType should return null.
assertNull(fn.getType());
// But the default fallback function type for number returns is 'exponential':
assertEquals(MBFunction.FunctionType.EXPONENTIAL, fn.getTypeWithDefault(Number.class));
assertEquals(
EnumSet.of(MBFunction.FunctionCategory.PROPERTY, MBFunction.FunctionCategory.ZOOM),
fn.category());
assertEquals("rating", fn.getProperty());
assertNotNull(fn.getStops());
assertEquals(4, fn.getStops().size());
}
// ---- PROPERTY FUNCTIONS ---------------------------------------------------------
/**
* Verify that a linear property function can be parsed correctly.
*/
@Test
public void testParsePropertyFunction() throws IOException, ParseException {
JSONObject layer = testLayersById.get("function5");
Optional<JSONObject> o = traverse(layer, JSONObject.class, "paint", "circle-color");
JSONObject j = o.get();
MBFunction fn = new MBFunction(j);
// The function type is not specified in the JSON, so getType should return null.
assertNull(fn.getType());
// But the default fallback function type for color returns is 'exponential':
assertEquals(MBFunction.FunctionType.EXPONENTIAL, fn.getTypeWithDefault(Color.class));
assertEquals(EnumSet.of(MBFunction.FunctionCategory.PROPERTY), fn.category());
assertEquals("temperature", fn.getProperty());
assertNotNull(fn.getStops());
assertEquals(2, fn.getStops().size());
}
/**
* Verify that a categorical property functions can be parsed correctly.
*/
@Test
public void testParseCategoricalFunction() throws IOException, ParseException {
JSONObject layer = testLayersById.get("function6");
Optional<JSONObject> o = traverse(layer, JSONObject.class, "paint", "circle-color");
JSONObject j = o.get();
MBFunction fn = new MBFunction(j);
assertEquals(MBFunction.FunctionType.CATEGORICAL, fn.getType());
assertEquals(
EnumSet.of(MBFunction.FunctionCategory.PROPERTY),
fn.category());
assertEquals("color", fn.getProperty());
assertNotNull(fn.getStops());
assertEquals(5, fn.getStops().size());
}
/**
* Verify that an interval function for colours can be parsed.
*/
@Test
public void testParseIntervalFunctionColour() throws IOException, ParseException {
JSONObject layer = testLayersById.get("functionIntervalColour");
JSONObject j = traverse(layer, JSONObject.class, "paint", "circle-color").get();
MBFunction fn = new MBFunction(j);
assertThat(fn, hasProperty("type", is(MBFunction.FunctionType.INTERVAL)));
assertThat(fn, categories(containsInAnyOrder(MBFunction.FunctionCategory.PROPERTY)));
assertThat(fn, hasProperty("property", equalTo("temperature")));
assertThat(fn, hasProperty("stops", contains(
contains(-273.15, "#4488FF"),
contains(10.0, "#00FF00"),
contains(30.0, "#FF8800")
)));
}
/**
* Verify that an interval function for number can be parsed.
*/
@Test
public void testParseIntervalFunctionNumber() throws IOException, ParseException {
JSONObject layer = testLayersById.get("functionIntervalNumeric");
JSONObject j = traverse(layer, JSONObject.class, "paint", "circle-radius").get();
MBFunction fn = new MBFunction(j);
assertThat(fn, hasProperty("type", is(MBFunction.FunctionType.INTERVAL)));
assertThat(fn, categories(containsInAnyOrder(MBFunction.FunctionCategory.PROPERTY)));
assertThat(fn, hasProperty("property", equalTo("rating")));
assertThat(fn, hasProperty("stops", contains(
contains(0L, 0L),
contains(2L, 5L),
contains(5L, 10L),
contains(10L, 20L)
)));
}
/**
* Traverse a nested map using the array of strings, and cast the result to the provided class, or return {@link Optional#empty()}.
* @param map
* @param clazz expected type
* @param path used to access map
* @return result at the provided path, or {@link Optional#empty()}.
*/
private <T> Optional<T> traverse(JSONObject map, Class<T> clazz,
String... path) {
if (path == null || path.length == 0) {
return Optional.empty();
}
Object value = map;
for (String key : path) {
if (value instanceof JSONObject) {
JSONObject m = (JSONObject) value;
if (m.containsKey(key)) {
value = m.get(key);
} else {
return Optional.empty();
}
} else {
return Optional.empty();
}
}
return Optional.of(clazz.cast(value));
}
}