feat(init): add polish-lituanian commonwealth & muscovy duchy polygons

prepare all mechanisms for map editing
master
TBS093A 2025-12-12 16:38:02 +01:00
commit ccfcef0ca5
4 changed files with 20920 additions and 0 deletions

1043
index.html 100644

File diff suppressed because it is too large Load Diff

319
js/image-scanner.js 100644
View File

@ -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