You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ry...@apache.org on 2016/03/16 18:14:55 UTC

lucene-solr:master: SOLR-8814: Support GeoJSON response format

Repository: lucene-solr
Updated Branches:
  refs/heads/master 24830b7f1 -> 36145d02c


SOLR-8814: Support GeoJSON response format


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/36145d02
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/36145d02
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/36145d02

Branch: refs/heads/master
Commit: 36145d02ccc838f50538a8b9d6ff9c68f3ccce22
Parents: 24830b7
Author: Ryan McKinley <ry...@apache.org>
Authored: Wed Mar 16 09:49:46 2016 -0700
Committer: Ryan McKinley <ry...@apache.org>
Committed: Wed Mar 16 10:06:49 2016 -0700

----------------------------------------------------------------------
 solr/CHANGES.txt                                |  12 +
 .../src/java/org/apache/solr/core/SolrCore.java |   3 +-
 .../solr/response/GeoJSONResponseWriter.java    | 345 +++++++++++++++++++
 .../solr/response/JSONResponseWriter.java       |   2 +-
 .../transform/GeoTransformerFactory.java        | 224 ++++++++++++
 .../response/transform/TransformerFactory.java  |   1 +
 .../response/transform/WriteableGeoJSON.java    |  55 +++
 .../solr/schema/AbstractSpatialFieldType.java   |   7 +
 .../solr/collection1/conf/schema-spatial.xml    |   1 +
 .../response/TestGeoJSONResponseWriter.java     | 279 +++++++++++++++
 10 files changed, 927 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 691e87f..c48032e 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -34,6 +34,18 @@ Detailed Change List
 New Features
 ----------------------
 
