View Javadoc
1   /*******************************************************************************
2    * Copyright (c) 2015 MITRE and VoyagerSearch
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Apache License, Version 2.0 which
5    * accompanies this distribution and is available at
6    *    http://www.apache.org/licenses/LICENSE-2.0.txt
7    ******************************************************************************/
8   
9   package org.locationtech.spatial4j.context;
10  
11  import org.locationtech.spatial4j.distance.CartesianDistCalc;
12  import org.locationtech.spatial4j.distance.DistanceCalculator;
13  import org.locationtech.spatial4j.distance.GeodesicSphereDistCalc;
14  import org.locationtech.spatial4j.io.*;
15  import org.locationtech.spatial4j.shape.Rectangle;
16  import org.locationtech.spatial4j.shape.ShapeFactory;
17  import org.locationtech.spatial4j.shape.impl.ShapeFactoryImpl;
18  
19  import java.lang.reflect.Constructor;
20  import java.lang.reflect.Field;
21  import java.util.*;
22  
23  //import org.slf4j.LoggerFactory;
24  
25  /**
26   * Factory for a {@link SpatialContext} based on configuration data.  Call
27   * {@link #makeSpatialContext(java.util.Map, ClassLoader)} to construct one via String name-value
28   * pairs. To construct one via code then create a factory instance, set the fields, then call
29   * {@link #newSpatialContext()}.
30   * <p>
31   * The following keys are looked up in the args map:
32   * <DL>
33   * <DT>spatialContextFactory</DT>
34   * <DD>org.locationtech.spatial4j.context.SpatialContext or
35   * org.locationtech.spatial4j.context.jts.JtsSpatialContext</DD>
36   * <DT>geo</DT>
37   * <DD>true (default)| false -- see {@link SpatialContext#isGeo()} </DD>
38   * <DT>shapeFactoryClass</DT>
39   * <DD>Java class of the {@link ShapeFactory}.</DD>
40   * <DT>distCalculator</DT>
41   * <DD>haversine | lawOfCosines | vincentySphere | cartesian | cartesian^2
42   * -- see {@link DistanceCalculator}</DD>
43   * <DT>worldBounds</DT>
44   * <DD>{@code ENVELOPE(xMin, xMax, yMax, yMin)} -- see {@link SpatialContext#getWorldBounds()}</DD>
45   * <DT>normWrapLongitude</DT>
46   * <DD>true | false (default) -- see {@link SpatialContext#isNormWrapLongitude()}</DD>
47   * <DT>readers</DT>
48   * <DD>Comma separated list of {@link org.locationtech.spatial4j.io.ShapeReader} class names</DD>
49   * <DT>writers</DT>
50   * <DD>Comma separated list of {@link org.locationtech.spatial4j.io.ShapeWriter} class names</DD>
51   * <DT>binaryCodecClass</DT>
52   * <DD>Java class of the {@link org.locationtech.spatial4j.io.BinaryCodec}</DD>
53   * </DL>
54   */
55  public class SpatialContextFactory {
56  
57    /** Set by {@link #makeSpatialContext(java.util.Map, ClassLoader)}. */
58    protected Map<String, String> args;
59    /** Set by {@link #makeSpatialContext(java.util.Map, ClassLoader)}. */
60    protected ClassLoader classLoader;
61  
62    /* These fields are public to make it easy to set them without bothering with setters. */
63  
64    public boolean geo = true;
65    public DistanceCalculator distCalc;//defaults in SpatialContext c'tor based on geo
66    public Rectangle worldBounds;//defaults in SpatialContext c'tor based on geo
67  
68    public boolean normWrapLongitude = false;
69  
70    public Class<? extends ShapeFactory> shapeFactoryClass = ShapeFactoryImpl.class;
71    public Class<? extends BinaryCodec> binaryCodecClass = BinaryCodec.class;
72    public final List<Class<? extends ShapeReader>> readers = new ArrayList<>();
73    public final List<Class<? extends ShapeWriter>> writers = new ArrayList<>();
74    public boolean hasFormatConfig = false;
75  
76    public SpatialContextFactory() {
77    }
78  
79    /**
80     * Creates a new {@link SpatialContext} based on configuration in
81     * <code>args</code>.  See the class definition for what keys are looked up
82     * in it.
83     * The factory class is looked up via "spatialContextFactory" in args
84     * then falling back to a Java system property (with initial caps). If neither are specified
85     * then {@link SpatialContextFactory} is chosen.
86     *
87     * @param args Non-null map of name-value pairs.
88     * @param classLoader Optional, except when a class name is provided to an
89     *                    argument.
90     */
91    public static SpatialContext makeSpatialContext(Map<String,String> args, ClassLoader classLoader) {
92      if (classLoader == null)
93        classLoader = SpatialContextFactory.class.getClassLoader();
94      SpatialContextFactory instance;
95      String cname = args.get("spatialContextFactory");
96      if (cname == null)
97        cname = System.getProperty("SpatialContextFactory");
98      if (cname == null)
99        instance = new SpatialContextFactory();
100     else {
101       try {
102         Class<?> c = classLoader.loadClass(cname);
103         instance = (SpatialContextFactory) c.newInstance();
104       } catch (Exception e) {
105         throw new RuntimeException(e);
106       }
107     }
108     instance.init(args, classLoader);
109     return instance.newSpatialContext();
110   }
111 
112   protected void init(Map<String, String> args, ClassLoader classLoader) {
113     this.args = args;
114     this.classLoader = classLoader;
115 
116     initField("geo");
117 
118     initField("shapeFactoryClass");
119 
120     initCalculator();
121 
122     //init wktParser before worldBounds because WB needs to be parsed
123     initFormats();
124     initWorldBounds();
125 
126     initField("normWrapLongitude");
127 
128     initField("binaryCodecClass");
129   }
130 
131   /** Gets {@code name} from args and populates a field by the same name with the value. */
132   @SuppressWarnings("unchecked")
133   protected void initField(String name) {
134     //  note: java.beans API is more verbose to use correctly (?) but would arguably be better
135     Field field;
136     try {
137       field = getClass().getField(name);
138     } catch (NoSuchFieldException e) {
139       throw new Error(e);
140     }
141     String str = args.get(name);
142     if (str != null) {
143       try {
144         Object o;
145         if (field.getType() == Boolean.TYPE) {
146           o = Boolean.valueOf(str);
147         } else if (field.getType() == Class.class) {
148           try {
149             o = classLoader.loadClass(str);
150           } catch (ClassNotFoundException e) {
151             throw new RuntimeException(e);
152           }
153         } else if (field.getType().isEnum()) {
154           o = Enum.valueOf(field.getType().asSubclass(Enum.class), str);
155         } else {
156           throw new Error("unsupported field type: "+field.getType());//not plausible at runtime unless developing
157         }
158         field.set(this, o);
159       } catch (IllegalAccessException e) {
160         throw new Error(e);
161       } catch (Exception e) {
162         throw new RuntimeException(
163             "Invalid value '"+str+"' on field "+name+" of type "+field.getType(), e);
164       }
165     }
166   }
167 
168   protected void initCalculator() {
169     String calcStr = args.get("distCalculator");
170     if (calcStr == null)
171       return;
172     if (calcStr.equalsIgnoreCase("haversine")) {
173       distCalc = new GeodesicSphereDistCalc.Haversine();
174     } else if (calcStr.equalsIgnoreCase("lawOfCosines")) {
175       distCalc = new GeodesicSphereDistCalc.LawOfCosines();
176     } else if (calcStr.equalsIgnoreCase("vincentySphere")) {
177       distCalc = new GeodesicSphereDistCalc.Vincenty();
178     } else if (calcStr.equalsIgnoreCase("cartesian")) {
179       distCalc = new CartesianDistCalc();
180     } else if (calcStr.equalsIgnoreCase("cartesian^2")) {
181       distCalc = new CartesianDistCalc(true);
182     } else {
183       throw new RuntimeException("Unknown calculator: "+calcStr);
184     }
185   }
186 
187   /**
188    * Check args for 'readers' and 'writers'.  The value should be a comma separated list
189    * of class names.
190    * 
191    * The legacy parameter 'wktShapeParserClass' is also supported to add a specific WKT prarser
192    */
193   protected void initFormats() {
194     try {
195       String val = args.get("readers");
196       if (val != null) {
197         for (String name : val.split(",")) {
198           readers.add(Class.forName(name.trim(), false, classLoader).asSubclass(ShapeReader.class));
199         }
200       } else {//deprecated; a parameter from when this was a raw class
201         val = args.get("wktShapeParserClass");
202         if (val != null) {
203           //LoggerFactory.getLogger(getClass()).warn("Using deprecated argument: wktShapeParserClass={}", val);
204           readers.add(Class.forName(val.trim(), false, classLoader).asSubclass(ShapeReader.class));
205         }
206       }
207       val = args.get("writers");
208       if (val != null) {
209         for (String name : val.split(",")) {
210           writers.add(Class.forName(name.trim(), false, classLoader).asSubclass(ShapeWriter.class));
211         }
212       }
213     } catch (ClassNotFoundException ex) {
214       throw new RuntimeException("Unable to find format class", ex);
215     }
216   }
217   
218 
219   public SupportedFormats makeFormats(SpatialContext ctx) {
220     checkDefaultFormats();  // easy to override
221     
222     List<ShapeReader> read = new ArrayList<>(readers.size());
223     for (Class<? extends ShapeReader> clazz : readers) {
224       try {
225         read.add(makeClassInstance(clazz, ctx, this));
226       } catch (Exception ex) {
227         throw new RuntimeException(ex);
228       }
229     }
230     
231     List<ShapeWriter> write = new ArrayList<>(writers.size());
232     for (Class<? extends ShapeWriter> clazz : writers) {
233       try {
234         write.add(makeClassInstance(clazz, ctx, this));
235       } catch (Exception ex) {
236         throw new RuntimeException(ex);
237       }
238     }
239     
240     return new SupportedFormats(
241         Collections.unmodifiableList(read), 
242         Collections.unmodifiableList(write));
243   }
244 
245   /**
246    * If no formats were defined in the config, this will make sure GeoJSON and WKT are registered
247    */
248   protected void checkDefaultFormats() {
249     if (readers.isEmpty()) {
250       addReaderIfNoggitExists(GeoJSONReader.class);
251       readers.add(WKTReader.class);
252       readers.add(PolyshapeReader.class);
253       readers.add(LegacyShapeReader.class);
254     }
255     if (writers.isEmpty()) {
256       writers.add(GeoJSONWriter.class);
257       writers.add(WKTWriter.class);
258       writers.add(PolyshapeWriter.class);
259       writers.add(LegacyShapeWriter.class);
260     }
261   }
262 
263   public void addReaderIfNoggitExists(Class<? extends ShapeReader> reader) {
264     try {
265       if (classLoader==null) {
266         Class.forName("org.noggit.JSONParser");
267       } else {
268         Class.forName("org.noggit.JSONParser", true, classLoader);
269       }
270       readers.add(reader);
271     } catch (ClassNotFoundException e) {
272       //LoggerFactory.getLogger(getClass()).warn("Unable to support GeoJSON Without Noggit");
273     }
274   }
275 
276   protected void initWorldBounds() {
277     String worldBoundsStr = args.get("worldBounds");
278     if (worldBoundsStr == null)
279       return;
280 
281     //kinda ugly we do this just to read a rectangle.  TODO refactor
282     final SpatialContext ctx = newSpatialContext();
283     worldBounds = (Rectangle) ctx.readShape(worldBoundsStr);//TODO use readShapeFromWkt
284   }
285 
286   /** Subclasses should simply construct the instance from the initialized configuration. */
287   public SpatialContext newSpatialContext() {
288     return new SpatialContext(this);
289   }
290 
291   public ShapeFactory makeShapeFactory(SpatialContext ctx) {
292     return makeClassInstance(shapeFactoryClass, ctx, this);
293   }
294 
295   public BinaryCodec makeBinaryCodec(SpatialContext ctx) {
296     return makeClassInstance(binaryCodecClass, ctx, this);
297   }
298 
299   private <T> T makeClassInstance(Class<? extends T> clazz, Object... ctorArgs) {
300     try {
301       Constructor<?> empty = null;
302 
303       //can't simply lookup constructor by arg type because might be subclass type
304       ctorLoop: for (Constructor<?> ctor : clazz.getConstructors()) {
305         Class<?>[] parameterTypes = ctor.getParameterTypes();
306         if (parameterTypes.length == 0) {
307           empty = ctor; // the empty constructor;
308         }
309         if (parameterTypes.length != ctorArgs.length)
310           continue;
311         for (int i = 0; i < ctorArgs.length; i++) {
312           Object ctorArg = ctorArgs[i];
313           if (!parameterTypes[i].isAssignableFrom(ctorArg.getClass()))
314             continue ctorLoop;
315         }
316         return clazz.cast(ctor.newInstance(ctorArgs));
317       }
318 
319       // If an empty constructor exists, use that
320       if (empty != null) {
321         return clazz.cast(empty.newInstance());
322       }
323     } catch (Exception e) {
324       throw new RuntimeException(e);
325     }
326     throw new RuntimeException(clazz + " needs a constructor that takes: "
327         + Arrays.toString(ctorArgs));
328   }
329 
330 }