diff --git a/README.md b/README.md index 430a464..38119e9 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,63 @@ # cat -All maps offline in one app and track writer. +All maps offline with a track writer. + +[download](https://github.com/aqoleg/cat/releases/download/5.0.0/cat.apk) + +## add maps + +[mende](https://cat.aqoleg.com/app?newMap=mende&newUrl=http%3A%2F%2Fcat.aqoleg.com%2Fmaps%2Fmende%2F%253%24d%2F%252%24d%2F%251%24d.jpeg) + +[osm](https://cat.aqoleg.com/app?newMap=osm&newUrl=http%3A%2F%2Fa.tile.openstreetmap.org%2F%253%24d%2F%251%24d%2F%252%24d.png) + +[otm](https://cat.aqoleg.com/app?newMap=otm&newUrl=https%3A%2F%2Fa.tile.opentopomap.org%2F%253%24d%2F%251%24d%2F%252%24d.png) + +[topo](https://cat.aqoleg.com/app?newMap=topo&newUrl=https%3A%2F%2Fmaps.marshruty.ru%2Fml.ashx%3Fal%3D1%26x%3D%251%24d%26y%3D%252%24d%26z%3D%253%24d) + +[gsat](https://cat.aqoleg.com/app?newMap=gsat&newUrl=https%3A%2F%2Fkhms0.googleapis.com%2Fkh%3Fv%3D937%26hl%3Den%26x%3D%251%24d%26y%3D%252%24d%26z%3D%253%24d) + +[gmap](https://cat.aqoleg.com/app?newMap=gmap&newUrl=http%3A%2F%2Fmt0.google.com%2Fvt%2Flyrs%3Dm%26hl%3Den%26x%3D%251%24d%26y%3D%252%24d%26z%3D%253%24d) + +[yasat (ellipsoid)](https://cat.aqoleg.com/app?newMap=yasat&newUrl=https%3A%2F%2Fsat01.maps.yandex.net%2Ftiles%3Fl%3Dsat%26x%3D%251%24d%26y%3D%252%24d%26z%3D%253%24d%26g%3DGagarin&newProjection=ellipsoid) + +[arcsat](https://cat.aqoleg.com/app?newMap=arcsat&newUrl=https%3A%2F%2Fservices.arcgisonline.com%2FArcGIS%2Frest%2Fservices%2FWorld_Imagery%2FMapServer%2Ftile%2F%253%24d%2F%252%24d%2F%251%24d) + +[arctopo](https://cat.aqoleg.com/app?newMap=arctopo&newUrl=https%3A%2F%2Fservices.arcgisonline.com%2FArcGIS%2Frest%2Fservices%2FWorld_Topo_Map%2FMapServer%2Ftile%2F%253%24d%2F%252%24d%2F%251%24d) + +## external api + +http(s)://cat.aqoleg.com/app? + +### add map + +newMap=mapName&newUrl=urlToDownload&newProjection=projection& + +where + +- newMap - the name of the map to add +- newUrl - url to download tiles, with %1$d for x, %2$d for y an %3$d for z, for example https://tileserver.com/x=%1$d/y=%2$d/z=%3$d.png +- newProjection - optional, use 'ellipsoid' for ellipsoid prjection + +### open map + +map=mapName& + +### setZoom + +z=zoom& + +### open point + +longitude=lon&latitude=lat& or +lon=lon&lat=lat& + +### open track + +track=track& + +where track is decoded track "xLonyLatxNextLonyNextLat", for example +track=x-34y-51x-34.5y-51.5x-34.1y-51.2 -[download](https://github.com/aqoleg/cat/releases/download/4.0.1/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 [deprecated store](https://play.google.com/store/apps/details?id=space.aqoleg.cat) diff --git a/src/app/build.gradle b/src/android/build.gradle similarity index 86% rename from src/app/build.gradle rename to src/android/build.gradle index e2062ff..a68a143 100644 --- a/src/app/build.gradle +++ b/src/android/build.gradle @@ -4,11 +4,11 @@ android { compileSdkVersion 26 buildToolsVersion "27.0.1" defaultConfig { - applicationId "space.aqoleg.cat" + applicationId 'com.aqoleg.cat' minSdkVersion 11 targetSdkVersion 26 - versionCode 47 - versionName "4.0.1" + versionCode 48 + versionName '5.0.0' testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -18,6 +18,8 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + productFlavors { + } } dependencies { @@ -26,4 +28,4 @@ dependencies { exclude group: 'com.android.support', module: 'support-annotations' }) testCompile 'junit:junit:4.12' -} +} \ No newline at end of file diff --git a/src/app/src/main/AndroidManifest.xml b/src/android/src/main/AndroidManifest.xml similarity index 50% rename from src/app/src/main/AndroidManifest.xml rename to src/android/src/main/AndroidManifest.xml index 43518ff..e0e488b 100644 --- a/src/app/src/main/AndroidManifest.xml +++ b/src/android/src/main/AndroidManifest.xml @@ -1,14 +1,15 @@ - + + - - @@ -18,9 +19,27 @@ + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/ActivityMain.java b/src/android/src/main/java/com/aqoleg/cat/ActivityMain.java new file mode 100644 index 0000000..6b34659 --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/ActivityMain.java @@ -0,0 +1,314 @@ +/* +one single activity, handles permissions, intents, state, lifecycle, buttons +launches fragments and service + +lifecycle: + launch - onCreate, onStart + invisible, home, screen off - onSaveInstanceState, onStop + visible again - onStart + rotate - onSaveInstanceState, onStop, onDestroy, onCreate, onStart + close - onStop, onDestroy + */ +package com.aqoleg.cat; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.SystemClock; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.TextView; +import android.widget.Toast; +import com.aqoleg.cat.app.App; +import com.aqoleg.cat.data.Files; +import com.aqoleg.cat.data.UriData; + +public class ActivityMain extends Activity implements View.OnClickListener, View.OnLongClickListener { + private static final String prefs = "prefs"; + private static final String prefsMap = "map"; + private static final String prefsZ = "zoom"; + private static final String prefsCenterLongitude = "centerLon"; + private static final String prefsCenterLatitude = "centerLat"; + private static final String prefsLocationLongitude = "locationLon"; + private static final String prefsLocationLatitude = "locationLat"; + + private static final String stateOpenedTrack = "openedTrack"; + private static final String stateSelectedTracks = "selectedTracks"; + private static final String stateHasPoint = "hasPoint"; + private static final String statePointLongitude = "pointLon"; + private static final String statePointLatitude = "pointLat"; + private static final String stateTracksPosition = "tracksPosition"; + + private boolean hasPermissions; + private long lastClickTime; // debounce + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + try { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + for (int result : grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + finish(); + return; + } + } + recreate(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + try { + super.onCreate(savedInstanceState); + // permissions + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + int permissions = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION); + permissions |= checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE); + permissions |= checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); + if (permissions != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + new String[]{ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + }, + 0 + ); + return; + } + } + hasPermissions = true; + // get state + SharedPreferences sharedPrefs = getSharedPreferences(prefs, MODE_PRIVATE); + String map = sharedPrefs.getString(prefsMap, null); + int z = sharedPrefs.getInt(prefsZ, 2); + float centerLongitude = sharedPrefs.getFloat(prefsCenterLongitude, 0); + float centerLatitude = sharedPrefs.getFloat(prefsCenterLatitude, 0); + String openedTrack = null; + String[] selectedTracks = new String[0]; + boolean hasPoint = false; + double pointLongitude = 0, pointLatitude = 0; + int tracksPosition = 0; + UriData uriData = null; + // if stop app before permission had been granted, savedInstanceState became not null and empty + if (savedInstanceState != null && savedInstanceState.containsKey(stateSelectedTracks)) { + openedTrack = savedInstanceState.getString(stateOpenedTrack); + selectedTracks = savedInstanceState.getStringArray(stateSelectedTracks); + hasPoint = savedInstanceState.getBoolean(stateHasPoint); + pointLongitude = savedInstanceState.getDouble(statePointLongitude); + pointLatitude = savedInstanceState.getDouble(statePointLatitude); + tracksPosition = savedInstanceState.getInt(stateTracksPosition); + } else if (getIntent().getData() != null) { + uriData = UriData.parse(getIntent().getData()); + } + // load views, app + setContentView(R.layout.activity_main); + ActivityView activityView = new ActivityView(this); + ((FrameLayout) findViewById(R.id.mapView)).addView(activityView); + findViewById(R.id.close).setOnClickListener(this); + findViewById(R.id.localDistance).setOnClickListener(this); + findViewById(R.id.zPlus).setOnClickListener(this); + findViewById(R.id.zPlus).setOnLongClickListener(this); + findViewById(R.id.zMinus).setOnClickListener(this); + findViewById(R.id.zMinus).setOnLongClickListener(this); + findViewById(R.id.center).setOnClickListener(this); + findViewById(R.id.tracks).setOnClickListener(this); + findViewById(R.id.tracks).setOnLongClickListener(this); + findViewById(R.id.maps).setOnClickListener(this); + findViewById(R.id.extra).setOnClickListener(this); + + App.load( + this, + activityView, + map, + z, + centerLongitude, + centerLatitude, + sharedPrefs.getFloat(prefsLocationLongitude, 0), + sharedPrefs.getFloat(prefsLocationLatitude, 0), + openedTrack, + selectedTracks, + hasPoint, + pointLongitude, + pointLatitude, + tracksPosition, + uriData + ); + + getApplicationContext().startService(new Intent(getApplicationContext(), ServiceMain.class)); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + protected void onNewIntent(Intent intent) { + try { + super.onNewIntent(intent); + if (intent.getData() != null) { + App.open(UriData.parse(intent.getData())); + } + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onStart() { + try { + super.onStart(); + if (hasPermissions) { + App.setVisible(true); + } + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + try { + super.onSaveInstanceState(outState); + if (!hasPermissions) { + return; + } + outState.putString(stateOpenedTrack, App.getOpenedTrack()); + outState.putStringArray(stateSelectedTracks, App.getSelectedTracks()); + outState.putBoolean(stateHasPoint, App.hasPoint()); + outState.putDouble(statePointLongitude, App.getPointLongitude()); + outState.putDouble(statePointLatitude, App.getPointLatitude()); + outState.putInt(stateTracksPosition, App.getTracksPosition()); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onStop() { + try { + super.onStop(); + if (!hasPermissions) { + return; + } + getSharedPreferences(prefs, MODE_PRIVATE) + .edit() + .putString(prefsMap, App.getMapName()) + .putInt(prefsZ, App.getZ()) + .putFloat(prefsCenterLongitude, App.getCenterLongitude()) + .putFloat(prefsCenterLatitude, App.getCenterLatitude()) + .putFloat(prefsLocationLongitude, App.getLocationLongitude()) + .putFloat(prefsLocationLatitude, App.getLocationLatitude()) + .apply(); + App.setVisible(false); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + protected void onDestroy() { + try { + super.onDestroy(); + if (hasPermissions) { + App.unload(); + } + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onClick(View view) { + try { + boolean doubleClick = (SystemClock.elapsedRealtime() - lastClickTime) < 1000; + lastClickTime = SystemClock.elapsedRealtime(); + + switch (view.getId()) { + case R.id.close: + if (!doubleClick) { + stopService(new Intent(getApplicationContext(), ServiceMain.class)); + finish(); + } + break; + case R.id.localDistance: + App.clearLocalDistance(); + break; + case R.id.zPlus: + App.changeZoom(1); + break; + case R.id.zMinus: + App.changeZoom(-1); + break; + case R.id.center: + App.center(); + break; + case R.id.maps: + if (!doubleClick) { + DialogMaps.newInstance().show(getFragmentManager(), null); + } + break; + case R.id.tracks: + if (!doubleClick) { + DialogTracks.newInstance().show(getFragmentManager(), null); + } + break; + case R.id.extra: + if (!doubleClick) { + DialogExtra.newInstance().show(getFragmentManager(), null); + } + break; + } + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public boolean onLongClick(View view) { + try { + if (SystemClock.elapsedRealtime() - lastClickTime < 300) { + return false; + } + lastClickTime = SystemClock.elapsedRealtime(); + + switch (view.getId()) { + case R.id.zPlus: + App.changeZoom(3); + return true; + case R.id.zMinus: + App.changeZoom(-3); + return true; + case R.id.tracks: + Toast.makeText(getApplicationContext(), R.string.searchingTracks, Toast.LENGTH_SHORT).show(); + App.searchVisibleTracks(); + return true; + } + return false; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return false; + } + + + public void printDistance(float totalDistance, float localDistance) { + String format = getString(R.string.km); + ((TextView) findViewById(R.id.totalDistance)).setText(String.format(format, totalDistance / 1000)); + ((TextView) findViewById(R.id.localDistance)).setText(String.format(format, localDistance / 1000)); + } + + public void printZoom(int z) { + ((TextView) findViewById(R.id.zoom)).setText("z".concat(String.valueOf(z + 1))); + } + + public void changeTrackButtonAvailability(boolean available) { + findViewById(R.id.tracks).setEnabled(available); + findViewById(R.id.tracks).setAlpha(available ? 1 : 0.2f); + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/ActivityView.java b/src/android/src/main/java/com/aqoleg/cat/ActivityView.java new file mode 100644 index 0000000..c2668ab --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/ActivityView.java @@ -0,0 +1,469 @@ +/* +view with the map, handles touch events + */ +package com.aqoleg.cat; + +import android.content.Context; +import android.graphics.*; +import android.os.SystemClock; +import android.view.MotionEvent; +import android.view.View; +import com.aqoleg.cat.app.App; +import com.aqoleg.cat.data.Files; +import com.aqoleg.cat.data.Track; + +import java.util.Iterator; + +public class ActivityView extends View implements View.OnTouchListener { + private final Path path = new Path(); + private final Paint locationPaint = new Paint(); + private final Paint redTrackPaint = new Paint(); + private final Paint selectedTracksPaint = new Paint(); + private final Paint textPaint = new Paint(); + private final String nearPointText; + private final String farPointText; + // layout constants + private int xPxCenter; + private int yPxCenter; + private int xPxRight; // width + private int yPxBottom; // height + private int tileRounds; // the biggest number of tiles from the center to the edge of screen + // map parameters, tile size 256px + private int totalTiles; + private int pxTotal; // totalTiles * tileSize + // center, location, points coordinates, from 0 to 1, in the current projection + private double xCenter; + private double yCenter; + private double xLocation; + private double yLocation; + private boolean hasPoint; + private double xPoint; + private double yPoint; + private float sinPoint; + private float cosPoint; + private String textPoint; + private float xPxWidthTextPoint; + // touch events + private boolean isMoving; + private long touchStartTime; + private float xPxTouchStart; + private float yPxTouchStart; + private double xCenterTouchStart; + private double yCenterTouchStart; + + @SuppressWarnings("deprecation") + ActivityView(Context context) { + super(context); + locationPaint.setColor(context.getResources().getColor(R.color.pointerTransparent)); + locationPaint.setAntiAlias(true); + redTrackPaint.setStyle(Paint.Style.STROKE); + redTrackPaint.setStrokeWidth(2); + redTrackPaint.setColor(context.getResources().getColor(R.color.mainRed)); + redTrackPaint.setAntiAlias(true); + selectedTracksPaint.setStyle(Paint.Style.STROKE); + selectedTracksPaint.setStrokeWidth(2); + selectedTracksPaint.setColor(context.getResources().getColor(R.color.tracksTransparentPurple)); + selectedTracksPaint.setAntiAlias(true); + textPaint.setColor(context.getResources().getColor(R.color.mainRed)); + textPaint.setTextSize(16); + textPaint.setTypeface(Typeface.MONOSPACE); + textPaint.setFakeBoldText(true); + textPaint.setAntiAlias(true); + nearPointText = context.getString(R.string.nearSelection); + farPointText = context.getString(R.string.farSelection); + setOnTouchListener(this); + } + + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + try { + super.onLayout(changed, left, top, right, bottom); + if (changed) { + xPxCenter = getWidth() / 2; + yPxCenter = getHeight() / 2; + xPxRight = getWidth(); + yPxBottom = getHeight(); + tileRounds = (int) ((float) Math.max(xPxCenter, yPxCenter) / 256) + 1; + } + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + protected void onDraw(Canvas canvas) { + try { + super.onDraw(canvas); + drawTiles(canvas); + drawTracks(canvas); + drawPoint(canvas); + drawLocation(canvas); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public boolean onTouch(View view, MotionEvent event) { + try { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + touchStartTime = SystemClock.elapsedRealtime(); + xPxTouchStart = event.getX(); + yPxTouchStart = event.getY(); + xCenterTouchStart = xCenter; + yCenterTouchStart = yCenter; + return true; + case MotionEvent.ACTION_MOVE: + xCenter = xCenterTouchStart + ((double) (xPxTouchStart - event.getX()) / pxTotal); + if (xCenter < 0) { + xCenter += 1 + (int) -xCenter; + } else if (xCenter > 1) { + xCenter -= (int) xCenter; + } + yCenter = yCenterTouchStart + ((double) (yPxTouchStart - event.getY()) / pxTotal); + if (yCenter < 0) { + yCenter = 0; + } else if (yCenter > 1) { + yCenter = 1; + } + isMoving = true; + draw(); + return true; + case MotionEvent.ACTION_UP: + isMoving = false; + long timeSinceTouchStart = SystemClock.elapsedRealtime() - touchStartTime; + float xPxDistance = Math.abs(xPxTouchStart - event.getX()); + float yPxDistance = Math.abs(yPxTouchStart - event.getY()); + if (timeSinceTouchStart < 300 && xPxDistance < 10 && yPxDistance < 10) { + double xPoint = xCenter + (xPxTouchStart - xPxCenter) / pxTotal; + if (xPoint < 0) { + xPoint += 1 + (int) -xPoint; + } else if (xPoint > 1) { + xPoint -= (int) xPoint; + } + double yPoint = yCenter + (yPxTouchStart - yPxCenter) / pxTotal; + if (yPoint < 0 || yPoint > 1) { + hasPoint = false; + textPoint = null; + App.deselect(); + draw(); + return true; + } + if (hasPoint) { + xPxDistance = (float) Math.abs((xPoint - this.xPoint) * pxTotal); + yPxDistance = (float) Math.abs((yPoint - this.yPoint) * pxTotal); + if (xPxDistance < 32 && yPxDistance < 32) { + hasPoint = false; + textPoint = null; + App.deselect(); + draw(); + return true; + } + } + App.select(xPoint, yPoint); + } + draw(); + return true; + } + } catch (Throwable t) { + Files.getInstance().log(t); + } + return false; + } + + + // sets zoom without drawing + public void setZoom(int z) { + totalTiles = 1 << z; + pxTotal = totalTiles * 256; + } + + // sets center coordinates in the current projection without drawing + public void setCenter(double xCenter, double yCenter) { + this.xCenter = xCenter; + this.yCenter = yCenter; + } + + // sets location coordinates in the current projection without drawing + public void setLocation(double xLocation, double yLocation) { + this.xLocation = xLocation; + this.yLocation = yLocation; + } + + // sets point coordinates in the current projection without drawing + public void setPoint(double xPoint, double yPoint) { + hasPoint = true; + this.xPoint = xPoint; + this.yPoint = yPoint; + } + + // sets angles and distance of the point without drawing + public void setBearingAndDistance(float bearingFromSelection, float bearingToSelection, float distance) { + cosPoint = (float) Math.cos(Math.toRadians(90 - bearingFromSelection)); + sinPoint = (float) Math.sin(Math.toRadians(90 - bearingFromSelection)); + if (bearingToSelection < 0) { + bearingToSelection += 360; + } + if (distance >= 10000) { + textPoint = String.format(farPointText, Math.round(bearingToSelection), distance / 1000); + } else { + textPoint = String.format(nearPointText, Math.round(bearingToSelection), Math.round(distance)); + } + xPxWidthTextPoint = textPaint.measureText(textPoint); + } + + // plans to onDraw() on the main thread + public void draw() { + invalidate(); + } + + public double getXCenter() { + return xCenter; + } + + public double getYCenter() { + return yCenter; + } + + public int getXTileCenter() { + int xTile = (int) (xCenter * totalTiles); + if (xTile < 0) { + do { + xTile += totalTiles; + } while (xTile < 0); + } else if (xTile >= totalTiles) { + do { + xTile -= totalTiles; + } while (xTile >= totalTiles); + } + return xTile; + } + + public int getYTileCenter() { + int yTile = (int) (yCenter * totalTiles); + if (yTile < 0) { + yTile = 0; + } else if (yTile >= totalTiles) { + yTile = totalTiles - 1; + } + return yTile; + } + + // returns Boundaries of the visible part of the map in the current projection + public Boundaries getBoundaries() { + return new Boundaries(); + } + + + private void drawTiles(Canvas canvas) { + // center tile + int xTile = (int) (xCenter * totalTiles); + int yTile = (int) (yCenter * totalTiles); + int xPxLeft = xPxCenter - (int) Math.round((xCenter * totalTiles - xTile) * 256); + int yPxTop = yPxCenter - (int) Math.round((yCenter * totalTiles - yTile) * 256); + drawTile(canvas, xTile, yTile, xPxLeft, yPxTop); + // clockwise from the top + int sideSize = 1; + for (int round = 0; round < tileRounds; round++) { + sideSize += 2; + xTile--; + yTile--; + xPxLeft -= 256; + yPxTop -= 256; + for (int i = 1; i < sideSize; i++) { + xTile++; + xPxLeft += 256; + drawTile(canvas, xTile, yTile, xPxLeft, yPxTop); + } + for (int i = 1; i < sideSize; i++) { + yTile++; + yPxTop += 256; + drawTile(canvas, xTile, yTile, xPxLeft, yPxTop); + } + for (int i = 1; i < sideSize; i++) { + xTile--; + xPxLeft -= 256; + drawTile(canvas, xTile, yTile, xPxLeft, yPxTop); + } + for (int i = 1; i < sideSize; i++) { + yTile--; + yPxTop -= 256; + drawTile(canvas, xTile, yTile, xPxLeft, yPxTop); + } + } + } + + private void drawTile(Canvas canvas, int xTile, int yTile, int xPxLeft, int yPxTop) { + if (xPxLeft < -256 || xPxLeft > xPxRight || yPxTop < -256 || yPxTop > yPxBottom) { + return; + } + if (xTile < 0) { + do { + xTile += totalTiles; + } while (xTile < 0); + } else if (xTile >= totalTiles) { + do { + xTile -= totalTiles; + } while (xTile >= totalTiles); + } + if (yTile < 0 || yTile >= totalTiles) { + return; + } + Bitmap bitmap = App.getBitmap(yTile, xTile); + if (bitmap != null) { + canvas.drawBitmap(bitmap, xPxLeft, yPxTop, null); + } + } + + private void drawTracks(Canvas canvas) { + drawTrack(canvas, App.getOpenedTrackIterator(), false); + Iterator iterator = App.getTrackIterator(); + long startTimestamp = SystemClock.elapsedRealtime(); + while (iterator.hasNext()) { + drawTrack(canvas, iterator.next(), true); + if (isMoving && SystemClock.elapsedRealtime() - startTimestamp > 5) { // keep moving the map smooth + break; + } + } + drawTrack(canvas, App.getCurrentTrackIterator(), false); + } + + private void drawTrack(Canvas canvas, Track track, boolean isSelected) { + if (track == null) { + return; + } + double x, xDeltaFromCenter; + float xPx, yPx, previousXPx = 0, previousYPx = 0; + boolean isFirstPoint = true; + boolean hasPreviousPoint = false; + boolean isLineStarted = false; + while (track.next()) { + x = track.getX(); + if (x != x) { // NaN + hasPreviousPoint = false; + isLineStarted = false; + continue; + } + xDeltaFromCenter = x - xCenter; + if (xDeltaFromCenter > 0.5) { + xDeltaFromCenter -= 1; + } else if (xDeltaFromCenter < -0.5) { + xDeltaFromCenter += 1; + } + xPx = (float) (xPxCenter + (xDeltaFromCenter * pxTotal)); + yPx = (float) (yPxCenter + ((track.getY() - yCenter) * pxTotal)); + if (xPx > 0 && xPx < xPxRight && yPx > 0 && yPx < yPxBottom) { + if (isFirstPoint && !isSelected) { + canvas.drawCircle(xPx, yPx, 2, redTrackPaint); + } + if (hasPreviousPoint) { + if (!isLineStarted) { + path.moveTo(previousXPx, previousYPx); + isLineStarted = true; + } + path.lineTo(xPx, yPx); + } else { + path.moveTo(xPx, yPx); + hasPreviousPoint = true; + isLineStarted = true; + } + } else { + if (isLineStarted) { + path.lineTo(xPx, yPx); + isLineStarted = false; + } + previousXPx = xPx; + previousYPx = yPx; + hasPreviousPoint = true; + } + isFirstPoint = false; + } + canvas.drawPath(path, isSelected ? selectedTracksPaint : redTrackPaint); + path.reset(); + } + + private void drawPoint(Canvas canvas) { + if (!hasPoint || textPoint == null) { + return; + } + float yPx = (float) (yPxCenter + ((yPoint - yCenter) * pxTotal)); + if (yPx <= 0 || yPx >= yPxBottom) { + return; + } + double xDeltaFromCenter = xPoint - xCenter; + if (xDeltaFromCenter > 0.5) { + xDeltaFromCenter -= 1; + } else if (xDeltaFromCenter < -0.5) { + xDeltaFromCenter += 1; + } + float xPx = (float) (xPxCenter + xDeltaFromCenter * pxTotal); + if (xPx <= 0 || xPx >= xPxRight) { + return; + } + canvas.drawCircle(xPx, yPx, 8, redTrackPaint); + canvas.drawLine( + xPx + cosPoint * 8, + yPx - sinPoint * 8, + xPx + cosPoint * 16, + yPx - sinPoint * 16, + redTrackPaint + ); + xPx -= xPxWidthTextPoint / 2; + if (xPx < 10) { + xPx = 10; + } else if (xPx > xPxRight - xPxWidthTextPoint - 10) { + xPx = xPxRight - xPxWidthTextPoint - 10; + } + if (yPx < yPxCenter) { + yPx += 31; + } else { + yPx -= 18; + } + canvas.drawText(textPoint, xPx, yPx, textPaint); + } + + private void drawLocation(Canvas canvas) { + float yPx = (float) (yPxCenter + ((yLocation - yCenter) * pxTotal)); + if (yPx <= 0 || yPx >= yPxBottom) { + return; + } + double xDeltaFromCenter = xLocation - xCenter; + if (xDeltaFromCenter > 0.5) { + xDeltaFromCenter -= 1; + } else if (xDeltaFromCenter < -0.5) { + xDeltaFromCenter += 1; + } + float xPx = (float) (xPxCenter + xDeltaFromCenter * pxTotal); + if (xPx < 0 || xPx > xPxRight) { + return; + } + canvas.drawCircle(xPx, yPx, 12, locationPaint); + canvas.drawCircle(xPx, yPx, 8, redTrackPaint); + canvas.drawLine(xPx, yPx + 8, xPx, yPx + 4, redTrackPaint); + canvas.drawLine(xPx, yPx - 8, xPx, yPx - 4, redTrackPaint); + canvas.drawLine(xPx + 8, yPx, xPx + 4, yPx, redTrackPaint); + canvas.drawLine(xPx - 8, yPx, xPx - 4, yPx, redTrackPaint); + } + + + public class Boundaries { + public final double xLeft; // xLeft > xRight, if includes longitude 180 + public final double xRight; + public final double yTop; // yTop < yBottom + public final double yBottom; + + private Boundaries() { + if (xPxRight > pxTotal) { + xLeft = 0; + xRight = 1; + } else { + xLeft = (xCenter - (double) xPxCenter / pxTotal + 1) % 1; + xRight = (xCenter + (double) xPxCenter / pxTotal) % 1; + } + yTop = Math.max(0, yCenter - (double) yPxCenter / pxTotal); + yBottom = Math.min(1, yCenter + (double) yPxCenter / pxTotal); + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/DialogExtra.java b/src/android/src/main/java/com/aqoleg/cat/DialogExtra.java new file mode 100644 index 0000000..6aecc74 --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/DialogExtra.java @@ -0,0 +1,408 @@ +/* +fragment with some extra info + */ +package com.aqoleg.cat; + +import android.annotation.SuppressLint; +import android.app.DialogFragment; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.location.GpsSatellite; +import android.location.GpsStatus; +import android.location.Location; +import android.location.LocationManager; +import android.net.Uri; +import android.os.Bundle; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.ClickableSpan; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; +import android.widget.Toast; +import com.aqoleg.cat.app.App; +import com.aqoleg.cat.data.Files; + +import java.util.Locale; + +import static android.content.Context.LOCATION_SERVICE; + +@SuppressWarnings("deprecation") +public class DialogExtra extends DialogFragment implements GpsStatus.Listener { + private LocationManager locationManager; + private GpsStatus gpsStatus; + private SatellitesView satellitesView; + private String pointLink; + private String centerTilePath; + + static DialogExtra newInstance() { + DialogExtra dialog = new DialogExtra(); + dialog.setStyle(DialogFragment.STYLE_NO_TITLE, R.style.dialog); + return dialog; + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + try { + locationManager = ((LocationManager) getActivity().getSystemService(LOCATION_SERVICE)); + View view = inflater.inflate(R.layout.dialog_extra, container, false); + satellitesView = new SatellitesView(getActivity().getApplicationContext()); + ((FrameLayout) view.findViewById(R.id.frame)).addView(satellitesView); + + String copy = getString(R.string.copy); + String send = getString(R.string.send); + SpannableString span = new SpannableString(String.format(getString(R.string.track), copy, send)); + span.setSpan( + new Clickable("copyTrack"), + span.length() - send.length() - 2 - copy.length(), + span.length() - send.length() - 2, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + span.setSpan( + new Clickable("sendTrack"), + span.length() - send.length(), + span.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + TextView textView = view.findViewById(R.id.track); + textView.setText(span); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + + if (App.hasPoint()) { + pointLink = String.format( + Locale.ENGLISH, + "cat.aqoleg.com/app?lon=%1$.6f&lat=%2$.6f&z=%3$d&map=%4$s", + App.getPointLongitude(), + App.getPointLatitude(), + App.getZ() + 1, + App.getMapName() + ); + span = new SpannableString(String.format(getString(R.string.point), pointLink)); + span.setSpan( + new Clickable("copyPoint"), + span.length() - pointLink.length(), + span.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + textView = view.findViewById(R.id.point); + textView.setText(span); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + } else { + view.findViewById(R.id.point).setVisibility(View.GONE); + } + + centerTilePath = App.getCenterTilePath(); + int indexOfLastSeparator = centerTilePath.lastIndexOf('/'); + span = new SpannableString(String.format(getString(R.string.tile), centerTilePath)); + span.setSpan( + new Clickable("openFolder"), + span.length() - centerTilePath.length(), + span.length() - centerTilePath.length() + indexOfLastSeparator, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + if (centerTilePath.indexOf('.') > indexOfLastSeparator) { + span.setSpan( + new Clickable("openTile"), + span.length() - centerTilePath.length() + indexOfLastSeparator + 1, + span.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + } + textView = view.findViewById(R.id.tile); + textView.setText(span); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + + span = new SpannableString(String.format(getString(R.string.visit), "cat.aqoleg.com")); + span.setSpan(new Clickable("openWeb"), span.length() - 14, span.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + textView = view.findViewById(R.id.website); + textView.setText(span); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + return view; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + + @SuppressLint("RtlHardcoded") + @Override + public void onStart() { + try { + super.onStart(); + getDialog().getWindow().setLayout( + (int) (192 * getActivity().getResources().getDisplayMetrics().density), + ViewGroup.LayoutParams.MATCH_PARENT + ); + getDialog().getWindow().setGravity(Gravity.RIGHT | Gravity.BOTTOM); + locationManager.addGpsStatusListener(this); + String text = String.format(getString(R.string.cacheText), App.getTileCacheSize(), App.getTrackCacheSize()); + ((TextView) getView().findViewById(R.id.cache)).setText(text); + App.registerForUpdate(this); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onStop() { + try { + super.onStop(); + locationManager.removeGpsStatusListener(this); + gpsStatus = null; + App.unregisterForUpdates(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onGpsStatusChanged(int event) { + try { + gpsStatus = locationManager.getGpsStatus(gpsStatus); + satellitesView.invalidate(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + + public void updateLocation(Location location) { + String text = String.format( + Locale.ENGLISH, + getString(R.string.location), + location.getLongitude(), + location.getLatitude(), + Math.round(location.getAltitude()), + Math.round(location.getAccuracy()), + Math.round(location.getSpeed() * 3.6f) + ); + ((TextView) getView().findViewById(R.id.location)).setText(text); + } + + + private void onSpanClick(String id) { + switch (id) { + case "copyTrack": + ClipboardManager clipboard = (ClipboardManager) getActivity().getApplicationContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + String track = App.getEncodedTrack(); + String url; + if (track == null) { + url = String.format( + Locale.ENGLISH, + "https://cat.aqoleg.com/app?lon=%1$.6f&lat=%2$.6f&z=%3$d", + App.getLocationLongitude(), + App.getLocationLatitude(), + App.getZ() + 1 + ); + } else { + url = String.format( + Locale.ENGLISH, + "https://cat.aqoleg.com/app?track=%1$s&z=%2$d", + track, + App.getZ() + 1 + ); + } + ClipData data = ClipData.newPlainText("cat track", url); + clipboard.setPrimaryClip(data); + Toast.makeText(getActivity().getApplicationContext(), R.string.copied, Toast.LENGTH_SHORT).show(); + break; + case "sendTrack": + track = App.getEncodedTrack(); + if (track == null) { + url = "https://writeme.aqoleg.com/?text=%3Ca%20href%3D%22https%3A%2F%2Fcat.aqoleg.com" + + "%2Fapp%3Flon%3D" + + String.format(Locale.ENGLISH, "%1$.6f", App.getLocationLongitude()) + + "%26lat%3D" + + String.format(Locale.ENGLISH, "%1$.6f", App.getLocationLatitude()) + + "%26z%3D" + + (App.getZ() + 1) + + "%22%3Epoint%3C%2Fa%3E"; + } else { + url = "https://writeme.aqoleg.com/?text=%3Ca%20href%3D%22https%3A%2F%2Fcat.aqoleg.com" + + "%2Fapp%3Ftrack%3D" + + track + + "%26z%3D" + + (App.getZ() + 1) + + "%22%3Etrack%3C%2Fa%3E"; + } + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + if (intent.resolveActivity(getActivity().getPackageManager()) != null) { + startActivity(intent); + } else { + Toast.makeText( + getActivity().getApplicationContext(), + R.string.noBrowser, + Toast.LENGTH_SHORT + ).show(); + } + break; + case "copyPoint": + clipboard = (ClipboardManager) getActivity().getApplicationContext() + .getSystemService(Context.CLIPBOARD_SERVICE); + data = ClipData.newPlainText("cat point", "https://" + pointLink); + clipboard.setPrimaryClip(data); + Toast.makeText(getActivity().getApplicationContext(), R.string.copied, Toast.LENGTH_SHORT).show(); + break; + case "openFolder": + intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType( + Uri.parse(centerTilePath.substring(0, centerTilePath.lastIndexOf('/'))), + "resource/folder" + ); + if (intent.resolveActivity(getActivity().getPackageManager()) != null) { + startActivity(intent); + } else { + Toast.makeText( + getActivity().getApplicationContext(), + R.string.noExplorer, + Toast.LENGTH_SHORT + ).show(); + } + break; + case "openTile": + intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.parse(centerTilePath), "image/*"); + if (intent.resolveActivity(getActivity().getPackageManager()) != null) { + startActivity(intent); + } else { + Toast.makeText( + getActivity().getApplicationContext(), + R.string.noViewer, + Toast.LENGTH_SHORT + ).show(); + } + break; + case "openWeb": + intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://cat.aqoleg.com")); + if (intent.resolveActivity(getActivity().getPackageManager()) != null) { + startActivity(intent); + } else { + Toast.makeText( + getActivity().getApplicationContext(), + R.string.noBrowser, + Toast.LENGTH_SHORT + ).show(); + } + break; + } + } + + + private class SatellitesView extends View { + private final Paint grid; + private final Paint usedSatellite; + private final Paint notUsedSatellite; + + private int xCenterPx; + private int yCenterPx; + private float outerRadiusPx; + private float middleRadiusPx; + private float innerRadiusPx; + private float positionScalePx; + private float usedSatelliteRadiusScalePx; + private float notUsedSatelliteRadiusPx; + + SatellitesView(Context context) { + super(context); + grid = new Paint(); + grid.setStyle(Paint.Style.STROKE); + grid.setStrokeWidth(1); + grid.setColor(Color.BLACK); + grid.setAntiAlias(true); + usedSatellite = new Paint(); + usedSatellite.setStyle(Paint.Style.FILL_AND_STROKE); + usedSatellite.setColor(Color.GREEN); + usedSatellite.setAntiAlias(true); + notUsedSatellite = new Paint(); + notUsedSatellite.setStyle(Paint.Style.FILL_AND_STROKE); + notUsedSatellite.setColor(Color.RED); + notUsedSatellite.setAntiAlias(true); + } + + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + try { + super.onLayout(changed, left, top, right, bottom); + if (!changed) { + return; + } + xCenterPx = getWidth() >> 1; + yCenterPx = getHeight() >> 1; + outerRadiusPx = yCenterPx - 10; + middleRadiusPx = outerRadiusPx * 2 / 3; + innerRadiusPx = outerRadiusPx / 3; + positionScalePx = outerRadiusPx / 90; + usedSatelliteRadiusScalePx = outerRadiusPx / 280; + notUsedSatelliteRadiusPx = outerRadiusPx / 60; + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + protected void onDraw(Canvas canvas) { + try { + super.onDraw(canvas); + canvas.drawCircle(xCenterPx, yCenterPx, outerRadiusPx, grid); + canvas.drawCircle(xCenterPx, yCenterPx, middleRadiusPx, grid); + canvas.drawCircle(xCenterPx, yCenterPx, innerRadiusPx, grid); + canvas.drawLine(xCenterPx, yCenterPx - innerRadiusPx, xCenterPx, yCenterPx - outerRadiusPx, grid); + canvas.drawLine(xCenterPx, yCenterPx + innerRadiusPx, xCenterPx, yCenterPx + outerRadiusPx, grid); + canvas.drawLine(xCenterPx - innerRadiusPx, yCenterPx, xCenterPx - outerRadiusPx, yCenterPx, grid); + canvas.drawLine(xCenterPx + innerRadiusPx, yCenterPx, xCenterPx + outerRadiusPx, yCenterPx, grid); + + int satellitesN = 0; + int satellitesUsed = 0; + if (gpsStatus != null) { + Iterable satellites = gpsStatus.getSatellites(); + for (GpsSatellite satellite : satellites) { + satellitesN++; + float radian = (-satellite.getAzimuth() + 90) * (float) Math.PI / 180; + float fromCenterPx = (-satellite.getElevation() + 90) * positionScalePx; + float xPx = xCenterPx + (float) Math.cos(radian) * fromCenterPx; + float yPx = yCenterPx - (float) Math.sin(radian) * fromCenterPx; + if (satellite.usedInFix()) { + satellitesUsed++; + canvas.drawCircle(xPx, yPx, satellite.getSnr() * usedSatelliteRadiusScalePx, usedSatellite); + } else { + canvas.drawCircle(xPx, yPx, notUsedSatelliteRadiusPx, notUsedSatellite); + } + } + } + String text = String.format(getString(R.string.satellites), satellitesUsed, satellitesN); + ((TextView) getView().findViewById(R.id.satellites)).setText(text); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + } + + private class Clickable extends ClickableSpan { + private final String id; + + private Clickable(String id) { + this.id = id; + } + + @Override + public void onClick(View widget) { + try { + onSpanClick(id); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/DialogMaps.java b/src/android/src/main/java/com/aqoleg/cat/DialogMaps.java new file mode 100644 index 0000000..2a826d6 --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/DialogMaps.java @@ -0,0 +1,139 @@ +/* +list with all maps to select + */ +package com.aqoleg.cat; + +import android.annotation.SuppressLint; +import android.app.DialogFragment; +import android.content.Context; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; +import com.aqoleg.cat.app.App; +import com.aqoleg.cat.data.Files; + +import java.util.ArrayList; + +public class DialogMaps extends DialogFragment implements AdapterView.OnItemClickListener { + private ArrayList mapNames; + private int colorNotSelected; + private int colorSelected; + + static DialogMaps newInstance() { + DialogMaps dialog = new DialogMaps(); + dialog.setStyle(DialogFragment.STYLE_NO_TITLE, R.style.dialog); + return dialog; + } + + + @SuppressWarnings("deprecation") + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + try { + View view = inflater.inflate(R.layout.dialog_maps, container, false); + ((ListView) view.findViewById(R.id.list)).setOnItemClickListener(this); + colorNotSelected = getResources().getColor(R.color.mainBlack); + colorSelected = getResources().getColor(R.color.mainRed); + return view; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + + @SuppressLint("RtlHardcoded") + @Override + public void onStart() { + try { + super.onStart(); + getDialog().getWindow().setLayout( + (int) (192 * getActivity().getResources().getDisplayMetrics().density), + ViewGroup.LayoutParams.WRAP_CONTENT + ); + getDialog().getWindow().setGravity(Gravity.RIGHT | Gravity.BOTTOM); + if (mapNames == null) { + mapNames = Files.getInstance().getMapNames(); + } + Adapter adapter = new Adapter(getActivity().getApplicationContext()); + ((ListView) getView().findViewById(R.id.list)).setAdapter(adapter); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + try { + if (!mapNames.get(position).equals(App.getMapName())) { + App.selectMap(mapNames.get(position)); + dismiss(); + } + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + + private class Adapter extends BaseAdapter { + private final LayoutInflater inflater; + + Adapter(Context context) { + inflater = LayoutInflater.from(context); + } + + + @Override + public int getCount() { + try { + return mapNames.size(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + return 0; + } + + @Override + public Object getItem(int position) { + try { + return mapNames.get(position); + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + try { + TextView view; + if (convertView == null) { + view = (TextView) inflater.inflate(R.layout.list_item, parent, false); + } else { + view = (TextView) convertView; + } + String mapName = mapNames.get(position); + view.setText(mapName); + if (mapName.equals(App.getMapName())) { + view.setTextColor(colorSelected); + } else { + view.setTextColor(colorNotSelected); + } + return view; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/DialogTracks.java b/src/android/src/main/java/com/aqoleg/cat/DialogTracks.java new file mode 100644 index 0000000..6ddbf8f --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/DialogTracks.java @@ -0,0 +1,247 @@ +/* +list with all tracks to select, search, delete + */ +package com.aqoleg.cat; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; +import com.aqoleg.cat.app.App; +import com.aqoleg.cat.data.Files; + +import java.util.ArrayList; + +public class DialogTracks extends DialogFragment implements AdapterView.OnItemClickListener, View.OnClickListener { + private String title; + private ArrayList trackNames; + private Adapter adapter; + private int colorNotSelected; + private int colorSelected; + + static DialogTracks newInstance() { + DialogTracks dialog = new DialogTracks(); + dialog.setStyle(DialogFragment.STYLE_NO_TITLE, R.style.dialog); + return dialog; + } + + + @SuppressWarnings("deprecation") + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + try { + View view = inflater.inflate(R.layout.dialog_tracks, container, false); + title = getResources().getString(R.string.tracksTitle); + ((ListView) view.findViewById(R.id.list)).setOnItemClickListener(this); + colorNotSelected = getResources().getColor(R.color.mainBlack); + colorSelected = getResources().getColor(R.color.tracksPurple); + view.findViewById(R.id.delete).setOnClickListener(this); + view.findViewById(R.id.deselect).setOnClickListener(this); + view.findViewById(R.id.searchUp).setOnClickListener(this); + return view; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + + @SuppressLint("RtlHardcoded") + @Override + public void onStart() { + try { + super.onStart(); + getDialog().getWindow().setLayout( + (int) (192 * getActivity().getResources().getDisplayMetrics().density), + ViewGroup.LayoutParams.MATCH_PARENT + ); + getDialog().getWindow().setGravity(Gravity.RIGHT | Gravity.BOTTOM); + if (trackNames == null) { + trackNames = Files.getInstance().getTrackNames(); + } + setTracksTitle(); + adapter = new Adapter(getActivity().getApplicationContext()); + ListView listView = getView().findViewById(R.id.list); + listView.setAdapter(adapter); + listView.setSelection(App.getTracksPosition()); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + try { + super.onSaveInstanceState(outState); + App.setTracksPosition(((ListView) getView().findViewById(R.id.list)).getFirstVisiblePosition()); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onStop() { + try { + super.onStop(); + App.setTracksPosition(((ListView) getView().findViewById(R.id.list)).getFirstVisiblePosition()); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + try { + App.changeTrackVisibility(trackNames.get(position)); + setTracksTitle(); + adapter.notifyDataSetChanged(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onClick(View view) { + try { + switch (view.getId()) { + case R.id.delete: + new DialogDelete().show(); + break; + case R.id.deselect: + App.deselectAllTracks(); + setTracksTitle(); + adapter.notifyDataSetChanged(); + break; + case R.id.searchUp: + int stopPos = ((ListView) getView().findViewById(R.id.list)).getLastVisiblePosition(); + App.searchUpVisibleTracks(trackNames, stopPos); + dismiss(); + break; + } + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + + private void setTracksTitle() { + String text = String.format(title, App.getNumberOfSelectedTracks(), trackNames.size()); + ((TextView) getView().findViewById(R.id.title)).setText(text); + } + + + private class Adapter extends BaseAdapter { + private final LayoutInflater inflater; + + Adapter(Context context) { + inflater = LayoutInflater.from(context); + } + + + @Override + public int getCount() { + try { + return trackNames.size(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + return 0; + } + + @Override + public Object getItem(int position) { + try { + return trackNames.get(position); + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + try { + TextView view; + if (convertView == null) { + view = (TextView) inflater.inflate(R.layout.list_item, parent, false); + } else { + view = (TextView) convertView; + } + String trackName = trackNames.get(position); + view.setText(trackName.substring(0, trackName.length() - 4)); + if (App.isTrackSelected(trackName)) { + view.setTextColor(colorSelected); + } else { + view.setTextColor(colorNotSelected); + } + return view; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + } + + private class DialogDelete extends Dialog implements View.OnClickListener { + + DialogDelete() { + super(getActivity(), R.style.prompt); + } + + + @Override + protected void onCreate(Bundle savedInstanceState) { + try { + super.onCreate(savedInstanceState); + setContentView(R.layout.dialog_delete); + int n = App.getNumberOfSelectedTracks(); + String deleteTitle; + if (n == 0) { + deleteTitle = getString(R.string.selectTracks); + } else if (n == 1) { + deleteTitle = getString(R.string.deleteTrack); + } else { + deleteTitle = String.format(getString(R.string.deleteTracks), n); + } + ((TextView) findViewById(R.id.title)).setText(deleteTitle); + findViewById(R.id.cancel).setOnClickListener(this); + findViewById(R.id.ok).setOnClickListener(this); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onClick(View view) { + try { + switch (view.getId()) { + case R.id.cancel: + dismiss(); + break; + case R.id.ok: + App.deleteSelectedTracks(); + trackNames = Files.getInstance().getTrackNames(); + setTracksTitle(); + adapter.notifyDataSetChanged(); + dismiss(); + break; + } + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/ServiceMain.java b/src/android/src/main/java/com/aqoleg/cat/ServiceMain.java new file mode 100644 index 0000000..8b665cd --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/ServiceMain.java @@ -0,0 +1,90 @@ +/* +single foreground service +*/ +package com.aqoleg.cat; + +import android.app.*; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import com.aqoleg.cat.app.App; +import com.aqoleg.cat.data.Files; + +public class ServiceMain extends Service implements LocationListener { + private LocationManager locationManager; + + @SuppressWarnings("deprecation") + @Override + public void onCreate() { + try { + super.onCreate(); + Intent startActivityIntent = new Intent(getApplicationContext(), ActivityMain.class); + Notification.Builder builder = new Notification.Builder(getApplicationContext()) + .setSmallIcon(R.drawable.notification) + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.icon)) + .setContentTitle(getString(R.string.serviceDescription)) + .setContentIntent(PendingIntent.getActivity(getApplicationContext(), 0, startActivityIntent, 0)); + if (Build.VERSION.SDK_INT >= 26) { + NotificationChannel channel = new NotificationChannel("cat", "cat", NotificationManager.IMPORTANCE_LOW); + getApplicationContext().getSystemService(NotificationManager.class).createNotificationChannel(channel); + builder.setChannelId("cat"); + } + startForeground(1, builder.getNotification()); + + locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); + // loses satellites if delay > 0 + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); + + App.beginTrackLog(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onDestroy() { + try { + super.onDestroy(); + locationManager.removeUpdates(this); + App.endTrackLog(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onLocationChanged(Location location) { + try { + App.updateLocation(location); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + + @Override + public void onProviderEnabled(String provider) { + } + + @Override + public void onProviderDisabled(String provider) { + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/app/App.java b/src/android/src/main/java/com/aqoleg/cat/app/App.java new file mode 100644 index 0000000..6c0fa48 --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/app/App.java @@ -0,0 +1,556 @@ +/* +the app main center, one single instance + +tile's parameters: +z - zoom from 0 to 17, displayed zoom = (z + 1) +x, y - tiles number, tile(0, 0) is the top left tile, from 0 to (2^z - 1) + */ +package com.aqoleg.cat.app; + +import android.graphics.Bitmap; +import android.location.Location; +import com.aqoleg.cat.ActivityMain; +import com.aqoleg.cat.ActivityView; +import com.aqoleg.cat.DialogExtra; +import com.aqoleg.cat.data.Files; +import com.aqoleg.cat.data.Map; +import com.aqoleg.cat.data.Track; +import com.aqoleg.cat.data.UriData; +import com.aqoleg.cat.utils.Projection; + +import java.util.ArrayList; +import java.util.Iterator; + +public class App { + private static Location lastLocation; // non-filtered last update or null at start + // service + private static long lastUpdateTime; // unix timestamp + private static Location lastTrackLocation; + private static Location bestTrackLocation; + private static Track.Current currentTrack; + private static float totalDistance; + private static float localDistance; + // activity + private static ActivityMain activityMain; + private static ActivityView activityView; + private static Map map; + private static int z; + private static Tiles tiles; + private static Location viewLocation; + private static Track.Opened openedTrack; // null if there is no opened track + private static Tracks selectedTracks; + private static Location pointLocation; // null if there is no point + private static boolean isVisible; + // tracks + private static int tracksPosition; + // extra + private static DialogExtra dialogExtra; + + // service + + // call once at start service + public static void beginTrackLog() { + lastUpdateTime = 0; + lastTrackLocation = null; + bestTrackLocation = null; + currentTrack = Track.loadCurrent(); + totalDistance = 0; + localDistance = 0; + } + + // call at each location update (1 Hz average) + public static void updateLocation(Location location) { + lastLocation = location; + boolean toRefresh = false; + // current track + if (lastUpdateTime == 0) { + lastUpdateTime = location.getTime(); + } + if (bestTrackLocation == null) { + bestTrackLocation = location; + } + int timeSincePreviousUpdate = (int) (location.getTime() - lastUpdateTime); + if (timeSincePreviousUpdate > 60000) { + lastUpdateTime = location.getTime(); + bestTrackLocation = location; + currentTrack.addSegmentDelimiter(); + } else if (timeSincePreviousUpdate > 8000) { + if (location.getAccuracy() < bestTrackLocation.getAccuracy()) { + bestTrackLocation = location; + } + float distance = lastTrackLocation == null ? 0 : lastTrackLocation.distanceTo(bestTrackLocation); + if (lastTrackLocation != null && distance < 50) { + lastUpdateTime = location.getTime() - 4000; + bestTrackLocation = location; + } else if (lastTrackLocation == null || distance > bestTrackLocation.getAccuracy() * 4) { + lastUpdateTime = bestTrackLocation.getTime(); + currentTrack.addPoint( + bestTrackLocation.getLongitude(), + bestTrackLocation.getLatitude(), + (int) bestTrackLocation.getAltitude(), + bestTrackLocation.getTime() + ); + lastTrackLocation = bestTrackLocation; + bestTrackLocation = location; + if (distance > 0) { + totalDistance += distance; + localDistance += distance; + if (isVisible) { + activityMain.printDistance(totalDistance, localDistance); + } + } + toRefresh = true; + } + } else if (timeSincePreviousUpdate > 4000 && location.getAccuracy() < bestTrackLocation.getAccuracy()) { + bestTrackLocation = location; + } + // activity + if (isVisible) { + if (lastLocation.distanceTo(viewLocation) > 20) { + viewLocation = lastLocation; + activityView.setLocation( + Projection.getX(viewLocation.getLongitude()), + Projection.getY(viewLocation.getLatitude(), map.ellipsoid) + ); + if (pointLocation != null) { + activityView.setBearingAndDistance( + pointLocation.bearingTo(viewLocation), + viewLocation.bearingTo(pointLocation), + viewLocation.distanceTo(pointLocation)); + } + toRefresh = true; + } + } + + if (toRefresh && isVisible) { + activityView.draw(); + } + if (dialogExtra != null) { + dialogExtra.updateLocation(location); + } + } + + // call once at destroy service + public static void endTrackLog() { + lastUpdateTime = 0; + lastTrackLocation = null; + bestTrackLocation = null; + if (currentTrack != null) { + currentTrack.close(); + } + currentTrack = null; + totalDistance = 0; + localDistance = 0; + } + + // activityMain + + // call start() after this + public static void load( + ActivityMain activityMain, + ActivityView activityView, + String mapName, // can be null + int z, + double centerLongitude, + double centerLatitude, + float locationLongitude, + float locationLatitude, + String openedTrack, // can be null + String[] selectedTracks, + boolean hasPoint, + double pointLongitude, + double pointLatitude, + int tracksPosition, + UriData uriData // can be null + ) { + if (lastLocation == null) { + lastLocation = new Location(""); + lastLocation.setLongitude(locationLongitude); + lastLocation.setLatitude(locationLatitude); + } + App.activityMain = activityMain; + App.activityView = activityView; + + if (uriData != null) { + if (uriData.newMapName != null) { + String newMapName = Map.save(uriData.newMapName, uriData.newMapUrl, uriData.newMapProjection); + if (newMapName != null) { + mapName = newMapName; + } + } else if (uriData.mapName != null) { + mapName = uriData.mapName; + } + if (uriData.hasZ) { + z = uriData.z; + } + if (uriData.track != null) { + openedTrack = uriData.track; + } + if (uriData.hasPoint) { + hasPoint = true; + pointLongitude = uriData.pointLongitude; + pointLatitude = uriData.pointLatitude; + centerLongitude = (float) pointLongitude; + centerLatitude = (float) pointLatitude; + } + } + + map = Map.load(mapName); + App.z = z; + tiles = new Tiles(activityMain.getWindowManager().getDefaultDisplay()); + App.openedTrack = Track.open(openedTrack); + App.selectedTracks = Tracks.load(selectedTracks); + if (hasPoint) { + pointLocation = new Location(""); + pointLocation.setLongitude(pointLongitude); + pointLocation.setLatitude(pointLatitude); + } else { + pointLocation = null; + } + App.tracksPosition = tracksPosition; + isVisible = false; + + activityMain.printZoom(z); + activityView.setZoom(z); + if (uriData != null && uriData.track != null && !uriData.hasPoint && App.openedTrack != null) { + activityView.setCenter(App.openedTrack.getEndX(), App.openedTrack.getEndY(map.ellipsoid)); + } else { + activityView.setCenter(Projection.getX(centerLongitude), Projection.getY(centerLatitude, map.ellipsoid)); + } + if (pointLocation != null) { + activityView.setPoint( + Projection.getX(pointLocation.getLongitude()), + Projection.getY(pointLocation.getLatitude(), map.ellipsoid) + ); + } + } + + public static void open(UriData uriData) { + if (uriData == null) { + return; + } + String mapName = null; + if (uriData.newMapName != null) { + mapName = Map.save(uriData.newMapName, uriData.newMapUrl, uriData.newMapProjection); + } else if (uriData.mapName != null) { + mapName = uriData.mapName; + } + boolean prevEllipsoid = map.ellipsoid; + if (mapName != null) { + map = Map.load(mapName); + } + if (uriData.hasZ) { + z = uriData.z; + activityMain.printZoom(z); + activityView.setZoom(z); + } + if (uriData.track != null) { + openedTrack = Track.open(uriData.track); + if (openedTrack != null && !uriData.hasPoint) { + activityView.setCenter(openedTrack.getEndX(), openedTrack.getEndY(map.ellipsoid)); + } + } + if (uriData.hasPoint) { + pointLocation = new Location(""); + pointLocation.setLongitude(uriData.pointLongitude); + pointLocation.setLatitude(uriData.pointLatitude); + double x = Projection.getX(uriData.pointLongitude); + double y = Projection.getY(uriData.pointLatitude, map.ellipsoid); + activityView.setCenter(x, y); + activityView.setPoint(x, y); + } + if (map.ellipsoid != prevEllipsoid) { + activityView.setLocation( + Projection.getX(viewLocation.getLongitude()), + Projection.getY(viewLocation.getLatitude(), map.ellipsoid) + ); + if ((uriData.track == null || openedTrack == null) && !uriData.hasPoint) { + activityView.setCenter( + activityView.getXCenter(), + Projection.getY(Projection.getLatitude(activityView.getYCenter(), prevEllipsoid), map.ellipsoid) + ); + } + } + } + + // start/stop refresh with last location + public static void setVisible(boolean isVisible) { + if (isVisible) { + activityMain.printDistance(totalDistance, localDistance); + viewLocation = new Location(lastLocation); + activityView.setLocation( + Projection.getX(viewLocation.getLongitude()), + Projection.getY(viewLocation.getLatitude(), map.ellipsoid) + ); + if (pointLocation != null) { + activityView.setBearingAndDistance( + pointLocation.bearingTo(viewLocation), + viewLocation.bearingTo(pointLocation), + viewLocation.distanceTo(pointLocation) + ); + } + activityView.draw(); + } + App.isVisible = isVisible; + } + + // clear resources + public static void unload() { + activityMain = null; + activityView = null; + map = null; + tiles.unload(); + tiles = null; + viewLocation = null; + openedTrack = null; + selectedTracks.unload(); + selectedTracks = null; + pointLocation = null; + isVisible = false; + } + + public static void clearLocalDistance() { + localDistance = 0; + activityMain.printDistance(totalDistance, localDistance); + } + + public static void changeZoom(int delta) { + int newZ = z + delta; + if (newZ > 17) { + newZ = 17; + } else if (newZ < 0) { + newZ = 0; + } + z = newZ; + activityMain.printZoom(z); + activityView.setZoom(z); + activityView.draw(); + } + + public static void center() { + viewLocation = new Location(lastLocation); + double x = Projection.getX(viewLocation.getLongitude()); + double y = Projection.getY(viewLocation.getLatitude(), map.ellipsoid); + activityView.setCenter(x, y); + activityView.setLocation(x, y); + if (pointLocation != null) { + activityView.setBearingAndDistance( + pointLocation.bearingTo(viewLocation), + viewLocation.bearingTo(pointLocation), + viewLocation.distanceTo(pointLocation) + ); + } + activityView.draw(); + } + + public static void searchVisibleTracks() { + activityMain.changeTrackButtonAvailability(false); + selectedTracks.searchVisible(null, -1, activityView.getBoundaries(), map.ellipsoid); + } + + public static String getMapName() { + return map.name; + } + + public static int getZ() { + return z; + } + + public static float getCenterLongitude() { + return (float) Projection.getLongitude(activityView.getXCenter()); + } + + public static float getCenterLatitude() { + return (float) Projection.getLatitude(activityView.getYCenter(), map.ellipsoid); + } + + public static float getLocationLongitude() { + return (float) lastLocation.getLongitude(); + } + + public static float getLocationLatitude() { + return (float) lastLocation.getLatitude(); + } + + public static String getOpenedTrack() { + return openedTrack == null ? null : openedTrack.pathOrEncoded; + } + + public static String[] getSelectedTracks() { + return selectedTracks.getTrackNames(); + } + + public static boolean hasPoint() { + return pointLocation != null; + } + + public static double getPointLongitude() { + return pointLocation == null ? 0 : pointLocation.getLongitude(); + } + + public static double getPointLatitude() { + return pointLocation == null ? 0 : pointLocation.getLatitude(); + } + + public static int getTracksPosition() { + return tracksPosition; + } + + // activityView + + // remove selection without drawing + public static void deselect() { + pointLocation = null; + } + + // set selection without drawing + public static void select(double xSelection, double ySelection) { + pointLocation = new Location(""); + pointLocation.setLongitude(Projection.getLongitude(xSelection)); + pointLocation.setLatitude(Projection.getLatitude(ySelection, map.ellipsoid)); + activityView.setPoint(xSelection, ySelection); + activityView.setBearingAndDistance( + pointLocation.bearingTo(viewLocation), + viewLocation.bearingTo(pointLocation), + viewLocation.distanceTo(pointLocation) + ); + } + + public static Bitmap getBitmap(int y, int x) { + return tiles.getBitmap(map, z, y, x); + } + + public static Track getOpenedTrackIterator() { + if (openedTrack != null) { + openedTrack.startPointIterator(map.ellipsoid); + } + return openedTrack; + } + + public static Iterator getTrackIterator() { + return selectedTracks.getTrackIterator(map.ellipsoid); + } + + public static Track getCurrentTrackIterator() { + if (currentTrack != null) { + currentTrack.startPointIterator(map.ellipsoid); + } + return currentTrack; + } + + // dialogMaps + + public static void selectMap(String mapName) { + boolean prevEllipsoid = map.ellipsoid; + map = Map.load(mapName); + if (map.ellipsoid != prevEllipsoid) { + activityView.setLocation( + Projection.getX(viewLocation.getLongitude()), + Projection.getY(viewLocation.getLatitude(), map.ellipsoid) + ); + activityView.setCenter( + activityView.getXCenter(), + Projection.getY(Projection.getLatitude(activityView.getYCenter(), prevEllipsoid), map.ellipsoid) + ); + if (pointLocation != null) { + activityView.setPoint( + Projection.getX(pointLocation.getLongitude()), + Projection.getY(pointLocation.getLatitude(), map.ellipsoid) + ); + } + } + activityView.draw(); + } + + // dialogTracks + + public static void setTracksPosition(int position) { + tracksPosition = position; + } + + public static int getNumberOfSelectedTracks() { + return selectedTracks.getSelectedCount(); + } + + public static boolean isTrackSelected(String trackName) { + return selectedTracks.isSelected(trackName); + } + + public static void changeTrackVisibility(String trackName) { + selectedTracks.changeVisibility(trackName); + } + + public static void deleteSelectedTracks() { + selectedTracks.deleteSelected(); + } + + public static void deselectAllTracks() { + selectedTracks.deselectAll(); + } + + public static void searchUpVisibleTracks(ArrayList tracks, int stopPos) { + activityMain.changeTrackButtonAvailability(false); + selectedTracks.searchVisible(tracks, stopPos, activityView.getBoundaries(), map.ellipsoid); + } + + // dialogExtra + + public static void registerForUpdate(DialogExtra dialogExtra) { + App.dialogExtra = dialogExtra; + } + + public static void unregisterForUpdates() { + dialogExtra = null; + } + + // returns encoded current track or null + public static String getEncodedTrack() { + return currentTrack.getEncoded(); + } + + public static int getTileCacheSize() { + return tiles.cacheSize; + } + + public static int getTrackCacheSize() { + return selectedTracks.getCacheSize(); + } + + public static String getCenterTilePath() { + return Files.getInstance().getTilePathOrName( + map.name, + z, + activityView.getYTileCenter(), + activityView.getXTileCenter() + ); + } + + // loader callbacks + + static void refresh() { + if (activityView != null && isVisible) { + activityView.draw(); + } + } + + static void finishLoadingTracks() { + if (activityMain != null && activityView != null) { + activityMain.changeTrackButtonAvailability(true); + if (isVisible) { + activityView.draw(); + } + } + } + + static void centerOnTrack(Track track) { + if (activityView != null) { + double x = track.getStartX(); + if (x == x) { + activityView.setCenter(x, track.getStartY(map.ellipsoid)); + } + if (isVisible) { + activityView.draw(); + } + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/app/Tiles.java b/src/android/src/main/java/com/aqoleg/cat/app/Tiles.java new file mode 100644 index 0000000..06577ee --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/app/Tiles.java @@ -0,0 +1,327 @@ +/* +handles tile cache, reads tiles from storage and downloads tiles + */ +package com.aqoleg.cat.app; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.AsyncTask; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.Display; +import com.aqoleg.cat.data.Files; +import com.aqoleg.cat.data.Map; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.Socket; +import java.net.URL; + +class Tiles { + final int cacheSize; + private final BitmapFactory.Options options = new BitmapFactory.Options(); + private final Files files = Files.getInstance(); + private final Tile[] cache; + + private int nextStackLevel; + private Reader reader; + private Downloader downloader; + + Tiles(Display display) { + options.inPreferredConfig = Bitmap.Config.RGB_565; // 2 bytes/px + DisplayMetrics displayMetrics = new DisplayMetrics(); + display.getMetrics(displayMetrics); + // 3 full screens of tiles + int cacheSize = (displayMetrics.heightPixels / 256 + 2) * (displayMetrics.widthPixels / 256 + 2) * 3; + if (cacheSize < 50) { + cacheSize = 50; + } + this.cacheSize = cacheSize; + cache = new Tile[cacheSize]; + } + + + // returns bitmap from the cache, puts it on the top of the stack and starts downloader (if available) + // if there is no such tile in the cache, returns null and asynchronously loads it from storage + // if there is no such tile in the storage, gets it using lower zooms + Bitmap getBitmap(Map map, int z, int y, int x) { + for (Tile tile : cache) { + if (tile == null) { + break; + } + if (tile.equals(map, z, y, x)) { + tile.stackLevel = nextStackLevel++; // put on the top of the stack + if (tile.url != null && downloader == null) { + downloader = new Downloader(tile); + } + return tile.bitmap; + } + } + // if tile have not found in the cache, start reader + if (reader == null) { + reader = new Reader(map, z, y, x); + } + return null; + } + + void unload() { + if (reader != null) { + reader.cancel(true); + reader = null; + } + if (downloader != null) { + downloader.cancel(true); + downloader = null; + } + } + + + private class Reader extends AsyncTask { + private final Tile tile; + + private Reader(Map map, int z, int y, int x) { + tile = new Tile(map, z, y, x); + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + + @Override + protected Void doInBackground(Void... voids) { + try { + tile.bitmap = loadBitmap(tile.z, tile.y, tile.x); + if (tile.bitmap != null) { + return null; + } + tile.url = tile.map.getUrl(tile.z, tile.y, tile.x); + // try to fill the tile using lower zooms + int pxSize = 256; + int xPxLeft = 0, yPxTop = 0; + int zTile = tile.z; + int yTile = tile.y; + int xTile = tile.x; + for (int i = 0; i < 5; i++) { + if (zTile-- == 0) { + break; + } + pxSize = pxSize >> 1; + + xPxLeft = xPxLeft >> 1; + if ((xTile & 0b1) == 1) { + xPxLeft += 128; // right half of the tile + } + xTile = xTile >> 1; + + yPxTop = yPxTop >> 1; + if ((yTile & 0b1) == 1) { + yPxTop += 128; // bottom half of the tile + } + yTile = yTile >> 1; + + tile.bitmap = loadBitmap(zTile, yTile, xTile); + if (tile.bitmap != null) { + tile.bitmap = Bitmap.createBitmap(tile.bitmap, xPxLeft, yPxTop, pxSize, pxSize); + tile.bitmap = Bitmap.createScaledBitmap(tile.bitmap, 256, 256, true); + return null; + } + } + return null; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + try { + super.onPostExecute(aVoid); + int cachePos = 0; // replace the bottom item + int stackLevel, lowestStackLevel = nextStackLevel; + for (int i = 0; i < cacheSize; i++) { + if (cache[i] == null) { + cachePos = i; + break; + } + stackLevel = cache[i].stackLevel; + if (stackLevel < lowestStackLevel) { + cachePos = i; + lowestStackLevel = stackLevel; + } + } + tile.stackLevel = nextStackLevel++; + cache[cachePos] = tile; + reader = null; + App.refresh(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + + + private Bitmap loadBitmap(int z, int y, int x) { + return BitmapFactory.decodeFile(files.getTilePath(tile.map.name, z, y, x), options); + } + } + + private class Downloader extends AsyncTask { + private final Tile tile; + private Bitmap bitmap; + + private Downloader(Tile tile) { + this.tile = tile; + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + + @Override + protected Void doInBackground(Void... voids) { + HttpURLConnection connection = null; + InputStream inputStream = null; + try { + connection = (HttpURLConnection) tile.url.openConnection(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (tile.url.getProtocol().equals("https")) { + ((HttpsURLConnection) connection).setSSLSocketFactory(new TlsSocketFactory()); + } + } + connection.setConnectTimeout(10000); + connection.setReadTimeout(15000); + connection.setRequestProperty("User-Agent", "cat.aqoleg.com"); + inputStream = connection.getInputStream(); + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException(connection.getResponseMessage()); + } + + ByteArrayOutputStream byteArray = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length = inputStream.read(buffer); + while (length >= 0) { + byteArray.write(buffer, 0, length); + length = inputStream.read(buffer); + } + byte[] tileBytes = byteArray.toByteArray(); // use bytes, because of creating both file and bitmap + + bitmap = BitmapFactory.decodeByteArray(tileBytes, 0, tileBytes.length, options); + if (bitmap == null) { + files.logOnce("Tiles.Downloader.doInBackground.0", "no bitmap for " + tile.url); + } else { + String extension = connection.getContentType(); // image/png, image/jpeg + if (extension == null) { + extension = "jpeg"; + } else { + extension = extension.substring(extension.lastIndexOf('/') + 1); + } + files.saveTile(tileBytes, tile.map.name, tile.z, tile.y, tile.x, extension); + } + } catch (Throwable t) { + files.logOnce("Tiles.Downloader.doInBackground.1", "cannot download " + tile.url + ": " + t); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignored) { + } + } + if (connection != null) { + connection.disconnect(); + } + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + try { + super.onPostExecute(aVoid); + if (bitmap != null) { // do not remove preview if bitmap has not been loaded + tile.bitmap = bitmap; + } + tile.url = null; + downloader = null; + App.refresh(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + } + + private class Tile { + private final Map map; + private final int z; + private final int y; + private final int x; + private Bitmap bitmap; + private int stackLevel; + private URL url; + + private Tile(Map map, int z, int y, int x) { + this.map = map; + this.z = z; + this.y = y; + this.x = x; + } + + + private boolean equals(Map map, int z, int y, int x) { + return this.map.name.equals(map.name) && this.z == z && this.y == y && this.x == x; + } + } + + private class TlsSocketFactory extends SSLSocketFactory { + private final SSLSocketFactory socketFactory = HttpsURLConnection.getDefaultSSLSocketFactory(); + + @Override + public String[] getDefaultCipherSuites() { + return socketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return socketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return enableTls(socketFactory.createSocket()); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return enableTls(socketFactory.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return enableTls(socketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + return enableTls(socketFactory.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return enableTls(socketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return enableTls(socketFactory.createSocket(address, port, localAddress, localPort)); + } + + + private Socket enableTls(Socket socket) { + if (socket != null && (socket instanceof SSLSocket)) { + ((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.1", "TLSv1.2"}); + } + return socket; + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/app/Tracks.java b/src/android/src/main/java/com/aqoleg/cat/app/Tracks.java new file mode 100644 index 0000000..90b9fac --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/app/Tracks.java @@ -0,0 +1,282 @@ +/* +handles track cache and selected track list + */ +package com.aqoleg.cat.app; + +import android.os.AsyncTask; +import com.aqoleg.cat.ActivityView; +import com.aqoleg.cat.data.Files; +import com.aqoleg.cat.data.Track; + +import java.util.*; + +class Tracks { + private final WeakHashMap cache = new WeakHashMap<>(); // synchronize it! + + private HashMap selected = new HashMap<>(); // main thread only, track == null if not loaded + private Opener opener; + private Loader loader; + private Searcher searcher; + + private Tracks() { + } + + + static Tracks load(String[] trackNames) { + Tracks tracks = new Tracks(); + for (String trackName : trackNames) { + tracks.selected.put(trackName, null); + } + tracks.opener = tracks.new Opener(trackNames); + return tracks; + } + + + void changeVisibility(String trackName) { + if (selected.containsKey(trackName)) { + if (selected.remove(trackName) != null) { + App.refresh(); + } + } else { + Track track; + synchronized (cache) { + track = cache.get(trackName); + } + selected.put(trackName, track); + if (track != null) { + App.centerOnTrack(track); + } else if (opener == null && loader == null) { + loader = new Loader(trackName); + } + } + } + + void deselectAll() { + selected = new HashMap<>(); + unload(); + App.refresh(); + } + + void searchVisible(ArrayList tracks, int stopPos, ActivityView.Boundaries boundaries, boolean ellipsoid) { + if (searcher != null) { + searcher.cancel(true); + } + searcher = new Searcher(tracks, stopPos, boundaries, ellipsoid); + } + + int getCacheSize() { + synchronized (cache) { + return cache.size(); + } + } + + int getSelectedCount() { + return selected.size(); + } + + String[] getTrackNames() { + return selected.keySet().toArray(new String[selected.size()]); + } + + boolean isSelected(String trackName) { + return selected.containsKey(trackName); + } + + void deleteSelected() { + for (Map.Entry entry : selected.entrySet()) { + Files.getInstance().deleteTrack(entry.getKey()); + } + selected = new HashMap<>(); + App.refresh(); + } + + Iterator getTrackIterator(final boolean ellipsoid) { + final Iterator> iterator = selected.entrySet().iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + // returns track or null + @Override + public Track next() { + Map.Entry entry = iterator.next(); + Track track = entry.getValue(); + if (track != null) { + track.startPointIterator(ellipsoid); + return track; + } else if (opener == null && loader == null) { + loader = new Loader(entry.getKey()); + } + return null; + } + }; + } + + void unload() { + if (opener != null) { + opener.cancel(true); + opener = null; + } + if (loader != null) { + loader.cancel(true); + loader = null; + } + if (searcher != null) { + searcher.cancel(true); + searcher = null; + } + } + + + private class Opener extends AsyncTask { + private final String[] trackNames; + private final HashMap openedTracks = new HashMap<>(); + + private Opener(String[] trackNames) { + this.trackNames = trackNames; + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + + @Override + protected Void doInBackground(Void... voids) { + try { + for (String trackName : trackNames) { + Track track = Track.load(trackName); + openedTracks.put(trackName, track); + synchronized (cache) { + cache.put(trackName, track); + } + } + return null; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + try { + super.onPostExecute(result); + Set> entrySet = openedTracks.entrySet(); + for (Map.Entry entry : entrySet) { + if (selected.containsKey(entry.getKey())) { + selected.put(entry.getKey(), entry.getValue()); + } + } + opener = null; + App.finishLoadingTracks(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + } + + private class Loader extends AsyncTask { + private final String trackName; + private Track loadedTrack; + + private Loader(String trackName) { + this.trackName = trackName; + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + + @Override + protected Void doInBackground(Void... voids) { + try { + loadedTrack = Track.load(trackName); + synchronized (cache) { + cache.put(trackName, loadedTrack); + } + return null; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + try { + super.onPostExecute(result); + if (selected.containsKey(trackName)) { + selected.put(trackName, loadedTrack); + } + loader = null; + App.centerOnTrack(loadedTrack); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + } + + private class Searcher extends AsyncTask { + private ArrayList allTrackNames; + private int stopPos; + private final ActivityView.Boundaries boundaries; + private final boolean ellipsoid; + private final HashMap foundTracks = new HashMap<>(); + + private Searcher( + ArrayList allTrackNames, + int stopPos, + ActivityView.Boundaries boundaries, + boolean ellipsoid + ) { + this.allTrackNames = allTrackNames; + this.stopPos = stopPos; + this.boundaries = boundaries; + this.ellipsoid = ellipsoid; + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + + @Override + protected Void doInBackground(Void... voids) { + try { + if (allTrackNames == null) { + allTrackNames = Files.getInstance().getTrackNames(); + } + if (stopPos == -1) { + stopPos = allTrackNames.size() - 1; + } + for (int i = 0; i <= stopPos; i++) { + String trackName = allTrackNames.get(i); + Track track; + synchronized (cache) { + track = cache.get(trackName); + } + if (track == null) { + track = Track.load(trackName); + synchronized (cache) { + cache.put(trackName, track); + } + } + if (track.contains(boundaries, ellipsoid)) { + foundTracks.put(trackName, track); + } + } + return null; + } catch (Throwable t) { + Files.getInstance().log(t); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + try { + super.onPostExecute(result); + selected.putAll(foundTracks); + searcher = null; + App.finishLoadingTracks(); + } catch (Throwable t) { + Files.getInstance().log(t); + } + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/data/Files.java b/src/android/src/main/java/com/aqoleg/cat/data/Files.java new file mode 100644 index 0000000..da8ce6a --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/data/Files.java @@ -0,0 +1,305 @@ +/* +handles operations in filesystem, creates folders and files + +files: +/cat/ // app's folder in the storage root + .nomedia + log.txt + maps/ + mapName/ // map1, map2, ... + properties.txt // optional + z/ // 0 ... 18 + y/ // 0 ... 2^z-1 + x.extension // /cat/maps/myMap/10/4/4.png or /maps/otherMap/11/40/48.jpeg + tracks/ + trackName.gpx // track1, track2, ... + current/ + trackName.tmp // currently writing track (unclosed) + */ +package com.aqoleg.cat.data; + +import android.os.Environment; + +import java.io.*; +import java.text.SimpleDateFormat; +import java.util.*; + +public class Files { + private static final String mapPropertiesFileName = "properties.txt"; + private static final SimpleDateFormat currentTrackFileName = + new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss'.tmp'", Locale.ENGLISH); + // 2023-01-20T11-28-00.tmp in the local time zone + + private static Files files; // singleton + + private final HashSet tags = new HashSet<>(); + private final File log; + private final File maps; + private final File tracks; + private final File currentTrack; + + // initialization, creates default files and folders + @SuppressWarnings("ResultOfMethodCallIgnored") + private Files() { + File root = new File(Environment.getExternalStorageDirectory(), "cat"); + if (!root.isDirectory()) { + if (!root.mkdirs()) { + root.delete(); + } + try { + new File(root, ".nomedia").createNewFile(); + } catch (IOException ignored) { + } + } + log = new File(root, "log.txt"); + maps = new File(root, "maps"); + if (!maps.isDirectory()) { + if (!maps.mkdirs()) { + maps.delete(); + } + Map.addDefault(); + } + tracks = new File(root, "tracks"); + if (!tracks.isDirectory()) { + if (!tracks.mkdirs()) { + tracks.delete(); + } + Track.writeDefault(new File(tracks, "cat.gpx")); + } + currentTrack = new File(tracks, "current"); + if (!currentTrack.isDirectory()) { + if (!currentTrack.mkdirs()) { + currentTrack.delete(); + } + } + } + + + public static Files getInstance() { + if (files == null) { + files = new Files(); + } + return files; + } + + + // appends stacktrace in the end of log.txt + public void log(Throwable throwable) { + StringWriter writer = new StringWriter(); + throwable.printStackTrace(new PrintWriter(writer, true)); + log(writer.toString()); + } + + // if this tag is the first tag since the app launch, appends the msg in the end of the log.txt + public void logOnce(String tag, String msg) { + if (!tags.contains(tag)) { + tags.add(tag); + log(msg); + } + } + + // appends msg in the end of log.txt + public void log(String msg) { + Writer writer = null; + try { + writer = new FileWriter(log, true).append(msg).append('\n').append('\n'); + } catch (Exception ignored) { + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ignored) { + } + } + } + } + + // returns sorted list of names of map folders + public ArrayList getMapNames() { + ArrayList arrayList = new ArrayList<>(); + String[] list = maps.list(); + if (list == null) { + log("empty map folder"); + return arrayList; + } + Arrays.sort(list); + for (String s : list) { + if (new File(maps, s).isDirectory()) { + arrayList.add(s); + } + } + return arrayList; + } + + // creates new map, changes mapName if it exists, returns new map name or null + String addMap(String mapName, String properties) { + File mapDir = new File(maps, mapName); + if (mapDir.exists()) { + String mapNameBase = mapName; + int i = 1; + do { + mapName = mapNameBase + '_' + i++; + mapDir = new File(maps, mapName); + } while (mapDir.exists()); + } + if (!mapDir.mkdirs()) { + log("cannot create " + mapDir.getAbsolutePath()); + return null; + } + Writer writer = null; + try { + writer = new FileWriter(new File(mapDir, mapPropertiesFileName)).append(properties); + } catch (IOException e) { + log("cannot write properties in " + mapDir.getAbsolutePath() + ": " + e); + return null; + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ignored) { + } + } + } + return mapName; + } + + // returns properties, or empty string if map contains no properties, or null if there is no such map + String readMapProperties(String mapName) { + File mapDir = new File(maps, mapName); + if (!mapDir.isDirectory()) { + return null; + } + File properties = new File(mapDir, mapPropertiesFileName); + if (!properties.isFile()) { + return ""; + } + StringBuilder builder = new StringBuilder(); + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(properties)); + String line = reader.readLine(); + while (line != null) { + builder.append(line); + line = reader.readLine(); + } + } catch (IOException e) { + log("cannot read " + properties.getAbsolutePath() + ": " + e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) { + } + } + } + return builder.toString(); + } + + // extension 'jpeg' or 'png' + public void saveTile(byte[] tileBytes, String mapName, int z, int y, int x, String extension) { + File yDir = new File(maps, mapName + File.separator + z + File.separator + y); + if (!yDir.isDirectory() && !yDir.mkdirs()) { + logOnce("Files.saveFile.0", "cannot create " + yDir.getAbsolutePath()); + } + FileOutputStream stream = null; + try { + stream = new FileOutputStream(new File(yDir, x + "." + extension)); + stream.write(tileBytes); + } catch (IOException e) { + logOnce("Files.saveTile.1", "cannot save tile in " + yDir.getAbsolutePath() + ": " + e); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException ignored) { + } + } + } + } + + // returns tile absolute path or null + public String getTilePath(String mapName, int z, int y, int x) { + String path = mapName + File.separator + z + File.separator + y + File.separator + x; + File file = new File(maps, path + ".png"); + if (file.isFile()) { + return file.getAbsolutePath(); + } + file = new File(maps, path + ".jpeg"); + if (file.isFile()) { + return file.getAbsolutePath(); + } + return null; + } + + // returns tile path, if exists, or tile path without extension + public String getTilePathOrName(String mapName, int z, int y, int x) { + String path = mapName + File.separator + z + File.separator + y + File.separator + x; + File file = new File(maps, path + ".png"); + if (file.isFile()) { + return file.getAbsolutePath(); + } + file = new File(maps, path + ".jpeg"); + if (file.isFile()) { + return file.getAbsolutePath(); + } + return new File(maps, path).getAbsolutePath(); + } + + // returns reverse sorted list of names of track files which ends with '.gpx' + public ArrayList getTrackNames() { + // Do not cache because of WeakHashMap keys! + ArrayList arrayList = new ArrayList<>(); + String[] list = tracks.list(); + if (list == null) { + return arrayList; + } + Arrays.sort(list); + String s; + for (int i = list.length - 1; i >= 0; i--) { + s = list[i]; + if (new File(tracks, s).isFile() && s.endsWith(".gpx")) { + arrayList.add(s); + } + } + return arrayList; + } + + public void deleteTrack(String trackName) { + if (!new File(tracks, trackName).delete()) { + log("cannot delete track " + trackName); + } + } + + File getTrack(String trackName) { + return new File(tracks, trackName); + } + + // returns last current track which ends with '.tmp' or null + File getLastCurrentTrack() { + String[] list = currentTrack.list(); + if (list == null || list.length == 0) { + return null; + } + Arrays.sort(list); + for (int i = list.length - 1; i >= 0; i--) { + File file = new File(currentTrack, list[i]); + if (file.isFile() && list[i].endsWith(".tmp")) { + return file; + } + } + return null; + } + + File createCurrentTrack() { + return new File(currentTrack, currentTrackFileName.format(new Date())); + } + + // moves current track from /tracks/current to /tracks + void moveCurrentTrack(File file) { + String name = file.getName(); + name = name.substring(0, name.length() - 4) + ".gpx"; // .tmp -> .gpx + if (!file.renameTo(new File(tracks, name))) { + log("cannot move current track " + file.getAbsolutePath()); + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/data/Map.java b/src/android/src/main/java/com/aqoleg/cat/data/Map.java new file mode 100644 index 0000000..67184de --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/data/Map.java @@ -0,0 +1,153 @@ +/* +creates and reads maps + +properties.txt json file: +{ + "url": "https://example/x=%1$d/y=%2$d/z=%3$d", // optional, if this is not specified, cannot be downloaded + "projection": "ellipsoid" // optional, if this is not specified, use spherical +} + +https://json.org + */ +package com.aqoleg.cat.data; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.IllegalFormatException; + +public class Map { + private static final String jsonUrl = "url"; + private static final String jsonProjection = "projection"; + private static final String jsonProjectionEllipsoid = "ellipsoid"; + + public final String name; + public final boolean ellipsoid; + private final String url; // can be null + + private Map(String name, String url, boolean ellipsoid) { + this.name = name; + this.url = url; + this.ellipsoid = ellipsoid; + } + + + // creates all default maps + static void addDefault() { + new Map( + "mende", + "http://cat.aqoleg.com/maps/mende/%3$d/%2$d/%1$d.jpeg", + false + ).save(); + new Map( + "osm", + "http://a.tile.openstreetmap.org/%3$d/%1$d/%2$d.png", + false + ).save(); + new Map( + "otm", + "https://a.tile.opentopomap.org/%3$d/%1$d/%2$d.png", + false + ).save(); + new Map( + "topo", + "https://maps.marshruty.ru/ml.ashx?al=1&x=%1$d&y=%2$d&z=%3$d", + false + ).save(); + new Map( + "gsat", + "https://khms0.googleapis.com/kh?v=937&hl=en&x=%1$d&y=%2$d&z=%3$d", + false + ).save(); + new Map( + "gmap", + "http://mt0.google.com/vt/lyrs=m&hl=en&x=%1$d&y=%2$d&z=%3$d", + false + ).save(); + new Map( + "yasat", + "https://sat01.maps.yandex.net/tiles?l=sat&x=%1$d&y=%2$d&z=%3$d&g=Gagarin", + true + ).save(); + new Map( + "arcsat", + "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/%3$d/%2$d/%1$d", + false + ).save(); + new Map( + "arctopo", + "https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/%3$d/%2$d/%1$d", + false + ).save(); + } + + // checks url, saves map, changes name if it exists, returns actual name or null + public static String save(String name, String url, String projection) { + try { + new URL(String.format(url, 1, 1, 1)); + } catch (NullPointerException | IllegalFormatException | MalformedURLException e) { + Files.getInstance().log("incorrect url of " + name + ": " + url + ": " + e); + return null; + } + boolean ellipsoid = false; + if (projection != null) { + if (projection.equals(jsonProjectionEllipsoid)) { + ellipsoid = true; + } else { + Files.getInstance().log("incorrect projection of " + name + ": " + projection); + } + } + return new Map(name, url, ellipsoid).save(); + } + + // if mapName == null or map directory does not exist, returns default map + public static Map load(String mapName) { + if (mapName == null) { + mapName = "osm"; + } + String properties = Files.getInstance().readMapProperties(mapName); + if (properties == null) { + mapName = "osm"; + properties = Files.getInstance().readMapProperties(mapName); + } + String url = null; + boolean ellipsoid = false; + if (properties != null && !properties.isEmpty()) { + try { + JSONObject json = new JSONObject(properties); + url = json.optString(jsonUrl, null); + ellipsoid = jsonProjectionEllipsoid.equals(json.optString(jsonProjection)); + } catch (JSONException e) { + Files.getInstance().logOnce("Map.load", "cannot read properties of " + mapName + ": " + e.toString()); + } + } + return new Map(mapName, url, ellipsoid); + } + + + // returns url to download the tile or null + public URL getUrl(int z, int y, int x) { + if (url == null) { + return null; + } + try { + return new URL(String.format(url, x, y, z)); + } catch (MalformedURLException e) { + Files.getInstance().logOnce("Map.getUrl", "incorrect url of " + name + ": " + url + ": " + e); + return null; + } + } + + + private String save() { + // JSON.toString() escapes '\' so it looks like : http:\/\/map + String properties = "{\n \"" + jsonUrl + "\": \"" + url + '"'; + if (ellipsoid) { + properties += ",\n \"" + jsonProjection + "\": \"" + jsonProjectionEllipsoid + '"'; + } + properties += "\n}"; + return Files.getInstance().addMap(name, properties); + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/data/Track.java b/src/android/src/main/java/com/aqoleg/cat/data/Track.java new file mode 100644 index 0000000..ac7e705 --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/data/Track.java @@ -0,0 +1,472 @@ +/* +reads and writes tracks + +precision - 0.000001 degree - 0.1 meters + +track gpx file: + + + + // open new segment when gps had been lost + + 0 // altitude in meters + + + + + + +http://www.topografix.com/GPX/1/1 + +url encoding: +x1.123y-90.0xNaNyNaNx0y0 + */ +package com.aqoleg.cat.data; + +import com.aqoleg.cat.ActivityView; +import com.aqoleg.cat.utils.Projection; + +import java.io.*; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Locale; +import java.util.TimeZone; + +public class Track { + private double[] x; // NaN (x != x) for segment delimiter + private double[] ySpherical; + private double[] yEllipsoid; + private int length; // increasing only after create + private boolean iteratorEllipsoid; + private int iteratorPos; + + private Track(int size) { + x = new double[size]; + ySpherical = new double[size]; + yEllipsoid = new double[size]; + } + + + // returns not-null saved track + public static Track load(String trackName) { + File file = Files.getInstance().getTrack(trackName); + Track track = new Track((int) (file.length()) / 108); // size 91 + ele + lat + lon + track.parse(file); + track.trim(); + return track; + } + + // reads and returns opened track or null + public static Opened open(String pathOrEncoded) { + if (pathOrEncoded == null) { + return null; + } + Opened track; + File file = new File(pathOrEncoded); + if (file.isFile()) { + track = new Opened(pathOrEncoded, (int) (file.length()) / 108); + track.parse(file); + } else { + track = new Opened(pathOrEncoded, 20); + track.decode(); + } + return track.cut(); + } + + // if current track exist, returns it with added segment delimiter, or returns new created current track + public static Current loadCurrent() { + File file = Files.getInstance().getLastCurrentTrack(); + if (file != null) { + Current track = new Current(file, ((int) file.length() / 108)); // size 91 + ele + lat + lon + track.parse(file); + track.addSegmentDelimiter(); + return track; + } + file = Files.getInstance().createCurrentTrack(); + Current track = new Current(file, 20); + track.writeOpening(); + return track; + } + + static void writeDefault(File file) { + Current track = new Current(file, 3); + track.writeOpening(); + track.addPoint(-105.0, -63.338986, 0, 0); + track.addPoint(-60.0, 46.397463, 0, 0); + track.addPoint(-22.5, -7.478673, 0, 0); + track.addPoint(22.5, -7.478673, 0, 0); + track.addPoint(60.0, 46.397463, 0, 0); + track.addPoint(105.0, -63.338986, 0, 0); + track.writeClosing(); + } + + + // puts iterator in the beginning, use only one instance for drawing + public void startPointIterator(boolean ellipsoid) { + iteratorEllipsoid = ellipsoid; + iteratorPos = -1; + } + + // moves iterator to the next point, returns false if there are no points left + public boolean next() { + return ++iteratorPos < length; + } + + // returns x at the current position of iterator, NaN for segment delimiter + public double getX() { + return x[iteratorPos]; + } + + // returns y for the projection that had been set in startPointIterator() or NaN for segment delimiter + public double getY() { + return iteratorEllipsoid ? yEllipsoid[iteratorPos] : ySpherical[iteratorPos]; + } + + // returns x of the the first point, possibly NaN + public double getStartX() { + if (length == 0) { + return Double.NaN; + } + return x[0]; + } + + // returns y of the first point + public double getStartY(boolean ellipsoid) { + return ellipsoid ? yEllipsoid[0] : ySpherical[0]; + } + + // returns true if boundaries contain this track + public boolean contains(ActivityView.Boundaries boundaries, boolean ellipsoid) { + double d; + if (boundaries.xLeft < boundaries.xRight) { + for (int i = 0; i < length; i++) { + d = x[i]; + if (boundaries.xLeft < d && d < boundaries.xRight) { // false for NaN + d = ellipsoid ? yEllipsoid[i] : ySpherical[i]; + if (boundaries.yTop < d && d < boundaries.yBottom) { + return true; + } + } + } + } else { + for (int i = 0; i < length; i++) { + d = x[i]; + if (boundaries.xLeft < d || d < boundaries.xRight) { // false for NaN + d = ellipsoid ? yEllipsoid[i] : ySpherical[i]; + if (boundaries.yTop < d && d < boundaries.yBottom) { + return true; + } + } + } + } + return false; + } + + + @SuppressWarnings("WeakerAccess") + protected void parse(File file) { + BufferedReader reader = null; + int tempIndex, startIndex; + double latitude, longitude; + try { + reader = new BufferedReader(new FileReader(file)); + String line = reader.readLine(); + while (line != null) { + tempIndex = line.indexOf("= 0) { + tempIndex += 6; + startIndex = line.indexOf("lat=\"", tempIndex) + 5; + latitude = Double.parseDouble(line.substring(startIndex, line.indexOf('"', startIndex))); + startIndex = line.indexOf("lon=\"", tempIndex) + 5; + longitude = Double.parseDouble(line.substring(startIndex, line.indexOf('"', startIndex))); + addPoint( + Projection.getX(longitude), + Projection.getY(latitude, false), + Projection.getY(latitude, true), + 40 + ); + } else if (line.contains("")) { + addPoint(Double.NaN, Double.NaN, Double.NaN, 40); + } + line = reader.readLine(); + } + } catch (Exception e) { + Files.getInstance().log("cannot parse track " + file.getAbsolutePath() + ": " + e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) { + } + } + } + } + + // returns true if it would be correct to add a segment delimiter in the end + @SuppressWarnings("WeakerAccess") + protected boolean endsWithPoint() { + return length != 0 && x[length - 1] == x[length - 1]; + } + + + // thread-safe for reading + private void addPoint(double x, double ySpherical, double yEllipsoid, int sizeIncrease) { + if (this.x.length == length) { + this.x = Arrays.copyOf(this.x, length + sizeIncrease); + this.ySpherical = Arrays.copyOf(this.ySpherical, length + sizeIncrease); + this.yEllipsoid = Arrays.copyOf(this.yEllipsoid, length + sizeIncrease); + } + this.x[length] = x; + this.ySpherical[length] = ySpherical; + this.yEllipsoid[length] = yEllipsoid; + length++; + } + + // saves memory + private void trim() { + if (x.length - length > 20) { // 480 bytes + x = Arrays.copyOf(x, length); + ySpherical = Arrays.copyOf(ySpherical, length); + yEllipsoid = Arrays.copyOf(yEllipsoid, length); + } + } + + + public static class Opened extends Track { + public final String pathOrEncoded; + + private Opened(String pathOrEncoded, int size) { + super(size); + this.pathOrEncoded = pathOrEncoded; + } + + + // returns x of the last point + public double getEndX() { + return super.x[super.length - 1]; + } + + public double getEndY(boolean ellipsoid) { + return ellipsoid ? super.yEllipsoid[super.length - 1] : super.ySpherical[super.length - 1]; + } + + + private void decode() { + int xIndex, yIndex; + double longitude, latitude; + try { + xIndex = pathOrEncoded.indexOf('x'); + while (xIndex < pathOrEncoded.length()) { + yIndex = pathOrEncoded.indexOf('y', ++xIndex); + longitude = Double.parseDouble(pathOrEncoded.substring(xIndex, yIndex)); + longitude = Projection.normalizeLongitude(longitude); + xIndex = pathOrEncoded.indexOf('x', ++yIndex); + if (xIndex < 0) { + xIndex = pathOrEncoded.length(); + } + latitude = Double.parseDouble(pathOrEncoded.substring(yIndex, xIndex)); + latitude = Projection.normalizeLatitude(latitude); + if (longitude != longitude || latitude != latitude) { + super.addPoint(Double.NaN, Double.NaN, Double.NaN, 40); + } else { + super.addPoint( + Projection.getX(longitude), + Projection.getY(latitude, false), + Projection.getY(latitude, true), + 40 + ); + } + } + } catch (Exception e) { + Files.getInstance().log("cannot decode track " + pathOrEncoded + ": " + e); + } + } + + private Opened cut() { + while (super.length > 0) { + if (!Double.isNaN(super.x[super.length - 1])) { + break; + } + super.length--; + } + if (super.length < 3) { + return null; + } + return this; + } + } + + // writable Track + public static class Current extends Track { + private static final SimpleDateFormat gpxTime; // 2023-01-20T11:28:00Z in utc + private static final String gpxOpen = "\n" + + "\n" + + " "; + private static final String gpxTrksegOpen = "\n" + + " "; + private static final String gpxTrkpt = "\n" + + " \n" + // 6 digits after the dot by default + " %3$d\n" + + " \n" + + " "; + private static final String gpxTrksegClose = "\n" + + " "; + private static final String gpxClose = "\n" + + " \n" + + ""; + + private final File file; + + + static { + gpxTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); + gpxTime.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + + private Current(File file, int length) { + super(length); + this.file = file; + } + + + // adds and writes this point in the end of the track + public void addPoint(double longitude, double latitude, int altitudeMeters, long unixTime) { + super.addPoint( + Projection.getX(longitude), + Projection.getY(latitude, false), + Projection.getY(latitude, true), + 10 + ); + + String point = String.format( + Locale.ENGLISH, + gpxTrkpt, + longitude, + latitude, + altitudeMeters, + gpxTime.format(unixTime) + ); + + Writer writer = null; + try { + writer = new FileWriter(file, true).append(point); + } catch (IOException e) { + Files.getInstance().log("cannot write track point " + file.getAbsolutePath() + ": " + e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ignored) { + } + } + } + } + + // adds and writes segment delimiter in the end of the track, if it ends with a point + public void addSegmentDelimiter() { + if (!super.endsWithPoint()) { + return; + } + super.addPoint(Double.NaN, Double.NaN, Double.NaN, 10); + + Writer writer = null; + try { + writer = new FileWriter(file, true).append(gpxTrksegClose).append(gpxTrksegOpen); + } catch (IOException e) { + Files.getInstance().log("cannot write track segment delimiter " + file.getAbsolutePath() + ": " + e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ignored) { + } + } + } + } + + // deletes small current track or finishes and moves it to the saved tracks + public void close() { + if (super.length < 4) { + if (!file.delete()) { + Files.getInstance().log("cannot delete current track " + file.getAbsolutePath()); + } + } else { + writeClosing(); + Files.getInstance().moveCurrentTrack(file); + } + } + + // returns encoded track "x1.0y1.0x2.0y2.0xNaNyNaNx4.0y4.0" or null + public String getEncoded() { + if (super.length < 3) { + return null; + } + StringBuilder builder = new StringBuilder(); + BufferedReader reader = null; + int tempIndex, startIndex; + try { + reader = new BufferedReader(new FileReader(file)); + String line = reader.readLine(); + while (line != null) { + tempIndex = line.indexOf("= 0) { + tempIndex += 6; + startIndex = line.indexOf("lon=\"", tempIndex) + 5; + builder.append('x').append(line.substring(startIndex, line.indexOf('"', startIndex))); + startIndex = line.indexOf("lat=\"", tempIndex) + 5; + builder.append('y').append(line.substring(startIndex, line.indexOf('"', startIndex))); + } else if (line.contains("")) { + builder.append("xNaNyNaN"); + } + line = reader.readLine(); + } + } catch (Exception e) { + Files.getInstance().log("cannot parse current track: " + e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignored) { + } + } + } + return builder.toString(); + } + + + private void writeOpening() { + Writer writer = null; + try { + writer = new FileWriter(file).append(gpxOpen).append(gpxTrksegOpen); + } catch (IOException e) { + Files.getInstance().log("cannot write track opening " + file.getAbsolutePath() + ": " + e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ignored) { + } + } + } + } + + private void writeClosing() { + Writer writer = null; + try { + writer = new FileWriter(file, true).append(gpxTrksegClose).append(gpxClose); + } catch (IOException e) { + Files.getInstance().log("cannot write track closing " + file.getAbsolutePath() + ": " + e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ignored) { + } + } + } + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/data/UriData.java b/src/android/src/main/java/com/aqoleg/cat/data/UriData.java new file mode 100644 index 0000000..94a20d3 --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/data/UriData.java @@ -0,0 +1,158 @@ +/* +parses uri from intent + opens any gpx file + opens geo: + geo:0,0?q=latitude,longitude(label) + geo:0,0?q=latitude,longitude + geo:latitude,longitude?z=zoom + geo:latitude,longitude + opens url http(s)://cat.aqoleg.com/app? + adds new map: + newMap=newMapName&newUrl=urlToDownloadNewMap&newProjection=ellipsoid& + newMap=map&newUrl=https%3A%2F%2Fcat.aqoleg.com%2Fmaps%2Fn%2F%253%24d%2F%252%24d%2F%251%24d.png& + selects map: + map=mapName& + selects zoom: + z=2& + displays track: + track=xlongitudeylatitudexnextLongitudeynextLatitude& + track=x10y10x11y11xNaNyNaNx10.55y12.56& + selects point: + longitude=longitude&latitude=latitude& + lon=longitude&lat=latitude& + lon=10&lat=12& + */ +package com.aqoleg.cat.data; + +import android.net.Uri; +import com.aqoleg.cat.utils.Projection; + +public class UriData { + public final String newMapName; // map name to save or null + public final String newMapUrl; // map url to save, null if newMapName == null + public final String newMapProjection; // map projection to save, null if newMapName == null + public final String mapName; // map name to open or null + public final boolean hasZ; + public final int z; // normalized zoom to open, if hasZ + public final String track; // track to open: absolute track file path, encoded track or null + public final boolean hasPoint; + public final double pointLongitude; // normalized, if hasPoint + public final double pointLatitude; // normalized, if hasPoint + + private UriData( + String newMapName, + String newMapUrl, + String newMapProjection, + String mapName, + int z, + String track, + boolean hasPoint, + double pointLongitude, + double pointLatitude + ) { + this.newMapName = newMapName; + this.newMapUrl = newMapUrl; + this.newMapProjection = newMapProjection; + this.mapName = mapName; + hasZ = z > 0; + if (--z > 17) { + z = 17; + } + this.z = z; + this.track = track; + this.hasPoint = hasPoint; + this.pointLongitude = Projection.normalizeLongitude(pointLongitude); + this.pointLatitude = Projection.normalizeLatitude(pointLatitude); + } + + + // returns UriData or null + public static UriData parse(Uri uri) { + if ("file".equals(uri.getScheme())) { + return new UriData(null, null, null, null, 0, uri.getPath(), false, 0, 0); + } else if ("geo".equals(uri.getScheme())) { + String geo = uri.toString(); + int startIndex = geo.indexOf("0,0?q="); + int middleIndex, endIndex; + if (startIndex > 0) { + startIndex += 6; + middleIndex = geo.indexOf(",", startIndex); + endIndex = geo.indexOf("(", middleIndex); + } else { + startIndex = 4; + middleIndex = geo.indexOf(",", 4); + endIndex = geo.indexOf("?", startIndex); + } + if (endIndex < 0) { + endIndex = geo.length(); + } + double latitude, longitude; + try { + latitude = Double.parseDouble(geo.substring(startIndex, middleIndex)); + longitude = Double.parseDouble(geo.substring(middleIndex + 1, endIndex)); + } catch (IndexOutOfBoundsException | NumberFormatException e) { + Files.getInstance().log("incorrect geo uri in intent " + uri.toString() + ": " + e); + return null; + } + int z = -1; + int zIndex = geo.indexOf("?z="); + if (zIndex > 0) { + try { + z = Integer.parseInt(geo.substring(zIndex + 3)); + } catch (Exception e) { + Files.getInstance().log("incorrect z in uri in intent " + uri.toString() + ": " + e); + } + } + return new UriData(null, null, null, null, z, null, true, longitude, latitude); + } else if ("cat.aqoleg.com".equals(uri.getHost())) { + if (uri.getPath() == null || !uri.getPath().startsWith("/app")) { + Files.getInstance().log("incorrect uri path in intent " + uri.toString()); + return null; + } + int z = 0; + String s = uri.getQueryParameter("z"); + if (s != null) { + try { + z = Integer.parseInt(s); + } catch (Exception e) { + Files.getInstance().log("incorrect z in uri in intent " + uri.toString() + ": " + e); + } + } + boolean hasSelection = false; + double longitude = 0, latitude = 0; + try { + s = uri.getQueryParameter("longitude"); + if (s == null) { + s = uri.getQueryParameter("lon"); + } + if (s != null) { + longitude = Double.parseDouble(s); + s = uri.getQueryParameter("latitude"); + if (s == null) { + s = uri.getQueryParameter("lat"); + } + if (s != null) { + latitude = Double.parseDouble(s); + hasSelection = true; + } + } + } catch (NumberFormatException e) { + Files.getInstance().log("incorrect uri in intent " + uri.toString() + ": " + e); + } + return new UriData( + uri.getQueryParameter("newMap"), + uri.getQueryParameter("newUrl"), + uri.getQueryParameter("newProjection"), + uri.getQueryParameter("map"), + z, + uri.getQueryParameter("track"), + hasSelection, + longitude, + latitude + ); + } else { + Files.getInstance().log("incorrect uri in intent " + uri.toString()); + return null; + } + } +} \ No newline at end of file diff --git a/src/android/src/main/java/com/aqoleg/cat/utils/Projection.java b/src/android/src/main/java/com/aqoleg/cat/utils/Projection.java new file mode 100644 index 0000000..45cfc39 --- /dev/null +++ b/src/android/src/main/java/com/aqoleg/cat/utils/Projection.java @@ -0,0 +1,133 @@ +/* +converts tiles coordinates (x, y) to longitude-latitude coordinates and back +normalizes longitude and latitude +x, longitude - horizontal axis; y, latitude - vertical axis + +projections: + spherical (epsg3857, epsg4326, web mercator, default, "not ellipsoid") + ellipsoid (epsg3395, true mercator) + +https://pubs.usgs.gov/pp/1395/report.pdf (pages 12, 41, 44-45) + */ +package com.aqoleg.cat.utils; + +import static java.lang.Math.PI; + +public class Projection { + private static final double pi2; // 2*pi + private static final double pi1_2; // pi/2 + private static final double pi1_4; // pi/4 + private static final double e; // ellipsoid eccentricity + private static final double e1_2; // e/2 + + static { + pi2 = PI * 2.0; + pi1_2 = PI / 2.0; + pi1_4 = PI / 4.0; + // [1 - (polarRadius**2 / equatorialRadius**2)]**0.5 + e = Math.sqrt(1.0 - (Math.pow(6356752.3142, 2) / Math.pow(6378137, 2))); + e1_2 = e / 2.0; + } + + private Projection() { + } + + // returns x, from 0 (west, longitude = -180) to 1 (east, longitude = 180) + // the same for both projections + public static double getX(double longitude) { + // formula (7 - 1a) + // x = pi * R * (lambdaDeg - lambda0Deg) / 180 + // tilesX = x = pi / 2pi * (lon - -180) / 180 = (lon + 180) / 360 + return (longitude + 180.0) / 360.0; + } + + // returns decimal degrees, from -180 (x = 0) to 180 (x = 1) + // the same for both projections + public static double getLongitude(double x) { + // formula (7 - 5) + // lambda = x/R + lambda0 + // lon = lambda * 180 / pi = tilesX * 2pi * 180 / pi + lambda0Deg = tilesX * 360 - 180 + return x * 360.0 - 180.0; + } + + // returns -180 < longitude < 180 + public static double normalizeLongitude(double longitude) { + if (longitude < -180.0 || longitude > 180.0) { + longitude = ((longitude % 360.0) + 360.0) % 360.0; + if (longitude > 180.0) { + longitude -= 360.0; + } + } + return longitude; + } + + // returns y, from 0 (north, latitude 85) to 1 (south, latitude -85) + public static double getY(double latitude, boolean ellipsoid) { + if (ellipsoid) { + // formula (7 - 7) + // y = a * ln( tan(pi/4 + phi/2) * ( (1 - e*sin(phi)) / (1 + e*sin(phi)) )**(e/2) ) + // phi = lat / 180 * pi + // temp = ( (1 - e*sin(phi)) / (1 + e*sin(phi)) )**(e/2) + // tilesY = 0.5 - y = 0.5 - ln( tan(pi/4 + phi/2) * temp) / 2pi + double phi = latitude / 180.0 * PI; + double temp = e * Math.sin(phi); + temp = Math.pow((1.0 - temp) / (1.0 + temp), e1_2); + return 0.5 - Math.log(Math.tan(pi1_4 + phi / 2.0) * temp) / pi2; + } else { + // formula (7 - 2) + // y = R * ln(tan(pi/4 + phi/2)) + // tilesY = 0.5 - y = 0.5 - ln(tan(pi/4 + lat / 360 * pi)) / 2pi + return 0.5 - Math.log(Math.tan(pi1_4 + latitude / 360.0 * PI)) / pi2; + } + } + + // returns decimal degrees, from -85 (y = 1) to 85 (y = 0) + public static double getLatitude(double y, boolean ellipsoid) { + if (ellipsoid) { + // formula (7 - 9) + // phi = pi/2 - 2 * arctan( t * ( (1 - e*sin(phi)) / (1 + e*sin(phi)) )**(e/2) ) + // formula (7 - 10) + // t = e**(-y/a) + // t = e**( -(0.5 - tilesY) * 2pi ) = e**( tilesY * 2pi - pi ) + // formula (7 - 11) + // trialPhi = pi/2 - 2 * arctan(t) + double t = Math.exp((pi2 * y) - PI); + double trialPhi = pi1_2 - 2.0 * Math.atan(t); + double phi = 0; + // usually 1 - 2 iterations + for (int i = 0; i < 5; i++) { + phi = e * Math.sin(trialPhi); + phi = pi1_2 - 2.0 * Math.atan(t * Math.pow((1.0 - phi) / (1.0 + phi), e1_2)); + if (Math.abs(trialPhi - phi) < 0.000000001) { // 1 cm max + break; + } + trialPhi = phi; + } + return phi * 180.0 / PI; + } else { + // formula (7 - 4a) + // phi = arctan(sinh(y / R)) + // lat = phi * 180 / pi = arctan(sinh( (0.5 - tilesY) * 2pi )) * 180 / pi = + // = arctan(sinh( pi - 2pi * tilesY )) * 180 / pi + return Math.atan(Math.sinh(PI - (pi2 * y))) * 180.0 / PI; + } + } + + // returns -85 < latitude < 85 + public static double normalizeLatitude(double latitude) { + if (latitude < -90.0 || latitude > 90.0) { + latitude = ((latitude % 360.0) + 360.0) % 360.0; + if (latitude > 270.0) { + latitude -= 360.0; + } else if (latitude > 90.0) { + latitude = 180.0 - latitude; + } + } + if (latitude < -85.0) { + return -85.0; + } else if (latitude > 85.0) { + return 85.0; + } + return latitude; + } +} \ No newline at end of file diff --git a/src/app/src/main/res/drawable-hdpi/exit.png b/src/android/src/main/res/drawable-hdpi/close.png similarity index 100% rename from src/app/src/main/res/drawable-hdpi/exit.png rename to src/android/src/main/res/drawable-hdpi/close.png diff --git a/src/app/src/main/res/drawable-hdpi/satellites.png b/src/android/src/main/res/drawable-hdpi/extra.png similarity index 100% rename from src/app/src/main/res/drawable-hdpi/satellites.png rename to src/android/src/main/res/drawable-hdpi/extra.png diff --git a/src/app/src/main/res/drawable-hdpi/maps.png b/src/android/src/main/res/drawable-hdpi/maps.png similarity index 100% rename from src/app/src/main/res/drawable-hdpi/maps.png rename to src/android/src/main/res/drawable-hdpi/maps.png diff --git a/src/app/src/main/res/drawable-hdpi/minus.png b/src/android/src/main/res/drawable-hdpi/minus.png similarity index 100% rename from src/app/src/main/res/drawable-hdpi/minus.png rename to src/android/src/main/res/drawable-hdpi/minus.png diff --git a/src/app/src/main/res/drawable-hdpi/notification.png b/src/android/src/main/res/drawable-hdpi/notification.png similarity index 100% rename from src/app/src/main/res/drawable-hdpi/notification.png rename to src/android/src/main/res/drawable-hdpi/notification.png diff --git a/src/app/src/main/res/drawable-hdpi/plus.png b/src/android/src/main/res/drawable-hdpi/plus.png similarity index 100% rename from src/app/src/main/res/drawable-hdpi/plus.png rename to src/android/src/main/res/drawable-hdpi/plus.png diff --git a/src/app/src/main/res/drawable-hdpi/pointer.png b/src/android/src/main/res/drawable-hdpi/pointer.png similarity index 100% rename from src/app/src/main/res/drawable-hdpi/pointer.png rename to src/android/src/main/res/drawable-hdpi/pointer.png diff --git a/src/app/src/main/res/drawable-hdpi/tracks.png b/src/android/src/main/res/drawable-hdpi/tracks.png similarity index 100% rename from src/app/src/main/res/drawable-hdpi/tracks.png rename to src/android/src/main/res/drawable-hdpi/tracks.png diff --git a/src/app/src/main/res/drawable-mdpi/exit.png b/src/android/src/main/res/drawable-mdpi/close.png similarity index 100% rename from src/app/src/main/res/drawable-mdpi/exit.png rename to src/android/src/main/res/drawable-mdpi/close.png diff --git a/src/app/src/main/res/drawable-mdpi/satellites.png b/src/android/src/main/res/drawable-mdpi/extra.png similarity index 100% rename from src/app/src/main/res/drawable-mdpi/satellites.png rename to src/android/src/main/res/drawable-mdpi/extra.png diff --git a/src/app/src/main/res/drawable-mdpi/maps.png b/src/android/src/main/res/drawable-mdpi/maps.png similarity index 100% rename from src/app/src/main/res/drawable-mdpi/maps.png rename to src/android/src/main/res/drawable-mdpi/maps.png diff --git a/src/app/src/main/res/drawable-mdpi/minus.png b/src/android/src/main/res/drawable-mdpi/minus.png similarity index 100% rename from src/app/src/main/res/drawable-mdpi/minus.png rename to src/android/src/main/res/drawable-mdpi/minus.png diff --git a/src/app/src/main/res/drawable-mdpi/notification.png b/src/android/src/main/res/drawable-mdpi/notification.png similarity index 100% rename from src/app/src/main/res/drawable-mdpi/notification.png rename to src/android/src/main/res/drawable-mdpi/notification.png diff --git a/src/app/src/main/res/drawable-mdpi/plus.png b/src/android/src/main/res/drawable-mdpi/plus.png similarity index 100% rename from src/app/src/main/res/drawable-mdpi/plus.png rename to src/android/src/main/res/drawable-mdpi/plus.png diff --git a/src/app/src/main/res/drawable-mdpi/pointer.png b/src/android/src/main/res/drawable-mdpi/pointer.png similarity index 100% rename from src/app/src/main/res/drawable-mdpi/pointer.png rename to src/android/src/main/res/drawable-mdpi/pointer.png diff --git a/src/app/src/main/res/drawable-mdpi/tracks.png b/src/android/src/main/res/drawable-mdpi/tracks.png similarity index 100% rename from src/app/src/main/res/drawable-mdpi/tracks.png rename to src/android/src/main/res/drawable-mdpi/tracks.png diff --git a/src/app/src/main/res/drawable-xhdpi/exit.png b/src/android/src/main/res/drawable-xhdpi/close.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/exit.png rename to src/android/src/main/res/drawable-xhdpi/close.png diff --git a/src/app/src/main/res/drawable-xhdpi/satellites.png b/src/android/src/main/res/drawable-xhdpi/extra.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/satellites.png rename to src/android/src/main/res/drawable-xhdpi/extra.png diff --git a/src/app/src/main/res/drawable-xhdpi/maps.png b/src/android/src/main/res/drawable-xhdpi/maps.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/maps.png rename to src/android/src/main/res/drawable-xhdpi/maps.png diff --git a/src/app/src/main/res/drawable-xhdpi/minus.png b/src/android/src/main/res/drawable-xhdpi/minus.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/minus.png rename to src/android/src/main/res/drawable-xhdpi/minus.png diff --git a/src/app/src/main/res/drawable-xhdpi/notification.png b/src/android/src/main/res/drawable-xhdpi/notification.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/notification.png rename to src/android/src/main/res/drawable-xhdpi/notification.png diff --git a/src/app/src/main/res/drawable-xhdpi/plus.png b/src/android/src/main/res/drawable-xhdpi/plus.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/plus.png rename to src/android/src/main/res/drawable-xhdpi/plus.png diff --git a/src/app/src/main/res/drawable-xhdpi/pointer.png b/src/android/src/main/res/drawable-xhdpi/pointer.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/pointer.png rename to src/android/src/main/res/drawable-xhdpi/pointer.png diff --git a/src/app/src/main/res/drawable-xhdpi/tracks.png b/src/android/src/main/res/drawable-xhdpi/tracks.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/tracks.png rename to src/android/src/main/res/drawable-xhdpi/tracks.png diff --git a/src/android/src/main/res/layout/activity_main.xml b/src/android/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..cb214c1 --- /dev/null +++ b/src/android/src/main/res/layout/activity_main.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/src/main/res/layout/dialog_delete.xml b/src/android/src/main/res/layout/dialog_delete.xml new file mode 100644 index 0000000..00a163c --- /dev/null +++ b/src/android/src/main/res/layout/dialog_delete.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/android/src/main/res/layout/dialog_extra.xml b/src/android/src/main/res/layout/dialog_extra.xml new file mode 100644 index 0000000..be61b01 --- /dev/null +++ b/src/android/src/main/res/layout/dialog_extra.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/src/main/res/layout/dialog_maps.xml b/src/android/src/main/res/layout/dialog_maps.xml new file mode 100644 index 0000000..a414ee5 --- /dev/null +++ b/src/android/src/main/res/layout/dialog_maps.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/src/android/src/main/res/layout/dialog_tracks.xml b/src/android/src/main/res/layout/dialog_tracks.xml new file mode 100644 index 0000000..5d217a5 --- /dev/null +++ b/src/android/src/main/res/layout/dialog_tracks.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/android/src/main/res/layout/list_item.xml b/src/android/src/main/res/layout/list_item.xml new file mode 100644 index 0000000..95f485c --- /dev/null +++ b/src/android/src/main/res/layout/list_item.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/src/android/src/main/res/mipmap-hdpi/icon.png b/src/android/src/main/res/mipmap-hdpi/icon.png new file mode 100644 index 0000000..2dfe4e8 Binary files /dev/null and b/src/android/src/main/res/mipmap-hdpi/icon.png differ diff --git a/src/android/src/main/res/mipmap-mdpi/icon.png b/src/android/src/main/res/mipmap-mdpi/icon.png new file mode 100644 index 0000000..6b18947 Binary files /dev/null and b/src/android/src/main/res/mipmap-mdpi/icon.png differ diff --git a/src/android/src/main/res/mipmap-xhdpi/icon.png b/src/android/src/main/res/mipmap-xhdpi/icon.png new file mode 100644 index 0000000..cd71b9f Binary files /dev/null and b/src/android/src/main/res/mipmap-xhdpi/icon.png differ diff --git a/src/android/src/main/res/values/colors.xml b/src/android/src/main/res/values/colors.xml new file mode 100644 index 0000000..3e72045 --- /dev/null +++ b/src/android/src/main/res/values/colors.xml @@ -0,0 +1,9 @@ + + + #FA0505 + #000000 + #EA04BC + #88EA04BC + #30FFFFFF + #A0FFFFFF + \ No newline at end of file diff --git a/src/android/src/main/res/values/strings.xml b/src/android/src/main/res/values/strings.xml new file mode 100644 index 0000000..eb8989b --- /dev/null +++ b/src/android/src/main/res/values/strings.xml @@ -0,0 +1,45 @@ + + + cat + all maps offline with track writer + track writer + + %1$.1f km + close app, stop track writer + center map + increase zoom, long click to increase by 3 + decrease zoom, long click to decrease by 3 + maps + tracks + extra + searching tracks… + + %1$d\u00B0 %2$d m + %1$d\u00B0 %2$.1f km + + tracks (%1$d/%2$d) + delete + deselect + search + select tracks + delete track? + delete %1$d tracks? + cancel + ok + + satellites %1$d/%2$d + lon %1$.6f\u00B0\nlat %2$.6f\u00B0\naltitude %3$d m\naccuracy %4$d m\nspeed %5$d km/h + + track %1$s, %2$s + copy + send + point %1$s + tile %1$s + tiles %1$d, cache %2$d + visit %1$s + version 5.0.0 + copied + no file explorer + no image viewer + no web browser + \ No newline at end of file diff --git a/src/android/src/main/res/values/styles.xml b/src/android/src/main/res/values/styles.xml new file mode 100644 index 0000000..b88ff11 --- /dev/null +++ b/src/android/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/BitmapCache.java b/src/app/src/main/java/space/aqoleg/cat/BitmapCache.java deleted file mode 100644 index a525d49..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/BitmapCache.java +++ /dev/null @@ -1,199 +0,0 @@ -/* -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 { - 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(); - } - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/CurrentTrack.java b/src/app/src/main/java/space/aqoleg/cat/CurrentTrack.java deleted file mode 100644 index f06979f..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/CurrentTrack.java +++ /dev/null @@ -1,187 +0,0 @@ -/* -current track singleton -filters and keeps points of the current track, writes it in .gpx file, calculates total distance - -gpx file: - - - - // at least one - // decimal degrees, latitude from -90 till 90, longitude from -180 till 180 - 0 // elevation (in meters), optional, default is 0 - // utc date YYYY-MM-DD, T if one line, time hh:mm:ss, Z zero meridian - - - - - */ -package space.aqoleg.cat; - -import android.location.Location; - -import java.io.File; -import java.io.FileWriter; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - -class CurrentTrack { - private static final SimpleDateFormat fileName; // 2019-09-06T11-28-00.gpx in local time zone - private static final SimpleDateFormat gpxTime; // 2019-09-06T11:28:00Z in utc - private static final String gpxOpen; // \n - private static final String gpxPoint; // \n - private static final String gpxClose; // ... - - private static final CurrentTrack track = new CurrentTrack(); - - private final ArrayList xList = new ArrayList<>(); - private final ArrayList ySphericalList = new ArrayList<>(); - private final ArrayList yEllipsoidList = new ArrayList<>(); - private File file; - private Location previousLocation; - private float totalDistance; - private float localDistance; // can be reset - - static { - fileName = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss'.gpx'", Locale.getDefault()); - gpxTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault()); - gpxTime.setTimeZone(TimeZone.getTimeZone("UTC")); - gpxOpen = "" + - System.getProperty("line.separator") + - "" + - System.getProperty("line.separator") + - " " + System.getProperty("line.separator") + - " " + System.getProperty("line.separator"); - gpxPoint = " %n" + - " %3$d%n" + - " %n" + - " %n"; - gpxClose = " " + System.getProperty("line.separator") + - " " + System.getProperty("line.separator") + - ""; - } - - static CurrentTrack getInstance() { - return track; - } - - // clears all data - void open() { - xList.clear(); - ySphericalList.clear(); - yEllipsoidList.clear(); - file = null; - previousLocation = null; - totalDistance = 0; - localDistance = 0; - } - - // deletes track if it is too short or closes it, and clears data - void close() { - if (file != null) { - if (totalDistance > 200 && file.length() > 480) { // more then 3 points and 200 m - try { - FileWriter writer = new FileWriter(file, true); - writer.append(gpxClose); - writer.flush(); - writer.close(); - } catch (Exception exception) { - Data.getInstance().writeLog("can not close track: " + exception.toString()); - } - } else if (!file.delete()) { - Data.getInstance().writeLog("can not delete short track"); - } - } - open(); - } - - // this track will not have add to saved track list - String getName() { - if (file == null) { - return null; - } - return file.getName(); - } - - // returns last written location or null - Location getLastLocation() { - return previousLocation; - } - - int getPointsNumber() { - return xList.size(); - } - - double getX(int pointN) { - return xList.get(pointN); - } - - double getY(int pointN, boolean isEllipsoid) { - if (isEllipsoid) { - return yEllipsoidList.get(pointN); - } else { - return ySphericalList.get(pointN); - } - } - - float getTotalDistance() { - return totalDistance; - } - - float getLocalDistance() { - return localDistance; - } - - void resetLocalDistance() { - localDistance = 0; - } - - void setNewLocation(Location location) { - if (previousLocation == null) { - // first point - previousLocation = location; - } else { - float distance = previousLocation.distanceTo(location); - if (distance > 50 && distance > location.getAccuracy() * 4) { - totalDistance += distance; - localDistance += distance; - previousLocation = location; - } else { - return; - } - } - double longitude = location.getLongitude(); - double latitude = location.getLatitude(); - - xList.add(Projection.getX(longitude)); - ySphericalList.add(Projection.getY(latitude, false)); - yEllipsoidList.add(Projection.getY(latitude, true)); - // decimal separator is a dot, float is rounded by 6 digits after dot - String point = String.format( - Locale.ENGLISH, - gpxPoint, - longitude, - latitude, - Math.round(location.getAltitude()), - gpxTime.format(new Date()) - ); - - boolean firstPoint = file == null; - if (firstPoint) { - file = new File(Data.getInstance().getTracks(), fileName.format(new Date())); - } - try { - FileWriter writer = new FileWriter(file, true); - if (firstPoint) { - writer.append(gpxOpen); - } - writer.append(point); - writer.flush(); - writer.close(); - } catch (Exception exception) { - Data.getInstance().writeLog("can not write track: " + exception.toString()); - } - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/Data.java b/src/app/src/main/java/space/aqoleg/cat/Data.java deleted file mode 100644 index 01a8a7a..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/Data.java +++ /dev/null @@ -1,79 +0,0 @@ -/* -root singleton -creates root directory and files on the first launch, writes log - -files: -sd/cat/.nomedia -sd/cat/log.txt -sd/cat/maps -sd/cat/tracks - */ -package space.aqoleg.cat; - -import android.os.Environment; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; - -class Data { - private static Data data; - - private final File log; - private final File maps; - private final File tracks; - - private Data() { - File root = new File(Environment.getExternalStorageDirectory(), "cat"); - log = new File(root, "log.txt"); - if (!root.isDirectory()) { - if (root.mkdirs()) { - try { - if (!new File(root, ".nomedia").createNewFile()) { - throw new IOException(); - } - } catch (IOException e) { - writeLog("can not create .nomedia " + e.toString()); - } - } - } - maps = new File(root, "maps"); - if (!maps.isDirectory()) { - if (!maps.mkdir()) { - writeLog("can not create " + maps.getAbsolutePath()); - } - } - tracks = new File(root, "tracks"); - if (!tracks.isDirectory()) { - if (!tracks.mkdir()) { - writeLog("can not create " + tracks.getAbsolutePath()); - } - } - } - - static Data getInstance() { - if (data == null) { - data = new Data(); - } - return data; - } - - void writeLog(String string) { - try { - FileWriter writer = new FileWriter(log, true); - writer.append(string); - writer.append(System.getProperty("line.separator")); - writer.flush(); - writer.close(); - } catch (IOException ignored) { - } - } - - File getMaps() { - return maps; - } - - File getTracks() { - return tracks; - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/DialogMaps.java b/src/app/src/main/java/space/aqoleg/cat/DialogMaps.java deleted file mode 100644 index 2db607e..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/DialogMaps.java +++ /dev/null @@ -1,74 +0,0 @@ -/* -list with all maps to select - */ -package space.aqoleg.cat; - -import android.app.DialogFragment; -import android.content.Context; -import android.graphics.Color; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -public class DialogMaps extends DialogFragment implements AdapterView.OnItemClickListener { - private static final String argumentCurrentMapN = "mapN"; - - private int currentMapN; - - static DialogMaps newInstance(int currentMapN) { - Bundle args = new Bundle(); - args.putInt(argumentCurrentMapN, currentMapN); - DialogMaps dialog = new DialogMaps(); - dialog.setArguments(args); - dialog.setStyle(DialogFragment.STYLE_NO_TITLE, 0); - return dialog; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.dialog_list, container, false); - ((TextView) view.findViewById(R.id.title)).setText(getString(R.string.maps)); - currentMapN = getArguments().getInt(argumentCurrentMapN); - - ListView listView = view.findViewById(R.id.list); - listView.setAdapter(new Adapter(getActivity().getApplicationContext())); - listView.setSelection(currentMapN); - listView.setOnItemClickListener(this); - return view; - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (position != currentMapN) { - ((MainActivity) getActivity()).selectMap(position); - } - dismiss(); - } - - private class Adapter extends ArrayAdapter { - Adapter(Context context) { - super( - context, - R.layout.list_item, - R.id.text, - Maps.getInstance().getList() - ); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = super.getView(position, convertView, parent); - if (position == currentMapN) { - view.setBackgroundColor(Color.GRAY); - } else { - view.setBackgroundColor(Color.TRANSPARENT); - } - return view; - } - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/DialogSatellites.java b/src/app/src/main/java/space/aqoleg/cat/DialogSatellites.java deleted file mode 100644 index 0eab48e..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/DialogSatellites.java +++ /dev/null @@ -1,179 +0,0 @@ -/* -fragment with detailed information about satellites and location - */ -package space.aqoleg.cat; - -import android.app.DialogFragment; -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.location.*; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.TextView; - -import static android.content.Context.LOCATION_SERVICE; - -@SuppressWarnings("deprecation") -public class DialogSatellites extends DialogFragment implements LocationListener, GpsStatus.Listener { - private LocationManager locationManager; // null if listeners has removed - private GpsStatus gpsStatus; - - private SatellitesView satellitesView; - - static DialogSatellites newInstance() { - DialogSatellites dialog = new DialogSatellites(); - dialog.setStyle(DialogFragment.STYLE_NO_TITLE, 0); - return dialog; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.dialog_satellites, container, false); - satellitesView = new SatellitesView(getActivity().getApplicationContext()); - ((FrameLayout) view.findViewById(R.id.frame)).addView(satellitesView); - return view; - } - - @Override - public void onStart() { - super.onStart(); - locationManager = ((LocationManager) getActivity().getSystemService(LOCATION_SERVICE)); - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); - locationManager.addGpsStatusListener(this); - } - - @Override - public void onStop() { - locationManager.removeUpdates(this); - locationManager.removeGpsStatusListener(this); - locationManager = null; - super.onStop(); - } - - @Override - public void onLocationChanged(Location location) { - if (locationManager == null) { - return; - } - String text = String.format( - getString(R.string.locationText), - location.getLongitude(), - location.getLatitude(), - Math.round(location.getAltitude()), - Math.round(location.getAccuracy()), - Math.round(location.getSpeed() * 3.6f) - ); - ((TextView) getView().findViewById(R.id.coordinates)).setText(text); - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - } - - @Override - public void onProviderEnabled(String provider) { - } - - @Override - public void onProviderDisabled(String provider) { - } - - @Override - public void onGpsStatusChanged(int event) { - if (locationManager == null) { - return; - } - gpsStatus = locationManager.getGpsStatus(gpsStatus); - satellitesView.invalidate(); - } - - class SatellitesView extends View { - private final Paint grid; - private final Paint usedSatellite; - private final Paint notUsedSatellite; - - private int xCenterPx; - private int yCenterPx; - private float outerRadiusPx; - private float middleRadiusPx; - private float innerRadiusPx; - private float positionScalePx; - private float usedSatelliteRadiusScalePx; - private float notUsedSatelliteRadiusPx; - - SatellitesView(Context context) { - super(context); - - grid = new Paint(); - grid.setStyle(Paint.Style.STROKE); - grid.setStrokeWidth(1); - grid.setColor(Color.BLACK); - grid.setAntiAlias(true); - - usedSatellite = new Paint(); - usedSatellite.setStyle(Paint.Style.FILL_AND_STROKE); - usedSatellite.setColor(Color.GREEN); - usedSatellite.setAntiAlias(true); - - notUsedSatellite = new Paint(); - notUsedSatellite.setStyle(Paint.Style.FILL_AND_STROKE); - notUsedSatellite.setColor(Color.RED); - notUsedSatellite.setAntiAlias(true); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (!changed) { - return; - } - xCenterPx = getWidth() >> 1; - yCenterPx = getHeight() >> 1; - outerRadiusPx = (getHeight() / 2) - 10; - middleRadiusPx = outerRadiusPx * 2 / 3; - innerRadiusPx = outerRadiusPx / 3; - positionScalePx = outerRadiusPx / 90; - usedSatelliteRadiusScalePx = outerRadiusPx / 280; - notUsedSatelliteRadiusPx = outerRadiusPx / 60; - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - canvas.drawCircle(xCenterPx, yCenterPx, outerRadiusPx, grid); - canvas.drawCircle(xCenterPx, yCenterPx, middleRadiusPx, grid); - canvas.drawCircle(xCenterPx, yCenterPx, innerRadiusPx, grid); - canvas.drawLine(xCenterPx, yCenterPx - innerRadiusPx, xCenterPx, yCenterPx - outerRadiusPx, grid); - canvas.drawLine(xCenterPx, yCenterPx + innerRadiusPx, xCenterPx, yCenterPx + outerRadiusPx, grid); - canvas.drawLine(xCenterPx - innerRadiusPx, yCenterPx, xCenterPx - outerRadiusPx, yCenterPx, grid); - canvas.drawLine(xCenterPx + innerRadiusPx, yCenterPx, xCenterPx + outerRadiusPx, yCenterPx, grid); - - if (gpsStatus != null) { - int satellitesN = 0; - int satellitesUsed = 0; - Iterable satellites = gpsStatus.getSatellites(); - for (GpsSatellite satellite : satellites) { - satellitesN++; - float radian = (-satellite.getAzimuth() + 90) * (float) Math.PI / 180; - float fromCenterPx = (-satellite.getElevation() + 90) * positionScalePx; - float xPx = xCenterPx + (float) Math.cos(radian) * fromCenterPx; - float yPx = yCenterPx - (float) Math.sin(radian) * fromCenterPx; - if (satellite.usedInFix()) { - satellitesUsed++; - canvas.drawCircle(xPx, yPx, satellite.getSnr() * usedSatelliteRadiusScalePx, usedSatellite); - } else { - canvas.drawCircle(xPx, yPx, notUsedSatelliteRadiusPx, notUsedSatellite); - } - } - String text = String.format(getString(R.string.satellitesText), satellitesN, satellitesUsed); - ((TextView) getView().findViewById(R.id.satellites)).setText(text); - } - } - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/DialogTracks.java b/src/app/src/main/java/space/aqoleg/cat/DialogTracks.java deleted file mode 100644 index bfaf04f..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/DialogTracks.java +++ /dev/null @@ -1,76 +0,0 @@ -/* -list with all tracks to select - */ -package space.aqoleg.cat; - -import android.app.DialogFragment; -import android.content.Context; -import android.graphics.Color; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -public class DialogTracks extends DialogFragment implements AdapterView.OnItemClickListener { - private static final String argumentCurrentTrackN = "trackN"; - - private int currentTrackN; - - static DialogTracks newInstance(int currentTrackN) { - Bundle args = new Bundle(); - args.putInt(argumentCurrentTrackN, currentTrackN); - DialogTracks dialog = new DialogTracks(); - dialog.setArguments(args); - dialog.setStyle(DialogFragment.STYLE_NO_TITLE, 0); - return dialog; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.dialog_list, container, false); - ((TextView) view.findViewById(R.id.title)).setText(getString(R.string.tracks)); - currentTrackN = getArguments().getInt(argumentCurrentTrackN); - - ListView listView = view.findViewById(R.id.list); - listView.setAdapter(new Adapter(getActivity().getApplicationContext())); - listView.setSelection(currentTrackN); - listView.setOnItemClickListener(this); - return view; - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - SavedTracks.getInstance().loadTrack( - position == currentTrackN ? -1 : position, - true, - (MainActivity) getActivity() - ); - dismiss(); - } - - private class Adapter extends ArrayAdapter { - Adapter(Context context) { - super( - context, - R.layout.list_item, - R.id.text, - SavedTracks.getInstance().getList() - ); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = super.getView(position, convertView, parent); - if (position == currentTrackN) { - view.setBackgroundColor(Color.GRAY); - } else { - view.setBackgroundColor(Color.TRANSPARENT); - } - return view; - } - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/Downloader.java b/src/app/src/main/java/space/aqoleg/cat/Downloader.java deleted file mode 100644 index 4545133..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/Downloader.java +++ /dev/null @@ -1,95 +0,0 @@ -/* -asynchronous tile downloader -downloads tile, saves it into memory and invokes callback with bitmap - */ -package space.aqoleg.cat; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.AsyncTask; - -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; - -class Downloader extends AsyncTask { - private final int mapN; - private final int z; - private final int y; - private final int x; - private final Callback callback; - private Bitmap bitmap; - - Downloader(int mapN, int z, int y, int x, Callback callback) { - this.callback = callback; - this.mapN = mapN; - this.z = z; - this.y = y; - this.x = x; - } - - @Override - protected Boolean doInBackground(Void... voids) { - Maps maps = Maps.getInstance(); - HttpURLConnection connection = null; - try { - URL url = new URL(maps.getUrl(mapN, z, y, x)); - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(7000); - connection.setReadTimeout(15000); - connection.setRequestProperty("User-Agent", "cat"); - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - return false; - } - String contentType = connection.getContentType(); // image/png, image/jpeg - DataInputStream inputStream = new DataInputStream(connection.getInputStream()); - - ByteArrayOutputStream byteArray = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - do { - length = inputStream.read(buffer); - if (length > 0) { - byteArray.write(buffer, 0, length); - } else { - break; - } - } while (true); - inputStream.close(); - byte[] content = byteArray.toByteArray(); - - bitmap = BitmapFactory.decodeByteArray(content, 0, content.length); - if (bitmap == null) { - return false; - } - String extension = '.' + contentType.substring(contentType.lastIndexOf('/') + 1); - FileOutputStream outputStream = new FileOutputStream(maps.createTile(mapN, z, y, x, extension)); - outputStream.write(content); - outputStream.close(); - return true; - } catch (Exception exception) { - if (exception instanceof IOException) { - return false; - } - Data.getInstance().writeLog("can not download tile: " + exception.toString()); - return false; - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - @Override - protected void onPostExecute(Boolean result) { - super.onPostExecute(result); - callback.onDownloadFinish(mapN, z, y, x, result ? bitmap : null); - } - - interface Callback { - void onDownloadFinish(int mapN, int z, int y, int x, Bitmap bitmap); - } -} diff --git a/src/app/src/main/java/space/aqoleg/cat/MainActivity.java b/src/app/src/main/java/space/aqoleg/cat/MainActivity.java deleted file mode 100644 index ef83bd2..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/MainActivity.java +++ /dev/null @@ -1,337 +0,0 @@ -/* -one single activity, handles state of the app, permissions, buttons and starts service - */ -package space.aqoleg.cat; - -import android.Manifest; -import android.app.Activity; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Build; -import android.os.Bundle; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.TextView; - -public class MainActivity extends Activity implements View.OnClickListener, LocationListener, SavedTracks.Callback { - private static final String preferences = "preferences"; - private static final String preferencesMapDirectoryName = "currentMapDirectoryName"; - private static final String preferencesZ = "zoomStartsFromZero"; - private static final String preferencesLocationLongitude = "lastLocationLongitude"; - private static final String preferencesLocationLatitude = "lastLocationLatitude"; - - private static final String stateSavedTrackName = "trackName"; - private static final String stateCenterLongitude = "centerLongitude"; - private static final String stateCenterLatitude = "centerLatitude"; - private static final String stateHasSelection = "hasSelection"; - private static final String stateSelectionLongitude = "selectionLongitude"; - private static final String stateSelectionLatitude = "selectionLatitude"; - - private boolean hasPermissions; - private LocationManager locationManager; // null if listener has removed - private MapView mapView; // null if listener has removed - private int mapN; - private int z; - private int trackN; - - @Override - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - for (int result : grantResults) { - if (result != PackageManager.PERMISSION_GRANTED) { - finish(); - return; - } - } - recreate(); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - checkPermissions(); - if (!hasPermissions) { - return; - } - // map and zoom from preferences - SharedPreferences sharedPreferences = getSharedPreferences(preferences, MODE_PRIVATE); - String mapDirectoryName = sharedPreferences.getString(preferencesMapDirectoryName, ""); - z = sharedPreferences.getInt(preferencesZ, 5); - // selected track and point and screen position - String trackName = ""; - float centerLongitude, centerLatitude; - boolean hasSelection = false; - float selectedLongitude = 0; - float selectedLatitude = 0; - if (savedInstanceState != null) { - trackName = savedInstanceState.getString(stateSavedTrackName); - centerLongitude = savedInstanceState.getFloat(stateCenterLongitude); - centerLatitude = savedInstanceState.getFloat(stateCenterLatitude); - hasSelection = savedInstanceState.getBoolean(stateHasSelection); - selectedLongitude = savedInstanceState.getFloat(stateSelectionLongitude); - selectedLatitude = savedInstanceState.getFloat(stateSelectionLatitude); - } else { - float[] intentData = getDataFromIntent(); - if (intentData != null) { - centerLongitude = intentData[0]; - centerLatitude = intentData[1]; - hasSelection = true; - selectedLongitude = centerLongitude; - selectedLatitude = centerLatitude; - } else { - Location location = CurrentTrack.getInstance().getLastLocation(); - if (location != null) { - centerLongitude = (float) location.getLongitude(); - centerLatitude = (float) location.getLatitude(); - } else { - centerLongitude = sharedPreferences.getFloat(preferencesLocationLongitude, 0); - centerLatitude = sharedPreferences.getFloat(preferencesLocationLatitude, 0); - } - } - } - // load data - mapN = Maps.getInstance().loadMaps(mapDirectoryName); - trackN = SavedTracks.getInstance().loadTracks(trackName); - if (trackN != -1) { - SavedTracks.getInstance().loadTrack(trackN, false, this); - } - // set views - setContentView(R.layout.activity_main); - findViewById(R.id.localDistance).setOnClickListener(this); - findViewById(R.id.zPlus).setOnClickListener(this); - findViewById(R.id.zMinus).setOnClickListener(this); - findViewById(R.id.exit).setOnClickListener(this); - findViewById(R.id.satellites).setOnClickListener(this); - findViewById(R.id.selectTrack).setOnClickListener(this); - findViewById(R.id.selectMap).setOnClickListener(this); - findViewById(R.id.center).setOnClickListener(this); - if (CurrentTrack.getInstance().getLocalDistance() != CurrentTrack.getInstance().getTotalDistance()) { - findViewById(R.id.totalDistance).setVisibility(View.VISIBLE); - } - printZoom(); - // set mapView - mapView = new MapView(this); - mapView.set(mapN, z, centerLongitude, centerLatitude, hasSelection, selectedLongitude, selectedLatitude); - ((FrameLayout) findViewById(R.id.mapView)).addView(mapView); - - startService(new Intent(getApplicationContext(), MainService.class)); - } - - @Override - public void onStart() { - super.onStart(); - if (hasPermissions) { - printDistance(); - float locationLongitude, locationLatitude; - Location location = CurrentTrack.getInstance().getLastLocation(); - if (location != null) { - locationLongitude = (float) location.getLongitude(); - locationLatitude = (float) location.getLatitude(); - } else { - SharedPreferences sharedPreferences = getSharedPreferences(preferences, MODE_PRIVATE); - locationLongitude = sharedPreferences.getFloat(preferencesLocationLongitude, 0); - locationLatitude = sharedPreferences.getFloat(preferencesLocationLatitude, 0); - } - mapView.setLocation(locationLongitude, locationLatitude); - locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); - } - } - - @Override - public void onStop() { - super.onStop(); - if (hasPermissions) { - locationManager.removeUpdates(this); - locationManager = null; - getSharedPreferences(preferences, MODE_PRIVATE) - .edit() - .putString(preferencesMapDirectoryName, Maps.getInstance().getDirectoryName(mapN)) - .putInt(preferencesZ, z) - .putFloat(preferencesLocationLongitude, mapView.getLocationLongitude()) - .putFloat(preferencesLocationLatitude, mapView.getLocationLatitude()) - .apply(); - } - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (hasPermissions) { - outState.putString(stateSavedTrackName, SavedTracks.getInstance().getTrackName(trackN)); - outState.putFloat(stateCenterLongitude, mapView.getCenterLongitude()); - outState.putFloat(stateCenterLatitude, mapView.getCenterLatitude()); - outState.putBoolean(stateHasSelection, mapView.hasSelection()); - outState.putFloat(stateSelectionLongitude, mapView.getSelectedLongitude()); - outState.putFloat(stateSelectionLatitude, mapView.getSelectedLatitude()); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (hasPermissions) { - SavedTracks.getInstance().clear(); - mapView = null; - } - } - - @Override - public void onClick(View view) { - switch (view.getId()) { - case R.id.localDistance: - if (CurrentTrack.getInstance().getLocalDistance() != 0) { - findViewById(R.id.totalDistance).setVisibility(View.VISIBLE); - } - CurrentTrack.getInstance().resetLocalDistance(); - printDistance(); - break; - case R.id.zPlus: - if (z == 18) { - return; - } - z++; - printZoom(); - mapView.changeZoom(z); - break; - case R.id.zMinus: - if (z == 0) { - return; - } - z--; - printZoom(); - mapView.changeZoom(z); - break; - case R.id.exit: - stopService(new Intent(getApplicationContext(), MainService.class)); - finish(); - break; - case R.id.satellites: - DialogSatellites.newInstance().show(getFragmentManager(), null); - break; - case R.id.selectTrack: - DialogTracks.newInstance(trackN).show(getFragmentManager(), null); - break; - case R.id.selectMap: - DialogMaps.newInstance(mapN).show(getFragmentManager(), null); - break; - case R.id.center: - mapView.center(); - break; - } - } - - @Override - public void onLocationChanged(Location location) { - if (locationManager != null) { - printDistance(); - mapView.refreshLocation(location); - } - } - - @Override - public void onStatusChanged(String s, int i, Bundle bundle) { - } - - @Override - public void onProviderEnabled(String s) { - } - - @Override - public void onProviderDisabled(String s) { - } - - @Override - public void onTrackLoad(int trackN, boolean centerMap) { - if (mapView != null) { - this.trackN = trackN; - mapView.refresh(centerMap); - } - } - - void selectMap(int mapN) { - this.mapN = mapN; - mapView.changeMap(mapN); - } - - private float[] getDataFromIntent() { - if (getIntent() == null || getIntent().getData() == null || getIntent().getData().toString() == null) { - return null; - } - try { - // geo:lat,lon or geo:lat,lon?z=zoom or geo:0,0?q=lat,lon(label) - String path = getIntent().getData().toString(); - float latitude; - float longitude; - if (path.contains("0,0?q=")) { - int index = path.indexOf(",", 6); - latitude = Float.parseFloat(path.substring(path.indexOf("=") + 1, index)); - longitude = Float.parseFloat(path.substring(index + 1, path.indexOf("(", index))); - } else { - int index = path.indexOf(","); - latitude = Float.parseFloat(path.substring(4, index)); - if (path.contains("?")) { - longitude = Float.parseFloat(path.substring(index + 1, path.indexOf("?"))); - z = Integer.parseInt(path.substring(path.indexOf("?z=") + 3)) - 1; - if (z < 0) { - z = 0; - } else if (z > 18) { - z = 18; - } - } else { - longitude = Float.parseFloat(path.substring(index + 1)); - } - } - if (latitude < -90) { - latitude = -90; - } else if (latitude > 90) { - latitude = 90; - } - if (longitude < -180) { - longitude = -180; - } else if (longitude > 180) { - longitude = 180; - } - return new float[]{longitude, latitude}; - } catch (Exception exception) { - Data.getInstance().writeLog("can not parse geo: " + exception.toString()); - return null; - } - } - - private void printDistance() { - String format = getString(R.string.distanceKm); - float distance = CurrentTrack.getInstance().getTotalDistance() / 1000; - ((TextView) findViewById(R.id.totalDistance)).setText(String.format(format, distance)); - distance = CurrentTrack.getInstance().getLocalDistance() / 1000; - ((TextView) findViewById(R.id.localDistance)).setText(String.format(format, distance)); - } - - private void printZoom() { - ((TextView) findViewById(R.id.zoom)).setText("z".concat(String.valueOf(z + 1))); - } - - private void checkPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - int permissions = checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) + - checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) + - checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (permissions != PackageManager.PERMISSION_GRANTED) { - requestPermissions( - new String[]{ - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - }, - 0 - ); - return; - } - } - hasPermissions = true; - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/MainService.java b/src/app/src/main/java/space/aqoleg/cat/MainService.java deleted file mode 100644 index 8eb728b..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/MainService.java +++ /dev/null @@ -1,106 +0,0 @@ -/* -single foreground service -opens new track and writes filtered track at starts and closes it at destroy -*/ -package space.aqoleg.cat; - -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Intent; -import android.graphics.BitmapFactory; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Bundle; -import android.os.IBinder; - -@SuppressWarnings("deprecation") -public class MainService extends Service implements LocationListener { - private LocationManager locationManager; // null if listeners has removed - private CurrentTrack track; - private long previousLocationTime; - private Location bestLocation; - private float bestLocationAccuracy; - - @Override - public void onCreate() { - super.onCreate(); - locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); - // delay 0 for not to loose the satellites - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); - - Intent startActivityIntent = new Intent(getApplicationContext(), MainActivity.class); - Notification.Builder builder = new Notification.Builder(getApplicationContext()) - .setSmallIcon(R.drawable.notification) - .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.icon)) - .setContentTitle(getString(R.string.serviceDescription)) - .setContentIntent(PendingIntent.getActivity(getApplicationContext(), 0, startActivityIntent, 0)); - startForeground(1, builder.getNotification()); - - track = CurrentTrack.getInstance(); - track.open(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return START_STICKY; - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onDestroy() { - super.onDestroy(); - locationManager.removeUpdates(this); - locationManager = null; - track.close(); - } - - @Override - public void onLocationChanged(Location location) { - if (locationManager == null) { - return; - } - if (previousLocationTime == 0) { - // first location - previousLocationTime = location.getTime() - 4000; // no delay - bestLocation = location; - bestLocationAccuracy = location.getAccuracy(); - } else { - long timeSincePreviousLocation = location.getTime() - previousLocationTime; - if (timeSincePreviousLocation > 10000) { - // time is out, use the best location - if (bestLocation == null) { - bestLocation = location; - } - track.setNewLocation(bestLocation); - // start searching the next best location - previousLocationTime = bestLocation.getTime(); - bestLocation = null; - } else if (timeSincePreviousLocation > 4000) { - // searching the new best location after delay - if (bestLocation == null || location.getAccuracy() < bestLocationAccuracy) { - // this location is the best - bestLocation = location; - bestLocationAccuracy = location.getAccuracy(); - } - } - } - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - } - - @Override - public void onProviderEnabled(String provider) { - } - - @Override - public void onProviderDisabled(String provider) { - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/MapView.java b/src/app/src/main/java/space/aqoleg/cat/MapView.java deleted file mode 100644 index f57e6a3..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/MapView.java +++ /dev/null @@ -1,521 +0,0 @@ -/* -view with the map, handles touch events - */ -package space.aqoleg.cat; - -import android.content.Context; -import android.graphics.*; -import android.location.Location; -import android.view.MotionEvent; -import android.view.View; - -public class MapView extends View implements View.OnTouchListener, BitmapCache.Callback { - private final Path path = new Path(); - private final Paint mainPaint = new Paint(); - private final Paint secondaryPaint = new Paint(); - private final Paint textPaint = new Paint(); - - private final String selectionText; - private BitmapCache bitmapCache; - private CurrentTrack currentTrack; - private SavedTracks savedTrack; - // layout constants - private float xCenterPx; - private float yCenterPx; - private int xRightPx; - private int yBottomPx; - // map parameters - private int mapN; - private int z; - private boolean isEllipsoid; - private int tileSizePx; - private int maxTileN; - private double wholeSizePx; // tileSizePx * tilesN - // center and location coordinates, from 0 to 1, in the current projection - private Location location = new Location(""); - private double xLocation1; - private double yLocation1; - private double xCenter1; - private double yCenter1; - // touch events - private long onTouchTimestampMs; - private float xTouchStartPx; - private float yTouchStartPx; - private double xCenter1TouchStart; - private double yCenter1TouchStart; - // selection - private final Location selectionLocation = new Location(""); - private boolean hasSelection; - private double xSelection1; - private double ySelection1; - - MapView(Context context) { - super(context); - selectionText = context.getString(R.string.selection); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - if (changed) { - xCenterPx = getWidth() / 2; - yCenterPx = getHeight() / 2; - xRightPx = getWidth(); - yBottomPx = getHeight(); - } - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - drawTiles(canvas); - drawPointers(canvas); - if (z > 4) { - drawCurrentPath(canvas, currentTrack.getPointsNumber()); - } - if (z > 4) { - drawSavedPath(canvas, savedTrack.getPointsNumber()); - } - if (hasSelection) { - drawSelections(canvas); - } - } - - @Override - public boolean onTouch(View view, MotionEvent event) { - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - onTouchTimestampMs = System.currentTimeMillis(); - xTouchStartPx = event.getX(); - yTouchStartPx = event.getY(); - xCenter1TouchStart = xCenter1; - yCenter1TouchStart = yCenter1; - break; - case MotionEvent.ACTION_MOVE: - xCenter1 = xCenter1TouchStart + ((xTouchStartPx - event.getX()) / wholeSizePx); - yCenter1 = yCenter1TouchStart + ((yTouchStartPx - event.getY()) / wholeSizePx); - // normalize - if (xCenter1 < 0) { - xCenter1 += 1 + (int) -xCenter1; - } else if (xCenter1 > 1) { - xCenter1 -= (int) xCenter1; - } - if (yCenter1 < 0) { - yCenter1 = 0; - } else if (yCenter1 >= 1) { - yCenter1 = 1; - } - invalidate(); - break; - case MotionEvent.ACTION_UP: - if (System.currentTimeMillis() - onTouchTimestampMs < 300 && - Math.abs(xTouchStartPx - event.getX()) < 10 && Math.abs(yTouchStartPx - event.getY()) < 10) { - onShortTouch(); - } - break; - } - return true; - } - - @Override - public void onBitmapCacheLoad() { - invalidate(); - } - - void set( - int mapN, - int z, - float centerLongitude, - float centerLatitude, - boolean hasSelection, - float selectedLongitude, - float selectedLatitude - ) { - bitmapCache = new BitmapCache(this); - currentTrack = CurrentTrack.getInstance(); - savedTrack = SavedTracks.getInstance(); - - mainPaint.setColor(Color.rgb(0xFA, 0x05, 0x05)); - mainPaint.setStyle(Paint.Style.STROKE); - mainPaint.setStrokeWidth(2); - mainPaint.setAntiAlias(true); - secondaryPaint.setColor(Color.rgb(0xEA, 0x04, 0xBC)); - secondaryPaint.setStyle(Paint.Style.STROKE); - secondaryPaint.setStrokeWidth(2); - secondaryPaint.setAntiAlias(true); - textPaint.setColor(Color.rgb(0xFA, 0x05, 0x05)); - textPaint.setAntiAlias(true); - textPaint.setTextSize(16); - - this.mapN = mapN; - this.z = z; - isEllipsoid = Maps.getInstance().isEllipsoid(mapN); - tileSizePx = Maps.getInstance().getSize(mapN); - maxTileN = (1 << z) - 1; - wholeSizePx = tileSizePx * (1 << z); - - xCenter1 = Projection.getX(centerLongitude); - yCenter1 = Projection.getY(centerLatitude, isEllipsoid); - - this.hasSelection = hasSelection; - if (hasSelection) { - selectionLocation.setLongitude(selectedLongitude); - selectionLocation.setLatitude(selectedLatitude); - xSelection1 = Projection.getX(selectedLongitude); - ySelection1 = Projection.getY(selectedLatitude, isEllipsoid); - } - - setOnTouchListener(this); - } - - void setLocation(float locationLongitude, float locationLatitude) { - location.setLongitude(locationLongitude); - location.setLatitude(locationLatitude); - xLocation1 = Projection.getX(locationLongitude); - yLocation1 = Projection.getY(locationLatitude, isEllipsoid); - invalidate(); - } - - void refreshLocation(Location location) { - if (location.distanceTo(this.location) < 20) { - return; - } - this.location = new Location(location); - xLocation1 = Projection.getX(location.getLongitude()); - yLocation1 = Projection.getY(location.getLatitude(), isEllipsoid); - invalidate(); - } - - void refresh(boolean centerMap) { - if (centerMap && savedTrack.getPointsNumber() > 0) { - xCenter1 = savedTrack.getX(0); - yCenter1 = savedTrack.getY(0, isEllipsoid); - } - invalidate(); - } - - void changeMap(int mapN) { - this.mapN = mapN; - boolean isEllipsoid = Maps.getInstance().isEllipsoid(mapN); - if (this.isEllipsoid != isEllipsoid) { - yLocation1 = Projection.getY(location.getLatitude(), isEllipsoid); - yCenter1 = Projection.getY(Projection.getLatitude(yCenter1, this.isEllipsoid), isEllipsoid); - if (hasSelection) { - ySelection1 = Projection.getY(selectionLocation.getLatitude(), isEllipsoid); - } - this.isEllipsoid = isEllipsoid; - } - tileSizePx = Maps.getInstance().getSize(mapN); - wholeSizePx = tileSizePx * (1 << z); - invalidate(); - } - - void changeZoom(int z) { - this.z = z; - maxTileN = (1 << z) - 1; - wholeSizePx = (1 << z) * tileSizePx; - invalidate(); - } - - void center() { - xCenter1 = xLocation1; - yCenter1 = yLocation1; - invalidate(); - } - - float getLocationLongitude() { - return (float) location.getLongitude(); - } - - float getLocationLatitude() { - return (float) location.getLatitude(); - } - - float getCenterLongitude() { - return (float) Projection.getLongitude(xCenter1); - } - - float getCenterLatitude() { - return (float) Projection.getLatitude(yCenter1, isEllipsoid); - } - - boolean hasSelection() { - return hasSelection; - } - - float getSelectedLongitude() { - return (float) selectionLocation.getLongitude(); - } - - float getSelectedLatitude() { - return (float) selectionLocation.getLatitude(); - } - - private void drawTiles(Canvas canvas) { - // center tile - int xTileN = (int) (xCenter1 * (1 << z)); - int yTileN = (int) (yCenter1 * (1 << z)); - int xLeftPx = (int) (xCenterPx - ((xCenter1 * (1 << z) - xTileN) * tileSizePx)); - int yTopPx = (int) (yCenterPx - ((yCenter1 * (1 << z) - yTileN) * tileSizePx)); - drawTile(canvas, xTileN, yTileN, xLeftPx, yTopPx); - // clockwise from the top - int rounds = (int) (Math.max(xCenterPx, yCenterPx) / tileSizePx) + 1; - int step = 0; - for (int round = 0; round < rounds; round++) { - step += 2; - xTileN--; - yTileN--; - xLeftPx -= tileSizePx; - yTopPx -= tileSizePx; - for (int i = 0; i < step; i++) { - xTileN++; - xLeftPx += tileSizePx; - drawTile(canvas, xTileN, yTileN, xLeftPx, yTopPx); - } - for (int i = 0; i < step; i++) { - yTileN++; - yTopPx += tileSizePx; - drawTile(canvas, xTileN, yTileN, xLeftPx, yTopPx); - } - for (int i = 0; i < step; i++) { - xTileN--; - xLeftPx -= tileSizePx; - drawTile(canvas, xTileN, yTileN, xLeftPx, yTopPx); - } - for (int i = 0; i < step; i++) { - yTileN--; - yTopPx -= tileSizePx; - drawTile(canvas, xTileN, yTileN, xLeftPx, yTopPx); - } - } - } - - private void drawTile(Canvas canvas, int tileX, int tileY, int leftPx, int topPx) { - if (leftPx < -tileSizePx || leftPx > xRightPx || topPx < -tileSizePx || topPx > yBottomPx) { - return; - } - if (tileX < 0) { - tileX += maxTileN + 1; - if (tileX < 0) { - return; - } - } else if (tileX > maxTileN) { - tileX -= maxTileN + 1; - if (tileX > maxTileN) { - return; - } - } - if (tileY < 0 || tileY > maxTileN) { - return; - } - Bitmap bitmap = bitmapCache.getBitmap(mapN, z, tileY, tileX); - if (bitmap != null) { - canvas.drawBitmap(bitmap, leftPx, topPx, null); - } - } - - private void drawPointers(Canvas canvas) { - float yPx = (float) (yCenterPx + ((yLocation1 - yCenter1) * wholeSizePx)); - if (yPx <= 0 || yPx >= yBottomPx) { - return; - } - float xPx = (float) (xCenterPx + ((xLocation1 - xCenter1) * wholeSizePx)); - drawPointer(canvas, xPx, yPx); - drawPointer(canvas, xPx + (float) wholeSizePx, yPx); - drawPointer(canvas, xPx - (float) wholeSizePx, yPx); - } - - private void drawPointer(Canvas canvas, float xPx, float yPx) { - if (xPx < 0 || xPx > xRightPx) { - return; - } - canvas.drawCircle(xPx, yPx, 8, mainPaint); - canvas.drawLine(xPx, yPx + 8, xPx, yPx + 4, mainPaint); - canvas.drawLine(xPx, yPx - 8, xPx, yPx - 4, mainPaint); - canvas.drawLine(xPx + 8, yPx, xPx + 4, yPx, mainPaint); - canvas.drawLine(xPx - 8, yPx, xPx - 4, yPx, mainPaint); - } - - private void drawCurrentPath(Canvas canvas, int pointsN) { - if (pointsN <= 0) { - return; - } - double xDeltaFromCenter1 = currentTrack.getX(0) - xCenter1; - if (xDeltaFromCenter1 > 0.5) { - xDeltaFromCenter1 -= 1; - } else if (xDeltaFromCenter1 < -0.5) { - xDeltaFromCenter1 += 1; - } - float xPx = (float) (xCenterPx + (xDeltaFromCenter1 * wholeSizePx)); - float yPx = (float) (yCenterPx + ((currentTrack.getY(0, isEllipsoid) - yCenter1) * wholeSizePx)); - float previousXPx = xPx; - float previousYPx = yPx; - boolean hasStarted = false; - if (xPx > 0 && xPx < xRightPx && yPx > 0 && yPx < yBottomPx) { - canvas.drawCircle(xPx, yPx, 2, mainPaint); - hasStarted = true; - path.moveTo(xPx, yPx); - } - for (int i = 1; i < pointsN; i++) { - xDeltaFromCenter1 = currentTrack.getX(i) - xCenter1; - if (xDeltaFromCenter1 > 0.5) { - xDeltaFromCenter1 -= 1; - } else if (xDeltaFromCenter1 < -0.5) { - xDeltaFromCenter1 += 1; - } - xPx = (float) (xCenterPx + (xDeltaFromCenter1 * wholeSizePx)); - yPx = (float) (yCenterPx + ((currentTrack.getY(i, isEllipsoid) - yCenter1) * wholeSizePx)); - if (xPx > 0 && xPx < xRightPx && yPx > 0 && yPx < yBottomPx) { - if (!hasStarted) { - hasStarted = true; - path.moveTo(previousXPx, previousYPx); - } - path.lineTo(xPx, yPx); - } else { - if (hasStarted) { - hasStarted = false; - path.lineTo(xPx, yPx); - } - previousXPx = xPx; - previousYPx = yPx; - } - } - canvas.drawPath(path, mainPaint); - path.reset(); - } - - private void drawSavedPath(Canvas canvas, int pointsN) { - if (pointsN <= 0) { - return; - } - double xDeltaFromCenter1 = savedTrack.getX(0) - xCenter1; - if (xDeltaFromCenter1 > 0.5) { - xDeltaFromCenter1 -= 1; - } else if (xDeltaFromCenter1 < -0.5) { - xDeltaFromCenter1 += 1; - } - float xPx = (float) (xCenterPx + (xDeltaFromCenter1 * wholeSizePx)); - float yPx = (float) (yCenterPx + ((savedTrack.getY(0, isEllipsoid) - yCenter1) * wholeSizePx)); - float previousXPx = xPx; - float previousYPx = yPx; - boolean hasStarted = false; - if (xPx > 0 && xPx < xRightPx && yPx > 0 && yPx < yBottomPx) { - canvas.drawCircle(xPx, yPx, 2, secondaryPaint); - hasStarted = true; - path.moveTo(xPx, yPx); - } - for (int i = 1; i < pointsN; i++) { - xDeltaFromCenter1 = savedTrack.getX(i) - xCenter1; - if (xDeltaFromCenter1 > 0.5) { - xDeltaFromCenter1 -= 1; - } else if (xDeltaFromCenter1 < -0.5) { - xDeltaFromCenter1 += 1; - } - xPx = (float) (xCenterPx + (xDeltaFromCenter1 * wholeSizePx)); - yPx = (float) (yCenterPx + ((savedTrack.getY(i, isEllipsoid) - yCenter1) * wholeSizePx)); - if (xPx > 0 && xPx < xRightPx && yPx > 0 && yPx < yBottomPx) { - if (!hasStarted) { - hasStarted = true; - path.moveTo(previousXPx, previousYPx); - } - path.lineTo(xPx, yPx); - } else { - if (hasStarted) { - hasStarted = false; - path.lineTo(xPx, yPx); - } - previousXPx = xPx; - previousYPx = yPx; - } - } - canvas.drawPath(path, secondaryPaint); - path.reset(); - } - - private void drawSelections(Canvas canvas) { - float yPx = (float) (yCenterPx + ((ySelection1 - yCenter1) * wholeSizePx)); - if (yPx <= 0 || yPx >= yBottomPx) { - return; - } - float xPx = (float) (xCenterPx + ((xSelection1 - xCenter1) * wholeSizePx)); - boolean hasText = drawSelection(canvas, xPx, yPx, true); - hasText |= drawSelection(canvas, xPx - (float) wholeSizePx, yPx, !hasText); - drawSelection(canvas, xPx + (float) wholeSizePx, yPx, !hasText); - } - - private boolean drawSelection(Canvas canvas, float xPx, float yPx, boolean withText) { - if (xPx < 0 || xPx > xRightPx) { - return false; - } - canvas.drawCircle(xPx, yPx, 8, mainPaint); - double rad = Math.toRadians(90 - selectionLocation.bearingTo(location)); - canvas.drawLine( - (float) (xPx + Math.cos(rad) * 8), - (float) (yPx - Math.sin(rad) * 8), - (float) (xPx + Math.cos(rad) * 16), - (float) (yPx - Math.sin(rad) * 16), - mainPaint - ); - if (!withText) { - return false; - } - - float distance = location.distanceTo(selectionLocation); - float bearing = location.bearingTo(selectionLocation); - if (bearing < 0) { - bearing += 360; - } - String text = String.format(selectionText, Math.round(bearing), distance / 1000); - float halfTextWidth = textPaint.measureText(text) / 2; - if (xPx < halfTextWidth + 10) { - xPx = 10; - } else if (xPx > xRightPx - halfTextWidth - 10) { - xPx = xRightPx - 2 * halfTextWidth - 10; - } else { - xPx -= halfTextWidth; - } - if (yPx < yCenterPx) { - yPx += 28; - } else { - yPx -= 18; - } - canvas.drawText(text, xPx, yPx, textPaint); - return true; - } - - private void onShortTouch() { - if (hasSelection) { - // if touch current selection, deselect and return - float xSelectionPx = (float) (xCenterPx + ((xSelection1 - xCenter1) * wholeSizePx)); - float ySelectionPx = (float) (yCenterPx + ((ySelection1 - yCenter1) * wholeSizePx)); - if (Math.abs(yTouchStartPx - ySelectionPx) < 16) { - if (Math.abs(xTouchStartPx - xSelectionPx) < 16 || - Math.abs(xTouchStartPx - xSelectionPx - wholeSizePx) < 16 || - Math.abs(xTouchStartPx - xSelectionPx + wholeSizePx) < 16) { - hasSelection = false; - invalidate(); - return; - } - } - } - ySelection1 = yCenter1 + ((yTouchStartPx - yCenterPx) / wholeSizePx); - if (ySelection1 < 0 || ySelection1 > 1) { - // out of map, deselect and return - hasSelection = false; - invalidate(); - return; - } - xSelection1 = xCenter1 + ((xTouchStartPx - xCenterPx) / wholeSizePx); - // normalize - if (xSelection1 < 0) { - xSelection1 += 1 + (int) -xSelection1; - } else if (xSelection1 > 1) { - xSelection1 -= (int) xSelection1; - } - // set new selection - hasSelection = true; - selectionLocation.setLongitude(Projection.getLongitude(xSelection1)); - selectionLocation.setLatitude(Projection.getLatitude(ySelection1, isEllipsoid)); - invalidate(); - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/Maps.java b/src/app/src/main/java/space/aqoleg/cat/Maps.java deleted file mode 100644 index 389765c..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/Maps.java +++ /dev/null @@ -1,217 +0,0 @@ -/* -map properties singleton -creates default maps on the first app launch, parses list of maps properties from json files, -searches current map by its name on the app start, handles tile's parameters: url, size, projection, name and files - -tile's parameters: -z - zoom from 0 to 18, displayed zoom = (z + 1) -x, y - tiles number, tile(0, 0) is the top left tile, from 0 to (2^z - 1) - -files: -/maps/map1/, /maps/map2/, ... , /maps/mapN/ -/maps/map1/properties.txt // optional -/maps/map1/z/y/x.extension // for example /maps/map1/10/4/4.png or /maps/map1/10/4/4.jpeg - -properties.txt json file: -{ - "name": "map name", // optional, if this is not specified, use directory name - "url": "https://example/x=%1$d/y=%2$d/z=%3$d", // optional, if this is not specified, can not download - "size": 256, // optional, if this is not specified, use 256 - "projection": "ellipsoid" // optional, if this is not specified, use spherical -} - */ -package space.aqoleg.cat; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import org.json.JSONObject; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; - -class Maps { - private static final String propertiesFileName = "properties.txt"; - private static final String jsonName = "name"; - private static final String jsonUrl = "url"; - private static final String jsonSize = "size"; - private static final String jsonProjection = "projection"; - private static final String jsonProjectionEllipsoid = "ellipsoid"; - private static final int sizeDefault = 256; - - private static final Maps maps = new Maps(); - - private final ArrayList nameList = new ArrayList<>(); - private String[] pathArray; // absolute path of the map's directory - private String[] urlArray; // can be empty - private int[] sizeArray; - private boolean[] isEllipsoidArray; // if true ellipsoid, else spherical - - private Maps() { - } - - static Maps getInstance() { - return maps; - } - - // returns number of the map with this searchingDirectoryName or 0 - int loadMaps(String searchingDirectoryName) { - String[] list = Data.getInstance().getMaps().list(); - Arrays.sort(list); - int mapsCount = list.length; - if (mapsCount == 0) { - addDefaultMaps(); - return loadMaps(""); - } else { - nameList.clear(); - pathArray = new String[mapsCount]; - urlArray = new String[mapsCount]; - sizeArray = new int[mapsCount]; - isEllipsoidArray = new boolean[mapsCount]; - int mapN = 0; - int i = 0; - for (String string : list) { - if (string.equals(searchingDirectoryName)) { - mapN = i; - } - loadMap(string, i); - i++; - } - return mapN; - } - } - - // for save state and then load it with loadMaps() - String getDirectoryName(int mapN) { - return new File(pathArray[mapN]).getName(); - } - - // for fragment's adapter - ArrayList getList() { - return nameList; - } - - boolean canDownload(int mapN) { - return !urlArray[mapN].isEmpty(); - } - - String getUrl(int mapN, int z, int y, int x) { - return String.format(urlArray[mapN], x, y, z); - } - - int getSize(int mapN) { - return sizeArray[mapN]; - } - - boolean isEllipsoid(int mapN) { - return isEllipsoidArray[mapN]; - } - - // returns .png file, or .jpeg file, or null - Bitmap getTile(int mapN, int z, int y, int x) { - String path = pathArray[mapN] + File.separator + z + File.separator + y + File.separator + x; - Bitmap bitmap = BitmapFactory.decodeFile(path + ".png"); - if (bitmap != null) { - return bitmap; - } - return BitmapFactory.decodeFile(path + ".jpeg"); - } - - // creates all directories if they are not exist - File createTile(int mapN, int z, int y, int x, String extension) { - File yDirectory = new File(pathArray[mapN] + File.separator + z + File.separator + y); - if (!yDirectory.isDirectory() && !yDirectory.mkdirs()) { - Data.getInstance().writeLog("can not create " + yDirectory.getAbsolutePath()); - } - return new File(yDirectory, x + extension); - } - - private void addDefaultMaps() { - addMap( - "osm", - "open street map", - "http://a.tile.openstreetmap.org/%3$d/%1$d/%2$d.png", - false - ); - addMap( - "topo", - "marshruty", - "http://maps.marshruty.ru/ml.ashx?al=1&i=1&x=%1$d&y=%2$d&z=%3$d", - false - ); - addMap( - "yasat", - "yandex satellites", - "http://sat01.maps.yandex.net/tiles?l=sat&x=%1$d&y=%2$d&z=%3$d&g=Gagarin", - true - ); - addMap( - "arctopo", - "arcgis topo map", - "http://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/%3$d/%2$d/%1$d", - false - ); - addMap( - "arcsat", - "arcgis satellite", - "http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/%3$d/%2$d/%1$d", - false - ); - } - - private void addMap(String directoryName, String mapName, String url, boolean projectionsIsEllipsoid) { - File directory = new File(Data.getInstance().getMaps(), directoryName); - if (!directory.mkdir()) { - Data.getInstance().writeLog("can not create " + directory.getAbsolutePath()); - return; - } - try { - JSONObject json = new JSONObject(); - json.put(jsonName, mapName); - json.put(jsonUrl, url); - if (projectionsIsEllipsoid) { - json.put(jsonProjection, jsonProjectionEllipsoid); - } - byte[] data = json.toString(3).getBytes(); - - FileOutputStream outputStream = new FileOutputStream(new File(directory, propertiesFileName)); - outputStream.write(data); - outputStream.close(); - } catch (Exception e) { - Data.getInstance().writeLog("can not write properties " + directory.getAbsolutePath() + " " + e.toString()); - } - } - - private void loadMap(String directoryName, int mapN) { - pathArray[mapN] = new File(Data.getInstance().getMaps(), directoryName).getAbsolutePath(); - urlArray[mapN] = ""; - String name = ""; - File file = new File(pathArray[mapN], propertiesFileName); - if (file.isFile()) { - try { - FileInputStream inputStream = new FileInputStream(file); - byte[] data = new byte[inputStream.available()]; - int bytesRead = inputStream.read(data); - inputStream.close(); - if (bytesRead != data.length) { - throw new IOException(); - } - - JSONObject json = new JSONObject(new String(data, "UTF-8")); - name = json.optString(jsonName); - urlArray[mapN] = json.optString(jsonUrl); - sizeArray[mapN] = json.optInt(jsonSize); - isEllipsoidArray[mapN] = json.optString(jsonProjection).equals(jsonProjectionEllipsoid); - } catch (Exception e) { - Data.getInstance().writeLog("can not read properties " + e.toString()); - } - } - nameList.add(name.isEmpty() ? directoryName : name); - if (sizeArray[mapN] == 0) { - sizeArray[mapN] = sizeDefault; - } - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/Projection.java b/src/app/src/main/java/space/aqoleg/cat/Projection.java deleted file mode 100644 index 4bd1dbb..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/Projection.java +++ /dev/null @@ -1,86 +0,0 @@ -/* -math for projections -converts tiles coordinates to longitude-latitude coordinates and back -spherical (epsg3857, epsg4326, web mercator, default) and ellipsoid (epsg3395) -https://pubs.usgs.gov/pp/1395/report.pdf - */ -package space.aqoleg.cat; - -class Projection { - private static final double pi2; // 2*pi - private static final double pi1_2; // pi/2 - private static final double pi1_4; // pi/4 - private static final double e; // ellipsoid eccentricity - private static final double e1_2; // e/2 - - static { - pi2 = Math.PI * 2; - pi1_2 = Math.PI / 2; - pi1_4 = Math.PI / 4; - // [1 - (polarRadius**2 / equatorialRadius**2)]**0.5 - e = Math.sqrt(1 - (Math.pow(6356752.3142, 2) / Math.pow(6378137, 2))); - e1_2 = e / 2; - } - - /* - longitude in decimal degrees from -180 (west) to 180 (east) - x from 0 (longitude -180) to 1 (longitude 180) - use double values, float has not enough precious - this is the same for both projections - xRadian = longitudeRadian - x = (xRadian - x0Radian) / 2pi = (longitude + 180) / 360 - */ - static double getX(double longitude) { - return (longitude + 180) / 360; - } - - static double getLongitude(double x) { - return x * 360 - 180; - } - - /* - latitude in decimal degrees from -85 (south) to 85 (north) - y from 0 (north) to 1 (south), reversed direction - use double values, float has not enough precious - y = (y0Radian - yRadian) / 2pi = (pi - yRadian) / 2pi = 0.5 - yRadian / 2pi - yRadian = y0Radian - y * 2pi = pi - y * 2pi - */ - static double getY(double latitude, boolean ellipsoid) { - // yRadian = ln[ tan(pi/4 + latitudeRadian/2) * k ] - if (!ellipsoid) { - // k = 1 - return 0.5 - Math.log(Math.tan(pi1_4 + Math.toRadians(latitude) / 2)) / pi2; - } else { - // k = [ (1 - e*sin(latitudeRadian)) / (1 + e*sin(latitudeRadian)) ]**(e/2) - double latitudeRadian = Math.toRadians(latitude); - double k = e * Math.sin(latitudeRadian); - k = Math.pow((1 - k) / (1 + k), e1_2); - return 0.5 - Math.log(Math.tan(pi1_4 + latitudeRadian / 2) * k) / pi2; - } - } - - static double getLatitude(double y, boolean ellipsoid) { - if (!ellipsoid) { - // latitudeRadian = arctan[ sinh(yRadian) ] - return Math.toDegrees(Math.atan(Math.sinh(Math.PI - (pi2 * y)))); - } else { - // latitudeRadian = - // pi/2 - 2*arctan[ t * ( (1 - e * sin(latitudeRadian) / (1 + e * sin(latitudeRadian)) )**(e/2) ] - // t = e ** (-yRadian) - double t = Math.exp((pi2 * y) - Math.PI); - // first trial latitude = pi/2 - 2*arctan(t) - double trialLatitudeRadian = pi1_2 - 2 * Math.atan(t); - double latitudeRadian = 0; - // usually 1 - 2 iterations - for (int i = 0; i < 5; i++) { - latitudeRadian = e * Math.sin(trialLatitudeRadian); - latitudeRadian = pi1_2 - 2 * Math.atan(t * Math.pow((1 - latitudeRadian) / (1 + latitudeRadian), e1_2)); - if (Math.abs(trialLatitudeRadian - latitudeRadian) < 0.000000001) { // 1 cm max - break; - } - trialLatitudeRadian = latitudeRadian; - } - return Math.toDegrees(latitudeRadian); - } - } -} \ No newline at end of file diff --git a/src/app/src/main/java/space/aqoleg/cat/SavedTracks.java b/src/app/src/main/java/space/aqoleg/cat/SavedTracks.java deleted file mode 100644 index 6707407..0000000 --- a/src/app/src/main/java/space/aqoleg/cat/SavedTracks.java +++ /dev/null @@ -1,189 +0,0 @@ -/* -saved tracks singleton -creates list of tracks, searches current track by its name on the app start, -asynchronous parses .gpx file and keeps points of the track - */ -package space.aqoleg.cat; - -import android.os.AsyncTask; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Arrays; - -class SavedTracks { - private static final SavedTracks savedTracks = new SavedTracks(); - - private final ArrayList nameList = new ArrayList<>(); - private ArrayList xList = new ArrayList<>(); - private ArrayList ySphericalList = new ArrayList<>(); - private ArrayList yEllipsoidList = new ArrayList<>(); - private Loader loader; - - private SavedTracks() { - } - - static SavedTracks getInstance() { - return savedTracks; - } - - // returns number of the track with this searchingTrackName or -1 - int loadTracks(String searchingTrackName) { - clear(); - String currentTrackName = CurrentTrack.getInstance().getName(); - String[] list = Data.getInstance().getTracks().list(); - Arrays.sort(list); - int trackN = -1; - int i = 0; - for (String string : list) { - if (!string.equals(currentTrackName)) { // can be null - if (string.equals(searchingTrackName)) { - trackN = i; - } - nameList.add(string); - i++; - } - } - return trackN; - } - - void clear() { - nameList.clear(); - xList.clear(); - ySphericalList.clear(); - yEllipsoidList.clear(); - if (loader != null) { - loader.cancel(true); - loader = null; - } - } - - String getTrackName(int trackN) { - return trackN == -1 ? "" : nameList.get(trackN); - } - - // for fragment's adapter - ArrayList getList() { - return nameList; - } - - int getPointsNumber() { - return xList.size(); - } - - double getX(int pointN) { - return xList.get(pointN); - } - - double getY(int pointN, boolean isEllipsoid) { - if (isEllipsoid) { - return yEllipsoidList.get(pointN); - } else { - return ySphericalList.get(pointN); - } - } - - void loadTrack(int trackN, boolean centerMap, Callback callback) { - if (loader != null) { - loader.cancel(true); - loader = null; - } - if (trackN == -1) { - xList.clear(); - ySphericalList.clear(); - yEllipsoidList.clear(); - callback.onTrackLoad(-1, false); - } else { - loader = new Loader(trackN, centerMap, callback); - loader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - } - - interface Callback { - void onTrackLoad(int trackN, boolean centerMap); - } - - class Loader extends AsyncTask { - private final int trackN; - private final boolean centerMap; - private final Callback callback; - private final ArrayList loaderXList = new ArrayList<>(); - private final ArrayList loaderYSphericalList = new ArrayList<>(); - private final ArrayList loaderYEllipsoidList = new ArrayList<>(); - - Loader(int trackN, boolean centerMap, Callback callback) { - this.trackN = trackN; - this.centerMap = centerMap; - this.callback = callback; - } - - @Override - protected Void doInBackground(Void... voids) { - try { - File file = new File(Data.getInstance().getTracks(), nameList.get(trackN)); - BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file))); - String trackPoint = null; // the whole string - String line; - int startIndex, stopIndex; - double longitude, latitude; - do { - line = reader.readLine(); - if (line == null) { - break; - } - if (trackPoint == null) { - if (line.contains("")) { - continue; - } - - startIndex = trackPoint.indexOf("lat"); - startIndex = trackPoint.indexOf('"', startIndex) + 1; - stopIndex = trackPoint.indexOf('"', startIndex); - latitude = Double.valueOf(trackPoint.substring(startIndex, stopIndex)); - - startIndex = trackPoint.indexOf("lon"); - startIndex = trackPoint.indexOf('"', startIndex) + 1; - stopIndex = trackPoint.indexOf('"', startIndex); - longitude = Double.valueOf(trackPoint.substring(startIndex, stopIndex)); - - trackPoint = null; - loaderYSphericalList.add(Projection.getY(latitude, false)); - loaderYEllipsoidList.add(Projection.getY(latitude, true)); - loaderXList.add(Projection.getX(longitude)); - } while (true); - reader.close(); - } catch (Exception exception) { - Data.getInstance().writeLog("can not read track " + exception.toString()); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - loader = null; - if (loaderXList.size() > 0) { - xList = loaderXList; - ySphericalList = loaderYSphericalList; - yEllipsoidList = loaderYEllipsoidList; - callback.onTrackLoad(trackN, centerMap); - } else { - xList.clear(); - ySphericalList.clear(); - yEllipsoidList.clear(); - callback.onTrackLoad(trackN, false); - } - } - } -} \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_main.xml b/src/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 7e521e0..0000000 --- a/src/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/app/src/main/res/layout/dialog_list.xml b/src/app/src/main/res/layout/dialog_list.xml deleted file mode 100644 index 1549697..0000000 --- a/src/app/src/main/res/layout/dialog_list.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/app/src/main/res/layout/dialog_satellites.xml b/src/app/src/main/res/layout/dialog_satellites.xml deleted file mode 100644 index 5fd73d3..0000000 --- a/src/app/src/main/res/layout/dialog_satellites.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/app/src/main/res/layout/list_item.xml b/src/app/src/main/res/layout/list_item.xml deleted file mode 100644 index 58dbe60..0000000 --- a/src/app/src/main/res/layout/list_item.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/app/src/main/res/mipmap-hdpi/icon.png b/src/app/src/main/res/mipmap-hdpi/icon.png deleted file mode 100644 index 3cc76fa..0000000 Binary files a/src/app/src/main/res/mipmap-hdpi/icon.png and /dev/null differ diff --git a/src/app/src/main/res/mipmap-mdpi/icon.png b/src/app/src/main/res/mipmap-mdpi/icon.png deleted file mode 100644 index 3fb791d..0000000 Binary files a/src/app/src/main/res/mipmap-mdpi/icon.png and /dev/null differ diff --git a/src/app/src/main/res/mipmap-xhdpi/icon.png b/src/app/src/main/res/mipmap-xhdpi/icon.png deleted file mode 100644 index 36b9847..0000000 Binary files a/src/app/src/main/res/mipmap-xhdpi/icon.png and /dev/null differ diff --git a/src/app/src/main/res/values/colors.xml b/src/app/src/main/res/values/colors.xml deleted file mode 100644 index 76a8d77..0000000 --- a/src/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #fa0505 - \ No newline at end of file diff --git a/src/app/src/main/res/values/strings.xml b/src/app/src/main/res/values/strings.xml deleted file mode 100644 index af24a30..0000000 --- a/src/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - cat - all maps offline and track writer - track writer - - %1$.1f km - increase zoom - decrease zoom - exit - satellites - select track - select map - center map - %1$d\u00B0 %2$.1f km - maps - - tracks - - longitude %1$f\u00B0\nlatitude %2$f\u00B0\naltitude %3$d m\naccuracy %4$d m\nspeed %5$d - km/h - - available %1$d, used %2$d - \ No newline at end of file diff --git a/src/svg/icon.svg b/src/svg/icon.svg index 726e466..b39c61f 100644 --- a/src/svg/icon.svg +++ b/src/svg/icon.svg @@ -1,12 +1,33 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -14,13 +35,4 @@ - - - - - - - - - - \ No newline at end of file +