diff --git a/examples/geojson.html b/examples/geojson.html new file mode 100644 index 0000000000..b4c2b25192 --- /dev/null +++ b/examples/geojson.html @@ -0,0 +1,168 @@ + + + Vector Formats + + + + + + +
+

OpenLayers Vector Formats Example

+
+
+

Use the drop-down below to select the input/output format + for vector features. New features can be added by using the drawing + tools above or by pasting their text representation below.

+ + +   + + +
+ +
+ +
+
+
+

Use the tools to the left to draw new polygons, lines, and points. + After drawing some new features, hover over a feature to see the + serialized version below.

+ +
+ + diff --git a/lib/OpenLayers.js b/lib/OpenLayers.js index 8a0263708d..41b65b9864 100644 --- a/lib/OpenLayers.js +++ b/lib/OpenLayers.js @@ -166,6 +166,8 @@ "OpenLayers/Format/GeoRSS.js", "OpenLayers/Format/WFS.js", "OpenLayers/Format/WKT.js", + "OpenLayers/Format/JSON.js", + "OpenLayers/Format/GeoJSON.js", "OpenLayers/Layer/WFS.js", "OpenLayers/Control/MouseToolbar.js", "OpenLayers/Control/NavToolbar.js", diff --git a/lib/OpenLayers/Format/GeoJSON.js b/lib/OpenLayers/Format/GeoJSON.js new file mode 100644 index 0000000000..0b90b05035 --- /dev/null +++ b/lib/OpenLayers/Format/GeoJSON.js @@ -0,0 +1,644 @@ +/* Copyright (c) 2006 MetaCarta, Inc., published under a modified BSD license. + * See http://svn.openlayers.org/trunk/openlayers/repository-license.txt + * for the full text of the license. */ + +/** + * @requires OpenLayers/Format/JSON.js + * + * Class: OpenLayers.Format.GeoJSON + * Read and write GeoJSON. Create a new parser with the + * constructor. + * + * Inherits from: + * - + */ +OpenLayers.Format.GeoJSON = OpenLayers.Class(OpenLayers.Format.JSON, { + + /** + * Constructor: OpenLayers.Format.GeoJSON + * Create a new parser for GeoJSON. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + initialize: function(options) { + OpenLayers.Format.JSON.prototype.initialize.apply(this, [options]); + }, + + /** + * APIMethod: read + * Deserialize a GeoJSON string. + * + * Parameters: + * json - {String} A GeoJSON string + * type - {String} Optional string that determines the structure of + * the output. Supported values are "Geometry", "Feature", + * "GeometryCollection", and "FeatureCollection". If absent or null, + * a default of "FeatureCollection" is assumed. + * filter - {Function} A function which will be called for every key and + * value at every level of the final result. Each value will be + * replaced by the result of the filter function. This can be used to + * reform generic objects into instances of classes, or to transform + * date strings into Date objects. + * + * Returns: + * {Object} The return depends on the value of the type argument. If type + * is "FeatureCollection" (the default), the return will be an array + * of . If type is "Geometry", the input json + * must represent a single geometry, and the return will be an + * . If type is "Feature", the input json must + * represent a single feature, and the return will be an + * . If type is "GeometryCollection", the + * input json must represent a geometry collection, and the return will + * be an array of . + */ + read: function(json, type, filter) { + type = (type) ? type : "FeatureCollection"; + var results = null; + var obj = null; + if (typeof json == "string") { + obj = OpenLayers.Format.JSON.prototype.read.apply(this, + [json, filter]); + } else { + obj = json; + } + if(!obj) { + OpenLayers.Console.error("Bad JSON: " + json); + } else if(typeof(obj.type) != "string") { + OpenLayers.Console.error("Bad GeoJSON - no type: " + json); + } else if(this.isValidType(obj, type)) { + switch(type) { + case "Geometry": + try { + results = this.parseGeometry(obj); + } catch(err) { + OpenLayers.Console.error(err); + } + break; + case "Feature": + try { + results = this.parseFeature(obj); + results.type = "Feature"; + } catch(err) { + OpenLayers.Console.error(err); + } + break; + case "GeometryCollection": + results = []; + for(var i=0; i. + * + * Parameters: + * obj - {Object} An object created from a GeoJSON object + * + * Returns: + * {} A feature. + */ + parseFeature: function(obj) { + var feature, geometry, attributes; + attributes = (obj.properties) ? obj.properties : {}; + try { + geometry = this.parseGeometry(obj.geometry); + } catch(err) { + // deal with bad geometries + throw err; + } + feature = new OpenLayers.Feature.Vector(geometry, attributes); + if(obj.id) { + feature.fid = obj.id; + } + return feature; + }, + + /** + * Method: parseGeometry + * Convert a geometry object from GeoJSON into an . + * + * Parameters: + * obj - {Object} An object created from a GeoJSON object + * + * Returns: + * {} A geometry. + */ + parseGeometry: function(obj) { + var geometry; + if(!(obj.coordinates instanceof Array)) { + throw "Geometry must have coordinates array: " + obj; + } + if(!this.parseCoords[obj.type.toLowerCase()]) { + throw "Unsupported geometry type: " + obj.type; + } + try { + geometry = this.parseCoords[obj.type.toLowerCase()].apply(this, [obj.coordinates]); + } catch(err) { + // deal with bad coordinates + throw err; + } + return geometry; + }, + + /** + * Property: parseCoords + * Object with properties corresponding to the GeoJSON geometry types. + * Property values are functions that do the actual parsing. + */ + parseCoords: { + /** + * Method: parseCoords.point + * Convert a coordinate array from GeoJSON into an + * . + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {} A geometry. + */ + "point": function(array) { + if(array.length != 2) { + throw "Only 2D points are supported: " + array; + } + return new OpenLayers.Geometry.Point(array[0], array[1]); + }, + + /** + * Method: parseCoords.multipoint + * Convert a coordinate array from GeoJSON into an + * . + * + * Parameters: + * array {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {} A geometry. + */ + "multipoint": function(array) { + var points = []; + var p = null; + for(var i=0; i. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {} A geometry. + */ + "linestring": function(array) { + var points = []; + var p = null; + for(var i=0; i. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {} A geometry. + */ + "multilinestring": function(array) { + var lines = []; + var l = null; + for(var i=0; i. + * + * Returns: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {} A geometry. + */ + "polygon": function(array) { + var rings = []; + var r, l; + for(var i=0; i. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {} A geometry. + */ + "multipolygon": function(array) { + var polys = []; + var p = null; + for(var i=0; i. + * + * Parameters: + * array - {Object} The coordinates array from the GeoJSON fragment. + * + * Returns: + * {} A geometry. + */ + "box": function(array) { + if(array.length != 2) { + throw "GeoJSON box coordinates must have 2 elements"; + } + return new OpenLayers.Geometry.Polygon([ + new OpenLayers.Geometry.LinearRing([ + new OpenLayers.Geometry.Point(array[0][0], array[0][1]), + new OpenLayers.Geometry.Point(array[1][0], array[0][1]), + new OpenLayers.Geometry.Point(array[1][0], array[1][1]), + new OpenLayers.Geometry.Point(array[0][0], array[1][1]), + new OpenLayers.Geometry.Point(array[0][0], array[0][1]) + ]) + ]); + } + + }, + + /** + * APIMethod: write + * Serialize a feature, geometry, array of features, or array of geometries + * into a GeoJSON string. + * + * Parameters: + * obj - {Object} An , , + * or an array of either features or geometries. + * pretty - {Boolean} Structure the output with newlines and indentation. + * Default is false. + * + * Returns: + * {String} The GeoJSON string representation of the input geometry, + * features, array of geometries, or array of features. + */ + write: function(obj, pretty) { + var geojson = { + "type": null + }; + if(obj instanceof Array) { + geojson.members = []; + for(var i=0; i} + * + * Returns: + * {Object} An object representing the point. + */ + 'feature': function(feature) { + var geom = this.extract.geometry.apply(this, [feature.geometry]); + return { + "type": "Feature", + "id": feature.fid == null ? feature.id : feature.fid, + "properties": feature.attributes, + "geometry": geom + } + }, + + /** + * Method: extract.geometry + * Return a GeoJSON object representing a single geometry. + * + * Parameters: + * geometry - {} + * + * Returns: + * {Object} An object representing the geometry. + */ + 'geometry': function(geometry) { + var geometryType = geometry.CLASS_NAME.split('.')[2]; + var data = this.extract[geometryType.toLowerCase()].apply(this, [geometry]); + return { + "type": geometryType, + "coordinates": data + } + }, + + /** + * Method: extract.poin + * Return an array of coordinates from a point. + * + * Parameters: + * point - {} + * + * Returns: + * {Array} An array of coordinates representing the point. + */ + 'point': function(point) { + return [point.x, point.y]; + }, + + /** + * Method: extract.multipoint + * Return an array of point coordinates from a multipoint. + * + * Parameters: + * multipoint - {} + * + * Returns: + * {Array} An array of point coordinate arrays representing + * the multipoint. + */ + 'multipoint': function(multipoint) { + var array = []; + for(var i=0; i} + * + * Returns: + * {Array} An array of coordinate arrays representing + * the linestring. + */ + 'linestring': function(linestring) { + var array = []; + for(var i=0; i} + * + * Returns: + * {Array} An array of linestring arrays representing + * the multilinestring. + */ + 'multilinestring': function(multilinestring) { + var array = []; + for(var i=0; i} + * + * Returns: + * {Array} An array of linear ring arrays representing the polygon. + */ + 'polygon': function(polygon) { + var array = []; + for(var i=0; i} + * + * Returns: + * {Array} An array of polygon arrays representing + * the multipolygon + */ + 'multipolygon': function(multipolygon) { + var array = []; + for(var i=0; i constructor. + * + * Inherits from: + * - + */ +OpenLayers.Format.JSON = OpenLayers.Class(OpenLayers.Format, { + + /** + * APIProperty: indent + * {String} For "pretty" printing, the indent string will be used once for + * each indentation level. + */ + indent: " ", + + /** + * APIProperty: space + * {String} For "pretty" printing, the space string will be used after + * the ":" separating a name/value pair. + */ + space: " ", + + /** + * APIProperty: newline + * {String} For "pretty" printing, the newline string will be used at the + * end of each name/value pair or array item. + */ + newline: "\n", + + /** + * Property: level + * {Integer} For "pretty" printing, this is incremented/decremented during + * serialization. + */ + level: 0, + + /** + * Property: pretty + * {Boolean} Serialize with extra whitespace for structure. This is set + * by the method. + */ + pretty: false, + + /** + * Constructor: OpenLayers.Format.JSON + * Create a new parser for JSON. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + initialize: function(options) { + OpenLayers.Format.prototype.initialize.apply(this, [options]); + }, + + /** + * APIMethod: read + * Deserialize a json string. + * + * Parameters: + * json - {String} A JSON string + * filter - {Function} A function which will be called for every key and + * value at every level of the final result. Each value will be + * replaced by the result of the filter function. This can be used to + * reform generic objects into instances of classes, or to transform + * date strings into Date objects. + * + * Returns: + * {Object} An object, array, string, or number . + */ + read: function(json, filter) { + /** + * Parsing happens in three stages. In the first stage, we run the text + * against a regular expression which looks for non-JSON + * characters. We are especially concerned with '()' and 'new' + * because they can cause invocation, and '=' because it can cause + * mutation. But just to be safe, we will reject all unexpected + * characters. + */ + try { + if(/^("(\\.|[^"\\\n\r])*?"|[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t])+?$/. + test(json)) { + + /** + * In the second stage we use the eval function to compile the + * text into a JavaScript structure. The '{' operator is + * subject to a syntactic ambiguity in JavaScript - it can + * begin a block or an object literal. We wrap the text in + * parens to eliminate the ambiguity. + */ + var object = eval('(' + json + ')'); + + /** + * In the optional third stage, we recursively walk the new + * structure, passing each name/value pair to a filter + * function for possible transformation. + */ + if(typeof filter === 'function') { + function walk(k, v) { + if(v && typeof v === 'object') { + for(var i in v) { + if(v.hasOwnProperty(i)) { + v[i] = walk(i, v[i]); + } + } + } + return filter(k, v); + } + object = walk('', object); + } + return object; + } + } catch(e) { + // Fall through if the regexp test fails. + } + return null; + }, + + /** + * APIMethod: write + * Serialize an object into a JSON string. + * + * Parameters: + * value - {String} The object, array, string, number, boolean or date + * to be serialized. + * pretty - {Boolean} Structure the output with newlines and indentation. + * Default is false. + * + * Returns: + * {String} The JSON string representation of the input value. + */ + write: function(value, pretty) { + this.pretty = !!pretty; + var json = null; + var type = typeof value; + if(this.serialize[type]) { + json = this.serialize[type].apply(this, [value]); + } + return json; + }, + + /** + * Method: writeIndent + * Output an indentation string depending on the indentation level. + * + * Returns: + * {String} An appropriate indentation string. + */ + writeIndent: function() { + var pieces = []; + if(this.pretty) { + for(var i=0; i 0) { + pieces.push(','); + } + pieces.push(this.writeNewline(), this.writeIndent(), json); + } + } + + this.level -= 1; + pieces.push(this.writeNewline(), this.writeIndent(), ']'); + return pieces.join(''); + }, + + /** + * Method: serialize.string + * Transform a string into a JSON string. + * + * Parameters + * string - {String} The string to be serialized + * + * Returns: + * {String} A JSON string representing the string. + */ + 'string': function(string) { + // If the string contains no control characters, no quote characters, and no + // backslash characters, then we can simply slap some quotes around it. + // Otherwise we must also replace the offending characters with safe + // sequences. + var m = { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }; + if(/["\\\x00-\x1f]/.test(string)) { + return '"' + string.replace(/([\x00-\x1f\\"])/g, function(a, b) { + var c = m[b]; + if(c) { + return c; + } + c = b.charCodeAt(); + return '\\u00' + + Math.floor(c / 16).toString(16) + + (c % 16).toString(16); + }) + '"'; + } + return '"' + string + '"'; + }, + + /** + * Method: serialize.number + * Transform a number into a JSON string. + * + * Parameters: + * number - {Number} The number to be serialized. + * + * Returns: + * {String} A JSON string representing the number. + */ + 'number': function(number) { + return isFinite(number) ? String(number) : "null"; + }, + + /** + * Method: serialize.boolean + * Transform a boolean into a JSON string. + * + * Parameters: + * bool - {Boolean} The boolean to be serialized. + * + * Returns: + * {String} A JSON string representing the boolean. + */ + 'boolean': function(bool) { + return String(bool); + }, + + /** + * Method: serialize.object + * Transform a date into a JSON string. + * + * Parameters: + * date - {Date} The date to be serialized. + * + * Returns: + * {String} A JSON string representing the date. + */ + 'date': function(date) { + function format(number) { + // Format integers to have at least two digits. + return (number < 10) ? '0' + number : number; + } + return '"' + date.getFullYear() + '-' + + format(date.getMonth() + 1) + '-' + + format(date.getDate()) + 'T' + + format(date.getHours()) + ':' + + format(date.getMinutes()) + ':' + + format(date.getSeconds()) + '"'; + } + }, + + CLASS_NAME: "OpenLayers.Format.JSON" + +}); diff --git a/tests/Format/test_GeoJSON.html b/tests/Format/test_GeoJSON.html new file mode 100644 index 0000000000..50e8527c0b --- /dev/null +++ b/tests/Format/test_GeoJSON.html @@ -0,0 +1,267 @@ + + + + + + + + diff --git a/tests/Format/test_JSON.html b/tests/Format/test_JSON.html new file mode 100644 index 0000000000..d9520e9eb9 --- /dev/null +++ b/tests/Format/test_JSON.html @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/tests/list-tests.html b/tests/list-tests.html index 177ce47a97..61b681489b 100644 --- a/tests/list-tests.html +++ b/tests/list-tests.html @@ -22,6 +22,8 @@
  • test_Format.html
  • Format/test_XML.html
  • Format/test_GeoRSS.html
  • +
  • Format/test_JSON.html
  • +
  • Format/test_GeoJSON.html
  • Format/test_GML.html
  • Format/test_WKT.html
  • test_Icon.html