feat(init): add polish-lituanian commonwealth & muscovy duchy polygons
prepare all mechanisms for map editingmaster
commit
ccfcef0ca5
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,319 @@
|
|||
// ===============================================
|
||||
// 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();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue