All checks were successful
CI / Validate i18n Files (pull_request) Successful in 19s
Dependency Review / Review Dependencies (pull_request) Successful in 26s
CI / TypeScript Type Check (pull_request) Successful in 1m20s
CI / Lint Code (pull_request) Successful in 1m37s
CI / Build Project (pull_request) Successful in 1m32s
CI / Security Checks (pull_request) Successful in 1m35s
CI / Run Tests (Node 20) (pull_request) Successful in 1m42s
CI / Run Tests (Node 18) (pull_request) Successful in 1m49s
Code Quality / Code Quality Checks (pull_request) Successful in 1m57s
CI / Test Coverage (pull_request) Successful in 1m32s
- Created example-shared-components.html to demonstrate TypeScript-based shared header and footer components. - Added original-style.css for theming with CSS variables and dark mode support. - Introduced style-backup.css for legacy styles. - Developed test-refactored.html for testing map components with Leaflet integration. - Updated deployment documentation to reflect changes in log file paths and service names. - Renamed project from "great-lakes-ice-report" to "icewatch" in package.json and package-lock.json. - Updated Caddyfile for new log file path. - Added S3 bucket policy for public read access to greatlakes-conditions. - Removed old service file and created new systemd service for icewatch.
221 lines
No EOL
8.2 KiB
JavaScript
221 lines
No EOL
8.2 KiB
JavaScript
/**
|
|
* Nominatim-only implementation using MapBase
|
|
*/
|
|
class NominatimMapApp extends MapBase {
|
|
constructor() {
|
|
super();
|
|
this.initializeMap();
|
|
}
|
|
|
|
/**
|
|
* Initialize the Leaflet map
|
|
*/
|
|
initializeMap() {
|
|
this.map = L.map('map').setView([42.9634, -85.6681], 10);
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 18,
|
|
}).addTo(this.map);
|
|
}
|
|
|
|
/**
|
|
* Create custom marker for location
|
|
*/
|
|
createMarker(location) {
|
|
const isPersistent = !!location.persistent;
|
|
const timeAgo = window.getTimeAgo ? window.getTimeAgo(location.created_at) : '';
|
|
const persistentText = isPersistent ? '<br><small><strong>📌 Persistent Report</strong></small>' : '';
|
|
|
|
const customIcon = this.createCustomIcon(isPersistent);
|
|
|
|
const marker = L.marker([location.latitude, location.longitude], {
|
|
icon: customIcon
|
|
})
|
|
.addTo(this.map)
|
|
.bindPopup(`<strong>${location.address}</strong><br>${location.description || 'No additional details'}<br><small>Reported ${timeAgo}</small>${persistentText}`);
|
|
|
|
return marker;
|
|
}
|
|
|
|
/**
|
|
* Create custom icon for markers
|
|
*/
|
|
createCustomIcon(isPersistent = false) {
|
|
const iconColor = isPersistent ? '#28a745' : '#dc3545'; // Green for persistent, red for temporary
|
|
const iconSymbol = isPersistent ? '🔒' : '⚠️'; // Lock for persistent, warning for temporary
|
|
|
|
return L.divIcon({
|
|
className: 'custom-marker',
|
|
html: `
|
|
<div style="
|
|
background-color: ${iconColor};
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
border: 3px solid white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
|
cursor: pointer;
|
|
">${iconSymbol}</div>
|
|
`,
|
|
iconSize: [30, 30],
|
|
iconAnchor: [15, 15],
|
|
popupAnchor: [0, -15]
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Search for addresses using Nominatim
|
|
*/
|
|
async searchAddress(query) {
|
|
try {
|
|
// Generate multiple search variations for better results
|
|
const searches = [
|
|
query, // Original query
|
|
query.replace(' & ', ' and '), // Replace & with 'and'
|
|
query.replace(' and ', ' & '), // Replace 'and' with &
|
|
query.replace(' at ', ' & '), // Replace 'at' with &
|
|
`${query}, Michigan`, // Add Michigan if not present
|
|
`${query}, MI`, // Add MI if not present
|
|
];
|
|
|
|
// Add street type variations
|
|
const streetVariations = [];
|
|
searches.forEach(search => {
|
|
streetVariations.push(search);
|
|
streetVariations.push(search.replace(' St ', ' Street '));
|
|
streetVariations.push(search.replace(' Street ', ' St '));
|
|
streetVariations.push(search.replace(' Ave ', ' Avenue '));
|
|
streetVariations.push(search.replace(' Avenue ', ' Ave '));
|
|
streetVariations.push(search.replace(' Rd ', ' Road '));
|
|
streetVariations.push(search.replace(' Road ', ' Rd '));
|
|
streetVariations.push(search.replace(' Blvd ', ' Boulevard '));
|
|
streetVariations.push(search.replace(' Boulevard ', ' Blvd '));
|
|
});
|
|
|
|
// Remove duplicates
|
|
const uniqueSearches = [...new Set(streetVariations)];
|
|
|
|
// Try each search variation until we get results
|
|
for (const searchQuery of uniqueSearches) {
|
|
try {
|
|
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&extratags=1&limit=5&q=${encodeURIComponent(searchQuery)}`);
|
|
const data = await response.json();
|
|
|
|
if (data && data.length > 0) {
|
|
// Prefer results that are in Michigan/Grand Rapids
|
|
const michiganResults = data.filter(item =>
|
|
item.display_name.toLowerCase().includes('michigan') ||
|
|
item.display_name.toLowerCase().includes('grand rapids') ||
|
|
item.display_name.toLowerCase().includes(', mi')
|
|
);
|
|
|
|
const results = michiganResults.length > 0 ? michiganResults : data;
|
|
this.showAutocomplete(results.slice(0, 5));
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Geocoding failed for: "${searchQuery}"`, error);
|
|
}
|
|
}
|
|
|
|
// If no results found, show empty autocomplete
|
|
this.showAutocomplete([]);
|
|
|
|
} catch (error) {
|
|
console.error('Error searching addresses:', error);
|
|
this.showAutocomplete([]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle form submission with enhanced geocoding
|
|
*/
|
|
async handleSubmit() {
|
|
const address = this.addressInput.value.trim();
|
|
const description = this.descriptionInput.value.trim();
|
|
|
|
if (!address) {
|
|
this.showMessage('Please enter an address', 'error');
|
|
return;
|
|
}
|
|
|
|
// If coordinates are not set, try to geocode the address
|
|
let lat = parseFloat(this.addressInput.dataset.lat);
|
|
let lon = parseFloat(this.addressInput.dataset.lon);
|
|
|
|
if (!lat || !lon || isNaN(lat) || isNaN(lon)) {
|
|
// Try to geocode the current address value
|
|
const geocodeResult = await this.geocodeAddress(address);
|
|
if (geocodeResult) {
|
|
lat = parseFloat(geocodeResult.lat);
|
|
lon = parseFloat(geocodeResult.lon);
|
|
} else {
|
|
this.showMessage('Please select a valid address from the suggestions', 'error');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Disable submit button
|
|
this.submitButton.disabled = true;
|
|
this.submitButton.innerHTML = '<span class="loading-spinner"></span> Submitting...';
|
|
|
|
try {
|
|
const response = await fetch('/api/locations', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
address,
|
|
latitude: lat,
|
|
longitude: lon,
|
|
description
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
this.showMessage('Location reported successfully!', 'success');
|
|
this.resetForm();
|
|
this.refreshLocations();
|
|
} else {
|
|
this.handleSubmitError(data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error submitting location:', error);
|
|
this.showMessage('Error submitting location. Please try again.', 'error');
|
|
} finally {
|
|
this.submitButton.disabled = false;
|
|
this.submitButton.innerHTML = '<i class="fas fa-flag"></i> Report Ice';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Geocode an address using Nominatim
|
|
*/
|
|
async geocodeAddress(address) {
|
|
try {
|
|
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&limit=1&q=${encodeURIComponent(address + ', Michigan')}`);
|
|
const data = await response.json();
|
|
|
|
if (data && data.length > 0) {
|
|
return data[0];
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error geocoding address:', error);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize the app when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
new NominatimMapApp();
|
|
}); |