diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ApiCallback.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ApiCallback.java new file mode 100644 index 00000000..16b5a07e --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ApiCallback.java @@ -0,0 +1,6 @@ +package com.openpositioning.PositionMe.data.remote; + +public interface ApiCallback { + void onSuccess(T result); + void onError(Exception e); +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/OkHttpServerApi.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/OkHttpServerApi.java new file mode 100644 index 00000000..7d433653 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/OkHttpServerApi.java @@ -0,0 +1,103 @@ +package com.openpositioning.PositionMe.data.remote; + +import com.openpositioning.PositionMe.BuildConfig; +import okhttp3.*; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +public class OkHttpServerApi implements ServerApi { + private static final String userKey = BuildConfig.OPENPOSITIONING_API_KEY; + private static final String masterKey = BuildConfig.OPENPOSITIONING_MASTER_KEY; + private static final String uploadURL = + "https://openpositioning.org/api/live/trajectory/upload/" + userKey + "/?key=" + masterKey; + private static final String downloadURL = + "https://openpositioning.org/api/live/trajectory/download/" + userKey + "?skip=0&limit=30&key=" + masterKey; + private static final String infoRequestURL = + "https://openpositioning.org/api/live/users/trajectories/" + userKey + "?key=" + masterKey; + private static final String PROTOCOL_CONTENT_TYPE = "multipart/form-data"; + private static final String PROTOCOL_ACCEPT_TYPE = "application/json"; + + private final OkHttpClient client = new OkHttpClient(); + + @Override + public void uploadFile(File file, ApiCallback callback) { + RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) + .addFormDataPart("file", file.getName(), + RequestBody.create(MediaType.parse("text/plain"), file)) + .build(); + Request request = new Request.Builder().url(uploadURL).post(requestBody) + .addHeader("accept", PROTOCOL_ACCEPT_TYPE) + .addHeader("Content-Type", PROTOCOL_CONTENT_TYPE).build(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + callback.onError(e); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + try (ResponseBody body = response.body()) { + if (!response.isSuccessful()) { + String err = body != null ? body.string() : ""; + callback.onError(new IOException(err)); + return; + } + callback.onSuccess(body != null ? body.string() : ""); + } + } + }); + } + + @Override + public void downloadZip(ApiCallback callback) { + Request request = new Request.Builder().url(downloadURL) + .addHeader("accept", PROTOCOL_ACCEPT_TYPE) + .get().build(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + callback.onError(e); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (!response.isSuccessful()) { + callback.onError(new IOException("Unexpected code " + response)); + return; + } + ResponseBody body = response.body(); + if (body != null) { + callback.onSuccess(body.byteStream()); + } else { + callback.onError(new IOException("Empty body")); + } + } + }); + } + + @Override + public void fetchInfo(ApiCallback callback) { + Request request = new Request.Builder().url(infoRequestURL) + .addHeader("accept", PROTOCOL_ACCEPT_TYPE) + .get().build(); + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + callback.onError(e); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + try (ResponseBody body = response.body()) { + if (!response.isSuccessful()) { + callback.onError(new IOException("Unexpected code " + response)); + return; + } + callback.onSuccess(body != null ? body.string() : ""); + } + } + }); + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerApi.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerApi.java new file mode 100644 index 00000000..aebe60da --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerApi.java @@ -0,0 +1,10 @@ +package com.openpositioning.PositionMe.data.remote; + +import java.io.File; +import java.io.InputStream; + +public interface ServerApi { + void uploadFile(File file, ApiCallback callback); + void downloadZip(ApiCallback callback); + void fetchInfo(ApiCallback callback); +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java index 7f7e74b2..c6c944e3 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java +++ b/app/src/main/java/com/openpositioning/PositionMe/data/remote/ServerCommunications.java @@ -7,7 +7,6 @@ import java.io.FileReader; import org.json.JSONObject; -import android.os.Environment; import java.io.FileInputStream; import java.io.OutputStream; @@ -22,16 +21,17 @@ import android.os.Looper; import android.widget.Toast; -import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import com.google.protobuf.util.JsonFormat; -import com.openpositioning.PositionMe.BuildConfig; import com.openpositioning.PositionMe.Traj; import com.openpositioning.PositionMe.presentation.fragment.FilesFragment; import com.openpositioning.PositionMe.presentation.activity.MainActivity; import com.openpositioning.PositionMe.sensors.Observable; import com.openpositioning.PositionMe.sensors.Observer; +import com.openpositioning.PositionMe.data.remote.ServerApi; +import com.openpositioning.PositionMe.data.remote.ApiCallback; +import com.openpositioning.PositionMe.data.remote.OkHttpServerApi; import java.io.ByteArrayOutputStream; import java.io.File; @@ -39,7 +39,6 @@ import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -47,21 +46,10 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.OkHttp; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; /** - * This class handles communications with the server through HTTPs. The class uses an - * {@link OkHttpClient} for making requests to the server. The class includes methods for sending + * This class handles communications with the server through HTTPs using a pluggable + * {@link ServerApi} implementation. The class includes methods for sending * a recorded trajectory, uploading locally-stored trajectories, downloading trajectories from the * server and requesting information about the uploaded trajectories. * @@ -85,20 +73,8 @@ public class ServerCommunications implements Observable { private boolean success; private List observers; - // Static constants necessary for communications - private static final String userKey = BuildConfig.OPENPOSITIONING_API_KEY; - private static final String masterKey = BuildConfig.OPENPOSITIONING_MASTER_KEY; - private static final String uploadURL = - "https://openpositioning.org/api/live/trajectory/upload/" + userKey - + "/?key=" + masterKey; - private static final String downloadURL = - "https://openpositioning.org/api/live/trajectory/download/" + userKey - + "?skip=0&limit=30&key=" + masterKey; - private static final String infoRequestURL = - "https://openpositioning.org/api/live/users/trajectories/" + userKey - + "?key=" + masterKey; - private static final String PROTOCOL_CONTENT_TYPE = "multipart/form-data"; - private static final String PROTOCOL_ACCEPT_TYPE = "application/json"; + // API client handling server requests + private final ServerApi serverApi; @@ -116,7 +92,7 @@ public ServerCommunications(Context context) { this.isWifiConn = false; this.isMobileConn = false; checkNetworkStatus(); - + this.serverApi = new OkHttpServerApi(); this.observers = new ArrayList<>(); } @@ -170,92 +146,39 @@ public void sendTrajectory(Traj.Trajectory trajectory){ boolean enableMobileData = this.settings.getBoolean("mobile_sync", false); // Check if device is connected to WiFi or to mobile data with enabled preference if(this.isWifiConn || (enableMobileData && isMobileConn)) { - // Instantiate client for HTTP requests - OkHttpClient client = new OkHttpClient(); + serverApi.uploadFile(file, new ApiCallback() { + @Override + public void onSuccess(String result) { + System.out.println("Successful post response: " + result); - // Creaet a equest body with a file to upload in multipart/form-data format - RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) - .addFormDataPart("file", file.getName(), - RequestBody.create(MediaType.parse("text/plain"), file)) - .build(); + System.out.println("Get file: " + file.getName()); + String originalPath = file.getAbsolutePath(); + System.out.println("Original trajectory file saved at: " + originalPath); - // Create a POST request with the required headers - Request request = new Request.Builder().url(uploadURL).post(requestBody) - .addHeader("accept", PROTOCOL_ACCEPT_TYPE) - .addHeader("Content-Type", PROTOCOL_CONTENT_TYPE).build(); + File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File downloadFile = new File(downloadsDir, file.getName()); + try { + copyFile(file, downloadFile); + System.out.println("Trajectory file copied to Downloads: " + downloadFile.getAbsolutePath()); + } catch (IOException e) { + e.printStackTrace(); + System.err.println("Failed to copy file to Downloads: " + e.getMessage()); + } - // Enqueue the request to be executed asynchronously and handle the response - client.newCall(request).enqueue(new Callback() { + success = file.delete(); + notifyObservers(1); + } - // Handle failure to get response from the server - @Override public void onFailure(Call call, IOException e) { + @Override + public void onError(Exception e) { e.printStackTrace(); System.err.println("Failure to get response"); - // Delete the local file and set success to false - //file.delete(); success = false; + infoResponse = "Upload failed: " + e.getMessage(); + new Handler(Looper.getMainLooper()).post(() -> + Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); notifyObservers(1); } - - private void copyFile(File src, File dst) throws IOException { - try (InputStream in = new FileInputStream(src); - OutputStream out = new FileOutputStream(dst)) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } - } - - // Process the server's response - @Override public void onResponse(Call call, Response response) throws IOException { - try (ResponseBody responseBody = response.body()) { - // If the response is unsuccessful, delete the local file and throw an - // exception - if (!response.isSuccessful()) { - //file.delete(); -// System.err.println("POST error response: " + responseBody.string()); - - String errorBody = responseBody.string(); - infoResponse = "Upload failed: " + errorBody; - new Handler(Looper.getMainLooper()).post(() -> - Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); // show error message to users - - System.err.println("POST error response: " + errorBody); - success = false; - notifyObservers(1); - throw new IOException("Unexpected code " + response); - } - - // Print the response headers - Headers responseHeaders = response.headers(); - for (int i = 0, size = responseHeaders.size(); i < size; i++) { - System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); - } - // Print a confirmation of a successful POST to API - System.out.println("Successful post response: " + responseBody.string()); - - System.out.println("Get file: " + file.getName()); - String originalPath = file.getAbsolutePath(); - System.out.println("Original trajectory file saved at: " + originalPath); - - // Copy the file to the Downloads folder - File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - File downloadFile = new File(downloadsDir, file.getName()); - try { - copyFile(file, downloadFile); - System.out.println("Trajectory file copied to Downloads: " + downloadFile.getAbsolutePath()); - } catch (IOException e) { - e.printStackTrace(); - System.err.println("Failed to copy file to Downloads: " + e.getMessage()); - } - - // Delete local file and set success to true - success = file.delete(); - notifyObservers(1); - } - } }); } else { @@ -268,88 +191,29 @@ private void copyFile(File src, File dst) throws IOException { } /** - * Uploads a local trajectory file to the API server in the specified format. - * {@link OkHttp} library is used for the asynchronous POST request. + * Uploads a local trajectory file to the API server in the specified format + * using the configured {@link ServerApi}. * * @param localTrajectory the File object of the local trajectory to be uploaded */ public void uploadLocalTrajectory(File localTrajectory) { - - // Instantiate client for HTTP requests - OkHttpClient client = new OkHttpClient(); - - // robustness improvement - RequestBody fileRequestBody; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - try { - byte[] fileBytes = Files.readAllBytes(localTrajectory.toPath()); - fileRequestBody = RequestBody.create(MediaType.parse("text/plain"), fileBytes); - } catch (IOException e) { - e.printStackTrace(); - // if failed, use File object to construct RequestBody - fileRequestBody = RequestBody.create(MediaType.parse("text/plain"), localTrajectory); + serverApi.uploadFile(localTrajectory, new ApiCallback() { + @Override + public void onSuccess(String result) { + System.out.println("UPLOAD SUCCESSFUL: " + result); + success = localTrajectory.delete(); + notifyObservers(1); } - } else { - fileRequestBody = RequestBody.create(MediaType.parse("text/plain"), localTrajectory); - } - - // Create request body with a file to upload in multipart/form-data format - RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) - .addFormDataPart("file", localTrajectory.getName(), fileRequestBody) - .build(); - // Create a POST request with the required headers - okhttp3.Request request = new okhttp3.Request.Builder().url(uploadURL).post(requestBody) - .addHeader("accept", PROTOCOL_ACCEPT_TYPE) - .addHeader("Content-Type", PROTOCOL_CONTENT_TYPE).build(); - - // Enqueue the request to be executed asynchronously and handle the response - client.newCall(request).enqueue(new okhttp3.Callback() { @Override - public void onFailure(Call call, IOException e) { - // Print error message, set success to false and notify observers + public void onError(Exception e) { e.printStackTrace(); -// localTrajectory.delete(); success = false; System.err.println("UPLOAD: Failure to get response"); - notifyObservers(1); - infoResponse = "Upload failed: " + e.getMessage(); // Store error message + infoResponse = "Upload failed: " + e.getMessage(); new Handler(Looper.getMainLooper()).post(() -> - Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); // show error message to users - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - try (ResponseBody responseBody = response.body()) { - if (!response.isSuccessful()) { - // Print error message, set success to false and throw an exception - success = false; -// System.err.println("UPLOAD unsuccessful: " + responseBody.string()); - notifyObservers(1); -// localTrajectory.delete(); - assert responseBody != null; - String errorBody = responseBody.string(); - System.err.println("UPLOAD unsuccessful: " + errorBody); - infoResponse = "Upload failed: " + errorBody; - new Handler(Looper.getMainLooper()).post(() -> - Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); - throw new IOException("UPLOAD failed with code " + response); - } - - // Print the response headers - Headers responseHeaders = response.headers(); - for (int i = 0, size = responseHeaders.size(); i < size; i++) { - System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); - } - - // Print a confirmation of a successful POST to API - assert responseBody != null; - System.out.println("UPLOAD SUCCESSFUL: " + responseBody.string()); - - // Delete local file, set success to true and notify observers - success = localTrajectory.delete(); - notifyObservers(1); - } + Toast.makeText(context, infoResponse, Toast.LENGTH_SHORT).show()); + notifyObservers(1); } }); } @@ -474,30 +338,10 @@ private void saveDownloadRecord(long startTimestamp, String fileName, String id, public void downloadTrajectory(int position, String id, String dateSubmitted) { loadDownloadRecords(); // Load existing records from app-specific directory - // Initialise OkHttp client - OkHttpClient client = new OkHttpClient(); - - // Create GET request with required header - okhttp3.Request request = new okhttp3.Request.Builder() - .url(downloadURL) - .addHeader("accept", PROTOCOL_ACCEPT_TYPE) - .get() - .build(); - - // Enqueue the GET request for asynchronous execution - client.newCall(request).enqueue(new okhttp3.Callback() { - @Override - public void onFailure(Call call, IOException e) { - e.printStackTrace(); - } - + serverApi.downloadZip(new ApiCallback() { @Override - public void onResponse(Call call, Response response) throws IOException { - try (ResponseBody responseBody = response.body()) { - if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); - - // Extract the nth entry from the zip - InputStream inputStream = responseBody.byteStream(); + public void onSuccess(InputStream inputStream) { + try { ZipInputStream zipInputStream = new ZipInputStream(inputStream); java.util.zip.ZipEntry zipEntry; @@ -557,8 +401,15 @@ public void onResponse(Call call, Response response) throws IOException { // Save the download record saveDownloadRecord(startTimestamp, fileName, id, dateSubmitted); loadDownloadRecords(); + } catch (Exception e) { + e.printStackTrace(); } } + + @Override + public void onError(Exception e) { + e.printStackTrace(); + } }); } @@ -569,35 +420,17 @@ public void onResponse(Call call, Response response) throws IOException { * */ public void sendInfoRequest() { - // Create a new OkHttpclient - OkHttpClient client = new OkHttpClient(); - - // Create GET info request with appropriate URL and header - okhttp3.Request request = new okhttp3.Request.Builder() - .url(infoRequestURL) - .addHeader("accept", PROTOCOL_ACCEPT_TYPE) - .get() - .build(); - - // Enqueue the GET request for asynchronous execution - client.newCall(request).enqueue(new okhttp3.Callback() { - @Override public void onFailure(Call call, IOException e) { - e.printStackTrace(); + serverApi.fetchInfo(new ApiCallback() { + @Override + public void onSuccess(String result) { + infoResponse = result; + System.out.println("Response received"); + notifyObservers(0); } - @Override public void onResponse(Call call, Response response) throws IOException { - try (ResponseBody responseBody = response.body()) { - // Check if the response is successful - if (!response.isSuccessful()) throw new IOException("Unexpected code " + - response); - - // Get the requested information from the response body and save it in a string - // TODO: add printing to the screen somewhere - infoResponse = responseBody.string(); - // Print a message in the console and notify observers - System.out.println("Response received"); - notifyObservers(0); - } + @Override + public void onError(Exception e) { + e.printStackTrace(); } }); } @@ -620,6 +453,17 @@ private void checkNetworkStatus() { } } + private void copyFile(File src, File dst) throws IOException { + try (InputStream in = new FileInputStream(src); + OutputStream out = new FileOutputStream(dst)) { + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } + } + private void logDataSize(Traj.Trajectory trajectory) { Log.i("ServerCommunications", "IMU Data size: " + trajectory.getImuDataCount()); diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/MarkerFactory.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/MarkerFactory.java new file mode 100644 index 00000000..bce42e7c --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/MarkerFactory.java @@ -0,0 +1,69 @@ +package com.openpositioning.PositionMe.presentation.trajmap; + +import android.content.Context; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.BitmapDescriptorFactory; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import com.openpositioning.PositionMe.R; +import com.openpositioning.PositionMe.utils.UtilFunctions; + +import org.json.JSONObject; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Factory class responsible for creating map markers using icon information + * loaded from a JSON configuration file. + */ +public class MarkerFactory { + private static Map iconMap; + + private static void ensureIconsLoaded(Context context) { + if (iconMap != null) return; + iconMap = new HashMap<>(); + try { + InputStream is = context.getResources().openRawResource(R.raw.marker_icons); + byte[] buffer = new byte[is.available()]; + int read = is.read(buffer); + is.close(); + if (read > 0) { + JSONObject obj = new JSONObject(new String(buffer)); + Iterator keys = obj.keys(); + while (keys.hasNext()) { + String key = keys.next(); + String drawableName = obj.getString(key); + int resId = context.getResources().getIdentifier(drawableName, "drawable", context.getPackageName()); + if (resId != 0) { + iconMap.put(key, resId); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static Marker createMarker(GoogleMap map, + LatLng location, + String title, + String iconKey, + Context context) { + ensureIconsLoaded(context); + MarkerOptions options = new MarkerOptions() + .position(location) + .flat(true) + .title(title); + Integer resId = iconMap.get(iconKey); + if (resId != null) { + options.icon(BitmapDescriptorFactory.fromBitmap( + UtilFunctions.getBitmapFromVector(context, resId))); + } + return map.addMarker(options); + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/POILocationProvider.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/POILocationProvider.java new file mode 100644 index 00000000..c8072b15 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/POILocationProvider.java @@ -0,0 +1,79 @@ +package com.openpositioning.PositionMe.presentation.trajmap; + +import android.content.Context; + +import com.google.android.gms.maps.model.LatLng; +import com.openpositioning.PositionMe.R; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Utility class for loading point-of-interest locations from a JSON configuration file. + */ +public class POILocationProvider { + private static Map>>> locationMap; + + private static void ensureLoaded(Context context) { + if (locationMap != null) return; + locationMap = new HashMap<>(); + try { + InputStream is = context.getResources().openRawResource(R.raw.poi_locations); + byte[] buffer = new byte[is.available()]; + int read = is.read(buffer); + is.close(); + if (read > 0) { + JSONObject root = new JSONObject(new String(buffer)); + Iterator types = root.keys(); + while (types.hasNext()) { + String type = types.next(); + JSONObject buildingsObj = root.getJSONObject(type); + Map>> buildingMap = new HashMap<>(); + Iterator buildingKeys = buildingsObj.keys(); + while (buildingKeys.hasNext()) { + String building = buildingKeys.next(); + JSONObject floorsObj = buildingsObj.getJSONObject(building); + Map> floorMap = new HashMap<>(); + Iterator floorKeys = floorsObj.keys(); + while (floorKeys.hasNext()) { + String floorKey = floorKeys.next(); + int floor = Integer.parseInt(floorKey); + JSONArray arr = floorsObj.getJSONArray(floorKey); + List list = new ArrayList<>(); + for (int i = 0; i < arr.length(); i++) { + JSONObject obj = arr.getJSONObject(i); + double lat = obj.getDouble("lat"); + double lng = obj.getDouble("lng"); + list.add(new LatLng(lat, lng)); + } + floorMap.put(floor, list); + } + buildingMap.put(building, floorMap); + } + locationMap.put(type, buildingMap); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Retrieves the list of locations for the given marker type, building and floor. + */ + public static List getLocations(String type, String building, int floor, Context context) { + ensureLoaded(context); + Map>> buildings = locationMap.get(type); + if (buildings == null) return null; + Map> floors = buildings.get(building); + if (floors == null) return null; + return floors.get(floor); + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/TrajectoryMapMaker.java b/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/TrajectoryMapMaker.java index aa9d18e1..772048b0 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/TrajectoryMapMaker.java +++ b/app/src/main/java/com/openpositioning/PositionMe/presentation/trajmap/TrajectoryMapMaker.java @@ -3,16 +3,12 @@ import android.content.Context; import com.google.android.gms.maps.GoogleMap; -import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.Marker; -import com.google.android.gms.maps.model.MarkerOptions; -import com.openpositioning.PositionMe.R; -import com.openpositioning.PositionMe.utils.UtilFunctions; +import com.openpositioning.PositionMe.presentation.trajmap.MarkerFactory; +import com.openpositioning.PositionMe.presentation.trajmap.POILocationProvider; -import java.util.Arrays; import java.util.List; -import java.util.Objects; /** * Utility class for managing and updating markers on a Google Map, representing various points of interest within a building, @@ -48,39 +44,19 @@ public static void updateMedicalRoomMarkers( } medicalRoomMarkers.clear(); - // Determine the placement of MedicalRoom based on floor and building name. - List medicalRoomLocations = null; - if (currentFloor == 0 && Objects.equals(currentBuilding, "nucleus")) { - medicalRoomLocations = Arrays.asList( - ); - } - else if (currentFloor == 1 && Objects.equals(currentBuilding, "nucleus")) { - medicalRoomLocations = Arrays.asList( - ); - } - else if (currentFloor == 2 && Objects.equals(currentBuilding, "nucleus")) { - medicalRoomLocations = Arrays.asList( - new LatLng(55.923291498633766, -3.1743306666612625) - ); - } - else if (currentFloor == 3 && Objects.equals(currentBuilding, "nucleus")) { - medicalRoomLocations = Arrays.asList( - ); - } - else if (currentFloor == 4 && Objects.equals(currentBuilding, "nucleus")) { - medicalRoomLocations = Arrays.asList( - ); - } + // Retrieve locations from configuration + List medicalRoomLocations = POILocationProvider.getLocations( + "medicalRoom", currentBuilding, currentFloor, context); // If not null, add Markers sequentially. if (medicalRoomLocations != null) { for (LatLng location : medicalRoomLocations) { - Marker marker = gMap.addMarker(new MarkerOptions() - .position(location) - .flat(true) - .title("Medical Room") - .icon(BitmapDescriptorFactory.fromBitmap( - UtilFunctions.getBitmapFromVector(context, R.drawable.iso_first_aid_icon))) + Marker marker = MarkerFactory.createMarker( + gMap, + location, + "Medical Room", + "medicalRoom", + context ); if (marker != null) { medicalRoomMarkers.add(marker); @@ -112,50 +88,18 @@ public static void updateEmergencyExitMarkers( } emergencyExitMarkers.clear(); - List exitLocations = null; - - if (currentFloor == 0 && Objects.equals(currentBuilding, "nucleus")) { - exitLocations = Arrays.asList( - new LatLng(55.92327684586336, -3.174331672489643), - new LatLng(55.92305836864246, -3.174259588122368), - new LatLng(55.923040334354255, -3.174433596432209) - ); - } - else if (currentFloor == 1 && Objects.equals(currentBuilding, "nucleus")) { - exitLocations = Arrays.asList( - new LatLng(55.922839326615474, -3.17451573908329), - new LatLng(55.92283913875728, -3.1742961332201958), - new LatLng(55.92330295784777, -3.174157999455929), - new LatLng(55.92287501965453, -3.174012154340744), - new LatLng(55.92301422219288, -3.173900842666626), - new LatLng(55.92308936505573, -3.1738971546292305), - new LatLng(55.923300515720484, -3.1740912795066833) - ); - } - else if (currentFloor == 2 && Objects.equals(currentBuilding, "nucleus")) { - exitLocations = Arrays.asList( - new LatLng(55.92303563792365, -3.174491263926029), - new LatLng(55.92327647015123, -3.174472488462925) - ); - } - else if (currentFloor == 3 && Objects.equals(currentBuilding, "nucleus")) { - exitLocations = Arrays.asList( - ); - } - else if (currentFloor == 4 && Objects.equals(currentBuilding, "nucleus")) { - exitLocations = Arrays.asList( - ); - } + List exitLocations = POILocationProvider.getLocations( + "emergencyExit", currentBuilding, currentFloor, context); if (exitLocations != null) { for (LatLng location : exitLocations) { - Marker marker = gMap.addMarker(new MarkerOptions() - .position(location) - .flat(true) - .title("Emergency Exit") - .icon(BitmapDescriptorFactory.fromBitmap( - UtilFunctions.getBitmapFromVector(context, R.drawable.iso_emergency_exit))) + Marker marker = MarkerFactory.createMarker( + gMap, + location, + "Emergency Exit", + "emergencyExit", + context ); if (marker != null) { emergencyExitMarkers.add(marker); @@ -187,51 +131,17 @@ public static void updateLiftMarkers( } liftMarkers.clear(); - List liftLocations = null; - if (currentFloor == 0 && Objects.equals(currentBuilding, "nucleus")) { - liftLocations = Arrays.asList( - new LatLng(55.92303883149652, -3.1743890047073364), - new LatLng(55.923040334354255, -3.1743494421243668), - new LatLng(55.92304277649792, -3.1743118911981583) - ); - } - else if (currentFloor == 1 && Objects.equals(currentBuilding, "nucleus")) { - liftLocations = Arrays.asList( - new LatLng(55.923027560061676, -3.1743665412068367), - new LatLng(55.92303075363521, -3.17432664334774), - new LatLng(55.923030565777935, -3.174288421869278) - ); - } - else if (currentFloor == 2 && Objects.equals(currentBuilding, "nucleus")) { - liftLocations = Arrays.asList( - new LatLng(55.92304484292707, -3.17434910684824), - new LatLng(55.923045970070206, -3.1743139028549194), - new LatLng(55.92304390364111, -3.1743836402893066) - ); - } - else if (currentFloor == 3 && Objects.equals(currentBuilding, "nucleus")) { - liftLocations = Arrays.asList( - new LatLng(55.92304484292707, -3.17434910684824), - new LatLng(55.923045970070206, -3.1743139028549194), - new LatLng(55.92304390364111, -3.1743836402893066) - ); - } - else if (currentFloor == 4 && Objects.equals(currentBuilding, "nucleus")) { - liftLocations = Arrays.asList( - new LatLng(55.92304484292707, -3.17434910684824), - new LatLng(55.923045970070206, -3.1743139028549194), - new LatLng(55.92304390364111, -3.1743836402893066) - ); - } + List liftLocations = POILocationProvider.getLocations( + "lift", currentBuilding, currentFloor, context); if (liftLocations != null) { for (LatLng location : liftLocations) { - Marker marker = gMap.addMarker(new MarkerOptions() - .position(location) - .flat(true) - .title("Lift") - .icon(BitmapDescriptorFactory.fromBitmap( - UtilFunctions.getBitmapFromVector(context, R.drawable.iso_lift))) + Marker marker = MarkerFactory.createMarker( + gMap, + location, + "Lift", + "lift", + context ); if (marker != null) { liftMarkers.add(marker); @@ -266,39 +176,17 @@ public static void updateAccessibleToiletMarkers( } accessibleToiletMarkers.clear(); - List toiletLocations = null; - - if (currentFloor == 0 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - ); - } - else if (currentFloor == 1 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - new LatLng(55.923273276597925, -3.1740865856409073), - new LatLng(55.922906391930155, -3.174562007188797) - ); - } - else if (currentFloor == 2 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - ); - } - else if (currentFloor == 3 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - ); - } - else if (currentFloor == 4 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - ); - } + List toiletLocations = POILocationProvider.getLocations( + "accessibleToilet", currentBuilding, currentFloor, context); if (toiletLocations != null) { for (LatLng location : toiletLocations) { - Marker marker = gMap.addMarker(new MarkerOptions() - .position(location) - .flat(true) - .title("Accessible Toilet") - .icon(BitmapDescriptorFactory.fromBitmap( - UtilFunctions.getBitmapFromVector(context, R.drawable.iso_accessible__toilet))) + Marker marker = MarkerFactory.createMarker( + gMap, + location, + "Accessible Toilet", + "accessibleToilet", + context ); if (marker != null) { accessibleToiletMarkers.add(marker); @@ -329,38 +217,17 @@ public static void updateDrinkingWaterMarkers( } drinkingWaterMarkers.clear(); - List waterLocations = null; - - if (currentFloor == 0 && Objects.equals(currentBuilding, "nucleus")) { - waterLocations = Arrays.asList( - ); - } - else if (currentFloor == 1 && Objects.equals(currentBuilding, "nucleus")) { - waterLocations = Arrays.asList( - new LatLng(55.922907331219456, -3.1744718179106712) - ); - } - else if (currentFloor == 2 && Objects.equals(currentBuilding, "nucleus")) { - waterLocations = Arrays.asList( - ); - } - else if (currentFloor == 3 && Objects.equals(currentBuilding, "nucleus")) { - waterLocations = Arrays.asList( - ); - } - else if (currentFloor == 4 && Objects.equals(currentBuilding, "nucleus")) { - waterLocations = Arrays.asList( - ); - } + List waterLocations = POILocationProvider.getLocations( + "drinkingWater", currentBuilding, currentFloor, context); if (waterLocations != null) { for (LatLng location : waterLocations) { - Marker marker = gMap.addMarker(new MarkerOptions() - .position(location) - .flat(true) - .title("Drinking Water") - .icon(BitmapDescriptorFactory.fromBitmap( - UtilFunctions.getBitmapFromVector(context, R.drawable.iso_drinking_water))) + Marker marker = MarkerFactory.createMarker( + gMap, + location, + "Drinking Water", + "drinkingWater", + context ); if (marker != null) { drinkingWaterMarkers.add(marker); @@ -392,41 +259,17 @@ public static void updateToiletMarkers( } toiletMarkers.clear(); - List toiletLocations = null; - - if (currentFloor == 0 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - new LatLng(55.92308617148703, -3.174508698284626) - ); - } - else if (currentFloor == 1 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - new LatLng(55.9229417091921, -3.174556642770767) - ); - } - else if (currentFloor == 2 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - new LatLng(55.92308617148703, -3.174508698284626) - ); - } - else if (currentFloor == 3 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - new LatLng(55.92308617148703, -3.174508698284626) - ); - } - else if (currentFloor == 4 && Objects.equals(currentBuilding, "nucleus")) { - toiletLocations = Arrays.asList( - ); - } + List toiletLocations = POILocationProvider.getLocations( + "toilet", currentBuilding, currentFloor, context); if (toiletLocations != null) { for (LatLng location : toiletLocations) { - Marker marker = gMap.addMarker(new MarkerOptions() - .position(location) - .flat(true) - .title("Toilet") - .icon(BitmapDescriptorFactory.fromBitmap( - UtilFunctions.getBitmapFromVector(context, R.drawable.iso_toilet_icon))) + Marker marker = MarkerFactory.createMarker( + gMap, + location, + "Toilet", + "toilet", + context ); if (marker != null) { toiletMarkers.add(marker); @@ -464,39 +307,17 @@ public static void updateAccessibleRouteMarkers( } accessibleRouteMarkers.clear(); - List accessibleLocations = null; - - if (currentFloor == 0 && Objects.equals(currentBuilding, "nucleus")) { - accessibleLocations = Arrays.asList( - ); - } - else if (currentFloor == 1 && Objects.equals(currentBuilding, "nucleus")) { - accessibleLocations = Arrays.asList( - new LatLng(55.92284627736777, -3.174579441547394), - new LatLng(55.92327102232486, -3.1744134798645973) - ); - } - else if (currentFloor == 2 && Objects.equals(currentBuilding, "nucleus")) { - accessibleLocations = Arrays.asList( - ); - } - else if (currentFloor == 3 && Objects.equals(currentBuilding, "nucleus")) { - accessibleLocations = Arrays.asList( - ); - } - else if (currentFloor == 4 && Objects.equals(currentBuilding, "nucleus")) { - accessibleLocations = Arrays.asList( - ); - } + List accessibleLocations = POILocationProvider.getLocations( + "accessibleRoute", currentBuilding, currentFloor, context); if (accessibleLocations != null) { for (LatLng location : accessibleLocations) { - Marker marker = gMap.addMarker(new MarkerOptions() - .position(location) - .flat(true) - .title("Accessible Route") - .icon(BitmapDescriptorFactory.fromBitmap( - UtilFunctions.getBitmapFromVector(context, R.drawable.iso_symbol_of_access))) + Marker marker = MarkerFactory.createMarker( + gMap, + location, + "Accessible Route", + "accessibleRoute", + context ); if (marker != null) { accessibleRouteMarkers.add(marker); diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java index a5282150..37fce0cd 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensor.java @@ -17,52 +17,39 @@ */ public class MovementSensor { // Application context for permissions and hardware access - protected Context context; + private final Context context; // Sensor Manager from the android hardware manager - protected SensorManager sensorManager; + private final SensorManager sensorManager; // The Sensor instance determined by the type upon initialisation - protected Sensor sensor; + private final Sensor sensor; // Information about the sensor stored in a SensorInfo object - protected SensorInfo sensorInfo; + private final SensorInfo sensorInfo; /** - * Public default constructor for the Movement Sensor class. - * - * It calls the superclass constructor with context, and then initialises local properties. - * - * @param context Application context used to check permissions and access devices. - * @param sensorType Type of the sensor to be created, using Sensor.TYPE constants. - * - * @see SensorInfo objects holding physical sensors properties. + * Private constructor used by {@link MovementSensorFactory}. */ - public MovementSensor(Context context, int sensorType) { + MovementSensor(Context context, SensorManager sensorManager, Sensor sensor, SensorInfo sensorInfo) { this.context = context; + this.sensorManager = sensorManager; + this.sensor = sensor; + this.sensorInfo = sensorInfo; + } - this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - this.sensor = sensorManager.getDefaultSensor(sensorType); + public Context getContext() { + return context; + } - if (sensor != null) { - this.sensorInfo = new SensorInfo( - sensor.getName(), - sensor.getVendor(), - sensor.getResolution(), - sensor.getPower(), - sensor.getVersion(), - sensor.getType() - ); - System.err.println(sensorInfo); - } else { - this.sensorInfo = new SensorInfo( - "Not available", - "-", - -1.0f, - 0.0f, - 0, - 0 - ); + public SensorManager getSensorManager() { + return sensorManager; + } + + public Sensor getSensor() { + return sensor; + } - } + public SensorInfo getSensorInfo() { + return sensorInfo; } } diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensorFactory.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensorFactory.java new file mode 100644 index 00000000..0b48284d --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/MovementSensorFactory.java @@ -0,0 +1,41 @@ +package com.openpositioning.PositionMe.sensors; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorManager; + +/** + * Factory class for creating {@link MovementSensor} instances. + * + * Encapsulates the logic of obtaining the Android {@link Sensor} and preparing + * {@link SensorInfo} so other classes do not need to directly interact with + * {@link SensorManager}. + */ +public class MovementSensorFactory { + + /** + * Creates a {@link MovementSensor} for the given Android sensor type. + * + * @param context application context used for hardware access + * @param sensorType one of the {@link Sensor} TYPE constants + * @return fully constructed {@link MovementSensor} + */ + public MovementSensor create(Context context, int sensorType) { + SensorManager manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + Sensor sensor = manager.getDefaultSensor(sensorType); + SensorInfo info; + if (sensor != null) { + info = new SensorInfo( + sensor.getName(), + sensor.getVendor(), + sensor.getResolution(), + sensor.getPower(), + sensor.getVersion(), + sensor.getType() + ); + } else { + info = SensorInfo.unavailable(); + } + return new MovementSensor(context, manager, sensor, info); + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java index 1e14e5eb..3f378af9 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusion.java @@ -111,6 +111,7 @@ public class SensorFusion implements SensorEventListener, Observer { private MovementSensor rotationSensor; private MovementSensor gravitySensor; private MovementSensor linearAccelerationSensor; + private final MovementSensorFactory sensorFactory = new MovementSensorFactory(); // Other data recording private WifiDataProcessor wifiProcessor; private GNSSDataProcessor gnssProcessor; @@ -270,17 +271,17 @@ public static SensorFusion getInstance() { public void setContext(Context context) { this.appContext = context.getApplicationContext(); // store app context for later use - // Initialise data collection devices (unchanged)... - this.accelerometerSensor = new MovementSensor(context, Sensor.TYPE_ACCELEROMETER); - this.barometerSensor = new MovementSensor(context, Sensor.TYPE_PRESSURE); - this.gyroscopeSensor = new MovementSensor(context, Sensor.TYPE_GYROSCOPE); - this.lightSensor = new MovementSensor(context, Sensor.TYPE_LIGHT); - this.proximitySensor = new MovementSensor(context, Sensor.TYPE_PROXIMITY); - this.magnetometerSensor = new MovementSensor(context, Sensor.TYPE_MAGNETIC_FIELD); - this.stepDetectionSensor = new MovementSensor(context, Sensor.TYPE_STEP_DETECTOR); - this.rotationSensor = new MovementSensor(context, Sensor.TYPE_ROTATION_VECTOR); - this.gravitySensor = new MovementSensor(context, Sensor.TYPE_GRAVITY); - this.linearAccelerationSensor = new MovementSensor(context, Sensor.TYPE_LINEAR_ACCELERATION); + // Initialise data collection devices via factory + this.accelerometerSensor = sensorFactory.create(context, Sensor.TYPE_ACCELEROMETER); + this.barometerSensor = sensorFactory.create(context, Sensor.TYPE_PRESSURE); + this.gyroscopeSensor = sensorFactory.create(context, Sensor.TYPE_GYROSCOPE); + this.lightSensor = sensorFactory.create(context, Sensor.TYPE_LIGHT); + this.proximitySensor = sensorFactory.create(context, Sensor.TYPE_PROXIMITY); + this.magnetometerSensor = sensorFactory.create(context, Sensor.TYPE_MAGNETIC_FIELD); + this.stepDetectionSensor = sensorFactory.create(context, Sensor.TYPE_STEP_DETECTOR); + this.rotationSensor = sensorFactory.create(context, Sensor.TYPE_ROTATION_VECTOR); + this.gravitySensor = sensorFactory.create(context, Sensor.TYPE_GRAVITY); + this.linearAccelerationSensor = sensorFactory.create(context, Sensor.TYPE_LINEAR_ACCELERATION); // Listener based devices this.wifiProcessor = new WifiDataProcessor(context); wifiProcessor.registerObserver(this); @@ -1011,12 +1012,12 @@ public List getWifiList() { */ public List getSensorInfos() { List sensorInfoList = new ArrayList<>(); - sensorInfoList.add(this.accelerometerSensor.sensorInfo); - sensorInfoList.add(this.barometerSensor.sensorInfo); - sensorInfoList.add(this.gyroscopeSensor.sensorInfo); - sensorInfoList.add(this.lightSensor.sensorInfo); - sensorInfoList.add(this.proximitySensor.sensorInfo); - sensorInfoList.add(this.magnetometerSensor.sensorInfo); + sensorInfoList.add(this.accelerometerSensor.getSensorInfo()); + sensorInfoList.add(this.barometerSensor.getSensorInfo()); + sensorInfoList.add(this.gyroscopeSensor.getSensorInfo()); + sensorInfoList.add(this.lightSensor.getSensorInfo()); + sensorInfoList.add(this.proximitySensor.getSensorInfo()); + sensorInfoList.add(this.magnetometerSensor.getSensorInfo()); return sensorInfoList; } @@ -1082,16 +1083,16 @@ public int getHoldMode(){ * @see GNSSDataProcessor handles location data. */ public void resumeListening() { - accelerometerSensor.sensorManager.registerListener(this, accelerometerSensor.sensor, 10000, (int) maxReportLatencyNs); - accelerometerSensor.sensorManager.registerListener(this, linearAccelerationSensor.sensor, 10000, (int) maxReportLatencyNs); - accelerometerSensor.sensorManager.registerListener(this, gravitySensor.sensor, 10000, (int) maxReportLatencyNs); - barometerSensor.sensorManager.registerListener(this, barometerSensor.sensor, (int) 1e6); - gyroscopeSensor.sensorManager.registerListener(this, gyroscopeSensor.sensor, 10000, (int) maxReportLatencyNs); - lightSensor.sensorManager.registerListener(this, lightSensor.sensor, (int) 1e6); - proximitySensor.sensorManager.registerListener(this, proximitySensor.sensor, (int) 1e6); - magnetometerSensor.sensorManager.registerListener(this, magnetometerSensor.sensor, 10000, (int) maxReportLatencyNs); - stepDetectionSensor.sensorManager.registerListener(this, stepDetectionSensor.sensor, SensorManager.SENSOR_DELAY_NORMAL); - rotationSensor.sensorManager.registerListener(this, rotationSensor.sensor, (int) 1e6); + accelerometerSensor.getSensorManager().registerListener(this, accelerometerSensor.getSensor(), 10000, (int) maxReportLatencyNs); + accelerometerSensor.getSensorManager().registerListener(this, linearAccelerationSensor.getSensor(), 10000, (int) maxReportLatencyNs); + accelerometerSensor.getSensorManager().registerListener(this, gravitySensor.getSensor(), 10000, (int) maxReportLatencyNs); + barometerSensor.getSensorManager().registerListener(this, barometerSensor.getSensor(), (int) 1e6); + gyroscopeSensor.getSensorManager().registerListener(this, gyroscopeSensor.getSensor(), 10000, (int) maxReportLatencyNs); + lightSensor.getSensorManager().registerListener(this, lightSensor.getSensor(), (int) 1e6); + proximitySensor.getSensorManager().registerListener(this, proximitySensor.getSensor(), (int) 1e6); + magnetometerSensor.getSensorManager().registerListener(this, magnetometerSensor.getSensor(), 10000, (int) maxReportLatencyNs); + stepDetectionSensor.getSensorManager().registerListener(this, stepDetectionSensor.getSensor(), SensorManager.SENSOR_DELAY_NORMAL); + rotationSensor.getSensorManager().registerListener(this, rotationSensor.getSensor(), (int) 1e6); wifiProcessor.startListening(); gnssProcessor.startLocationUpdates(); } @@ -1108,16 +1109,16 @@ public void resumeListening() { public void stopListening() { if(!saveRecording) { // Unregister sensor-manager based devices - accelerometerSensor.sensorManager.unregisterListener(this); - barometerSensor.sensorManager.unregisterListener(this); - gyroscopeSensor.sensorManager.unregisterListener(this); - lightSensor.sensorManager.unregisterListener(this); - proximitySensor.sensorManager.unregisterListener(this); - magnetometerSensor.sensorManager.unregisterListener(this); - stepDetectionSensor.sensorManager.unregisterListener(this); - rotationSensor.sensorManager.unregisterListener(this); - linearAccelerationSensor.sensorManager.unregisterListener(this); - gravitySensor.sensorManager.unregisterListener(this); + accelerometerSensor.getSensorManager().unregisterListener(this); + barometerSensor.getSensorManager().unregisterListener(this); + gyroscopeSensor.getSensorManager().unregisterListener(this); + lightSensor.getSensorManager().unregisterListener(this); + proximitySensor.getSensorManager().unregisterListener(this); + magnetometerSensor.getSensorManager().unregisterListener(this); + stepDetectionSensor.getSensorManager().unregisterListener(this); + rotationSensor.getSensorManager().unregisterListener(this); + linearAccelerationSensor.getSensorManager().unregisterListener(this); + gravitySensor.getSensorManager().unregisterListener(this); //The app often crashes here because the scan receiver stops after it has found the list. // It will only unregister one if there is to unregister try { @@ -1398,7 +1399,7 @@ public void run() { if (counter == 99) { counter = 0; // Store pressure and light data - if (barometerSensor.sensor != null) { + if (barometerSensor.getSensor() != null) { trajectory.addPressureData(Traj.Pressure_Sample.newBuilder() .setPressure(pressure) .setRelativeTimestamp(SystemClock.uptimeMillis() - bootTime)) diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusionUtils.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusionUtils.java index 2faff394..66c3dc47 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusionUtils.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorFusionUtils.java @@ -14,13 +14,14 @@ public class SensorFusionUtils { // but now a static helper taking exactly what it needs. public static Traj.Sensor_Info.Builder createInfoBuilder(MovementSensor sensor) { + SensorInfo info = sensor.getSensorInfo(); return Traj.Sensor_Info.newBuilder() - .setName(sensor.sensorInfo.getName()) - .setVendor(sensor.sensorInfo.getVendor()) - .setResolution(sensor.sensorInfo.getResolution()) - .setPower(sensor.sensorInfo.getPower()) - .setVersion(sensor.sensorInfo.getVersion()) - .setType(sensor.sensorInfo.getType()); + .setName(info.getName()) + .setVendor(info.getVendor()) + .setResolution(info.getResolution()) + .setPower(info.getPower()) + .setVersion(info.getVersion()) + .setType(info.getType()); } // Move your old getAllSensorData code here. diff --git a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorInfo.java b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorInfo.java index 23b92848..ff0cd161 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorInfo.java +++ b/app/src/main/java/com/openpositioning/PositionMe/sensors/SensorInfo.java @@ -84,4 +84,12 @@ public String toString() { ", type=" + type + '}'; } + + /** + * Convenience factory returning a placeholder instance when a sensor is not + * available on the device. + */ + public static SensorInfo unavailable() { + return new SensorInfo("Not available", "-", -1.0f, 0.0f, 0, 0); + } } diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/BuildingOverlayProvider.java b/app/src/main/java/com/openpositioning/PositionMe/utils/BuildingOverlayProvider.java new file mode 100644 index 00000000..2967b964 --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/BuildingOverlayProvider.java @@ -0,0 +1,41 @@ +package com.openpositioning.PositionMe.utils; + +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; + +import java.util.List; + +/** + * Provides building specific information for displaying indoor maps. + */ +public interface BuildingOverlayProvider { + /** + * @return the unique building name used throughout the app. + */ + String getName(); + + /** + * Checks whether the given location lies inside this building. + */ + boolean contains(LatLng point); + + /** + * @return resources for each floor overlay ordered by floor index. + */ + List getFloorResources(); + + /** + * @return the bounds used when adding the overlay to the map. + */ + LatLngBounds getBounds(); + + /** + * @return height of a single floor for this building. + */ + float getFloorHeight(); + + /** + * @return the default floor shown when entering the building. + */ + int getDefaultFloor(); +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorMapManager.java b/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorMapManager.java index 2ae50a43..00759403 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorMapManager.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/IndoorMapManager.java @@ -10,8 +10,12 @@ import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.PolylineOptions; import com.openpositioning.PositionMe.R; -import java.util.Arrays; +import com.openpositioning.PositionMe.utils.BuildingOverlayProvider; +import com.openpositioning.PositionMe.utils.NucleusBuildingManager; +import com.openpositioning.PositionMe.utils.LibraryBuildingManager; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Class used to manage indoor floor map overlays @@ -36,30 +40,14 @@ public class IndoorMapManager { // NEW: Track which building's overlay is currently shown ("nucleus", "library", or empty) private String currentBuilding = ""; - // Indoor map resource lists for Nucleus and Library - private final List NUCLEUS_MAPS = Arrays.asList( - R.drawable.nucleuslg, R.drawable.nucleusg, R.drawable.nucleus1, - R.drawable.nucleus2, R.drawable.nucleus3); - private final List LIBRARY_MAPS = Arrays.asList( - R.drawable.libraryg, R.drawable.library1, R.drawable.library2, - R.drawable.library3); - - // Building bounds for overlay positioning - LatLngBounds NUCLEUS = new LatLngBounds( - BuildingPolygon.NUCLEUS_SW, - BuildingPolygon.NUCLEUS_NE - ); - LatLngBounds LIBRARY = new LatLngBounds( - BuildingPolygon.LIBRARY_SW, - BuildingPolygon.LIBRARY_NE - ); - - // Average floor heights for each building - public static final float NUCLEUS_FLOOR_HEIGHT = 4.2F; - public static final float LIBRARY_FLOOR_HEIGHT = 3.6F; + // Building overlay providers indexed by building name + private final Map providers = new HashMap<>(); + private BuildingOverlayProvider currentProvider; public IndoorMapManager(GoogleMap map) { this.gMap = map; + providers.put("nucleus", new NucleusBuildingManager(map)); + providers.put("library", new LibraryBuildingManager(map)); } /** @@ -93,20 +81,18 @@ public boolean getIsIndoorMapSet() { * @param autoFloor True if this change comes from an auto-floor feature. */ public void setCurrentFloor(int newFloor, boolean autoFloor) { - if ("nucleus".equals(currentBuilding)) { - // Special case for Nucleus: auto-floor adds a bias (e.g., lower-ground is floor 0) - if (autoFloor) { - newFloor += 1; - } - if (newFloor >= 0 && newFloor < NUCLEUS_MAPS.size() && newFloor != this.currentFloor) { - groundOverlay.setImage(BitmapDescriptorFactory.fromResource(NUCLEUS_MAPS.get(newFloor))); - this.currentFloor = newFloor; - } - } else if ("library".equals(currentBuilding)) { - if (newFloor >= 0 && newFloor < LIBRARY_MAPS.size() && newFloor != this.currentFloor) { - groundOverlay.setImage(BitmapDescriptorFactory.fromResource(LIBRARY_MAPS.get(newFloor))); - this.currentFloor = newFloor; - } + if (currentProvider == null) { + return; + } + + if ("nucleus".equals(currentProvider.getName()) && autoFloor) { + newFloor += 1; + } + + List resources = currentProvider.getFloorResources(); + if (newFloor >= 0 && newFloor < resources.size() && newFloor != this.currentFloor) { + groundOverlay.setImage(BitmapDescriptorFactory.fromResource(resources.get(newFloor))); + this.currentFloor = newFloor; } } @@ -124,43 +110,33 @@ public void decreaseFloor() { */ private void setBuildingOverlay() { try { - if (BuildingPolygon.inNucleus(currentLocation)) { - // If we're in Nucleus but either no overlay is set or a different building's overlay is active - if (!isIndoorMapSet || !"nucleus".equals(currentBuilding)) { - // Remove existing overlay if present - if (isIndoorMapSet && groundOverlay != null) { - groundOverlay.remove(); - } - groundOverlay = gMap.addGroundOverlay(new GroundOverlayOptions() - .image(BitmapDescriptorFactory.fromResource(R.drawable.nucleusg)) - .positionFromBounds(NUCLEUS)); - isIndoorMapSet = true; - currentFloor = 1; // Default floor for Nucleus - floorHeight = NUCLEUS_FLOOR_HEIGHT; - currentBuilding = "nucleus"; - } - } else if (BuildingPolygon.inLibrary(currentLocation)) { - if (!isIndoorMapSet || !"library".equals(currentBuilding)) { - if (isIndoorMapSet && groundOverlay != null) { - groundOverlay.remove(); + for (BuildingOverlayProvider provider : providers.values()) { + if (provider.contains(currentLocation)) { + if (!isIndoorMapSet || !provider.getName().equals(currentBuilding)) { + if (isIndoorMapSet && groundOverlay != null) { + groundOverlay.remove(); + } + groundOverlay = gMap.addGroundOverlay(new GroundOverlayOptions() + .image(BitmapDescriptorFactory.fromResource(provider.getFloorResources().get(provider.getDefaultFloor()))) + .positionFromBounds(provider.getBounds())); + isIndoorMapSet = true; + currentFloor = provider.getDefaultFloor(); + floorHeight = provider.getFloorHeight(); + currentBuilding = provider.getName(); + currentProvider = provider; } - groundOverlay = gMap.addGroundOverlay(new GroundOverlayOptions() - .image(BitmapDescriptorFactory.fromResource(R.drawable.libraryg)) - .positionFromBounds(LIBRARY)); - isIndoorMapSet = true; - currentFloor = 0; // Default floor for Library - floorHeight = LIBRARY_FLOOR_HEIGHT; - currentBuilding = "library"; - } - } else { - // If the user is no longer in any building with an indoor map - if (isIndoorMapSet && groundOverlay != null) { - groundOverlay.remove(); - isIndoorMapSet = false; - currentFloor = 0; - currentBuilding = ""; + return; } } + + // Not inside any building + if (isIndoorMapSet && groundOverlay != null) { + groundOverlay.remove(); + isIndoorMapSet = false; + currentFloor = 0; + currentBuilding = ""; + currentProvider = null; + } } catch (Exception ex) { Log.e("Error with overlay, Exception:", ex.toString()); } diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/LibraryBuildingManager.java b/app/src/main/java/com/openpositioning/PositionMe/utils/LibraryBuildingManager.java new file mode 100644 index 00000000..55288d0a --- /dev/null +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/LibraryBuildingManager.java @@ -0,0 +1,111 @@ +package com.openpositioning.PositionMe.utils; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; +import com.openpositioning.PositionMe.R; +import com.openpositioning.PositionMe.presentation.fragment.IndoorMapFragment; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Building manager providing overlay data for the Library building. + */ +public class LibraryBuildingManager implements BuildingOverlayProvider { + private IndoorMapFragment indoorMapFragment; + private ArrayList buildingPolygon; + private final List floorResources = Arrays.asList( + R.drawable.libraryg, + R.drawable.library1, + R.drawable.library2, + R.drawable.library3 + ); + + private static final float FLOOR_HEIGHT = 3.6f; + private static final int DEFAULT_FLOOR = 0; + + public LibraryBuildingManager(GoogleMap map) { + indoorMapFragment = new IndoorMapFragment(map, 4); + + double N1 = 55.92281045664704; + double W1 = 3.175184089079065; + double N2 = 55.92306692576906; + double W2 = 3.174771893078224; + + buildingPolygon = new ArrayList<>(); + buildingPolygon.add(new LatLng(N1, -W1)); + buildingPolygon.add(new LatLng(N1, -W2)); + buildingPolygon.add(new LatLng(N2, -W2)); + buildingPolygon.add(new LatLng(N2, -W1)); + + indoorMapFragment.addFloor(0, R.drawable.libraryg, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2))); + indoorMapFragment.addFloor(1, R.drawable.library1, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2))); + indoorMapFragment.addFloor(2, R.drawable.library2, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2))); + indoorMapFragment.addFloor(3, R.drawable.library3, new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2))); + } + + public IndoorMapFragment getIndoorMapManager() { + return indoorMapFragment; + } + + private boolean isPointInBuilding(LatLng point) { + int intersectCount = 0; + for (int j = 0; j < buildingPolygon.size(); j++) { + LatLng vertA = buildingPolygon.get(j); + LatLng vertB = buildingPolygon.get((j + 1) % buildingPolygon.size()); + if (rayCastIntersect(point, vertA, vertB)) { + intersectCount++; + } + } + return ((intersectCount % 2) == 1); + } + + private boolean rayCastIntersect(LatLng point, LatLng vertA, LatLng vertB) { + double aY = vertA.latitude; + double bY = vertB.latitude; + double aX = vertA.longitude; + double bX = vertB.longitude; + double pY = point.latitude; + double pX = point.longitude; + if ((aY > pY && bY > pY) || (aY < pY && bY < pY) || (aX < pX && bX < pX)) { + return false; + } + double m = (aY - bY) / (aX - bX); + double bee = -aX * m + aY; + double x = (pY - bee) / m; + return x > pX; + } + + // BuildingOverlayProvider implementation + @Override + public String getName() { + return "library"; + } + + @Override + public boolean contains(LatLng point) { + return isPointInBuilding(point); + } + + @Override + public List getFloorResources() { + return floorResources; + } + + @Override + public LatLngBounds getBounds() { + return new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)); + } + + @Override + public float getFloorHeight() { + return FLOOR_HEIGHT; + } + + @Override + public int getDefaultFloor() { + return DEFAULT_FLOOR; + } +} diff --git a/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java b/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java index 3570e8ad..55ee7190 100644 --- a/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java +++ b/app/src/main/java/com/openpositioning/PositionMe/utils/NucleusBuildingManager.java @@ -7,10 +7,25 @@ import com.openpositioning.PositionMe.presentation.fragment.IndoorMapFragment; import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; -public class NucleusBuildingManager { +/** + * Building manager providing overlay data for the Nucleus building. + */ +public class NucleusBuildingManager implements BuildingOverlayProvider { private IndoorMapFragment indoorMapFragment; private ArrayList buildingPolygon; + private final List floorResources = Arrays.asList( + R.drawable.nucleuslg, + R.drawable.nucleusg, + R.drawable.nucleus1, + R.drawable.nucleus2, + R.drawable.nucleus3 + ); + + private static final float FLOOR_HEIGHT = 4.2f; + private static final int DEFAULT_FLOOR = 1; public NucleusBuildingManager(GoogleMap map) { // The nuclear building has 5 floors @@ -43,6 +58,37 @@ public IndoorMapFragment getIndoorMapManager() { return indoorMapFragment; } + // --- Implementation of BuildingOverlayProvider --- + @Override + public String getName() { + return "nucleus"; + } + + @Override + public boolean contains(LatLng point) { + return isPointInBuilding(point); + } + + @Override + public List getFloorResources() { + return floorResources; + } + + @Override + public LatLngBounds getBounds() { + return new LatLngBounds(buildingPolygon.get(0), buildingPolygon.get(2)); + } + + @Override + public float getFloorHeight() { + return FLOOR_HEIGHT; + } + + @Override + public int getDefaultFloor() { + return DEFAULT_FLOOR; + } + /** * Determines if a given point is inside the building polygon. * diff --git a/app/src/main/res/raw/marker_icons.json b/app/src/main/res/raw/marker_icons.json new file mode 100644 index 00000000..3f4a5790 --- /dev/null +++ b/app/src/main/res/raw/marker_icons.json @@ -0,0 +1,9 @@ +{ + "medicalRoom": "iso_first_aid_icon", + "emergencyExit": "iso_emergency_exit", + "lift": "iso_lift", + "accessibleToilet": "iso_accessible__toilet", + "drinkingWater": "iso_drinking_water", + "toilet": "iso_toilet_icon", + "accessibleRoute": "iso_symbol_of_access" +} diff --git a/app/src/main/res/raw/poi_locations.json b/app/src/main/res/raw/poi_locations.json new file mode 100644 index 00000000..436a1a68 --- /dev/null +++ b/app/src/main/res/raw/poi_locations.json @@ -0,0 +1,99 @@ +{ + "medicalRoom": { + "nucleus": { + "2": [ + { "lat": 55.923291498633766, "lng": -3.1743306666612625 } + ] + } + }, + "emergencyExit": { + "nucleus": { + "0": [ + { "lat": 55.92327684586336, "lng": -3.174331672489643 }, + { "lat": 55.92305836864246, "lng": -3.174259588122368 }, + { "lat": 55.923040334354255, "lng": -3.174433596432209 } + ], + "1": [ + { "lat": 55.922839326615474, "lng": -3.17451573908329 }, + { "lat": 55.92283913875728, "lng": -3.1742961332201958 }, + { "lat": 55.92330295784777, "lng": -3.174157999455929 }, + { "lat": 55.92287501965453, "lng": -3.174012154340744 }, + { "lat": 55.92301422219288, "lng": -3.173900842666626 }, + { "lat": 55.92308936505573, "lng": -3.1738971546292305 }, + { "lat": 55.923300515720484, "lng": -3.1740912795066833 } + ], + "2": [ + { "lat": 55.92303563792365, "lng": -3.174491263926029 }, + { "lat": 55.92327647015123, "lng": -3.174472488462925 } + ] + } + }, + "lift": { + "nucleus": { + "0": [ + { "lat": 55.92303883149652, "lng": -3.1743890047073364 }, + { "lat": 55.923040334354255, "lng": -3.1743494421243668 }, + { "lat": 55.92304277649792, "lng": -3.1743118911981583 } + ], + "1": [ + { "lat": 55.923027560061676, "lng": -3.1743665412068367 }, + { "lat": 55.92303075363521, "lng": -3.17432664334774 }, + { "lat": 55.923030565777935, "lng": -3.174288421869278 } + ], + "2": [ + { "lat": 55.92304484292707, "lng": -3.17434910684824 }, + { "lat": 55.923045970070206, "lng": -3.1743139028549194 }, + { "lat": 55.92304390364111, "lng": -3.1743836402893066 } + ], + "3": [ + { "lat": 55.92304484292707, "lng": -3.17434910684824 }, + { "lat": 55.923045970070206, "lng": -3.1743139028549194 }, + { "lat": 55.92304390364111, "lng": -3.1743836402893066 } + ], + "4": [ + { "lat": 55.92304484292707, "lng": -3.17434910684824 }, + { "lat": 55.923045970070206, "lng": -3.1743139028549194 }, + { "lat": 55.92304390364111, "lng": -3.1743836402893066 } + ] + } + }, + "accessibleToilet": { + "nucleus": { + "1": [ + { "lat": 55.923273276597925, "lng": -3.1740865856409073 }, + { "lat": 55.922906391930155, "lng": -3.174562007188797 } + ] + } + }, + "drinkingWater": { + "nucleus": { + "1": [ + { "lat": 55.922907331219456, "lng": -3.1744718179106712 } + ] + } + }, + "toilet": { + "nucleus": { + "0": [ + { "lat": 55.92308617148703, "lng": -3.174508698284626 } + ], + "1": [ + { "lat": 55.9229417091921, "lng": -3.174556642770767 } + ], + "2": [ + { "lat": 55.92308617148703, "lng": -3.174508698284626 } + ], + "3": [ + { "lat": 55.92308617148703, "lng": -3.174508698284626 } + ] + } + }, + "accessibleRoute": { + "nucleus": { + "1": [ + { "lat": 55.92284627736777, "lng": -3.174579441547394 }, + { "lat": 55.92327102232486, "lng": -3.1744134798645973 } + ] + } + } +} diff --git a/gradlew b/gradlew old mode 100644 new mode 100755