+* SOLR-8814: Support GeoJSON response writer and general spatial formatting.  Adding
+     &wt=geojson&geojson.field=<your geometry field>
+  Will return a FeatureCollection for each SolrDocumentList and a Feature with the
+  requested geometry for each SolrDocument.  The requested geometry field needs
+  to either extend AbstractSpatialFieldType or store a GeoJSON string.  This also adds
+  a [geo] DocumentTransformer that can return the Shape in a variety of formats:
+    &fl=[geo f=<your geometry field> w=(GeoJSON|WKT|POLY)]
+  The default format is GeoJSON.  For information on the supported formats, see:
+  https://github.com/locationtech/spatial4j/blob/master/FORMATS.md
+  To return the FeatureCollection as the root element, add '&omitHeader=true" (ryan)
+
+
 Bug Fixes
 ----------------------
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/core/src/java/org/apache/solr/core/SolrCore.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java b/solr/core/src/java/org/apache/solr/core/SolrCore.java
index 7a65a72..c5e54d2 100644
--- a/solr/core/src/java/org/apache/solr/core/SolrCore.java
+++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java
@@ -2123,10 +2123,11 @@ public final class SolrCore implements SolrInfoMBean, Closeable {
   private final PluginBag<QueryResponseWriter> responseWriters = new PluginBag<>(QueryResponseWriter.class, this);
   public static final Map<String ,QueryResponseWriter> DEFAULT_RESPONSE_WRITERS ;
   static{
-    HashMap<String, QueryResponseWriter> m= new HashMap<>(14, 1);
+    HashMap<String, QueryResponseWriter> m= new HashMap<>(15, 1);
     m.put("xml", new XMLResponseWriter());
     m.put("standard", m.get("xml"));
     m.put(CommonParams.JSON, new JSONResponseWriter());
+    m.put("geojson", new GeoJSONResponseWriter());
     m.put("python", new PythonResponseWriter());
     m.put("php", new PHPResponseWriter());
     m.put("phps", new PHPSerializedResponseWriter());

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java b/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java
new file mode 100644
index 0000000..896be92
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/GeoJSONResponseWriter.java
@@ -0,0 +1,345 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.solr.response;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.lucene.index.IndexableField;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.transform.GeoTransformerFactory;
+import org.apache.solr.response.transform.WriteableGeoJSON;
+import org.apache.solr.schema.AbstractSpatialFieldType;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.ReturnFields;
+import org.locationtech.spatial4j.context.SpatialContext;
+import org.locationtech.spatial4j.io.ShapeWriter;
+import org.locationtech.spatial4j.io.SupportedFormats;
+import org.locationtech.spatial4j.shape.Shape;
+
+/**
+ * Extend the standard JSONResponseWriter to support GeoJSON.  This writes
+ * a {@link SolrDocumentList} with a 'FeatureCollection', following the
+ * specification in <a href="http://geojson.org/">geojson.org</a>
+ */
+public class GeoJSONResponseWriter extends JSONResponseWriter {
+  
+  public static final String FIELD = "geojson.field";
+  
+  @Override
+  public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
+    
+    String geofield = req.getParams().get(FIELD, null);
+    if(geofield==null || geofield.length()==0) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "GeoJSON.  Missing parameter: '"+FIELD+"'");
+    }
+    
+    SchemaField sf = req.getSchema().getFieldOrNull(geofield);
+    if(sf==null) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "GeoJSON.  Unknown field: '"+FIELD+"'="+geofield);
+    }
+    
+    SupportedFormats formats = null;
+    if(sf.getType() instanceof AbstractSpatialFieldType) {
+      SpatialContext ctx = ((AbstractSpatialFieldType)sf.getType()).getSpatialContext();
+      formats = ctx.getFormats();
+    }
+
+    JSONWriter w = new GeoJSONWriter(writer, req, rsp, 
+        geofield,
+        formats); 
+    
+    try {
+      w.writeResponse();
+    } finally {
+      w.close();
+    }
+  }
+}
+
+class GeoJSONWriter extends JSONWriter {
+  
+  final SupportedFormats formats;
+  final ShapeWriter geowriter;
+  final String geofield;
+  
+  public GeoJSONWriter(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp, 
+      String geofield, SupportedFormats formats) {
+    super(writer, req, rsp);
+    this.geofield = geofield;
+    this.formats = formats;
+    if(formats==null) {
+      this.geowriter = null;
+    }
+    else {
+      this.geowriter = formats.getGeoJsonWriter();
+    }
+  }
+
+  @Override
+  public void writeResponse() throws IOException {
+    if(req.getParams().getBool(CommonParams.OMIT_HEADER, false)) {
+      if(wrapperFunction!=null) {
+          writer.write(wrapperFunction + "(");
+      }
+      rsp.removeResponseHeader();
+
+      NamedList<Object> vals = rsp.getValues();
+      Object response = vals.remove("response");
+      if(vals.size()==0) {
+        writeVal(null, response);
+      }
+      else {
+        throw new SolrException(ErrorCode.BAD_REQUEST, 
+            "GeoJSON with "+CommonParams.OMIT_HEADER +
+            " can not return more than a result set");
+      }
+      
+      if(wrapperFunction!=null) {
+        writer.write(')');
+      }
+      writer.write('\n');  // ending with a newline looks much better from the command line
+    }
+    else {
+      super.writeResponse();
+    }
+  }
+  
+  @Override
+  public void writeSolrDocument(String name, SolrDocument doc, ReturnFields returnFields, int idx) throws IOException {
+    if( idx > 0 ) {
+      writeArraySeparator();
+    }
+
+    indent();
+    writeMapOpener(-1); 
+    incLevel();
+
+    writeKey("type", false);
+    writeVal(null, "Feature");
+    
+    Object val = doc.getFieldValue(geofield);
+    if(val != null) {  
+      writeFeatureGeometry(val);
+    }
+    
+    boolean first=true;
+    for (String fname : doc.getFieldNames()) {
+      if (fname.equals(geofield) || ((returnFields!= null && !returnFields.wantsField(fname)))) {
+        continue;
+      }
+      writeMapSeparator();
+      if (first) {
+        indent();
+        writeKey("properties", false);
+        writeMapOpener(-1); 
+        incLevel();
+        
+        first=false;
+      }
+
+      indent();
+      writeKey(fname, true);
+      val = doc.getFieldValue(fname);
+
+      // SolrDocument will now have multiValued fields represented as a Collection,
+      // even if only a single value is returned for this document.
+      if (val instanceof List) {
+        // shortcut this common case instead of going through writeVal again
+        writeArray(name,((Iterable)val).iterator());
+      } else {
+        writeVal(fname, val);
+      }
+    }
+
+    // GeoJSON does not really support nested FeatureCollections
+    if(doc.hasChildDocuments()) {
+      if(first == false) {
+        writeMapSeparator();
+        indent();
+      }
+      writeKey("_childDocuments_", true);
+      writeArrayOpener(doc.getChildDocumentCount());
+      List<SolrDocument> childDocs = doc.getChildDocuments();
+      for(int i=0; i<childDocs.size(); i++) {
+        writeSolrDocument(null, childDocs.get(i), null, i);
+      }
+      writeArrayCloser();
+    }
+
+    // check that we added any properties
+    if(!first) {
+      decLevel();
+      writeMapCloser();
+    }
+    
+    decLevel();
+    writeMapCloser();
+  }
+
+  protected void writeFeatureGeometry(Object geo) throws IOException 
+  {
+    // Support multi-valued geometries
+    if(geo instanceof Iterable) {
+      Iterator iter = ((Iterable)geo).iterator();
+      if(!iter.hasNext()) {
+        return; // empty list
+      }
+      else {
+        geo = iter.next();
+        
+        // More than value
+        if(iter.hasNext()) {
+          writeMapSeparator();
+          indent();
+          writeKey("geometry", false);
+          incLevel();
+
+          // TODO: in the future, we can be smart and try to make this the appropriate MULTI* value
+          // if all the values are the same
+          // { "type": "GeometryCollection",
+          //    "geometries": [
+          writeMapOpener(-1); 
+          writeKey("type",false);
+          writeStr(null, "GeometryCollection", false);
+          writeMapSeparator();
+          writeKey("geometries", false);
+          writeArrayOpener(-1); // no trivial way to determine array size
+          incLevel();
+          
+          // The first one
+          indent();
+          writeGeo(geo);
+          while(iter.hasNext()) {
+            // Each element in the array
+            writeArraySeparator();
+            indent();
+            writeGeo(iter.next());
+          }
+          
+          decLevel();
+          writeArrayCloser();
+          writeMapCloser();
+          
+          decLevel();
+          return;
+        }
+      }
+    }
+    
+    // Single Value
+    if(geo!=null) {
+      writeMapSeparator();
+      indent();
+      writeKey("geometry", false);
+      writeGeo(geo);
+    }
+  }
+
+  protected void writeGeo(Object geo) throws IOException {
+    Shape shape = null;
+    String str = null;
+    if(geo instanceof Shape) {
+      shape = (Shape)geo;
+    }
+    else if(geo instanceof IndexableField) {
+      str = ((IndexableField)geo).stringValue();
+    }
+    else if(geo instanceof WriteableGeoJSON) {
+      shape = ((WriteableGeoJSON)geo).shape;
+    }
+    else {
+      str = geo.toString();
+    }
+    
+    if(str !=null) {
+      // Assume it is well formed JSON
+      if(str.startsWith("{") && str.endsWith("}")) {
+        writer.write(str);
+        return;
+      }
+      
+      if(formats==null) {
+        // The check is here and not in the constructor because we do not know if the
+        // *stored* values for the field look like JSON until we actually try to read them
+        throw new SolrException(ErrorCode.BAD_REQUEST, 
+            "GeoJSON unable to write field: '&"+ GeoJSONResponseWriter.FIELD +"="+geofield+"' ("+str+")");
+      }
+      shape = formats.read(str); 
+    }
+    
+    if(geowriter==null) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, 
+          "GeoJSON unable to write field: '&"+ GeoJSONResponseWriter.FIELD +"="+geofield+"'");
+    }
+    
+    if(shape!=null) {
+      geowriter.write(writer, shape);
+    }
+  }
+
+  @Override
+  public void writeStartDocumentList(String name, 
+      long start, int size, long numFound, Float maxScore) throws IOException
+  {
+    writeMapOpener((maxScore==null) ? 3 : 4);
+    incLevel();
+    writeKey("type",false);
+    writeStr(null, "FeatureCollection", false);
+    writeMapSeparator();
+    writeKey("numFound",false);
+    writeLong(null,numFound);
+    writeMapSeparator();
+    writeKey("start",false);
+    writeLong(null,start);
+
+    if (maxScore!=null) {
+      writeMapSeparator();
+      writeKey("maxScore",false);
+      writeFloat(null,maxScore);
+    }
+    writeMapSeparator();
+    
+    // if can we get bbox of all results, we should write it here
+    
+    // indent();
+    writeKey("features",false);
+    writeArrayOpener(size);
+
+    incLevel();
+  }
+
+  @Override
+  public void writeEndDocumentList() throws IOException
+  {
+    decLevel();
+    writeArrayCloser();
+
+    decLevel();
+    indent();
+    writeMapCloser();
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
index cf894b8..d257f57 100644
--- a/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
+++ b/solr/core/src/java/org/apache/solr/response/JSONResponseWriter.java
@@ -70,8 +70,8 @@ public class JSONResponseWriter implements QueryResponseWriter {
 }
 
 class JSONWriter extends TextResponseWriter {
+  protected String wrapperFunction;
   private String namedListStyle;
-  private String wrapperFunction;
 
   private static final String JSON_NL_STYLE="json.nl";
   private static final String JSON_NL_MAP="map";

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java b/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
new file mode 100644
index 0000000..7b7974b
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/transform/GeoTransformerFactory.java
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.response.transform;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.spatial.SpatialStrategy;
+import org.apache.lucene.spatial.composite.CompositeSpatialStrategy;
+import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
+import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.response.JSONResponseWriter;
+import org.apache.solr.response.QueryResponseWriter;
+import org.apache.solr.schema.AbstractSpatialFieldType;
+import org.apache.solr.schema.SchemaField;
+import org.apache.solr.search.QParser;
+import org.apache.solr.search.SyntaxError;
+import org.locationtech.spatial4j.io.GeoJSONWriter;
+import org.locationtech.spatial4j.io.ShapeWriter;
+import org.locationtech.spatial4j.io.SupportedFormats;
+import org.locationtech.spatial4j.shape.Shape;
+
+
+/**
+ * This DocumentTransformer will write a {@link Shape} to the SolrDocument using
+ * the requested format.  Supported formats include:
+ * <ul>
+ *  <li>GeoJSON</li>
+ *  <li>WKT</li>
+ *  <li>Polyshape</li>
+ * </ul>
+ * For more information see: <a href="https://github.com/locationtech/spatial4j/blob/master/FORMATS.md">spatial4j/FORMATS.md</a>
+ * 
+ * The shape is either read from a stored field, or a ValueSource.
+ * 
+ * This transformer is useful when:
+ * <ul>
+ *  <li>You want to return a format different than the stored encoding (WKT vs GeoJSON)</li>
+ *  <li>The {@link Shape} is stored in a {@link ValueSource}, not a stored field</li>
+ *  <li>the value is not stored in a format the output understands (ie, raw GeoJSON)</li>
+ * </ul>
+ * 
+ */
+public class GeoTransformerFactory extends TransformerFactory
+{ 
+  @Override
+  public DocTransformer create(String display, SolrParams params, SolrQueryRequest req) {
+
+    String fname = params.get("f", display);
+    if(fname.startsWith("[") && fname.endsWith("]")) {
+      fname = display.substring(1,display.length()-1);
+    }
+    SchemaField sf = req.getSchema().getFieldOrNull(fname);
+    if(sf==null) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, 
+          this.getClass().getSimpleName() +" using unknown field: "+fname);
+    }
+    if(!(sf.getType() instanceof AbstractSpatialFieldType)) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, 
+          "GeoTransformer requested non-spatial field: "+fname + " ("+sf.getType().getClass().getSimpleName()+")");
+    }
+
+    final GeoFieldUpdater updater = new GeoFieldUpdater();
+    updater.field = fname;
+    updater.display = display;
+    updater.display_error = display+"_error"; 
+        
+    ValueSource shapes = null;
+    AbstractSpatialFieldType<?> sdv = (AbstractSpatialFieldType<?>)sf.getType();
+    SpatialStrategy strategy = sdv.getStrategy(fname);
+    if(strategy instanceof CompositeSpatialStrategy) {
+      shapes = ((CompositeSpatialStrategy)strategy)
+          .getGeometryStrategy().makeShapeValueSource();
+    }
+    else if(strategy instanceof SerializedDVStrategy) {
+      shapes = ((SerializedDVStrategy)strategy)
+          .makeShapeValueSource();
+    }
+    
+    
+    String writerName = params.get("w", "GeoJSON");
+    updater.formats = strategy.getSpatialContext().getFormats();
+    updater.writer = updater.formats.getWriter(writerName);
+    if(updater.writer==null) {
+      StringBuilder str = new StringBuilder();
+      str.append( "Unknown Spatial Writer: " ).append(writerName);
+      str.append(" [");
+      for(ShapeWriter w : updater.formats.getWriters()) {
+        str.append(w.getFormatName()).append(' ');
+      }
+      str.append("]");
+      throw new SolrException(ErrorCode.BAD_REQUEST, str.toString());
+    }
+    
+    QueryResponseWriter qw = req.getCore().getQueryResponseWriter(req);
+    updater.isJSON =
+        (qw.getClass() == JSONResponseWriter.class) &&
+        (updater.writer instanceof GeoJSONWriter);
+
+
+    // Using ValueSource
+    if(shapes!=null) {
+      // we don't really need the qparser... just so we can reuse valueSource
+      QParser parser = new QParser(null,null,params, req) {
+        @Override
+        public Query parse() throws SyntaxError {
+          return new MatchAllDocsQuery();
+        }
+      }; 
+
+      return new ValueSourceAugmenter(display, parser, shapes) {
+        @Override
+        protected void setValue(SolrDocument doc, Object val) {
+          updater.setValue(doc, val);
+        }
+      };
+    }
+    
+    // Using the raw stored values
+    return new DocTransformer() {
+      
+      @Override
+      public void transform(SolrDocument doc, int docid, float score) throws IOException {
+        Object val = doc.remove(updater.field);
+        if(val!=null) {
+          updater.setValue(doc, val);
+        }
+      }
+      
+      @Override
+      public String getName() {
+        return updater.display;
+      }
+
+      @Override
+      public String[] getExtraRequestFields() {
+        return new String[] {updater.field};
+      }
+    };
+  }
+}
+
+class GeoFieldUpdater {
+  String field;
+  String display;
+  String display_error;
+  
+  boolean isJSON;
+  ShapeWriter writer;
+  SupportedFormats formats;
+  
+  void addShape(SolrDocument doc, Shape shape) {
+    if(isJSON) {
+      doc.addField(display, new WriteableGeoJSON(shape, writer));
+    }
+    else {
+      doc.addField(display, writer.toString(shape));
+    }
+  }
+  
+  void setValue(SolrDocument doc, Object val) {
+    doc.remove(display);
+    if(val != null) {
+      if(val instanceof Iterable) {
+        Iterator iter = ((Iterable)val).iterator();
+        while(iter.hasNext()) {
+          addValue(doc, iter.next());
+        }
+      }
+      else {
+        addValue(doc, val);
+      }
+    }
+  }
+    
+  void addValue(SolrDocument doc, Object val) {
+    if(val == null) {
+      return;
+    }
+    
+    if(val instanceof Shape) {
+      addShape(doc, (Shape)val);
+    }
+    // Don't explode on 'InvalidShpae'
+    else if( val instanceof Exception) {
+      doc.setField( display_error, ((Exception)val).toString() );
+    }
+    else {
+      // Use the stored value
+      if(val instanceof IndexableField) {
+        val = ((IndexableField)val).stringValue();
+      }
+      try {
+        addShape(doc, formats.read(val.toString()));
+      }
+      catch(Exception ex) {
+        doc.setField( display_error, ex.toString() );
+      }
+    }
+  }
+}
+

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java b/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
index a600adf..6e7a3dd 100644
--- a/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
+++ b/solr/core/src/java/org/apache/solr/response/transform/TransformerFactory.java
@@ -49,5 +49,6 @@ public abstract class TransformerFactory implements NamedListInitializedPlugin
     defaultFactories.put( "child", new ChildDocTransformerFactory() );
     defaultFactories.put( "json", new RawValueTransformerFactory("json") );
     defaultFactories.put( "xml", new RawValueTransformerFactory("xml") );
