generated from Grigo/AndroidTemplate
added linear slider
This commit is contained in:
+145
-11
@@ -158,6 +158,14 @@
|
||||
}
|
||||
#mapRulerTools button.active { background: #00ff88; color: #111; border-color: #00ff88; }
|
||||
#mapRulerHint { font-size: 0.7rem; color: #aaa; margin-bottom: 4px; min-height: 1em; }
|
||||
#mapRulerPointsRow {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap;
|
||||
font-size: 0.7rem; color: #ccc;
|
||||
}
|
||||
#mapRulerPointsRow label { display: flex; align-items: center; gap: 4px; cursor: pointer; white-space: nowrap; }
|
||||
#mapRulerPointsSlider { flex: 1; min-width: 120px; accent-color: #00ff88; }
|
||||
#mapRulerPointsSlider:disabled { opacity: 0.45; }
|
||||
#mapRulerPointsLabel { min-width: 7em; color: #aaa; white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -188,6 +196,11 @@
|
||||
<button type="button" id="btnRulerClear">Сброс</button>
|
||||
</div>
|
||||
<div id="mapRulerHint">Клик на карте — точка A, затем точка B</div>
|
||||
<div id="mapRulerPointsRow">
|
||||
<label><input type="checkbox" id="mapRulerPointsAuto" checked /> Авто</label>
|
||||
<input type="range" id="mapRulerPointsSlider" min="20" max="500" value="100" disabled />
|
||||
<span id="mapRulerPointsLabel">100 точек</span>
|
||||
</div>
|
||||
<canvas id="mapRulerCanvas" width="800" height="120"></canvas>
|
||||
</div>
|
||||
<div id="map"></div>
|
||||
@@ -368,6 +381,11 @@
|
||||
let mapRulerChartHover = false;
|
||||
let mapRulerLineHover = false;
|
||||
let mapRulerLeaveTimer = null;
|
||||
const RULER_POINTS_MIN = 20;
|
||||
const RULER_POINTS_MAX = 500;
|
||||
let mapRulerPointsAuto = true;
|
||||
let mapRulerManualPoints = 100;
|
||||
let mapRulerReloadTimer = null;
|
||||
|
||||
const DEVICE_POLL_MS = 1000;
|
||||
const CHAT_POLL_MS = 2500;
|
||||
@@ -600,6 +618,48 @@
|
||||
return pts;
|
||||
}
|
||||
|
||||
function autoRulerTargetPoints(distM) {
|
||||
if (distM < 1) return RULER_POINTS_MIN;
|
||||
if (distM <= 200) return Math.max(RULER_POINTS_MIN, Math.round(distM / 4));
|
||||
if (distM <= 1000) return Math.max(50, Math.round(distM / 8));
|
||||
if (distM <= 5000) return Math.max(80, Math.round(distM / 15));
|
||||
return Math.min(RULER_POINTS_MAX, Math.max(100, Math.round(distM / 20)));
|
||||
}
|
||||
|
||||
function getMapRulerTargetPoints(distM) {
|
||||
if (mapRulerPointsAuto) return autoRulerTargetPoints(distM);
|
||||
return Math.max(RULER_POINTS_MIN, Math.min(RULER_POINTS_MAX, mapRulerManualPoints));
|
||||
}
|
||||
|
||||
function updateMapRulerPointsUi(distM) {
|
||||
const autoEl = document.getElementById('mapRulerPointsAuto');
|
||||
const slider = document.getElementById('mapRulerPointsSlider');
|
||||
const label = document.getElementById('mapRulerPointsLabel');
|
||||
if (!autoEl || !slider || !label) return;
|
||||
const effective = getMapRulerTargetPoints(distM || 0);
|
||||
const autoVal = autoRulerTargetPoints(distM || 0);
|
||||
autoEl.checked = mapRulerPointsAuto;
|
||||
slider.disabled = mapRulerPointsAuto;
|
||||
slider.value = String(mapRulerPointsAuto ? autoVal : mapRulerManualPoints);
|
||||
if (mapRulerPointsAuto && distM > 0) {
|
||||
const step = effective > 1 ? distM / (effective - 1) : distM;
|
||||
label.textContent = `${effective} точек · ~${step.toFixed(1)} м`;
|
||||
} else if (mapRulerPointsAuto) {
|
||||
label.textContent = `${autoVal} точек · авто`;
|
||||
} else {
|
||||
label.textContent = `${effective} точек`;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleMapRulerProfileReload() {
|
||||
clearTimeout(mapRulerReloadTimer);
|
||||
mapRulerReloadTimer = setTimeout(() => {
|
||||
if (mapRulerPtA && mapRulerPtB) {
|
||||
loadMapRulerProfileFromPoints(mapRulerPtA, mapRulerPtB);
|
||||
}
|
||||
}, 350);
|
||||
}
|
||||
|
||||
function makeRulerPointIcon(label, color) {
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
@@ -762,16 +822,21 @@
|
||||
setMapRulerStatus('загрузка…');
|
||||
updateMapRulerLineLayer(a, b);
|
||||
drawMapRulerChart();
|
||||
const linePts = buildDirectLinePoints(a, b, 10);
|
||||
elevProfileMapLine = await fetchElevationProfile(linePts);
|
||||
mapRulerLoadState = 'done';
|
||||
const dist = haversineM(a.lat, a.lon, b.lat, b.lon);
|
||||
const targetPoints = getMapRulerTargetPoints(dist);
|
||||
updateMapRulerPointsUi(dist);
|
||||
const linePts = [{ lat: a.lat, lon: a.lon }, { lat: b.lat, lon: b.lon }];
|
||||
elevProfileMapLine = await fetchElevationProfile(linePts, null, { targetPoints });
|
||||
mapRulerLoadState = 'done';
|
||||
const n = elevationPointCount(elevProfileMapLine);
|
||||
if (n > 0) {
|
||||
const src = elevProfileMapLine.source === 'elevation' ? 'высоты'
|
||||
: elevProfileMapLine.source === 'server' ? 'сервер'
|
||||
: elevProfileMapLine.source || 'данные';
|
||||
mapRulerBaseStatus = `${dist.toFixed(0)} m · ${src} · ${n} точек`;
|
||||
const step = elevProfileMapLine.step_m != null
|
||||
? elevProfileMapLine.step_m
|
||||
: (n > 1 ? dist / (n - 1) : dist);
|
||||
mapRulerBaseStatus = `${dist.toFixed(0)} m · ${src} · ${n} точек · ~${Number(step).toFixed(1)} m`;
|
||||
setMapRulerStatus(mapRulerBaseStatus);
|
||||
setMapRulerHint('Наведите на линию или график — высота и позиция');
|
||||
} else {
|
||||
@@ -848,6 +913,34 @@
|
||||
return samples;
|
||||
}
|
||||
|
||||
function resampleTrackPathCount(points, count) {
|
||||
const cleaned = [];
|
||||
for (const p of points) {
|
||||
if (p.lat == null || p.lon == null) continue;
|
||||
const lat = Number(p.lat);
|
||||
const lon = Number(p.lon);
|
||||
if (!cleaned.length || haversineM(cleaned[cleaned.length - 1][0], cleaned[cleaned.length - 1][1], lat, lon) > 0.5) {
|
||||
cleaned.push([lat, lon]);
|
||||
}
|
||||
}
|
||||
if (!cleaned.length || count < 2) return [];
|
||||
if (cleaned.length === 1) return [{ lat: cleaned[0][0], lon: cleaned[0][1], dist_m: 0 }];
|
||||
const cum = [0];
|
||||
for (let i = 1; i < cleaned.length; i++) {
|
||||
cum.push(cum[i - 1] + haversineM(cleaned[i - 1][0], cleaned[i - 1][1], cleaned[i][0], cleaned[i][1]));
|
||||
}
|
||||
const total = cum[cum.length - 1];
|
||||
if (total < 1e-6) return [{ lat: cleaned[0][0], lon: cleaned[0][1], dist_m: 0 }];
|
||||
const n = Math.max(2, Math.min(RULER_POINTS_MAX, Math.round(count)));
|
||||
const samples = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const dist = (total * i) / (n - 1);
|
||||
const [lat, lon] = interpTrackAtDist(cleaned, cum, dist);
|
||||
samples.push({ lat, lon, dist_m: Math.round(dist * 10) / 10 });
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
function nearestElevation(points, lat, lon) {
|
||||
let best = null;
|
||||
let bestD = Infinity;
|
||||
@@ -866,8 +959,10 @@
|
||||
return profile.points.filter(p => p.elevation_m != null && !Number.isNaN(p.elevation_m)).length;
|
||||
}
|
||||
|
||||
function buildLocalElevationProfile(points, stepM = 10) {
|
||||
const samples = resampleTrackPath(points, stepM);
|
||||
function buildLocalElevationProfile(points, stepM = 10, targetPoints = null) {
|
||||
const samples = targetPoints != null
|
||||
? resampleTrackPathCount(points, targetPoints)
|
||||
: resampleTrackPath(points, stepM);
|
||||
if (!samples.length) return null;
|
||||
const profilePts = samples.map(s => ({
|
||||
dist_m: s.dist_m,
|
||||
@@ -877,8 +972,11 @@
|
||||
}));
|
||||
const elevVals = profilePts.map(p => p.elevation_m).filter(v => v != null);
|
||||
if (!elevVals.length) return null;
|
||||
const effStep = targetPoints != null && profilePts.length > 1
|
||||
? (profilePts[profilePts.length - 1].dist_m - profilePts[0].dist_m) / (profilePts.length - 1)
|
||||
: stepM;
|
||||
return {
|
||||
step_m: stepM,
|
||||
step_m: Math.round(effStep * 10) / 10,
|
||||
total_m: profilePts[profilePts.length - 1].dist_m,
|
||||
min_elevation_m: Math.min(...elevVals),
|
||||
max_elevation_m: Math.max(...elevVals),
|
||||
@@ -931,13 +1029,14 @@
|
||||
return elevationPointCount(profile) > 0 ? profile : profile;
|
||||
}
|
||||
|
||||
async function fetchElevationProfile(points, trackId) {
|
||||
async function fetchElevationProfile(points, trackId, options = {}) {
|
||||
if (!points || !points.length) return null;
|
||||
const { targetPoints = null, stepM = 10 } = options;
|
||||
let lastError = null;
|
||||
|
||||
const health = await ensureElevationApi();
|
||||
if (!health.ok) {
|
||||
const cached = buildLocalElevationProfile(points, 10);
|
||||
const cached = buildLocalElevationProfile(points, stepM, targetPoints);
|
||||
if (cached) return cached;
|
||||
return {
|
||||
points: [],
|
||||
@@ -964,10 +1063,13 @@
|
||||
}
|
||||
|
||||
try {
|
||||
const body = targetPoints != null
|
||||
? { points, target_points: targetPoints }
|
||||
: { points, step_m: stepM };
|
||||
const res = await fetch('/api/elevation/profile', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ points, step_m: 10 })
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = normalizeServerProfile(await res.json());
|
||||
@@ -980,7 +1082,7 @@
|
||||
lastError = lastError || String(e.message || e);
|
||||
}
|
||||
|
||||
const cached = buildLocalElevationProfile(points, 10);
|
||||
const cached = buildLocalElevationProfile(points, stepM, targetPoints);
|
||||
if (cached) return cached;
|
||||
|
||||
return {
|
||||
@@ -1211,6 +1313,7 @@
|
||||
setMapRulerHint('');
|
||||
} else {
|
||||
setMapRulerMode(mapRulerMode);
|
||||
updateMapRulerPointsUi(0);
|
||||
}
|
||||
setTimeout(() => map.invalidateSize(), 80);
|
||||
}
|
||||
@@ -1924,6 +2027,37 @@
|
||||
};
|
||||
document.getElementById('btnRulerClear').onclick = () => resetMapRulerPick();
|
||||
|
||||
(function bindMapRulerPointsControls() {
|
||||
const autoEl = document.getElementById('mapRulerPointsAuto');
|
||||
const slider = document.getElementById('mapRulerPointsSlider');
|
||||
if (!autoEl || !slider) return;
|
||||
autoEl.addEventListener('change', () => {
|
||||
mapRulerPointsAuto = autoEl.checked;
|
||||
if (!mapRulerPointsAuto) {
|
||||
mapRulerManualPoints = Number(slider.value) || 100;
|
||||
}
|
||||
const dist = mapRulerPtA && mapRulerPtB
|
||||
? haversineM(mapRulerPtA.lat, mapRulerPtA.lon, mapRulerPtB.lat, mapRulerPtB.lon)
|
||||
: 0;
|
||||
updateMapRulerPointsUi(dist);
|
||||
scheduleMapRulerProfileReload();
|
||||
});
|
||||
slider.addEventListener('input', () => {
|
||||
if (mapRulerPointsAuto) return;
|
||||
mapRulerManualPoints = Number(slider.value) || RULER_POINTS_MIN;
|
||||
updateMapRulerPointsUi(
|
||||
mapRulerPtA && mapRulerPtB
|
||||
? haversineM(mapRulerPtA.lat, mapRulerPtA.lon, mapRulerPtB.lat, mapRulerPtB.lon)
|
||||
: 0
|
||||
);
|
||||
});
|
||||
slider.addEventListener('change', () => {
|
||||
if (mapRulerPointsAuto) return;
|
||||
mapRulerManualPoints = Number(slider.value) || RULER_POINTS_MIN;
|
||||
scheduleMapRulerProfileReload();
|
||||
});
|
||||
})();
|
||||
|
||||
(function bindMapRulerChartProbe() {
|
||||
const canvas = document.getElementById('mapRulerCanvas');
|
||||
if (!canvas) return;
|
||||
|
||||
Reference in New Issue
Block a user