Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
978cf01
Uncommented map layer to have a map I can work with
May 9, 2025
5caf0d3
fix: changed leaflet implementation to use vite/npm
May 12, 2025
664f489
refactor: switched to a modular js style to help split up the code a …
May 13, 2025
1cb9396
fix: got waypoint working
May 13, 2025
b1767b0
fix: fixed some vite shit, fixed circle tools
May 14, 2025
e6cff97
feat: successfully implemented a tile layer that uses locally-downloa…
May 15, 2025
1393162
docs: updated docs with npm stuff
May 19, 2025
03ba5eb
style: changed default zoom to 17 (for now)
May 19, 2025
837072d
refactor: removed dist folder for gh
May 22, 2025
4138658
fix: updated .gitignore
May 22, 2025
fed45b4
feat: started working on webtransport impl for rover coords on map
May 24, 2025
1cc064c
feat: python impl of fake gps stream (I can't run this)
May 24, 2025
9f4d921
feat: gps server and client for frontend
May 24, 2025
df50b47
fix: fixed gitignore to not include debug stuff
May 24, 2025
dea2350
fix: changed rust library to webtransport, got both the frontend and …
May 24, 2025
32f777b
fix: fixed 42 error, got NEW 48 error
May 26, 2025
fd805e6
feat: got static coordinate working
May 26, 2025
0911d16
Updated gitignore
May 26, 2025
ceaac1a
feat: added updating coord to confirm that marker moves correctly
May 26, 2025
0ea4fc6
refac: got rid of target file on working tree
May 27, 2025
9ad7180
fix: updated .gitignore
May 27, 2025
7d84ae9
refac: removed dist/ from working tree
May 27, 2025
31d34e9
docs: updated README
May 27, 2025
ab355c3
fix: fixed some minor typos and stuff
May 27, 2025
40fc685
chore: got rid of some unnecessary comments
May 27, 2025
633c772
feat: added circle on point button/input
May 28, 2025
4b13cf4
fix: made suggested review changes
May 28, 2025
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
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
tiles/
tiles.tar
tiles.tar
node_modules
map_tiles
public/
dist/
gps_webtransport_server/target/
59 changes: 43 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,60 @@
# Map Server

This houses both the HTML/CSS/JS frontend to render the map with the rover's coordinates,
and the backend server that powers it.
This houses both the JS/vite frontend to render the map with the rover's coordinates, the home for generated map tiles, and a fake gps server for testing. For the backend of the map server, see the `auto_ros2` repository.

## Structure

The frontend provides a leafletjs map which draws the rover's position on it.
The map renderer uses image files served by `server.py`, and generated by
the generating program in `RoverMapTileGenerator`.
The map renderer uses map tile image files created using QGIS software and stored locally in the `public` and `dist` folders.

### Using QGIS to generate map tiles
**1. Generate an osm file using Open Street Maps**
First, you need to generate an osm file using Open Street Maps to ensure the correct extent for the map data.
Our current map uses the following bounds from the old map server:
``` javascript
// the lat/lon coords are the center of the map, 10 is the default zoom.
var map = L.map('map').setView([38.4375, -110.8125], 10);

// These are the bounds of the rover team's map region for debugging.
new L.marker([38.4375, -110.8125]).addTo(map)
new L.marker([38.3750, -110.7500]).addTo(map)
```
Go to [this link](https://www.openstreetmap.org/export#map=13/38.40107/-110.79815) to export an Open Street Map. Set the center as your desired center (in this case 38.4375, -110.8125), and set the bounds for the map as your desired bounds. Once the map has the correct bounds, export as an osm file, and save to your computer.
**2. Import to QGIS**
Download [QGIS](https://qgis.org/download/). Once downloaded, follow [these instructions](https://learnosm.org/en/osm-data/osm-in-qgis/) for importing an osm file. This should allow you to view the map on QGIS.
**3. Add satellite imagery**
Our current map uses Satellite Imagery on QGIS, which you can find instructions for adding to a map [here](https://gis.stackexchange.com/questions/439936/adding-google-satellite-imagery-to-qgis).
**4. Export map tiles**
Once the map looks the way you want it to, follow [these instructions](https://www.google.com/url?sa=t&source=web&rct=j&opi=89978449&url=https://www.youtube.com/watch%3Fv%3DvU4bGCh5khM&ved=2ahUKEwjVz6-KjsSNAxWhGDQIHbdHH0UQwqsBegQIEhAG&usg=AOvVaw0kwEDk3Qe84kvYYel7yQvp) to download the QTiles plugin and export your map as a set of map tiles. When choosing the directory to export your map tiles to, select `RoverMap/frontend/public`.
Once you upload new map tiles, don't forget to change the directory path in `frontend/map.js`. Also, if you want to use the tiles offline, make sure to rebuild the `dist/` folder (see offline instructions).

This project uses `npm` and `vite` to manage package dependencies for Javascript and
Leaflet. To use the map server, make sure to [install npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). Once npm is installed, go to the frontend directory:
`cd frontend`
and run the following command to install project dependencies:
`npm install`

## Running and Integration

You can run the server in standalone mode by running `python3 server.py`.
### For Frontend
For online run, you can run `npx vite` to run the server's frontend. (Note: this requires for there to be map tiles in the `public` folder.)

To run it from python, import and call `start_map_server`.
The flask app isn't wrapped nicely in a class (this would be a good refactor),
so we really need to test this integration and make sure it works.
#### Offline Instructions
For offline, first make sure to run `npx vite build` for any new changes before
running offline. Then, given that the `dist` folder is properly populated, run
`npx serve dist` to serve the frontend offline.

The server sends the rover's gps coords to the web client,
so the server will need to be supplied with those coords.
Call `update_rover_coords` with an array of lat, lng like `[38.4065, -110.79147]`,
the center of the mars thingy.

There's an example in `example/updater.py` to go off of.
### For Server
The server for this frontend is to be implemented in the `auto_ros2` repository. For testing purposes, a fake gps server is set up in `gps_webtransport_server`. To run the fake gps server, simply run:
```
cd gps_webtransport_server
cargo run
```
When you run this server in combination with the map server, it should generate a fake rover position on the map that alternates between two points.

## Accessing

The server should open to port 5000, so you can access by connecting to `10.0.0.2:5000`.
That port could change somehow, so you may need to check stdout to find the exact address and port.
The server should open to port 5173, so you can access by connecting to `https://localhost:5173/`. When you run `npx vite`, it should give you this link automatically.

## Documentation

Expand Down
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules/
dist/
97 changes: 97 additions & 0 deletions frontend/circleTools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { map } from './map.js';
import { toggleActivateButton } from './helpers.js';

let circles = [];
let labels = [];
let selectedCircle = null;
let isResizing = false;
let resizeHandle;
let activeTool = "none";

let circleList = document.getElementById('circleList');
let createCircleButton = document.getElementById('toggleCircleButton');

export const toggleCircles = () => {
activeTool = (activeTool === 'circleDraw') ? 'none' : 'circleDraw';
toggleActivateButton(createCircleButton, activeTool === 'circleDraw');
};

export const clearCircles = () => {
circles.forEach(circle => circle.remove());
labels.forEach(label => label.remove());
circles = [];
labels = [];
updateCircleList();
};

export function circleOnPoint() {
const input = prompt("Enter coordinates in `lat lon` format (e.g. 37.7749 -122.4194):")?.trim();
if (!input) return;

const [latStr, lonStr] = input.split(/\s+/);
const lat = parseFloat(latStr);
const lon = parseFloat(lonStr);

if (isNaN(lat) || isNaN(lon)) {
alert("Invalid coordinates. Please enter valid numbers.");
return;
}

const isConfirmed = confirm(`Create a circle at latitude: ${lat}, longitude: ${lon}?`);
if (!isConfirmed) return;

const center = L.latLng(lat, lon);
const circle = L.circle(center, { radius: 50, color: "blue" }).addTo(map);
circles.push(circle);

const label = L.marker(center, { opacity: 0 });
label.bindTooltip(`${circles.length}`, {
permanent: true, className: "circleLabel", direction: 'top', offset: [-15, 20]
}).addTo(map);
labels.push(label);

updateCircleList();
}

function updateCircleList() {
circleList.innerHTML = '<h2>Circle List</h2>';
circles.forEach((circle, index) => {
let radius = Math.round(circle.getRadius());
circleList.innerHTML += `<p>${index + 1}. Radius: ${radius}m<br /> Circumference: ${2 * radius}m</p>`;
});
}

map.on('mousedown', function (e) {
if (activeTool !== 'circleDraw') return;
if (!isResizing) {
selectedCircle = L.circle(e.latlng, { radius: 20, color: "green" }).addTo(map);
circles.push(selectedCircle);

let marker = new L.marker(e.latlng, { opacity: 0 });
marker.bindTooltip(circles.length + "", {
permanent: true, className: "circleLabel", direction: 'top', offset: [-15, 20]
}).addTo(map);
labels.push(marker);

isResizing = true;
resizeHandle = e.latlng;
map.dragging.disable();
updateCircleList();
}
});

map.on('mousemove', function (e) {
if (isResizing && selectedCircle) {
let newRadius = resizeHandle.distanceTo(e.latlng);
selectedCircle.setRadius(newRadius);
updateCircleList();
}
});

map.on('mouseup', function () {
if (isResizing) {
isResizing = false;
map.dragging.enable();
updateCircleList();
}
});
2 changes: 2 additions & 0 deletions frontend/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ACTIVE_BUTTON_TEXT = "ACTIVE";
export const INACTIVE_BUTTON_TEXT = "INACTIVE";
27 changes: 27 additions & 0 deletions frontend/gps-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export function startGPSClient() {
const socket = new WebSocket('ws://localhost:9001'); // make sure port matches server port

socket.onopen = () => {
console.log('🔌 WebSocket connection established');
}

socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
const gpsEvent = new CustomEvent('gps-update', { detail: data });
window.dispatchEvent(gpsEvent);
} catch (e) {
console.error('❌ Invalid GPS data:', e);
}
};

socket.onerror = (e) => {
console.error('❌ WebSocket error:', e);
};

socket.onclose = () => {
console.warn('⚠️ WebSocket connection closed');
};

return socket;
}
101 changes: 101 additions & 0 deletions frontend/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ACTIVE_BUTTON_TEXT, INACTIVE_BUTTON_TEXT } from './constants.js';

// UI Helpers
export const toggleActivateButton = (button, active) => {
if (active) {
button.textContent = ACTIVE_BUTTON_TEXT;
button.classList.remove("deactivated-tool-button");
button.classList.add("activated-tool-button");
} else {
button.textContent = INACTIVE_BUTTON_TEXT;
button.classList.remove("activated-tool-button");
button.classList.add("deactivated-tool-button");
}
};

// Other Tools
export const degMinSecToDecimal = (deg, min, sec) => {
return deg + (min / 60) + (sec / 3600);
};

export const degDecimalMinToDecimal = (deg, min) => {
return deg + (min / 60);
};

export const updateConversionsBasedOn = (el) => {
const sources = {
"DMS": ["DMSDeg", "DMSMin", "DMSSec"],
"DDM": ["DDMDeg", "DDMMin"],
"Decimal": ["Decimal"]
};

let source = "DMS";
if (el.id.includes("DDM")) source = "DDM";
else if (el.id.includes("Decimal")) source = "Decimal";

let sourceValues = sources[source].map(id =>
parseFloat(document.getElementById(id)?.value) || 0
);

let DMSValues = [];
let DDMValues = [];
let DecimalValue = 0;

if (source === "DMS") {
DMSValues = sourceValues;
DecimalValue = degMinSecToDecimal(...sourceValues);
} else if (source === "DDM") {
DDMValues = sourceValues;
DecimalValue = degDecimalMinToDecimal(...sourceValues);
} else if (source === "Decimal") {
DecimalValue = sourceValues[0];
}

document.getElementById("DMSDeg").value = DMSValues[0] || "";
document.getElementById("DMSMin").value = DMSValues[1] || "";
document.getElementById("DMSSec").value = DMSValues[2] || "";

document.getElementById("DDMDeg").value = DDMValues[0] || "";
document.getElementById("DDMMin").value = DDMValues[1] || "";

document.getElementById("Decimal").value = DecimalValue;
};

export const copyCoords = () => {
const el = document.getElementById("Decimal");
el.select();
el.setSelectionRange(0, 9999);
document.execCommand("copy");

const saved = document.getElementById("saved");
saved.innerHTML += `<p>${el.value}</p>`;
};

let open = false;
export const openUnitConverter = () => {
if (!open) {
document.getElementById("unitConverter").style.display = "block";
open = true;
} else {
closeUnitConverter();
}
};

export const closeUnitConverter = () => {
document.getElementById("unitConverter").style.display = "none";
open = false;
};

export const addWayPoint = (map, pathMarkers) => {
const coordinates = prompt("Enter your coordinate in `lat lon` format")?.split(" ");
if (!coordinates || coordinates.length !== 2) return;

const lat = parseFloat(coordinates[0]);
const lng = parseFloat(coordinates[1]);

const isConfirmed = confirm(`Add indicator at lat: ${lat}, lng: ${lng}?`);
if (isConfirmed) {
const marker = L.marker([lat, lng]).addTo(map);
pathMarkers.push(marker);
}
};
35 changes: 35 additions & 0 deletions frontend/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,39 @@ h2 {
#pathOutput {
flex: 1;
padding: 1em;
}

.gps-marker {
position: relative;
width: 20px;
height: 20px;
border-radius: 50%;
background-color: transparent;
}

.gps-pulse {
width: 20px;
height: 20px;
background-color: rgba(0, 128, 255, 0.5);
border: 2px solid #0080ff;
border-radius: 50%;
animation: pulse 2s infinite;
box-sizing: border-box;
}

@keyframes pulse {
0% {
transform: scale(0.9);
opacity: 1;
}

70% {
transform: scale(1.5);
opacity: 0;
}

100% {
transform: scale(0.9);
opacity: 0;
}
}
Loading