stations offset fix, live geo tracking, sights on route
All checks were successful
release-tag / release-image (push) Successful in 54s
All checks were successful
release-tag / release-image (push) Successful in 54s
This commit is contained in:
@ -25,8 +25,9 @@
|
||||
<script>
|
||||
import * as PIXI from "pixi.js";
|
||||
import { Viewport } from "pixi-viewport";
|
||||
import { Text, TextStyle, FillGradient } from "pixi.js";
|
||||
import { Text, TextStyle, FillGradient, Assets } from "pixi.js";
|
||||
import { API_URL, GEO_URL } from "../config";
|
||||
import sightIcon from "../icons/sight.svg";
|
||||
|
||||
export default {
|
||||
name: "MainMap",
|
||||
@ -38,10 +39,16 @@ export default {
|
||||
routeGraphics: null,
|
||||
videoOverlayVisible: false,
|
||||
stationContainer: null,
|
||||
sightsContainer: null,
|
||||
toCanvasPoint: null,
|
||||
routePollTimer: null,
|
||||
attemptCount: 0,
|
||||
routeGroup: null,
|
||||
routeLatlngs: [],
|
||||
tramPollTimer: null,
|
||||
passedGraphics: null,
|
||||
remainingGraphics: null,
|
||||
tramGraphic: null,
|
||||
routeRotate: 0,
|
||||
routeCentered: false,
|
||||
viewportAdjusted: false, // авто‑фит сделан?
|
||||
@ -72,6 +79,7 @@ export default {
|
||||
|
||||
// add canvas to DOM (missing after last refactor)
|
||||
container.appendChild(this.pixiApp.canvas);
|
||||
await Assets.load(sightIcon);
|
||||
|
||||
// гарантируем, что канвас всегда принимает pointer‑/touch‑события
|
||||
this.pixiApp.canvas.style.pointerEvents = "auto";
|
||||
@ -103,9 +111,15 @@ export default {
|
||||
|
||||
this.routeGraphics = new PIXI.Graphics();
|
||||
this.routeGroup.addChild(this.routeGraphics);
|
||||
this.passedGraphics = new PIXI.Graphics();
|
||||
this.routeGroup.addChild(this.passedGraphics);
|
||||
this.remainingGraphics = new PIXI.Graphics();
|
||||
this.routeGroup.addChild(this.remainingGraphics);
|
||||
|
||||
this.stationContainer = new PIXI.Container();
|
||||
this.routeGroup.addChild(this.stationContainer);
|
||||
this.sightsContainer = new PIXI.Container();
|
||||
this.routeGroup.addChild(this.sightsContainer);
|
||||
},
|
||||
// Применяем ограничения масштабирования к viewport
|
||||
updateViewportClampZoom() {
|
||||
@ -114,11 +128,13 @@ export default {
|
||||
this.routeScaleMin !== null &&
|
||||
this.routeScaleMax !== null
|
||||
) {
|
||||
const minScale = this.routeScaleMin / 20;
|
||||
const minScale = this.routeScaleMin / 30;
|
||||
const maxScale = this.routeScaleMax / 20;
|
||||
// убедимся, что min < max
|
||||
if (minScale < maxScale) {
|
||||
this.viewport.clampZoom({ minScale, maxScale });
|
||||
// apply initial zoom at minimum scale
|
||||
this.viewport.setZoom(minScale);
|
||||
console.log(
|
||||
`%cViewport clampZoom → min=${minScale}, max=${maxScale}`,
|
||||
"color: violet;"
|
||||
@ -141,7 +157,7 @@ export default {
|
||||
this.routeGraphics.clear();
|
||||
this.routeGraphics.setStrokeStyle({
|
||||
width: 10,
|
||||
color: "#ED1C24",
|
||||
color: 0xffffff,
|
||||
alpha: 1,
|
||||
});
|
||||
this.routeGraphics.beginPath();
|
||||
@ -230,7 +246,8 @@ export default {
|
||||
|
||||
const { x, y } = this.toCanvasPoint([st.latitude, st.longitude]);
|
||||
const ox = (st.offset_x || 0) / 1.25; // смещение по X (½)
|
||||
const oy = (st.offset_y || 0) * 1; // смещение по Y (½)
|
||||
const oy = st.offset_y || 0; // смещение по Y (½)
|
||||
// const oy = 1;
|
||||
if (isNaN(x) || isNaN(y)) {
|
||||
console.warn(`station[${idx}] (${st.id}) координаты NaN → пропущен`);
|
||||
return;
|
||||
@ -243,19 +260,17 @@ export default {
|
||||
this.stationContainer.addChild(node);
|
||||
|
||||
const g = new PIXI.Graphics();
|
||||
g.fill("#ED1C24")
|
||||
// initially white fill for all stations
|
||||
g.fill(0xffffff)
|
||||
.stroke({ width: 5, color: 0x000000 })
|
||||
.circle(0, 0, 11)
|
||||
.fill()
|
||||
.stroke();
|
||||
node.addChild(g);
|
||||
// store station coordinates and graphic for dynamic recoloring
|
||||
node._stationLatlng = [st.latitude, st.longitude];
|
||||
node._circleGraphic = g;
|
||||
|
||||
// const g = new PIXI.Graphics();
|
||||
// g.fill(0xffffff).stroke({ width: 2, color: 0x000000 });
|
||||
// g.circle(0, 0, 60);
|
||||
// node.addChild(g);
|
||||
|
||||
/* 2. Двухстрочная подпись (RU + EN) */
|
||||
const solidWhite = new FillGradient({
|
||||
type: "linear",
|
||||
colorStops: [
|
||||
@ -264,6 +279,14 @@ export default {
|
||||
],
|
||||
});
|
||||
|
||||
const lightGray = new FillGradient({
|
||||
type: "linear",
|
||||
colorStops: [
|
||||
{ offset: 0, color: "#CBCBCB" },
|
||||
{ offset: 1, color: "#CBCBCB" },
|
||||
],
|
||||
});
|
||||
|
||||
const labelGroup = new PIXI.Container();
|
||||
labelGroup.x = ox;
|
||||
labelGroup.y = oy;
|
||||
@ -272,6 +295,52 @@ export default {
|
||||
labelGroup._oy = oy;
|
||||
node.addChild(labelGroup);
|
||||
|
||||
const stationPoint = { x, y };
|
||||
const thresholdX = 15;
|
||||
const thresholdY = 30;
|
||||
let shiftX = 0,
|
||||
shiftY = 0;
|
||||
if (this.routeLatlngs && this.routeLatlngs.length > 1) {
|
||||
for (let i = 0; i < this.routeLatlngs.length - 1; i++) {
|
||||
const p1 = this.toCanvasPoint(this.routeLatlngs[i]);
|
||||
const p2 = this.toCanvasPoint(this.routeLatlngs[i + 1]);
|
||||
const vx = p2.x - p1.x,
|
||||
vy = p2.y - p1.y;
|
||||
const len2 = vx * vx + vy * vy;
|
||||
const t =
|
||||
((stationPoint.x - p1.x) * vx + (stationPoint.y - p1.y) * vy) /
|
||||
(len2 || 1);
|
||||
const tClamped = Math.max(0, Math.min(1, t));
|
||||
const projX = p1.x + vx * tClamped;
|
||||
const projY = p1.y + vy * tClamped;
|
||||
const dx = stationPoint.x - projX,
|
||||
dy = stationPoint.y - projY;
|
||||
// elliptical distance check
|
||||
const normDist =
|
||||
(dx * dx) / (thresholdX * thresholdX) +
|
||||
(dy * dy) / (thresholdY * thresholdY);
|
||||
if (normDist < 1) {
|
||||
// compute normal to segment
|
||||
let nx = -vy,
|
||||
ny = vx;
|
||||
const nlen = Math.hypot(nx, ny) || 1;
|
||||
nx /= nlen;
|
||||
ny /= nlen;
|
||||
// choose direction based on original offset
|
||||
const dot = ox * nx + oy * ny;
|
||||
const sign = dot >= 0 ? 1 : -1;
|
||||
shiftX = nx * thresholdX * sign;
|
||||
shiftY = ny * thresholdY * sign;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// apply shift
|
||||
labelGroup.x += shiftX;
|
||||
labelGroup.y += shiftY;
|
||||
labelGroup._ox += shiftX;
|
||||
labelGroup._oy += shiftY;
|
||||
|
||||
// RU
|
||||
const ruStyle = new TextStyle({
|
||||
fill: solidWhite,
|
||||
@ -283,15 +352,15 @@ export default {
|
||||
const ruText = new Text({ text: st.name, style: ruStyle });
|
||||
ruText.anchor.set(1, 0.5); // bottom-right
|
||||
ruText.x = 24;
|
||||
ruText.y = -20;
|
||||
ruText.y = -35;
|
||||
labelGroup.addChild(ruText);
|
||||
|
||||
// EN
|
||||
const enName = enById[st.id];
|
||||
if (enName) {
|
||||
const enStyle = new TextStyle({
|
||||
fill: solidWhite,
|
||||
fontSize: 16,
|
||||
fill: lightGray,
|
||||
fontSize: 12,
|
||||
fontFamily: "Arial",
|
||||
align: "right",
|
||||
fontWeight: "bold",
|
||||
@ -326,10 +395,6 @@ export default {
|
||||
position: { x: worldPos.x, y: worldPos.y },
|
||||
easing: "easeInOutSine",
|
||||
});
|
||||
|
||||
this.viewport.once("moved-end", () => {
|
||||
this.applyPanClamp();
|
||||
});
|
||||
this.stationZoomed = true;
|
||||
console.log(
|
||||
`%c[centerToStation] world=(${worldPos.x.toFixed(
|
||||
@ -366,6 +431,11 @@ export default {
|
||||
}
|
||||
});
|
||||
});
|
||||
if (this.sightsContainer) {
|
||||
this.sightsContainer.children.forEach((node) => {
|
||||
node.rotation = -this.routeGroup.rotation;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
autoFitViewport() {
|
||||
@ -387,63 +457,42 @@ export default {
|
||||
if (this.routeScaleMax !== null)
|
||||
scale = Math.min(scale, this.routeScaleMax / 10);
|
||||
|
||||
if (this.centerLatitude != null && this.centerLongitude != null) {
|
||||
const local = this.toCanvasPoint([
|
||||
this.centerLatitude,
|
||||
this.centerLongitude,
|
||||
]);
|
||||
const global = this.routeGroup.toGlobal(
|
||||
new PIXI.Point(local.x, local.y)
|
||||
);
|
||||
this.viewport.setZoom(scale, global.x, global.y);
|
||||
} else {
|
||||
const cx = b.x + b.width / 2;
|
||||
const cy = b.y + b.height / 2;
|
||||
|
||||
// setZoom(scale, worldX, worldY) — Pixi‑Viewport v4/v8 API
|
||||
this.viewport.setZoom(scale, cx, cy);
|
||||
}
|
||||
this.viewportAdjusted = true;
|
||||
|
||||
// выставляем границы пэннинга
|
||||
this.applyPanClamp();
|
||||
|
||||
if (this.centerLatitude != null && this.centerLongitude != null) {
|
||||
console.log(
|
||||
`%c[autoFitViewport] scale=${scale.toFixed(2)}, center=(${
|
||||
this.centerLatitude
|
||||
},${this.centerLongitude})`,
|
||||
"color: mediumspringgreen"
|
||||
);
|
||||
} else {
|
||||
const cx = b.x + b.width / 2;
|
||||
const cy = b.y + b.height / 2;
|
||||
console.log(
|
||||
`%c[autoFitViewport] scale=${scale.toFixed(2)}, center=(${cx.toFixed(
|
||||
1
|
||||
)},${cy.toFixed(1)})`,
|
||||
"color: mediumspringgreen"
|
||||
);
|
||||
}
|
||||
},
|
||||
// Ограничиваем панорамирование так, чтобы пользователь
|
||||
// не мог «уехать» далеко от маршрута, динамически распределяя «воздух»
|
||||
// в зависимости от угла поворота (чтобы слева/сверху не оставалось пустоты).
|
||||
applyPanClamp() {
|
||||
if (!this.viewport || !this.routeGroup) return;
|
||||
|
||||
// Bounding‑box маршрута (учитывает rotation/scale)
|
||||
const b = this.routeGroup.getBounds();
|
||||
|
||||
// Базовый «воздух» 10% от большей стороны
|
||||
const pad = Math.max(b.width, b.height) * 0.1;
|
||||
|
||||
// Направление маршрута (радианы) — используем, чтобы перераспределить паддинги
|
||||
const ang = this.routeGroup.rotation; // радианы
|
||||
const cos = Math.cos(ang);
|
||||
const sin = Math.sin(ang);
|
||||
|
||||
// Распределяем паддинги: если cos > 0 → маршрут «смотрит» вправо,
|
||||
// поэтому больше воздуха добавляем справа, меньше слева и т.д.
|
||||
const padLeft = (pad * (1 - cos)) / 2;
|
||||
const padRight = (pad * (1 + cos)) / 2;
|
||||
const padTop = (pad * (1 - sin)) / 2;
|
||||
const padBottom = (pad * (1 + sin)) / 2;
|
||||
|
||||
this.viewport.clamp({
|
||||
left: b.x - padLeft,
|
||||
right: b.x + b.width + padRight,
|
||||
top: b.y - padTop,
|
||||
bottom: b.y + b.height + padBottom,
|
||||
underflow: "center",
|
||||
});
|
||||
|
||||
console.log(
|
||||
`%c[applyPanClamp] angle=${((ang * 180) / Math.PI).toFixed(
|
||||
1
|
||||
)}°, pads L${padLeft.toFixed(1)} R${padRight.toFixed(
|
||||
1
|
||||
)} T${padTop.toFixed(1)} B${padBottom.toFixed(1)}`,
|
||||
"color: orange"
|
||||
);
|
||||
return;
|
||||
},
|
||||
// ───────────────────────── polling helpers ─────────────────────────
|
||||
startRoutePolling() {
|
||||
@ -541,14 +590,44 @@ export default {
|
||||
route.path.length >= 2
|
||||
) {
|
||||
this.drawRoute(route.path);
|
||||
this.drawStations(stationsRu, stationsEn);
|
||||
if (typeof route.rotate === "number") {
|
||||
// this.routeRotate = route.rotate;
|
||||
this.routeGroup.rotation = (route.rotate * Math.PI) / 180;
|
||||
this.updateLabelOrientation(); // подписи остаются горизонтальными
|
||||
// --- Insert tram progress logic here ---
|
||||
this.routeLatlngs = route.path;
|
||||
if (!this.tramPollTimer) {
|
||||
this.tramPollTimer = setInterval(
|
||||
() => this.updateTramPosition(),
|
||||
500
|
||||
);
|
||||
}
|
||||
// apply rotation before rendering stations and sights
|
||||
if (typeof route.rotate === "number") {
|
||||
this.routeGroup.rotation = (route.rotate * Math.PI) / 180;
|
||||
}
|
||||
// draw stations and keep labels horizontal
|
||||
this.drawStations(stationsRu, stationsEn);
|
||||
this.updateLabelOrientation();
|
||||
// draw sights (icons rotate correctly with routeGroup)
|
||||
this.drawSights(sights);
|
||||
// compensate rotation for sight icons
|
||||
this.updateLabelOrientation();
|
||||
// после линии, станций и (возможного) поворота автоматически подгоняем viewport
|
||||
this.autoFitViewport();
|
||||
this.updateTramPosition();
|
||||
// center view on route-provided center after route load
|
||||
if (this.centerLatitude != null && this.centerLongitude != null) {
|
||||
const localCenter = this.toCanvasPoint([
|
||||
this.centerLatitude,
|
||||
this.centerLongitude,
|
||||
]);
|
||||
const globalCenter = this.routeGroup.toGlobal(
|
||||
new PIXI.Point(localCenter.x, localCenter.y)
|
||||
);
|
||||
// animate viewport to this center (keep current zoom)
|
||||
this.viewport.animate({
|
||||
time: 600,
|
||||
position: { x: globalCenter.x, y: globalCenter.y },
|
||||
easing: "easeInOutSine",
|
||||
});
|
||||
}
|
||||
gotPath = true;
|
||||
} else {
|
||||
console.warn(
|
||||
@ -567,9 +646,218 @@ export default {
|
||||
if (this.attemptCount > 1e6) this.attemptCount = 0;
|
||||
}
|
||||
},
|
||||
// ── Update tram position and color route based on progress ──
|
||||
async updateTramPosition() {
|
||||
if (!this.routeLatlngs || !this.routeLatlngs.length) return;
|
||||
try {
|
||||
const res = await fetch(`${GEO_URL}/v1/geolocation/context`);
|
||||
const data = await res.json();
|
||||
const percentage = data.routeProgress?.percentageCompleted;
|
||||
if (percentage == null) return;
|
||||
const total = this.routeLatlngs.length;
|
||||
const passedCount = Math.round(percentage * (total - 1));
|
||||
const passed = this.routeLatlngs.slice(0, passedCount + 1);
|
||||
const remaining = this.routeLatlngs.slice(passedCount);
|
||||
|
||||
// draw passed segment in red
|
||||
this.passedGraphics.clear();
|
||||
this.passedGraphics.setStrokeStyle({
|
||||
width: 10,
|
||||
color: 0xed1c24,
|
||||
alignment: 0.5,
|
||||
alpha: 1,
|
||||
});
|
||||
this.passedGraphics.beginPath();
|
||||
passed.forEach((pt, i) => {
|
||||
const { x, y } = this.toCanvasPoint(pt);
|
||||
if (i === 0) this.passedGraphics.moveTo(x, y);
|
||||
else this.passedGraphics.lineTo(x, y);
|
||||
});
|
||||
this.passedGraphics.stroke();
|
||||
|
||||
// draw remaining segment in white
|
||||
this.remainingGraphics.clear();
|
||||
this.remainingGraphics.setStrokeStyle({
|
||||
width: 10,
|
||||
color: 0xffffff,
|
||||
alignment: 0.5,
|
||||
alpha: 1,
|
||||
});
|
||||
this.remainingGraphics.beginPath();
|
||||
remaining.forEach((pt, i) => {
|
||||
const { x, y } = this.toCanvasPoint(pt);
|
||||
if (i === 0) this.remainingGraphics.moveTo(x, y);
|
||||
else this.remainingGraphics.lineTo(x, y);
|
||||
});
|
||||
this.remainingGraphics.stroke();
|
||||
|
||||
// draw or update tram marker
|
||||
const current = this.routeLatlngs[passedCount];
|
||||
const { x, y } = this.toCanvasPoint(current);
|
||||
if (!this.tramGraphic) {
|
||||
this.tramGraphic = new PIXI.Graphics();
|
||||
// set fill style to yellow
|
||||
this.tramGraphic.fill(0xfcd500);
|
||||
// set stroke style for border
|
||||
this.tramGraphic.setStrokeStyle({
|
||||
width: 5,
|
||||
color: 0x000000,
|
||||
alignment: 0.5,
|
||||
alpha: 1,
|
||||
});
|
||||
// draw circle path
|
||||
this.tramGraphic.beginPath();
|
||||
this.tramGraphic.circle(0, 0, 14);
|
||||
// fill the circle
|
||||
this.tramGraphic.fill();
|
||||
// draw the stroke
|
||||
this.tramGraphic.stroke();
|
||||
this.routeGroup.addChild(this.tramGraphic);
|
||||
}
|
||||
this.tramGraphic.x = x;
|
||||
this.tramGraphic.y = y;
|
||||
|
||||
// update station marker colors based on tram progress
|
||||
this.stationContainer.children.forEach((node) => {
|
||||
const latlng = node._stationLatlng;
|
||||
if (!latlng) return;
|
||||
// find nearest index on route for this station
|
||||
let closestIdx = 0,
|
||||
minDist = Infinity;
|
||||
this.routeLatlngs.forEach((pt, i) => {
|
||||
const dx = pt[0] - latlng[0],
|
||||
dy = pt[1] - latlng[1];
|
||||
const d = dx * dx + dy * dy;
|
||||
if (d < minDist) {
|
||||
minDist = d;
|
||||
closestIdx = i;
|
||||
}
|
||||
});
|
||||
// red if passed, white otherwise
|
||||
const color = closestIdx <= passedCount ? 0xed1c24 : 0xffffff;
|
||||
const g = node._circleGraphic;
|
||||
if (g) {
|
||||
g.clear();
|
||||
g.fill(color)
|
||||
.stroke({ width: 5, color: 0x000000 })
|
||||
.circle(0, 0, 11)
|
||||
.fill()
|
||||
.stroke();
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error updating tram position:", err);
|
||||
}
|
||||
},
|
||||
// Render sight icons with counts
|
||||
drawSights(sights) {
|
||||
if (!Array.isArray(sights) || !sights.length || !this.toCanvasPoint)
|
||||
return;
|
||||
// clear any previous sight icons
|
||||
this.sightsContainer.removeChildren();
|
||||
// group sights by approximate location (2 decimal places)
|
||||
const grouped = {};
|
||||
sights.forEach((s) => {
|
||||
const keyLat = s.latitude.toFixed(2);
|
||||
const keyLng = s.longitude.toFixed(2);
|
||||
const key = `${keyLat},${keyLng}`;
|
||||
if (!grouped[key]) grouped[key] = [];
|
||||
grouped[key].push(s);
|
||||
});
|
||||
// for each group, render an icon and, if multiple, a count badge
|
||||
Object.values(grouped).forEach((group) => {
|
||||
const first = group[0];
|
||||
const { x, y } = this.toCanvasPoint([first.latitude, first.longitude]);
|
||||
const node = new PIXI.Container();
|
||||
node.x = x;
|
||||
node.y = y;
|
||||
this.sightsContainer.addChild(node);
|
||||
// use a static sight icon
|
||||
const texture = PIXI.Texture.from(sightIcon);
|
||||
const sprite = new PIXI.Sprite(texture);
|
||||
sprite.anchor.set(0.5);
|
||||
node.addChild(sprite);
|
||||
|
||||
// ── Route collision avoidance: offset from route segments if too close ──
|
||||
if (Array.isArray(this.routeLatlngs) && this.routeLatlngs.length > 1) {
|
||||
const canvasPts = this.routeLatlngs.map((pt) =>
|
||||
this.toCanvasPoint(pt)
|
||||
);
|
||||
let minDist = Infinity;
|
||||
let normalX = 0,
|
||||
normalY = 0;
|
||||
let bestDx = 0,
|
||||
bestDy = 0;
|
||||
// for each segment, find perpendicular distance
|
||||
for (let i = 0; i < canvasPts.length - 1; i++) {
|
||||
const p1 = canvasPts[i];
|
||||
const p2 = canvasPts[i + 1];
|
||||
const vx = p2.x - p1.x;
|
||||
const vy = p2.y - p1.y;
|
||||
const denom = vx * vx + vy * vy || 1;
|
||||
const t = ((node.x - p1.x) * vx + (node.y - p1.y) * vy) / denom;
|
||||
const tClamped = Math.max(0, Math.min(1, t));
|
||||
const projX = p1.x + vx * tClamped;
|
||||
const projY = p1.y + vy * tClamped;
|
||||
const dx = node.x - projX;
|
||||
const dy = node.y - projY;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
bestDx = dx;
|
||||
bestDy = dy;
|
||||
// perpendicular normal (one of two)
|
||||
const nx = -vy;
|
||||
const ny = vx;
|
||||
const nlen = Math.hypot(nx, ny) || 1;
|
||||
normalX = nx / nlen;
|
||||
normalY = ny / nlen;
|
||||
}
|
||||
}
|
||||
const threshold = 50; // pixels
|
||||
if (minDist < threshold) {
|
||||
// ensure normal points toward the sight (dot > 0), else flip
|
||||
if (bestDx * normalX + bestDy * normalY < 0) {
|
||||
normalX = -normalX;
|
||||
normalY = -normalY;
|
||||
}
|
||||
const shift = threshold - minDist;
|
||||
node.x += normalX * shift;
|
||||
node.y += normalY * shift;
|
||||
}
|
||||
}
|
||||
|
||||
const solidWhite = new FillGradient({
|
||||
type: "linear",
|
||||
colorStops: [
|
||||
{ offset: 0, color: 0xffffff },
|
||||
{ offset: 1, color: 0xffffff },
|
||||
],
|
||||
});
|
||||
|
||||
if (group.length > 1) {
|
||||
const style = new TextStyle({
|
||||
fill: solidWhite,
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
});
|
||||
const badge = new Text(String(group.length), style);
|
||||
// якорь в правом верхнем углу значка
|
||||
badge.anchor.set(1, 0);
|
||||
// смещаем к правому верхнему краю спрайта
|
||||
badge.x = sprite.width / 2 - 4;
|
||||
badge.y = -sprite.height / 2 + 4;
|
||||
node.addChild(badge);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.stopRoutePolling();
|
||||
if (this.tramPollTimer) {
|
||||
clearInterval(this.tramPollTimer);
|
||||
this.tramPollTimer = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
21
src/icons/sight.svg
Normal file
21
src/icons/sight.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg width="67" height="62" viewBox="0 0 67 62" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 25.6107H66.3216V20.5767L33.1702 0.100708L0 20.5767V25.6107ZM33.1699 4.90984L5.97994 21.6835H60.341L33.1699 4.90984Z" fill="#A6A6A6"/>
|
||||
<path d="M66.3216 58.2223H0V62.0001H66.3216V58.2223Z" fill="#A6A6A6"/>
|
||||
<path d="M62.9961 53.1895H3.32556V56.9673H62.9961V53.1895Z" fill="#A6A6A6"/>
|
||||
<path d="M11.9052 27.7542H6.91687V50.0529H11.9052V27.7542Z" fill="#A6A6A6"/>
|
||||
<path d="M14.3989 50.5732H4.42236V52.0466H14.3989V50.5732Z" fill="#A6A6A6"/>
|
||||
<path d="M5.53691 26.5447C5.15901 26.5447 4.85669 26.8469 4.85669 27.2247C4.85669 27.6025 5.15901 27.9047 5.53691 27.9047C5.91481 27.9047 6.21713 27.6025 6.21713 27.2247H12.6037C12.6037 27.6025 12.906 27.9047 13.2839 27.9047C13.6618 27.9047 13.9641 27.6025 13.9641 27.2247C13.9641 26.8469 13.6618 26.5447 13.2839 26.5447H5.53691Z" fill="#A6A6A6"/>
|
||||
<path d="M27.8002 27.7542H22.8119V50.0529H27.8002V27.7542Z" fill="#A6A6A6"/>
|
||||
<path d="M30.2962 50.5732H20.3196V52.0466H30.2962V50.5732Z" fill="#A6A6A6"/>
|
||||
<path d="M21.4319 26.5447C21.054 26.5447 20.7517 26.8469 20.7517 27.2247C20.7517 27.6025 21.054 27.9047 21.4319 27.9047C21.8098 27.9047 22.1122 27.6025 22.1122 27.2247H28.4987C28.4987 27.6025 28.801 27.9047 29.1789 27.9047C29.5568 27.9047 29.8591 27.6025 29.8591 27.2247C29.8591 26.8469 29.5568 26.5447 29.1789 26.5447H21.4319Z" fill="#A6A6A6"/>
|
||||
<path d="M43.7068 27.7542H38.7185V50.0529H43.7068V27.7542Z" fill="#A6A6A6"/>
|
||||
<path d="M46.2005 50.5732H36.2239V52.0466H46.2005V50.5732Z" fill="#A6A6A6"/>
|
||||
<path d="M37.3385 26.5447C36.9606 26.5447 36.6583 26.8469 36.6583 27.2247C36.6583 27.6025 36.9606 27.9047 37.3385 27.9047C37.7164 27.9047 38.0188 27.6025 38.0188 27.2247H44.4053C44.4053 27.6025 44.7076 27.9047 45.0855 27.9047C45.4634 27.9047 45.7657 27.6025 45.7657 27.2247C45.7657 26.8469 45.4634 26.5447 45.0855 26.5447H37.3385Z" fill="#A6A6A6"/>
|
||||
<path d="M59.6012 27.754H54.6129V50.0528H59.6012V27.754Z" fill="#A6A6A6"/>
|
||||
<path d="M62.0972 50.5731H52.1206V52.0465H62.0972V50.5731Z" fill="#A6A6A6"/>
|
||||
<path d="M53.233 26.5446C52.8551 26.5446 52.5527 26.8468 52.5527 27.2246C52.5527 27.6024 52.8551 27.9046 53.233 27.9046C53.6109 27.9046 53.9132 27.6024 53.9132 27.2246H60.2997C60.2997 27.6024 60.602 27.9046 60.9799 27.9046C61.3578 27.9046 61.6601 27.6024 61.6601 27.2246C61.6601 26.8468 61.3578 26.5446 60.9799 26.5446H53.233Z" fill="#A6A6A6"/>
|
||||
<path d="M33.7759 12.162C33.7759 11.8598 33.4831 11.6143 33.124 11.6143C32.765 11.6143 32.4722 11.8598 32.4722 12.162V12.5965C32.4722 12.9554 32.765 13.2482 33.124 13.2482C33.4831 13.2482 33.7759 12.9554 33.7759 12.5965V12.162Z" fill="#A6A6A6"/>
|
||||
<path d="M24.5449 15.0146C24.2426 15.0146 23.9969 15.3074 23.9969 15.6758C23.9969 16.0347 24.2426 16.3369 24.5449 16.3369H25.6786C26.0376 16.3369 26.3399 16.0441 26.3399 15.6758C26.3399 15.3169 26.0471 15.0146 25.6786 15.0146H24.5449Z" fill="#A6A6A6"/>
|
||||
<path d="M40.5117 15.0146C40.2094 15.0146 39.9637 15.3074 39.9637 15.6758C39.9637 16.0347 40.2094 16.3369 40.5117 16.3369H41.6454C42.0044 16.3369 42.3067 16.0441 42.3067 15.6758C42.3067 15.3169 42.0139 15.0146 41.6454 15.0146H40.5117Z" fill="#A6A6A6"/>
|
||||
<path d="M34.7458 14.3911L33.9333 13.8717C33.9333 13.8717 33.8388 13.8245 33.7821 13.8245H32.4595C32.4028 13.8245 32.3556 13.8434 32.3083 13.8717L31.4958 14.3911C31.4108 14.4478 31.3636 14.5328 31.3636 14.6273V15.2317C31.3636 15.2884 31.3825 15.3451 31.4108 15.3923L32.1194 16.4406C32.1761 16.5256 32.1855 16.6201 32.1477 16.7145L31.3825 18.4523C31.3825 18.4523 31.3541 18.5279 31.3541 18.5657V20.294C31.3541 20.4546 31.4769 20.5774 31.6376 20.5774H34.5852C34.7458 20.5774 34.8686 20.4546 34.8686 20.294V18.5657C34.8686 18.5657 34.8686 18.4901 34.8403 18.4523L34.075 16.7145C34.0372 16.6295 34.0467 16.5256 34.1034 16.4406L34.8119 15.3923C34.8119 15.3923 34.8592 15.2884 34.8592 15.2317V14.6273C34.8592 14.5328 34.8119 14.4384 34.7269 14.3911H34.7458Z" fill="#A6A6A6"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
Reference in New Issue
Block a user