Skip to content
Merged
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 removed Delafontaine_CV.pdf
Binary file not shown.
Binary file added about/Delafontaine_CV.pdf
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes
Binary file added about/images/victor.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
422 changes: 422 additions & 0 deletions about/index.html

Large diffs are not rendered by default.

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file removed images/Delafontaine_photo.jpg
Binary file not shown.
Binary file removed images/portfolio/ai-tin174/overview.PNG
Binary file not shown.
Binary file removed images/portfolio/ai-tin174/overview_smaller.png
Binary file not shown.
Binary file removed images/portfolio/elec-assistant/tp10.PNG
Binary file not shown.
Binary file removed images/portfolio/elec-assistant/tp6.PNG
Binary file not shown.
Binary file not shown.
Binary file removed images/portfolio/fish-lsro/final.png
Binary file not shown.
Binary file removed images/portfolio/fish-lsro/merged.png
Binary file not shown.
Binary file removed images/portfolio/fish-lsro/pcb1.jpg
Binary file not shown.
Binary file not shown.
Binary file removed images/portfolio/flower/flowerpot.bmp
Binary file not shown.
Binary file removed images/portfolio/flower/flowerpot_margin.bmp
Binary file not shown.
Binary file removed images/portfolio/legged/bob.jpg
Binary file not shown.
Binary file not shown.
Binary file removed images/portfolio/prog-assistant/project.png
Diff not rendered.
Binary file removed images/portfolio/prog-assistant/project_margin.png
Diff not rendered.
Binary file removed images/portfolio/sim/delafontaine-balelec-report.pdf
Binary file not shown.
Binary file removed images/portfolio/sim/em_0_before.png
Diff not rendered.
Binary file not shown.
Binary file removed images/portfolio/simulator-lis/main_swarm2.png
Diff not rendered.
Diff not rendered.
Binary file removed images/portfolio/simulator-lis/sim.gif
Diff not rendered.
Binary file removed images/portfolio/swisscom/drone.png
Diff not rendered.
Binary file removed images/portfolio/swisscom/drone_square.png
Diff not rendered.
Binary file removed images/profile-pic.jpg
Diff not rendered.
Binary file removed images/sample-image.jpg
Diff not rendered.
853 changes: 15 additions & 838 deletions index.html

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions topo-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Playbook

### Strava activities

