130 lines
No EOL
4.7 KiB
TypeScript
130 lines
No EOL
4.7 KiB
TypeScript
import https from 'https';
|
|
import { Location } from '../types';
|
|
|
|
interface MapOptions {
|
|
width: number;
|
|
height: number;
|
|
padding?: number; // Reintroduced padding property
|
|
}
|
|
|
|
export class MapImageService {
|
|
private defaultOptions: MapOptions = {
|
|
width: 800,
|
|
height: 600
|
|
};
|
|
|
|
/**
|
|
* Generate a static map image using Mapbox Static Maps API
|
|
*/
|
|
async generateMapImage(locations: Location[], options: Partial<MapOptions> = {}): Promise<Buffer> {
|
|
const opts = { ...this.defaultOptions, ...options };
|
|
|
|
console.info('Generating Mapbox static map focused on location data');
|
|
console.info('Canvas size:', opts.width, 'x', opts.height);
|
|
console.info('Number of locations:', locations.length);
|
|
|
|
const mapboxBuffer = await this.fetchMapboxStaticMapAutoFit(opts, locations);
|
|
|
|
if (mapboxBuffer) {
|
|
return mapboxBuffer;
|
|
} else {
|
|
// Return a simple error image if Mapbox fails
|
|
return this.generateErrorImage(opts);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch map using Mapbox Static Maps API with auto-fit to location data
|
|
*/
|
|
private async fetchMapboxStaticMapAutoFit(options: MapOptions, locations: Location[]): Promise<Buffer | null> {
|
|
const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN;
|
|
if (!mapboxToken) {
|
|
console.error('No Mapbox token available');
|
|
return null;
|
|
}
|
|
|
|
// Build overlay string for all locations with correct format
|
|
let overlays = '';
|
|
locations.forEach((location, index) => {
|
|
if (location.latitude && location.longitude) {
|
|
console.info(`Location ${index + 1}: ${location.latitude}, ${location.longitude} (${location.address})`);
|
|
// Correct format: pin-s-label+color(lng,lat)
|
|
const color = location.persistent ? 'ff9800' : 'ff0000'; // Orange for persistent, red for regular
|
|
const label = (index + 1).toString();
|
|
overlays += `pin-s-${label}+${color}(${location.longitude},${location.latitude}),`;
|
|
}
|
|
});
|
|
|
|
// Remove trailing comma
|
|
overlays = overlays.replace(/,$/, '');
|
|
|
|
console.info('Generated overlays string:', overlays);
|
|
|
|
// Build Mapbox Static Maps URL with auto-fit
|
|
let mapboxUrl;
|
|
if (overlays) {
|
|
// Use auto-fit to center on all pins
|
|
mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/${overlays}/auto/${options.width}x${options.height}?access_token=${mapboxToken}`;
|
|
} else {
|
|
// No locations, use Grand Rapids as fallback
|
|
const fallbackLat = 42.960081464833195;
|
|
const fallbackLng = -85.67402711517647;
|
|
mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/${fallbackLng},${fallbackLat},10/${options.width}x${options.height}?access_token=${mapboxToken}`;
|
|
}
|
|
|
|
console.info('Fetching Mapbox static map with auto-fit...');
|
|
console.info('URL:', mapboxUrl.replace(mapboxToken, 'TOKEN_HIDDEN'));
|
|
|
|
return new Promise((resolve) => {
|
|
const request = https.get(mapboxUrl, { timeout: 10000 }, (response) => {
|
|
if (response.statusCode === 200) {
|
|
const chunks: Buffer[] = [];
|
|
response.on('data', (chunk) => chunks.push(chunk));
|
|
response.on('end', () => {
|
|
console.info('Mapbox static map fetched successfully');
|
|
resolve(Buffer.concat(chunks));
|
|
});
|
|
} else {
|
|
console.error('Mapbox API error:', response.statusCode);
|
|
resolve(null);
|
|
}
|
|
});
|
|
|
|
request.on('error', (err) => {
|
|
console.error('Error fetching Mapbox map:', err.message);
|
|
resolve(null);
|
|
});
|
|
|
|
request.on('timeout', () => {
|
|
console.error('Mapbox request timeout');
|
|
request.destroy();
|
|
resolve(null);
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Generate a simple error image when Mapbox fails
|
|
*/
|
|
private generateErrorImage(options: MapOptions): Buffer {
|
|
// Generate a simple 1x1 transparent PNG as fallback
|
|
// This is a valid PNG header + IHDR + IDAT + IEND for a 1x1 transparent pixel
|
|
const transparentPng = Buffer.from([
|
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
|
|
0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 dimensions
|
|
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, // RGBA, no compression
|
|
0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, // IDAT chunk
|
|
0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, // Compressed data
|
|
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, // (transparent pixel)
|
|
0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, // IEND chunk
|
|
0x42, 0x60, 0x82
|
|
]);
|
|
|
|
console.info('Generated transparent PNG fallback due to Mapbox failure');
|
|
return transparentPng;
|
|
}
|
|
}
|
|
|
|
export default MapImageService; |