+    defaultFactories.put( "geo", new GeoTransformerFactory() );
   }
 }

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java b/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java
new file mode 100644
index 0000000..40acebf
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/response/transform/WriteableGeoJSON.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.response.transform;
+
+import java.io.IOException;
+
+import org.apache.solr.common.util.JavaBinCodec;
+import org.apache.solr.response.TextResponseWriter;
+import org.apache.solr.response.WriteableValue;
+import org.locationtech.spatial4j.io.ShapeWriter;
+import org.locationtech.spatial4j.shape.Shape;
+
+/**
+ * This will let the writer add values to the response directly
+ */
+public class WriteableGeoJSON extends WriteableValue {
+
+  public final Shape shape;
+  public final ShapeWriter jsonWriter;
+  
+  public WriteableGeoJSON(Shape shape, ShapeWriter jsonWriter) {
+    this.shape = shape;
+    this.jsonWriter = jsonWriter;
+  }
+
+  @Override
+  public Object resolve(Object o, JavaBinCodec codec) throws IOException {
+    codec.writeStr(jsonWriter.toString(shape));
+    return null; // this means we wrote it
+  }
+
+  @Override
+  public void write(String name, TextResponseWriter writer) throws IOException {
+    jsonWriter.write(writer.getWriter(), shape);
+  }
+
+  @Override
+  public String toString() {
+    return jsonWriter.toString(shape);
+  }
+}

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java b/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java
index 222f0b8..e5fd8c6 100644
--- a/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java
+++ b/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java
@@ -390,6 +390,13 @@ public abstract class AbstractSpatialFieldType<T extends SpatialStrategy> extend
     }
   }
 
