// =============================================== // IMAGE SCANNER - Wykrywanie linii z obrazów // =============================================== const ImageScanner = { canvas: null, ctx: null, imageData: null, imgBounds: null, sampledColor: null, colorTolerance: 30, // Inicjalizacja init() { this.canvas = document.createElement('canvas'); this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); }, // Załaduj obraz do canvas loadImage(imageOverlay, bounds) { return new Promise((resolve, reject) => { const img = imageOverlay.getElement(); if (!img) { reject('Brak elementu obrazu'); return; } this.imgBounds = bounds; this.canvas.width = img.naturalWidth || img.width; this.canvas.height = img.naturalHeight || img.height; this.ctx.drawImage(img, 0, 0); this.imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); resolve(); }); }, // Pobierz kolor z pozycji pixel getColorAtPixel(x, y) { const idx = (y * this.canvas.width + x) * 4; return { r: this.imageData.data[idx], g: this.imageData.data[idx + 1], b: this.imageData.data[idx + 2], a: this.imageData.data[idx + 3] }; }, // Pobierz kolor z pozycji geograficznej getColorAtLatLng(latlng, map) { const pixel = this.latLngToPixel(latlng); if (pixel.x < 0 || pixel.x >= this.canvas.width || pixel.y < 0 || pixel.y >= this.canvas.height) { return null; } return this.getColorAtPixel(Math.floor(pixel.x), Math.floor(pixel.y)); }, // Konwersja lat/lng -> pixel w obrazie latLngToPixel(latlng) { const bounds = this.imgBounds; const x = (latlng.lng - bounds.west) / (bounds.east - bounds.west) * this.canvas.width; const y = (bounds.north - latlng.lat) / (bounds.north - bounds.south) * this.canvas.height; return { x, y }; }, // Konwersja pixel -> lat/lng pixelToLatLng(x, y) { const bounds = this.imgBounds; const lng = bounds.west + (x / this.canvas.width) * (bounds.east - bounds.west); const lat = bounds.north - (y / this.canvas.height) * (bounds.north - bounds.south); return { lat, lng }; }, // Sprawdź czy kolor pasuje do próbki colorMatches(color, sampleColor, tolerance) { if (!color || !sampleColor) return false; const dr = Math.abs(color.r - sampleColor.r); const dg = Math.abs(color.g - sampleColor.g); const db = Math.abs(color.b - sampleColor.b); return dr <= tolerance && dg <= tolerance && db <= tolerance; }, // Skanuj obraz i znajdź wszystkie piksele pasujące do koloru scanForColor(sampleColor, tolerance = 30) { const matchingPixels = []; const width = this.canvas.width; const height = this.canvas.height; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const color = this.getColorAtPixel(x, y); if (this.colorMatches(color, sampleColor, tolerance)) { matchingPixels.push({ x, y }); } } } return matchingPixels; }, // Grupuj piksele w linie (prosty algorytm łączenia sąsiadów) groupPixelsIntoLines(pixels, maxGap = 3) { if (pixels.length === 0) return []; const visited = new Set(); const lines = []; const getKey = (x, y) => `${x},${y}`; const pixelSet = new Set(pixels.map(p => getKey(p.x, p.y))); // Znajdź sąsiadów const getNeighbors = (x, y, gap) => { const neighbors = []; for (let dy = -gap; dy <= gap; dy++) { for (let dx = -gap; dx <= gap; dx++) { if (dx === 0 && dy === 0) continue; const key = getKey(x + dx, y + dy); if (pixelSet.has(key) && !visited.has(key)) { neighbors.push({ x: x + dx, y: y + dy }); } } } return neighbors; }; // BFS do grupowania połączonych pikseli for (const startPixel of pixels) { const startKey = getKey(startPixel.x, startPixel.y); if (visited.has(startKey)) continue; const line = []; const queue = [startPixel]; while (queue.length > 0) { const current = queue.shift(); const key = getKey(current.x, current.y); if (visited.has(key)) continue; visited.add(key); line.push(current); const neighbors = getNeighbors(current.x, current.y, maxGap); queue.push(...neighbors); } if (line.length >= 5) { // Minimum 5 pikseli żeby uznać za linię lines.push(line); } } return lines; }, // Uprość linię (zmniejsz liczbę punktów) simplifyLine(points, tolerance = 2) { if (points.length <= 2) return points; // Algorytm Ramer-Douglas-Peucker const sqDist = (p1, p2) => { const dx = p1.x - p2.x; const dy = p1.y - p2.y; return dx * dx + dy * dy; }; const sqDistToSegment = (p, v, w) => { const l2 = sqDist(v, w); if (l2 === 0) return sqDist(p, v); let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; t = Math.max(0, Math.min(1, t)); return sqDist(p, { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) }); }; const simplifyDP = (points, first, last, tolerance, simplified) => { let maxDist = 0; let index = 0; for (let i = first + 1; i < last; i++) { const dist = sqDistToSegment(points[i], points[first], points[last]); if (dist > maxDist) { maxDist = dist; index = i; } } if (maxDist > tolerance * tolerance) { simplifyDP(points, first, index, tolerance, simplified); simplified.push(points[index]); simplifyDP(points, index, last, tolerance, simplified); } }; const simplified = [points[0]]; simplifyDP(points, 0, points.length - 1, tolerance, simplified); simplified.push(points[points.length - 1]); return simplified; }, // Sortuj punkty wzdłuż linii sortLinePoints(points) { if (points.length <= 2) return points; // Znajdź punkt startowy (najbardziej na lewo-górze) let startIdx = 0; for (let i = 1; i < points.length; i++) { if (points[i].y < points[startIdx].y || (points[i].y === points[startIdx].y && points[i].x < points[startIdx].x)) { startIdx = i; } } const sorted = [points[startIdx]]; const remaining = [...points]; remaining.splice(startIdx, 1); while (remaining.length > 0) { const last = sorted[sorted.length - 1]; let nearestIdx = 0; let nearestDist = Infinity; for (let i = 0; i < remaining.length; i++) { const dx = remaining[i].x - last.x; const dy = remaining[i].y - last.y; const dist = dx * dx + dy * dy; if (dist < nearestDist) { nearestDist = dist; nearestIdx = i; } } sorted.push(remaining[nearestIdx]); remaining.splice(nearestIdx, 1); } return sorted; }, // Główna funkcja: skanuj i zwróć koordynaty geograficzne async scan(sampleColor, options = {}) { const { tolerance = 30, simplifyTolerance = 3, maxGap = 5, minPoints = 10 } = options; // Skanuj piksele const matchingPixels = this.scanForColor(sampleColor, tolerance); console.log(`Znaleziono ${matchingPixels.length} pasujących pikseli`); if (matchingPixels.length === 0) { return { lines: [], polygons: [] }; } // Grupuj w linie const pixelLines = this.groupPixelsIntoLines(matchingPixels, maxGap); console.log(`Zgrupowano w ${pixelLines.length} linii`); // Przetwórz każdą linię const results = { lines: [], polygons: [] }; for (const pixelLine of pixelLines) { if (pixelLine.length < minPoints) continue; // Sortuj punkty const sorted = this.sortLinePoints(pixelLine); // Uprość linię const simplified = this.simplifyLine(sorted, simplifyTolerance); // Konwertuj na koordynaty geograficzne const coords = simplified.map(p => { const latLng = this.pixelToLatLng(p.x, p.y); return [Number(latLng.lng.toFixed(4)), Number(latLng.lat.toFixed(4))]; }); // Sprawdź czy to zamknięty polygon const first = coords[0]; const last = coords[coords.length - 1]; const isClosed = Math.abs(first[0] - last[0]) < 0.01 && Math.abs(first[1] - last[1]) < 0.01; if (isClosed && coords.length >= 4) { // Zamknij polygon coords.push([...coords[0]]); results.polygons.push(coords); } else { results.lines.push(coords); } } return results; }, // Konwertuj hex color na RGB hexToRgb(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }, // Konwertuj RGB na hex rgbToHex(r, g, b) { return '#' + [r, g, b].map(x => { const hex = x.toString(16); return hex.length === 1 ? '0' + hex : hex; }).join(''); } }; // Inicjalizuj przy załadowaniu ImageScanner.init();