Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
24 changes: 9 additions & 15 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
.gradle/
.gradle-user-home/
.gradle-project-cache/
.android-home/
local.properties
/.idea/
secrets.properties
*.iml
.idea/
build/
app/build/
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}
Binary file added _local_doc/01_main_screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added _local_doc/02_recording_map_screen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file added _local_docs/Assignment 2.pdf
Binary file not shown.
6 changes: 5 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ plugins {
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}

// (Optional) load local secrets file:
// Load checked-in placeholder values first, then allow local secrets to override them.
def localProperties = new Properties()
def defaultsFile = rootProject.file('local.defaults.properties')
if (defaultsFile.exists()) {
localProperties.load(new FileInputStream(defaultsFile))
}
def localPropertiesFile = rootProject.file('secrets.properties')
if (localPropertiesFile.exists()) {
localProperties.load(new FileInputStream(localPropertiesFile))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import okhttp3.Call;
import okhttp3.Callback;
Expand Down Expand Up @@ -352,12 +353,12 @@ private JSONArray extractFirstRing(JSONObject geometry, String type)
}

/**
* Parses the map_shapes JSON string into a list of FloorShapes, sorted by floor key.
* Parses the map_shapes JSON string into a list of FloorShapes, sorted by actual floor order.
* The top-level JSON is an object with keys like "B1", "B2", etc. Each value is a
* GeoJSON FeatureCollection containing indoor features (walls, rooms, etc.).
*
* @param mapShapesJson the raw map_shapes JSON string from the API
* @return list of FloorShapes sorted by key (B1=index 0, B2=index 1, ...)
* @return list of FloorShapes sorted by logical floor order (e.g. LG, G, 1, 2, 3)
*/
private List<FloorShapes> parseMapShapes(String mapShapesJson) {
List<FloorShapes> result = new ArrayList<>();
Expand All @@ -366,13 +367,12 @@ private List<FloorShapes> parseMapShapes(String mapShapesJson) {
try {
JSONObject root = new JSONObject(mapShapesJson);

// Collect and sort floor keys (B1, B2, B3...)
// Collect floor keys first so we can parse and then sort by real floor order.
List<String> keys = new ArrayList<>();
Iterator<String> it = root.keys();
while (it.hasNext()) {
keys.add(it.next());
}
Collections.sort(keys);

for (String key : keys) {
JSONObject floorCollection = root.getJSONObject(key);
Expand All @@ -391,13 +391,84 @@ private List<FloorShapes> parseMapShapes(String mapShapesJson) {
}
result.add(new FloorShapes(key, displayName, shapeFeatures));
}
result.sort((left, right) -> {
int leftOrder = parseFloorOrder(left.getDisplayName(), left.getKey());
int rightOrder = parseFloorOrder(right.getDisplayName(), right.getKey());
if (leftOrder != rightOrder) {
return Integer.compare(leftOrder, rightOrder);
}
return left.getDisplayName().compareToIgnoreCase(right.getDisplayName());
});
} catch (JSONException e) {
Log.e(TAG, "Failed to parse map_shapes", e);
}

return result;
}

private int parseFloorOrder(String displayName, String fallbackKey) {
Integer parsed = parseFloorLabel(displayName);
if (parsed != null) {
return parsed;
}
parsed = parseFloorLabel(fallbackKey);
return parsed != null ? parsed : Integer.MAX_VALUE / 2;
}

private Integer parseFloorLabel(String rawLabel) {
if (rawLabel == null) {
return null;
}

String normalized = rawLabel.trim().toUpperCase(Locale.UK);
if (normalized.isEmpty()) {
return null;
}

normalized = normalized
.replace("FLOOR", "")
.replace("LEVEL", "")
.replace("STOREY", "")
.replace("STORY", "")
.replace("_", "")
.replace("-", "")
.replace(" ", "");

if ("LG".equals(normalized) || "LOWGROUND".equals(normalized)
|| "LOWERGROUND".equals(normalized)) {
return -1;
}
if ("G".equals(normalized) || "GF".equals(normalized) || "GROUND".equals(normalized)) {
return 0;
}
if ("UG".equals(normalized) || "UPGROUND".equals(normalized)
|| "UPPERGROUND".equals(normalized)) {
return 1;
}

if (normalized.startsWith("B") && normalized.length() > 1) {
try {
return -Integer.parseInt(normalized.substring(1));
} catch (NumberFormatException ignored) {
return null;
}
}

if (normalized.startsWith("L") && normalized.length() > 1) {
try {
return Integer.parseInt(normalized.substring(1));
} catch (NumberFormatException ignored) {
return null;
}
}

try {
return Integer.parseInt(normalized);
} catch (NumberFormatException ignored) {
return null;
}
}

/**
* Parses a single GeoJSON Feature from map_shapes into a MapShapeFeature.
*
Expand All @@ -407,8 +478,7 @@ private List<FloorShapes> parseMapShapes(String mapShapesJson) {
private MapShapeFeature parseMapShapeFeature(JSONObject feature) {
try {
JSONObject properties = feature.optJSONObject("properties");
String indoorType = (properties != null)
? properties.optString("indoor_type", "unknown") : "unknown";
String indoorType = normalizeIndoorType(properties);

JSONObject geometry = feature.getJSONObject("geometry");
String geoType = geometry.getString("type");
Expand Down Expand Up @@ -451,6 +521,43 @@ private MapShapeFeature parseMapShapeFeature(JSONObject feature) {
}
}

private String normalizeIndoorType(JSONObject properties) {
if (properties == null) {
return "unknown";
}

String rawIndoorType = properties.optString("indoor_type", "");
if (rawIndoorType.isEmpty()) {
rawIndoorType = properties.optString("indoor", "");
}
if (rawIndoorType.isEmpty()) {
rawIndoorType = properties.optString("feature_type", "");
}
if (rawIndoorType.isEmpty()) {
rawIndoorType = properties.optString("name", "");
}

String normalized = rawIndoorType.trim().toLowerCase(Locale.UK);
if (normalized.isEmpty()) {
return "unknown";
}
if (normalized.contains("wall")) {
return "wall";
}
if (normalized.contains("lift") || normalized.contains("elevator")) {
return "lift";
}
if (normalized.contains("stair") || normalized.contains("steps")
|| normalized.contains("escalator")) {
return "stairs";
}
if (normalized.contains("room") || normalized.contains("corridor")
|| normalized.contains("hall")) {
return "room";
}
return normalized;
}

/**
* Parses a GeoJSON coordinate array into a list of LatLng points.
* GeoJSON coordinate order is [longitude, latitude].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
setContentView(R.layout.activity_recording);

if (savedInstanceState == null) {
SensorFusion.getInstance().resetLivePositioningState();
// Show trajectory name input dialog before proceeding to start location
showTrajectoryNameDialog();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class CorrectionFragment extends Fragment {
private static float scalingRatio = 0f;
private static LatLng start;
private PathView pathView;
private float initialMapZoom = Float.NaN;

public CorrectionFragment() {
// Required empty public constructor
Expand All @@ -64,6 +65,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,
activity.getSupportActionBar().hide();
}
View rootView = inflater.inflate(R.layout.fragment_correction, container, false);
pathView = rootView.findViewById(R.id.pathView1);

// Validate trajectory quality before uploading
validateAndUpload();
Expand All @@ -80,19 +82,33 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onMapReady(GoogleMap map) {
mMap = map;
mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
mMap.getUiSettings().setCompassEnabled(true);
mMap.getUiSettings().setTiltGesturesEnabled(true);
mMap.getUiSettings().setRotateGesturesEnabled(true);
mMap.getUiSettings().setScrollGesturesEnabled(true);
// 校正页允许缩放查看轨迹,但不允许平移/旋转底图,
// 否则屏幕坐标轨迹会和地图发生相对漂移。
mMap.getUiSettings().setScrollGesturesEnabled(false);
mMap.getUiSettings().setRotateGesturesEnabled(false);
mMap.getUiSettings().setTiltGesturesEnabled(false);
mMap.getUiSettings().setZoomGesturesEnabled(false);
mMap.getUiSettings().setZoomControlsEnabled(true);
mMap.getUiSettings().setCompassEnabled(false);
mMap.getUiSettings().setMapToolbarEnabled(false);

// Add a marker at the start position
start = new LatLng(startPosition[0], startPosition[1]);
mMap.addMarker(new MarkerOptions().position(start).title("Start Position"));

// Calculate zoom for demonstration
double zoom = Math.log(156543.03392f * Math.cos(startPosition[0] * Math.PI / 180)
* scalingRatio) / Math.log(2);
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(start, (float) zoom));
if (pathView != null) {
pathView.post(() -> {
float baseScale = pathView.ensureBaseScale();
double safeScale = Math.max(1e-3, baseScale);
double zoom = Math.log(156543.03392f
* Math.cos(startPosition[0] * Math.PI / 180)
* safeScale) / Math.log(2);
initialMapZoom = (float) zoom;
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(start, initialMapZoom));
pathView.setMapZoomScale(1f);
mMap.setOnCameraMoveListener(() -> CorrectionFragment.this.syncOverlayZoom());
});
}
}
});

Expand All @@ -119,6 +135,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
sensorFusion.redrawPath(newStepLength / averageStepLength);
averageStepLengthText.setText(getString(R.string.averageStepLgn)
+ ": " + String.format("%.2f", newStepLength));
if (mMap != null && !Float.isNaN(initialMapZoom)) {
syncOverlayZoom();
}
pathView.invalidate();

secondPass++;
Expand Down Expand Up @@ -162,6 +181,16 @@ public void setScalingRatio(float scalingRatio) {
this.scalingRatio = scalingRatio;
}

private void syncOverlayZoom() {
if (mMap == null || pathView == null || Float.isNaN(initialMapZoom)) {
return;
}
float currentZoom = mMap.getCameraPosition().zoom;
float zoomScale = (float) Math.pow(2d, currentZoom - initialMapZoom);
pathView.setMapZoomScale(zoomScale);
pathView.invalidate();
}

/**
* Runs pre-upload quality validation and either uploads directly (if clean)
* or shows a warning dialog letting the user choose to proceed or cancel.
Expand Down
Loading