+  /**
+   * @return The Spatial Context for this field type
+   */
+  public SpatialContext getSpatialContext() {
+    return ctx;
+  }
+
   @Override
   public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
     writer.writeStr(name, f.stringValue(), true);

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml
----------------------------------------------------------------------
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml b/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml
index 2c1ca1f..15837f3 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml
@@ -70,6 +70,7 @@
     <field name="bbox" type="bbox" />
 
     <dynamicField name="bboxD_*" type="bbox" indexed="true" />
+    <dynamicField name="str_*" type="string" indexed="true" stored="true"/>
 
   </fields>
 

http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/36145d02/solr/core/src/test/org/apache/solr/response/TestGeoJSONResponseWriter.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/response/TestGeoJSONResponseWriter.java b/solr/core/src/test/org/apache/solr/response/TestGeoJSONResponseWriter.java
new file mode 100644
index 0000000..191136b
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/response/TestGeoJSONResponseWriter.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.response;
+
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.locationtech.spatial4j.context.SpatialContext;
+import org.locationtech.spatial4j.io.SupportedFormats;
+import org.locationtech.spatial4j.shape.Shape;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class TestGeoJSONResponseWriter extends SolrTestCaseJ4 {
+  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+  final ObjectMapper jsonmapper = new ObjectMapper();
+  
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig-basic.xml","schema-spatial.xml");
+    createIndex();
+  }
+
+  public static void createIndex() {
+
+
+//    <field name="srpt_geohash" type="srpt_geohash" multiValued="true" />
+//    <field name="" type="srpt_quad" multiValued="true" />
+//    <field name="" type="srpt_packedquad" multiValued="true" />
+//    <field name="" type="stqpt_geohash" multiValued="true" />
+    
+    // multiple valued field
+    assertU(adoc("id","H.A", "srpt_geohash","POINT( 1 2 )"));
+    assertU(adoc("id","H.B", "srpt_geohash","POINT( 1 2 )", 
+                             "srpt_geohash","POINT( 3 4 )"));
+    assertU(adoc("id","H.C", "srpt_geohash","LINESTRING (30 10, 10 30, 40 40)"));
+
+    assertU(adoc("id","Q.A", "srpt_quad","POINT( 1 2 )"));
+    assertU(adoc("id","Q.B", "srpt_quad","POINT( 1 2 )", 
+                             "srpt_quad","POINT( 3 4 )"));
+    assertU(adoc("id","Q.C", "srpt_quad","LINESTRING (30 10, 10 30, 40 40)"));
+
+    assertU(adoc("id","P.A", "srpt_packedquad","POINT( 1 2 )"));
+    assertU(adoc("id","P.B", "srpt_packedquad","POINT( 1 2 )", 
+                             "srpt_packedquad","POINT( 3 4 )"));
+    assertU(adoc("id","P.C", "srpt_packedquad","LINESTRING (30 10, 10 30, 40 40)"));
+
+    
+    // single valued field
+    assertU(adoc("id","R.A", "srptgeom","POINT( 1 2 )"));
+
+    // non-spatial field
+    assertU(adoc("id","S.X", "str_shape","POINT( 1 2 )"));
+    assertU(adoc("id","S.A", "str_shape","{\"type\":\"Point\",\"coordinates\":[1,2]}"));
+    
+
+    assertU(commit());
+  }
+
+  protected Map<String,Object> readJSON(String json) {
+    try {
+      return jsonmapper.readValue(json, Map.class);
+    }
+    catch(Exception ex) {
+      log.warn("Unable to read GeoJSON From: {}", json);
+      log.warn("Error", ex);
+      fail("Unable to parse JSON GeoJSON Response");
+    }
+    return null; 
+  }
+  
+  protected Map<String,Object> getFirstFeatureGeometry(Map<String,Object> json)
+  {
+    Map<String,Object> rsp = (Map<String,Object>)json.get("response");
+    assertEquals("FeatureCollection", rsp.get("type"));
+    List<Object> vals = (List<Object>)rsp.get("features");
+    assertEquals(1, vals.size());
+    Map<String,Object> feature = (Map<String,Object>)vals.get(0);
+    assertEquals("Feature", feature.get("type"));
+    return (Map<String,Object>)feature.get("geometry");
+  }
+
+  @Test
+  public void testRequestExceptions() throws Exception {
+    
+    // Make sure we select the field
+    try {
+      h.query(req(
+        "q","*:*", 
+        "wt","geojson", 
+        "fl","*"));
+      fail("should Require a parameter to select the field");
+    }
+    catch(SolrException ex) {}
+    
+
+    // non-spatial fields *must* be stored as JSON
+    try {
+      h.query(req(
+        "q","id:S.X", 
+        "wt","geojson", 
+        "fl","*",
+        "geojson.field", "str_shape"));
+      fail("should complain about bad shape config");
+    }
+    catch(SolrException ex) {}
+    
+  }
+
+  @Test
+  public void testGeoJSONAtRoot() throws Exception {
+    
+    // Try reading the whole resposne
+    String json = h.query(req(
+        "q","*:*", 
+        "wt","geojson", 
+        "rows","2", 
+        "fl","*", 
+        "geojson.field", "stqpt_geohash",
+        "indent","true"));
+    
+    // Check that we have a normal solr response with 'responseHeader' and 'response'
+    Map<String,Object> rsp = readJSON(json);
+    assertNotNull(rsp.get("responseHeader"));
+    assertNotNull(rsp.get("response"));
+    
+    json = h.query(req(
+        "q","*:*", 
+        "wt","geojson", 
+        "rows","2", 
+        "fl","*", 
+        "omitHeader", "true",
+        "geojson.field", "stqpt_geohash",
+        "indent","true"));
+    
+    // Check that we have a normal solr response with 'responseHeader' and 'response'
+    rsp = readJSON(json);
+    assertNull(rsp.get("responseHeader"));
+    assertNull(rsp.get("response"));
+    assertEquals("FeatureCollection", rsp.get("type"));
+    assertNotNull(rsp.get("features"));
+  }
+  
+  @Test
+  public void testGeoJSONOutput() throws Exception {
+    
+    // Try reading the whole resposne
+    readJSON(h.query(req(
+        "q","*:*", 
+        "wt","geojson", 
+        "fl","*", 
+        "geojson.field", "stqpt_geohash",
+        "indent","true")));
+    
+    // Multivalued Valued Point
+    Map<String,Object> json = readJSON(h.query(req(
+        "q","id:H.B", 
+        "wt","geojson", 
+        "fl","*", 
+        "geojson.field", "srpt_geohash",
+        "indent","true")));
+    
+    Map<String,Object> geo = getFirstFeatureGeometry(json);
+    assertEquals( // NOTE: not actual JSON, it is Map.toString()!
+        "{type=GeometryCollection, geometries=["
+        + "{type=Point, coordinates=[1, 2]}, "
+        + "{type=Point, coordinates=[3, 4]}]}", ""+geo);  
+    
+    
+    // Check the same value encoded on different field types
+    String[][] check = new String[][] {
+      { "id:H.A", "srpt_geohash" },
+      { "id:Q.A", "srpt_quad" },
+      { "id:P.A", "srpt_packedquad" },
+      { "id:R.A", "srptgeom" },
+      { "id:S.A", "str_shape" },
+    };
+    
+    for(String[] args : check) {
+      json = readJSON(h.query(req(
+          "q",args[0], 
+          "wt","geojson", 
+          "fl","*", 
+          "geojson.field", args[1])));
+      
+      geo = getFirstFeatureGeometry(json);
+      assertEquals( 
+        "Error reading point from: "+args[1] + " ("+args[0]+")",
+        // NOTE: not actual JSON, it is Map.toString()!
+        "{type=Point, coordinates=[1, 2]}", ""+geo);  
+    }
+  }
+  
+  protected Map<String,Object> readFirstDoc(String json)
+  {
+    List docs = (List)((Map)readJSON(json).get("response")).get("docs");
+    return (Map)docs.get(0);
+  }
+  
+  public static String normalizeMapToJSON(String val) {
+    val = val.replace("\"", ""); // remove quotes
+    val = val.replace(':', '=');
+    val = val.replace(", ", ",");
+    return val;
+  }
+
+  @Test
+  public void testTransformToAllFormats() throws Exception {
+    
+    String wkt = "POINT( 1 2 )";
+    SupportedFormats fmts = SpatialContext.GEO.getFormats();
+    Shape shape = fmts.read(wkt);
+    
+    String[] check = new String[] {
+        "srpt_geohash",
+        "srpt_geohash",
+        "srpt_quad",
+        "srpt_packedquad",
+        "srptgeom",
+ //       "str_shape",  // NEEDS TO BE A SpatialField!
+    };
+    
+    String[] checkFormats = new String[] {
+        "GeoJSON",
+        "WKT",
+        "POLY"
+    };
+    
+    for(String field : check) {
+      // Add a document with the given field
+      assertU(adoc("id","test", 
+          field, wkt));
+      assertU(commit());
+      
+      
+      for(String fmt : checkFormats) {
+        String json = h.query(req(
+            "q","id:test", 
+            "wt","json", 
+            "indent", "true",
+            "fl","xxx:[geo f="+field+" w="+fmt+"]"
+            ));
+        
+        Map<String,Object> doc = readFirstDoc(json);
+        Object v = doc.get("xxx");
+        String expect = fmts.getWriter(fmt).toString(shape);
+        
+        if(!(v instanceof String)) {
+          v = normalizeMapToJSON(v.toString());
+          expect = normalizeMapToJSON(expect);
+        }
+        
+        assertEquals("Bad result: "+field+"/"+fmt, expect, v.toString());
+      }
+    }
+  }
+}