From d049d378dd9dd52b5b4e4ce4bd1c4e49202fabe8 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 12 Jan 2018 10:02:04 +1100 Subject: [PATCH] vtm-mvt module with new MVT tile decoder (#481) --- settings.gradle | 1 + vtm-android-example/AndroidManifest.xml | 3 + vtm-android-example/build.gradle | 13 +- vtm-android-example/proguard-rules.pro | 10 + .../test/OpenMapTilesMvtMapActivity.java | 68 +++++++ .../src/org/oscim/android/test/Samples.java | 1 + vtm-mvt/build.gradle | 17 ++ .../source/mvt/MapzenMvtTileSource.java | 2 +- .../tiling/source/mvt/MvtTileDecoder.java | 181 ++++++++++++++++++ .../source/mvt/OpenMapTilesMvtTileSource.java | 71 +++++++ vtm-playground/build.gradle | 1 + .../org/oscim/test/OpenMapTilesMvtTest.java | 65 +++++++ vtm-tests/build.gradle | 1 + vtm-tests/resources/mvt-test.pbf | Bin 0 -> 31199 bytes .../tiling/source/mvt/MvtTileDecoderTest.java | 52 +++++ 15 files changed, 484 insertions(+), 2 deletions(-) create mode 100644 vtm-android-example/proguard-rules.pro create mode 100644 vtm-android-example/src/org/oscim/android/test/OpenMapTilesMvtMapActivity.java create mode 100644 vtm-mvt/build.gradle rename {vtm => vtm-mvt}/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java (96%) create mode 100644 vtm-mvt/src/org/oscim/tiling/source/mvt/MvtTileDecoder.java create mode 100644 vtm-mvt/src/org/oscim/tiling/source/mvt/OpenMapTilesMvtTileSource.java create mode 100644 vtm-playground/src/org/oscim/test/OpenMapTilesMvtTest.java create mode 100644 vtm-tests/resources/mvt-test.pbf create mode 100644 vtm-tests/test/org/oscim/tiling/source/mvt/MvtTileDecoderTest.java diff --git a/settings.gradle b/settings.gradle index 3733ffea..673f8532 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,7 @@ include ':vtm-ios-example' include ':vtm-jeo' include ':vtm-json' include ':vtm-jts' +include ':vtm-mvt' include ':vtm-playground' include ':vtm-tests' include ':vtm-theme-comparator' diff --git a/vtm-android-example/AndroidManifest.xml b/vtm-android-example/AndroidManifest.xml index a95510e9..fcecbdc7 100644 --- a/vtm-android-example/AndroidManifest.xml +++ b/vtm-android-example/AndroidManifest.xml @@ -87,6 +87,9 @@ + diff --git a/vtm-android-example/build.gradle b/vtm-android-example/build.gradle index 6d0f2b91..4087b1e2 100644 --- a/vtm-android-example/build.gradle +++ b/vtm-android-example/build.gradle @@ -10,6 +10,7 @@ dependencies { implementation project(':vtm-jeo') implementation project(':vtm-json') implementation project(':vtm-jts') + implementation project(':vtm-mvt') implementation project(':vtm-themes') implementation "org.slf4j:slf4j-android:$slf4jVersion" @@ -35,7 +36,9 @@ android { defaultConfig { versionCode versionCode() versionName versionName() - minSdkVersion androidMinSdk() + // FIXME Minimum API Level by mapbox-vector-tile + //minSdkVersion androidMinSdk() + minSdkVersion 26 targetSdkVersion androidTargetSdk() } @@ -64,6 +67,14 @@ android { exclude 'META-INF/LICENSE' exclude 'META-INF/NOTICE' } + + buildTypes { + all { + minifyEnabled true + useProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } } task run(dependsOn: 'installDebug') { diff --git a/vtm-android-example/proguard-rules.pro b/vtm-android-example/proguard-rules.pro new file mode 100644 index 00000000..584189fa --- /dev/null +++ b/vtm-android-example/proguard-rules.pro @@ -0,0 +1,10 @@ +-keep class com.** { *; } +-dontwarn com.** +-keep class jsqlite.** { *; } +-dontwarn jsqlite.** +-keep class okhttp3.** { *; } +-dontwarn okhttp3.** +-keep class okio.** { *; } +-dontwarn okio.** +-keep class org.** { *; } +-dontwarn org.** diff --git a/vtm-android-example/src/org/oscim/android/test/OpenMapTilesMvtMapActivity.java b/vtm-android-example/src/org/oscim/android/test/OpenMapTilesMvtMapActivity.java new file mode 100644 index 00000000..7d1e02b8 --- /dev/null +++ b/vtm-android-example/src/org/oscim/android/test/OpenMapTilesMvtMapActivity.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016-2017 devemux86 + * + * 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.android.test; + +import android.os.Bundle; + +import org.oscim.android.cache.TileCache; +import org.oscim.layers.TileGridLayer; +import org.oscim.layers.tile.buildings.BuildingLayer; +import org.oscim.layers.tile.vector.VectorTileLayer; +import org.oscim.layers.tile.vector.labeling.LabelLayer; +import org.oscim.theme.VtmThemes; +import org.oscim.tiling.source.OkHttpEngine; +import org.oscim.tiling.source.UrlTileSource; +import org.oscim.tiling.source.mvt.OpenMapTilesMvtTileSource; + +public class OpenMapTilesMvtMapActivity extends MapActivity { + + private static final boolean USE_CACHE = false; + + private TileCache mCache; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + UrlTileSource tileSource = OpenMapTilesMvtTileSource.builder() + .apiKey("xxxxxxx") // Put a proper API key + .httpFactory(new OkHttpEngine.OkHttpFactory()) + //.locale("en") + .build(); + + if (USE_CACHE) { + // Cache the tiles into a local SQLite database + mCache = new TileCache(this, null, "tile.db"); + mCache.setCacheSize(512 * (1 << 10)); + tileSource.setCache(mCache); + } + + VectorTileLayer l = mMap.setBaseMap(tileSource); + mMap.setTheme(VtmThemes.OPENMAPTILES); + + mMap.layers().add(new BuildingLayer(mMap, l)); + mMap.layers().add(new LabelLayer(mMap, l)); + + mMap.layers().add(new TileGridLayer(mMap, getResources().getDisplayMetrics().density)); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (mCache != null) + mCache.dispose(); + } +} diff --git a/vtm-android-example/src/org/oscim/android/test/Samples.java b/vtm-android-example/src/org/oscim/android/test/Samples.java index 30ed6350..c9cc9284 100644 --- a/vtm-android-example/src/org/oscim/android/test/Samples.java +++ b/vtm-android-example/src/org/oscim/android/test/Samples.java @@ -83,6 +83,7 @@ public class Samples extends Activity { linearLayout.addView(createButton(MapsforgeMapActivity.class)); /*linearLayout.addView(createButton(MapzenMvtMapActivity.class)); linearLayout.addView(createButton(MapzenGeojsonMapActivity.class));*/ + linearLayout.addView(createButton(OpenMapTilesMvtMapActivity.class)); linearLayout.addView(createButton(OpenMapTilesGeojsonMapActivity.class)); linearLayout.addView(createButton(GdxMapActivity.class)); diff --git a/vtm-mvt/build.gradle b/vtm-mvt/build.gradle new file mode 100644 index 00000000..5cfeda96 --- /dev/null +++ b/vtm-mvt/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'java-library' +apply plugin: 'maven' + +dependencies { + api project(':vtm') + api 'com.wdtinc:mapbox-vector-tile:2.0.0' +} + +sourceSets { + main.java.srcDirs = ['src'] +} + +if (project.hasProperty("SONATYPE_USERNAME")) { + afterEvaluate { + project.apply from: "${rootProject.projectDir}/deploy.gradle" + } +} diff --git a/vtm/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java b/vtm-mvt/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java similarity index 96% rename from vtm/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java rename to vtm-mvt/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java index b9f4d7e4..0ae1dc59 100644 --- a/vtm/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java +++ b/vtm-mvt/src/org/oscim/tiling/source/mvt/MapzenMvtTileSource.java @@ -66,6 +66,6 @@ public class MapzenMvtTileSource extends UrlTileSource { @Override public ITileDataSource getDataSource() { - return new UrlTileDataSource(this, new TileDecoder(locale), getHttpEngine()); + return new UrlTileDataSource(this, new MvtTileDecoder(locale), getHttpEngine()); } } diff --git a/vtm-mvt/src/org/oscim/tiling/source/mvt/MvtTileDecoder.java b/vtm-mvt/src/org/oscim/tiling/source/mvt/MvtTileDecoder.java new file mode 100644 index 00000000..83fb6ee2 --- /dev/null +++ b/vtm-mvt/src/org/oscim/tiling/source/mvt/MvtTileDecoder.java @@ -0,0 +1,181 @@ +/* + * Copyright 2014 Hannes Janetzek + * Copyright 2017 devemux86 + * Copyright 2018 boldtrn + * + * 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.mvt; + +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LineString; +import com.vividsolutions.jts.geom.MultiLineString; +import com.vividsolutions.jts.geom.MultiPoint; +import com.vividsolutions.jts.geom.MultiPolygon; +import com.vividsolutions.jts.geom.Point; +import com.vividsolutions.jts.geom.Polygon; +import com.wdtinc.mapbox_vector_tile.adapt.jts.MvtReader; +import com.wdtinc.mapbox_vector_tile.adapt.jts.TagKeyValueMapConverter; +import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsLayer; +import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsMvt; + +import org.oscim.core.MapElement; +import org.oscim.core.Tag; +import org.oscim.core.Tile; +import org.oscim.tiling.ITileDataSink; +import org.oscim.tiling.source.ITileDecoder; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +public class MvtTileDecoder implements ITileDecoder { + private final String mLocale; + + private final static float REF_TILE_SIZE = 4096.0f; + private float mScale; + + private final GeometryFactory mGeomFactory; + private final MapElement mMapElement; + private ITileDataSink mTileDataSink; + + public MvtTileDecoder() { + this(""); + } + + public MvtTileDecoder(String locale) { + mLocale = locale; + mGeomFactory = new GeometryFactory(); + mMapElement = new MapElement(); + mMapElement.layer = 5; + } + + @Override + public boolean decode(Tile tile, ITileDataSink sink, InputStream is) + throws IOException { + + mTileDataSink = sink; + mScale = REF_TILE_SIZE / Tile.SIZE; + + JtsMvt jtsMvt = MvtReader.loadMvt( + is, + mGeomFactory, + new TagKeyValueMapConverter(), + MvtReader.RING_CLASSIFIER_V1); + + + for (JtsLayer layer : jtsMvt.getLayers()) { + for (Geometry geometry : layer.getGeometries()) { + parseGeometry(layer.getName(), geometry, (Map) geometry.getUserData()); + } + } + + return true; + } + + private void parseGeometry(String layerName, Geometry geometry, Map tags) { + mMapElement.clear(); + mMapElement.tags.clear(); + + parseTags(tags, layerName); + if (mMapElement.tags.size() == 0) { + return; + } + + boolean err = false; + if (geometry instanceof Point) { + mMapElement.startPoints(); + processCoordinateArray(geometry.getCoordinates(), false); + } else if (geometry instanceof MultiPoint) { + MultiPoint multiPoint = (MultiPoint) geometry; + for (int i = 0; i < multiPoint.getNumGeometries(); i++) { + mMapElement.startPoints(); + processCoordinateArray(multiPoint.getGeometryN(i).getCoordinates(), false); + } + } else if (geometry instanceof LineString) { + processLineString((LineString) geometry); + } else if (geometry instanceof MultiLineString) { + MultiLineString multiLineString = (MultiLineString) geometry; + for (int i = 0; i < multiLineString.getNumGeometries(); i++) { + processLineString((LineString) multiLineString.getGeometryN(i)); + } + } else if (geometry instanceof Polygon) { + Polygon polygon = (Polygon) geometry; + processPolygon(polygon); + } else if (geometry instanceof MultiPolygon) { + MultiPolygon multiPolygon = (MultiPolygon) geometry; + for (int i = 0; i < multiPolygon.getNumGeometries(); i++) { + processPolygon((Polygon) multiPolygon.getGeometryN(i)); + } + } else { + err = true; + } + + if (!err) { + mTileDataSink.process(mMapElement); + } + } + + private void processLineString(LineString lineString) { + mMapElement.startLine(); + processCoordinateArray(lineString.getCoordinates(), false); + } + + private void processPolygon(Polygon polygon) { + mMapElement.startPolygon(); + processCoordinateArray(polygon.getExteriorRing().getCoordinates(), true); + for (int i = 0; i < polygon.getNumInteriorRing(); i++) { + mMapElement.startHole(); + processCoordinateArray(polygon.getInteriorRingN(i).getCoordinates(), true); + } + } + + private void processCoordinateArray(Coordinate[] coordinates, boolean removeLast) { + int length = removeLast ? coordinates.length - 1 : coordinates.length; + for (int i = 0; i < length; i++) { + mMapElement.addPoint((float) coordinates[i].x / mScale, (float) coordinates[i].y / mScale); + } + } + + private void parseTags(Map map, String layerName) { + mMapElement.tags.add(new Tag("layer", layerName)); + boolean hasName = false; + String fallbackName = null; + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + String val = (value instanceof String) ? (String) value : String.valueOf(value); + if (key.startsWith(Tag.KEY_NAME)) { + int len = key.length(); + if (len == 4) { + fallbackName = val; + continue; + } + if (len < 7) + continue; + if (mLocale.equals(key.substring(5))) { + hasName = true; + mMapElement.tags.add(new Tag(Tag.KEY_NAME, val, false)); + } + } else { + mMapElement.tags.add(new Tag(key, val)); + } + } + if (!hasName && fallbackName != null) + mMapElement.tags.add(new Tag(Tag.KEY_NAME, fallbackName, false)); + } +} + diff --git a/vtm-mvt/src/org/oscim/tiling/source/mvt/OpenMapTilesMvtTileSource.java b/vtm-mvt/src/org/oscim/tiling/source/mvt/OpenMapTilesMvtTileSource.java new file mode 100644 index 00000000..f61d5320 --- /dev/null +++ b/vtm-mvt/src/org/oscim/tiling/source/mvt/OpenMapTilesMvtTileSource.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013 Hannes Janetzek + * Copyright 2016-2017 devemux86 + * Copyright 2018 boldtrn + * + * 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.mvt; + +import org.oscim.tiling.ITileDataSource; +import org.oscim.tiling.source.UrlTileDataSource; +import org.oscim.tiling.source.UrlTileSource; + +public class OpenMapTilesMvtTileSource extends UrlTileSource { + + private final static String DEFAULT_URL = "https://free.tilehosting.com/data/v3"; + private final static String DEFAULT_PATH = "/{Z}/{X}/{Y}.pbf.pict"; + + public static class Builder> extends UrlTileSource.Builder { + private String locale = ""; + + public Builder() { + super(DEFAULT_URL, DEFAULT_PATH, 1, 14); + } + + public T locale(String locale) { + this.locale = locale; + return self(); + } + + public OpenMapTilesMvtTileSource build() { + return new OpenMapTilesMvtTileSource(this); + } + } + + @SuppressWarnings("rawtypes") + public static Builder builder() { + return new Builder(); + } + + private final String locale; + + public OpenMapTilesMvtTileSource(Builder builder) { + super(builder); + this.locale = builder.locale; + } + + public OpenMapTilesMvtTileSource() { + this(builder()); + } + + public OpenMapTilesMvtTileSource(String urlString) { + this(builder().url(urlString)); + } + + @Override + public ITileDataSource getDataSource() { + return new UrlTileDataSource(this, new MvtTileDecoder(locale), getHttpEngine()); + } +} diff --git a/vtm-playground/build.gradle b/vtm-playground/build.gradle index 12de8206..34d42e2d 100644 --- a/vtm-playground/build.gradle +++ b/vtm-playground/build.gradle @@ -9,6 +9,7 @@ dependencies { implementation project(':vtm-jeo') implementation project(':vtm-json') implementation project(':vtm-jts') + implementation project(':vtm-mvt') implementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" implementation "org.slf4j:slf4j-jdk14:$slf4jVersion" } diff --git a/vtm-playground/src/org/oscim/test/OpenMapTilesMvtTest.java b/vtm-playground/src/org/oscim/test/OpenMapTilesMvtTest.java new file mode 100644 index 00000000..fbc462fe --- /dev/null +++ b/vtm-playground/src/org/oscim/test/OpenMapTilesMvtTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016-2017 devemux86 + * + * 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.test; + +import org.oscim.gdx.GdxMapApp; +import org.oscim.layers.tile.buildings.BuildingLayer; +import org.oscim.layers.tile.vector.VectorTileLayer; +import org.oscim.layers.tile.vector.labeling.LabelLayer; +import org.oscim.theme.VtmThemes; +import org.oscim.tiling.source.OkHttpEngine; +import org.oscim.tiling.source.UrlTileSource; +import org.oscim.tiling.source.mvt.OpenMapTilesMvtTileSource; + +import java.io.File; +import java.util.UUID; + +import okhttp3.Cache; +import okhttp3.OkHttpClient; + +public class OpenMapTilesMvtTest extends GdxMapApp { + + private static final boolean USE_CACHE = false; + + @Override + public void createLayers() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (USE_CACHE) { + // Cache the tiles into file system + File cacheDirectory = new File(System.getProperty("java.io.tmpdir"), UUID.randomUUID().toString()); + int cacheSize = 10 * 1024 * 1024; // 10 MB + Cache cache = new Cache(cacheDirectory, cacheSize); + builder.cache(cache); + } + OkHttpEngine.OkHttpFactory factory = new OkHttpEngine.OkHttpFactory(builder); + + UrlTileSource tileSource = OpenMapTilesMvtTileSource.builder() + .apiKey("xxxxxxx") // Put a proper API key + .httpFactory(factory) + //.locale("en") + .build(); + + VectorTileLayer l = mMap.setBaseMap(tileSource); + mMap.setTheme(VtmThemes.OPENMAPTILES); + + mMap.layers().add(new BuildingLayer(mMap, l)); + mMap.layers().add(new LabelLayer(mMap, l)); + } + + public static void main(String[] args) { + GdxMapApp.init(); + GdxMapApp.run(new OpenMapTilesMvtTest()); + } +} diff --git a/vtm-tests/build.gradle b/vtm-tests/build.gradle index 5e466bae..cee19e71 100644 --- a/vtm-tests/build.gradle +++ b/vtm-tests/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'java' dependencies { implementation project(':vtm-http') + implementation project(':vtm-mvt') testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.0' testImplementation 'junit:junit:4.12' testImplementation 'org.easytesting:fest-assert-core:2.0M10' diff --git a/vtm-tests/resources/mvt-test.pbf b/vtm-tests/resources/mvt-test.pbf new file mode 100644 index 0000000000000000000000000000000000000000..c5752ed3b9f151d453d66edd48e1710e91af017f GIT binary patch literal 31199 zcmbuod0bW3xi{Xu*WPF?Y-CdtY`Q>&$A58{14Tlzh*CX6vi#JhU&8y*o%s6+@crEcG&X+LW7yj&tvIh zOCJmU_Ob7L=iA4YKK>nFp#-Sk`A+Dd7e2MfPk)hbU+9?dy>4Me4tuCZ#Wqii$5VxQ zMf0pLpomjZw!P36X$kLd7+BW7r~g3z5v#o1HBi}qWMJ9A^8P~umF_o}*!(TNXAT!G zu;)DGAs-<`Ng{8eGdk^=X>TS7D+Is?5N3Ff>y&Wh%?0$2uE!+~; zyXmjq>~Oz#)csze&Bx;PY(b&*sn;BZOXd~iyB`$UyeyWd3XA3!SQi!)I$XuG?KWSF z_p|mw>zvr9WEb5doLxjU(FdJ-@kQ6>@lO`ZPdZ!7Hs;J3Z)1+#oT{Sv4(mVKUv)Jx z$6@ofc+v#Up69?6y6bT)uuZc>^=v+0+HrU4xld=)NeS7CYAzCKfyL=Q_rV7ZW{vaI&wxud%PbueHyK-$Qg| z?K{}l*4N?sJgB`P zTbLzeA_ZG7l=U9Hxa-2k-eVUwy5B0W1z7w>H!K_7)_!aI)?4ehT5oS?3AM;gx4>SU z=bqRFFPe+z096%RpUoSenFYl(B;U^2{+a_58 zsc&-sVOMRHyU)F=@)t<@!LmE7|2j^21mUJU68*a!;@oRQfPA7RoV# zP#x|kjwNTxT@z4PB3Z)UZ1e{#!QH8%`L3lUTAZ+@ix!ys&9&2CcK4|06&n8c9mRRB z{y3Jn+AJ#k>*1TsXS}6XX`mQHrxQdp-)=2>xwF;15XJL|5(nGf9Bey0czo$#+gpRj zmkqWZ7;M`$*w!-G=H`RNg%kLo-THmU1U^_Y|8M!=h1KAM<9yKFl_m3N*{`oWGIF?P zq~h>6P5xqzYnZ+zA>AGQX zE`*1N#p3Dd<>l?|qxkyx`TF|>_y-0A1x^Z@JZZ}0U>-6hG&n3|YG`;^#MH=eYebY{ zi=1YSj+$$q%7mHjEAr8hLj+Yg=%Q{cF7k&0(>eMp} zO2C81x8xl6Msu%4Hds7Za?h5Y(~`X(!tZHUOB4CItI2}=2dv!vRyxN?Y;sWqNEfvb-36$Ec|t;u5dR4;PBUAWNo z_PFRSEOC_-xvMU6*I4w5`;mKMi`cK6-)5A;k zR(&kKs-LGn4^RWWg7_rw$v#tjgZ)C(P&LedYCw2kL{Q`;s~$DkHf36Hv^rgl35gAj z(`N+6dq1KkgeCGv1Cs)i^_e0?ofVj>r>W^vGXgX9tnh3#N6l5g8$nAkyPzb$xNvc( zkyljk3ww%9v#3A*2`$`@3kr)%=7O_;RlD@kBmMjOPtY|91o`|G`+~g1cH3l2P;XuD zVTil33+27dU>hedY`jqIDu>?rpuD#YJ){azJJpu=8a0QydUo`jK3{r%8Mb*d zG`W*KhkA}o{HEmP@o%VydG78yT-|*v-}>Wus1W;dZs*>-1yX>=y2|F-Q2;J-$%);$ z=Tbd>*InBBp!&}G1$lN`uw`;zWnbHeOZ!gax2$g$RH4Sc{eAWB&UT)l#-K`3&kJz9 zpnx{g)$(mu%lBU`UwO5x5f?VAC1R+2`%pP{A5@#2L*=`M%GXH+2Wk#fo3&EdHSHU0 zt{QAQjIB4=R6E#QI@t6kZOP^}w83vzH{N=y<@U>dQo-uNKoR6bNAM6)fy50sNw@r9V5x8(0Fz9b5)_Ki2{Z>(K*ebr|7et7+b$|uFOKX!NjuYe^=Cwh?9wnkYqAW3e^7NT8Dc_xyo0^>#mHrPI-^=`e zvL4Hh%8AO2!hQhpKlwNj$4~R~iXC&TPlJHw6fAU`W&;)YZ`nY3|6XaH4OC!19OysP zzn8e}<9US(&Tj6iTbS2XXMMU56MN>!E4B#B)ZTR$s%bw+L+l9lLKXB>2v#{6(=F3_ zxAz|DJqlsl29bLdf1oa&AX5z=mEzT=-VWkR&li-~^NSq|982aU{xsj74;utK^Jk6) zc~GDpFN6}1@1lU`9dq*?RvLoqjrpDR@{P{={5%H@@B5C$h567~f12-@lQ)-2UMMao zd{I_{SMqHkmMK4T6gt4;=N3DPtWQHd%bSb)+4F2smdKtrdrqBS)zjW{fCR9$XC2vA z*rX?Wj>6BiaMB5bf`8=vApzh2mppE!KNy&Iic3Y?=_+RGD$tx;eoWHo(I==*l z(_DKI{w|i3_9MMcb5s6BGe;phL8h#OR{jTb7dwhN+ZPmrUJAiJ78iBa&!rypE$e%$ z@1XUACa{tHU?gN$5eor#eZ0_7OeT~$L37OsDwd`wnMq{_$xGurMKiD;^NL1o~xlFB5%on@Ih*wiSw%i+PM(}PWQrkqb2Y+51Z z{+sd{7|wE+8UW4%(KfoN^s}P}NSQYYEI-d)ShCPc1Xi+;+9M*tg3P6%=jS`-7j`;{ zM)1M+^X7DQj=Q*+~5{ zttNAH*c)=Yj`yeIZzd9}{lcOGkO?ph47UDEM}Z_1B7QFU+%BK{58CdGkx4 zYtOL6_U^i{$=u)&A4qE-{g1|Jan*azvQ+}Fm!fHkHba&Y0UG_m8$y;~y#{tJBf zarFuIeeDPO4~-}JKZ>V3{>k#R=QCc&#(1%_ofmO9z;%wDksSeFzEZY9b$X zfH%qFGh3tW)hrkvEdi+)X@a z4g8q46V>{%gD4uj>4im|_1H5H?sm`}x(b!V3HG#pyninjpnr16RKenc1qFpgONhmw zcR*}T^_>{!EV2=>`FvM%bMs9Tq3rn8@>+?%G!x4mDhJP~mx{^5EQIvmrZ&k8ppvXK zxdaV$w9h{lcOW>_*HQuAidmph=eHt}RaMvd3H}tP1UD_e|K` zSE$LH%_a2xgEgA>V2!YsNmFz!&C8uqvvTVo6BHBdAHocvuxzGYm;{5hM*TP5I(@zR z04W_OOyb|3x93Um)#&a{Jz$Rl>VH*zjs1tVSYKlNC;u<;-yZ+3<(HmI6|YjoyHxQh zQ+&%6zh#R5YXKEX;BqBsg)(WSGI^CUrBVs5QbJZMp=*?|*OjT&O88nO;teHoonrmZ zsP&4iMwzxjiGFSRMkVG=C3ce%w^^A{tHiHV9@(NKY*iAsDUYsIlC~(x|2cEJlJeiP z>Xg*YN?NIszC+2VS2CAm?NqW$m7H=Vw_GuI%2Pj}iFm5eAteYVxPJmrjGY1kbK%3Z zRiGt2ga}tb0jIJFkdWLiw16&*m#Cb?n_LPP*yJuNkSmQB38;+k$45-R#$(`q*N1LW!7LXT_d*VTNd=&Zmw2lAA$m)sK3)o1&$OgV$p9g+Ns0nM zVUP*X;eH9C|(B@?}LiZA;s6J_#IaK_bUNM zl)xq>s9BlRqD(%jOlehuk0~Mhl+ZRM?6@-Zgc81Ai8!f5wky^>O4KRE)}c&0twcMO z=?9gVx0TpmDRJ*8GxjU-oysEzm4q`&;(q1P{Yuh)C3%lBvr9?Yr_AbBQr}V14l3!t zRx;jIGS4bm?r}q`9a@5C?Q;tA=Fr4LRX+qzN@f2cq9CBLhv6E5Cf?gJ z@g7P@3(;)PR^7eDBDo(XK!OWFadVIrwDwE8!9H;!~1ySZX@z`N$^fA#X# z?~Cr0C+7qlQzSTXPGRpW+Brr4jbijD{G1}rDIVt(%Q?mKoZ@v(@jj>coL79$DSo|* z|2ZY#oDz6W2|A}tI;Tv&piDWZ1YcA_&MBeil(63_Q_m^k=ah&Kl*m5C+M`5$sMszk z)B2U@bISB{O3Z)~ds&I=QD&S|;?F6MoKq4$QWDQ8kDgPK&MC<~%FJ_0$~k4$6(zMt zNjs;cf2?GjQ!;y$tlufw=aigtO71yYd)2Dhp0WJX6ZRtei(o6W?bhL=TZfOX8g6MG zKKeQ?%=`87W(_?!ir=lK)9p}ygWLbM2Ncta+j?>}ND$=jJ_Lb1?P>}vlfaN#D0ihG zvLh}(9CTZEdGE2mD#zCkI!a1)Pc6u=6@S{HcbtUp>J;2qM~IA{wl9$b0($_NUTV0s z5ta-w!Uk9|X!irMF;3ynPL~%49vqYF?Y~-f^lJG=7uPFu0TY`gH9`@9767|q-19=^ zSt?h5%I@DYGR`Lt-rF+HpnB{x_ zefS;I=|r%Kw0lKuxJ<8vkRs*?4E zl0Bm2j3~JyE}r)Nk{1_1`ve~VN83X%zT{vvz}-M>Kp9TC?kgqn0aGAO`2CUv4oFXM zGWx#rfE2DMT%0fEb`9{hik>Ekw85fxzX!WrS{gvi9M;a#;(2!K69Aonlfo8%T$VmI zp%4TLen>qJvN)X5zG+im`R7=jqm_& znm9Ia=Tr2eM5)JV5ymVb*2Kzgt*Id@+$}ZCAJE2^ny5KrCXaMoo7KAZAyw;|7~mZy zrv#6LWR+w_KBvu+QEO!*ouN1F4iuWDG=g*4o9mQ`<@&8f@xT6H#QG)I$lkO;!|DsI! zS_!_dgxpa=?x!pD?|rD|lUYAsWv%2nGkby|fQeMgyoM~PXk#;#D~R;n}Z zDDiicNA4&Ica+3CO41!AxlEmTM@hM(%vze7SXNdHfr{{1$jkzi`*lj?n&kLnY<r}65)w^2tsZo8`seT(&|21krwHjEh25nR)RjZTVRHv*{gEy%m>($WB zYFMp0wOS3|qDE|0Be$v6?P^q=YTKbst5>7fsMD*}n4N0uPBrcgHNIL+*rg_}Q6Jr+ zChb&{x2rSPt0}wGSzFZ9H`KIhHQlLZyrpKYRkL=h+0|;!IyHBlN+g9@dBO2&-U7Ib zKsS(}L{pXER+XSC_e0nwL{#+>1lb7YwHgX3sb{hT+x;Z&@kY-s;EGKWUp#55V`kl^ zO-;?nxqqQ^FSTeo;D~58OY*6qD*M)axB}GD*mu-~-(JXjHE%BA=XiFt?Bvz*^%8H} zX)+=Z3G8FU8h{JAVfyi&%6?H#rELKE0AzMl(hA}A6187WYdX5D?bh*SqZ`_Z!llIw zGNEhPc^4ERuVFAhTss-bG)d^>0uw}d!SR}$O!w5mGM+F|z>yA1zD1@lBqV6;=wRDU zl8Sh1q<;JLlPx1F_kMb!HlWL-uRH(e@V|<|RqZ=H0-9Y?ut8;eRBf-S?^BKaDsNCl zgX+^Zr!2@fYgq~-Z?@2%G)$$Ej%Nnkhov@`?k_Vgi%LU(xrMAWmlHUePZeptDlMS+@ z)-)MsPy>XU>|ROMEE<3qqPB5CoJY2}$koY`!WmTdHzRVtCkHd11~Y@??_ZB?3ANSh z>^*v+)RtsPlrTsg$N}AK+b&i9qwca&c>&Lj45~Q3dIpgR`V#juIMd)ym=RDUy_D^< zKYvB6-L`Gm67Zs_D!rq!PSxX#>eZ!scdI_XR(-ov|1LG~T{Wm%4LPfZpH(B?Q=`tQ zw)fTOE;Z&iYHW`he_s7wubOZ{O}wZk|5i=wQqwTW&YEGA$+a=c&VDR(y z7wyGR&TEESj>vVbGj;MKgah^;qkc*?+H9BaA0>bP4j zto@g)P6O*Uyb;k8u^JDHK^K#)M@={ zbiX>iUyT`7W3Q@l1L}-^HNIc{-lu9pzna*uCiSby1M19vHKkvjHKL{tsA>Ib`ZYD9 zU(Fm)v#zVz{c29Xn%ggz7_|Q!eBm_tP%&2YA0c#C1`Dk>!xMX3ti5Xq8;5=YUfH|d zbuB;a+(BhMb+F7~6w!{N$_mU2pq^AM&>?c`u7$#Ycl$QL9*5gSu5TGsO#*#rvfUn8 z6aL&p{KVas_2L#ZkN8RG1Xbp$ua^rCvq!3$ShdkjPFOocSJVTnxV$=33dwKwbeFlJ z*kE#x(Yi_{^7{JSH;(SVvFSj-K9dvg`LEqCi4U9G%9VgkE>3(yWj9s*Gu61I@*Aqh zsA{>PdfrgIZm8ZjRG-_b?+w-O_p1LN)PNgm;0-nCh8q028gfGo{ev3zg&O{QHR6sM z`A5}ySB?6UYP+XSyP-zkP-DJSW4}`4ZmID%)Px&q;-A%|8*1_`b> zrjMx^H`L5qYSv%W>>Fy%4K??MTsP1ncthu&{MnG15LU!uWe8ET8=A*sM}CnTZ(Jv5 z5Y9}@A;C=GVes?NtjaPs-Y7q!DzQ|qYX`kc9!2R*Jk`?%kK&OD&m{(9zH3VQ4blLv zmySGq+V?uvyAgi3R_KO=|6#GOmSbDP9p6lWhh;lWeUL&4rFe&^hp7RX4{7gm$3vc* zSRbwTU{k9U?}y0V1!uW+cz~PnyA@$ z!!@V-WjOIgH97P*R9m$IlzW~*Y;ReFC$$Vf^hxy#0#nZ3=Y&!NKd9 z*jsa94QP!5Q4qr><3z_FT6=3igbyB)KYIt6CBXWQSbMhNucM&7W?(L0GE_*+K}L)0 zfXBGT3LAGTLPpjDa)Amc13o}lr>~Ynja)&&9~Akq)D~-*?vDHbd6Ba=QR00dH|UgR z3Xw+D4|o$&5{m*Dz7TJZhMwhI!R6?;7T_nfcZ* zzZ&LW!vbnpU=0hZVZpU5w1!QsVc|6_VhfAh%A&S0+jbUR!(wV!Y#oc+#NumMLJdo- zVM#SCc?V0WVY6yj>L!*}!_w>8F+!*XlbcXu)3|8R>gL=3?^`$7V+ zpf(&q%*OKm6A(5H0~OY|{uur0fx1-x1Ep_5YW-ycC!>L z3#EiXNU~}txUFX^0J;N^M(toy&F5E;#zAjGzt{t8cr$>xbutorn*`^Y`u0N_wGbA9 zc%sALQ7!bgm-8$?d&k4h5&?sBLOD{W^BSCAR15)-Ts|EQBNsY2bQWUkO+`6 zELhN#>D@mrc^PrB!KQ<5?WJkit;1_Z-&{tN^kC}1%%Bgu?(GX9-yY1YW1(F_gT#?c zV|Ldd{0?pP(!yOKsp=qZX=XodKXh%=>(@^o4(Kq6XKTryBgMhK4L=PyK*Zx@>@BA4 zW_klN_Au^b!pS_G%;IF8PUhug-cIJTm-#xGpOg7JS%8xTI$4mDO>(lyPBz8Kg7>iy zCku75u>EYRlZ883L?eqlz^n&Z)FEa&%%&Y-(M~qq$zqyVY%_~H$l{$W!O0Sx>`^C6 zav6||m+VEwG*#eC%SYL_7( zk2^{pd`n6Vfoe`ThBr}TA2fk<8b4ePL3awOt0bY#GU^PLNeb}p;V#2h{x4-alY8Lpc%rA5$o z$tHFY2G|PB59mDrJvM+sND$v>*@iglYt0*gc92$k+N6q||6Z3W-ac|?G!Hv$QhL6U?iXdABm3lgzi3`L#3uRu<680$W*7E1T5HCLd>0 zPO;z)7ShT>TUpp?Hno+7x3Y-0S>&&nwT(r+!)%>w+8Gwz%3``$Y&VN*V>4P=d@D<6 zWr?jUsg)(Sv6-zbrIn?&v9wl}{%e-e$}-zn*1IgbmF2Xu+*U~z(Dr_oH&@~h1EtV~ zP7JK1#YW86M&c9ZHMmAIg1Ar)#pV!1%W1lXp1K+QVFO)5YuOK;Am>?v5$#vYq-)9z zLtyJ~Bf@iI4cO9^q;vve_^f#cX~5_PRs;Qcoccqrnm7Q3>Xn-PkbUZ)&#t!~xVCa- zK)pHt2Y=L-F4`||SkFwJewMNKn0}5K?=yaud7NdIv&{1>^E%7C&oZCiFyFJx?=15_ z%L2}_z_TpqEDP>op=Vjxc@}<_Mf9@B3(WdHi@L~czh%*9+4Qq4<^vYn$KubjgtIL1 zEK54elHX@D&$3x(S?c>N?JP_GkY$`@nU`2rKg&MLa?Y~cvvP(pJC7F>&T-gF{)4y> z=>*{1hcM3=G(9Px;@&r~^_D`~cew5-*@^CG-xieM`vvADu1aKLkKBU@GbI^+{&o3~ zUMrBi4jYcF2n29bb=Yte35pnPnpX)b!hei->vke)30a%*osj3-Xg|8)LNlNf;=GEs z$*txV#BtuLUMBYp8pDbKAaf0{b1(Wu-%jMJw0P1@cddCRWhgjn-P+#e|5|Dj@0E6y zeQ#Xb8({1*(>`MQ6=r zz$Oo{DFZBcn1u|m&;b^9l}#OB;R7tWYur3|oH11$ApmNvlBKVumKEc0WQb&F*Wu$%#wJK$c_ zCpw$4b}!19)qyfB=`jfFy<`hMO^f=f-5NI@?lVL3*ji~KQc^+_b{0Wq?h*uQnS1St z&L;USE$0(d4eG~>J+3&+?VY)(gk1}?fX}@RsADHnuSoA2HKFVUgWwA@OxX)SqC`or-M%k=UmijqM zE7#JOX&Iv|vrNmX(6UEa&M3o1QQav2KR6O&i-cN*EDtG*C4&# z2{1%GILfjM8zt+M*S&4TUa2x-p)YjSztnkvo&ujf0!e;|7!sbEg%H+jA+lu@Qs0`s zdWh@&wC|-K7IJzQO|vvRVTD3GZ=#3fZy;HGni_yp^>$4=(a+!Sy4hOZLQ>VJQwcJ8 zaW>WK_LD*YAAQh&j0M%+*tqxRw(S8M%tdW``osSg-_+cxQUc0pRhMgQg{H66j8&RP zrDmzpJS#P?O3k}c^I5I=R%(8gn*SOtuu==E)Pi5vLMpY;N-eBf3$N57)@qS&Xi@7l z+j=d!Qj4k7Vk@<{Ra$(d_PrV{p;AlSpe1e8l2>VIm0J3nT1KUoxk<~~tYt6Paw@gl zp09f za-lau-r{9!UGfOLK?o7ugR7HAU?XK>kgX16go6$<( z;o6}MBm<7M++0=`&~C2Z?t+d-#hI~BmMZ41tku{SO{>-Pt(vh-KS^Kr@S}mtm%dM5m45|7o0yE}0pdA;|a1o&cC=h<4X0ps^4asi1 zyu5$6EZL1Rtci@=Idx&hg*6m4i{GUaO9}f%X(dWs^-}~pTx`--GtGIpA&(-U6Gr~| zQrBB&R-qS&O~XEV7AT^-Q`p(Ei^w7&g&qdoj!U~^NeREShgy~KKCOMnp^QTf?|bXR zrL>+`%MtYh_FpQU2FE2Z&~6%ZX6z5ZfW7YcOK9WJ&nXn>Z-;@wET!+rSaU^OATtCi zyIV__Rvr=Zt%g0LO)aFTFO!)a)N2INgVOL2H)qz!S%&w?oE3SGSZcxp2m^rSwpnWJ zh&MLFCm*;5sETV_+DA^EbgAqvy4zlKbcXod_Pc+!m~_{uu>+cRP}2`-#$k;&YNAo| zXw)o?n&%PCt5NfA)O?yW-$u=^S@Unv0vffzMlGmOo7AXHKB`S=)Ph^JkVY-EQ42e! zO>NY|8?}fwE%LZ#J)uRN)NJkAv{PDiqZZSl#h%vUPH6FsT0*0i*r+8nYRM{PfH=;8}Em zL>M^kE=ciieuPrmcfbyGd4z)*c>hkRTBF$xjh% zkE+3B1Q;fOXJ;|N4I9_);zm4uWrq|Tl4v~ z=G(3Ly{q}3)dIVjzyS0dOn)Q7x>NlFLM~m*(V$N%^y;|G_Exud( z-bF3pw_0Mimej2!cWEiz+N^FZty@d~K+EXUGP|^_54G%fw481&w_C0;%-f&XDYqSq zTqk8`wOs1M*8XDz{+NLc~ph`Ux;JGY=I#>FRJ4!nQOABX1^56)-X|DoJAh!(_|J+9?N4$TqPY zB$1gc2iJsYlYbHr0rm)*L((wOTifxGwW}cT06rREGSVaY&A)%@5N-FC9s9w!x8#z> z`ZaAp(=ThrN1C{zd3>x{u4tZDG_Na~_Z7|Ocbe}fn%|)2Kcod-(SojM!NXd}6)p6N z7Isw&|5S?@(IT&D*6Ui-4b65_oA#L&{fQQHON$-V;%;m4SG4c_UQ4*5CH_H6x}qh2 zq@`TZQg3T%SG4rcwTv&c%#XCJJ6iS?E$0(0_Y>{Af7ECgGAoeyt;=5BXWFYWX_AT` zX) zDHpxkm0L(ODSKCBeF7c|YME}a?DQ?!p)v`OkUmI<@Zgk@~WzSvt2hslZ zr*#D;ymwb)f6}yjn*OC`e5LWbnz*ZZ+|?|1HP5@6*Imu~uIBS+&G)Y6cUSWt(*o{l zfp@i_yV|6?+T^?1l)GB+U$l_BTIgLZ>}zf6T`l~s7I9yT{6@2WsYQLO*_P_lO7-Zw z+Vs0xOqm{gPmB9Xi@&QS+|?5AYDsssQ`FYT`j#_&$z2)eyL?G z)3fhtId`?(yK>3E=wJuVCmJ6p2me1ZP)^D|j4fH_vNTW7v``t6=FBf<(-#QXBp~4g zU89!S_Pn|C=-OpAHqbRfGs$2)B$uiT3IY(GL*`PA8;7`%ihw)p(u5BvqJY|kO?bPs zalDPW0BK#l!6Jd=s9!E7?mjPpR;!fWf;~4)ncEA*HeJ$8d0w^E-`p~qL~2^D%` zg`QNQC$H3JR_G}e`m7at>PkJWLQmhQXH@8!EA^~5_3R2gr$W!I&}m;GJ@ct&iBO*^ zLO^1{OE7ry$-D(iZvysq1bLaJ8cDn5Sx3GVo}r>M(tiVAT!&N+DeY=I&f|cCNJd7Q z8YO-3gM7q){0!dQSLyCKsDwxWq!y?_L^4qb3EjFL7QE`d`+*#&eP}1tTL$l6jg?2^NaHGi%Jl;WDaJ$ZiJ`Mm!KE?Rb;mp=FuCSuVc8b z)*+;yLpUY`iS^leXcNJdDEa}4?5+}uuOxj{9sy&8+UoL!KUZ8ZU-k?1ATtwNiH4s= zYA12?r-}+_h^e9xJ2Y}^x%p|`F zO~QbsCgChNMYM8rZ{xMny(3Lq+;LeeUwSK9e7$qyM>&YgLh8jPUEQp+T3y?s>sxhW zo6fiEqE7eNpHfR*fCfEqj~=vFpVXjF-ltF5uLn2k zAqVu(20iScKJ}0ueprtw@sgMT#rAYKhmHl zoYWKB^`ujJa)&;%UQcPzXPwql-`3MM>*>GJGv3iNJN2wHdiE|oXS1HW*}X;o=?BDd z{)rB^k*6EM+eUj6kpfiyL}zPZk<@G8hm~p;7Gt9f5a@W6e6y`&zadCoW}2avCCD#b zLZtG2(me2~sc^NDvL%f<3F4e-p*N9mj2E9IrwdRRkkR)YojVGPkg+k3?tr#81Jy;W z0NwRfk+MZm0sq>(Jr0*bDw9OQ%hy(+%o17Zpjfvds zSPIpgM*A4f$uKiDT)mez7_QH($57zvM?ZS2$7>wFljE1l61;54`*g!}6x}EFB`8az zosM@0Qz#|!DZ~U!pr_HIbEbnBtrde}vMYPTNV ztw;P;kNiNl_UTa{>b6Vzw0=FhTaOvgV=wD*AL%o?_4sc6k#0SqTTkrPle+cfK7D4l zp3<$)>ef@Q=xN=0`p0@kx1RYsJ?j%ayIarc)^oev^z`JD#1Ee=L1qi6lu444tPLawwH%m~>DWao*#xS?bYI(X z98~6xfdBm~AM2v=(+|GL31}sjHK?mYIvduttGfQFZj9*snl6TPk0ISMqHSO?mMLW-PHYu^nf8fa7YjOOrJEQPrjv38PbDC^^hSw^tK-MdwuGV9zLW;{6UZW zT(^FqN8QnFf7GYl)uV^>=|g(VpP={YabM~)hV=L${gEL(VMtFL(vybt)Atk&XArvB&iJR`2upcPrQgDJ-}XwSM@_5SO&y&8aMy}kcefX6+jcf@a1_M5=GhoEOIMx zKH$S|xy~6OG_sLMK}IFx6p^&AVZkLey`x@AoF8q>YUbf0f@ z-!a|qTit(54;<5j#`NH&M#z{RI;Mw}8sTGlM41s;ZbU6JY!$||W%=>y)rI9_R=ZxvOV{&Pw$f=)$ z^GGm4_?akvs9CDZb{sn)`7e^SkPTRcxLM-A@{SS$=%pvzFG0m`lm`m75yN>J#Y<$| zA-(V)R5f({^)Tk^sScTj{#(cY6)oMZW5To?stmT;(AF6G z>xNNn@G3)886Im5OO@eSWq4H?-c^Rr8-{O{;kVB4uQCFvjKC@*XuUDH%9v7R1lJfL zRYqu)5w^jYT4jW9G$P(KA~zXPn+;p7F>Q+xU1dzKGGewGvD=KeY9qeNNZ4*9))|jh z8A(+}ap!fJB+j{BfZ|ps4_Bl8d?$Lt%E+xU$a26~9<#zUE1{zW zppU_=&|q~LdPsG_c5jf{9wI*zW%AxqxnW(xWwRY(U303$?( z3xdZ?JCL^hDH*m%OGEEKFToejm|svA#_tN~XaKy1sydG&dK7(wj{-qvrpJ*^5u+1I z&;rt3>a8v-l1?OQmAMRrIG63D*fX*zL0%9jRaeV*(Mc9E(2Sza#tlsH3{v4ibQ4EQ z2L!oCi}b_6rq`jjg9hP@gkA$dvxlP3u<;S=OpeK2dv2iua|q_}B;5Ged0MUauXkd_Q%SI}M+`hOg7` z+h_RiHv*hSpwkFyG$uQZDF=+;gGPwc2z44^hm7$3M#Nzw@`zz=GNPIdTZ<9xG-8e# zv8_hjF(ck-Bsh)4HY3StBsUo|_Zz9ljWnl`e!|Fb8ktQ-)=4AVY2-MKT&E;FlE4nf z9LZm0`pcpICNjI8$ur9+rHW2zrRl_^we=o%-+`iz$=HN4$zCX_ZavrwSdC14a)P;l zs^x*2fD%s7G|2_W>LLsQ$7Q;p1W19UgA`H-GmKmZ#3YbROeqPVyJsA89Hbzikh)~@ zkY~>9$0ao-yF}fS+yXu-D4*aK@Di-1PmokHQgIwv;u4(O`r63^ai{;Xg7v{_v>WV{ zp>-JgX~THi;O&NJH$2)6OS|FOZg{mD-tC6ZuMFRI!>`@&Z#M$kjlgyzsNI;yOGjvq@FX<+Ku%0jf{39^R$um8zZ~j$Z0on+vWOTzx~MZ+H8b% z!Ne|ERFH4|$ztqCo5f08VPNGzIV48&z%pz9E<*AKmRtJ~j*2JCiOherxMaS)Xo1x( zKeQv85+C+%1K)U^jc>^*74&uI!?wT{07>F}Um z`F7c>66x982F^kp1}VYNQ%}-H9)B~DMvy2+fZid7;R!pAR&pQ^?MEec*g#NLBtzK% z_Occ_$W|hu@vm*s7F*wPuobYE50}a4vc^PsuFL6c!o&}bqvR+;>5*I_d5qLdNTT69 zSA?A50!#?32nV2n72*1{>%fszONyKCfB~5z55le%oC(6u1S+@U#)P%aP{&QSf3W15 z@%DrsGy|}nC>3F8xsZkm!Zpc1w#N#t1z6L4XbCvutyL|fC)SQuHjZv=F(YT`028pA z9~HccV~mh>jq|h?;Hs!(5xfP!yb(1D4gN=Ya#;aDEW&wH)}O!wSU@betDo2&88_z& z`bHsk9vI|dnaP6-VCQ(p1kqR18eVT%ixlVUEjy4FBKP8rSHH~=JO6NbHe$T67kdnL z-q3mt{eod!GcHC@b57KdW^sxBdEui)MHHU zF{XTI1Ya^jdW_J1BW%E!+GB+G7!j9^$d8PuD~9c3W7_YG=pG~H6C-xei0?5HdW^&# zBdO0w9x`V37%4qQ>ada4W29d-GJ1^6AtUQkBfH1Q=`nJ9+~WC}Je;bvsGvY9;KV5r zUWqIpY&jVnX+AEIrx8k?*(7bE)0oqf5-55?1hWw72r5Ly#HhTZ3Wo$F+ zKoEnp^N@3f-QZU4>0F4WWw+NYC7{qfBElR3|wEe_uA3j?l9lw1s}zU;fw2DGW`G}2D@fx z*A4xKVcazMh#^J{j}gN%Vt9ULc#Rm|BZki{!*|5+8#Vk#jDQg%aKs22F(!=|lW!YS zes2W-!3Y^KLPw0S&yA@gM)-&k@r4n2$FTm7%}7%3yhtPvyi&qmsakv?W*j2M}JF|xikvPX=Z5hHg*QX)j+ z4@(dN05c5=4?=_}5xPa1fn+N?rJ}S_T7DGAyIWEdMG%(5J~&C!EU9D1rM;jK&`jS( zNf)hs$E0BgF?tpGgk(uLk|@A%Ocd440FhKER~@8%`;GR| z@$8`^J>} zMsO()xo?EtH^R#J)cZ#GeIufrM=s-072LL*N8dN5-#21b@Yt0+{=SiL-$=Y~B;7ZX zzcpswH&X5!soxrD_l@*bJmbER`K^&v$+PbpIroj+`*KMz+~;w40g^XyMTfXR_5vqI z85Q*I7)SDVNLUps_=FqB%E^?h^8 zF<`##oznK|*M=Ca+40Nq$mc4~R )*I(yGHRo%&M-{hJanH5ftBQM9ai2H1Zx#2e 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. + */ +package org.oscim.tiling.source.mvt; + +import org.junit.Test; +import org.oscim.backend.canvas.Bitmap; +import org.oscim.core.MapElement; +import org.oscim.core.Tile; +import org.oscim.tiling.ITileDataSink; +import org.oscim.tiling.QueryResult; + +import static org.junit.Assert.assertEquals; + +public class MvtTileDecoderTest { + + @Test + public void tileDecodingTest() throws Exception { + MvtTileDecoder decoder = new MvtTileDecoder(); + Tile tile = new Tile(0, 0, (byte) 0); + ITileDataSink sink = new ITileDataSink() { + @Override + public void process(MapElement element) { + if (element.tags.contains("class", "ocean")) + assertEquals(4, element.getNumPoints()); + if (element.tags.contains("layer", "water_name")) + assertEquals("Irish Sea", element.tags.getValue("name")); + } + + @Override + public void setTileImage(Bitmap bitmap) { + } + + @Override + public void completed(QueryResult result) { + } + }; + decoder.decode(tile, sink, getClass().getResourceAsStream("/mvt-test.pbf")); + } +}