Browse Source

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
HikeMap User 1 month ago
parent
commit
4211bf9b3c
  1. 8
      .claude/settings.local.json
  2. 19
      Dockerfile
  3. 1433
      Test.kml
  4. 492
      default.kml
  5. 447
      default.kml.backup.2025-12-30T14-07-34-870Z
  6. 447
      default.kml.backup.2025-12-30T14-08-13-130Z
  7. 611
      default.kml.backup.2025-12-30T14-08-58-379Z
  8. 1161
      default.kml.backup.2025-12-30T14-11-32-501Z
  9. 1168
      default.kml.backup.2025-12-30T14-12-39-684Z
  10. 604
      default.kml.backup.2025-12-30T14-18-50-550Z
  11. 1168
      default.kml.backup.2025-12-30T14-19-18-638Z
  12. 1168
      default.kml.backup.2025-12-30T14-19-29-904Z
  13. 1168
      default.kml.backup.2025-12-30T14-36-41-318Z
  14. 1161
      default.kml.backup.2025-12-30T18-14-18-396Z
  15. 12
      docker-compose.yml
  16. 21
      geocaches.json
  17. 1828
      index.html
  18. 17
      package.json
  19. 315
      server.js
  20. 1063
      tracks-export (1).kml

8
.claude/settings.local.json

@ -4,7 +4,13 @@
"Bash(powershell:*)",
"WebFetch(domain:app.unpkg.com)",
"WebSearch",
"WebFetch(domain:github.com)"
"WebFetch(domain:github.com)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git config:*)",
"Bash(git push:*)",
"Bash(docker-compose:*)",
"Bash(curl:*)"
]
}
}

19
Dockerfile

@ -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

492
default.kml
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

447
default.kml.backup.2025-12-30T14-08-13-130Z
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

1161
default.kml.backup.2025-12-30T14-11-32-501Z
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

604
default.kml.backup.2025-12-30T14-18-50-550Z
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

1168
default.kml.backup.2025-12-30T14-19-29-904Z
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

1161
default.kml.backup.2025-12-30T18-14-18-396Z
File diff suppressed because it is too large
View File

12
docker-compose.yml

@ -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

21
geocaches.json

@ -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
}
]

1828
index.html
File diff suppressed because it is too large
View File

17
package.json

@ -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"
}
}

315
server.js

@ -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

Loading…
Cancel
Save