snowva-backend / src /controllers /trackController.ts
zerito's picture
Deploy backend update Tue Jan 20 20:36:55 CET 2026
fefce67
Raw
History Blame Contribute Delete
13.7 kB
// import { Request, Response } from 'express';
// import { TrackModel } from '../models/TrackModel';
// import { TrackFeature, TrackFeatureCollection } from '../types/types';
// /**
// * TrackController - Handles HTTP requests for public ski tracks
// */
// export class TrackController {
// private static parseOptionalString(value: unknown): string | undefined {
// if (typeof value !== 'string') return undefined;
// const trimmed = value.trim();
// if (!trimmed || trimmed === 'undefined' || trimmed === 'null') return undefined;
// return trimmed;
// }
// private static normalizeStringList(values: unknown[]): string[] | undefined {
// const normalized = values
// .filter((v): v is string => typeof v === 'string')
// .map(v => v.trim().toLowerCase())
// .filter(v => v.length > 0 && v !== 'undefined' && v !== 'null');
// if (normalized.length === 0) return undefined;
// return Array.from(new Set(normalized));
// }
// private static parseActivitiesParam(value: unknown): string[] | undefined {
// if (Array.isArray(value)) {
// return TrackController.normalizeStringList(value);
// }
// if (typeof value === 'string') {
// return TrackController.normalizeStringList(value.split(','));
// }
// return undefined;
// }
// /**
// * GET /api/tracks
// * Fetch all public tracks with optional location filtering and pagination
// */
// static async getTracks(req: Request, res: Response): Promise<void> {
// console.log('--- Start getTracks Controller ---');
// try {
// const { lat, lng, radius, q, activities, difficulty } = req.query;
// console.log(`Received query parameters: lat=${lat}, lng=${lng}, radius=${radius}, q=${q}, activities=${activities}, difficulty=${difficulty}`);
// // Prepare filters for DB query
// const filters: { activities?: string[], searchQuery?: string, difficulty?: string } = {};
// const parsedActivities = TrackController.parseActivitiesParam(activities);
// if (parsedActivities) {
// filters.activities = parsedActivities;
// }
// const parsedQuery = TrackController.parseOptionalString(q);
// if (parsedQuery) {
// filters.searchQuery = parsedQuery;
// }
// const parsedDifficulty = TrackController.parseOptionalString(difficulty);
// if (parsedDifficulty) {
// filters.difficulty = parsedDifficulty;
// }
// // Pagination (must be applied at DB level to avoid huge Supabase egress)
// const page = Math.max(1, parseInt(req.query.page as string) || 1);
// const requestedLimit = parseInt(req.query.limit as string) || 100;
// const limit = Math.min(Math.max(1, requestedLimit), 200);
// const offset = (page - 1) * limit;
// const locationFilter = (lat && lng && radius)
// ? {
// lat: parseFloat(lat as string),
// lng: parseFloat(lng as string),
// radiusKm: parseFloat(radius as string),
// }
// : undefined;
// // Fetch filtered + paginated tracks from DB
// const { tracks, total } = await TrackModel.findWithFiltersPaginated(
// filters,
// { limit, offset },
// locationFilter
// );
// console.log(`Fetched ${tracks.length} tracks from the database (after DB filtering + pagination). Total matching: ${total}.`);
// // Convert tracks to GeoJSON features
// const features: TrackFeature[] = tracks.map(track => {
// return {
// type: 'Feature',
// geometry: typeof track.geojson === 'string'
// ? JSON.parse(track.geojson as any)
// : track.geojson,
// properties: {
// id: track.id,
// external_id: track.external_id,
// name: track.name,
// description: track.description,
// ...track.properties,
// difficulty: track.difficulty,
// difficulty_convention: track.difficulty_convention,
// type: track.type,
// uses: track.uses,
// ref: track.ref,
// oneway: track.oneway,
// gladed: track.gladed,
// patrolled: track.patrolled,
// lit: track.lit,
// grooming: track.grooming,
// status: track.status,
// websites: track.websites,
// wikidata_id: track.wikidata_id,
// elevation_profile: track.elevation_profile
// }
// };
// });
// const totalPages = Math.ceil(total / limit);
// console.log(`Response features count: ${features.length}. Total pages: ${totalPages}.`);
// const response: TrackFeatureCollection & { pagination: any } = {
// type: 'FeatureCollection',
// features,
// pagination: {
// total,
// page,
// limit,
// pages: totalPages
// }
// };
// res.json(response);
// console.log('--- End getTracks Controller (Success) ---');
// } catch (err) {
// console.error('Error in getTracks:', err);
// res.status(500).json({ error: 'Server Error' });
// console.log('--- End getTracks Controller (Error) ---');
// }
// }
// /**
// * GET /api/tracks/:id
// * Fetch a single track by ID
// */
// static async getTrackById(req: Request, res: Response): Promise<void> {
// console.log('--- Start getTrackById Controller ---');
// try {
// const id = parseInt(req.params.id);
// console.log(`Fetching track with ID: ${id}`);
// const track = await TrackModel.findById(id);
// if (!track) {
// console.log(`Track with ID ${id} not found.`);
// res.status(404).json({ error: 'Track not found' });
// return;
// }
// const feature: TrackFeature = {
// type: 'Feature',
// geometry: typeof track.geojson === 'string'
// ? JSON.parse(track.geojson)
// : track.geojson,
// properties: {
// id: track.id,
// name: track.name,
// description: track.description,
// ...track.properties,
// difficulty: track.difficulty,
// type: track.type
// }
// };
// res.json(feature);
// console.log('--- End getTrackById Controller (Success) ---');
// } catch (err) {
// console.error('Error in getTrackById:', err);
// res.status(500).json({ error: 'Server Error' });
// console.log('--- End getTrackById Controller (Error) ---');
// }
// }
// }
import { Request, Response } from 'express';
import { TrackModel } from '../models/TrackModel';
import { TrackFeature, TrackFeatureCollection } from '../types/types';
import { getDistanceFromLatLonInKm } from '../utils/geoUtils';
/**
* TrackController - Handles HTTP requests for public ski tracks
*/
export class TrackController {
/**
* GET /api/tracks
* Fetch all public tracks with optional location filtering and pagination
*/
static async getTracks(req: Request, res: Response): Promise<void> {
console.log('--- Start getTracks Controller ---');
try {
const { lat, lng, radius } = req.query;
console.log(`Received query parameters: lat=${lat}, lng=${lng}, radius=${radius}`);
const tracks = await TrackModel.findAll();
console.log(`Fetched ${tracks.length} tracks from the database.`);
// Convert tracks to GeoJSON features
let features: TrackFeature[] = tracks.map(track => {
return {
type: 'Feature',
geometry: typeof track.geojson === 'string'
? JSON.parse(track.geojson)
: track.geojson,
properties: {
id: track.id,
name: track.name,
description: track.description,
...track.properties,
difficulty: track.difficulty,
type: track.type
}
};
});
console.log(`Initial features count after mapping: ${features.length}`);
// Filter by location if provided
if (lat && lng && radius) {
console.log('Location filtering is active.');
const centerLat = parseFloat(lat as string);
const centerLng = parseFloat(lng as string);
const radiusKm = parseFloat(radius as string);
console.log(`Filter criteria: Center Lat=${centerLat}, Lng=${centerLng}, Radius=${radiusKm} km.`);
const initialFeatureCount = features.length;
features = features.filter(feature => {
if (!feature.geometry || !feature.geometry.coordinates || feature.geometry.coordinates.length === 0) {
return false;
}
// Use the first point of the track for distance check
// GeoJSON coordinates are [lng, lat]
const trackPoint = feature.geometry.coordinates[0];
const trackLng = trackPoint[0];
const trackLat = trackPoint[1];
const dist = getDistanceFromLatLonInKm(centerLat, centerLng, trackLat, trackLng);
return dist <= radiusKm;
});
console.log(`Features remaining after location filter: ${features.length}. Filtered out ${initialFeatureCount - features.length} features.`);
} else {
console.log('No location filter applied.');
}
// Pagination
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 100;
const startIndex = (page - 1) * limit;
const endIndex = page * limit;
console.log(`Pagination parameters: Page=${page}, Limit=${limit}. Total features: ${features.length}`);
console.log(`Slicing features from index ${startIndex} to ${endIndex}.`);
const paginatedFeatures = features.slice(startIndex, endIndex);
const totalPages = Math.ceil(features.length / limit);
console.log(`Response features count: ${paginatedFeatures.length}. Total pages: ${totalPages}.`);
const response: TrackFeatureCollection & { pagination: any } = {
type: 'FeatureCollection',
features: paginatedFeatures,
pagination: {
total: features.length,
page,
limit,
pages: totalPages
}
};
res.json(response);
console.log('--- End getTracks Controller (Success) ---');
} catch (err) {
console.error('Error in getTracks:', err);
res.status(500).json({ error: 'Server Error' });
console.log('--- End getTracks Controller (Error) ---');
}
}
/**
* GET /api/tracks/:id
* Fetch a single track by ID
*/
static async getTrackById(req: Request, res: Response): Promise<void> {
console.log('--- Start getTrackById Controller ---');
try {
const id = parseInt(req.params.id);
console.log(`Fetching track with ID: ${id}`);
const track = await TrackModel.findById(id);
if (!track) {
console.log(`Track with ID ${id} not found.`);
res.status(404).json({ error: 'Track not found' });
return;
}
const feature: TrackFeature = {
type: 'Feature',
geometry: typeof track.geojson === 'string'
? JSON.parse(track.geojson)
: track.geojson,
properties: {
id: track.id,
name: track.name,
description: track.description,
...track.properties,
difficulty: track.difficulty,
type: track.type
}
};
res.json(feature);
console.log('--- End getTrackById Controller (Success) ---');
} catch (err) {
console.error('Error in getTrackById:', err);
res.status(500).json({ error: 'Server Error' });
console.log('--- End getTrackById Controller (Error) ---');
}
}
}