diff --git a/src/org/oscim/database/MapDatabaseFactory.java b/src/org/oscim/database/MapDatabaseFactory.java index 604ee4d7..d081c5a7 100644 --- a/src/org/oscim/database/MapDatabaseFactory.java +++ b/src/org/oscim/database/MapDatabaseFactory.java @@ -45,7 +45,8 @@ public final class MapDatabaseFactory { case TEST_READER: return new org.oscim.database.test.MapDatabase(); case PBMAP_READER: - return new org.oscim.database.pbmap.MapDatabase(); + //return new org.oscim.database.pbmap.MapDatabase(); + return new org.oscim.database.mapnik.MapDatabase(); case OSCIMAP_READER: return new org.oscim.database.oscimap.MapDatabase(); default: diff --git a/src/org/oscim/database/mapnik/LwHttp.java b/src/org/oscim/database/mapnik/LwHttp.java new file mode 100644 index 00000000..03facef8 --- /dev/null +++ b/src/org/oscim/database/mapnik/LwHttp.java @@ -0,0 +1,397 @@ +/* + * Copyright 2013 Hannes Janetzek + * + * 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.database.mapnik; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.URL; +import java.util.zip.InflaterInputStream; + +import org.oscim.core.Tile; + +import android.os.SystemClock; +import android.util.Log; + +public class LwHttp { + private static final String TAG = LwHttp.class.getName(); + private final static int BUFFER_SIZE = 65536; + + // + byte[] buffer = new byte[BUFFER_SIZE]; + + // position in buffer + int bufferPos; + + // bytes available in buffer + int bufferFill; + + // offset of buffer in message + private int mBufferOffset; + + private String mHost; + private int mPort; + private InputStream mInputStream; + + private int mMaxReq = 0; + private Socket mSocket; + private OutputStream mCommandStream; + private BufferedInputStream mResponseStream; + long mLastRequest = 0; + private SocketAddress mSockAddr; + + //private final static byte[] RESPONSE_HTTP_OK = "HTTP/1.1 200 OK".getBytes(); + private final static byte[] RESPONSE_HTTP_OK = "200 OK".getBytes(); + private final static byte[] RESPONSE_CONTENT_LEN = "Content-Length: ".getBytes(); + private final static int RESPONSE_EXPECTED_LIVES = 100; + private final static int RESPONSE_EXPECTED_TIMEOUT = 10000; + + private byte[] REQUEST_GET_START; + private byte[] REQUEST_GET_END; + + private byte[] mRequestBuffer; + + boolean setServer(String urlString) { + urlString = "http://d1s11ojcu7opje.cloudfront.net/dev/764e0b8d"; + + URL url; + try { + url = new URL(urlString); + } catch (MalformedURLException e) { + + e.printStackTrace(); + return false; + //return new OpenResult("invalid url: " + options.get("url")); + } + + int port = url.getPort(); + if (port < 0) + port = 80; + + String host = url.getHost(); + String path = url.getPath(); + Log.d(TAG, "open database: " + host + " " + port + " " + path); + + REQUEST_GET_START = ("GET " + path).getBytes(); + REQUEST_GET_END = (".vector.pbf HTTP/1.1\n" + + "User-Agent: Wget/1.13.4 (linux-gnu)\n" + + "Accept: */*\n" + + "Host: " + host + "\n" + + "Connection: Keep-Alive\n\n").getBytes(); + + mHost = host; + mPort = port; + + mRequestBuffer = new byte[1024]; + System.arraycopy(REQUEST_GET_START, 0, + mRequestBuffer, 0, REQUEST_GET_START.length); + + return true; + } + + void close() { + if (mSocket != null) { + try { + mSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + mSocket = null; + } + } + } + + int readHeader() throws IOException { + + InputStream is = mResponseStream; + mResponseStream.mark(1 << 16); + + byte[] buf = buffer; + boolean first = true; + int read = 0; + int pos = 0; + int end = 0; + int len = 0; + + int contentLength = 0; + + // header cannot be larger than BUFFER_SIZE for this to work + for (; pos < read || (len = is.read(buf, read, BUFFER_SIZE - read)) >= 0; len = 0) { + read += len; + while (end < read && (buf[end] != '\n')) + end++; + + if (buf[end] == '\n') { + if (first) { + // check only for OK + first = false; + if (!compareBytes(buf, pos + 9, end, RESPONSE_HTTP_OK, 6)){ + String line = new String(buf, pos, end - pos - 1); + Log.d(TAG, ">" + line + "< "); + return -1; + } + } else if (end - pos == 1) { + // check empty line (header end) + end += 1; + break; + } + else { + // parse Content-Length, TODO just encode this with message + for (int i = 0; pos + i < end - 1; i++) { + if (i < 16) { + if (buf[pos + i] == RESPONSE_CONTENT_LEN[i]) + continue; + + break; + } + + // read int value + contentLength = contentLength * 10 + (buf[pos + i]) - '0'; + } + } + //String line = new String(buf, pos, end - pos - 1); + //Log.d(TAG, ">" + line + "< "); + + pos += (end - pos) + 1; + end = pos; + } + } + + // back to start of content + mResponseStream.reset(); + mResponseStream.mark(0); + mResponseStream.skip(end); + + // start of content + bufferPos = 0; + mBufferOffset = 0; + + // buffer fill + bufferFill = 0; + + // decode zlib compressed content + mInputStream = new InflaterInputStream(mResponseStream); + + return 1; + } + + boolean sendRequest(Tile tile) throws IOException { + + bufferFill = 0; + bufferPos = 0; + //mReadPos = 0; + + if (mSocket != null && ((mMaxReq-- <= 0) + || (SystemClock.elapsedRealtime() - mLastRequest + > RESPONSE_EXPECTED_TIMEOUT))) { + try { + mSocket.close(); + } catch (IOException e) { + + } + + // Log.d(TAG, "not alive - recreate connection " + mMaxReq); + mSocket = null; + } + + if (mSocket == null) { + lwHttpConnect(); + // we know our server + mMaxReq = RESPONSE_EXPECTED_LIVES; + // Log.d(TAG, "create connection"); + } else { + // should not be needed + int avail = mResponseStream.available(); + if (avail > 0) { + Log.d(TAG, "Consume left-over bytes: " + avail); + mResponseStream.read(buffer, 0, avail); + } + } + + byte[] request = mRequestBuffer; + int pos = REQUEST_GET_START.length; + + request[pos++] = '/'; + request[pos++] = pos2hex(tile.tileX); + request[pos++] = pos2hex(tile.tileY); + request[pos++] = '/'; + pos = writeInt(tile.zoomLevel, pos, request); + request[pos++] = '/'; + pos = writeInt(tile.tileX, pos, request); + request[pos++] = '/'; + pos = writeInt(tile.tileY, pos, request); + + int len = REQUEST_GET_END.length; + System.arraycopy(REQUEST_GET_END, 0, request, pos, len); + len += pos; + + //Log.d(TAG, "request " + new String(request,0,len)); + // this does the same but with a few more allocations: + // byte[] request = String.format(REQUEST, + // Integer.valueOf(tile.zoomLevel), + // Integer.valueOf(tile.tileX), Integer.valueOf(tile.tileY)).getBytes(); + + try { + mCommandStream.write(request, 0, len); + mCommandStream.flush(); + return true; + } catch (IOException e) { + Log.d(TAG, "recreate connection"); + } + + lwHttpConnect(); + + mCommandStream.write(request, 0, len); + mCommandStream.flush(); + + return true; + } + + private final static byte[] hexTable = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + private static byte pos2hex(int pos){ + return hexTable[(pos % 16)]; + } + + private boolean lwHttpConnect() throws IOException { + if (mSockAddr == null) + mSockAddr = new InetSocketAddress(mHost, mPort); + + mSocket = new Socket(); + mSocket.connect(mSockAddr, 30000); + mSocket.setTcpNoDelay(true); + + mCommandStream = mSocket.getOutputStream(); + mResponseStream = new BufferedInputStream(mSocket.getInputStream()); + + return true; + } + + // write (positive) integer as char sequence to buffer + private static int writeInt(int val, int pos, byte[] buf) { + if (val == 0) { + buf[pos] = '0'; + return pos + 1; + } + + int i = 0; + for (int n = val; n > 0; n = n / 10, i++) + buf[pos + i] = (byte) ('0' + n % 10); + + // reverse bytes + for (int j = pos, end = pos + i - 1, mid = pos + i / 2; j < mid; j++, end--) { + byte tmp = buf[j]; + buf[j] = buf[end]; + buf[end] = tmp; + } + + return pos + i; + } + + private static boolean compareBytes(byte[] buffer, int position, int available, + byte[] string, int length) { + + if (available - position < length) + return false; + + for (int i = 0; i < length; i++) + if (buffer[position + i] != string[i]) + return false; + + return true; + } + + static int decodeInt(byte[] buffer, int offset) { + return buffer[offset] << 24 | (buffer[offset + 1] & 0xff) << 16 + | (buffer[offset + 2] & 0xff) << 8 + | (buffer[offset + 3] & 0xff); + } + + public boolean hasData() { + try { + return readBuffer(1); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return false; + } + + public int position() { + return mBufferOffset + bufferPos; + } + + public boolean readBuffer(int size) throws IOException { + // check if buffer already contains the request bytes + if (bufferPos + size < bufferFill) + return true; + + // check if inputstream is read to the end + //if (mReadPos == mReadEnd) + // return; + + int maxSize = buffer.length; + + if (size > maxSize) { + Log.d(TAG, "increase read buffer to " + size + " bytes"); + maxSize = size; + byte[] tmp = new byte[maxSize]; + bufferFill -= bufferPos; + System.arraycopy(buffer, bufferPos, tmp, 0, bufferFill); + mBufferOffset += bufferPos; + bufferPos = 0; + buffer = tmp; + } + + if (bufferFill == bufferPos) { + mBufferOffset += bufferPos; + bufferPos = 0; + bufferFill = 0; + } else if (bufferPos + size > maxSize) { + // copy bytes left to the beginning of buffer + bufferFill -= bufferPos; + System.arraycopy(buffer, bufferPos, buffer, 0, bufferFill); + mBufferOffset += bufferPos; + bufferPos = 0; + } + + int max = maxSize - bufferFill; + + while ((bufferFill - bufferPos) < size && max > 0) { + + max = maxSize - bufferFill; + + // read until requested size is available in buffer + int len = mInputStream.read(buffer, bufferFill, max); + + if (len < 0) { + // finished reading, mark end + buffer[bufferFill] = 0; + return false; + } + + bufferFill += len; + } + return true; + } + +} diff --git a/src/org/oscim/database/mapnik/MapDatabase.java b/src/org/oscim/database/mapnik/MapDatabase.java new file mode 100644 index 00000000..365a529a --- /dev/null +++ b/src/org/oscim/database/mapnik/MapDatabase.java @@ -0,0 +1,899 @@ +/* + * Copyright 2013 + * + * 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.database.mapnik; + +import java.io.IOException; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.util.ArrayList; + +import org.oscim.core.BoundingBox; +import org.oscim.core.GeoPoint; +import org.oscim.core.GeometryBuffer.GeometryType; +import org.oscim.core.MapElement; +import org.oscim.core.Tag; +import org.oscim.core.Tile; +import org.oscim.database.IMapDatabase; +import org.oscim.database.IMapDatabaseCallback; +import org.oscim.database.MapInfo; +import org.oscim.database.MapOptions; +import org.oscim.layers.tile.MapTile; +import org.oscim.utils.UTF8Decoder; +import org.oscim.utils.pool.Inlist; +import org.oscim.utils.pool.Pool; + +import android.os.SystemClock; +import android.util.Log; + +public class MapDatabase implements IMapDatabase { + private static final String TAG = MapDatabase.class.getName(); + + private static final MapInfo mMapInfo = + new MapInfo(new BoundingBox(-180, -90, 180, 90), + new Byte((byte) 4), new GeoPoint(53.11, 8.85), + null, 0, 0, 0, "de", "comment", "author", + new int[] { 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 } + ); + + private final static float REF_TILE_SIZE = 4096.0f; + + // 'open' state + private boolean mOpen = false; + + private IMapDatabaseCallback mMapGenerator; + private float mScaleFactor; + private MapTile mTile; + + private LwHttp lwHttp; + + private final UTF8Decoder mStringDecoder; + + //private final MapElement mElem; + + public MapDatabase() { + mStringDecoder = new UTF8Decoder(); + //mElem = new MapElement(); + } + + @Override + public QueryResult executeQuery(MapTile tile, IMapDatabaseCallback mapDatabaseCallback) { + QueryResult result = QueryResult.SUCCESS; + + mTile = tile; + + mMapGenerator = mapDatabaseCallback; + + // scale coordinates to tile size + mScaleFactor = REF_TILE_SIZE / Tile.SIZE; + + try { + + if (lwHttp.sendRequest(tile) && lwHttp.readHeader() >= 0) { + decode(); + } else { + Log.d(TAG, tile + " Network Error"); + result = QueryResult.FAILED; + } + } catch (SocketException ex) { + Log.d(TAG, tile + " Socket exception: " + ex.getMessage()); + result = QueryResult.FAILED; + } catch (SocketTimeoutException ex) { + Log.d(TAG, tile + " Socket Timeout exception: " + ex.getMessage()); + result = QueryResult.FAILED; + } catch (UnknownHostException ex) { + Log.d(TAG, tile + " no network"); + result = QueryResult.FAILED; + } catch (Exception ex) { + ex.printStackTrace(); + result = QueryResult.FAILED; + } + + lwHttp.mLastRequest = SystemClock.elapsedRealtime(); + + if (result != QueryResult.SUCCESS) { + lwHttp.close(); + } + Log.d(TAG, ">>> " + result + " >>> " + mTile); + return result; + } + + @Override + public MapInfo getMapInfo() { + return mMapInfo; + } + + @Override + public boolean isOpen() { + return mOpen; + } + + @Override + public OpenResult open(MapOptions options) { + if (mOpen) + return OpenResult.SUCCESS; + + if (options == null || !options.containsKey("url")) + return new OpenResult("options missing"); + + lwHttp = new LwHttp(); + + if (!lwHttp.setServer(options.get("url"))) { + return new OpenResult("invalid url: " + options.get("url")); + } + + mOpen = true; + initDecorder(); + + return OpenResult.SUCCESS; + } + + @Override + public void close() { + mOpen = false; + lwHttp.close(); + } + + @Override + public String getMapProjection() { + return null; + } + + @Override + public void cancel() { + } + + private static final int TAG_TILE_LAYERS = 3; + + private static final int TAG_LAYER_VERSION = 15; + private static final int TAG_LAYER_NAME = 1; + private static final int TAG_LAYER_FEATURES = 2; + private static final int TAG_LAYER_KEYS = 3; + private static final int TAG_LAYER_VALUES = 4; + private static final int TAG_LAYER_EXTENT = 5; + + private static final int TAG_FEATURE_ID = 1; + private static final int TAG_FEATURE_TAGS = 2; + private static final int TAG_FEATURE_TYPE = 3; + private static final int TAG_FEATURE_GEOMETRY = 4; + + private static final int TAG_VALUE_STRING = 1; + private static final int TAG_VALUE_FLOAT = 2; + private static final int TAG_VALUE_DOUBLE = 3; + private static final int TAG_VALUE_LONG = 4; + private static final int TAG_VALUE_UINT = 5; + private static final int TAG_VALUE_SINT = 6; + private static final int TAG_VALUE_BOOL = 7; + + private static final int TAG_GEOM_UNKNOWN = 0; + private static final int TAG_GEOM_POINT = 1; + private static final int TAG_GEOM_LINE = 2; + private static final int TAG_GEOM_POLYGON = 3; + + //rivate final short[] mTmpKeys = new short[100]; + //private final Tag[] mTmpTags = new Tag[20]; + //private Tag[][] mElementTags; + + private short[] mTmpTags = new short[1024]; + + //private final short[] mPrevTags = new short[1024]; + + private void initDecorder() { + // reusable tag set + // Tag[][] tags = new Tag[10][]; + // for (int i = 0; i < 10; i++) + // tags[i] = new Tag[i + 1]; + //mElementTags = tags; + } + + private boolean decode() throws IOException { + + int val; + + while (lwHttp.hasData() && (val = decodeVarint32()) > 0) { + // read tag and wire type + int tag = (val >> 3); + + switch (tag) { + case TAG_TILE_LAYERS: + decodeLayer(); + break; + + default: + Log.d(TAG, mTile + " invalid type for tile: " + tag); + return false; + } + } + return true; + } + + private boolean decodeLayer() throws IOException { + + int version = 0; + int extent = 4096; + + int bytes = decodeVarint32(); + + ArrayList keys = new ArrayList(); + ArrayList values = new ArrayList(); + + String name = null; + int numFeatures = 0; + ArrayList features = new ArrayList(); + + int end = lwHttp.position() + bytes; + while (lwHttp.position() < end) { + // read tag and wire type + int val = decodeVarint32(); + if (val == 0) + break; + + int tag = (val >> 3); + + switch (tag) { + case TAG_LAYER_KEYS: + keys.add(decodeString()); + break; + + case TAG_LAYER_VALUES: + values.add(decodeValue()); + break; + + case TAG_LAYER_FEATURES: + numFeatures++; + decodeFeature(features); + break; + + case TAG_LAYER_VERSION: + version = decodeVarint32(); + break; + + case TAG_LAYER_NAME: + name = decodeString(); + break; + + case TAG_LAYER_EXTENT: + extent = decodeVarint32(); + break; + + default: + Log.d(TAG, mTile + " invalid type for layer: " + tag); + break; + } + + } + boolean isRoad = "road".equals(name); + boolean isBridge = "bridge".equals(name); + boolean isTunnel = "tunnel".equals(name); + boolean isBuilding = "building".equals(name); + boolean isLanduse = "landuse".equals(name); + boolean isWater = "water".equals(name); + + Tag layerTag = new Tag(name, Tag.VALUE_YES); + + if (numFeatures == 0) + return true; + + for (Feature f : features) { + int addTags = 0; + if (isBuilding || isWater) + addTags = 1; + + Tag[] tags = new Tag[f.numTags + addTags]; + + if (isBuilding) + tags[tags.length - 1] = BUILDING_TAG; + if (isWater) + tags[tags.length - 1] = WATER_TAG; + + if (tags.length == 0) + continue; + + for (int j = 0; j < (f.numTags << 1); j += 2) { + + String key = keys.get(f.tags[j]); + String val = values.get(f.tags[j + 1]); + + Tag tag = null; + if ("class".equals(key)) { + if (isRoad || isTunnel || isBridge) { + if ("street".equals(val)) + tag = HIGHWAY_STREET_TAG; + else if ("main".equals(val)) + tag = HIGHWAY_MAIN_TAG; + else if ("major".equals(val)) + tag = HIGHWAY_MAJOR_TAG; + else if ("major_rail".equals(val)) + tag = HIGHWAY_RAIL_TAG; + else + tag = new Tag(Tag.TAG_KEY_HIGHWAY, val); + + } else if (isLanduse) + tag = new Tag(Tag.TAG_KEY_LANDUSE, val); + } + if (tag == null) + tag = new Tag(key, val); + + tags[j >> 1] = tag; + } + + f.elem.set(tags, 5); + mMapGenerator.renderElement(f.elem); + mFeaturePool.release(f); + } + + return true; + } + + private final Pool mFeaturePool = new Pool() { + int count; + + @Override + protected Feature createItem() { + count++; + return new Feature(); + } + + @Override + protected boolean clearItem(Feature item) { + if (count > 50) { + count--; + return false; + } + + item.elem.tags = null; + item.elem.clear(); + item.tags = null; + item.type = 0; + item.numTags = 0; + + return true; + } + }; + + private final static Tag WATER_TAG = new Tag("natural", "water"); + private final static Tag BUILDING_TAG = new Tag("building", "yes"); + private final static Tag HIGHWAY_MAIN_TAG = new Tag("highway", "secondary"); + private final static Tag HIGHWAY_MAJOR_TAG = new Tag("highway", "primary"); + private final static Tag HIGHWAY_STREET_TAG = new Tag("highway", "residential"); + private final static Tag HIGHWAY_RAIL_TAG = new Tag("railway", "rail"); + + //private final Tag[] mFallbackTag = new Tag[] { new Tag("debug", "way") }; + + // private int mClipped; + + static class Feature extends Inlist { + short[] tags; + int numTags; + int type; + + final MapElement elem; + + Feature() { + elem = new MapElement(); + } + + boolean match(short otherTags[], int otherNumTags, int otherType) { + if (numTags != otherNumTags) + return false; + + if (type != otherType) + return false; + + for (int i = 0; i < numTags << 1; i++) { + if (tags[i] != otherTags[i]) + return false; + } + return true; + } + + } + + private void decodeFeature(ArrayList features) throws IOException { + int bytes = decodeVarint32(); + int end = lwHttp.position() + bytes; + + int type = 0; + long id; + + lastX = 0; + lastY = 0; + + mTmpTags[0] = -1; + + Feature curFeature = null; + int numTags = 0; + + //Log.d(TAG, "start feature"); + while (lwHttp.position() < end) { + // read tag and wire type + int val = decodeVarint32(); + if (val == 0) + break; + + int tag = (val >>> 3); + + switch (tag) { + case TAG_FEATURE_ID: + id = decodeVarint32(); + break; + + case TAG_FEATURE_TAGS: + mTmpTags = decodeShortArray(mTmpTags); + + for (; numTags < mTmpTags.length && mTmpTags[numTags] >= 0;) + numTags += 2; + + numTags >>= 1; + + break; + + case TAG_FEATURE_TYPE: + type = decodeVarint32(); + + //Log.d(TAG, "got type " + type); + + break; + + case TAG_FEATURE_GEOMETRY: + + for (Feature f : features) { + if (f.match(mTmpTags, numTags, type)) { + curFeature = f; + break; + } + } + + if (curFeature == null) { + curFeature = mFeaturePool.get(); + curFeature.tags = new short[numTags << 1]; + System.arraycopy(mTmpTags, 0, curFeature.tags, 0, numTags << 1); + curFeature.numTags = numTags; + curFeature.type = type; + + features.add(curFeature); + } + + decodeCoordinates(type, curFeature); + break; + + default: + Log.d(TAG, mTile + " invalid type for feature: " + tag); + break; + } + } + } + + private final static int CLOSE_PATH = 0x07; + private final static int MOVE_TO = 0x01; + //private final static int LINE_TO = 0x02; + + private int lastX, lastY; + private final int pixel = 7; + + private int decodeCoordinates(int type, Feature feature) throws IOException { + int bytes = decodeVarint32(); + lwHttp.readBuffer(bytes); + + if (feature == null) { + lwHttp.bufferPos += bytes; + return 0; + } + + MapElement elem = feature.elem; + + boolean isPoint = false; + boolean isPoly = false; + boolean isLine = false; + + if (type == TAG_GEOM_LINE) { + elem.startLine(); + isLine = true; + } + else if (type == TAG_GEOM_POLYGON) { + elem.startPolygon(); + isPoly = true; + } else if (type == TAG_GEOM_POINT) { + isPoint = true; + elem.startPoints(); + } else if (type == TAG_GEOM_UNKNOWN) + elem.startPoints(); + + int cnt = 0; + + boolean even = true; + + float scale = mScaleFactor; + + byte[] buf = lwHttp.buffer; + int pos = lwHttp.bufferPos; + int end = pos + bytes; + int val; + int curX = 0; + int curY = 0; + int prevX = 0; + int prevY = 0; + + int cmd = 0; + int num = 0; + boolean first = true; + boolean lastClip = false; + + boolean isOuter = true; + + int xmin = Integer.MAX_VALUE, xmax = Integer.MIN_VALUE; + int ymin = Integer.MAX_VALUE, ymax = Integer.MIN_VALUE; + + while (pos < end) { + if (buf[pos] >= 0) { + val = buf[pos++]; + + } else if (buf[pos + 1] >= 0) { + val = (buf[pos++] & 0x7f) + | buf[pos++] << 7; + + } else if (buf[pos + 2] >= 0) { + val = (buf[pos++] & 0x7f) + | (buf[pos++] & 0x7f) << 7 + | (buf[pos++]) << 14; + + } else if (buf[pos + 3] >= 0) { + val = (buf[pos++] & 0x7f) + | (buf[pos++] & 0x7f) << 7 + | (buf[pos++] & 0x7f) << 14 + | (buf[pos++]) << 21; + + } else { + val = (buf[pos++] & 0x7f) + | (buf[pos++] & 0x7f) << 7 + | (buf[pos++] & 0x7f) << 14 + | (buf[pos++] & 0x7f) << 21 + | (buf[pos]) << 28; + + int max = pos + VARINT_LIMIT; + while (pos < max) + if (buf[pos++] >= 0) + break; + + if (pos == max) + throw new IOException("malformed VarInt32 in " + mTile); + } + + if (num == 0) { + num = val >>> 3; + cmd = val & 0x07; + + if (isLine && lastClip) { + elem.addPoint(curX / scale, curY / scale); + lastClip = false; + } + + if (cmd == CLOSE_PATH) { + num = 0; + continue; + } + if (first) { + first = false; + continue; + } + if (cmd == MOVE_TO) { + if (type == TAG_GEOM_LINE) + elem.startLine(); + else if (type == TAG_GEOM_POLYGON) { + isOuter = false; + elem.startHole(); + } + } + } else { + // zigzag decoding + int s = ((val >>> 1) ^ -(val & 1)); + + if (even) { + even = false; + curX = lastX = lastX + s; + } else { + even = true; + + curY = lastY = lastY + s; + + int dx = (curX - prevX); + int dy = (curY - prevY); + + if ((isPoint || cmd == MOVE_TO) + || (dx > pixel || dx < -pixel) + || (dy > pixel || dy < -pixel) + // dont clip at tile boundaries + || (curX <= 0 || curX >= 4095) + || (curY <= 0 || curY >= 4095)) { + prevX = curX; + prevY = curY; + elem.addPoint(curX / scale, curY / scale); + lastClip = false; + + if (isPoly) { + if (curX < xmin) + xmin = curX; + if (curX > xmax) + xmax = curX; + + if (curY < ymin) + ymin = curY; + if (curY > ymax) + ymax = curY; + + } + + } else { + lastClip = true; + } + + num--; + cnt++; + } + } + } + + if (isPoly && isOuter && !testBBox(xmax - xmin, ymax - ymin)) { + //Log.d(TAG, "skip small poly "+ elem.indexPos + " > " + // + (xmax - xmin) * (ymax - ymin)); + elem.pointPos -= elem.index[elem.indexPos]; + if (elem.indexPos > 0) { + elem.indexPos -= 3; + elem.index[elem.indexPos + 1] = -1; + } else { + elem.type = GeometryType.NONE; + } + + lwHttp.bufferPos += bytes; + + return 0; + } + + if (isLine && lastClip) + elem.addPoint(curX / scale, curY / scale); + + lwHttp.bufferPos = pos; + + return cnt; + } + + private static boolean testBBox(int dx, int dy) { + return dx * dy > 64 * 64; + } + + private String decodeValue() throws IOException { + int bytes = decodeVarint32(); + + String value = null; + + int end = lwHttp.position() + bytes; + + while (lwHttp.position() < end) { + // read tag and wire type + int val = decodeVarint32(); + if (val == 0) + break; + + int tag = (val >> 3); + + switch (tag) { + case TAG_VALUE_STRING: + value = decodeString(); + break; + + case TAG_VALUE_UINT: + value = String.valueOf(decodeVarint32()); + break; + + case TAG_VALUE_SINT: + value = String.valueOf(decodeVarint32()); + break; + + case TAG_VALUE_LONG: + value = String.valueOf(decodeVarint32()); + break; + + case TAG_VALUE_FLOAT: + value = String.valueOf(decodeFloat()); + break; + + case TAG_VALUE_DOUBLE: + value = String.valueOf(decodeDouble()); + break; + + case TAG_VALUE_BOOL: + value = decodeBool() ? "yes" : "no"; + break; + default: + break; + } + + } + return value; + } + + private final static int VARINT_LIMIT = 7; + private final static int VARINT_MAX = 10; + + private short[] decodeShortArray(short[] array) throws IOException { + int bytes = decodeVarint32(); + int arrayLength = array.length; + + lwHttp.readBuffer(bytes); + int cnt = 0; + + byte[] buf = lwHttp.buffer; + int pos = lwHttp.bufferPos; + int end = pos + bytes; + int val; + + while (pos < end) { + if (buf[pos] >= 0) { + val = buf[pos++]; + } else if (buf[pos + 1] >= 0) { + val = (buf[pos++] & 0x7f) + | buf[pos++] << 7; + } else if (buf[pos + 2] >= 0) { + val = (buf[pos++] & 0x7f) + | (buf[pos++] & 0x7f) << 7 + | (buf[pos++]) << 14; + } else if (buf[pos + 3] >= 0) { + val = (buf[pos++] & 0x7f) + | (buf[pos++] & 0x7f) << 7 + | (buf[pos++] & 0x7f) << 14 + | (buf[pos++]) << 21; + } else { + val = (buf[pos++] & 0x7f) + | (buf[pos++] & 0x7f) << 7 + | (buf[pos++] & 0x7f) << 14 + | (buf[pos++] & 0x7f) << 21 + | (buf[pos]) << 28; + + int max = pos + VARINT_LIMIT; + while (pos < max) + if (buf[pos++] >= 0) + break; + + if (pos == max) + throw new IOException("malformed VarInt32 in " + mTile); + } + + if (arrayLength <= cnt) { + arrayLength = cnt + 16; + short[] tmp = array; + array = new short[arrayLength]; + System.arraycopy(tmp, 0, array, 0, cnt); + } + + array[cnt++] = (short) val; + } + + lwHttp.bufferPos = pos; + + if (arrayLength > cnt) + array[cnt] = -1; + + return array; + } + + private int decodeVarint32() throws IOException { + if (lwHttp.bufferPos + VARINT_MAX > lwHttp.bufferFill) + lwHttp.readBuffer(4096); + + byte[] buf = lwHttp.buffer; + int pos = lwHttp.bufferPos; + int val; + + if (buf[pos] >= 0) { + val = buf[pos++]; + } else { + + if (buf[pos + 1] >= 0) { + val = (buf[pos++] & 0x7f) + | (buf[pos++]) << 7; + + } else if (buf[pos + 2] >= 0) { + val = (buf[pos++] & 0x7f) + | (buf[pos++] & 0x7f) << 7 + | (buf[pos++]) << 14; + + } else if (buf[pos + 3] >= 0) { + val = (buf[pos++] & 0x7f + | (buf[pos++] & 0x7f) << 7 + | (buf[pos++] & 0x7f) << 14 + | (buf[pos++]) << 21); + } else { + val = (buf[pos++] & 0x7f) + | (buf[pos++] & 0x7f) << 7 + | (buf[pos++] & 0x7f) << 14 + | (buf[pos++] & 0x7f) << 21 + | (buf[pos]) << 28; + + // 'Discard upper 32 bits' + int max = pos + VARINT_LIMIT; + while (pos < max) + if (buf[pos++] >= 0) + break; + + if (pos == max) + throw new IOException("malformed VarInt32 in " + mTile); + } + } + + lwHttp.bufferPos = pos; + + return val; + } + + private float decodeFloat() throws IOException { + if (lwHttp.bufferPos + 4 > lwHttp.bufferFill) + lwHttp.readBuffer(4096); + + byte[] buf = lwHttp.buffer; + int pos = lwHttp.bufferPos; + + int val = (buf[pos++] & 0xFF + | (buf[pos++] & 0xFF) << 8 + | (buf[pos++] & 0xFF) << 16 + | (buf[pos++] & 0xFF) << 24); + + lwHttp.bufferPos += 4; + + return Float.intBitsToFloat(val); + } + + private double decodeDouble() throws IOException { + if (lwHttp.bufferPos + 8 > lwHttp.bufferFill) + lwHttp.readBuffer(4096); + + byte[] buf = lwHttp.buffer; + int pos = lwHttp.bufferPos; + + long val = (buf[pos++] & 0xFF + | (buf[pos++] & 0xFF) << 8 + | (buf[pos++] & 0xFF) << 16 + | (buf[pos++] & 0xFF) << 24 + | (buf[pos++] & 0xFF) << 32 + | (buf[pos++] & 0xFF) << 40 + | (buf[pos++] & 0xFF) << 48 + | (buf[pos++] & 0xFF) << 56); + + lwHttp.bufferPos += 8; + + return Double.longBitsToDouble(val); + } + + private boolean decodeBool() throws IOException { + if (lwHttp.bufferPos + 1 > lwHttp.bufferFill) + lwHttp.readBuffer(4096); + + boolean val = lwHttp.buffer[lwHttp.bufferPos++] != 0; + + return val; + } + + private String decodeString() throws IOException { + final int size = decodeVarint32(); + lwHttp.readBuffer(size); + final String result = mStringDecoder.decode(lwHttp.buffer, lwHttp.bufferPos, size); + + //Log.d(TAG, "string: " + result); + + lwHttp.bufferPos += size; + + return result; + + } +}