stations offset fix, live geo tracking, sights on route
All checks were successful
release-tag / release-image (push) Successful in 54s

This commit is contained in:
2025-07-07 02:10:46 +03:00
parent c1e6c6cfb8
commit 12c2a678a5
2 changed files with 385 additions and 76 deletions

View File

@ -25,8 +25,9 @@
<script> <script>
import * as PIXI from "pixi.js"; import * as PIXI from "pixi.js";
import { Viewport } from "pixi-viewport"; 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 { API_URL, GEO_URL } from "../config";
import sightIcon from "../icons/sight.svg";
export default { export default {
name: "MainMap", name: "MainMap",
@ -38,10 +39,16 @@ export default {
routeGraphics: null, routeGraphics: null,
videoOverlayVisible: false, videoOverlayVisible: false,
stationContainer: null, stationContainer: null,
sightsContainer: null,
toCanvasPoint: null, toCanvasPoint: null,
routePollTimer: null, routePollTimer: null,
attemptCount: 0, attemptCount: 0,
routeGroup: null, routeGroup: null,
routeLatlngs: [],
tramPollTimer: null,
passedGraphics: null,
remainingGraphics: null,
tramGraphic: null,
routeRotate: 0, routeRotate: 0,
routeCentered: false, routeCentered: false,
viewportAdjusted: false, // авто‑фит сделан? viewportAdjusted: false, // авто‑фит сделан?
@ -72,6 +79,7 @@ export default {
// add canvas to DOM (missing after last refactor) // add canvas to DOM (missing after last refactor)
container.appendChild(this.pixiApp.canvas); container.appendChild(this.pixiApp.canvas);
await Assets.load(sightIcon);
// гарантируем, что канвас всегда принимает pointer/touchсобытия // гарантируем, что канвас всегда принимает pointer/touchсобытия
this.pixiApp.canvas.style.pointerEvents = "auto"; this.pixiApp.canvas.style.pointerEvents = "auto";
@ -103,9 +111,15 @@ export default {
this.routeGraphics = new PIXI.Graphics(); this.routeGraphics = new PIXI.Graphics();
this.routeGroup.addChild(this.routeGraphics); 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.stationContainer = new PIXI.Container();
this.routeGroup.addChild(this.stationContainer); this.routeGroup.addChild(this.stationContainer);
this.sightsContainer = new PIXI.Container();
this.routeGroup.addChild(this.sightsContainer);
}, },
// Применяем ограничения масштабирования к viewport // Применяем ограничения масштабирования к viewport
updateViewportClampZoom() { updateViewportClampZoom() {
@ -114,11 +128,13 @@ export default {
this.routeScaleMin !== null && this.routeScaleMin !== null &&
this.routeScaleMax !== null this.routeScaleMax !== null
) { ) {
const minScale = this.routeScaleMin / 20; const minScale = this.routeScaleMin / 30;
const maxScale = this.routeScaleMax / 20; const maxScale = this.routeScaleMax / 20;
// убедимся, что min < max // убедимся, что min < max
if (minScale < maxScale) { if (minScale < maxScale) {
this.viewport.clampZoom({ minScale, maxScale }); this.viewport.clampZoom({ minScale, maxScale });
// apply initial zoom at minimum scale
this.viewport.setZoom(minScale);
console.log( console.log(
`%cViewport clampZoom → min=${minScale}, max=${maxScale}`, `%cViewport clampZoom → min=${minScale}, max=${maxScale}`,
"color: violet;" "color: violet;"
@ -141,7 +157,7 @@ export default {
this.routeGraphics.clear(); this.routeGraphics.clear();
this.routeGraphics.setStrokeStyle({ this.routeGraphics.setStrokeStyle({
width: 10, width: 10,
color: "#ED1C24", color: 0xffffff,
alpha: 1, alpha: 1,
}); });
this.routeGraphics.beginPath(); this.routeGraphics.beginPath();
@ -230,7 +246,8 @@ export default {
const { x, y } = this.toCanvasPoint([st.latitude, st.longitude]); const { x, y } = this.toCanvasPoint([st.latitude, st.longitude]);
const ox = (st.offset_x || 0) / 1.25; // смещение по X (½) 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)) { if (isNaN(x) || isNaN(y)) {
console.warn(`station[${idx}] (${st.id}) координаты NaN → пропущен`); console.warn(`station[${idx}] (${st.id}) координаты NaN → пропущен`);
return; return;
@ -243,19 +260,17 @@ export default {
this.stationContainer.addChild(node); this.stationContainer.addChild(node);
const g = new PIXI.Graphics(); const g = new PIXI.Graphics();
g.fill("#ED1C24") // initially white fill for all stations
g.fill(0xffffff)
.stroke({ width: 5, color: 0x000000 }) .stroke({ width: 5, color: 0x000000 })
.circle(0, 0, 11) .circle(0, 0, 11)
.fill() .fill()
.stroke(); .stroke();
node.addChild(g); 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({ const solidWhite = new FillGradient({
type: "linear", type: "linear",
colorStops: [ 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(); const labelGroup = new PIXI.Container();
labelGroup.x = ox; labelGroup.x = ox;
labelGroup.y = oy; labelGroup.y = oy;
@ -272,6 +295,52 @@ export default {
labelGroup._oy = oy; labelGroup._oy = oy;
node.addChild(labelGroup); 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 // RU
const ruStyle = new TextStyle({ const ruStyle = new TextStyle({
fill: solidWhite, fill: solidWhite,
@ -283,15 +352,15 @@ export default {
const ruText = new Text({ text: st.name, style: ruStyle }); const ruText = new Text({ text: st.name, style: ruStyle });
ruText.anchor.set(1, 0.5); // bottom-right ruText.anchor.set(1, 0.5); // bottom-right
ruText.x = 24; ruText.x = 24;
ruText.y = -20; ruText.y = -35;
labelGroup.addChild(ruText); labelGroup.addChild(ruText);
// EN // EN
const enName = enById[st.id]; const enName = enById[st.id];
if (enName) { if (enName) {
const enStyle = new TextStyle({ const enStyle = new TextStyle({
fill: solidWhite, fill: lightGray,
fontSize: 16, fontSize: 12,
fontFamily: "Arial", fontFamily: "Arial",
align: "right", align: "right",
fontWeight: "bold", fontWeight: "bold",
@ -326,10 +395,6 @@ export default {
position: { x: worldPos.x, y: worldPos.y }, position: { x: worldPos.x, y: worldPos.y },
easing: "easeInOutSine", easing: "easeInOutSine",
}); });
this.viewport.once("moved-end", () => {
this.applyPanClamp();
});
this.stationZoomed = true; this.stationZoomed = true;
console.log( console.log(
`%c[centerToStation] world=(${worldPos.x.toFixed( `%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() { autoFitViewport() {
@ -387,63 +457,42 @@ export default {
if (this.routeScaleMax !== null) if (this.routeScaleMax !== null)
scale = Math.min(scale, this.routeScaleMax / 10); 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 cx = b.x + b.width / 2;
const cy = b.y + b.height / 2; const cy = b.y + b.height / 2;
// setZoom(scale, worldX, worldY) — PixiViewport v4/v8 API
this.viewport.setZoom(scale, cx, cy); this.viewport.setZoom(scale, cx, cy);
}
this.viewportAdjusted = true; this.viewportAdjusted = true;
// выставляем границы пэннинга if (this.centerLatitude != null && this.centerLongitude != null) {
this.applyPanClamp(); 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( console.log(
`%c[autoFitViewport] scale=${scale.toFixed(2)}, center=(${cx.toFixed( `%c[autoFitViewport] scale=${scale.toFixed(2)}, center=(${cx.toFixed(
1 1
)},${cy.toFixed(1)})`, )},${cy.toFixed(1)})`,
"color: mediumspringgreen" "color: mediumspringgreen"
); );
}
}, },
// Ограничиваем панорамирование так, чтобы пользователь
// не мог «уехать» далеко от маршрута, динамически распределяя «воздух»
// в зависимости от угла поворота (чтобы слева/сверху не оставалось пустоты).
applyPanClamp() { applyPanClamp() {
if (!this.viewport || !this.routeGroup) return; return;
// Boundingbox маршрута (учитывает 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"
);
}, },
// ───────────────────────── polling helpers ───────────────────────── // ───────────────────────── polling helpers ─────────────────────────
startRoutePolling() { startRoutePolling() {
@ -541,14 +590,44 @@ export default {
route.path.length >= 2 route.path.length >= 2
) { ) {
this.drawRoute(route.path); this.drawRoute(route.path);
this.drawStations(stationsRu, stationsEn); // --- Insert tram progress logic here ---
if (typeof route.rotate === "number") { this.routeLatlngs = route.path;
// this.routeRotate = route.rotate; if (!this.tramPollTimer) {
this.routeGroup.rotation = (route.rotate * Math.PI) / 180; this.tramPollTimer = setInterval(
this.updateLabelOrientation(); // подписи остаются горизонтальными () => 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 // после линии, станций и (возможного) поворота автоматически подгоняем viewport
this.autoFitViewport(); 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; gotPath = true;
} else { } else {
console.warn( console.warn(
@ -567,9 +646,218 @@ export default {
if (this.attemptCount > 1e6) this.attemptCount = 0; 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() { beforeUnmount() {
this.stopRoutePolling(); this.stopRoutePolling();
if (this.tramPollTimer) {
clearInterval(this.tramPollTimer);
this.tramPollTimer = null;
}
}, },
}; };
</script> </script>

21
src/icons/sight.svg Normal file
View 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