-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implemented travelTime Data Functionality #376
base: main
Are you sure you want to change the base?
Changes from all commits
fe1e1fe
28bb1e3
a58bc1a
76c933d
09b3fe0
3ff24c7
bbb2930
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,11 +14,13 @@ import { | |
ApartmentWithId, | ||
CantFindApartmentForm, | ||
QuestionForm, | ||
LocationTravelTimes, | ||
} from '@common/types/db-types'; | ||
// Import Firebase configuration and types | ||
import { auth } from 'firebase-admin'; | ||
import { Timestamp } from '@firebase/firestore-types'; | ||
import nodemailer from 'nodemailer'; | ||
import axios from 'axios'; | ||
import { db, FieldValue, FieldPath } from './firebase-config'; | ||
import { Faq } from './firebase-config/types'; | ||
import authenticate from './auth'; | ||
|
@@ -39,6 +41,8 @@ const usersCollection = db.collection('users'); | |
const pendingBuildingsCollection = db.collection('pendingBuildings'); | ||
const contactQuestionsCollection = db.collection('contactQuestions'); | ||
|
||
const travelTimesCollection = db.collection('travelTimes'); | ||
|
||
// Middleware setup | ||
const app: Express = express(); | ||
|
||
|
@@ -960,4 +964,269 @@ app.post('/api/add-contact-question', authenticate, async (req, res) => { | |
} | ||
}); | ||
|
||
interface TravelTimes { | ||
agQuadWalking: number; | ||
agQuadDriving: number; | ||
engQuadWalking: number; | ||
engQuadDriving: number; | ||
hoPlazaWalking: number; | ||
hoPlazaDriving: number; | ||
} | ||
|
||
const { REACT_APP_MAPS_API_KEY } = process.env; | ||
const LANDMARKS = { | ||
eng_quad: '42.4445,-76.4836', // Duffield Hall | ||
ag_quad: '42.4489,-76.4780', // Mann Library | ||
ho_plaza: '42.4468,-76.4851', // Ho Plaza | ||
}; | ||
|
||
interface TravelTimeInput { | ||
origin: string; // Can be either address or "latitude,longitude" | ||
} | ||
|
||
/** | ||
* getTravelTimes – Calculates travel times between an origin and multiple destinations using Google Distance Matrix API. | ||
* | ||
* @remarks | ||
* Makes an HTTP request to the Google Distance Matrix API and processes the response to extract duration values. | ||
* Times are converted from seconds to minutes before being returned. | ||
* | ||
* @param {string} origin - Starting location as either an address or coordinates in "lat,lng" format | ||
* @param {string[]} destinations - Array of destination addresses to calculate times to | ||
* @param {'walking' | 'driving'} mode - Mode of transportation to use for calculations | ||
* @return {Promise<number[]>} - Array of travel times in minutes to each destination | ||
*/ | ||
async function getTravelTimes( | ||
origin: string, | ||
destinations: string[], | ||
mode: 'walking' | 'driving' | ||
): Promise<number[]> { | ||
const response = await axios.get( | ||
`https://maps.googleapis.com/maps/api/distancematrix/json?origins=${encodeURIComponent( | ||
origin | ||
)}&destinations=${destinations | ||
.map((dest) => encodeURIComponent(dest)) | ||
.join('|')}&mode=${mode}&key=${REACT_APP_MAPS_API_KEY}` | ||
); | ||
|
||
return response.data.rows[0].elements.map( | ||
(element: { duration: { value: number } }) => element.duration.value / 60 | ||
); | ||
} | ||
|
||
/** | ||
* Travel Times Calculator - Calculates walking and driving times from a given origin to Cornell landmarks. | ||
* | ||
* @remarks | ||
* Uses Google Maps Distance Matrix API to calculate travel times to three landmarks: Engineering Quad, | ||
* Agriculture Quad, and Ho Plaza. Returns both walking and driving durations in minutes. | ||
* Origin can be either an address or coordinates in "latitude,longitude" format. | ||
* | ||
* @route POST /api/travel-times | ||
* | ||
* @input {string} req.body.origin - Starting point, either as address or "latitude,longitude" | ||
* | ||
* @status | ||
* - 200: Successfully retrieved travel times | ||
* - 400: Missing or invalid origin | ||
* - 500: Server error or Google Maps API failure | ||
*/ | ||
app.post('/api/calculate-travel-times', async (req, res) => { | ||
try { | ||
const { origin } = req.body as TravelTimeInput; | ||
console.log('Origin:', origin); | ||
|
||
if (!origin) { | ||
return res.status(400).json({ error: 'Origin is required' }); | ||
} | ||
|
||
const destinations = Object.values(LANDMARKS); | ||
console.log('Destinations array:', destinations); | ||
|
||
// Get walking and driving times using the helper function | ||
const [walkingTimes, drivingTimes] = await Promise.all([ | ||
getTravelTimes(origin, destinations, 'walking'), | ||
getTravelTimes(origin, destinations, 'driving'), | ||
]); | ||
|
||
console.log('Raw walking times:', walkingTimes); | ||
console.log('Raw driving times:', drivingTimes); | ||
|
||
const travelTimes: TravelTimes = { | ||
engQuadWalking: walkingTimes[0], | ||
engQuadDriving: drivingTimes[0], | ||
agQuadWalking: walkingTimes[1], | ||
agQuadDriving: drivingTimes[1], | ||
hoPlazaWalking: walkingTimes[2], | ||
hoPlazaDriving: drivingTimes[2], | ||
}; | ||
|
||
console.log('Final travel times:', travelTimes); | ||
return res.status(200).json(travelTimes); | ||
} catch (error) { | ||
console.error('Error calculating travel times:', error); | ||
return res.status(500).json({ error: 'Internal server error' }); | ||
} | ||
}); | ||
|
||
/** | ||
* Test Travel Times Endpoint - Creates a travel times document for a specific building. | ||
* | ||
* @remarks | ||
* Retrieves building coordinates from the buildings collection, calculates travel times to Cornell landmarks, | ||
* and stores the results in the travelTimes collection. This endpoint is used for testing and populating | ||
* travel time data for existing buildings. | ||
* | ||
* @param {string} buildingId - The ID of the building to calculate and store travel times for | ||
* | ||
* @return {Object} - Object containing success message, building ID, and calculated travel times | ||
*/ | ||
app.post('/api/test-travel-times/:buildingId', async (req, res) => { | ||
try { | ||
const { buildingId } = req.params; | ||
|
||
// Get building data | ||
const buildingDoc = await buildingsCollection.doc(buildingId).get(); | ||
if (!buildingDoc.exists) { | ||
return res.status(404).json({ error: 'Building not found' }); | ||
} | ||
|
||
const buildingData = buildingDoc.data(); | ||
if (!buildingData?.latitude || !buildingData?.longitude) { | ||
return res.status(400).json({ error: 'Building missing coordinate data' }); | ||
} | ||
|
||
// Calculate travel times using the main endpoint | ||
const response = await axios.post(`http://localhost:3000/api/calculate-travel-times`, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can delete |
||
origin: `${buildingData.latitude},${buildingData.longitude}`, | ||
}); | ||
|
||
// Store in Firebase | ||
await travelTimesCollection.doc(buildingId).set(response.data); | ||
|
||
return res.status(200).json({ | ||
message: 'Travel times calculated and stored successfully', | ||
buildingId, | ||
travelTimes: response.data, | ||
}); | ||
} catch (error) { | ||
console.error('Error in test endpoint:', error); | ||
return res.status(500).json({ error: 'Internal server error' }); | ||
} | ||
}); | ||
/** | ||
* Batch Travel Times Endpoint - Calculates and stores travel times for a batch of buildings. | ||
* | ||
* @remarks | ||
* Processes a batch of buildings from the buildings collection, calculating travel times to Cornell landmarks | ||
* for each one and storing the results in the travelTimes collection. This endpoint handles buildings in batches | ||
* with rate limiting between requests. Supports pagination through the startAfter parameter. | ||
* | ||
* @param {number} batchSize - Number of buildings to process in this batch (defaults to 50) | ||
* @param {string} [startAfter] - Optional ID of last processed building for pagination | ||
* @return {Object} - Summary object containing: | ||
* - Message indicating batch completion | ||
* - Total number of buildings processed in this batch | ||
* - Count of successful calculations | ||
* - Arrays of successful building IDs and failed buildings with error details | ||
*/ | ||
|
||
// Helper function for rate limiting API requests | ||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); | ||
|
||
app.post('/api/batch-create-travel-times/:batchSize/:startAfter?', async (req, res) => { | ||
try { | ||
const batchSize = parseInt(req.params.batchSize, 10) || 50; | ||
const { startAfter } = req.params; | ||
|
||
let query = buildingsCollection.limit(batchSize); | ||
if (startAfter) { | ||
const lastDoc = await buildingsCollection.doc(startAfter).get(); | ||
query = query.startAfter(lastDoc); | ||
} | ||
|
||
const buildingDocs = (await query.get()).docs; | ||
const results = { | ||
success: [] as string[], | ||
failed: [] as { id: string; error: string }[], | ||
}; | ||
|
||
const processPromises = buildingDocs.map(async (doc) => { | ||
try { | ||
await delay(100); // 100ms delay between requests | ||
const buildingData = doc.data(); | ||
if (!buildingData?.latitude || !buildingData?.longitude) { | ||
results.failed.push({ | ||
id: doc.id, | ||
error: 'Missing coordinates', | ||
}); | ||
return; | ||
} | ||
|
||
const response = await axios.post(`http://localhost:3000/api/calculate-travel-times`, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly, you can delete |
||
origin: `${buildingData.latitude},${buildingData.longitude}`, | ||
}); | ||
|
||
await travelTimesCollection.doc(doc.id).set(response.data); | ||
results.success.push(doc.id); | ||
} catch (error) { | ||
results.failed.push({ | ||
id: doc.id, | ||
error: error instanceof Error ? error.message : 'Unknown error', | ||
}); | ||
} | ||
}); | ||
|
||
await Promise.all(processPromises); | ||
|
||
res.status(200).json({ | ||
message: 'Batch processing completed', | ||
totalProcessed: buildingDocs.length, | ||
successCount: results.success.length, | ||
failureCount: results.failed.length, | ||
hasMore: buildingDocs.length === batchSize, | ||
lastProcessedId: buildingDocs[buildingDocs.length - 1]?.id, | ||
results, | ||
}); | ||
} catch (error) { | ||
console.error('Batch processing error:', error); | ||
res.status(500).json({ error: 'Internal server error' }); | ||
} | ||
}); | ||
|
||
/** | ||
* Get Travel Times By Building ID - Retrieves pre-calculated travel times from the travel times collection. | ||
* | ||
* @remarks | ||
* Looks up the travel times document for the given building ID and returns the stored walking and driving | ||
* times to Cornell landmarks: Engineering Quad, Agriculture Quad, and Ho Plaza. | ||
* | ||
* @route GET /api/travel-times/:buildingId | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. adjust to |
||
* | ||
* @input {string} req.params.buildingId - ID of the building to get travel times for | ||
* | ||
* @status | ||
* - 200: Successfully retrieved travel times | ||
* - 404: Building travel times not found | ||
* - 500: Server error | ||
*/ | ||
app.get('/api/travel-times-by-id/:buildingId', async (req, res) => { | ||
try { | ||
const { buildingId } = req.params; | ||
|
||
const travelTimeDoc = await travelTimesCollection.doc(buildingId).get(); | ||
|
||
if (!travelTimeDoc.exists) { | ||
return res.status(404).json({ error: 'Travel times not found for this building' }); | ||
} | ||
|
||
const travelTimes = travelTimeDoc.data() as LocationTravelTimes; | ||
|
||
return res.status(200).json(travelTimes); | ||
} catch (error) { | ||
console.error('Error retrieving travel times:', error); | ||
return res.status(500).json({ error: 'Internal server error' }); | ||
} | ||
}); | ||
|
||
export default app; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you already imported LocationTravelTimes, you can delete TravelTimes and use the imported type.