/*******************************************************************************
* Copyright (c) 2015 MITRE and VoyagerSearch
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Apache License, Version 2.0 which
* accompanies this distribution and is available at
* http://www.apache.org/licenses/LICENSE-2.0.txt
******************************************************************************/
package org.locationtech.spatial4j.context;
import org.locationtech.spatial4j.distance.CartesianDistCalc;
import org.locationtech.spatial4j.distance.DistanceCalculator;
import org.locationtech.spatial4j.distance.GeodesicSphereDistCalc;
import org.locationtech.spatial4j.io.*;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.ShapeFactory;
import org.locationtech.spatial4j.shape.impl.ShapeFactoryImpl;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.*;
//import org.slf4j.LoggerFactory;
/**
* Factory for a {@link SpatialContext} based on configuration data. Call
* {@link #makeSpatialContext(java.util.Map, ClassLoader)} to construct one via String name-value
* pairs. To construct one via code then create a factory instance, set the fields, then call
* {@link #newSpatialContext()}.
* <p>
* The following keys are looked up in the args map:
* <DL>
* <DT>spatialContextFactory</DT>
* <DD>org.locationtech.spatial4j.context.SpatialContext or
* org.locationtech.spatial4j.context.jts.JtsSpatialContext</DD>
* <DT>geo</DT>
* <DD>true (default)| false -- see {@link SpatialContext#isGeo()} </DD>
* <DT>shapeFactoryClass</DT>
* <DD>Java class of the {@link ShapeFactory}.</DD>
* <DT>distCalculator</DT>
* <DD>haversine | lawOfCosines | vincentySphere | cartesian | cartesian^2
* -- see {@link DistanceCalculator}</DD>
* <DT>worldBounds</DT>
* <DD>{@code ENVELOPE(xMin, xMax, yMax, yMin)} -- see {@link SpatialContext#getWorldBounds()}</DD>
* <DT>normWrapLongitude</DT>
* <DD>true | false (default) -- see {@link SpatialContext#isNormWrapLongitude()}</DD>
* <DT>readers</DT>
* <DD>Comma separated list of {@link org.locationtech.spatial4j.io.ShapeReader} class names</DD>
* <DT>writers</DT>
* <DD>Comma separated list of {@link org.locationtech.spatial4j.io.ShapeWriter} class names</DD>
* <DT>binaryCodecClass</DT>
* <DD>Java class of the {@link org.locationtech.spatial4j.io.BinaryCodec}</DD>
* </DL>
*/
public class SpatialContextFactory {
/** Set by {@link #makeSpatialContext(java.util.Map, ClassLoader)}. */
protected Map<String, String> args;
/** Set by {@link #makeSpatialContext(java.util.Map, ClassLoader)}. */
protected ClassLoader classLoader;
/* These fields are public to make it easy to set them without bothering with setters. */
public boolean geo = true;
public DistanceCalculator distCalc;//defaults in SpatialContext c'tor based on geo
public Rectangle worldBounds;//defaults in SpatialContext c'tor based on geo
public boolean normWrapLongitude = false;
public Class<? extends ShapeFactory> shapeFactoryClass = ShapeFactoryImpl.class;
public Class<? extends BinaryCodec> binaryCodecClass = BinaryCodec.class;
public final List<Class<? extends ShapeReader>> readers = new ArrayList<Class<? extends ShapeReader>>();
public final List<Class<? extends ShapeWriter>> writers = new ArrayList<Class<? extends ShapeWriter>>();
public boolean hasFormatConfig = false;
public SpatialContextFactory() {
}
/**
* Creates a new {@link SpatialContext} based on configuration in
* <code>args</code>. See the class definition for what keys are looked up
* in it.
* The factory class is looked up via "spatialContextFactory" in args
* then falling back to a Java system property (with initial caps). If neither are specified
* then {@link SpatialContextFactory} is chosen.
*
* @param args Non-null map of name-value pairs.
* @param classLoader Optional, except when a class name is provided to an
* argument.
*/
public static SpatialContext makeSpatialContext(Map<String,String> args, ClassLoader classLoader) {
if (classLoader == null)
classLoader = SpatialContextFactory.class.getClassLoader();
SpatialContextFactory instance;
String cname = args.get("spatialContextFactory");
if (cname == null)
cname = System.getProperty("SpatialContextFactory");
if (cname == null)
instance = new SpatialContextFactory();
else {
try {
Class c = classLoader.loadClass(cname);
instance = (SpatialContextFactory) c.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
instance.init(args, classLoader);
return instance.newSpatialContext();
}
protected void init(Map<String, String> args, ClassLoader classLoader) {
this.args = args;
this.classLoader = classLoader;
initField("geo");
initField("shapeFactoryClass");
initCalculator();
//init wktParser before worldBounds because WB needs to be parsed
initFormats();
initWorldBounds();
initField("normWrapLongitude");
initField("binaryCodecClass");
}
/** Gets {@code name} from args and populates a field by the same name with the value. */
protected void initField(String name) {
// note: java.beans API is more verbose to use correctly (?) but would arguably be better
Field field;
try {
field = getClass().getField(name);
} catch (NoSuchFieldException e) {
throw new Error(e);
}
String str = args.get(name);
if (str != null) {
try {
Object o;
if (field.getType() == Boolean.TYPE) {
o = Boolean.valueOf(str);
} else if (field.getType() == Class.class) {
try {
o = classLoader.loadClass(str);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
} else if (field.getType().isEnum()) {
o = Enum.valueOf(field.getType().asSubclass(Enum.class), str);
} else {
throw new Error("unsupported field type: "+field.getType());//not plausible at runtime unless developing
}
field.set(this, o);
} catch (IllegalAccessException e) {
throw new Error(e);
} catch (Exception e) {
throw new RuntimeException(
"Invalid value '"+str+"' on field "+name+" of type "+field.getType(), e);
}
}
}
protected void initCalculator() {
String calcStr = args.get("distCalculator");
if (calcStr == null)
return;
if (calcStr.equalsIgnoreCase("haversine")) {
distCalc = new GeodesicSphereDistCalc.Haversine();
} else if (calcStr.equalsIgnoreCase("lawOfCosines")) {
distCalc = new GeodesicSphereDistCalc.LawOfCosines();
} else if (calcStr.equalsIgnoreCase("vincentySphere")) {
distCalc = new GeodesicSphereDistCalc.Vincenty();
} else if (calcStr.equalsIgnoreCase("cartesian")) {
distCalc = new CartesianDistCalc();
} else if (calcStr.equalsIgnoreCase("cartesian^2")) {
distCalc = new CartesianDistCalc(true);
} else {
throw new RuntimeException("Unknown calculator: "+calcStr);
}
}
/**
* Check args for 'readers' and 'writers'. The value should be a comma separated list
* of class names.
*
* The legacy parameter 'wktShapeParserClass' is also supported to add a specific WKT prarser
*/
protected void initFormats() {
try {
String val = args.get("readers");
if (val != null) {
for (String name : val.split(",")) {
readers.add(Class.forName(name.trim(), false, classLoader).asSubclass(ShapeReader.class));
}
} else {//deprecated; a parameter from when this was a raw class
val = args.get("wktShapeParserClass");
if (val != null) {
//LoggerFactory.getLogger(getClass()).warn("Using deprecated argument: wktShapeParserClass={}", val);
readers.add(Class.forName(val.trim(), false, classLoader).asSubclass(ShapeReader.class));
}
}
val = args.get("writers");
if (val != null) {
for (String name : val.split(",")) {
writers.add(Class.forName(name.trim(), false, classLoader).asSubclass(ShapeWriter.class));
}
}
} catch (ClassNotFoundException ex) {
throw new RuntimeException("Unable to find format class", ex);
}
}
public SupportedFormats makeFormats(SpatialContext ctx) {
checkDefaultFormats(); // easy to override
List<ShapeReader> read = new ArrayList<ShapeReader>(readers.size());
for (Class<? extends ShapeReader> clazz : readers) {
try {
read.add(makeClassInstance(clazz, ctx, this));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
List<ShapeWriter> write = new ArrayList<ShapeWriter>(writers.size());
for (Class<? extends ShapeWriter> clazz : writers) {
try {
write.add(makeClassInstance(clazz, ctx, this));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
return new SupportedFormats(
Collections.unmodifiableList(read),
Collections.unmodifiableList(write));
}
/**
* If no formats were defined in the config, this will make sure GeoJSON and WKT are registered
*/
protected void checkDefaultFormats() {
if (readers.isEmpty()) {
addReaderIfNoggitExists(GeoJSONReader.class);
readers.add(WKTReader.class);
readers.add(PolyshapeReader.class);
readers.add(LegacyShapeReader.class);
}
if (writers.isEmpty()) {
writers.add(GeoJSONWriter.class);
writers.add(WKTWriter.class);
writers.add(PolyshapeWriter.class);
writers.add(LegacyShapeWriter.class);
}
}
public void addReaderIfNoggitExists(Class<? extends ShapeReader> reader) {
try {
if (classLoader==null) {
Class.forName("org.noggit.JSONParser");
} else {
Class.forName("org.noggit.JSONParser", true, classLoader);
}
readers.add(reader);
} catch (ClassNotFoundException e) {
//LoggerFactory.getLogger(getClass()).warn("Unable to support GeoJSON Without Noggit");
}
}
protected void initWorldBounds() {
String worldBoundsStr = args.get("worldBounds");
if (worldBoundsStr == null)
return;
//kinda ugly we do this just to read a rectangle. TODO refactor
final SpatialContext ctx = newSpatialContext();
worldBounds = (Rectangle) ctx.readShape(worldBoundsStr);//TODO use readShapeFromWkt
}
/** Subclasses should simply construct the instance from the initialized configuration. */
public SpatialContext newSpatialContext() {
return new SpatialContext(this);
}
public ShapeFactory makeShapeFactory(SpatialContext ctx) {
return makeClassInstance(shapeFactoryClass, ctx, this);
}
public BinaryCodec makeBinaryCodec(SpatialContext ctx) {
return makeClassInstance(binaryCodecClass, ctx, this);
}
@SuppressWarnings("unchecked")
private <T> T makeClassInstance(Class<? extends T> clazz, Object... ctorArgs) {
try {
Constructor<?> empty = null;
//can't simply lookup constructor by arg type because might be subclass type
ctorLoop: for (Constructor<?> ctor : clazz.getConstructors()) {
Class[] parameterTypes = ctor.getParameterTypes();
if (parameterTypes.length == 0) {
empty = ctor; // the empty constructor;
}
if (parameterTypes.length != ctorArgs.length)
continue;
for (int i = 0; i < ctorArgs.length; i++) {
Object ctorArg = ctorArgs[i];
if (!parameterTypes[i].isAssignableFrom(ctorArg.getClass()))
continue ctorLoop;
}
return clazz.cast(ctor.newInstance(ctorArgs));
}
// If an empty constructor exists, use that
if (empty != null) {
return clazz.cast(empty.newInstance());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
throw new RuntimeException(clazz + " needs a constructor that takes: "
+ Arrays.toString(ctorArgs));
}
}