|
|
@@ -0,0 +1,205 @@
|
|
|
+const router = require('express').Router();
|
|
|
+const multer = require('multer');
|
|
|
+const { requireAuth } = require('../middleware/auth');
|
|
|
+const { Track, TrackPoint, Directory, ShareLink } = require('../models');
|
|
|
+const { parseAndCompress } = require('../utils/gpx-processor');
|
|
|
+const { generateCode } = require('../utils/shortcode');
|
|
|
+let config;
|
|
|
+try { config = require('../../config'); } catch(e) { config = { upload: { maxFileSizeMB: 50 } }; }
|
|
|
+
|
|
|
+const upload = multer({
|
|
|
+ storage: multer.memoryStorage(),
|
|
|
+ limits: { fileSize: (config.upload?.maxFileSizeMB || 50) * 1024 * 1024 },
|
|
|
+ fileFilter: (req, file, cb) => {
|
|
|
+ if (file.originalname.toLowerCase().endsWith('.gpx') || file.mimetype === 'application/gpx+xml' || file.mimetype === 'text/xml' || file.mimetype === 'application/xml') {
|
|
|
+ cb(null, true);
|
|
|
+ } else {
|
|
|
+ cb(new Error('Only GPX files allowed'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+router.get('/', requireAuth, async (req, res) => {
|
|
|
+ try {
|
|
|
+ const where = { userId: req.user.id };
|
|
|
+ if (req.query.directoryId !== undefined) {
|
|
|
+ where.directoryId = req.query.directoryId || null;
|
|
|
+ }
|
|
|
+ const tracks = await Track.findAll({
|
|
|
+ where,
|
|
|
+ order: [['trackDate', 'DESC'], ['uploadDate', 'DESC']],
|
|
|
+ attributes: ['id', 'name', 'originalFilename', 'uploadDate', 'trackDate', 'pointCount', 'totalDistance', 'directoryId'],
|
|
|
+ });
|
|
|
+ res.json(tracks);
|
|
|
+ } catch (e) {
|
|
|
+ res.status(500).json({ error: 'Server error' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+router.get('/:id', requireAuth, async (req, res) => {
|
|
|
+ try {
|
|
|
+ const track = await Track.findOne({
|
|
|
+ where: { id: req.params.id, userId: req.user.id },
|
|
|
+ include: [{ model: ShareLink, attributes: ['code'] }],
|
|
|
+ });
|
|
|
+ if (!track) return res.status(404).json({ error: 'Track not found' });
|
|
|
+ res.json(track);
|
|
|
+ } catch (e) {
|
|
|
+ res.status(500).json({ error: 'Server error' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+router.get('/:id/points', requireAuth, async (req, res) => {
|
|
|
+ try {
|
|
|
+ const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
|
|
|
+ if (!track) return res.status(404).json({ error: 'Track not found' });
|
|
|
+
|
|
|
+ const points = await TrackPoint.findAll({
|
|
|
+ where: { trackId: track.id },
|
|
|
+ order: [['segmentId', 'ASC'], ['sequence', 'ASC']],
|
|
|
+ attributes: ['lat', 'lon', 'elevation', 'time', 'segmentId'],
|
|
|
+ });
|
|
|
+
|
|
|
+ // Group by segment
|
|
|
+ const segMap = {};
|
|
|
+ for (const p of points) {
|
|
|
+ if (!segMap[p.segmentId]) segMap[p.segmentId] = [];
|
|
|
+ segMap[p.segmentId].push([p.lat, p.lon, p.elevation, p.time]);
|
|
|
+ }
|
|
|
+ const segments = Object.keys(segMap).sort((a,b)=>a-b).map(k => segMap[k]);
|
|
|
+
|
|
|
+ res.json({
|
|
|
+ meta: { trackId: track.id, name: track.name, totalDistance: track.totalDistance, pointCount: track.pointCount, trackDate: track.trackDate },
|
|
|
+ segments
|
|
|
+ });
|
|
|
+ } catch (e) {
|
|
|
+ res.status(500).json({ error: 'Server error' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+router.post('/upload', requireAuth, upload.single('file'), async (req, res) => {
|
|
|
+ try {
|
|
|
+ if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
|
|
+
|
|
|
+ const xmlString = req.file.buffer.toString('utf8');
|
|
|
+ const { segments, trackName, trackDate, totalDistance, pointCount } = await parseAndCompress(xmlString);
|
|
|
+
|
|
|
+ if (pointCount === 0) return res.status(400).json({ error: 'No valid track points found in GPX file' });
|
|
|
+
|
|
|
+ const directoryId = req.body.directoryId ? parseInt(req.body.directoryId) : null;
|
|
|
+ if (directoryId) {
|
|
|
+ const dir = await Directory.findOne({ where: { id: directoryId, userId: req.user.id } });
|
|
|
+ if (!dir) return res.status(404).json({ error: 'Directory not found' });
|
|
|
+ }
|
|
|
+
|
|
|
+ const name = req.body.name || trackName || req.file.originalname.replace(/\.gpx$/i, '');
|
|
|
+
|
|
|
+ const track = await Track.create({
|
|
|
+ userId: req.user.id,
|
|
|
+ directoryId,
|
|
|
+ name,
|
|
|
+ originalFilename: req.file.originalname,
|
|
|
+ uploadDate: new Date(),
|
|
|
+ trackDate: trackDate || null,
|
|
|
+ pointCount,
|
|
|
+ totalDistance,
|
|
|
+ });
|
|
|
+
|
|
|
+ // Bulk insert points
|
|
|
+ const allPoints = [];
|
|
|
+ for (const seg of segments) {
|
|
|
+ for (const pt of seg.points) {
|
|
|
+ allPoints.push({
|
|
|
+ trackId: track.id,
|
|
|
+ lat: pt.lat,
|
|
|
+ lon: pt.lon,
|
|
|
+ elevation: pt.elevation,
|
|
|
+ time: pt.time,
|
|
|
+ sequence: pt.sequence,
|
|
|
+ segmentId: pt.segmentId,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ await TrackPoint.bulkCreate(allPoints);
|
|
|
+
|
|
|
+ // Update parent directory updatedAt
|
|
|
+ if (directoryId) {
|
|
|
+ await Directory.update({ updatedAt: new Date() }, { where: { id: directoryId } });
|
|
|
+ }
|
|
|
+
|
|
|
+ res.status(201).json(track);
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e);
|
|
|
+ res.status(500).json({ error: e.message || 'Server error' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+router.put('/:id', requireAuth, async (req, res) => {
|
|
|
+ try {
|
|
|
+ const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
|
|
|
+ if (!track) return res.status(404).json({ error: 'Track not found' });
|
|
|
+
|
|
|
+ const updates = {};
|
|
|
+ if (req.body.name) updates.name = req.body.name;
|
|
|
+ if (req.body.directoryId !== undefined) {
|
|
|
+ const dirId = req.body.directoryId || null;
|
|
|
+ if (dirId) {
|
|
|
+ const dir = await Directory.findOne({ where: { id: dirId, userId: req.user.id } });
|
|
|
+ if (!dir) return res.status(404).json({ error: 'Target directory not found' });
|
|
|
+ }
|
|
|
+ updates.directoryId = dirId;
|
|
|
+ }
|
|
|
+
|
|
|
+ await track.update(updates);
|
|
|
+ res.json(track);
|
|
|
+ } catch (e) {
|
|
|
+ res.status(500).json({ error: 'Server error' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+router.delete('/:id', requireAuth, async (req, res) => {
|
|
|
+ try {
|
|
|
+ const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
|
|
|
+ if (!track) return res.status(404).json({ error: 'Track not found' });
|
|
|
+ await track.destroy();
|
|
|
+ res.json({ ok: true });
|
|
|
+ } catch (e) {
|
|
|
+ res.status(500).json({ error: 'Server error' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// Share link
|
|
|
+router.post('/:id/share', requireAuth, async (req, res) => {
|
|
|
+ try {
|
|
|
+ const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
|
|
|
+ if (!track) return res.status(404).json({ error: 'Track not found' });
|
|
|
+
|
|
|
+ let link = await ShareLink.findOne({ where: { trackId: track.id } });
|
|
|
+ if (!link) {
|
|
|
+ let code, tries = 0;
|
|
|
+ do {
|
|
|
+ code = generateCode();
|
|
|
+ const existing = await ShareLink.findOne({ where: { code } });
|
|
|
+ if (!existing) break;
|
|
|
+ tries++;
|
|
|
+ } while (tries < 10);
|
|
|
+ link = await ShareLink.create({ trackId: track.id, code });
|
|
|
+ }
|
|
|
+ res.json({ code: link.code });
|
|
|
+ } catch (e) {
|
|
|
+ res.status(500).json({ error: 'Server error' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+router.delete('/:id/share', requireAuth, async (req, res) => {
|
|
|
+ try {
|
|
|
+ const track = await Track.findOne({ where: { id: req.params.id, userId: req.user.id } });
|
|
|
+ if (!track) return res.status(404).json({ error: 'Track not found' });
|
|
|
+ await ShareLink.destroy({ where: { trackId: track.id } });
|
|
|
+ res.json({ ok: true });
|
|
|
+ } catch (e) {
|
|
|
+ res.status(500).json({ error: 'Server error' });
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+module.exports = router;
|