Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
hauke96 committed Oct 24, 2021
2 parents cd2edbe + 2db4acd commit 2a36304
Show file tree
Hide file tree
Showing 31 changed files with 1,057 additions and 383 deletions.
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<img align="right" width="64px" src="https://raw.githubusercontent.com/hauke96/GeoNotes/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png">

# GeoNotes
A simple app to create and manage georeferenced notes (text and photos) on a map. The goal is to create the notes very fast and without any unnecessary UI/UX overhead.
A simple app to create and manage georeferenced notes (text and photos) on a map. The goal is to create the notes as fast as possible without any unnecessary UI/UX overhead.

<p align="center">
<img src="screenshots.png" alt="GeoNotes Screenshots"/>
</p>

## Get it
## Download

[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/packages/de.hauke_stieler.geonotes/)
[<img src="https://user-images.githubusercontent.com/663460/26973090-f8fdc986-4d14-11e7-995a-e7c5e79ed925.png" alt="Download APK from GitHub" height="60">](https://github.com/hauke96/geonotes/releases/latest)
Expand All @@ -26,6 +26,28 @@ See the [OSM Wiki page](https://wiki.openstreetmap.org/wiki/GeoNotes) for detail
* Export all notes in GeoJson format
* Show and follow current location

### Not in the scope of this app
## Use-case and Philosophy

This is the basic use-case of this app:

* Take notes while being outside (maybe even while walking or sitting in a bus)

To enable this, the app follows some basic principles:

* **Simplicity:** Make creating, editing, moving and deleting of notes as fast/easy as possible
* **No upload** of data and no creation of notes on osm.org
* **General purpose:** No restriction in the content of a note
* **Not a note management tool:** No import, no high level management operations
* **Simple and pragmatic UI:** No unnecessary animations, no overloaded UIs
* **Feature toggles:** The possibility to enable/disable features

Features that will *not* make it into GeoNotes:

* Offline maps: Too much work (where does the data come from? What format? When to update the data? Vector or raster data/tiles? etc.)
* Creating notes on osm.org
* Directly editing data
* All sorts of features that will only be used by ~5% (meaning a very small amount) of the users
* iOS support

Use other apps like [StreetComplete](https://github.com/streetcomplete/StreetComplete) if you want to directly edit OSM data or to create notes on osm.org.

* Add notes on osm.org (use other apps like [StreetComplete](https://github.com/streetcomplete/StreetComplete) for that)
12 changes: 7 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {
applicationId "de.hauke_stieler.geonotes"
minSdkVersion 16
targetSdkVersion 30
versionCode 1004002
versionName "1.4.2"
versionCode 1004003
versionName "1.4.3"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand All @@ -37,14 +37,16 @@ android {
dependencies {
implementation 'org.osmdroid:osmdroid-android:6.1.8'

implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'org.apache.commons:commons-text:1.9'
implementation 'com.google.code.gson:gson:2.8.8'
implementation 'me.himanshusoni.gpxparser:gpx-parser:1.13'

testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-inline:3.8.0'

testImplementation 'androidx.test.ext:junit:1.1.2'
testImplementation 'androidx.test.ext:junit:1.1.3'
testImplementation 'org.robolectric:robolectric:4.5.1'
}
14 changes: 10 additions & 4 deletions app/src/main/java/de/hauke_stieler/geonotes/Injector.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
import android.content.Context;
import android.content.SharedPreferences;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ComponentActivity;

import org.osmdroid.views.MapView;

import java.util.HashMap;
Expand Down Expand Up @@ -36,6 +33,7 @@ public class Injector {
classBuilders.put(Database.class, () -> buildDatabase());
classBuilders.put(Exporter.class, () -> buildExporter());
classBuilders.put(SharedPreferences.class, () -> buildSharedPreferences());
classBuilders.put(MapView.class, () -> buildMapView());
classBuilders.put(de.hauke_stieler.geonotes.map.Map.class, () -> buildMap());
}

Expand All @@ -58,6 +56,10 @@ public static <T> T get(Class<T> clazz) {
return (T) instance;
}

public static void put(Object instance) {
classes.put(instance.getClass(), instance);
}

private static Database buildDatabase() {
return new Database(context);
}
Expand All @@ -70,8 +72,12 @@ private static SharedPreferences buildSharedPreferences() {
return context.getSharedPreferences(context.getString(R.string.pref_file), MODE_PRIVATE);
}

private static MapView buildMapView() {
return activity.findViewById(R.id.map);
}

private static de.hauke_stieler.geonotes.map.Map buildMap() {
MapView mapView = activity.findViewById(R.id.map);
MapView mapView = get(MapView.class);
return new de.hauke_stieler.geonotes.map.Map(context, mapView, get(Database.class), get(SharedPreferences.class));
}
}
72 changes: 44 additions & 28 deletions app/src/main/java/de/hauke_stieler/geonotes/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
Expand All @@ -22,6 +20,7 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.appcompat.widget.PopupMenu;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
Expand All @@ -42,7 +41,7 @@
import de.hauke_stieler.geonotes.database.Database;
import de.hauke_stieler.geonotes.export.Exporter;
import de.hauke_stieler.geonotes.map.Map;
import de.hauke_stieler.geonotes.map.MarkerWindow;
import de.hauke_stieler.geonotes.map.MarkerFragment;
import de.hauke_stieler.geonotes.map.TouchDownListener;
import de.hauke_stieler.geonotes.note_list.NoteListActivity;
import de.hauke_stieler.geonotes.photo.ThumbnailUtil;
Expand Down Expand Up @@ -76,27 +75,36 @@ protected void onCreate(Bundle savedInstanceState) {

setContentView(R.layout.activity_main);

database = Injector.get(Database.class);
preferences = Injector.get(SharedPreferences.class);
exporter = Injector.get(Exporter.class);

toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

// Set HTML text of copyright label
((TextView) findViewById(R.id.copyright)).setMovementMethod(LinkMovementMethod.getInstance());
((TextView) findViewById(R.id.copyright)).setText(Html.fromHtml("© <a href=\"https://openstreetmap.org/copyright\">OpenStreetMap</a> contributors"));

final Context context = getApplicationContext();

requestPermissionsIfNecessary(new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.CAMERA
});

database = Injector.get(Database.class);
preferences = Injector.get(SharedPreferences.class);
exporter = Injector.get(Exporter.class);

createMarkerFragment();
createMap();
loadPreferences();
}

private void createMarkerFragment() {
MarkerFragment markerFragment = new MarkerFragment();

getSupportFragmentManager().beginTransaction()
.setReorderingAllowed(true)
.add(R.id.map_marker_fragment, markerFragment, null)
.commit();

Injector.put(markerFragment);
}

private void createMap() {
Expand All @@ -116,8 +124,9 @@ void loadPreferences() {
boolean snapNoteToGps = preferences.getBoolean(getString(R.string.pref_snap_note_gps), false);
map.setSnapNoteToGps(snapNoteToGps);

boolean enableRotatingMap1 = preferences.getBoolean(getString(R.string.pref_enable_rotating_map), false);
map.updateMapRotationBehavior(enableRotatingMap1);
boolean enableRotatingMap = preferences.getBoolean(getString(R.string.pref_enable_rotating_map), false);
float mapRotation = preferences.getFloat(getString(R.string.pref_map_rotation), 0f);
map.updateMapRotation(enableRotatingMap, mapRotation);

float lat = preferences.getFloat(getString(R.string.pref_last_location_lat), 0f);
float lon = preferences.getFloat(getString(R.string.pref_last_location_lon), 0f);
Expand All @@ -126,6 +135,26 @@ void loadPreferences() {
map.setLocation(lat, lon, zoom);
}

private void showExportPopupMenu() {
PopupMenu exportPopupMenu = new PopupMenu(this, findViewById(R.id.toolbar_btn_export));

exportPopupMenu.getMenu().add(0, 0, 0, "GeoJson");
exportPopupMenu.getMenu().add(0, 1, 1, "GPX");

exportPopupMenu.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case 0:
exporter.shareAsGeoJson();
break;
case 1:
exporter.shareAsGpx();
break;
}
return true;
});
exportPopupMenu.show();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.toolbar_menu, menu);
Expand All @@ -146,7 +175,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
}
return true;
case R.id.toolbar_btn_export:
exporter.export();
showExportPopupMenu();
return true;
case R.id.toolbar_btn_settings:
startActivity(new Intent(this, SettingsActivity.class));
Expand Down Expand Up @@ -227,7 +256,7 @@ public boolean onZoom(ZoomEvent event) {

@SuppressLint("RestrictedApi")
TouchDownListener touchDownListener = () -> {
ActionMenuItemView menuItem = (ActionMenuItemView) findViewById(R.id.toolbar_btn_gps_follow);
ActionMenuItemView menuItem = findViewById(R.id.toolbar_btn_gps_follow);
if (menuItem != null) {
menuItem.setIcon(getResources().getDrawable(R.drawable.ic_location_searching));
}
Expand All @@ -240,7 +269,7 @@ public boolean onZoom(ZoomEvent event) {
* Adds a listener for the camera button. The camera action can only be performed from within an activity.
*/
private void addCameraListener() {
MarkerWindow.RequestPhotoEventHandler requestPhotoEventHandler = (Long noteId) -> {
MarkerFragment.RequestPhotoEventHandler requestPhotoEventHandler = (Long noteId) -> {
if (!hasPermission(Manifest.permission.CAMERA) || !hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
// We don't have camera and/or storage permissions -> ask for them
ActivityCompat.requestPermissions(
Expand Down Expand Up @@ -280,7 +309,6 @@ protected void onActivityResult(int requestCode, int resultCode, @Nullable Inten
switch (requestCode) {
case REQUEST_IMAGE_CAPTURE:
addPhotoToDatabase(lastPhotoNoteId, lastPhotoFile);
addPhotoToGallery(lastPhotoFile);
map.addImagesToMarkerWindow();
break;
case REQUEST_NOTE_LIST_REQUEST_CODE:
Expand Down Expand Up @@ -318,18 +346,6 @@ private void addPhotoToDatabase(Long noteId, File photoFile) {
}
}

private void addPhotoToGallery(File photoFile) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.TITLE, photoFile.getName());
values.put(MediaStore.Images.Media.DISPLAY_NAME, photoFile.getName());
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpg");
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis());
values.put(MediaStore.Images.Media.DATE_TAKEN, photoFile.lastModified());
values.put(MediaStore.Images.Media.DATA, photoFile.toString());

getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
}

/**
* Stores the current map location and zoom in the shared preferences.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public void removePhotos(long noteId, File storageDir) {

photoStore.removePhotos(getWritableDatabase(), noteId);

for(String photo:photos){
for (String photo : photos) {
File photoFile = new File(storageDir, photo);
File thumbnailFile = ThumbnailUtil.getThumbnailFile(photoFile);

Expand Down
58 changes: 50 additions & 8 deletions app/src/main/java/de/hauke_stieler/geonotes/export/Exporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,85 @@
import android.content.Intent;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import de.hauke_stieler.geonotes.common.FileHelper;
import de.hauke_stieler.geonotes.database.Database;
import de.hauke_stieler.geonotes.notes.Note;
import me.himanshusoni.gpxparser.GPXWriter;
import me.himanshusoni.gpxparser.modal.GPX;
import me.himanshusoni.gpxparser.modal.Waypoint;

public class Exporter {
private static final String LOGTAG = Exporter.class.getName();

private final Database database;
private final Context context;

public Exporter(Database database, Context context){
public Exporter(Database database, Context context) {
this.database = database;
this.context = context;
}

public void export() {
public void shareAsGeoJson() {
String geoJson = GeoJson.toGeoJson(database.getAllNotes());
String fileExtension = ".geojson";
String mimeType = "application/geo+json";

openShareIntent(geoJson, fileExtension, mimeType);
}

public void shareAsGpx() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
List<Note> notes = database.getAllNotes();
GPX gpx = new GPX();

try {
for (Note note : notes) {
Waypoint waypoint = new Waypoint(note.getLat(), note.getLon());
waypoint.setName(note.getId() + "");
waypoint.setDescription(note.getDescription());
gpx.addWaypoint(waypoint);
}

GPXWriter writer = new GPXWriter();
writer.writeGPX(gpx, outputStream);
} catch (Exception e) {
Log.e(LOGTAG, "GPX creation failed: " + e.toString());
}

String fileExtension = ".gpx";
String mimeType = "application/gpx+xml";

openShareIntent(new String(outputStream.toByteArray()), fileExtension, mimeType);
}

private void openShareIntent(String data, String fileExtension, String mimeType) {
try {
String timeStamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date());

File storageDir = context.getExternalFilesDir("GeoNotes");
File exportFile = new File(storageDir, "geonotes-export.geojson");
File exportFile = new File(storageDir, "geonotes-export_" + timeStamp + fileExtension);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(exportFile));
outputStreamWriter.write(geoJson);
outputStreamWriter.write(data);
outputStreamWriter.close();

Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, FileHelper.getFileUri(context, exportFile));
sendIntent.setType("application/json");
sendIntent.setType(mimeType);

Intent shareIntent = Intent.createChooser(sendIntent, null);
shareIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // needed because we're outside of an activity
context.startActivity(shareIntent);
} catch (IOException e) {
Log.e("Export", "File write failed: " + e.toString());
} catch (Exception e) {
Log.e(LOGTAG, "File write failed: " + e.toString());
}
}
}
Loading

0 comments on commit 2a36304

Please sign in to comment.