Skip to content

Commit

Permalink
Merge pull request #376 from cornell-dti/map-backend
Browse files Browse the repository at this point in the history
Implemented travelTime Data Functionality
  • Loading branch information
ggsawatyanon authored Dec 4, 2024
2 parents c5e088d + 8fa5942 commit c479c9b
Show file tree
Hide file tree
Showing 6 changed files with 416 additions and 143 deletions.
2 changes: 0 additions & 2 deletions backend/scripts/add_buildings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ const formatBuilding = ({
area: getAreaType(area),
latitude,
longitude,
walkTime: 0,
driveTime: 0,
});

const makeBuilding = async (apartmentWithId: ApartmentWithId) => {
Expand Down
260 changes: 260 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();

Expand Down Expand Up @@ -980,4 +984,260 @@ app.post('/api/add-contact-question', authenticate, async (req, res) => {
}
});

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: LocationTravelTimes = {
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(`/api/calculate-travel-times`, {
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(`/api/calculate-travel-times`, {
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-by-id/:buildingId
*
* @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;
11 changes: 9 additions & 2 deletions common/types/db-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,15 @@ export type Apartment = {
readonly area: 'COLLEGETOWN' | 'WEST' | 'NORTH' | 'DOWNTOWN' | 'OTHER';
readonly latitude: number;
readonly longitude: number;
readonly walkTime: number;
readonly driveTime: number;
};

export type LocationTravelTimes = {
agQuadDriving: number;
agQuadWalking: number;
engQuadDriving: number;
engQuadWalking: number;
hoPlazaDriving: number;
hoPlazaWalking: number;
};

export type ApartmentWithId = Apartment & Id;
Expand Down
Loading

0 comments on commit c479c9b

Please sign in to comment.