diff --git a/examples/javascript-bundle/.gitignore b/examples/javascript-bundle/.gitignore new file mode 100644 index 0000000..ae2e759 --- /dev/null +++ b/examples/javascript-bundle/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +package-lock.json +coverage/ +.DS_Store +*.log \ No newline at end of file diff --git a/examples/javascript-bundle/data/sample-track.geojson b/examples/javascript-bundle/data/sample-track.geojson new file mode 100644 index 0000000..7035059 --- /dev/null +++ b/examples/javascript-bundle/data/sample-track.geojson @@ -0,0 +1,36 @@ +{ + "type": "Feature", + "properties": { + "name": "Sample GPS Track", + "description": "A sample GPS track for testing speed and direction calculations" + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [-0.1276, 51.5074], + [-0.1278, 51.5076], + [-0.1280, 51.5078], + [-0.1283, 51.5080], + [-0.1286, 51.5082], + [-0.1289, 51.5084], + [-0.1292, 51.5086], + [-0.1295, 51.5088], + [-0.1298, 51.5090], + [-0.1301, 51.5092] + ], + "properties": { + "timestamps": [ + "2024-01-18T10:00:00Z", + "2024-01-18T10:00:05Z", + "2024-01-18T10:00:10Z", + "2024-01-18T10:00:15Z", + "2024-01-18T10:00:20Z", + "2024-01-18T10:00:25Z", + "2024-01-18T10:00:30Z", + "2024-01-18T10:00:35Z", + "2024-01-18T10:00:40Z", + "2024-01-18T10:00:45Z" + ] + } + } +} \ No newline at end of file diff --git a/examples/javascript-bundle/index.json b/examples/javascript-bundle/index.json new file mode 100644 index 0000000..b157729 --- /dev/null +++ b/examples/javascript-bundle/index.json @@ -0,0 +1,399 @@ +{ + "bundle": { + "name": "JavaScript GeoJSON Tools", + "version": "1.0.0", + "description": "Mock JavaScript tool bundle for ToolVault UI testing with GeoJSON processing capabilities", + "created": "2024-01-18", + "categories": ["transform", "analysis", "statistics", "processing", "io"] + }, + "tools": [ + { + "id": "translate-features", + "name": "Translate Features", + "description": "Move GeoJSON features by a specified direction and distance", + "category": "transform", + "runtime": "javascript", + "script": "tools/transform/translate.js", + "function": "translateFeatures", + "inputs": [ + { + "name": "features", + "type": "GeoJSON", + "description": "GeoJSON FeatureCollection to translate" + } + ], + "parameters": [ + { + "name": "direction", + "type": "number", + "description": "Direction in degrees (0-360)", + "default": 0 + }, + { + "name": "distance", + "type": "number", + "description": "Distance in meters", + "default": 100 + } + ], + "outputs": [ + { + "name": "result", + "type": "GeoJSON", + "description": "Translated GeoJSON FeatureCollection" + } + ], + "tags": ["geometry", "transform", "spatial"] + }, + { + "id": "flip-horizontal", + "name": "Flip Horizontal", + "description": "Mirror GeoJSON features horizontally across a reference axis", + "category": "transform", + "runtime": "javascript", + "script": "tools/transform/flip-horizontal.js", + "function": "flipHorizontal", + "inputs": [ + { + "name": "features", + "type": "GeoJSON", + "description": "GeoJSON FeatureCollection to flip" + } + ], + "parameters": [ + { + "name": "axis", + "type": "string", + "description": "Reference axis (longitude or latitude)", + "default": "longitude", + "options": ["longitude", "latitude"] + } + ], + "outputs": [ + { + "name": "result", + "type": "GeoJSON", + "description": "Flipped GeoJSON FeatureCollection" + } + ], + "tags": ["geometry", "transform", "mirror"] + }, + { + "id": "flip-vertical", + "name": "Flip Vertical", + "description": "Mirror GeoJSON features vertically across a reference axis", + "category": "transform", + "runtime": "javascript", + "script": "tools/transform/flip-vertical.js", + "function": "flipVertical", + "inputs": [ + { + "name": "features", + "type": "GeoJSON", + "description": "GeoJSON FeatureCollection to flip" + } + ], + "parameters": [ + { + "name": "axis", + "type": "string", + "description": "Reference axis (longitude or latitude)", + "default": "latitude", + "options": ["longitude", "latitude"] + } + ], + "outputs": [ + { + "name": "result", + "type": "GeoJSON", + "description": "Flipped GeoJSON FeatureCollection" + } + ], + "tags": ["geometry", "transform", "mirror"] + }, + { + "id": "calculate-speed-series", + "name": "Calculate Speed Series", + "description": "Generate time series of speeds from a GPS track", + "category": "analysis", + "runtime": "javascript", + "script": "tools/analysis/speed-series.js", + "function": "calculateSpeedSeries", + "inputs": [ + { + "name": "track", + "type": "GeoJSON", + "description": "GeoJSON LineString with timestamp properties" + } + ], + "parameters": [ + { + "name": "time_unit", + "type": "string", + "description": "Time unit for speed calculation", + "default": "seconds", + "options": ["seconds", "minutes", "hours"] + } + ], + "outputs": [ + { + "name": "series", + "type": "JSON", + "description": "Array of {time, speed} objects" + } + ], + "tags": ["temporal", "analysis", "gps", "speed"] + }, + { + "id": "calculate-direction-series", + "name": "Calculate Direction Series", + "description": "Generate time series of travel directions from a GPS track", + "category": "analysis", + "runtime": "javascript", + "script": "tools/analysis/direction-series.js", + "function": "calculateDirectionSeries", + "inputs": [ + { + "name": "track", + "type": "GeoJSON", + "description": "GeoJSON LineString with timestamp properties" + } + ], + "parameters": [ + { + "name": "smoothing", + "type": "boolean", + "description": "Apply smoothing to directions", + "default": false + }, + { + "name": "window_size", + "type": "number", + "description": "Smoothing window size", + "default": 3 + } + ], + "outputs": [ + { + "name": "series", + "type": "JSON", + "description": "Array of {time, direction} objects" + } + ], + "tags": ["temporal", "analysis", "gps", "bearing"] + }, + { + "id": "average-speed", + "name": "Average Speed", + "description": "Calculate average speed from a GPS track", + "category": "statistics", + "runtime": "javascript", + "script": "tools/statistics/average-speed.js", + "function": "calculateAverageSpeed", + "inputs": [ + { + "name": "track", + "type": "GeoJSON", + "description": "GeoJSON LineString with timestamp properties" + } + ], + "parameters": [ + { + "name": "time_unit", + "type": "string", + "description": "Time unit for speed calculation", + "default": "seconds", + "options": ["seconds", "minutes", "hours"] + } + ], + "outputs": [ + { + "name": "speed", + "type": "number", + "description": "Average speed value" + } + ], + "tags": ["statistics", "gps", "speed"] + }, + { + "id": "speed-histogram", + "name": "Speed Histogram", + "description": "Create histogram of speeds with configurable intervals", + "category": "statistics", + "runtime": "javascript", + "script": "tools/statistics/speed-histogram.js", + "function": "createSpeedHistogram", + "inputs": [ + { + "name": "track", + "type": "GeoJSON", + "description": "GeoJSON LineString with timestamp properties" + } + ], + "parameters": [ + { + "name": "interval_minutes", + "type": "number", + "description": "Time interval in minutes", + "default": 1 + }, + { + "name": "bins", + "type": "number", + "description": "Number of histogram bins", + "default": 20 + } + ], + "outputs": [ + { + "name": "histogram", + "type": "JSON", + "description": "Histogram data structure" + } + ], + "tags": ["statistics", "visualization", "gps", "speed"] + }, + { + "id": "smooth-polyline", + "name": "Smooth Polyline", + "description": "Apply smoothing algorithms to LineString geometries", + "category": "processing", + "runtime": "javascript", + "script": "tools/processing/smooth-polyline.js", + "function": "smoothPolyline", + "inputs": [ + { + "name": "linestring", + "type": "GeoJSON", + "description": "GeoJSON LineString to smooth" + } + ], + "parameters": [ + { + "name": "algorithm", + "type": "string", + "description": "Smoothing algorithm", + "default": "moving_average", + "options": ["moving_average", "gaussian"] + }, + { + "name": "window_size", + "type": "number", + "description": "Smoothing window size", + "default": 3 + } + ], + "outputs": [ + { + "name": "result", + "type": "GeoJSON", + "description": "Smoothed GeoJSON LineString" + } + ], + "tags": ["geometry", "processing", "smoothing"] + }, + { + "id": "import-rep", + "name": "Import REP File", + "description": "Parse REP format file to GeoJSON", + "category": "io", + "runtime": "javascript", + "script": "tools/io/import-rep.js", + "function": "importREP", + "inputs": [ + { + "name": "content", + "type": "text", + "description": "REP format text content" + } + ], + "parameters": [ + { + "name": "encoding", + "type": "string", + "description": "Text encoding", + "default": "utf-8" + } + ], + "outputs": [ + { + "name": "features", + "type": "GeoJSON", + "description": "Parsed GeoJSON FeatureCollection" + } + ], + "tags": ["import", "conversion", "rep"] + }, + { + "id": "export-rep", + "name": "Export to REP", + "description": "Convert GeoJSON to REP format", + "category": "io", + "runtime": "javascript", + "script": "tools/io/export-rep.js", + "function": "exportREP", + "inputs": [ + { + "name": "features", + "type": "GeoJSON", + "description": "GeoJSON FeatureCollection to export" + } + ], + "parameters": [ + { + "name": "precision", + "type": "number", + "description": "Coordinate precision", + "default": 6 + } + ], + "outputs": [ + { + "name": "content", + "type": "text", + "description": "REP format text" + } + ], + "tags": ["export", "conversion", "rep"] + }, + { + "id": "export-csv", + "name": "Export to CSV", + "description": "Convert GeoJSON to CSV format", + "category": "io", + "runtime": "javascript", + "script": "tools/io/export-csv.js", + "function": "exportCSV", + "inputs": [ + { + "name": "features", + "type": "GeoJSON", + "description": "GeoJSON FeatureCollection to export" + } + ], + "parameters": [ + { + "name": "include_properties", + "type": "boolean", + "description": "Include feature properties in CSV", + "default": true + }, + { + "name": "coordinate_format", + "type": "string", + "description": "Format for coordinates", + "default": "separate", + "options": ["separate", "wkt"] + } + ], + "outputs": [ + { + "name": "content", + "type": "text", + "description": "CSV format text" + } + ], + "tags": ["export", "conversion", "csv"] + } + ] +} \ No newline at end of file diff --git a/examples/javascript-bundle/package.json b/examples/javascript-bundle/package.json new file mode 100644 index 0000000..7e31029 --- /dev/null +++ b/examples/javascript-bundle/package.json @@ -0,0 +1,23 @@ +{ + "name": "toolvault-javascript-bundle", + "version": "1.0.0", + "description": "Mock JavaScript tool bundle for ToolVault UI testing", + "scripts": { + "test": "jest", + "test:watch": "jest --watch" + }, + "devDependencies": { + "@types/jest": "^29.5.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0" + }, + "jest": { + "testEnvironment": "jsdom", + "testMatch": [ + "**/tests/**/*.test.js" + ], + "collectCoverageFrom": [ + "tools/**/*.js" + ] + } +} diff --git a/examples/javascript-bundle/tests/helpers.js b/examples/javascript-bundle/tests/helpers.js new file mode 100644 index 0000000..37c2af8 --- /dev/null +++ b/examples/javascript-bundle/tests/helpers.js @@ -0,0 +1,55 @@ +// Test helpers and fixtures + +const samplePoint = { + type: 'Feature', + properties: { name: 'Test Point' }, + geometry: { + type: 'Point', + coordinates: [-0.1276, 51.5074] + } +}; + +const sampleLineString = { + type: 'Feature', + properties: { name: 'Test Line' }, + geometry: { + type: 'LineString', + coordinates: [ + [-0.1276, 51.5074], + [-0.1278, 51.5076], + [-0.1280, 51.5078] + ] + } +}; + +const sampleTrackWithTimestamps = { + type: 'Feature', + properties: { name: 'GPS Track' }, + geometry: { + type: 'LineString', + coordinates: [ + [-0.1276, 51.5074], + [-0.1278, 51.5076], + [-0.1280, 51.5078] + ], + properties: { + timestamps: [ + '2024-01-18T10:00:00Z', + '2024-01-18T10:00:05Z', + '2024-01-18T10:00:10Z' + ] + } + } +}; + +const sampleFeatureCollection = { + type: 'FeatureCollection', + features: [samplePoint, sampleLineString] +}; + +module.exports = { + samplePoint, + sampleLineString, + sampleTrackWithTimestamps, + sampleFeatureCollection +}; \ No newline at end of file diff --git a/examples/javascript-bundle/tests/tools.test.js b/examples/javascript-bundle/tests/tools.test.js new file mode 100644 index 0000000..4f5cd7e --- /dev/null +++ b/examples/javascript-bundle/tests/tools.test.js @@ -0,0 +1,251 @@ +// Load all tool files +require('../tools/transform/translate.js'); +require('../tools/transform/flip-horizontal.js'); +require('../tools/transform/flip-vertical.js'); +require('../tools/analysis/speed-series.js'); +require('../tools/analysis/direction-series.js'); +require('../tools/statistics/average-speed.js'); +require('../tools/statistics/speed-histogram.js'); +require('../tools/processing/smooth-polyline.js'); +require('../tools/io/import-rep.js'); +require('../tools/io/export-rep.js'); +require('../tools/io/export-csv.js'); + +const { + samplePoint, + sampleLineString, + sampleTrackWithTimestamps, + sampleFeatureCollection +} = require('./helpers'); + +// Initialize window object for browser environment simulation +global.window = global.window || {}; + +describe('Transform Tools', () => { + describe('translateFeatures', () => { + test('should translate features by specified distance and direction', () => { + const input = JSON.parse(JSON.stringify(samplePoint)); + const params = { direction: 0, distance: 100 }; + const result = window.ToolVault.tools.translateFeatures(input, params); + + expect(result.type).toBe('Feature'); + expect(result.geometry.type).toBe('Point'); + // Check that coordinates have changed + expect(result.geometry.coordinates[1]).toBeGreaterThan(input.geometry.coordinates[1]); + }); + + test('should handle FeatureCollection', () => { + const input = JSON.parse(JSON.stringify(sampleFeatureCollection)); + const params = { direction: 90, distance: 100 }; + const result = window.ToolVault.tools.translateFeatures(input, params); + + expect(result.type).toBe('FeatureCollection'); + expect(result.features).toHaveLength(2); + }); + }); + + describe('flipHorizontal', () => { + test('should flip features horizontally', () => { + const input = JSON.parse(JSON.stringify(sampleLineString)); + const params = { axis: 'longitude' }; + const result = window.ToolVault.tools.flipHorizontal(input, params); + + expect(result.type).toBe('Feature'); + expect(result.geometry.type).toBe('LineString'); + // First and last coordinates should be swapped in longitude + const origFirst = input.geometry.coordinates[0][0]; + const origLast = input.geometry.coordinates[2][0]; + const center = (origFirst + origLast + input.geometry.coordinates[1][0]) / 3; + expect(Math.abs(result.geometry.coordinates[0][0] - (2 * center - origFirst))).toBeLessThan(0.0001); + }); + }); + + describe('flipVertical', () => { + test('should flip features vertically', () => { + const input = JSON.parse(JSON.stringify(sampleLineString)); + const params = { axis: 'latitude' }; + const result = window.ToolVault.tools.flipVertical(input, params); + + expect(result.type).toBe('Feature'); + expect(result.geometry.type).toBe('LineString'); + }); + }); +}); + +describe('Analysis Tools', () => { + describe('calculateSpeedSeries', () => { + test('should calculate speed series from GPS track', () => { + const input = sampleTrackWithTimestamps; + const params = { time_unit: 'seconds' }; + const result = window.ToolVault.tools.calculateSpeedSeries(input, params); + + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); // One less than number of points + expect(result[0]).toHaveProperty('time'); + expect(result[0]).toHaveProperty('speed'); + expect(typeof result[0].speed).toBe('number'); + }); + + test('should handle different time units', () => { + const input = sampleTrackWithTimestamps; + const resultSeconds = window.ToolVault.tools.calculateSpeedSeries(input, { time_unit: 'seconds' }); + const resultMinutes = window.ToolVault.tools.calculateSpeedSeries(input, { time_unit: 'minutes' }); + + expect(resultMinutes[0].speed).toBeCloseTo(resultSeconds[0].speed * 60, 2); + }); + }); + + describe('calculateDirectionSeries', () => { + test('should calculate direction series from GPS track', () => { + const input = sampleTrackWithTimestamps; + const params = { smoothing: false }; + const result = window.ToolVault.tools.calculateDirectionSeries(input, params); + + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); + expect(result[0]).toHaveProperty('time'); + expect(result[0]).toHaveProperty('direction'); + expect(result[0].direction).toBeGreaterThanOrEqual(0); + expect(result[0].direction).toBeLessThanOrEqual(360); + }); + + test('should apply smoothing when requested', () => { + const input = sampleTrackWithTimestamps; + const resultNoSmooth = window.ToolVault.tools.calculateDirectionSeries(input, { smoothing: false }); + const resultSmooth = window.ToolVault.tools.calculateDirectionSeries(input, { smoothing: true, window_size: 3 }); + + expect(resultSmooth.length).toBe(resultNoSmooth.length); + }); + }); +}); + +describe('Statistics Tools', () => { + describe('calculateAverageSpeed', () => { + test('should calculate average speed from GPS track', () => { + const input = sampleTrackWithTimestamps; + const params = { time_unit: 'seconds' }; + const result = window.ToolVault.tools.calculateAverageSpeed(input, params); + + expect(typeof result).toBe('number'); + expect(result).toBeGreaterThan(0); + }); + }); + + describe('createSpeedHistogram', () => { + test('should create speed histogram from GPS track', () => { + const input = sampleTrackWithTimestamps; + const params = { interval_minutes: 1, bins: 10 }; + const result = window.ToolVault.tools.createSpeedHistogram(input, params); + + expect(result).toHaveProperty('bins'); + expect(result).toHaveProperty('counts'); + expect(result).toHaveProperty('min'); + expect(result).toHaveProperty('max'); + expect(Array.isArray(result.bins)).toBe(true); + expect(Array.isArray(result.counts)).toBe(true); + }); + }); +}); + +describe('Processing Tools', () => { + describe('smoothPolyline', () => { + test('should smooth polyline with moving average', () => { + const input = sampleLineString; + const params = { algorithm: 'moving_average', window_size: 3 }; + const result = window.ToolVault.tools.smoothPolyline(input, params); + + expect(result.type).toBe('Feature'); + expect(result.geometry.type).toBe('LineString'); + expect(result.geometry.coordinates).toHaveLength(input.geometry.coordinates.length); + }); + + test('should smooth polyline with gaussian', () => { + const input = sampleLineString; + const params = { algorithm: 'gaussian', window_size: 3 }; + const result = window.ToolVault.tools.smoothPolyline(input, params); + + expect(result.type).toBe('Feature'); + expect(result.geometry.type).toBe('LineString'); + expect(result.geometry.coordinates).toHaveLength(input.geometry.coordinates.length); + }); + }); +}); + +describe('I/O Tools', () => { + describe('importREP', () => { + test('should import REP format to GeoJSON', () => { + const repContent = `# Test REP file +51.5074,-0.1276,0,2024-01-18T10:00:00Z +51.5076,-0.1278,0,2024-01-18T10:00:05Z +51.5078,-0.1280,0,2024-01-18T10:00:10Z`; + + const result = window.ToolVault.tools.importREP(repContent, {}); + + expect(result.type).toBe('FeatureCollection'); + expect(result.features).toHaveLength(1); + expect(result.features[0].geometry.type).toBe('LineString'); + expect(result.features[0].geometry.coordinates).toHaveLength(3); + }); + + test('should handle single point', () => { + const repContent = '51.5074,-0.1276,0,2024-01-18T10:00:00Z'; + const result = window.ToolVault.tools.importREP(repContent, {}); + + expect(result.features[0].geometry.type).toBe('Point'); + }); + }); + + describe('exportREP', () => { + test('should export GeoJSON to REP format', () => { + const input = sampleLineString; + const params = { precision: 4 }; + const result = window.ToolVault.tools.exportREP(input, params); + + expect(typeof result).toBe('string'); + expect(result).toContain('51.5074'); + expect(result).toContain('-0.1276'); + const lines = result.split('\n'); + expect(lines.length).toBeGreaterThan(3); // Headers + coordinates + }); + + test('should export FeatureCollection', () => { + const input = sampleFeatureCollection; + const result = window.ToolVault.tools.exportREP(input, { precision: 6 }); + + expect(typeof result).toBe('string'); + expect(result).toContain('REP Format Export'); + }); + }); + + describe('exportCSV', () => { + test('should export GeoJSON to CSV with separate coordinates', () => { + const input = sampleLineString; + const params = { include_properties: true, coordinate_format: 'separate' }; + const result = window.ToolVault.tools.exportCSV(input, params); + + expect(typeof result).toBe('string'); + const lines = result.split('\n'); + expect(lines[0]).toContain('longitude'); + expect(lines[0]).toContain('latitude'); + expect(lines.length).toBe(4); // Header + 3 coordinates + }); + + test('should export to WKT format', () => { + const input = sampleLineString; + const params = { include_properties: false, coordinate_format: 'wkt' }; + const result = window.ToolVault.tools.exportCSV(input, params); + + expect(result).toContain('LINESTRING'); + const lines = result.split('\n'); + expect(lines.length).toBe(2); // Header + 1 WKT row + }); + + test('should handle FeatureCollection', () => { + const input = sampleFeatureCollection; + const result = window.ToolVault.tools.exportCSV(input, { coordinate_format: 'wkt' }); + + expect(result).toContain('POINT'); + expect(result).toContain('LINESTRING'); + }); + }); +}); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/analysis/direction-series.js b/examples/javascript-bundle/tools/analysis/direction-series.js new file mode 100644 index 0000000..52cfa98 --- /dev/null +++ b/examples/javascript-bundle/tools/analysis/direction-series.js @@ -0,0 +1,77 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.calculateDirectionSeries = function(input, params) { + const { smoothing = false, window_size = 3 } = params || {}; + + // Calculate bearing between two points + function calculateBearing(lat1, lon1, lat2, lon2) { + const φ1 = lat1 * Math.PI / 180; + const φ2 = lat2 * Math.PI / 180; + const Δλ = (lon2 - lon1) * Math.PI / 180; + + const y = Math.sin(Δλ) * Math.cos(φ2); + const x = Math.cos(φ1) * Math.sin(φ2) - + Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ); + + const θ = Math.atan2(y, x); + + return ((θ * 180 / Math.PI) + 360) % 360; // Convert to degrees and normalize to 0-360 + } + + // Extract coordinates and timestamps + let coordinates = []; + let timestamps = []; + + if (input.type === 'Feature' && input.geometry.type === 'LineString') { + coordinates = input.geometry.coordinates; + timestamps = input.geometry.properties?.timestamps || []; + } else if (input.type === 'LineString') { + coordinates = input.coordinates; + timestamps = input.properties?.timestamps || []; + } + + // Calculate directions + const rawSeries = []; + + for (let i = 1; i < coordinates.length && i < timestamps.length; i++) { + const coord1 = coordinates[i - 1]; + const coord2 = coordinates[i]; + + const bearing = calculateBearing(coord1[1], coord1[0], coord2[1], coord2[0]); + + rawSeries.push({ + time: timestamps[i], + direction: bearing + }); + } + + // Apply smoothing if requested + if (smoothing && window_size > 1) { + const smoothedSeries = []; + const halfWindow = Math.floor(window_size / 2); + + for (let i = 0; i < rawSeries.length; i++) { + let sum = 0; + let count = 0; + + for (let j = Math.max(0, i - halfWindow); + j <= Math.min(rawSeries.length - 1, i + halfWindow); + j++) { + sum += rawSeries[j].direction; + count++; + } + + smoothedSeries.push({ + time: rawSeries[i].time, + direction: sum / count + }); + } + + return smoothedSeries; + } + + return rawSeries; + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/analysis/speed-series.js b/examples/javascript-bundle/tools/analysis/speed-series.js new file mode 100644 index 0000000..7040c8b --- /dev/null +++ b/examples/javascript-bundle/tools/analysis/speed-series.js @@ -0,0 +1,70 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.calculateSpeedSeries = function(input, params) { + const { time_unit = 'seconds' } = params || {}; + + // Haversine formula to calculate distance between two points + function haversineDistance(lat1, lon1, lat2, lon2) { + const R = 6371000; // Earth radius in meters + const φ1 = lat1 * Math.PI / 180; + const φ2 = lat2 * Math.PI / 180; + const Δφ = (lat2 - lat1) * Math.PI / 180; + const Δλ = (lon2 - lon1) * Math.PI / 180; + + const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + + Math.cos(φ1) * Math.cos(φ2) * + Math.sin(Δλ/2) * Math.sin(Δλ/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return R * c; // Distance in meters + } + + // Extract coordinates and timestamps + let coordinates = []; + let timestamps = []; + + if (input.type === 'Feature' && input.geometry.type === 'LineString') { + coordinates = input.geometry.coordinates; + timestamps = input.geometry.properties?.timestamps || []; + } else if (input.type === 'LineString') { + coordinates = input.coordinates; + timestamps = input.properties?.timestamps || []; + } + + // Calculate speeds + const series = []; + + for (let i = 1; i < coordinates.length && i < timestamps.length; i++) { + const coord1 = coordinates[i - 1]; + const coord2 = coordinates[i]; + const time1 = new Date(timestamps[i - 1]); + const time2 = new Date(timestamps[i]); + + // Calculate distance in meters + const distance = haversineDistance(coord1[1], coord1[0], coord2[1], coord2[0]); + + // Calculate time difference in seconds + const timeDiff = (time2 - time1) / 1000; + + if (timeDiff > 0) { + // Calculate speed based on requested unit + let speed = distance / timeDiff; // m/s + + if (time_unit === 'minutes') { + speed = speed * 60; // m/min + } else if (time_unit === 'hours') { + speed = speed * 3600; // m/h + } + + series.push({ + time: timestamps[i], + speed: speed + }); + } + } + + return series; + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/io/export-csv.js b/examples/javascript-bundle/tools/io/export-csv.js new file mode 100644 index 0000000..11773b9 --- /dev/null +++ b/examples/javascript-bundle/tools/io/export-csv.js @@ -0,0 +1,141 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.exportCSV = function(input, params) { + const { include_properties = true, coordinate_format = 'separate' } = params || {}; + + const rows = []; + const headers = []; + + // Helper function to escape CSV values + function escapeCSV(value) { + if (value === null || value === undefined) { + return ''; + } + value = String(value); + if (value.includes(',') || value.includes('"') || value.includes('\n')) { + return '"' + value.replace(/"/g, '""') + '"'; + } + return value; + } + + // Helper function to convert geometry to WKT + function geometryToWKT(geometry) { + if (geometry.type === 'Point') { + return `POINT(${geometry.coordinates[0]} ${geometry.coordinates[1]})`; + } else if (geometry.type === 'LineString') { + const coords = geometry.coordinates + .map(c => `${c[0]} ${c[1]}`) + .join(','); + return `LINESTRING(${coords})`; + } else if (geometry.type === 'Polygon') { + const rings = geometry.coordinates + .map(ring => { + const coords = ring.map(c => `${c[0]} ${c[1]}`).join(','); + return `(${coords})`; + }) + .join(','); + return `POLYGON${rings}`; + } + return ''; + } + + // Helper function to flatten geometry coordinates + function flattenCoordinates(geometry) { + const points = []; + + if (geometry.type === 'Point') { + points.push(geometry.coordinates); + } else if (geometry.type === 'LineString') { + points.push(...geometry.coordinates); + } else if (geometry.type === 'Polygon') { + points.push(...geometry.coordinates[0]); // Only outer ring + } + + return points; + } + + // Process features + let features = []; + + if (input.type === 'FeatureCollection') { + features = input.features; + } else if (input.type === 'Feature') { + features = [input]; + } else if (input.type && input.coordinates) { + // Direct geometry + features = [{ + type: 'Feature', + properties: {}, + geometry: input + }]; + } + + // Collect all property keys for headers + const propertyKeys = new Set(); + + if (include_properties) { + features.forEach(feature => { + if (feature.properties) { + Object.keys(feature.properties).forEach(key => { + propertyKeys.add(key); + }); + } + }); + } + + // Build headers + if (coordinate_format === 'wkt') { + headers.push('geometry'); + } else { + headers.push('longitude', 'latitude'); + } + + if (include_properties) { + headers.push(...Array.from(propertyKeys)); + } + + rows.push(headers.map(escapeCSV).join(',')); + + // Process each feature + features.forEach(feature => { + if (!feature.geometry) { + return; + } + + if (coordinate_format === 'wkt') { + // Single row per feature with WKT geometry + const row = []; + row.push(escapeCSV(geometryToWKT(feature.geometry))); + + if (include_properties && feature.properties) { + propertyKeys.forEach(key => { + row.push(escapeCSV(feature.properties[key])); + }); + } + + rows.push(row.join(',')); + } else { + // One row per coordinate pair + const coordinates = flattenCoordinates(feature.geometry); + + coordinates.forEach(coord => { + const row = []; + row.push(escapeCSV(coord[0])); // longitude + row.push(escapeCSV(coord[1])); // latitude + + if (include_properties && feature.properties) { + propertyKeys.forEach(key => { + row.push(escapeCSV(feature.properties[key])); + }); + } + + rows.push(row.join(',')); + }); + } + }); + + return rows.join('\n'); + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/io/export-rep.js b/examples/javascript-bundle/tools/io/export-rep.js new file mode 100644 index 0000000..12f3d6e --- /dev/null +++ b/examples/javascript-bundle/tools/io/export-rep.js @@ -0,0 +1,87 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.exportREP = function(input, params) { + const { precision = 6 } = params || {}; + + // REP format output + let output = []; + + // Add header comment + output.push('# REP Format Export from ToolVault'); + output.push('# Format: lat,lon[,altitude][,timestamp]'); + output.push(''); + + // Helper function to format number with precision + function formatNumber(num) { + return Number(num).toFixed(precision); + } + + // Helper function to extract coordinates from geometry + function extractCoordinates(geometry) { + const coords = []; + const timestamps = geometry.properties?.timestamps || []; + + if (geometry.type === 'Point') { + const lon = formatNumber(geometry.coordinates[0]); + const lat = formatNumber(geometry.coordinates[1]); + const alt = geometry.coordinates[2] ? formatNumber(geometry.coordinates[2]) : '0'; + const time = timestamps[0] || ''; + coords.push(`${lat},${lon},${alt},${time}`); + } else if (geometry.type === 'LineString') { + geometry.coordinates.forEach((coord, index) => { + const lon = formatNumber(coord[0]); + const lat = formatNumber(coord[1]); + const alt = coord[2] ? formatNumber(coord[2]) : '0'; + const time = timestamps[index] || ''; + coords.push(`${lat},${lon},${alt},${time}`); + }); + } else if (geometry.type === 'Polygon') { + // Export only the outer ring + geometry.coordinates[0].forEach((coord, index) => { + const lon = formatNumber(coord[0]); + const lat = formatNumber(coord[1]); + const alt = coord[2] ? formatNumber(coord[2]) : '0'; + coords.push(`${lat},${lon},${alt},`); + }); + } + + return coords; + } + + // Process input based on type + if (input.type === 'FeatureCollection') { + input.features.forEach((feature, featureIndex) => { + if (featureIndex > 0) { + output.push(''); // Empty line between features + } + + // Add feature name/description if available + if (feature.properties?.name) { + output.push(`# ${feature.properties.name}`); + } + + if (feature.geometry) { + const coords = extractCoordinates(feature.geometry); + output = output.concat(coords); + } + }); + } else if (input.type === 'Feature') { + if (input.properties?.name) { + output.push(`# ${input.properties.name}`); + } + + if (input.geometry) { + const coords = extractCoordinates(input.geometry); + output = output.concat(coords); + } + } else if (input.type && input.coordinates) { + // Direct geometry object + const coords = extractCoordinates(input); + output = output.concat(coords); + } + + return output.join('\n'); + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/io/import-rep.js b/examples/javascript-bundle/tools/io/import-rep.js new file mode 100644 index 0000000..fe5863d --- /dev/null +++ b/examples/javascript-bundle/tools/io/import-rep.js @@ -0,0 +1,81 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.importREP = function(input, params) { + const { encoding = 'utf-8' } = params || {}; + + // REP format is a simple text format with lines of coordinates + // Format: lat,lon[,altitude][,time] + // Lines starting with # are comments + // First non-comment line may contain metadata + + const lines = input.split('\n'); + const features = []; + const coordinates = []; + const timestamps = []; + let metadata = {}; + + for (let line of lines) { + line = line.trim(); + + // Skip empty lines and comments + if (!line || line.startsWith('#')) { + continue; + } + + // Parse line as coordinate data + const parts = line.split(',').map(p => p.trim()); + + if (parts.length >= 2) { + const lat = parseFloat(parts[0]); + const lon = parseFloat(parts[1]); + + if (!isNaN(lat) && !isNaN(lon)) { + coordinates.push([lon, lat]); // GeoJSON uses [lon, lat] + + // Check for timestamp (4th field) + if (parts.length >= 4 && parts[3]) { + timestamps.push(parts[3]); + } + } else if (coordinates.length === 0) { + // First non-coordinate line might be metadata + try { + metadata = { description: line }; + } catch (e) { + // Ignore parsing errors + } + } + } + } + + // Create GeoJSON output + if (coordinates.length > 0) { + const feature = { + type: 'Feature', + properties: { + ...metadata, + source: 'REP import' + }, + geometry: { + type: coordinates.length === 1 ? 'Point' : 'LineString', + coordinates: coordinates.length === 1 ? coordinates[0] : coordinates + } + }; + + // Add timestamps if available + if (timestamps.length > 0) { + feature.geometry.properties = { + timestamps: timestamps + }; + } + + features.push(feature); + } + + return { + type: 'FeatureCollection', + features: features + }; + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/processing/smooth-polyline.js b/examples/javascript-bundle/tools/processing/smooth-polyline.js new file mode 100644 index 0000000..2516d76 --- /dev/null +++ b/examples/javascript-bundle/tools/processing/smooth-polyline.js @@ -0,0 +1,91 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.smoothPolyline = function(input, params) { + const { algorithm = 'moving_average', window_size = 3 } = params || {}; + + // Extract coordinates + let coordinates = []; + let inputType = null; + + if (input.type === 'Feature' && input.geometry.type === 'LineString') { + coordinates = input.geometry.coordinates; + inputType = 'Feature'; + } else if (input.type === 'LineString') { + coordinates = input.coordinates; + inputType = 'LineString'; + } + + if (coordinates.length < 2) { + return input; // Not enough points to smooth + } + + // Deep clone the input + const result = JSON.parse(JSON.stringify(input)); + + // Apply smoothing based on algorithm + let smoothedCoords = []; + + if (algorithm === 'moving_average') { + // Moving average smoothing + const halfWindow = Math.floor(window_size / 2); + + for (let i = 0; i < coordinates.length; i++) { + let sumLon = 0; + let sumLat = 0; + let count = 0; + + for (let j = Math.max(0, i - halfWindow); + j <= Math.min(coordinates.length - 1, i + halfWindow); + j++) { + sumLon += coordinates[j][0]; + sumLat += coordinates[j][1]; + count++; + } + + smoothedCoords.push([sumLon / count, sumLat / count]); + } + } else if (algorithm === 'gaussian') { + // Gaussian smoothing + const sigma = window_size / 3; // Standard deviation + + // Generate Gaussian weights + function gaussianWeight(distance, sigma) { + return Math.exp(-(distance * distance) / (2 * sigma * sigma)); + } + + for (let i = 0; i < coordinates.length; i++) { + let weightedSumLon = 0; + let weightedSumLat = 0; + let totalWeight = 0; + + for (let j = 0; j < coordinates.length; j++) { + const distance = Math.abs(i - j); + const weight = gaussianWeight(distance, sigma); + + weightedSumLon += coordinates[j][0] * weight; + weightedSumLat += coordinates[j][1] * weight; + totalWeight += weight; + } + + smoothedCoords.push([ + weightedSumLon / totalWeight, + weightedSumLat / totalWeight + ]); + } + } else { + // Default to original coordinates if algorithm not recognized + smoothedCoords = coordinates; + } + + // Update the result with smoothed coordinates + if (inputType === 'Feature') { + result.geometry.coordinates = smoothedCoords; + } else if (inputType === 'LineString') { + result.coordinates = smoothedCoords; + } + + return result; + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/statistics/average-speed.js b/examples/javascript-bundle/tools/statistics/average-speed.js new file mode 100644 index 0000000..e2c3ea7 --- /dev/null +++ b/examples/javascript-bundle/tools/statistics/average-speed.js @@ -0,0 +1,68 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.calculateAverageSpeed = function(input, params) { + const { time_unit = 'seconds' } = params || {}; + + // Haversine formula to calculate distance between two points + function haversineDistance(lat1, lon1, lat2, lon2) { + const R = 6371000; // Earth radius in meters + const φ1 = lat1 * Math.PI / 180; + const φ2 = lat2 * Math.PI / 180; + const Δφ = (lat2 - lat1) * Math.PI / 180; + const Δλ = (lon2 - lon1) * Math.PI / 180; + + const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + + Math.cos(φ1) * Math.cos(φ2) * + Math.sin(Δλ/2) * Math.sin(Δλ/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return R * c; // Distance in meters + } + + // Extract coordinates and timestamps + let coordinates = []; + let timestamps = []; + + if (input.type === 'Feature' && input.geometry.type === 'LineString') { + coordinates = input.geometry.coordinates; + timestamps = input.geometry.properties?.timestamps || []; + } else if (input.type === 'LineString') { + coordinates = input.coordinates; + timestamps = input.properties?.timestamps || []; + } + + // Calculate total distance and time + let totalDistance = 0; + let totalTime = 0; + + for (let i = 1; i < coordinates.length && i < timestamps.length; i++) { + const coord1 = coordinates[i - 1]; + const coord2 = coordinates[i]; + const time1 = new Date(timestamps[i - 1]); + const time2 = new Date(timestamps[i]); + + // Calculate distance in meters + totalDistance += haversineDistance(coord1[1], coord1[0], coord2[1], coord2[0]); + + // Calculate time difference in seconds + totalTime += (time2 - time1) / 1000; + } + + if (totalTime === 0) { + return 0; + } + + // Calculate average speed based on requested unit + let averageSpeed = totalDistance / totalTime; // m/s + + if (time_unit === 'minutes') { + averageSpeed = averageSpeed * 60; // m/min + } else if (time_unit === 'hours') { + averageSpeed = averageSpeed * 3600; // m/h + } + + return averageSpeed; + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/statistics/speed-histogram.js b/examples/javascript-bundle/tools/statistics/speed-histogram.js new file mode 100644 index 0000000..f4fb793 --- /dev/null +++ b/examples/javascript-bundle/tools/statistics/speed-histogram.js @@ -0,0 +1,82 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.createSpeedHistogram = function(input, params) { + const { interval_minutes = 1, bins = 20 } = params || {}; + + // Use the calculateSpeedSeries function to get speeds + const speedSeries = window.ToolVault.tools.calculateSpeedSeries(input, { time_unit: 'seconds' }); + + if (!speedSeries || speedSeries.length === 0) { + return { + bins: [], + counts: [], + min: 0, + max: 0 + }; + } + + // Group speeds by time intervals + const intervalMs = interval_minutes * 60 * 1000; + const intervalGroups = {}; + + speedSeries.forEach(point => { + const time = new Date(point.time); + const intervalKey = Math.floor(time.getTime() / intervalMs) * intervalMs; + + if (!intervalGroups[intervalKey]) { + intervalGroups[intervalKey] = []; + } + intervalGroups[intervalKey].push(point.speed); + }); + + // Calculate average speed for each interval + const intervalSpeeds = Object.values(intervalGroups).map(speeds => { + const sum = speeds.reduce((a, b) => a + b, 0); + return sum / speeds.length; + }); + + if (intervalSpeeds.length === 0) { + return { + bins: [], + counts: [], + min: 0, + max: 0 + }; + } + + // Find min and max speeds + const minSpeed = Math.min(...intervalSpeeds); + const maxSpeed = Math.max(...intervalSpeeds); + + // Create histogram bins + const binWidth = (maxSpeed - minSpeed) / bins; + const histogram = { + bins: [], + counts: new Array(bins).fill(0), + min: minSpeed, + max: maxSpeed, + binWidth: binWidth + }; + + // Create bin edges + for (let i = 0; i < bins; i++) { + histogram.bins.push({ + min: minSpeed + i * binWidth, + max: minSpeed + (i + 1) * binWidth, + center: minSpeed + (i + 0.5) * binWidth + }); + } + + // Count speeds in each bin + intervalSpeeds.forEach(speed => { + const binIndex = Math.min(Math.floor((speed - minSpeed) / binWidth), bins - 1); + if (binIndex >= 0 && binIndex < bins) { + histogram.counts[binIndex]++; + } + }); + + return histogram; + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/transform/flip-horizontal.js b/examples/javascript-bundle/tools/transform/flip-horizontal.js new file mode 100644 index 0000000..35c48d1 --- /dev/null +++ b/examples/javascript-bundle/tools/transform/flip-horizontal.js @@ -0,0 +1,87 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.flipHorizontal = function(input, params) { + const { axis = 'longitude' } = params || {}; + + // Deep clone the input + const result = JSON.parse(JSON.stringify(input)); + + // Find the center point for flipping + let center = 0; + let coordCount = 0; + + // Helper to collect all coordinates + function collectCoordinates(coords, axisIndex) { + if (typeof coords[0] === 'number') { + center += coords[axisIndex]; + coordCount++; + } else { + coords.forEach(c => collectCoordinates(c, axisIndex)); + } + } + + // Helper to flip coordinates + function flipCoordinate(coord, axisIndex, centerValue) { + const newCoord = [...coord]; + newCoord[axisIndex] = 2 * centerValue - coord[axisIndex]; + return newCoord; + } + + // Helper to flip coordinates recursively + function flipCoordinates(coords, axisIndex, centerValue) { + if (typeof coords[0] === 'number') { + return flipCoordinate(coords, axisIndex, centerValue); + } + return coords.map(c => flipCoordinates(c, axisIndex, centerValue)); + } + + // Determine axis index (0 for longitude, 1 for latitude) + const axisIndex = axis === 'latitude' ? 1 : 0; + + // Collect all coordinates to find center + if (result.type === 'FeatureCollection') { + result.features.forEach(feature => { + if (feature.geometry && feature.geometry.coordinates) { + collectCoordinates(feature.geometry.coordinates, axisIndex); + } + }); + } else if (result.type === 'Feature') { + if (result.geometry && result.geometry.coordinates) { + collectCoordinates(result.geometry.coordinates, axisIndex); + } + } else if (result.coordinates) { + collectCoordinates(result.coordinates, axisIndex); + } + + // Calculate center + center = coordCount > 0 ? center / coordCount : 0; + + // Apply flipping + if (result.type === 'FeatureCollection') { + result.features = result.features.map(feature => { + if (feature.geometry && feature.geometry.coordinates) { + feature.geometry.coordinates = flipCoordinates( + feature.geometry.coordinates, + axisIndex, + center + ); + } + return feature; + }); + } else if (result.type === 'Feature') { + if (result.geometry && result.geometry.coordinates) { + result.geometry.coordinates = flipCoordinates( + result.geometry.coordinates, + axisIndex, + center + ); + } + } else if (result.coordinates) { + result.coordinates = flipCoordinates(result.coordinates, axisIndex, center); + } + + return result; + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/transform/flip-vertical.js b/examples/javascript-bundle/tools/transform/flip-vertical.js new file mode 100644 index 0000000..e4926f9 --- /dev/null +++ b/examples/javascript-bundle/tools/transform/flip-vertical.js @@ -0,0 +1,87 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.flipVertical = function(input, params) { + const { axis = 'latitude' } = params || {}; + + // Deep clone the input + const result = JSON.parse(JSON.stringify(input)); + + // Find the center point for flipping + let center = 0; + let coordCount = 0; + + // Helper to collect all coordinates + function collectCoordinates(coords, axisIndex) { + if (typeof coords[0] === 'number') { + center += coords[axisIndex]; + coordCount++; + } else { + coords.forEach(c => collectCoordinates(c, axisIndex)); + } + } + + // Helper to flip coordinates + function flipCoordinate(coord, axisIndex, centerValue) { + const newCoord = [...coord]; + newCoord[axisIndex] = 2 * centerValue - coord[axisIndex]; + return newCoord; + } + + // Helper to flip coordinates recursively + function flipCoordinates(coords, axisIndex, centerValue) { + if (typeof coords[0] === 'number') { + return flipCoordinate(coords, axisIndex, centerValue); + } + return coords.map(c => flipCoordinates(c, axisIndex, centerValue)); + } + + // Determine axis index (0 for longitude, 1 for latitude) + const axisIndex = axis === 'longitude' ? 0 : 1; + + // Collect all coordinates to find center + if (result.type === 'FeatureCollection') { + result.features.forEach(feature => { + if (feature.geometry && feature.geometry.coordinates) { + collectCoordinates(feature.geometry.coordinates, axisIndex); + } + }); + } else if (result.type === 'Feature') { + if (result.geometry && result.geometry.coordinates) { + collectCoordinates(result.geometry.coordinates, axisIndex); + } + } else if (result.coordinates) { + collectCoordinates(result.coordinates, axisIndex); + } + + // Calculate center + center = coordCount > 0 ? center / coordCount : 0; + + // Apply flipping + if (result.type === 'FeatureCollection') { + result.features = result.features.map(feature => { + if (feature.geometry && feature.geometry.coordinates) { + feature.geometry.coordinates = flipCoordinates( + feature.geometry.coordinates, + axisIndex, + center + ); + } + return feature; + }); + } else if (result.type === 'Feature') { + if (result.geometry && result.geometry.coordinates) { + result.geometry.coordinates = flipCoordinates( + result.geometry.coordinates, + axisIndex, + center + ); + } + } else if (result.coordinates) { + result.coordinates = flipCoordinates(result.coordinates, axisIndex, center); + } + + return result; + }; +})(); \ No newline at end of file diff --git a/examples/javascript-bundle/tools/transform/translate.js b/examples/javascript-bundle/tools/transform/translate.js new file mode 100644 index 0000000..3f038fc --- /dev/null +++ b/examples/javascript-bundle/tools/transform/translate.js @@ -0,0 +1,56 @@ +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.translateFeatures = function(input, params) { + const { direction = 0, distance = 100 } = params || {}; + + // Convert direction to radians + const directionRad = (direction * Math.PI) / 180; + + // Calculate offset in degrees (approximation for small distances) + // Earth radius in meters + const earthRadius = 6371000; + const latOffset = (distance * Math.cos(directionRad)) / earthRadius * (180 / Math.PI); + const lonOffset = (distance * Math.sin(directionRad)) / earthRadius * (180 / Math.PI); + + // Helper function to translate a coordinate + function translateCoordinate(coord) { + const avgLat = coord[1]; + const lonCorrection = Math.cos(avgLat * Math.PI / 180); + return [ + coord[0] + lonOffset / lonCorrection, + coord[1] + latOffset + ]; + } + + // Helper function to translate coordinates recursively + function translateCoordinates(coords) { + if (typeof coords[0] === 'number') { + return translateCoordinate(coords); + } + return coords.map(translateCoordinates); + } + + // Deep clone the input + const result = JSON.parse(JSON.stringify(input)); + + // Handle different GeoJSON types + if (result.type === 'FeatureCollection') { + result.features = result.features.map(feature => { + if (feature.geometry && feature.geometry.coordinates) { + feature.geometry.coordinates = translateCoordinates(feature.geometry.coordinates); + } + return feature; + }); + } else if (result.type === 'Feature') { + if (result.geometry && result.geometry.coordinates) { + result.geometry.coordinates = translateCoordinates(result.geometry.coordinates); + } + } else if (result.coordinates) { + result.coordinates = translateCoordinates(result.coordinates); + } + + return result; + }; +})(); \ No newline at end of file diff --git a/prompts/tasks/Task_Issue_1.md b/prompts/tasks/Task_Issue_1.md new file mode 100644 index 0000000..1149da4 --- /dev/null +++ b/prompts/tasks/Task_Issue_1.md @@ -0,0 +1,183 @@ +# APM Task Assignment: Implement Mock JavaScript Tool Bundle (Issue #1) + +## 1. Agent Role & APM Context + +You are activated as an Implementation Agent within the Agentic Project Management (APM) framework for the ToolVault project. Your role is to execute the assigned task diligently and document your work comprehensively. You will implement a complete JavaScript-based tool bundle that enables UI development and testing without backend dependencies. + +## 2. Task Assignment + +### Reference to GitHub Issue +This assignment corresponds to GitHub Issue #1: "Implement Mock JavaScript Tool Bundle" +- Issue URL: https://github.com/debrief/ToolVault/issues/1 +- Priority: Medium (Enhancement) + +### Objective +Create a fully functional JavaScript tool bundle at `/examples/javascript-bundle/` containing 11 GeoJSON processing tools that execute entirely in the browser, following the specifications in ADR-013. + +### Detailed Action Steps + +#### Phase 1: Setup and Infrastructure +1. **Create the bundle directory structure** at `/examples/javascript-bundle/`: + - Create `tools/` subdirectories: `transform/`, `analysis/`, `statistics/`, `processing/`, `io/` + - Create `data/` directory for sample files + - Create `tests/` directory for Jest unit tests + +2. **Initialize the JavaScript project**: + - Create `package.json` with Jest testing framework + - Configure Jest for browser-compatible JavaScript testing + - Add npm scripts: `"test": "jest"`, `"test:watch": "jest --watch"` + +3. **Create the index.json metadata file**: + - Include `runtime: "javascript"` field for each tool + - Define all 11 tools with complete metadata + - Follow the schema established in ADR-013 + +4. **Generate sample data**: + - Create `data/sample-track.geojson` - a GPS track LineString with timestamp properties + - Ensure timestamps are suitable for speed/direction calculations + +#### Phase 2: Tool Implementation + +**CRITICAL: All tools MUST follow the IIFE pattern specified in ADR-013:** +```javascript +(function() { + window.ToolVault = window.ToolVault || {}; + window.ToolVault.tools = window.ToolVault.tools || {}; + + window.ToolVault.tools.toolName = function(input, params) { + // Implementation + return output; + }; +})(); +``` + +**Transform Tools** (`tools/transform/`): +1. **translate.js** - Translate GeoJSON features + - Inputs: GeoJSON FeatureCollection, direction (degrees), distance (meters) + - Calculate new coordinates using bearing and distance formulas + +2. **flip-horizontal.js** - Flip features horizontally + - Input: GeoJSON FeatureCollection + - Mirror coordinates across specified axis + +3. **flip-vertical.js** - Flip features vertically + - Input: GeoJSON FeatureCollection + - Mirror coordinates across specified axis + +**Analysis Tools** (`tools/analysis/`): +4. **speed-series.js** - Calculate speed time series + - Input: GeoJSON LineString with timestamps + - Output: JSON array with time/speed pairs + - Use Haversine formula for distance calculations + +5. **direction-series.js** - Calculate direction time series + - Input: GeoJSON LineString with timestamps + - Output: JSON array with time/bearing pairs + - Calculate bearing between consecutive points + +**Statistics Tools** (`tools/statistics/`): +6. **average-speed.js** - Calculate average speed + - Input: GeoJSON LineString with timestamps + - Output: Single numeric value + +7. **speed-histogram.js** - Generate speed histogram + - Input: GeoJSON LineString with timestamps + - Parameters: interval_minutes (default: 1), bins (default: 20) + - Output: JSON histogram structure + +**Processing Tools** (`tools/processing/`): +8. **smooth-polyline.js** - Smooth LineString geometry + - Input: GeoJSON LineString + - Parameters: algorithm ("moving_average" or "gaussian"), window_size + - Apply smoothing to coordinate sequence + +**I/O Tools** (`tools/io/`): +9. **import-rep.js** - Parse REP format to GeoJSON + - Input: REP format text + - Output: GeoJSON FeatureCollection + +10. **export-rep.js** - Convert GeoJSON to REP format + - Input: GeoJSON FeatureCollection + - Output: REP format string + +11. **export-csv.js** - Convert GeoJSON to CSV + - Input: GeoJSON FeatureCollection + - Parameters: include_properties, coordinate_format + - Output: CSV string + +#### Phase 3: Testing + +For each tool, create a corresponding test file in `tests/`: +- Use Jest's `describe` and `test` blocks +- Test with the sample GPS track data +- Verify output structure and calculations +- Ensure all tests pass before marking complete + +Example test structure: +```javascript +describe('translateFeatures', () => { + test('should translate features by specified distance and direction', () => { + const input = // sample GeoJSON + const params = { direction: 45, distance: 100 }; + const result = window.ToolVault.tools.translateFeatures(input, params); + expect(result.type).toBe('FeatureCollection'); + // Additional assertions + }); +}); +``` + +### Provide Necessary Context/Assets + +**Reference ADR-013**: Review `/docs/ADRs/ADR-013-mock-javascript-toolset.md` for: +- Complete tool specifications table +- IIFE implementation pattern +- Input/output type definitions +- Bundle structure requirements + +**Technical Constraints**: +- No external dependencies beyond standard Web APIs +- All tools must be synchronous (no async/await) +- Use browser-compatible JavaScript (ES2020+) +- Minimal error handling (assume valid input) + +## 3. Expected Output & Deliverables + +### Success Criteria +- All 11 tools implemented and functional in browser environment +- Jest tests passing for all tools +- index.json contains complete metadata with runtime field +- Sample GPS track data available for testing +- Tools accessible via `window.ToolVault.tools` namespace + +### Deliverables +1. Complete `/examples/javascript-bundle/` directory with: + - Implemented tool files in appropriate subdirectories + - index.json with full metadata + - package.json with Jest configuration + - Sample data file(s) + - Comprehensive test suite + +2. All acceptance criteria from Issue #1 checked off: + - [ ] All 11 tools implemented with IIFE pattern + - [ ] Jest unit tests for each tool with passing status + - [ ] index.json with complete metadata and runtime field + - [ ] Sample GPS track data (single LineString with timestamps) + - [ ] Tools executable in browser environment + +## 4. Memory Bank Logging Instructions + +Upon successful completion of this task, you **must** log your work comprehensively to the project's Memory Bank (if established) or create a detailed implementation summary. Your log should include: +- Reference to GitHub Issue #1 +- List of all files created with their purposes +- Key implementation decisions (e.g., distance calculation methods, coordinate transformation approaches) +- Any challenges encountered and how they were resolved +- Confirmation of all tests passing +- Next steps for GitHub Pages deployment + +## 5. Clarification Instruction + +If any part of this task assignment is unclear, please state your specific questions before proceeding. Pay particular attention to: +- REP file format specification (if not documented) +- Specific algorithms for smoothing functions +- Coordinate system assumptions (WGS84, etc.) +- Expected precision for calculations \ No newline at end of file