├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tehmou │ │ └── rxmaps │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── tehmou │ │ └── rxmaps │ │ ├── Configuration.java │ │ ├── MainActivity.java │ │ ├── data │ │ └── MapTileFileUtils.java │ │ ├── network │ │ ├── MapNetworkAdapter.java │ │ ├── MapNetworkAdapterSimple.java │ │ ├── NetworkClient.java │ │ └── NetworkClientOkHttp.java │ │ ├── pojo │ │ ├── MapTile.java │ │ ├── MapTileBitmap.java │ │ ├── MapTileDrawable.java │ │ └── ZoomLevel.java │ │ ├── provider │ │ ├── MapTileBitmapsTable.java │ │ ├── RxMapsContentProvider.java │ │ └── RxMapsDatabaseHelper.java │ │ ├── utils │ │ ├── CoordinateProjection.java │ │ ├── LatLng.java │ │ ├── LatLngCalculator.java │ │ ├── MapState.java │ │ ├── MapTileUtils.java │ │ ├── PointD.java │ │ ├── TileBitmapLoader.java │ │ └── ViewUtils.java │ │ └── view │ │ ├── MapCanvasView.java │ │ ├── MapFragment.java │ │ ├── MapView.java │ │ └── MapViewModel.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout │ ├── activity_main.xml │ └── rx_map_view.xml │ ├── menu │ └── main.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | 3 | # https://github.com/github/gitignore/blob/master/Android.gitignore 4 | 5 | # built application files 6 | *.apk 7 | *.ap_ 8 | *.orig 9 | viewserver/build 10 | build/ 11 | 12 | # files for the dex VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # generated files 19 | bin/ 20 | gen/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Eclipse project files 26 | .classpath 27 | .project 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Intellij project files 33 | *.iml 34 | *.ipr 35 | *.iws 36 | .idea/ 37 | 38 | # https://github.com/github/gitignore/blob/master/Global/OSX.gitignore 39 | 40 | .DS_Store 41 | .AppleDouble 42 | .LSOverride 43 | Icon 44 | 45 | # Node.js modules 46 | node_modules 47 | 48 | # Thumbnails 49 | ._* 50 | 51 | # Files that might appear on external disk 52 | .Spotlight-V100 53 | .Trashes 54 | *.bak 55 | season-cache.json 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RxMapsAndroid 2 | ============= 3 | 4 | This project is a generic maps client for Android built on RxJava. 5 | 6 | The purpose of this project is not be a competitor for other map clients and map SDKs, but to illustrate usage of RxJava in a real life scenario. 7 | 8 | Technology stack 9 | --------- 10 | 11 | Currently there are not so many frameworks in use: 12 | 13 | * RxJava 14 | * OkHttpClient 15 | 16 | 17 | Tile server 18 | -------------------- 19 | 20 | You can see the tile server end point in Configuration.java. The format is currently a little simplified and assumes the order of , , . However, as this the project is not in the form of a library yet anyway, you can create your own MapNetworkAdapter if the format does not work with your preferred tile server. 21 | 22 | The default tile server is set to [MapQuest-OSM tiles](http://developer.mapquest.com/web/products/open/map) 23 | 24 | // This is default configuration, replace with a desired one 25 | public static final String MAP_TILE_URL = "http://otile1.mqcdn.com/tiles/1.0.0/map/%d/%d/%d.jpg"; 26 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | 7 | apply plugin: 'com.android.application' 8 | 9 | android { 10 | compileSdkVersion 19 11 | buildToolsVersion "19.1.0" 12 | 13 | defaultConfig { 14 | applicationId "com.tehmou.rxmaps" 15 | minSdkVersion 16 16 | targetSdkVersion 19 17 | versionCode 1 18 | versionName "1.0" 19 | } 20 | buildTypes { 21 | release { 22 | runProguard false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | } 27 | 28 | dependencies { 29 | compile fileTree(dir: 'libs', include: ['*.jar']) 30 | compile "com.netflix.rxjava:rxjava-core:0.20.0" 31 | compile "com.netflix.rxjava:rxjava-android:0.20.0" 32 | compile "com.squareup.okhttp:okhttp:2.0.0" 33 | } 34 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /opt/android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tehmou/rxmaps/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/Configuration.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps; 2 | 3 | /** 4 | * Created by ttuo on 26/08/14. 5 | */ 6 | public class Configuration { 7 | private Configuration() { } 8 | 9 | // This is default configuration, replace with a desired one 10 | public static final String MAP_TILE_URL = "http://otile1.mqcdn.com/tiles/1.0.0/map/%d/%d/%d.jpg"; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | 8 | import com.tehmou.rxmaps.view.MapFragment; 9 | 10 | 11 | public class MainActivity extends Activity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | getFragmentManager().beginTransaction() 18 | .add(android.R.id.content, MapFragment.newInstance(Configuration.MAP_TILE_URL)) 19 | .commit(); 20 | } 21 | 22 | @Override 23 | public boolean onCreateOptionsMenu(Menu menu) { 24 | // Inflate the menu; this adds items to the action bar if it is present. 25 | getMenuInflater().inflate(R.menu.main, menu); 26 | return true; 27 | } 28 | 29 | @Override 30 | public boolean onOptionsItemSelected(MenuItem item) { 31 | // Handle action bar item clicks here. The action bar will 32 | // automatically handle clicks on the Home/Up button, so long 33 | // as you specify a parent activity in AndroidManifest.xml. 34 | int id = item.getItemId(); 35 | if (id == R.id.action_settings) { 36 | return true; 37 | } 38 | return super.onOptionsItemSelected(item); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/data/MapTileFileUtils.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.data; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | 6 | import com.tehmou.rxmaps.pojo.MapTile; 7 | 8 | import java.io.FileInputStream; 9 | import java.io.FileOutputStream; 10 | import java.io.IOException; 11 | 12 | import rx.functions.Action1; 13 | 14 | /** 15 | * Created by ttuo on 07/09/14. 16 | */ 17 | public class MapTileFileUtils { 18 | private MapTileFileUtils() { } 19 | 20 | static public String getFilename(final MapTile mapTile) { 21 | final int zoom = mapTile.getZoom(); 22 | final int x = mapTile.getX(); 23 | final int y = mapTile.getY(); 24 | return "/data/data/com.tehmou.rxmaps/" + zoom + "_" + x + "_" + y + ".png"; 25 | } 26 | 27 | static public Bitmap readFromDisk(final String filename) { 28 | FileInputStream in = null; 29 | try { 30 | in = new FileInputStream(filename); 31 | return BitmapFactory.decodeStream(in); 32 | } catch (Exception e) { 33 | e.printStackTrace(); 34 | } finally { 35 | try { 36 | if (in != null) { 37 | in.close(); 38 | } 39 | } catch (IOException e) { 40 | e.printStackTrace(); 41 | } 42 | } 43 | return null; 44 | } 45 | 46 | static public void writeOnDisk(final String filename, final Bitmap bitmap) { 47 | FileOutputStream out = null; 48 | try { 49 | out = new FileOutputStream(filename); 50 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); 51 | } catch (Exception e) { 52 | e.printStackTrace(); 53 | } finally { 54 | try { 55 | if (out != null) { 56 | out.close(); 57 | } 58 | } catch (IOException e) { 59 | e.printStackTrace(); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/network/MapNetworkAdapter.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.network; 2 | 3 | import android.graphics.Bitmap; 4 | import android.util.Log; 5 | 6 | import com.tehmou.rxmaps.Configuration; 7 | 8 | import rx.Observable; 9 | 10 | /** 11 | * Created by ttuo on 26/08/14. 12 | */ 13 | public interface MapNetworkAdapter { 14 | Observable getMapTile(int zoom, int x, int y); 15 | int getTileSizePx(); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/network/MapNetworkAdapterSimple.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.network; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.util.Log; 6 | 7 | import com.tehmou.rxmaps.Configuration; 8 | 9 | import java.io.FileInputStream; 10 | import java.io.FileOutputStream; 11 | import java.io.IOException; 12 | 13 | import rx.Observable; 14 | import rx.functions.Action1; 15 | 16 | /** 17 | * Created by ttuo on 26/08/14. 18 | */ 19 | public class MapNetworkAdapterSimple implements MapNetworkAdapter { 20 | private static final String TAG = MapNetworkAdapterSimple.class.getCanonicalName(); 21 | final private NetworkClient networkClient; 22 | final private String urlFormat; 23 | 24 | public MapNetworkAdapterSimple( 25 | final NetworkClient networkClient, 26 | final String urlFormat) { 27 | this.networkClient = networkClient; 28 | this.urlFormat = urlFormat; 29 | } 30 | 31 | public Observable getMapTile(final int zoom, final int x, final int y) { 32 | Log.d(TAG, "getMapTile(" + zoom + ", " + x + ", " + y + ")"); 33 | final String url = String.format(urlFormat, zoom, x, y); 34 | return networkClient 35 | .loadBitmap(url); 36 | } 37 | 38 | @Override 39 | public int getTileSizePx() { 40 | return 256; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/network/NetworkClient.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.network; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | import rx.Observable; 6 | 7 | /** 8 | * Created by ttuo on 26/08/14. 9 | */ 10 | public interface NetworkClient { 11 | Observable loadString(final String url); 12 | Observable loadBitmap(final String url); 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/network/NetworkClientOkHttp.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.network; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.util.Log; 6 | 7 | import com.squareup.okhttp.OkHttpClient; 8 | import com.squareup.okhttp.Request; 9 | import com.squareup.okhttp.Response; 10 | 11 | import java.io.InputStream; 12 | import java.net.HttpURLConnection; 13 | import java.net.URL; 14 | 15 | import rx.Observable; 16 | import rx.Subscriber; 17 | import rx.schedulers.Schedulers; 18 | 19 | /** 20 | * Created by ttuo on 26/08/14. 21 | */ 22 | public class NetworkClientOkHttp implements NetworkClient { 23 | private static final String TAG = NetworkClientOkHttp.class.getCanonicalName(); 24 | final private OkHttpClient client; 25 | 26 | public NetworkClientOkHttp() { 27 | client = new OkHttpClient(); 28 | } 29 | 30 | public Observable loadString(final String url) { 31 | Log.i(TAG, "loadString(" + url + ")"); 32 | return Observable 33 | .create(new LoadString(url)) 34 | .subscribeOn(Schedulers.io()); 35 | } 36 | 37 | public Observable loadBitmap(final String url) { 38 | Log.i(TAG, "loadBitmap(" + url + ")"); 39 | return Observable 40 | .create(new LoadBitmap(url)) 41 | .subscribeOn(Schedulers.io()); 42 | } 43 | 44 | abstract private class LoadOperation implements Observable.OnSubscribe { 45 | final protected String url; 46 | 47 | public LoadOperation(final String url) { 48 | this.url = url; 49 | } 50 | } 51 | 52 | private class LoadString extends LoadOperation { 53 | public LoadString(final String url) { 54 | super(url); 55 | } 56 | 57 | @Override 58 | public void call(Subscriber subscriber) { 59 | try { 60 | final Request request = new Request.Builder() 61 | .url(url) 62 | .build(); 63 | final Response response = client.newCall(request).execute(); 64 | final String responseBody = response.body().string(); 65 | subscriber.onNext(responseBody); 66 | subscriber.onCompleted(); 67 | } catch (final Exception e) { 68 | e.printStackTrace(); 69 | subscriber.onError(e); 70 | } 71 | } 72 | } 73 | 74 | private class LoadBitmap extends LoadOperation { 75 | public LoadBitmap(final String url) { 76 | super(url); 77 | } 78 | 79 | @Override 80 | public void call(Subscriber subscriber) { 81 | try { 82 | final URL url = new URL(this.url); 83 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 84 | connection.setDoInput(true); 85 | connection.connect(); 86 | InputStream input = connection.getInputStream(); 87 | Bitmap bitmap = BitmapFactory.decodeStream(input); 88 | subscriber.onNext(bitmap); 89 | subscriber.onCompleted(); 90 | } catch (Exception e) { 91 | e.printStackTrace(); 92 | subscriber.onError(e); 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/pojo/MapTile.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.pojo; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | /** 6 | * Created by ttuo on 27/08/14. 7 | */ 8 | public class MapTile { 9 | final private int zoom; 10 | final private int x; 11 | final private int y; 12 | 13 | public MapTile(int zoom, int x, int y) { 14 | this.zoom = zoom; 15 | this.x = x; 16 | this.y = y; 17 | } 18 | 19 | public int getZoom() { 20 | return zoom; 21 | } 22 | 23 | public int getX() { 24 | return x; 25 | } 26 | 27 | public int getY() { 28 | return y; 29 | } 30 | 31 | public int tileHashCode() { 32 | int hash = zoom; 33 | hash = 31 * hash + x; 34 | hash = 31 * hash + y; 35 | return hash; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/pojo/MapTileBitmap.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.pojo; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | /** 6 | * Created by ttuo on 28/08/14. 7 | */ 8 | public class MapTileBitmap { 9 | final private MapTile mapTile; 10 | final private Bitmap bitmap; 11 | 12 | public MapTileBitmap(MapTile mapTile, Bitmap bitmap) { 13 | this.mapTile = mapTile; 14 | this.bitmap = bitmap; 15 | } 16 | 17 | public MapTile getMapTile() { 18 | return mapTile; 19 | } 20 | 21 | public Bitmap getBitmap() { 22 | return bitmap; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/pojo/MapTileDrawable.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.pojo; 2 | 3 | /** 4 | * Created by ttuo on 28/08/14. 5 | */ 6 | public class MapTileDrawable extends MapTile { 7 | final double size; 8 | final double screenX; 9 | final double screenY; 10 | 11 | public MapTileDrawable(int zoom, int x, int y, 12 | double size, double screenX, double screenY) { 13 | super(zoom, x, y); 14 | this.size = size; 15 | this.screenX = screenX; 16 | this.screenY = screenY; 17 | } 18 | 19 | public double getScreenX() { 20 | return screenX; 21 | } 22 | 23 | public double getScreenY() { 24 | return screenY; 25 | } 26 | 27 | public double getSize() { 28 | return size; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/pojo/ZoomLevel.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.pojo; 2 | 3 | import rx.Observable; 4 | import rx.subjects.BehaviorSubject; 5 | import rx.subjects.Subject; 6 | 7 | /** 8 | * Created by ttuo on 27/08/14. 9 | */ 10 | public class ZoomLevel { 11 | final private Subject subject; 12 | private int zoomLevel; 13 | 14 | public ZoomLevel(int zoomLevel) { 15 | this.zoomLevel = zoomLevel; 16 | subject = BehaviorSubject.create(this.zoomLevel); 17 | } 18 | 19 | public Observable getObservable() { 20 | return subject; 21 | } 22 | 23 | public void setZoomLevel(int zoomLevel) { 24 | this.zoomLevel = zoomLevel; 25 | subject.onNext(zoomLevel); 26 | } 27 | 28 | public int getZoomLevel() { 29 | return zoomLevel; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/provider/MapTileBitmapsTable.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.provider; 2 | 3 | import android.database.sqlite.SQLiteDatabase; 4 | 5 | /** 6 | * Created by ttuo on 03/09/14. 7 | */ 8 | public class MapTileBitmapsTable { 9 | public final static String TABLE_NAME = "map_tile_bitmaps"; 10 | 11 | private static final String COLUMN_DB_ID = "_id"; 12 | public static final String COLUMN_X = "x"; 13 | public static final String COLUMN_Y = "y"; 14 | public static final String COLUMN_ZOOM_LEVEL = "zoom_level"; 15 | public static final String COLUMN_BITMAP_FILENAME = "bitmap_filename"; 16 | public static final String COLUMN_TIMESTAMP = "timestamp"; 17 | public static final String[] PROJECTION = new String[] { 18 | COLUMN_X, COLUMN_Y, COLUMN_ZOOM_LEVEL, COLUMN_BITMAP_FILENAME }; 19 | 20 | private MapTileBitmapsTable () { } 21 | 22 | static private String getDatabaseCreate() { 23 | return "CREATE TABLE IF NOT EXISTS " 24 | + TABLE_NAME 25 | + "(" 26 | + COLUMN_DB_ID + " INTEGER PRIMARY KEY AUTOINCREMENT" 27 | + ", " 28 | + COLUMN_X + " INTEGER" 29 | + ", " 30 | + COLUMN_Y + " INTEGER" 31 | + ", " 32 | + COLUMN_ZOOM_LEVEL + " INTEGER" 33 | + ", " 34 | + COLUMN_BITMAP_FILENAME + " STRING" 35 | + ", " 36 | + COLUMN_TIMESTAMP + " DATETIME DEFAULT CURRENT_TIMESTAMP" 37 | + ")"; 38 | } 39 | 40 | static private String getDatabaseDrop() { 41 | return "DROP TABLE IF EXISTS " + TABLE_NAME; 42 | } 43 | 44 | static public void create(final SQLiteDatabase db) { 45 | db.execSQL(getDatabaseCreate()); 46 | } 47 | 48 | static public void drop(final SQLiteDatabase db) { 49 | db.execSQL(getDatabaseDrop()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/provider/RxMapsContentProvider.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.provider; 2 | 3 | import android.content.ContentProvider; 4 | import android.content.ContentValues; 5 | import android.content.UriMatcher; 6 | import android.database.Cursor; 7 | import android.database.sqlite.SQLiteDatabase; 8 | import android.database.sqlite.SQLiteQueryBuilder; 9 | import android.graphics.Paint; 10 | import android.net.Uri; 11 | import android.text.TextUtils; 12 | import android.util.Log; 13 | import android.util.Pair; 14 | 15 | import com.tehmou.rxmaps.pojo.MapTile; 16 | import com.tehmou.rxmaps.pojo.MapTileBitmap; 17 | 18 | /** 19 | * Created by ttuo on 03/09/14. 20 | */ 21 | public class RxMapsContentProvider extends ContentProvider { 22 | private static final String TAG = RxMapsContentProvider.class.getCanonicalName(); 23 | private static final String DATA_PROVIDER_AUTHORITY = "com.tehmou.rxmaps.provider"; 24 | 25 | public static final int MAP_TILE_BITMAPS_ID = 10; 26 | public static final Uri MAP_TILE_BITMAPS_CONTENT_URI = 27 | Uri.parse("content://" + DATA_PROVIDER_AUTHORITY + "/" + MapTileBitmapsTable.TABLE_NAME); 28 | 29 | public static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); 30 | 31 | static { 32 | sURIMatcher.addURI(DATA_PROVIDER_AUTHORITY, MapTileBitmapsTable.TABLE_NAME + "/#/#/#", MAP_TILE_BITMAPS_ID); 33 | } 34 | 35 | private RxMapsDatabaseHelper database; 36 | 37 | @Override 38 | public boolean onCreate() { 39 | database = new RxMapsDatabaseHelper(getContext()); 40 | return false; 41 | } 42 | 43 | static private String getTableName(Uri uri) { 44 | int uriType = sURIMatcher.match(uri); 45 | String tableName = null; 46 | switch (uriType) { 47 | case MAP_TILE_BITMAPS_ID: 48 | tableName = MapTileBitmapsTable.TABLE_NAME; 49 | break; 50 | } 51 | return tableName; 52 | } 53 | 54 | @Override 55 | public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { 56 | final String tableName = getTableName(uri); 57 | final int zoom = Integer.valueOf(uri.getPathSegments().get(1)); 58 | final int x = Integer.valueOf(uri.getPathSegments().get(2)); 59 | final int y = Integer.valueOf(uri.getPathSegments().get(3)); 60 | Log.d(TAG, "query(" + tableName + "," + zoom + "," + x + "," + y + ")"); 61 | 62 | SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 63 | queryBuilder.setTables(tableName); 64 | final String where = MapTileBitmapsTable.COLUMN_X + "='" + x + "'" 65 | + " AND " + MapTileBitmapsTable.COLUMN_Y + "='" + y + "'" 66 | + " AND " + MapTileBitmapsTable.COLUMN_ZOOM_LEVEL + "='" + zoom + "'"; 67 | queryBuilder.appendWhere(where); 68 | SQLiteDatabase db = database.getWritableDatabase(); 69 | Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); 70 | cursor.setNotificationUri(getContext().getContentResolver(), uri); 71 | return cursor; 72 | } 73 | 74 | @Override 75 | public String getType(Uri uri) { 76 | return null; 77 | } 78 | 79 | @Override 80 | public Uri insert(Uri uri, ContentValues values) { 81 | final String tableName = getTableName(uri); 82 | final int zoom = Integer.valueOf(uri.getPathSegments().get(1)); 83 | final int x = Integer.valueOf(uri.getPathSegments().get(2)); 84 | final int y = Integer.valueOf(uri.getPathSegments().get(3)); 85 | Log.d(TAG, "insert(" + tableName + "," + zoom + "," + x + "," + y + ")"); 86 | 87 | final SQLiteDatabase db = database.getWritableDatabase(); 88 | values.put(MapTileBitmapsTable.COLUMN_ZOOM_LEVEL, zoom); 89 | values.put(MapTileBitmapsTable.COLUMN_X, x); 90 | values.put(MapTileBitmapsTable.COLUMN_Y, y); 91 | db.insert(tableName, null, values); 92 | return uri; 93 | } 94 | 95 | @Override 96 | public int delete(Uri uri, String selection, String[] selectionArgs) { 97 | return 0; 98 | } 99 | 100 | @Override 101 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 102 | return 0; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/provider/RxMapsDatabaseHelper.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.provider; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.database.sqlite.SQLiteOpenHelper; 6 | import android.util.Log; 7 | 8 | /** 9 | * Created by ttuo on 03/09/14. 10 | */ 11 | public class RxMapsDatabaseHelper extends SQLiteOpenHelper { 12 | private static final String TAG = RxMapsDatabaseHelper.class.getCanonicalName(); 13 | public static final String DATABASE_NAME = "com.tehmou.rxmaps"; 14 | private static final int DATABASE_VERSION = 3; 15 | 16 | public RxMapsDatabaseHelper(Context context) { 17 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 18 | onCreate(getWritableDatabase()); 19 | } 20 | 21 | @Override 22 | public void onCreate(SQLiteDatabase db) { 23 | Log.d(TAG, "onCreate"); 24 | MapTileBitmapsTable.create(db); 25 | } 26 | 27 | @Override 28 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 29 | Log.d(TAG, "onUpgrade"); 30 | MapTileBitmapsTable.drop(db); 31 | MapTileBitmapsTable.create(db); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/utils/CoordinateProjection.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.utils; 2 | 3 | /** 4 | * Created by ttuo on 27/08/14. 5 | * 6 | * Mercator map projection utilities. 7 | * 8 | * https://google-developers.appspot.com/maps/documentation/javascript/examples/map-coordinates 9 | * http://paulbourke.net/geometry/transformationprojection/ 10 | * http://en.wikipedia.org/wiki/Mercator_projection 11 | */ 12 | public class CoordinateProjection { 13 | final private int tileSize; 14 | final private PointD pxOrigin; 15 | final private double pxPerLonDegree; 16 | final private double pxPerLonRadian; 17 | 18 | public CoordinateProjection(final int tileSize) { 19 | this.tileSize = tileSize; 20 | this.pxOrigin = new PointD(tileSize / 2.0, tileSize / 2.0); 21 | this.pxPerLonDegree = tileSize / 360.0; 22 | this.pxPerLonRadian = tileSize / (2 * Math.PI); 23 | } 24 | 25 | public PointD fromLatLngToPoint(double lat, double lng, int zoom) { 26 | final int numTiles = 1 << zoom; 27 | final double sinY = Math.sin(Math.toRadians(lat)); 28 | final double boundSinY = Math.max(Math.min(sinY, 0.999999), -0.999999); 29 | final double x = pxOrigin.x + lng * pxPerLonDegree; 30 | final double y = pxOrigin.y + 0.5 * Math.log((1 + boundSinY) / (1 - boundSinY)) * -pxPerLonRadian; 31 | return new PointD(x * numTiles, y * numTiles); 32 | } 33 | 34 | public LatLng fromPointToLatLng(final PointD point, final int zoom) { 35 | final int numTiles = 1 << zoom; 36 | final double x = point.x / numTiles; 37 | final double y = point.y / numTiles; 38 | final double lng = (x - pxOrigin.x) / pxPerLonDegree; 39 | final double latRadians = (y - pxOrigin.y) / -pxPerLonRadian; 40 | final double lat = Math.toDegrees(2 * Math.atan(Math.exp(latRadians)) - Math.PI / 2); 41 | return new LatLng(lat, lng); 42 | } 43 | 44 | public int pxSize(final int zoom) { 45 | return tileSize * (1 << zoom); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/utils/LatLng.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.utils; 2 | 3 | /** 4 | * Created by ttuo on 27/08/14. 5 | */ 6 | public class LatLng { 7 | final private double lat; 8 | final private double lng; 9 | 10 | public LatLng(double lat, double lng) { 11 | this.lat = lat; 12 | this.lng = lng; 13 | } 14 | 15 | public double getLat() { 16 | return lat; 17 | } 18 | 19 | public double getLng() { 20 | return lng; 21 | } 22 | 23 | @Override 24 | public int hashCode() { 25 | int hash = Double.valueOf(lat).hashCode(); 26 | hash += 31 * hash + Double.valueOf(lng).hashCode(); 27 | return hash; 28 | } 29 | 30 | @Override 31 | public boolean equals(Object o) { 32 | return o != null && o instanceof LatLng && hashCode() == o.hashCode(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/utils/LatLngCalculator.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.utils; 2 | 3 | import android.util.Log; 4 | 5 | import com.tehmou.rxmaps.pojo.ZoomLevel; 6 | 7 | import rx.Observable; 8 | import rx.Subscriber; 9 | import rx.functions.Action1; 10 | import rx.subjects.BehaviorSubject; 11 | import rx.subjects.PublishSubject; 12 | import rx.subjects.Subject; 13 | 14 | /** 15 | * Created by ttuo on 29/08/14. 16 | */ 17 | public class LatLngCalculator { 18 | private static final String TAG = LatLngCalculator.class.getCanonicalName(); 19 | final private Subject observable; 20 | private MapState mapState; 21 | 22 | public LatLngCalculator(final CoordinateProjection coordinateProjection, 23 | final Observable pixelDelta) { 24 | final Subject latLngSubject = BehaviorSubject.create(); 25 | pixelDelta.subscribe(new Action1() { 26 | @Override 27 | public void call(final PointD pixedDelta) { 28 | Log.v(TAG, "pixelDelta(" + pixedDelta + ")"); 29 | final double cx = mapState.viewSize.x/2.0 - mapState.offset.x; 30 | final double cy = mapState.viewSize.y/2.0 - mapState.offset.y; 31 | final PointD newPoint = new PointD(cx - pixedDelta.x, cy - pixedDelta.y); 32 | final LatLng newLatLng = coordinateProjection.fromPointToLatLng(newPoint, mapState.zoomLevel); 33 | latLngSubject.onNext(newLatLng); 34 | } 35 | }); 36 | observable = latLngSubject; 37 | } 38 | 39 | public void setMapStateObservable(final Observable mapStateObservable) { 40 | mapStateObservable.subscribe(new Action1() { 41 | @Override 42 | public void call(MapState mapState) { 43 | LatLngCalculator.this.mapState = mapState; 44 | } 45 | }); 46 | } 47 | 48 | public Observable getObservable() { 49 | return observable; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/utils/MapState.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.utils; 2 | 3 | /** 4 | * Created by ttuo on 28/08/14. 5 | */ 6 | public class MapState { 7 | final public PointD offset; 8 | final public PointD viewSize; 9 | final public int zoomLevel; 10 | 11 | public MapState(PointD offset, PointD viewSize, int zoomLevel) { 12 | this.offset = offset; 13 | this.viewSize = viewSize; 14 | this.zoomLevel = zoomLevel; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/utils/MapTileUtils.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.utils; 2 | 3 | import android.content.ContentResolver; 4 | import android.content.ContentValues; 5 | import android.database.Cursor; 6 | import android.graphics.Bitmap; 7 | import android.graphics.BitmapFactory; 8 | import android.net.Uri; 9 | import android.util.Log; 10 | 11 | import com.tehmou.rxmaps.data.MapTileFileUtils; 12 | import com.tehmou.rxmaps.network.MapNetworkAdapter; 13 | import com.tehmou.rxmaps.pojo.MapTile; 14 | import com.tehmou.rxmaps.pojo.MapTileBitmap; 15 | import com.tehmou.rxmaps.pojo.MapTileDrawable; 16 | import com.tehmou.rxmaps.provider.MapTileBitmapsTable; 17 | import com.tehmou.rxmaps.provider.RxMapsContentProvider; 18 | 19 | import java.io.ByteArrayOutputStream; 20 | import java.util.ArrayList; 21 | import java.util.Collection; 22 | import java.util.List; 23 | 24 | import rx.Observable; 25 | import rx.functions.Action1; 26 | import rx.functions.Func1; 27 | import rx.functions.Func3; 28 | 29 | /** 30 | * Created by ttuo on 28/08/14. 31 | */ 32 | public class MapTileUtils { 33 | private static final String TAG = MapTileUtils.class.getCanonicalName(); 34 | 35 | static public PointD calculateOffset(final CoordinateProjection coordinateProjection, 36 | final Integer zoomLevel, 37 | final PointD viewSize, 38 | final LatLng center) { 39 | final double mapPxSize = coordinateProjection.pxSize(zoomLevel); 40 | final PointD centerPx = coordinateProjection 41 | .fromLatLngToPoint(center.getLat(), center.getLng(), zoomLevel); 42 | final double offsetX2 = centerPx.x - (mapPxSize / 2.0); 43 | final double offsetY2 = centerPx.y - (mapPxSize / 2.0); 44 | final double centerOffsetX = (viewSize.x - mapPxSize) / 2.0; 45 | final double centerOffsetY = (viewSize.y - mapPxSize) / 2.0; 46 | final double offsetX = centerOffsetX - offsetX2; 47 | final double offsetY = centerOffsetY - offsetY2; 48 | Log.d(TAG, "offsetPx(" + offsetX + ", " + offsetY + ")"); 49 | return new PointD(offsetX, offsetY); 50 | } 51 | 52 | static public Func1> calculateMapTiles(final double tileSizePx) { 53 | return new Func1>() { 54 | @Override 55 | public Collection call(MapState mapState) { 56 | return calculateMapTiles(tileSizePx, mapState.zoomLevel, mapState.viewSize, mapState.offset); 57 | } 58 | }; 59 | } 60 | 61 | static private Collection calculateMapTiles(final double tileSizePx, 62 | final Integer zoomLevel, 63 | final PointD viewSize, 64 | final PointD offset) { 65 | final int firstTileX = (int) Math.floor(-offset.x / tileSizePx); 66 | final int firstTileY = (int) Math.floor(-offset.y / tileSizePx); 67 | final int numX = (int) Math.ceil(viewSize.x / tileSizePx); 68 | final int numY = (int) Math.ceil(viewSize.y / tileSizePx); 69 | 70 | final int left = Math.max(0, firstTileX); 71 | final int right = Math.min(1 << zoomLevel, firstTileX + numX); 72 | final int top = Math.max(0, firstTileY); 73 | final int bottom = Math.min(1 << zoomLevel, firstTileY + numY); 74 | 75 | final List mapTileList = new ArrayList(); 76 | for (int i = left; i < right; i++) { 77 | for (int n = top; n < bottom; n++) { 78 | final MapTileDrawable mapTile = new MapTileDrawable( 79 | zoomLevel, i, n, tileSizePx, 80 | i*tileSizePx + offset.x, 81 | n*tileSizePx + offset.y); 82 | mapTileList.add(mapTile); 83 | } 84 | } 85 | return mapTileList; 86 | } 87 | 88 | static public Func3 combineToMapState( 89 | final CoordinateProjection coordinateProjection) { 90 | return new Func3() { 91 | @Override 92 | public MapState call(Integer zoomLevel, 93 | PointD viewSize, 94 | LatLng center) { 95 | final PointD offset = calculateOffset( 96 | coordinateProjection, zoomLevel, viewSize, center); 97 | return new MapState(offset, viewSize, zoomLevel); 98 | } 99 | }; 100 | } 101 | 102 | static public Uri createUri(final MapTile mapTile) { 103 | Uri uri = RxMapsContentProvider.MAP_TILE_BITMAPS_CONTENT_URI; 104 | uri = Uri.withAppendedPath(uri, String.valueOf(mapTile.getZoom())); 105 | uri = Uri.withAppendedPath(uri, String.valueOf(mapTile.getX())); 106 | uri = Uri.withAppendedPath(uri, String.valueOf(mapTile.getY())); 107 | return uri; 108 | } 109 | 110 | static private byte[] bitmapToByte(final Bitmap bitmap) { 111 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 112 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos); 113 | return bos.toByteArray(); 114 | } 115 | 116 | static private Bitmap byteToBitmap(final byte[] bitmapBlob) { 117 | return BitmapFactory.decodeByteArray(bitmapBlob, 0, bitmapBlob.length); 118 | } 119 | 120 | static public Func1> loadMapTile( 121 | final MapNetworkAdapter mapNetworkAdapter) { 122 | return new Func1>() { 123 | @Override 124 | public Observable call(final MapTile mapTile) { 125 | return mapNetworkAdapter.getMapTile( 126 | mapTile.getZoom(), mapTile.getX(), mapTile.getY()) 127 | .map(new Func1() { 128 | @Override 129 | public MapTileBitmap call(Bitmap bitmap) { 130 | return new MapTileBitmap(mapTile, bitmap); 131 | } 132 | }) 133 | .onErrorResumeNext(new Func1>() { 134 | @Override 135 | public Observable call(Throwable throwable) { 136 | Log.e(TAG, "Error loading tile (" + mapTile + ")", throwable); 137 | throwable.printStackTrace(); 138 | return Observable.from(new MapTileBitmap(mapTile, null)); 139 | } 140 | }); 141 | } 142 | }; 143 | } 144 | 145 | static public Action1 logOnNext(final String tag) { 146 | return new Action1() { 147 | @Override 148 | public void call(T value) { 149 | Log.d(TAG, tag + ": " + value); 150 | } 151 | }; 152 | } 153 | 154 | static final public Func1, Observable> expandCollection = 155 | new Func1, Observable>() { 156 | @Override 157 | public Observable call(Collection mapTiles) { 158 | return Observable.from(mapTiles); 159 | } 160 | }; 161 | } 162 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/utils/PointD.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.utils; 2 | 3 | /** 4 | * Created by ttuo on 27/08/14. 5 | */ 6 | public class PointD { 7 | final public double x; 8 | final public double y; 9 | 10 | public PointD(double x, double y) { 11 | this.x = x; 12 | this.y = y; 13 | } 14 | 15 | @Override 16 | public String toString() { 17 | return "PointD(" + x + ", " + y + ")"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/utils/TileBitmapLoader.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.utils; 2 | 3 | import android.content.ContentResolver; 4 | import android.content.ContentValues; 5 | import android.database.Cursor; 6 | import android.graphics.Bitmap; 7 | import android.net.Uri; 8 | 9 | import com.tehmou.rxmaps.data.MapTileFileUtils; 10 | import com.tehmou.rxmaps.network.MapNetworkAdapter; 11 | import com.tehmou.rxmaps.pojo.MapTile; 12 | import com.tehmou.rxmaps.pojo.MapTileBitmap; 13 | import com.tehmou.rxmaps.pojo.MapTileDrawable; 14 | import com.tehmou.rxmaps.provider.MapTileBitmapsTable; 15 | import com.tehmou.rxmaps.provider.RxMapsContentProvider; 16 | 17 | import java.util.Collection; 18 | import java.util.Map; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | 21 | import rx.Observable; 22 | import rx.functions.Func1; 23 | import rx.schedulers.Schedulers; 24 | 25 | /** 26 | * Created by ttuo on 03/09/14. 27 | */ 28 | public class TileBitmapLoader implements Func1, Observable>> { 29 | final private ContentResolver contentResolver; 30 | final private MapNetworkAdapter mapNetworkAdapter; 31 | final private Map loadedTileBitmaps = new ConcurrentHashMap(); 32 | 33 | public TileBitmapLoader(final ContentResolver contentResolver, 34 | final MapNetworkAdapter mapNetworkAdapter) { 35 | this.contentResolver = contentResolver; 36 | this.mapNetworkAdapter = mapNetworkAdapter; 37 | } 38 | 39 | @Override 40 | public Observable> call(Collection mapTileDrawables) { 41 | return Observable.from(mapTileDrawables) 42 | .filter(new Func1() { 43 | @Override 44 | public Boolean call(final MapTileDrawable mapTileDrawable) { 45 | return !loadedTileBitmaps.containsKey(mapTileDrawable.tileHashCode()); 46 | } 47 | }) 48 | .flatMap(loadTileBitmap) 49 | .map(new Func1>() { 50 | @Override 51 | public Map call(final MapTileBitmap mapTileBitmap) { 52 | if (mapTileBitmap != null && mapTileBitmap.getBitmap() != null) { 53 | loadedTileBitmaps.put(mapTileBitmap.getMapTile().tileHashCode(), 54 | mapTileBitmap.getBitmap()); 55 | } 56 | return loadedTileBitmaps; 57 | } 58 | }); 59 | } 60 | 61 | private MapTileBitmap getFromCache(final MapTile mapTile) { 62 | final Uri uri = MapTileUtils.createUri(mapTile); 63 | final Cursor cursor = contentResolver.query(uri, MapTileBitmapsTable.PROJECTION, null, null, null); 64 | if (cursor == null || cursor.getCount() == 0) { 65 | return null; 66 | } 67 | cursor.moveToFirst(); 68 | final String filename = cursor.getString( 69 | cursor.getColumnIndex(MapTileBitmapsTable.COLUMN_BITMAP_FILENAME)); 70 | cursor.close(); 71 | final Bitmap bitmap = MapTileFileUtils.readFromDisk(filename); 72 | return new MapTileBitmap(mapTile, bitmap); 73 | } 74 | 75 | private Observable getFromNetwork(final MapTile mapTile) { 76 | return MapTileUtils.loadMapTile(mapNetworkAdapter) 77 | .call(mapTile) 78 | .map(writeToContentProvider); 79 | } 80 | 81 | private Func1 writeToContentProvider = 82 | new Func1() { 83 | @Override 84 | public MapTileBitmap call(MapTileBitmap mapTileBitmap) { 85 | MapTileFileUtils.writeOnDisk( 86 | MapTileFileUtils.getFilename(mapTileBitmap.getMapTile()), mapTileBitmap.getBitmap()); 87 | 88 | final Uri uri = MapTileUtils.createUri(mapTileBitmap.getMapTile()); 89 | ContentValues contentValues = new ContentValues(); 90 | contentValues.put(MapTileBitmapsTable.COLUMN_BITMAP_FILENAME, 91 | MapTileFileUtils.getFilename(mapTileBitmap.getMapTile())); 92 | contentResolver.insert(uri, contentValues); 93 | return mapTileBitmap; 94 | } 95 | }; 96 | 97 | private Func1> loadTileBitmap = 98 | new Func1>() { 99 | @Override 100 | public Observable call(final MapTileDrawable mapTileDrawable) { 101 | return Observable.from((MapTile) mapTileDrawable) 102 | .flatMap(new Func1>() { 103 | @Override 104 | public Observable call(final MapTile mapTile) { 105 | final MapTileBitmap mapTileBitmap = getFromCache(mapTile); 106 | if (mapTileBitmap != null) { 107 | return Observable.from(mapTileBitmap); 108 | } 109 | return getFromNetwork(mapTile); 110 | } 111 | }); 112 | } 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/utils/ViewUtils.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.utils; 2 | 3 | import android.view.MotionEvent; 4 | import android.view.View; 5 | 6 | import rx.Observable; 7 | import rx.subjects.PublishSubject; 8 | import rx.subjects.Subject; 9 | 10 | /** 11 | * Created by ttuo on 29/08/14. 12 | */ 13 | public class ViewUtils { 14 | private ViewUtils() { } 15 | 16 | static public class TouchDelta implements View.OnTouchListener { 17 | private Subject deltaStream = PublishSubject.create(); 18 | private PointD lastTouch = null; 19 | 20 | @Override 21 | public boolean onTouch(View v, MotionEvent event) { 22 | switch (event.getAction()) { 23 | case MotionEvent.ACTION_DOWN: 24 | lastTouch = new PointD(event.getX(), event.getY()); 25 | break; 26 | case MotionEvent.ACTION_MOVE: 27 | if (lastTouch != null) { 28 | final PointD delta = new PointD(event.getX() - lastTouch.x, event.getY() - lastTouch.y); 29 | deltaStream.onNext(delta); 30 | lastTouch = new PointD(event.getX(), event.getY()); 31 | } 32 | break; 33 | case MotionEvent.ACTION_UP: 34 | lastTouch = null; 35 | break; 36 | } 37 | return true; 38 | } 39 | 40 | public Observable getObservable() { 41 | return deltaStream; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/view/MapCanvasView.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.graphics.Point; 9 | import android.util.AttributeSet; 10 | import android.util.DisplayMetrics; 11 | import android.util.Log; 12 | import android.view.DragEvent; 13 | import android.view.MotionEvent; 14 | import android.view.View; 15 | 16 | import com.tehmou.rxmaps.pojo.MapTileBitmap; 17 | import com.tehmou.rxmaps.pojo.MapTileDrawable; 18 | import com.tehmou.rxmaps.utils.PointD; 19 | import com.tehmou.rxmaps.utils.ViewUtils; 20 | 21 | import java.util.Collection; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | import rx.Observable; 26 | import rx.functions.Action1; 27 | 28 | /** 29 | * Created by ttuo on 26/08/14. 30 | */ 31 | public class MapCanvasView extends View { 32 | private static final String TAG = MapCanvasView.class.getCanonicalName(); 33 | private Paint paint; 34 | private Paint rectPaint; 35 | private Paint textPaint; 36 | private MapViewModel viewModel; 37 | 38 | private Collection mapTiles; 39 | private Map mapTileBitmaps; 40 | final private Observable touchDelta; 41 | 42 | public MapCanvasView(Context context) { 43 | this(context, null); 44 | } 45 | 46 | public MapCanvasView(Context context, AttributeSet attrs) { 47 | this(context, attrs, 0); 48 | } 49 | 50 | public MapCanvasView(Context context, AttributeSet attrs, int defStyleAttr) { 51 | super(context, attrs, defStyleAttr); 52 | init(); 53 | ViewUtils.TouchDelta touchDelta = new ViewUtils.TouchDelta(); 54 | setOnTouchListener(touchDelta); 55 | this.touchDelta = touchDelta.getObservable(); 56 | } 57 | 58 | private void init() { 59 | paint = new Paint(Paint.ANTI_ALIAS_FLAG); 60 | this.setBackgroundColor(Color.LTGRAY); 61 | rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 62 | rectPaint.setColor(Color.RED); 63 | rectPaint.setStyle(Paint.Style.STROKE); 64 | textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 65 | textPaint.setColor(Color.BLACK); 66 | textPaint.setTextSize(20); 67 | textPaint.setTextAlign(Paint.Align.LEFT); 68 | } 69 | 70 | public void setViewModel(final MapViewModel mapViewModel) { 71 | this.viewModel = mapViewModel; 72 | mapViewModel.setTouchDelta(touchDelta); 73 | mapViewModel.getMapTiles().subscribe(setMapTiles); 74 | mapViewModel.getMapTileBitmaps().subscribe(setMapTileBitmaps); 75 | } 76 | 77 | final private Action1> setMapTiles = 78 | new Action1>() { 79 | @Override 80 | public void call(Collection mapTiles) { 81 | Log.d(TAG, "setMapTiles(" + mapTiles + ")"); 82 | MapCanvasView.this.mapTiles = mapTiles; 83 | invalidate(); 84 | } 85 | }; 86 | 87 | final private Action1> setMapTileBitmaps = 88 | new Action1>() { 89 | @Override 90 | public void call(final Map mapTileBitmaps) { 91 | Log.d(TAG, "setMapTileBitmaps(" + mapTileBitmaps + ")"); 92 | MapCanvasView.this.mapTileBitmaps = mapTileBitmaps; 93 | invalidate(); 94 | } 95 | }; 96 | 97 | @Override 98 | protected void onDraw(final Canvas canvas) { 99 | if (mapTiles == null) { 100 | return; 101 | } 102 | for (MapTileDrawable mapTile : mapTiles) { 103 | final int hash = mapTile.tileHashCode(); 104 | 105 | final float x = (float) mapTile.getScreenX(); 106 | final float y = (float) mapTile.getScreenY(); 107 | final Bitmap bitmap = mapTileBitmaps != null ? mapTileBitmaps.get(hash) : null; 108 | if (bitmap != null) { 109 | canvas.drawBitmap(bitmap, x, y, paint); 110 | } else { 111 | Log.d(TAG, "Loaded bitmap was null: " + mapTile); 112 | } 113 | canvas.drawRect( 114 | x, y, 115 | x + (float) mapTile.getSize() - 1, 116 | y + (float) mapTile.getSize() - 1, 117 | rectPaint); 118 | canvas.drawText(mapTile.getX() + ", " + mapTile.getY(), x + 3, y + 20, textPaint); 119 | } 120 | } 121 | 122 | @Override 123 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 124 | super.onLayout(changed, left, top, right, bottom); 125 | if (viewModel != null) { 126 | viewModel.setViewSize(right - left, bottom - top); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/view/MapFragment.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.view; 2 | 3 | import android.app.Fragment; 4 | import android.content.ContentResolver; 5 | import android.os.Bundle; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import com.tehmou.rxmaps.R; 11 | import com.tehmou.rxmaps.network.MapNetworkAdapter; 12 | import com.tehmou.rxmaps.network.MapNetworkAdapterSimple; 13 | import com.tehmou.rxmaps.network.NetworkClientOkHttp; 14 | import com.tehmou.rxmaps.utils.TileBitmapLoader; 15 | 16 | /** 17 | * Created by ttuo on 26/08/14. 18 | */ 19 | public class MapFragment extends Fragment { 20 | private final static String URL_FORMAT_KEY = "urlFormat"; 21 | private MapView mapView; 22 | 23 | public static MapFragment newInstance(final String urlFormat) { 24 | final MapFragment mapFragment = new MapFragment(); 25 | final Bundle arguments = new Bundle(); 26 | arguments.putString(URL_FORMAT_KEY, urlFormat); 27 | mapFragment.setArguments(arguments); 28 | return mapFragment; 29 | } 30 | 31 | private String getUrlFormat() { 32 | return getArguments().getString(URL_FORMAT_KEY); 33 | } 34 | 35 | @Override 36 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 37 | this.mapView = (MapView) inflater.inflate(R.layout.rx_map_view, container, false); 38 | return mapView; 39 | } 40 | 41 | @Override 42 | public void onStart() { 43 | super.onStart(); 44 | 45 | NetworkClientOkHttp networkClient = new NetworkClientOkHttp(); 46 | MapNetworkAdapter mapNetworkClient = 47 | new MapNetworkAdapterSimple(networkClient, getUrlFormat()); 48 | MapViewModel mapViewModel = new MapViewModel(mapNetworkClient.getTileSizePx(), 49 | new TileBitmapLoader(getActivity().getContentResolver(), mapNetworkClient)); 50 | mapView.setViewModel(mapViewModel); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/view/MapView.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.widget.FrameLayout; 7 | 8 | import com.tehmou.rxmaps.R; 9 | 10 | /** 11 | * Created by ttuo on 27/08/14. 12 | */ 13 | public class MapView extends FrameLayout { 14 | private MapCanvasView mapCanvasView; 15 | private MapViewModel viewModel; 16 | 17 | public MapView(Context context) { 18 | super(context); 19 | } 20 | 21 | public MapView(Context context, AttributeSet attrs) { 22 | super(context, attrs); 23 | } 24 | 25 | public MapView(Context context, AttributeSet attrs, int defStyle) { 26 | super(context, attrs, defStyle); 27 | } 28 | 29 | public void setViewModel(final MapViewModel mapViewModel) { 30 | this.viewModel = mapViewModel; 31 | mapCanvasView.setViewModel(mapViewModel); 32 | } 33 | 34 | @Override 35 | protected void onFinishInflate() { 36 | super.onFinishInflate(); 37 | mapCanvasView = (MapCanvasView) findViewById(R.id.rx_map_view_canvas); 38 | findViewById(R.id.rx_map_view_zoom_in) 39 | .setOnClickListener(new OnClickListener() { 40 | @Override 41 | public void onClick(View v) { 42 | viewModel.zoomIn(); 43 | } 44 | }); 45 | findViewById(R.id.rx_map_view_zoom_out) 46 | .setOnClickListener(new OnClickListener() { 47 | @Override 48 | public void onClick(View v) { 49 | viewModel.zoomOut(); 50 | } 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/tehmou/rxmaps/view/MapViewModel.java: -------------------------------------------------------------------------------- 1 | package com.tehmou.rxmaps.view; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | import com.tehmou.rxmaps.pojo.MapTileDrawable; 6 | import com.tehmou.rxmaps.utils.CoordinateProjection; 7 | import com.tehmou.rxmaps.utils.LatLng; 8 | import com.tehmou.rxmaps.pojo.ZoomLevel; 9 | import com.tehmou.rxmaps.utils.LatLngCalculator; 10 | import com.tehmou.rxmaps.utils.MapState; 11 | import com.tehmou.rxmaps.utils.MapTileUtils; 12 | import com.tehmou.rxmaps.utils.PointD; 13 | import com.tehmou.rxmaps.utils.TileBitmapLoader; 14 | 15 | import java.util.Collection; 16 | import java.util.Map; 17 | 18 | import rx.Observable; 19 | import rx.android.schedulers.AndroidSchedulers; 20 | import rx.subjects.BehaviorSubject; 21 | import rx.subjects.PublishSubject; 22 | import rx.subjects.Subject; 23 | 24 | /** 25 | * Created by ttuo on 26/08/14. 26 | */ 27 | public class MapViewModel { 28 | private static final String TAG = MapViewModel.class.getCanonicalName(); 29 | final private Observable> mapTiles; 30 | final private ZoomLevel zoomLevel; 31 | final private Subject viewSize; 32 | final private Subject centerCoordSubject; 33 | final private Observable centerCoord; 34 | final private CoordinateProjection coordinateProjection; 35 | final private Subject dragDelta; 36 | 37 | final private Observable> loadedTileBitmapsObservable; 38 | 39 | public MapViewModel(final int tileSizePx, final TileBitmapLoader tileBitmapLoader) { 40 | dragDelta = PublishSubject.create(); 41 | zoomLevel = new ZoomLevel(0); 42 | viewSize = PublishSubject.create(); 43 | centerCoordSubject = BehaviorSubject.create(new LatLng(51.507351, -0.127758)); 44 | coordinateProjection = new CoordinateProjection(tileSizePx); 45 | 46 | final LatLngCalculator latLngCalculator = new LatLngCalculator( 47 | coordinateProjection, dragDelta); 48 | 49 | centerCoord = Observable.merge( 50 | centerCoordSubject, 51 | latLngCalculator.getObservable()) 52 | .distinctUntilChanged(); 53 | 54 | final Subject, Collection> mapTilesSubject = 55 | BehaviorSubject.create(); 56 | final Subject, Map> loadedTileBitmapsSubject = 57 | PublishSubject.create(); 58 | 59 | Observable mapStateObservable = 60 | Observable.combineLatest( 61 | zoomLevel.getObservable().doOnNext(MapTileUtils.logOnNext("zoomLevel")), 62 | viewSize.doOnNext(MapTileUtils.logOnNext("viewSize")), 63 | centerCoord.doOnNext(MapTileUtils.logOnNext("centerCoord")), 64 | MapTileUtils.combineToMapState(coordinateProjection) 65 | ) 66 | .cache(); 67 | 68 | latLngCalculator.setMapStateObservable(mapStateObservable); 69 | 70 | final Observable> mapTiles = 71 | mapStateObservable 72 | .map(MapTileUtils.calculateMapTiles(tileSizePx)); 73 | 74 | mapTiles 75 | .observeOn(AndroidSchedulers.mainThread()) 76 | .subscribe(mapTilesSubject); 77 | 78 | mapTiles 79 | .flatMap(tileBitmapLoader) 80 | .observeOn(AndroidSchedulers.mainThread()) 81 | .subscribe(loadedTileBitmapsSubject); 82 | 83 | loadedTileBitmapsObservable = loadedTileBitmapsSubject; 84 | this.mapTiles = mapTilesSubject; 85 | } 86 | 87 | public Observable> getMapTiles() { 88 | return mapTiles; 89 | } 90 | 91 | public Observable> getMapTileBitmaps() { 92 | return loadedTileBitmapsObservable; 93 | } 94 | 95 | public void zoomIn() { 96 | zoomLevel.setZoomLevel( 97 | Math.min(20, zoomLevel.getZoomLevel() + 1)); 98 | } 99 | 100 | public void zoomOut() { 101 | zoomLevel.setZoomLevel( 102 | Math.max(0, zoomLevel.getZoomLevel() - 1)); 103 | } 104 | 105 | public void setViewSize(int width, int height) { 106 | viewSize.onNext(new PointD(width, height)); 107 | } 108 | 109 | public void setTouchDelta(Observable touchDelta) { 110 | touchDelta.subscribe(dragDelta); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tehmou/RxMapsAndroid/e0ce31d259b633f823d117e840d8ae9163735bb6/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tehmou/RxMapsAndroid/e0ce31d259b633f823d117e840d8ae9163735bb6/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tehmou/RxMapsAndroid/e0ce31d259b633f823d117e840d8ae9163735bb6/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tehmou/RxMapsAndroid/e0ce31d259b633f823d117e840d8ae9163735bb6/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/rx_map_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 |