222 lines
9.0 KiB
HTML
222 lines
9.0 KiB
HTML
<!doctype html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="utf-8"/>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
<title>SVG from LINESTRINGs</title>
|
|
<style>
|
|
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; padding: 12px; }
|
|
#mapWrap { width: 100%; height: 80vh; border: 1px solid #ddd; background: #f8f9fb; }
|
|
svg { width: 100%; height: 100%; display: block; }
|
|
.line-sens-1 { stroke: #1f77b4; stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; }
|
|
.line-sens-2 { stroke: #ff7f0e; stroke-width: 3; fill: none; stroke-linecap: round; stroke-linejoin: round; }
|
|
.hovered { stroke-width: 5; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2)); }
|
|
.vertex { r: 2.2; fill: rgba(0,0,0,0.5); pointer-events: none; }
|
|
.tooltip {
|
|
position: absolute;
|
|
pointer-events: none;
|
|
background: rgba(0,0,0,0.75);
|
|
color: white;
|
|
padding: 4px 6px;
|
|
font-size: 12px;
|
|
border-radius: 4px;
|
|
transform: translate(8px, 8px);
|
|
display: none;
|
|
z-index: 10;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>Visualisation SVG des LINESTRING</h1>
|
|
<div id="mapWrap"></div>
|
|
<div id="tooltip" class="tooltip"></div>
|
|
|
|
<script>
|
|
/* --- données fournies par l'utilisateur --- */
|
|
const data = [
|
|
{
|
|
"fk_route_uuid": "8d004027-3279-4eb8-8de4-7f40ef542111",
|
|
"sens": 1,
|
|
"st_astext": "LINESTRING(240786.25000013656 79036.06584277935,240789.6701001395 79034.92654277757,240795.80500013664 79032.88284277637,240816.54300013464 79021.48284277692,240848.48400013373 79003.72684277594,240862.6130001347 78996.93084277771,240869.41630013572 78995.22494277824,240875.52820013772 78995.85994277615,240878.9446763978 78999.20556702279,240883.3350001378 79003.50484277774,240885.7940001368 79005.9478427777,240890.99600013572 79019.1748427758,240893.19700013375 79022.76474277768,240895.1890001397 79026.01384277735)"
|
|
},
|
|
{
|
|
"fk_route_uuid": "8d004027-3279-4eb8-8de4-7f40ef542111",
|
|
"sens": 2,
|
|
"st_astext": "LINESTRING(240876.93050013762 79027.10734277777,240880.75500013976 79023.49284277484,240883.52900013572 79017.29684277717,240884.25950013375 79012.2112427773,240884.10070013476 79007.92494277749,240883.3350001378 79003.50484277774,240878.94468326174 78999.20557374507,240875.52820013772 78995.85994277615,240869.41630013572 78995.22494277824,240862.6130001347 78996.93084277771,240848.48400013373 79003.72684277594,240816.54300013464 79021.48284277692,240795.80500013664 79032.88284277637,240789.6701001395 79034.92654277757)"
|
|
},
|
|
{
|
|
"fk_route_uuid": "e8af74b2-62bb-4ed9-841a-d72f01a471bf",
|
|
"sens": 1,
|
|
"st_astext": "LINESTRING(240788.70500013442 79077.95804277528,240726.47800013627 79107.39084277768,240719.76930013526 79110.84164277464,240718.30300013922 79111.59584277496,240713.81800014016 79116.28184277564,240711.32100013818 79123.2998427758,240711.94600013812 79131.3228427777,240710.94900013716 79134.09984277561,240718.46600013407 79143.87984277774,240728.20900013513 79148.93084277678,240737.10500013322 79148.99684277643,240749.14700013614 79143.51984277926,240772.7741277222 79129.7665901715,240784.77000013928 79122.7838427769,240811.7210001374 79105.72584277857)"
|
|
},
|
|
{
|
|
"fk_route_uuid": "e8af74b2-62bb-4ed9-841a-d72f01a471bf",
|
|
"sens": 2,
|
|
"st_astext": "LINESTRING(240811.7210001374 79105.72584277857,240784.77000013928 79122.7838427769,240772.77412333922 79129.76659272239,240749.14700013614 79143.51984277926,240737.10500013322 79148.99684277643,240728.20900013513 79148.93084277678,240718.46600013407 79143.87984277774,240710.94900013716 79134.09984277561,240708.14400013717 79132.68284277525,240702.59600013716 79122.08384277672,240697.10920013717 79117.31394277606)"
|
|
}
|
|
];
|
|
|
|
/* --- utilitaires --- */
|
|
|
|
/** Parse un WKT LINESTRING et renvoie un array de [x,y] */
|
|
function parseLinestring(wkt) {
|
|
// wkt attendu: "LINESTRING(x1 y1,x2 y2,...)"
|
|
const inner = wkt.replace(/^\s*LINESTRING\s*\(|\)\s*$/gi, '');
|
|
const parts = inner.split(',');
|
|
return parts.map(p => {
|
|
const [xs, ys] = p.trim().split(/\s+/);
|
|
return [parseFloat(xs), parseFloat(ys)];
|
|
});
|
|
}
|
|
|
|
/** calcule bbox pour toutes les géométries */
|
|
function computeBBox(allCoords) {
|
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
allCoords.forEach(coords => {
|
|
coords.forEach(([x,y]) => {
|
|
if (x < minX) minX = x;
|
|
if (y < minY) minY = y;
|
|
if (x > maxX) maxX = x;
|
|
if (y > maxY) maxY = y;
|
|
});
|
|
});
|
|
return { minX, minY, maxX, maxY };
|
|
}
|
|
|
|
/* --- construction du SVG --- */
|
|
(function render() {
|
|
const parsed = data.map(item => {
|
|
return {
|
|
fk_route_uuid: item.fk_route_uuid,
|
|
sens: item.sens,
|
|
coords: parseLinestring(item.st_astext)
|
|
};
|
|
});
|
|
|
|
const allCoords = parsed.map(p => p.coords);
|
|
const bbox = computeBBox(allCoords);
|
|
|
|
// padding en unités réelles (10% de la plus grande dimension)
|
|
const padFactor = 0.08;
|
|
const widthReal = bbox.maxX - bbox.minX || 1;
|
|
const heightReal = bbox.maxY - bbox.minY || 1;
|
|
const padX = widthReal * padFactor;
|
|
const padY = heightReal * padFactor;
|
|
const viewMinX = bbox.minX - padX;
|
|
const viewMinY = bbox.minY - padY;
|
|
const viewMaxX = bbox.maxX + padX;
|
|
const viewMaxY = bbox.maxY + padY;
|
|
const viewWidth = viewMaxX - viewMinX;
|
|
const viewHeight = viewMaxY - viewMinY;
|
|
|
|
// on veut que Y augmente vers le bas en SVG => on va inverser Y en transformant
|
|
// la vue en utilisant viewBox et en appliquant une transformation sur un <g>
|
|
const mapWrap = document.getElementById('mapWrap');
|
|
const svgNS = 'http://www.w3.org/2000/svg';
|
|
|
|
const svg = document.createElementNS(svgNS, 'svg');
|
|
// viewBox: minX minY width height — on met minX, minY = viewMinX, viewMinY, mais
|
|
// pour inverser Y on va créer un group <g transform="scale(1,-1) translate(0, -H)">
|
|
svg.setAttribute('viewBox', `${viewMinX} ${viewMinY} ${viewWidth} ${viewHeight}`);
|
|
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
|
|
|
// container group : on doit inverser Y axis because coordinates are in projected system
|
|
// Approach: apply transform to flip Y around center. Simpler: we apply transform to map coordinates to SVG coords when building path.
|
|
// Here we'll use transform on group: scale(1,-1) and translate(0, - (viewMinY + viewMaxY) )
|
|
const g = document.createElementNS(svgNS, 'g');
|
|
// translate to flip around the middle of viewbox
|
|
const translateY = -(viewMinY + viewMaxY);
|
|
g.setAttribute('transform', `scale(1,-1) translate(0, ${translateY})`);
|
|
|
|
// for each parsed geometry create a path
|
|
parsed.forEach((item, idx) => {
|
|
const d = item.coords.map((pt, i) => {
|
|
const [x, y] = pt;
|
|
return (i === 0 ? 'M ' : 'L ') + x + ' ' + y;
|
|
}).join(' ');
|
|
|
|
const path = document.createElementNS(svgNS, 'path');
|
|
const cls = item.sens === 1 ? 'line-sens-1' : 'line-sens-2';
|
|
path.setAttribute('d', d);
|
|
path.setAttribute('class', cls);
|
|
path.setAttribute('data-index', idx);
|
|
path.setAttribute('data-uuid', item.fk_route_uuid);
|
|
path.setAttribute('data-sens', item.sens);
|
|
// cursor crosshair on hover:
|
|
path.style.cursor = 'crosshair';
|
|
|
|
// events
|
|
path.addEventListener('mouseenter', (e) => {
|
|
path.classList.add('hovered');
|
|
showTooltip(e, `route: ${item.fk_route_uuid}<br>sens: ${item.sens}`);
|
|
});
|
|
path.addEventListener('mousemove', (e) => {
|
|
moveTooltip(e);
|
|
});
|
|
path.addEventListener('mouseleave', () => {
|
|
path.classList.remove('hovered');
|
|
hideTooltip();
|
|
});
|
|
path.addEventListener('dblclick', (e) => {
|
|
e.stopPropagation();
|
|
// action au double-clic : highlight et log — personnalise ici
|
|
console.log('Double-clic sur', {
|
|
uuid: item.fk_route_uuid,
|
|
sens: item.sens,
|
|
index: idx
|
|
});
|
|
// effet visuel bref
|
|
path.classList.add('hovered');
|
|
setTimeout(() => path.classList.remove('hovered'), 700);
|
|
// si tu veux centrer / zoomer sur la géométrie, tu peux ajouter ici
|
|
});
|
|
|
|
g.appendChild(path);
|
|
|
|
// optionally draw small vertices (disable pointer events to avoid fuss)
|
|
item.coords.forEach(([x,y]) => {
|
|
const c = document.createElementNS(svgNS, 'circle');
|
|
c.setAttribute('cx', x);
|
|
c.setAttribute('cy', y);
|
|
c.setAttribute('r', 0.0); // mettre petit rayon si tu veux les voir, sinon 0
|
|
c.setAttribute('class', 'vertex');
|
|
g.appendChild(c);
|
|
});
|
|
});
|
|
|
|
svg.appendChild(g);
|
|
mapWrap.appendChild(svg);
|
|
|
|
|
|
/* Tooltip utilities */
|
|
const tooltip = document.getElementById('tooltip');
|
|
function showTooltip(evt, html) {
|
|
tooltip.innerHTML = html;
|
|
tooltip.style.display = 'block';
|
|
moveTooltip(evt);
|
|
}
|
|
function moveTooltip(evt) {
|
|
// placer le tooltip à côté du pointeur (page coords)
|
|
const x = evt.clientX;
|
|
const y = evt.clientY;
|
|
tooltip.style.left = (x + 10) + 'px';
|
|
tooltip.style.top = (y + 10) + 'px';
|
|
}
|
|
function hideTooltip() {
|
|
tooltip.style.display = 'none';
|
|
}
|
|
|
|
// clic global (ex: décocher highlight / action)
|
|
svg.addEventListener('click', () => {
|
|
// exemple : retirer highlight de toutes les lignes
|
|
svg.querySelectorAll('path.hovered').forEach(p => p.classList.remove('hovered'));
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|