Skip to content

Commit

Permalink
4.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
aqoleg committed Nov 5, 2019
0 parents commit 0ba5263
Show file tree
Hide file tree
Showing 59 changed files with 3,013 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# cat

All maps offline in one app and track writer.

[download](https://github.com/aqoleg/cat/releases/download/4.0.0/cat.apk)

Put tiles in the /cat/maps/mapName/z/y/x.png or /x.jpeg
and/or specifiy parameters in /cat/maps/mapName/properties.txt as json file:
- "name" - optional, name of the map
- "url" - optional, url to download as java formatted string "https://example/x=%1$d/y=%2$d/z=%3$d"
- "size" - optional, size of the tile
- "projection": "ellipsoid" - optional, for ellipsoid projection
29 changes: 29 additions & 0 deletions src/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
apply plugin: 'com.android.application'

android {
compileSdkVersion 26
buildToolsVersion "27.0.1"
defaultConfig {
applicationId "space.aqoleg.cat"
minSdkVersion 11
targetSdkVersion 26
versionCode 46
versionName "4.0.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
testCompile 'junit:junit:4.12'
}
26 changes: 26 additions & 0 deletions src/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="space.aqoleg.cat">
<uses-feature android:name="android.hardware.location.gps" />
<uses-feature android:name="android.hardware.location.network" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:label="@string/label" android:icon="@mipmap/icon" android:description="@string/description"
android:allowBackup="false">
<activity android:name=".MainActivity" android:launchMode="singleTask"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="geo" />
</intent-filter>
</activity>
<service android:name=".MainService" android:description="@string/serviceDescription" />
</application>
</manifest>
199 changes: 199 additions & 0 deletions src/app/src/main/java/space/aqoleg/cat/BitmapCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
cache with bitmaps
when get the tile, returns bitmap from cache and puts on the top of the stack,
or, if there is no such tile, asynchronously loads it from memory and puts it on the top of the stack
instead of the bottom item, if there is no such tile in the memory, try to get it from the lower zooms
and calls downloader in the one instance
*/
package space.aqoleg.cat;

import android.graphics.Bitmap;
import android.os.AsyncTask;

import java.io.RandomAccessFile;
import java.util.Arrays;

class BitmapCache implements Downloader.Callback {
private final Maps maps;
private final Callback callback;

private final int cacheSize;
private final int[] cacheMapN;
private final int[] cacheZ;
private final int[] cacheY;
private final int[] cacheX;
private final Bitmap[] cacheBitmap;
private final boolean[] cacheToDownload;
private final int[] cacheStackLevel;
private int nextStackLevel = 0; // possibly can not overflow

private boolean hasLoader = false;
private boolean hasDownloader = false;

BitmapCache(Callback callback) {
maps = Maps.getInstance();
this.callback = callback;
cacheSize = getCacheSize();
cacheMapN = new int[cacheSize];
Arrays.fill(cacheMapN, -1); // tile with mapN = 0, z = 0, y = 0, x = 0 did not loaded yet
cacheZ = new int[cacheSize];
cacheY = new int[cacheSize];
cacheX = new int[cacheSize];
cacheBitmap = new Bitmap[cacheSize];
cacheToDownload = new boolean[cacheSize];
cacheStackLevel = new int[cacheSize];
}

@Override
public void onDownloadFinish(int mapN, int z, int y, int x, Bitmap bitmap) {
for (int i = 0; i < cacheSize; i++) {
if (x == cacheX[i] && y == cacheY[i] && z == cacheZ[i] && mapN == cacheMapN[i]) {
if (bitmap != null) {
cacheBitmap[i] = bitmap;
}
cacheToDownload[i] = false; // even if this tile did not loaded, do not try one more time
break;
}
}
hasDownloader = false;
callback.onBitmapCacheLoad();
}

// returns bitmap from cache or null if it does not exist in the cache
Bitmap getBitmap(int mapN, int z, int y, int x) {
for (int i = 0; i < cacheSize; i++) {
if (x == cacheX[i] && y == cacheY[i] && z == cacheZ[i] && mapN == cacheMapN[i]) {
// put on the top of the stack
nextStackLevel++;
cacheStackLevel[i] = nextStackLevel;
// start download
if (cacheToDownload[i] && !hasDownloader && maps.canDownload(mapN)) {
hasDownloader = true;
new Downloader(mapN, z, y, x, this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
return cacheBitmap[i];
}
}
// if tile did not find in the cache, start loader
if (!hasLoader) {
hasLoader = true;
new Loader(mapN, z, y, x).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
return null;
}

private int getCacheSize() {
try {
RandomAccessFile reader = new RandomAccessFile("/proc/meminfo", "r");
String line = reader.readLine();
reader.close();
// MemTotal: 1030428 kB
line = line.substring(line.indexOf("MemTotal:") + 9, line.indexOf("kB")).trim();
int value = Integer.valueOf(line);
// 30 + ~50 tiles per gb
value = value / 20000;
if (value < 10) {
value = 10;
} else if (value > 200) {
value = 200;
}
return value + 30;
} catch (Exception exception) {
Data.getInstance().writeLog("can not determine memory size: " + exception.toString());
}
return 50;
}

interface Callback {
void onBitmapCacheLoad();
}

class Loader extends AsyncTask<Void, Void, Void> {
private final int loaderMapN;
private final int loaderZ;
private final int loaderY;
private final int loaderX;
private Bitmap bitmap;
private boolean isBitmapLoaded;

Loader(int mapN, int z, int y, int x) {
loaderMapN = mapN;
loaderZ = z;
loaderY = y;
loaderX = x;
}

@Override
protected Void doInBackground(Void... voids) {
bitmap = maps.getTile(loaderMapN, loaderZ, loaderY, loaderX);
isBitmapLoaded = bitmap != null;
if (!isBitmapLoaded) {
// try to fill with tile from previous zoom
int fullTileSize = maps.getSize(loaderMapN);
int xLeftPx = 0;
int yTopPx = 0;
int size = fullTileSize;
int z = loaderZ;
int y = loaderY;
int x = loaderX;
for (int i = 0; i < 5; i++) {
z--;
if (z == 0) {
break;
}
size = size >> 1;

xLeftPx = xLeftPx >> 1;
if ((x & 0b1) == 1) {
// right half of the tile
xLeftPx += fullTileSize >> 1;
}
x = x >> 1;

yTopPx = yTopPx >> 1;
if ((y & 0b1) == 1) {
// bottom half of the tile
yTopPx += fullTileSize >> 1;
}
y = y >> 1;

bitmap = maps.getTile(loaderMapN, z, y, x);
if (bitmap != null) {
bitmap = Bitmap.createBitmap(bitmap, xLeftPx, yTopPx, size, size);
bitmap = Bitmap.createScaledBitmap(bitmap, fullTileSize, fullTileSize, true);
return null;
}
}
}
return null;
}

@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
// do operation in the stack in the main thread
// search for the bottom item of the stack
int cacheN = 0;
int lowestStackLevel = Integer.MAX_VALUE;
for (int i = 0; i < cacheSize; i++) {
if (cacheStackLevel[i] < lowestStackLevel) {
cacheN = i;
lowestStackLevel = cacheStackLevel[i];
}
}
// put tile instead the bottom item
cacheMapN[cacheN] = loaderMapN;
cacheZ[cacheN] = loaderZ;
cacheY[cacheN] = loaderY;
cacheX[cacheN] = loaderX;
cacheBitmap[cacheN] = bitmap;
cacheToDownload[cacheN] = !isBitmapLoaded;
// put on the top of the stack
nextStackLevel++;
cacheStackLevel[cacheN] = nextStackLevel;
// refresh
hasLoader = false;
callback.onBitmapCacheLoad();
}
}
}
Loading

0 comments on commit 0ba5263

Please sign in to comment.