From aee1b22c89936aba043d353811428e6320e402b5 Mon Sep 17 00:00:00 2001 From: Hannes Janetzek Date: Sun, 30 Mar 2014 03:49:54 +0200 Subject: [PATCH] add geoson tile source - based on a patch sent by: Yang (apachemaven) - use jackson-core for stream parsing - rewrite tags, this way one can use the same the for different data --- .../source/geojson/GeoJsonTileDecoder.java | 357 ++++++++++++++++++ .../source/geojson/GeoJsonTileSource.java | 64 ++++ vtm/src/org/oscim/utils/ArrayUtils.java | 77 ++++ 3 files changed, 498 insertions(+) create mode 100644 vtm-extras/src/org/oscim/tiling/source/geojson/GeoJsonTileDecoder.java create mode 100644 vtm-extras/src/org/oscim/tiling/source/geojson/GeoJsonTileSource.java diff --git a/vtm-extras/src/org/oscim/tiling/source/geojson/GeoJsonTileDecoder.java b/vtm-extras/src/org/oscim/tiling/source/geojson/GeoJsonTileDecoder.java new file mode 100644 index 00000000..9d7d0e6f --- /dev/null +++ b/vtm-extras/src/org/oscim/tiling/source/geojson/GeoJsonTileDecoder.java @@ -0,0 +1,357 @@ +/* + * Copyright 2014 Hannes Janetzek + * + * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.tiling.source.geojson; + +import static com.fasterxml.jackson.core.JsonToken.END_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.END_OBJECT; +import static com.fasterxml.jackson.core.JsonToken.FIELD_NAME; +import static com.fasterxml.jackson.core.JsonToken.START_ARRAY; +import static com.fasterxml.jackson.core.JsonToken.START_OBJECT; +import static com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_FLOAT; +import static com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_INT; +import static com.fasterxml.jackson.core.JsonToken.VALUE_STRING; +import static org.oscim.core.MercatorProjection.latitudeToY; +import static org.oscim.core.MercatorProjection.longitudeToX; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.LinkedHashMap; +import java.util.zip.GZIPInputStream; + +import org.oscim.core.GeometryBuffer.GeometryType; +import org.oscim.core.MapElement; +import org.oscim.core.Tile; +import org.oscim.tiling.ITileDataSink; +import org.oscim.tiling.source.ITileDecoder; +import org.oscim.utils.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; + +public class GeoJsonTileDecoder implements ITileDecoder { + static final Logger log = LoggerFactory.getLogger(GeoJsonTileDecoder.class); + + private final MapElement mMapElement; + private final GeoJsonTileSource mTileSource; + private final LinkedHashMap mTagMap; + private final JsonFactory mJsonFactory; + + private final static char[] FIELD_FEATURES = "features".toCharArray(); + private final static char[] FIELD_GEOMETRY = "geometry".toCharArray(); + private final static char[] FIELD_PROPERTIES = "properties".toCharArray(); + private final static char[] FIELD_COORDINATES = "coordinates".toCharArray(); + private final static char[] FIELD_TYPE = "type".toCharArray(); + + private final static char[] LINETRING = "LineString".toCharArray(); + private final static char[] POLYGON = "Polygon".toCharArray(); + private final static char[] POINT = "Point".toCharArray(); + private final static char[] MULTI_LINESTRING = "MultiLineString".toCharArray(); + private final static char[] MULTI_POLYGON = "MultiPolygon".toCharArray(); + private final static char[] MULTI_POINT = "MultiPoint".toCharArray(); + + private ITileDataSink mTileDataSink; + + private double mTileY, mTileX, mTileScale; + + GeoJsonTileDecoder(GeoJsonTileSource tileSource) { + mTileSource = tileSource; + mTagMap = new LinkedHashMap(); + mJsonFactory = new JsonFactory(); + + mMapElement = new MapElement(); + mMapElement.layer = 5; + } + + @Override + public boolean decode(Tile tile, ITileDataSink sink, InputStream is) throws IOException { + mTileDataSink = sink; + mTileScale = 1 << tile.zoomLevel; + mTileX = tile.tileX / mTileScale; + mTileY = tile.tileY / mTileScale; + mTileScale *= Tile.SIZE; + + is = new GZIPInputStream(is); + + JsonParser jp = mJsonFactory.createParser(new InputStreamReader(is)); + + for (JsonToken t; (t = jp.nextToken()) != null;) { + + if (t == FIELD_NAME) { + + if (match(jp, FIELD_FEATURES)) { + if (jp.nextToken() != START_ARRAY) + continue; + + while ((t = jp.nextToken()) != null) { + if (t == START_OBJECT) + parseFeature(jp); + + if (t == END_ARRAY) + break; + } + } + } + } + return true; + } + + private void parseFeature(JsonParser jp) + throws JsonParseException, IOException { + + mMapElement.clear(); + mMapElement.tags.clear(); + mTagMap.clear(); + + for (JsonToken t; (t = jp.nextToken()) != null;) { + + if (t == FIELD_NAME) { + + if (match(jp, FIELD_GEOMETRY)) { + if (jp.nextToken() == START_OBJECT) + parseGeometry(jp); + } + + if (match(jp, FIELD_PROPERTIES)) { + if (jp.nextToken() == START_OBJECT) + parseProperties(jp); + } + continue; + } + if (t == END_OBJECT) + break; + } + + //add tag information + mTileSource.decodeTags(mMapElement, mTagMap); + if (mMapElement.tags.numTags == 0) + return; + + mTileSource.postGeomHook(mMapElement); + + if (mMapElement.type == GeometryType.NONE) + return; + + //process this element + mTileDataSink.process(mMapElement); + } + + private void parseProperties(JsonParser jp) + throws JsonParseException, IOException { + for (JsonToken t; (t = jp.nextToken()) != null;) { + if (t == FIELD_NAME) { + String text = jp.getCurrentName(); + + t = jp.nextToken(); + if (t == VALUE_STRING) { + mTagMap.put(text, jp.getText()); + } else if (t == VALUE_NUMBER_INT) { + mTagMap.put(text, jp.getNumberValue()); + } + continue; + } + if (t == END_OBJECT) + break; + } + } + + private void parseGeometry(JsonParser jp) + throws JsonParseException, IOException { + + boolean multi = false; + GeometryType type = GeometryType.NONE; + + for (JsonToken t; (t = jp.nextToken()) != null;) { + if (t == FIELD_NAME) { + if (match(jp, FIELD_COORDINATES)) { + if (jp.nextToken() != START_ARRAY) + continue; + if (multi) { + parseMulti(jp, type); + } else { + if (type == GeometryType.POLY) + parsePolygon(jp); + + if (type == GeometryType.LINE) + parseLineString(jp); + + if (type == GeometryType.POINT) + parseCoordinate(jp); + + } + } else if (match(jp, FIELD_TYPE)) { + multi = false; + + jp.nextToken(); + + if (match(jp, LINETRING)) + type = GeometryType.LINE; + else if (match(jp, POLYGON)) + type = GeometryType.POLY; + else if (match(jp, POINT)) + type = GeometryType.POINT; + else if (match(jp, MULTI_LINESTRING)) { + type = GeometryType.LINE; + multi = true; + } + else if (match(jp, MULTI_POLYGON)) { + type = GeometryType.POLY; + multi = true; + } + else if (match(jp, MULTI_POINT)) { + type = GeometryType.POINT; + multi = true; + } + + if (type == GeometryType.POINT) + mMapElement.startPoints(); + } + continue; + } + if (t == END_OBJECT) + break; + } + } + + private void parseMulti(JsonParser jp, GeometryType type) + throws JsonParseException, IOException { + + for (JsonToken t; (t = jp.nextToken()) != null;) { + if (t == END_ARRAY) + break; + + if (t == START_ARRAY) { + if (type == GeometryType.POLY) + parsePolygon(jp); + + else if (type == GeometryType.LINE) + parseLineString(jp); + + else if (type == GeometryType.POINT) + parseCoordinate(jp);; + + } else { + //.... + } + } + } + + private void parsePolygon(JsonParser jp) + throws JsonParseException, IOException { + int ring = 0; + + for (JsonToken t; (t = jp.nextToken()) != null;) { + if (t == START_ARRAY) { + if (ring == 0) + mMapElement.startPolygon(); + else + mMapElement.startHole(); + + ring++; + parseCoordSequence(jp); + removeLastPoint(); + continue; + } + + if (t == END_ARRAY) + break; + } + } + + private void removeLastPoint() { + mMapElement.pointPos -= 2; + mMapElement.index[mMapElement.indexPos] -= 2; + } + + private void parseLineString(JsonParser jp) + throws JsonParseException, IOException { + mMapElement.startLine(); + parseCoordSequence(jp); + } + + private void parseCoordSequence(JsonParser jp) + throws JsonParseException, IOException { + + for (JsonToken t; (t = jp.nextToken()) != null;) { + + if (t == START_ARRAY) { + parseCoordinate(jp); + continue; + } + + if (t == END_ARRAY) + break; + + } + } + + private void parseCoordinate(JsonParser jp) + throws JsonParseException, IOException { + int pos = 0; + double x = 0, y = 0; //, z = 0; + + for (JsonToken t; (t = jp.nextToken()) != null;) { + if (t == VALUE_NUMBER_FLOAT || t == VALUE_NUMBER_INT) { + + // avoid String allocation (by getDouble...) + char[] val = jp.getTextCharacters(); + int offset = jp.getTextOffset(); + int length = jp.getTextLength(); + double c = ArrayUtils.parseNumber(val, offset, offset + length); + + if (pos == 0) + x = c; + if (pos == 1) + y = c; + //if (pos == 2) + //z = c; + + pos++; + continue; + } + + if (t == END_ARRAY) + break; + } + + mMapElement.addPoint((float) ((longitudeToX(x) - mTileX) * mTileScale), + (float) ((latitudeToY(y) - mTileY) * mTileScale)); + + } + + private final static boolean match(JsonParser jp, char[] fieldName) + throws JsonParseException, IOException { + + int length = jp.getTextLength(); + if (length != fieldName.length) + return false; + + char[] val = jp.getTextCharacters(); + int offset = jp.getTextOffset(); + + for (int i = 0; i < length; i++) { + if (fieldName[i] != val[i + offset]) + return false; + } + + return true; + } +} diff --git a/vtm-extras/src/org/oscim/tiling/source/geojson/GeoJsonTileSource.java b/vtm-extras/src/org/oscim/tiling/source/geojson/GeoJsonTileSource.java new file mode 100644 index 00000000..e76d63a8 --- /dev/null +++ b/vtm-extras/src/org/oscim/tiling/source/geojson/GeoJsonTileSource.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014 Hannes Janetzek + * + * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). + * + * This program is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with + * this program. If not, see . + */ +package org.oscim.tiling.source.geojson; + +import java.util.HashMap; +import java.util.Map; + +import org.oscim.core.MapElement; +import org.oscim.core.Tag; +import org.oscim.tiling.ITileDataSource; +import org.oscim.tiling.source.LwHttp; +import org.oscim.tiling.source.UrlTileDataSource; +import org.oscim.tiling.source.UrlTileSource; + +public abstract class GeoJsonTileSource extends UrlTileSource { + + public GeoJsonTileSource(String url) { + super(url); + setExtension(".json"); + } + + @Override + public ITileDataSource getDataSource() { + Map opt = new HashMap(); + opt.put("Accept-Encoding", "gzip"); + return new UrlTileDataSource(this, new GeoJsonTileDecoder(this), new LwHttp(getUrl(), opt)); + } + + public Tag getFeatureTag() { + return null; + } + + /** allow overriding tag handling */ + public abstract void decodeTags(MapElement mapElement, Map properties); + + public Tag rewriteTag(String key, Object value) { + + if (value == null) + return null; + + String val = (value instanceof String) ? (String) value : String.valueOf(value); + + return new Tag(key, val); + } + + /** modify mapElement before process() */ + public void postGeomHook(MapElement mapElement) { + + } +} diff --git a/vtm/src/org/oscim/utils/ArrayUtils.java b/vtm/src/org/oscim/utils/ArrayUtils.java index 73815698..112be334 100644 --- a/vtm/src/org/oscim/utils/ArrayUtils.java +++ b/vtm/src/org/oscim/utils/ArrayUtils.java @@ -61,4 +61,81 @@ public class ArrayUtils { right--; } } + + public static double parseNumber(char[] str, int pos, int end) { + + boolean neg = false; + if (str[pos] == '-') { + neg = true; + pos++; + } + + double val = 0; + int pre = 0; + char c = 0; + + for (; pos < end; pos++, pre++) { + c = str[pos]; + if (c < '0' || c > '9') { + if (pre == 0) + throw new NumberFormatException("s " + c); + + break; + } + val = val * 10 + (int) (c - '0'); + } + + if (pre == 0) + throw new NumberFormatException(); + + if (c == '.') { + float div = 10; + for (pos++; pos < end; pos++) { + c = str[pos]; + if (c < '0' || c > '9') + break; + val = val + ((int) (c - '0')) / div; + div *= 10; + } + } + + if (c == 'e' || c == 'E') { + // advance 'e' + pos++; + + // check direction + int dir = 1; + if (str[pos] == '-') { + dir = -1; + pos++; + } + // skip leading zeros + for (; pos < end; pos++) + if (str[pos] != '0') + break; + + int shift = 0; + for (pre = 0; pos < end; pos++, pre++) { + c = str[pos]; + if (c < '0' || c > '9') { + // nothing after 'e' + if (pre == 0) + throw new NumberFormatException("e " + c); + break; + } + shift = shift * 10 + (int) (c - '0'); + } + + // guess it's ok for sane values of E + if (dir > 0) { + while (shift-- > 0) + val *= 10; + } else { + while (shift-- > 0) + val /= 10; + } + } + + return neg ? -val : val; + } }