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