code: /*******************************************************************************
   * 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<>();
    public final List<Class<? extends ShapeWriter>> writers = new ArrayList<>();
    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. */
    @SuppressWarnings("unchecked")
    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<>(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<>(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);
    }
  
    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));
    }
  
  }
  