Skip to content

Commit 2a36304

Browse files
committed
Merge branch 'dev'
2 parents cd2edbe + 2db4acd commit 2a36304

31 files changed

+1057
-383
lines changed

README.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<img align="right" width="64px" src="https://raw.githubusercontent.com/hauke96/GeoNotes/main/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png">
22

33
# GeoNotes
4-
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.
4+
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.
55

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

10-
## Get it
10+
## Download
1111

1212
[<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/)
1313
[<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)
@@ -26,6 +26,28 @@ See the [OSM Wiki page](https://wiki.openstreetmap.org/wiki/GeoNotes) for detail
2626
* Export all notes in GeoJson format
2727
* Show and follow current location
2828

29-
### Not in the scope of this app
29+
## Use-case and Philosophy
30+
31+
This is the basic use-case of this app:
32+
33+
* Take notes while being outside (maybe even while walking or sitting in a bus)
34+
35+
To enable this, the app follows some basic principles:
36+
37+
* **Simplicity:** Make creating, editing, moving and deleting of notes as fast/easy as possible
38+
* **No upload** of data and no creation of notes on osm.org
39+
* **General purpose:** No restriction in the content of a note
40+
* **Not a note management tool:** No import, no high level management operations
41+
* **Simple and pragmatic UI:** No unnecessary animations, no overloaded UIs
42+
* **Feature toggles:** The possibility to enable/disable features
43+
44+
Features that will *not* make it into GeoNotes:
45+
46+
* Offline maps: Too much work (where does the data come from? What format? When to update the data? Vector or raster data/tiles? etc.)
47+
* Creating notes on osm.org
48+
* Directly editing data
49+
* All sorts of features that will only be used by ~5% (meaning a very small amount) of the users
50+
* iOS support
51+
52+
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.
3053

31-
* Add notes on osm.org (use other apps like [StreetComplete](https://github.com/streetcomplete/StreetComplete) for that)

app/build.gradle

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ android {
1010
applicationId "de.hauke_stieler.geonotes"
1111
minSdkVersion 16
1212
targetSdkVersion 30
13-
versionCode 1004002
14-
versionName "1.4.2"
13+
versionCode 1004003
14+
versionName "1.4.3"
1515

1616
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1717
}
@@ -37,14 +37,16 @@ android {
3737
dependencies {
3838
implementation 'org.osmdroid:osmdroid-android:6.1.8'
3939

40-
implementation 'androidx.appcompat:appcompat:1.3.0'
41-
implementation 'com.google.android.material:material:1.3.0'
40+
implementation 'androidx.appcompat:appcompat:1.3.1'
41+
implementation 'com.google.android.material:material:1.4.0'
4242
implementation 'androidx.preference:preference:1.1.1'
4343
implementation 'org.apache.commons:commons-text:1.9'
44+
implementation 'com.google.code.gson:gson:2.8.8'
45+
implementation 'me.himanshusoni.gpxparser:gpx-parser:1.13'
4446

4547
testImplementation 'junit:junit:4.13.2'
4648
testImplementation 'org.mockito:mockito-inline:3.8.0'
4749

48-
testImplementation 'androidx.test.ext:junit:1.1.2'
50+
testImplementation 'androidx.test.ext:junit:1.1.3'
4951
testImplementation 'org.robolectric:robolectric:4.5.1'
5052
}

app/src/main/java/de/hauke_stieler/geonotes/Injector.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
import android.content.Context;
55
import android.content.SharedPreferences;
66

7-
import androidx.appcompat.app.AppCompatActivity;
8-
import androidx.core.app.ComponentActivity;
9-
107
import org.osmdroid.views.MapView;
118

129
import java.util.HashMap;
@@ -36,6 +33,7 @@ public class Injector {
3633
classBuilders.put(Database.class, () -> buildDatabase());
3734
classBuilders.put(Exporter.class, () -> buildExporter());
3835
classBuilders.put(SharedPreferences.class, () -> buildSharedPreferences());
36+
classBuilders.put(MapView.class, () -> buildMapView());
3937
classBuilders.put(de.hauke_stieler.geonotes.map.Map.class, () -> buildMap());
4038
}
4139

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

59+
public static void put(Object instance) {
60+
classes.put(instance.getClass(), instance);
61+
}
62+
6163
private static Database buildDatabase() {
6264
return new Database(context);
6365
}
@@ -70,8 +72,12 @@ private static SharedPreferences buildSharedPreferences() {
7072
return context.getSharedPreferences(context.getString(R.string.pref_file), MODE_PRIVATE);
7173
}
7274

75+
private static MapView buildMapView() {
76+
return activity.findViewById(R.id.map);
77+
}
78+
7379
private static de.hauke_stieler.geonotes.map.Map buildMap() {
74-
MapView mapView = activity.findViewById(R.id.map);
80+
MapView mapView = get(MapView.class);
7581
return new de.hauke_stieler.geonotes.map.Map(context, mapView, get(Database.class), get(SharedPreferences.class));
7682
}
7783
}

app/src/main/java/de/hauke_stieler/geonotes/MainActivity.java

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import android.Manifest;
44
import android.annotation.SuppressLint;
5-
import android.content.ContentValues;
6-
import android.content.Context;
75
import android.content.Intent;
86
import android.content.SharedPreferences;
97
import android.content.pm.PackageManager;
@@ -22,6 +20,7 @@
2220
import androidx.annotation.Nullable;
2321
import androidx.appcompat.app.AppCompatActivity;
2422
import androidx.appcompat.view.menu.ActionMenuItemView;
23+
import androidx.appcompat.widget.PopupMenu;
2524
import androidx.appcompat.widget.Toolbar;
2625
import androidx.core.app.ActivityCompat;
2726
import androidx.core.content.ContextCompat;
@@ -42,7 +41,7 @@
4241
import de.hauke_stieler.geonotes.database.Database;
4342
import de.hauke_stieler.geonotes.export.Exporter;
4443
import de.hauke_stieler.geonotes.map.Map;
45-
import de.hauke_stieler.geonotes.map.MarkerWindow;
44+
import de.hauke_stieler.geonotes.map.MarkerFragment;
4645
import de.hauke_stieler.geonotes.map.TouchDownListener;
4746
import de.hauke_stieler.geonotes.note_list.NoteListActivity;
4847
import de.hauke_stieler.geonotes.photo.ThumbnailUtil;
@@ -76,27 +75,36 @@ protected void onCreate(Bundle savedInstanceState) {
7675

7776
setContentView(R.layout.activity_main);
7877

78+
database = Injector.get(Database.class);
79+
preferences = Injector.get(SharedPreferences.class);
80+
exporter = Injector.get(Exporter.class);
81+
7982
toolbar = findViewById(R.id.toolbar);
8083
setSupportActionBar(toolbar);
8184

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

86-
final Context context = getApplicationContext();
87-
8889
requestPermissionsIfNecessary(new String[]{
8990
Manifest.permission.WRITE_EXTERNAL_STORAGE,
9091
Manifest.permission.ACCESS_FINE_LOCATION,
9192
Manifest.permission.CAMERA
9293
});
9394

94-
database = Injector.get(Database.class);
95-
preferences = Injector.get(SharedPreferences.class);
96-
exporter = Injector.get(Exporter.class);
97-
95+
createMarkerFragment();
9896
createMap();
99-
loadPreferences();
97+
}
98+
99+
private void createMarkerFragment() {
100+
MarkerFragment markerFragment = new MarkerFragment();
101+
102+
getSupportFragmentManager().beginTransaction()
103+
.setReorderingAllowed(true)
104+
.add(R.id.map_marker_fragment, markerFragment, null)
105+
.commit();
106+
107+
Injector.put(markerFragment);
100108
}
101109

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

119-
boolean enableRotatingMap1 = preferences.getBoolean(getString(R.string.pref_enable_rotating_map), false);
120-
map.updateMapRotationBehavior(enableRotatingMap1);
127+
boolean enableRotatingMap = preferences.getBoolean(getString(R.string.pref_enable_rotating_map), false);
128+
float mapRotation = preferences.getFloat(getString(R.string.pref_map_rotation), 0f);
129+
map.updateMapRotation(enableRotatingMap, mapRotation);
121130

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

138+
private void showExportPopupMenu() {
139+
PopupMenu exportPopupMenu = new PopupMenu(this, findViewById(R.id.toolbar_btn_export));
140+
141+
exportPopupMenu.getMenu().add(0, 0, 0, "GeoJson");
142+
exportPopupMenu.getMenu().add(0, 1, 1, "GPX");
143+
144+
exportPopupMenu.setOnMenuItemClickListener(menuItem -> {
145+
switch (menuItem.getItemId()) {
146+
case 0:
147+
exporter.shareAsGeoJson();
148+
break;
149+
case 1:
150+
exporter.shareAsGpx();
151+
break;
152+
}
153+
return true;
154+
});
155+
exportPopupMenu.show();
156+
}
157+
129158
@Override
130159
public boolean onCreateOptionsMenu(Menu menu) {
131160
getMenuInflater().inflate(R.menu.toolbar_menu, menu);
@@ -146,7 +175,7 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
146175
}
147176
return true;
148177
case R.id.toolbar_btn_export:
149-
exporter.export();
178+
showExportPopupMenu();
150179
return true;
151180
case R.id.toolbar_btn_settings:
152181
startActivity(new Intent(this, SettingsActivity.class));
@@ -227,7 +256,7 @@ public boolean onZoom(ZoomEvent event) {
227256

228257
@SuppressLint("RestrictedApi")
229258
TouchDownListener touchDownListener = () -> {
230-
ActionMenuItemView menuItem = (ActionMenuItemView) findViewById(R.id.toolbar_btn_gps_follow);
259+
ActionMenuItemView menuItem = findViewById(R.id.toolbar_btn_gps_follow);
231260
if (menuItem != null) {
232261
menuItem.setIcon(getResources().getDrawable(R.drawable.ic_location_searching));
233262
}
@@ -240,7 +269,7 @@ public boolean onZoom(ZoomEvent event) {
240269
* Adds a listener for the camera button. The camera action can only be performed from within an activity.
241270
*/
242271
private void addCameraListener() {
243-
MarkerWindow.RequestPhotoEventHandler requestPhotoEventHandler = (Long noteId) -> {
272+
MarkerFragment.RequestPhotoEventHandler requestPhotoEventHandler = (Long noteId) -> {
244273
if (!hasPermission(Manifest.permission.CAMERA) || !hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
245274
// We don't have camera and/or storage permissions -> ask for them
246275
ActivityCompat.requestPermissions(
@@ -280,7 +309,6 @@ protected void onActivityResult(int requestCode, int resultCode, @Nullable Inten
280309
switch (requestCode) {
281310
case REQUEST_IMAGE_CAPTURE:
282311
addPhotoToDatabase(lastPhotoNoteId, lastPhotoFile);
283-
addPhotoToGallery(lastPhotoFile);
284312
map.addImagesToMarkerWindow();
285313
break;
286314
case REQUEST_NOTE_LIST_REQUEST_CODE:
@@ -318,18 +346,6 @@ private void addPhotoToDatabase(Long noteId, File photoFile) {
318346
}
319347
}
320348

321-
private void addPhotoToGallery(File photoFile) {
322-
ContentValues values = new ContentValues();
323-
values.put(MediaStore.Images.Media.TITLE, photoFile.getName());
324-
values.put(MediaStore.Images.Media.DISPLAY_NAME, photoFile.getName());
325-
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpg");
326-
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis());
327-
values.put(MediaStore.Images.Media.DATE_TAKEN, photoFile.lastModified());
328-
values.put(MediaStore.Images.Media.DATA, photoFile.toString());
329-
330-
getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
331-
}
332-
333349
/**
334350
* Stores the current map location and zoom in the shared preferences.
335351
*/

app/src/main/java/de/hauke_stieler/geonotes/database/Database.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public void removePhotos(long noteId, File storageDir) {
8181

8282
photoStore.removePhotos(getWritableDatabase(), noteId);
8383

84-
for(String photo:photos){
84+
for (String photo : photos) {
8585
File photoFile = new File(storageDir, photo);
8686
File thumbnailFile = ThumbnailUtil.getThumbnailFile(photoFile);
8787

app/src/main/java/de/hauke_stieler/geonotes/export/Exporter.java

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,85 @@
44
import android.content.Intent;
55
import android.util.Log;
66

7+
import java.io.ByteArrayOutputStream;
78
import java.io.File;
89
import java.io.FileOutputStream;
9-
import java.io.IOException;
1010
import java.io.OutputStreamWriter;
11+
import java.text.SimpleDateFormat;
12+
import java.util.Date;
13+
import java.util.List;
1114

1215
import de.hauke_stieler.geonotes.common.FileHelper;
1316
import de.hauke_stieler.geonotes.database.Database;
17+
import de.hauke_stieler.geonotes.notes.Note;
18+
import me.himanshusoni.gpxparser.GPXWriter;
19+
import me.himanshusoni.gpxparser.modal.GPX;
20+
import me.himanshusoni.gpxparser.modal.Waypoint;
1421

1522
public class Exporter {
23+
private static final String LOGTAG = Exporter.class.getName();
24+
1625
private final Database database;
1726
private final Context context;
1827

19-
public Exporter(Database database, Context context){
28+
public Exporter(Database database, Context context) {
2029
this.database = database;
2130
this.context = context;
2231
}
2332

24-
public void export() {
33+
public void shareAsGeoJson() {
2534
String geoJson = GeoJson.toGeoJson(database.getAllNotes());
35+
String fileExtension = ".geojson";
36+
String mimeType = "application/geo+json";
37+
38+
openShareIntent(geoJson, fileExtension, mimeType);
39+
}
40+
41+
public void shareAsGpx() {
42+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
43+
List<Note> notes = database.getAllNotes();
44+
GPX gpx = new GPX();
45+
46+
try {
47+
for (Note note : notes) {
48+
Waypoint waypoint = new Waypoint(note.getLat(), note.getLon());
49+
waypoint.setName(note.getId() + "");
50+
waypoint.setDescription(note.getDescription());
51+
gpx.addWaypoint(waypoint);
52+
}
2653

54+
GPXWriter writer = new GPXWriter();
55+
writer.writeGPX(gpx, outputStream);
56+
} catch (Exception e) {
57+
Log.e(LOGTAG, "GPX creation failed: " + e.toString());
58+
}
59+
60+
String fileExtension = ".gpx";
61+
String mimeType = "application/gpx+xml";
62+
63+
openShareIntent(new String(outputStream.toByteArray()), fileExtension, mimeType);
64+
}
65+
66+
private void openShareIntent(String data, String fileExtension, String mimeType) {
2767
try {
68+
String timeStamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date());
69+
2870
File storageDir = context.getExternalFilesDir("GeoNotes");
29-
File exportFile = new File(storageDir, "geonotes-export.geojson");
71+
File exportFile = new File(storageDir, "geonotes-export_" + timeStamp + fileExtension);
3072
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(exportFile));
31-
outputStreamWriter.write(geoJson);
73+
outputStreamWriter.write(data);
3274
outputStreamWriter.close();
3375

3476
Intent sendIntent = new Intent();
3577
sendIntent.setAction(Intent.ACTION_SEND);
3678
sendIntent.putExtra(Intent.EXTRA_STREAM, FileHelper.getFileUri(context, exportFile));
37-
sendIntent.setType("application/json");
79+
sendIntent.setType(mimeType);
3880

3981
Intent shareIntent = Intent.createChooser(sendIntent, null);
4082
shareIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // needed because we're outside of an activity
4183
context.startActivity(shareIntent);
42-
} catch (IOException e) {
43-
Log.e("Export", "File write failed: " + e.toString());
84+
} catch (Exception e) {
85+
Log.e(LOGTAG, "File write failed: " + e.toString());
4486
}
4587
}
4688
}

0 commit comments

Comments
 (0)