// 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 { // 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 { // 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 { 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 { 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) ---'); } } }