Go to [Strava API](https://www.strava.com/settings/api), then setup a token. Then run the `get_activities.sh` script and input them. This should create a file with the client ID in data.

### Roads
If you want roads: go to [Overpass](https://overpass-turbo.eu/#), then add this input (for all drivable roads in Lausanne)
```
[out:json][timeout:25];
area["name"="Lausanne"]->.searchArea;
( way["highway"~"^(motorway|trunk|primary|secondary|tertiary|unclassified|residential|living_street)$"](area.searchArea);
);
out geom;
```

Then export as GeoJSON, save in `data/lausanne_roads.geojson`
350 changes: 350 additions & 0 deletions topo-app/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
const LOCAL_ROADS = './data/lausanne_roads.geojson';
const ACTIVITIES_MANIFEST = './data/manifest.json';
const PASSWORD = 'mapsarecool';

let activeBaseLayer = null;
let roadLayer; // Declare it globally if needed outside initMap()
let stravaLayerGroup = L.layerGroup(); // Layer group for Strava traces
const stravaLayers = {
Cycling: L.layerGroup(),
HikingWalking: L.layerGroup(),
Running: L.layerGroup(),
Other: L.layerGroup()
};
const layerRegistry = {}; // global layer lookup

function initMap() {
console.log('init map')
const map = L.map('map').setView([46.5, 6.6], 10);
map.createPane('roadsPane');
map.getPane('roadsPane').style.zIndex = 390;

const topo = L.tileLayer(
'https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.pixelkarte-grau/default/current/3857/{z}/{x}/{y}.jpeg',
{attribution: '© Swisstopo', maxZoom: 18});

const world = L.tileLayer(
' https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
{attribution: '© OpenStreetMap contributors', maxZoom: 19});

const satellite = L.tileLayer(
' https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
{attribution: '© Esri', maxZoom: 18});

const hiking = L.tileLayer(
'https://wmts.geo.admin.ch/1.0.0/ch.astra.wanderland/default/current/3857/{z}/{x}/{y}.png',
{opacity: 0.7});

const cycling = L.tileLayer(
'https://wmts.geo.admin.ch/1.0.0/ch.astra.veloland/default/current/3857/{z}/{x}/{y}.png',
{opacity: 0.7});

const wanderwege = L.tileLayer(
'https://wmts.geo.admin.ch/1.0.0/ch.swisstopo.swisstlm3d-wanderwege/default/current/3857/{z}/{x}/{y}.png',
{opacity: 0.7, attribution: '© Swisstopo', maxZoom: 18});

roadLayer = L.geoJSON(
null,
{pane: 'roadsPane', style: {color: 'black', weight: 2, opacity: 0.6}});

// Add to registry
layerRegistry.topo = topo;
layerRegistry.world = world;
layerRegistry.satellite = satellite;
layerRegistry.hiking = hiking;
layerRegistry.cycling = cycling;
layerRegistry.wanderwege = wanderwege;
layerRegistry.stravaCycling = stravaLayers.Cycling;
layerRegistry.stravaHikingWalking = stravaLayers.HikingWalking;
layerRegistry.stravaRunning = stravaLayers.Running;
layerRegistry.stravaOther = stravaLayers.Other;
layerRegistry.roads = roadLayer;

topo.addTo(map);
activeBaseLayer = topo;

const baseMaps = {
'Topographic (CH)': topo,
'World': world,
'Satellite': satellite
};

const overlayMaps = {
'SwissMobile Hiking': hiking,
'SwissMobile Cycling': cycling,
'SwissTLM3D Hiking Trails': wanderwege,
'Cycling Activities': stravaLayers.Cycling,
'Hiking/Walking Activities': stravaLayers.HikingWalking,
'Running Activities': stravaLayers.Running,
'Other Activities': stravaLayers.Other,
'Lausanne Roads': roadLayer
};

const control = L.control.layers(baseMaps, overlayMaps).addTo(map);
const originalUpdate = control._update;
control._update = function() {
originalUpdate.call(this);
setTimeout(() => addLayerControls(control), 0);
};

map.on('baselayerchange', function(e) {
activeBaseLayer = e.layer;
console.log('Base layer changed to:', e.name);
});

return map;
}

function addLayerControls(control) {
const container = control.getContainer();
container.querySelectorAll('input.opacity-slider, input.linewidth-slider')
.forEach(el => el.remove());

const overlays =
container.querySelectorAll('.leaflet-control-layers-overlays label');
const bases =
container.querySelectorAll('.leaflet-control-layers-base label');
const allLabels = [...bases, ...overlays];

allLabels.forEach(label => {
const text = label.textContent.trim().toLowerCase();
let key = null;

if (text.includes('world')) key = 'world';
if (text.includes('topographic')) key = 'topo';
if (text.includes('satellite')) key = 'satellite';
if (text.includes('swisstlm3d')) key = 'wanderwege';
if (text.includes('swissmobile hiking')) key = 'hiking';
if (text.includes('swissmobile cycling')) key = 'cycling';
if (text.includes('cycling activities')) key = 'stravaCycling';
if (text.includes('hiking/walking activities')) key = 'stravaHikingWalking';
if (text.includes('running activities')) key = 'stravaRunning';
if (text.includes('other activities')) key = 'stravaOther';
if (text.includes('lausanne')) key = 'roads';

const layer = layerRegistry[key];
if (!key || !layer) return;

// Opacity slider
const opacitySlider = document.createElement('input');
opacitySlider.className = 'opacity-slider';
opacitySlider.type = 'range';
opacitySlider.min = 0;
opacitySlider.max = 1;
opacitySlider.step = 0.05;
opacitySlider.value = 1.0;
opacitySlider.style.marginLeft = '8px';
opacitySlider.style.width = '70px';
opacitySlider.title = 'Opacity';

opacitySlider.addEventListener('input', () => {
const value = parseFloat(opacitySlider.value);
if (typeof layer.setOpacity === 'function') {
layer.setOpacity(value);
} else if (layer.eachLayer) {
layer.eachLayer(l => {
if (typeof l.setStyle === 'function') {
l.setStyle({opacity: value, fillOpacity: value});
}
});
}
});

label.appendChild(opacitySlider);

// Line width slider (only for strava and roads)
const supportsLineWidth = key.startsWith('strava') || key === 'roads';
if (supportsLineWidth) {
const widthSlider = document.createElement('input');
widthSlider.className = 'linewidth-slider';
widthSlider.type = 'range';
widthSlider.min = 1;
widthSlider.max = 10;
widthSlider.step = 1;
widthSlider.value = 3;
widthSlider.style.marginLeft = '6px';
widthSlider.style.width = '60px';
widthSlider.title = 'Line Width';

widthSlider.addEventListener('input', () => {
const weight = parseInt(widthSlider.value);
if (layer.eachLayer) {
layer.eachLayer(l => {
if (typeof l.setStyle === 'function') {
l.setStyle({weight});
}
});
} else if (typeof layer.setStyle === 'function') {
layer.setStyle({weight});
}
});

label.appendChild(widthSlider);
}
});
}

function drawActivities(map, activities, filterType = null) {
// Clear old
Object.values(stravaLayers).forEach(layer => layer.clearLayers());

// Group by type
const groups = {Cycling: [], HikingWalking: [], Running: [], Other: []};

// Plot them
let skipped = 0;
activities.forEach(activity => {
const polylineStr = activity.map?.summary_polyline;
if (!polylineStr || polylineStr.trim() === '') {
console.warn(
`⛔ Skipping "${activity.name}" (ID: ${activity.id}) – no polyline`);
skipped++;
return;
}
let coords;
try {
coords = polyline.decode(polylineStr);
} catch (e) {
console.warn(`Skipping bad polyline for activity "${activity.name}"`);
return;
}
if (!coords || coords.length === 0) return;
const latlngs = coords.map(([lat, lng]) => [lat, lng]);

let group = 'Other';
let color = 'purple';
if (activity.type === 'Ride') {
group = 'Cycling';
color = 'blue';
} else if (activity.type === 'Run') {
group = 'Running';
color = 'red';
} else if (activity.type === 'Hike' || activity.type === 'Walk') {
group = 'HikingWalking';
color = 'green';
}

const polylineLayer =
L.polyline(latlngs, {color: color, weight: 3, opacity: 0.8});

const popupContent = `
<b>${activity.name}</b><br>
Type: ${activity.type}<br>
Distance: ${activity.distance} m<br>
Elevation Gain: ${activity.total_elevation_gain} m<br>
<a href="https://www.strava.com/activities/${
activity.id}" target="_blank">
View on Strava
</a>
`;
polylineLayer.bindPopup(popupContent);

stravaLayers[group].addLayer(polylineLayer);
});
if (skipped > 0) {
console.log(`⚠️ Skipped ${skipped} activities with no geometry`);
}

// Add all to map initially
Object.values(stravaLayers).forEach(layer => layer.addTo(map));
}

async function populateDatasetSelector(map) {
try {
const response = await fetch(ACTIVITIES_MANIFEST);
if (!response.ok) {
console.error(`Failed to load manifest.json: ${response.status}`);
return;
}

const datasets = await response.json();
console.log('Loaded manifest:', datasets);

const select = document.getElementById('dataset-select');
select.innerHTML = '';

datasets.forEach((filename, idx) => {
const match = filename.match(/activities_(\d+)\.json/);
if (!match) {
console.warn(`Filename did not match expected format: ${filename}`);
return;
}

const id = match[1];

const option = document.createElement('option');
option.value = filename;
option.textContent = `Client ID: ${id}`;
if (idx === 0) option.selected = true;
select.appendChild(option);
});

if (select.value) {
console.log('Auto-loading first dataset:', select.value);
const res = await fetch(select.value);
const activities = await res.json();
drawActivities(map, activities);
}

select.addEventListener('change', async () => {
const selectedFile = select.value;
try {
const res = await fetch(selectedFile);
const activities = await res.json();
drawActivities(map, activities);
console.log(`✅ Loaded ${selectedFile}`);
} catch (err) {
console.error(`❌ Failed to load ${selectedFile}`, err);
}
});

} catch (err) {
console.error('Failed to load or parse manifest.json:', err);
}
}

document.addEventListener('DOMContentLoaded', () => {
const authContainer = document.getElementById('auth-container');
const protectedContent = document.getElementById('protected');
const submitBtn = document.getElementById('password-submit');
const input = document.getElementById('password-input');
const errorMsg = document.getElementById('error-msg');

submitBtn.addEventListener('click', () => {
const entered = input.value;
if (entered === PASSWORD) {
authContainer.style.display = 'none';
protectedContent.style.display = 'block';
setTimeout(() => {
if (window._leafletMapInstance) {
window._leafletMapInstance.invalidateSize();
}
}, 100);
} else {
errorMsg.style.display = 'block';
}
});
});


(async function main() {
const map = initMap();
window._leafletMapInstance = map;

await populateDatasetSelector(map);

try {
// Add roads
const res2 = await fetch(LOCAL_ROADS);
console.log('Fetched local roads:', res2);
if (res2.ok) {
const data = await res2.json();
roadLayer.addData(data);
console.log('✅ Loaded Lausanne roads');
} else {
console.error('❌ Failed to load roads, status:', res2.status);
}
} catch (e) {
console.log('❌ Fetch failed:', e);
}
})();
Loading