Browse Source
Fix geocache markers disappearing when in proximity
Fix geocache markers disappearing when in proximity
- Removed icon switching between read/unread states - Fixed CSS conflict between transform transition and pulse animation - Changed proximity detection to directly toggle DOM classes instead of recreating icons - Now uses classList.add/remove on marker._icon instead of setIcon() to prevent rendering issues 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>master
20 changed files with 10689 additions and 3648 deletions
-
8.claude/settings.local.json
-
19Dockerfile
-
1433Test.kml
-
492default.kml
-
447default.kml.backup.2025-12-30T14-07-34-870Z
-
447default.kml.backup.2025-12-30T14-08-13-130Z
-
611default.kml.backup.2025-12-30T14-08-58-379Z
-
1161default.kml.backup.2025-12-30T14-11-32-501Z
-
1168default.kml.backup.2025-12-30T14-12-39-684Z
-
604default.kml.backup.2025-12-30T14-18-50-550Z
-
1168default.kml.backup.2025-12-30T14-19-18-638Z
-
1168default.kml.backup.2025-12-30T14-19-29-904Z
-
1168default.kml.backup.2025-12-30T14-36-41-318Z
-
1161default.kml.backup.2025-12-30T18-14-18-396Z
-
12docker-compose.yml
-
21geocaches.json
-
1852index.html
-
17package.json
-
315server.js
-
1063tracks-export (1).kml
@ -0,0 +1,19 @@ |
|||
FROM node:18-alpine |
|||
|
|||
WORKDIR /app |
|||
|
|||
# Copy package files |
|||
COPY package*.json ./ |
|||
|
|||
# Install dependencies |
|||
RUN npm install |
|||
|
|||
# Copy application files |
|||
COPY server.js ./ |
|||
COPY index.html ./ |
|||
# Copy default.kml if it exists (optional) |
|||
COPY default.kml* ./ |
|||
|
|||
EXPOSE 8080 |
|||
|
|||
CMD ["node", "server.js"] |
|||
1433
Test.kml
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
492
default.kml
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
447
default.kml.backup.2025-12-30T14-07-34-870Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
447
default.kml.backup.2025-12-30T14-08-13-130Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
611
default.kml.backup.2025-12-30T14-08-58-379Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1161
default.kml.backup.2025-12-30T14-11-32-501Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1168
default.kml.backup.2025-12-30T14-12-39-684Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
604
default.kml.backup.2025-12-30T14-18-50-550Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1168
default.kml.backup.2025-12-30T14-19-18-638Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1168
default.kml.backup.2025-12-30T14-19-29-904Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1168
default.kml.backup.2025-12-30T14-36-41-318Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1161
default.kml.backup.2025-12-30T18-14-18-396Z
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,9 +1,11 @@ |
|||
services: |
|||
web: |
|||
image: nginx:alpine |
|||
hikemap: |
|||
build: . |
|||
ports: |
|||
- "8080:80" |
|||
- "880:8080" |
|||
volumes: |
|||
- ./index.html:/usr/share/nginx/html/index.html:ro |
|||
- ./default.kml:/usr/share/nginx/html/default.kml:ro |
|||
- ./index.html:/app/index.html:ro |
|||
- ./:/app/data |
|||
restart: unless-stopped |
|||
environment: |
|||
- NODE_ENV=production |
|||
@ -0,0 +1,21 @@ |
|||
[ |
|||
{ |
|||
"id": "gc_1767115055219_ge0toyjos", |
|||
"lat": 30.5253513240288, |
|||
"lng": -97.83657789230348, |
|||
"messages": [ |
|||
{ |
|||
"author": "BortzMcgortz", |
|||
"text": "Best not-really-a-dog park within 1/4 miles of my house.", |
|||
"timestamp": 1767115098838 |
|||
}, |
|||
{ |
|||
"author": "DogDaddy", |
|||
"text": "My dogs poop here a lot.", |
|||
"timestamp": 1767115207491 |
|||
} |
|||
], |
|||
"createdAt": 1767115055219, |
|||
"alerted": true |
|||
} |
|||
] |
|||
1852
index.html
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,17 @@ |
|||
{ |
|||
"name": "hikemap", |
|||
"version": "1.0.0", |
|||
"description": "KML Track Editor with real-time user tracking", |
|||
"main": "server.js", |
|||
"scripts": { |
|||
"start": "node server.js", |
|||
"dev": "node server.js" |
|||
}, |
|||
"dependencies": { |
|||
"express": "^4.18.2", |
|||
"ws": "^8.14.2" |
|||
}, |
|||
"engines": { |
|||
"node": ">=14.0.0" |
|||
} |
|||
} |
|||
@ -0,0 +1,315 @@ |
|||
const WebSocket = require('ws'); |
|||
const http = require('http'); |
|||
const express = require('express'); |
|||
const path = require('path'); |
|||
const fs = require('fs').promises; |
|||
|
|||
const app = express(); |
|||
const server = http.createServer(app); |
|||
const wss = new WebSocket.Server({ server }); |
|||
|
|||
// Middleware to parse raw body for KML
|
|||
app.use(express.text({ type: 'application/xml', limit: '10mb' })); |
|||
|
|||
// Serve static files - prioritize data directory for default.kml
|
|||
app.get('/default.kml', async (req, res) => { |
|||
try { |
|||
// Try to serve from data directory first (mounted volume)
|
|||
const dataPath = path.join('/app/data', 'default.kml'); |
|||
await fs.access(dataPath); |
|||
console.log('Serving default.kml from data directory'); |
|||
res.sendFile(dataPath); |
|||
} catch (err) { |
|||
// Fall back to app directory
|
|||
const appPath = path.join(__dirname, 'default.kml'); |
|||
console.log('Serving default.kml from app directory'); |
|||
res.sendFile(appPath); |
|||
} |
|||
}); |
|||
|
|||
// Serve other static files
|
|||
app.use(express.static(path.join(__dirname))); |
|||
|
|||
// Store connected users
|
|||
const users = new Map(); |
|||
|
|||
// Store geocaches
|
|||
let geocaches = []; |
|||
|
|||
// Geocache file path
|
|||
const getGeocachePath = async () => { |
|||
let dataDir = __dirname; |
|||
try { |
|||
await fs.access('/app/data'); |
|||
dataDir = '/app/data'; |
|||
} catch (err) { |
|||
// Use local directory if /app/data doesn't exist
|
|||
} |
|||
return path.join(dataDir, 'geocaches.json'); |
|||
}; |
|||
|
|||
// Load geocaches from file
|
|||
const loadGeocaches = async () => { |
|||
try { |
|||
const geocachePath = await getGeocachePath(); |
|||
const data = await fs.readFile(geocachePath, 'utf8'); |
|||
geocaches = JSON.parse(data); |
|||
console.log(`Loaded ${geocaches.length} geocaches from file`); |
|||
} catch (err) { |
|||
if (err.code === 'ENOENT') { |
|||
console.log('No geocaches file found, starting fresh'); |
|||
} else { |
|||
console.error('Error loading geocaches:', err); |
|||
} |
|||
geocaches = []; |
|||
} |
|||
}; |
|||
|
|||
// Save geocaches to file
|
|||
const saveGeocaches = async () => { |
|||
try { |
|||
const geocachePath = await getGeocachePath(); |
|||
await fs.writeFile(geocachePath, JSON.stringify(geocaches, null, 2), 'utf8'); |
|||
console.log(`Saved ${geocaches.length} geocaches to file`); |
|||
} catch (err) { |
|||
console.error('Error saving geocaches:', err); |
|||
} |
|||
}; |
|||
|
|||
// KML save endpoint
|
|||
app.post('/save-kml', async (req, res) => { |
|||
try { |
|||
const kmlContent = req.body; |
|||
// Always use data directory when it exists (Docker), otherwise local
|
|||
let dataDir = __dirname; |
|||
try { |
|||
await fs.access('/app/data'); |
|||
dataDir = '/app/data'; |
|||
console.log('Using data directory for save'); |
|||
} catch (err) { |
|||
console.log('Using app directory for save'); |
|||
} |
|||
const defaultKmlPath = path.join(dataDir, 'default.kml'); |
|||
|
|||
// Create backup with timestamp
|
|||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); |
|||
const backupPath = path.join(dataDir, `default.kml.backup.${timestamp}`); |
|||
|
|||
try { |
|||
// Try to backup existing file
|
|||
const existingContent = await fs.readFile(defaultKmlPath, 'utf8'); |
|||
await fs.writeFile(backupPath, existingContent); |
|||
console.log(`Backed up existing KML to: ${backupPath}`); |
|||
} catch (err) { |
|||
// No existing file to backup, that's OK
|
|||
console.log('No existing default.kml to backup'); |
|||
} |
|||
|
|||
// Write new content
|
|||
await fs.writeFile(defaultKmlPath, kmlContent, 'utf8'); |
|||
console.log('Saved new default.kml'); |
|||
|
|||
// Clean up old backups (keep only last 10)
|
|||
const files = await fs.readdir(dataDir); |
|||
const backupFiles = files |
|||
.filter(f => f.startsWith('default.kml.backup.')) |
|||
.sort() |
|||
.reverse(); |
|||
|
|||
for (let i = 10; i < backupFiles.length; i++) { |
|||
await fs.unlink(path.join(dataDir, backupFiles[i])); |
|||
console.log(`Deleted old backup: ${backupFiles[i]}`); |
|||
} |
|||
|
|||
res.json({ |
|||
success: true, |
|||
message: 'Tracks saved to server successfully', |
|||
backup: backupPath.split('/').pop() |
|||
}); |
|||
} catch (err) { |
|||
console.error('Error saving KML:', err); |
|||
res.status(500).send('Failed to save: ' + err.message); |
|||
} |
|||
}); |
|||
|
|||
// Generate random user ID
|
|||
function generateUserId() { |
|||
return Math.random().toString(36).substring(7); |
|||
} |
|||
|
|||
// Broadcast to all clients except sender
|
|||
function broadcast(data, senderId) { |
|||
const message = JSON.stringify(data); |
|||
wss.clients.forEach(client => { |
|||
if (client.readyState === WebSocket.OPEN && client.userId !== senderId) { |
|||
client.send(message); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
// Clean up disconnected user
|
|||
function removeUser(userId) { |
|||
if (users.has(userId)) { |
|||
users.delete(userId); |
|||
broadcast({ type: 'userDisconnected', userId }, null); |
|||
console.log(`User ${userId} disconnected. Active users: ${users.size}`); |
|||
} |
|||
} |
|||
|
|||
wss.on('connection', (ws) => { |
|||
const userId = generateUserId(); |
|||
ws.userId = userId; |
|||
|
|||
console.log(`User ${userId} connected. Active users: ${users.size + 1}`); |
|||
|
|||
// Send user their ID, current visible users, and geocaches
|
|||
ws.send(JSON.stringify({ |
|||
type: 'init', |
|||
userId: userId, |
|||
users: Array.from(users.entries()) |
|||
.filter(([id, data]) => data.visible !== false) // Only send visible users
|
|||
.map(([id, data]) => ({ |
|||
userId: id, |
|||
...data |
|||
})) |
|||
})); |
|||
|
|||
// Send all geocaches
|
|||
if (geocaches.length > 0) { |
|||
ws.send(JSON.stringify({ |
|||
type: 'geocachesInit', |
|||
geocaches: geocaches |
|||
})); |
|||
} |
|||
|
|||
ws.on('message', (message) => { |
|||
try { |
|||
const data = JSON.parse(message); |
|||
|
|||
if (data.type === 'location') { |
|||
// Store user location with icon info
|
|||
users.set(userId, { |
|||
lat: data.lat, |
|||
lng: data.lng, |
|||
accuracy: data.accuracy, |
|||
icon: data.icon, |
|||
color: data.color, |
|||
visible: data.visible !== false, // default to true if not specified
|
|||
timestamp: Date.now() |
|||
}); |
|||
|
|||
// Broadcast to other users (including visibility status)
|
|||
broadcast({ |
|||
type: 'userLocation', |
|||
userId: userId, |
|||
lat: data.lat, |
|||
lng: data.lng, |
|||
accuracy: data.accuracy, |
|||
icon: data.icon, |
|||
color: data.color, |
|||
visible: data.visible !== false |
|||
}, userId); |
|||
} else if (data.type === 'iconUpdate') { |
|||
// Update user's icon
|
|||
const userData = users.get(userId) || {}; |
|||
userData.icon = data.icon; |
|||
userData.color = data.color; |
|||
users.set(userId, userData); |
|||
|
|||
// Broadcast icon update to other users if we have location and are visible
|
|||
if (userData.lat && userData.lng && userData.visible !== false) { |
|||
broadcast({ |
|||
type: 'userLocation', |
|||
userId: userId, |
|||
lat: userData.lat, |
|||
lng: userData.lng, |
|||
accuracy: userData.accuracy || 100, |
|||
icon: data.icon, |
|||
color: data.color, |
|||
visible: true |
|||
}, userId); |
|||
} |
|||
} else if (data.type === 'geocacheUpdate') { |
|||
// Handle geocache creation/update
|
|||
if (data.geocache) { |
|||
const existingIndex = geocaches.findIndex(g => g.id === data.geocache.id); |
|||
if (existingIndex >= 0) { |
|||
// Update existing geocache
|
|||
geocaches[existingIndex] = data.geocache; |
|||
} else { |
|||
// Add new geocache
|
|||
geocaches.push(data.geocache); |
|||
} |
|||
|
|||
// Save to file
|
|||
saveGeocaches(); |
|||
|
|||
// Broadcast to all other users
|
|||
broadcast({ |
|||
type: 'geocacheUpdate', |
|||
geocache: data.geocache |
|||
}, userId); |
|||
} |
|||
} else if (data.type === 'geocacheDelete') { |
|||
// Handle geocache deletion
|
|||
if (data.geocacheId) { |
|||
const index = geocaches.findIndex(g => g.id === data.geocacheId); |
|||
if (index > -1) { |
|||
geocaches.splice(index, 1); |
|||
|
|||
// Save to file
|
|||
saveGeocaches(); |
|||
|
|||
// Broadcast deletion to all other users
|
|||
broadcast({ |
|||
type: 'geocacheDelete', |
|||
geocacheId: data.geocacheId |
|||
}, userId); |
|||
} |
|||
} |
|||
} |
|||
} catch (err) { |
|||
console.error('Error processing message:', err); |
|||
} |
|||
}); |
|||
|
|||
ws.on('close', () => { |
|||
removeUser(userId); |
|||
}); |
|||
|
|||
ws.on('error', (err) => { |
|||
console.error(`WebSocket error for user ${userId}:`, err); |
|||
removeUser(userId); |
|||
}); |
|||
|
|||
// Heartbeat to detect disconnected clients
|
|||
ws.isAlive = true; |
|||
ws.on('pong', () => { |
|||
ws.isAlive = true; |
|||
}); |
|||
}); |
|||
|
|||
// Periodic cleanup of stale connections
|
|||
const heartbeatInterval = setInterval(() => { |
|||
wss.clients.forEach(ws => { |
|||
if (ws.isAlive === false) { |
|||
removeUser(ws.userId); |
|||
return ws.terminate(); |
|||
} |
|||
ws.isAlive = false; |
|||
ws.ping(); |
|||
}); |
|||
}, 30000); |
|||
|
|||
wss.on('close', () => { |
|||
clearInterval(heartbeatInterval); |
|||
}); |
|||
|
|||
const PORT = process.env.PORT || 8080; |
|||
server.listen(PORT, async () => { |
|||
console.log(`Server running on port ${PORT}`); |
|||
console.log(`Open http://localhost:${PORT} to view the map`); |
|||
|
|||
// Load geocaches on startup
|
|||
await loadGeocaches(); |
|||
}); |
|||
1063
tracks-export (1).kml
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue