fix: Delete ai comments
This commit is contained in:
@@ -48,7 +48,6 @@ export const CarrierCreatePage = observer(() => {
|
||||
languageStore.setLanguage("ru");
|
||||
}, []);
|
||||
|
||||
// Автоматически устанавливаем выбранный город при загрузке страницы
|
||||
useEffect(() => {
|
||||
if (selectedCityId && !createCarrierData.city_id) {
|
||||
setCreateCarrierData(
|
||||
|
||||
@@ -74,7 +74,6 @@ export const CarrierEditPage = observer(() => {
|
||||
mediaStore.getMedia();
|
||||
})();
|
||||
|
||||
// Устанавливаем русский язык при загрузке страницы
|
||||
languageStore.setLanguage("ru");
|
||||
}, [id]);
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export const CityEditPage = observer(() => {
|
||||
const { getMedia, getOneMedia } = mediaStore;
|
||||
|
||||
useEffect(() => {
|
||||
// Устанавливаем русский язык при загрузке страницы
|
||||
languageStore.setLanguage("ru");
|
||||
}, []);
|
||||
|
||||
@@ -64,12 +63,11 @@ export const CityEditPage = observer(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
await getCountries("ru");
|
||||
// Fetch data for all languages
|
||||
|
||||
const ruData = await getCity(id as string, "ru");
|
||||
const enData = await getCity(id as string, "en");
|
||||
const zhData = await getCity(id as string, "zh");
|
||||
|
||||
// Set data for each language
|
||||
setEditCityData(ruData.name, ruData.country_code, ruData.arms, "ru");
|
||||
setEditCityData(enData.name, enData.country_code, enData.arms, "en");
|
||||
setEditCityData(zhData.name, zhData.country_code, zhData.arms, "zh");
|
||||
@@ -207,7 +205,7 @@ export const CityEditPage = observer(() => {
|
||||
open={isSelectMediaOpen}
|
||||
onClose={() => setIsSelectMediaOpen(false)}
|
||||
onSelectMedia={handleMediaSelect}
|
||||
mediaType={1} // Тип медиа для иконок
|
||||
mediaType={1}
|
||||
/>
|
||||
|
||||
<UploadMediaDialog
|
||||
|
||||
@@ -17,7 +17,6 @@ export const CountryEditPage = observer(() => {
|
||||
countryStore;
|
||||
|
||||
useEffect(() => {
|
||||
// Устанавливаем русский язык при загрузке страницы
|
||||
languageStore.setLanguage("ru");
|
||||
}, []);
|
||||
|
||||
@@ -36,12 +35,10 @@ export const CountryEditPage = observer(() => {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (id) {
|
||||
// Fetch data for all languages
|
||||
const ruData = await getCountry(id as string, "ru");
|
||||
const enData = await getCountry(id as string, "en");
|
||||
const zhData = await getCountry(id as string, "zh");
|
||||
|
||||
// Set data for each language
|
||||
setEditCountryData(ruData.name, "ru");
|
||||
setEditCountryData(enData.name, "en");
|
||||
setEditCountryData(zhData.name, "zh");
|
||||
|
||||
@@ -24,7 +24,6 @@ export const LoginPage = () => {
|
||||
const { login } = authStore;
|
||||
const { getUsers } = userStore;
|
||||
useEffect(() => {
|
||||
// Load saved credentials if they exist
|
||||
const savedEmail = localStorage.getItem("rememberedEmail");
|
||||
const savedPassword = localStorage.getItem("rememberedPassword");
|
||||
if (savedEmail && savedPassword) {
|
||||
@@ -42,7 +41,6 @@ export const LoginPage = () => {
|
||||
try {
|
||||
await login(email, password);
|
||||
|
||||
// Save or clear credentials based on remember me checkbox
|
||||
if (rememberMe) {
|
||||
localStorage.setItem("rememberedEmail", email);
|
||||
localStorage.setItem("rememberedPassword", password);
|
||||
|
||||
@@ -60,7 +60,6 @@ import Source from "ol/source/Source";
|
||||
import { FeatureLike } from "ol/Feature";
|
||||
import { createEmpty, extend, getCenter } from "ol/extent";
|
||||
|
||||
// --- CUSTOM SCROLLBAR STYLES ---
|
||||
const scrollbarStyles = `
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
@@ -100,8 +99,6 @@ if (typeof document !== "undefined") {
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
|
||||
// --- MAP STORE ---
|
||||
// @ts-ignore
|
||||
import { languageInstance } from "@shared";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
@@ -114,14 +111,11 @@ import {
|
||||
carrierStore,
|
||||
} from "@shared";
|
||||
|
||||
// Функция для сброса кешей карты
|
||||
export const clearMapCaches = () => {
|
||||
// Сброс кешей маршрутов
|
||||
mapStore.routes = [];
|
||||
mapStore.stations = [];
|
||||
mapStore.sights = [];
|
||||
|
||||
// Сброс кешей MapService если он доступен
|
||||
if (typeof window !== "undefined" && (window as any).mapServiceInstance) {
|
||||
(window as any).mapServiceInstance.clearCaches();
|
||||
}
|
||||
@@ -166,7 +160,6 @@ export type SortType =
|
||||
| "updated_asc"
|
||||
| "updated_desc";
|
||||
|
||||
// --- HIDDEN ROUTES STORAGE ---
|
||||
const HIDDEN_ROUTES_KEY = "mapHiddenRoutes";
|
||||
const HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY = "mapHideSightsByHiddenRoutes";
|
||||
|
||||
@@ -202,9 +195,9 @@ const saveHiddenRoutes = (hiddenRoutes: Set<number>): void => {
|
||||
class MapStore {
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
// Загружаем скрытые маршруты из localStorage при инициализации
|
||||
|
||||
this.hiddenRoutes = getStoredHiddenRoutes();
|
||||
// Загружаем настройку скрытия достопримечательностей
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(HIDE_SIGHTS_BY_HIDDEN_ROUTES_KEY);
|
||||
this.hideSightsByHiddenRoutes = stored
|
||||
@@ -220,8 +213,8 @@ class MapStore {
|
||||
sights: ApiSight[] = [];
|
||||
hiddenRoutes: Set<number>;
|
||||
hideSightsByHiddenRoutes: boolean = false;
|
||||
routeStationsCache: Map<number, number[]> = new Map(); // Кэш станций для маршрутов
|
||||
routeSightsCache: Map<number, number[]> = new Map(); // Кэш достопримечательностей для маршрутов
|
||||
routeStationsCache: Map<number, number[]> = new Map();
|
||||
routeSightsCache: Map<number, number[]> = new Map();
|
||||
setHideSightsByHiddenRoutes(val: boolean) {
|
||||
this.hideSightsByHiddenRoutes = val;
|
||||
try {
|
||||
@@ -232,11 +225,9 @@ class MapStore {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// НОВЫЕ ПОЛЯ ДЛЯ СОРТИРОВКИ
|
||||
stationSort: SortType = "name_asc";
|
||||
sightSort: SortType = "name_asc";
|
||||
|
||||
// НОВЫЕ МЕТОДЫ-СЕТТЕРЫ
|
||||
setStationSort = (sortType: SortType) => {
|
||||
this.stationSort = sortType;
|
||||
};
|
||||
@@ -245,7 +236,6 @@ class MapStore {
|
||||
this.sightSort = sortType;
|
||||
};
|
||||
|
||||
// ПРИВАТНЫЙ МЕТОД ДЛЯ ОБЩЕЙ ЛОГИКИ СОРТИРОВКИ
|
||||
private sortFeatures<T extends ApiStation | ApiSight>(
|
||||
features: T[],
|
||||
sortType: SortType
|
||||
@@ -269,7 +259,7 @@ class MapStore {
|
||||
new Date(b.created_at).getTime()
|
||||
);
|
||||
}
|
||||
// Фоллбэк: сортировка по ID, если дата недоступна
|
||||
|
||||
return a.id - b.id;
|
||||
});
|
||||
case "created_desc":
|
||||
@@ -285,7 +275,7 @@ class MapStore {
|
||||
new Date(a.created_at).getTime()
|
||||
);
|
||||
}
|
||||
// Фоллбэк: сортировка по ID, если дата недоступна
|
||||
|
||||
return b.id - a.id;
|
||||
});
|
||||
case "updated_asc":
|
||||
@@ -319,7 +309,6 @@ class MapStore {
|
||||
}
|
||||
}
|
||||
|
||||
// НОВЫЕ ГЕТТЕРЫ, ВОЗВРАЩАЮЩИЕ ОТСОРТИРОВАННЫЕ СПИСКИ
|
||||
get sortedStations(): ApiStation[] {
|
||||
return this.sortFeatures(this.stations, this.stationSort);
|
||||
}
|
||||
@@ -328,7 +317,6 @@ class MapStore {
|
||||
return this.sortFeatures(this.sights, this.sightSort);
|
||||
}
|
||||
|
||||
// ГЕТТЕРЫ ДЛЯ ФИЛЬТРАЦИИ ПО ГОРОДУ
|
||||
get filteredStations(): ApiStation[] {
|
||||
const selectedCityId = selectedCityStore.selectedCityId;
|
||||
if (!selectedCityId) {
|
||||
@@ -345,12 +333,9 @@ class MapStore {
|
||||
return this.routes;
|
||||
}
|
||||
|
||||
// Получаем carriers для текущего языка
|
||||
const carriers = carrierStore.carriers.ru.data;
|
||||
|
||||
// Фильтруем маршруты по городу через carriers
|
||||
return this.routes.filter((route: ApiRoute) => {
|
||||
// Находим carrier для маршрута
|
||||
const carrier = carriers.find((c: any) => c.id === route.carrier_id);
|
||||
return carrier && carrier.city_id === selectedCityId;
|
||||
});
|
||||
@@ -366,14 +351,12 @@ class MapStore {
|
||||
return cityFiltered;
|
||||
}
|
||||
|
||||
// Собираем все достопримечательности, связанные со скрытыми маршрутами
|
||||
const hiddenSightIds = new Set<number>();
|
||||
this.hiddenRoutes.forEach((routeId) => {
|
||||
const sightIds = this.routeSightsCache.get(routeId) || [];
|
||||
sightIds.forEach((id) => hiddenSightIds.add(id));
|
||||
});
|
||||
|
||||
// Фильтруем достопримечательности, исключая привязанные к скрытым маршрутам
|
||||
return cityFiltered.filter((s) => !hiddenSightIds.has(s.id));
|
||||
}
|
||||
|
||||
@@ -397,16 +380,12 @@ class MapStore {
|
||||
a.route_number.localeCompare(b.route_number)
|
||||
);
|
||||
|
||||
// Предзагружаем станции для всех маршрутов и кэшируем их
|
||||
await this.preloadRouteStations(routesIds);
|
||||
// Предзагружаем достопримечательности для всех маршрутов
|
||||
|
||||
await this.preloadRouteSights(routesIds);
|
||||
};
|
||||
|
||||
preloadRouteStations = async (routesIds: number[]) => {
|
||||
console.log(
|
||||
`[MapStore] Preloading stations for ${routesIds.length} routes`
|
||||
);
|
||||
const stationPromises = routesIds.map(async (routeId) => {
|
||||
try {
|
||||
const stationsResponse = await languageInstance("ru").get(
|
||||
@@ -422,13 +401,9 @@ class MapStore {
|
||||
}
|
||||
});
|
||||
await Promise.all(stationPromises);
|
||||
console.log(
|
||||
`[MapStore] Preloaded stations for ${this.routeStationsCache.size} routes`
|
||||
);
|
||||
};
|
||||
|
||||
preloadRouteSights = async (routesIds: number[]) => {
|
||||
console.log(`[MapStore] Preloading sights for ${routesIds.length} routes`);
|
||||
const sightPromises = routesIds.map(async (routeId) => {
|
||||
try {
|
||||
const sightsResponse = await languageInstance("ru").get(
|
||||
@@ -441,9 +416,6 @@ class MapStore {
|
||||
}
|
||||
});
|
||||
await Promise.all(sightPromises);
|
||||
console.log(
|
||||
`[MapStore] Preloaded sights for ${this.routeSightsCache.size} routes`
|
||||
);
|
||||
};
|
||||
|
||||
getStations = async () => {
|
||||
@@ -473,7 +445,7 @@ class MapStore {
|
||||
|
||||
createFeature = async (featureType: string, geoJsonFeature: any) => {
|
||||
const { geometry, properties } = geoJsonFeature;
|
||||
let createdItem;
|
||||
let createdItem: any;
|
||||
|
||||
if (featureType === "station") {
|
||||
const name = properties.name || "Остановка 1";
|
||||
@@ -524,7 +496,6 @@ class MapStore {
|
||||
"EPSG:3857"
|
||||
);
|
||||
|
||||
// Автоматически назначаем перевозчика из выбранного города
|
||||
let carrier_id = 0;
|
||||
let carrier = "";
|
||||
|
||||
@@ -581,11 +552,8 @@ class MapStore {
|
||||
throw new Error(`Unknown feature type for creation: ${featureType}`);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (featureType === "route") this.routes.push(createdItem);
|
||||
// @ts-ignore
|
||||
else if (featureType === "station") this.stations.push(createdItem);
|
||||
// @ts-ignore
|
||||
else if (featureType === "sight") this.sights.push(createdItem);
|
||||
|
||||
return createdItem;
|
||||
@@ -686,18 +654,15 @@ class MapStore {
|
||||
|
||||
const mapStore = new MapStore();
|
||||
|
||||
// Делаем mapStore доступным глобально для сброса кешей
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).mapStore = mapStore;
|
||||
}
|
||||
|
||||
// --- CONFIGURATION ---
|
||||
export const mapConfig = {
|
||||
center: [30.311, 59.94] as [number, number],
|
||||
zoom: 13,
|
||||
};
|
||||
|
||||
// --- MAP POSITION STORAGE ---
|
||||
const MAP_POSITION_KEY = "mapPosition";
|
||||
const ACTIVE_SECTION_KEY = "mapActiveSection";
|
||||
|
||||
@@ -736,7 +701,6 @@ const saveMapPosition = (position: MapPosition): void => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- ACTIVE SECTION STORAGE ---
|
||||
const getStoredActiveSection = (): string | null => {
|
||||
try {
|
||||
const stored = localStorage.getItem(ACTIVE_SECTION_KEY);
|
||||
@@ -761,7 +725,6 @@ const saveActiveSection = (section: string | null): void => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- TYPE DEFINITIONS ---
|
||||
interface MapServiceConfig {
|
||||
target: HTMLElement;
|
||||
center: [number, number];
|
||||
@@ -774,15 +737,15 @@ class MapService {
|
||||
private map: OLMap | null;
|
||||
public pointSource: VectorSource<Feature<Point>>;
|
||||
public lineSource: VectorSource<Feature<LineString>>;
|
||||
public clusterLayer: VectorLayer<Cluster>; // Public for the deselect handler
|
||||
public routeLayer: VectorLayer<VectorSource<Feature<LineString>>>; // Public for deselect
|
||||
public clusterLayer: VectorLayer<Cluster>;
|
||||
public routeLayer: VectorLayer<VectorSource<Feature<LineString>>>;
|
||||
private clusterSource: Cluster;
|
||||
private clusterStyleCache: { [key: number]: Style };
|
||||
|
||||
private tooltipElement: HTMLElement;
|
||||
private tooltipOverlay: Overlay | null;
|
||||
private mode: string | null;
|
||||
// @ts-ignore
|
||||
|
||||
private currentDrawingType: "Point" | "LineString" | null;
|
||||
private currentDrawingFeatureType: FeatureType | null;
|
||||
private currentInteraction: Draw | null;
|
||||
@@ -801,7 +764,6 @@ class MapService {
|
||||
null;
|
||||
private isCreating: boolean = false;
|
||||
|
||||
// Styles
|
||||
private defaultStyle: Style;
|
||||
private selectedStyle: Style;
|
||||
private drawStyle: Style;
|
||||
@@ -816,7 +778,6 @@ class MapService {
|
||||
private hoverSightIconStyle: Style;
|
||||
private universalHoverStyleLine: Style;
|
||||
|
||||
// Callbacks
|
||||
private setLoading: (loading: boolean) => void;
|
||||
private setError: (error: string | null) => void;
|
||||
private onModeChangeCallback: (mode: string) => void;
|
||||
@@ -958,13 +919,12 @@ class MapService {
|
||||
|
||||
this.routeLayer = new VectorLayer({
|
||||
source: this.lineSource,
|
||||
// @ts-ignore
|
||||
|
||||
style: (featureLike: FeatureLike) => {
|
||||
const feature = featureLike as Feature<Geometry>;
|
||||
if (!feature) return this.defaultStyle;
|
||||
const fId = feature.getId();
|
||||
|
||||
// Все маршруты всегда отображаются, так как они не кластеризуются
|
||||
const isSelected =
|
||||
this.selectInteraction?.getFeatures().getArray().includes(feature) ||
|
||||
(fId !== undefined && this.selectedIds.has(fId));
|
||||
@@ -1029,9 +989,6 @@ class MapService {
|
||||
});
|
||||
|
||||
this.clusterSource.on("change", () => {
|
||||
// Поскольку маршруты больше не добавляются как точки,
|
||||
// нам не нужно отслеживать unclusteredRouteIds
|
||||
// Все маршруты всегда отображаются как линии
|
||||
this.routeLayer.changed();
|
||||
});
|
||||
|
||||
@@ -1080,21 +1037,18 @@ class MapService {
|
||||
new KeyboardZoom(),
|
||||
new PinchZoom(),
|
||||
new PinchRotate(),
|
||||
// Отключаем DoubleClickZoom как было изначально
|
||||
// new DoubleClickZoom(),
|
||||
|
||||
new DragPan({
|
||||
condition: (event) => {
|
||||
// Разрешаем перетаскивание только при нажатии средней кнопки мыши (колёсико)
|
||||
const originalEvent = event.originalEvent;
|
||||
if (!originalEvent) return false;
|
||||
|
||||
// Проверяем, что это событие мыши и нажата средняя кнопка
|
||||
if (
|
||||
originalEvent.type === "pointerdown" ||
|
||||
originalEvent.type === "pointermove"
|
||||
) {
|
||||
const pointerEvent = originalEvent as PointerEvent;
|
||||
return pointerEvent.buttons === 4; // 4 = средняя кнопка мыши
|
||||
return pointerEvent.buttons === 4;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -1167,7 +1121,7 @@ class MapService {
|
||||
originalFeatures.length === 1 &&
|
||||
originalFeatures[0].get("isProxy")
|
||||
)
|
||||
return false; // Ignore proxy points
|
||||
return false;
|
||||
return true;
|
||||
},
|
||||
multi: true,
|
||||
@@ -1302,7 +1256,7 @@ class MapService {
|
||||
const selected = new Set<string | number>();
|
||||
|
||||
this.pointSource.forEachFeatureInExtent(extent, (f) => {
|
||||
if (f.get("isProxy")) return; // Ignore proxy in lasso
|
||||
if (f.get("isProxy")) return;
|
||||
const geom = f.getGeometry();
|
||||
if (geom && geom.getType() === "Point") {
|
||||
const pointCoords = (geom as Point).getCoordinates();
|
||||
@@ -1339,7 +1293,6 @@ class MapService {
|
||||
this.selectInteraction.setActive(false);
|
||||
this.lassoInteraction.setActive(false);
|
||||
|
||||
// --- ИСПРАВЛЕНИЕ: Главный обработчик выбора объектов и кластеров
|
||||
this.selectInteraction.on("select", (e: SelectEvent) => {
|
||||
if (this.mode !== "edit" || !this.map) return;
|
||||
|
||||
@@ -1347,13 +1300,11 @@ class MapService {
|
||||
e.mapBrowserEvent.originalEvent.ctrlKey ||
|
||||
e.mapBrowserEvent.originalEvent.metaKey;
|
||||
|
||||
// Проверяем, был ли клик по кластеру (группе)
|
||||
if (e.selected.length === 1 && !ctrlKey) {
|
||||
const clickedFeature = e.selected[0];
|
||||
const originalFeatures = clickedFeature.get("features");
|
||||
|
||||
if (originalFeatures && originalFeatures.length > 1) {
|
||||
// Если да, то приближаем карту
|
||||
const extent = createEmpty();
|
||||
originalFeatures.forEach((feat: Feature<Point>) => {
|
||||
const geom = feat.getGeometry();
|
||||
@@ -1364,20 +1315,17 @@ class MapService {
|
||||
padding: [60, 60, 60, 60],
|
||||
maxZoom: 18,
|
||||
});
|
||||
// Сбрасываем выделение, так как мы не хотим "выделять" сам кластер
|
||||
|
||||
this.selectInteraction.getFeatures().clear();
|
||||
this.setSelectedIds(new Set());
|
||||
return; // Завершаем обработку
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// При Ctrl+клик сохраняем предыдущие выделения и добавляем/удаляем только изменённые
|
||||
// При обычном клике создаём новый набор
|
||||
const newSelectedIds = ctrlKey
|
||||
? new Set(this.selectedIds)
|
||||
: new Set<string | number>();
|
||||
|
||||
// Добавляем новые выбранные элементы
|
||||
e.selected.forEach((feature) => {
|
||||
const originalFeatures = feature.get("features");
|
||||
let targetId: string | number | undefined;
|
||||
@@ -1389,8 +1337,6 @@ class MapService {
|
||||
}
|
||||
|
||||
if (targetId !== undefined) {
|
||||
// При Ctrl+клик: если элемент уже был выбран, снимаем его (toggle)
|
||||
// Если не был выбран, добавляем
|
||||
if (ctrlKey && newSelectedIds.has(targetId)) {
|
||||
newSelectedIds.delete(targetId);
|
||||
} else {
|
||||
@@ -1399,9 +1345,6 @@ class MapService {
|
||||
}
|
||||
});
|
||||
|
||||
// При Ctrl+клик игнорируем deselected, так как Select interaction может снимать
|
||||
// предыдущие выделения, но мы хотим их сохранить
|
||||
// При обычном клике удаляем deselected элементы
|
||||
if (!ctrlKey) {
|
||||
e.deselected.forEach((feature) => {
|
||||
const originalFeatures = feature.get("features");
|
||||
@@ -1425,50 +1368,41 @@ class MapService {
|
||||
this.map.on("pointermove", this.boundHandlePointerMove as any);
|
||||
const targetEl = this.map.getTargetElement();
|
||||
if (targetEl instanceof HTMLElement) {
|
||||
// Устанавливаем курсор pointer по умолчанию для всей карты
|
||||
targetEl.style.cursor = "pointer";
|
||||
targetEl.addEventListener("contextmenu", this.boundHandleContextMenu);
|
||||
targetEl.addEventListener("pointerleave", this.boundHandlePointerLeave);
|
||||
|
||||
// Добавляем обработчики для изменения курсора при нажатии средней кнопки мыши
|
||||
targetEl.addEventListener("pointerdown", (e) => {
|
||||
if (e.buttons === 4) {
|
||||
// Средняя кнопка мыши
|
||||
e.preventDefault(); // Предотвращаем скролл страницы
|
||||
e.preventDefault();
|
||||
targetEl.style.cursor = "grabbing";
|
||||
}
|
||||
});
|
||||
|
||||
targetEl.addEventListener("pointerup", (e) => {
|
||||
if (e.button === 1) {
|
||||
// Средняя кнопка мыши отпущена
|
||||
e.preventDefault(); // Предотвращаем скролл страницы
|
||||
e.preventDefault();
|
||||
targetEl.style.cursor = "pointer";
|
||||
}
|
||||
});
|
||||
|
||||
// Также добавляем обработчик для mousedown/mouseup для совместимости
|
||||
targetEl.addEventListener("mousedown", (e) => {
|
||||
if (e.button === 1) {
|
||||
// Средняя кнопка мыши
|
||||
e.preventDefault(); // Предотвращаем скролл страницы
|
||||
e.preventDefault();
|
||||
targetEl.style.cursor = "grabbing";
|
||||
}
|
||||
});
|
||||
|
||||
targetEl.addEventListener("mouseup", (e) => {
|
||||
if (e.button === 1) {
|
||||
// Средняя кнопка мыши отпущена
|
||||
e.preventDefault(); // Предотвращаем скролл страницы
|
||||
e.preventDefault();
|
||||
targetEl.style.cursor = "pointer";
|
||||
}
|
||||
});
|
||||
|
||||
// Дополнительная защита от нежелательного поведения средней кнопки мыши
|
||||
targetEl.addEventListener("auxclick", (e) => {
|
||||
if (e.button === 1) {
|
||||
// Средняя кнопка мыши
|
||||
e.preventDefault(); // Предотвращаем открытие ссылки в новой вкладке
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1499,16 +1433,10 @@ class MapService {
|
||||
const pointFeatures: Feature<Point>[] = [];
|
||||
const lineFeatures: Feature<LineString>[] = [];
|
||||
|
||||
// Используем фильтрованные данные из mapStore
|
||||
const filteredStations = mapStore.filteredStations;
|
||||
const filteredSights = mapStore.filteredSights;
|
||||
const filteredRoutes = mapStore.filteredRoutes;
|
||||
|
||||
console.log(
|
||||
`[loadFeaturesFromApi] Loading with ${mapStore.hiddenRoutes.size} hidden routes`
|
||||
);
|
||||
|
||||
// Собираем все станции видимых маршрутов из кэша
|
||||
const stationsInVisibleRoutes = new Set<number>();
|
||||
filteredRoutes
|
||||
.filter((route) => !mapStore.hiddenRoutes.has(route.id))
|
||||
@@ -1517,15 +1445,10 @@ class MapService {
|
||||
stationIds.forEach((id) => stationsInVisibleRoutes.add(id));
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[loadFeaturesFromApi] Found ${stationsInVisibleRoutes.size} stations in visible routes, total stations: ${filteredStations.length}`
|
||||
);
|
||||
|
||||
let skippedStations = 0;
|
||||
filteredStations.forEach((station) => {
|
||||
if (station.longitude == null || station.latitude == null) return;
|
||||
|
||||
// Пропускаем станции, которые принадлежат только скрытым маршрутам
|
||||
if (!stationsInVisibleRoutes.has(station.id)) {
|
||||
skippedStations++;
|
||||
return;
|
||||
@@ -1562,7 +1485,6 @@ class MapService {
|
||||
filteredRoutes.forEach((route) => {
|
||||
if (!route.path || route.path.length === 0) return;
|
||||
|
||||
// Пропускаем скрытые маршруты
|
||||
if (mapStore.hiddenRoutes.has(route.id)) return;
|
||||
|
||||
const coordinates = route.path
|
||||
@@ -1583,10 +1505,6 @@ class MapService {
|
||||
lineFeatures.push(lineFeature);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[loadFeaturesFromApi] Skipped ${skippedStations} stations (belonging only to hidden routes)`
|
||||
);
|
||||
|
||||
this.pointSource.addFeatures(pointFeatures);
|
||||
this.lineSource.addFeatures(lineFeatures);
|
||||
|
||||
@@ -1611,7 +1529,6 @@ class MapService {
|
||||
}
|
||||
if (this.tooltipOverlay) this.tooltipOverlay.setPosition(undefined);
|
||||
|
||||
// Сбрасываем курсор при покидании области карты
|
||||
if (this.map) {
|
||||
const targetEl = this.map.getTargetElement();
|
||||
if (targetEl instanceof HTMLElement) {
|
||||
@@ -1692,10 +1609,9 @@ class MapService {
|
||||
const fType = this.currentDrawingFeatureType;
|
||||
if (!fType) return;
|
||||
|
||||
// Проверяем, не идет ли уже процесс создания
|
||||
if (this.isCreating) {
|
||||
toast.warning("Дождитесь завершения создания предыдущего объекта.");
|
||||
// Удаляем созданный объект из источника
|
||||
|
||||
const sourceForDrawing =
|
||||
type === "Point" ? this.pointSource : this.lineSource;
|
||||
setTimeout(() => {
|
||||
@@ -1712,7 +1628,6 @@ class MapService {
|
||||
|
||||
switch (fType) {
|
||||
case "station":
|
||||
// Используем полный список из mapStore, а не отфильтрованный
|
||||
const stationNumbers = mapStore.stations
|
||||
.map((station) => {
|
||||
const match = station.name?.match(/^Остановка (\d+)$/);
|
||||
@@ -1724,7 +1639,6 @@ class MapService {
|
||||
resourceName = `Остановка ${nextStationNumber}`;
|
||||
break;
|
||||
case "sight":
|
||||
// Используем полный список из mapStore, а не отфильтрованный
|
||||
const sightNumbers = mapStore.sights
|
||||
.map((sight) => {
|
||||
const match = sight.name?.match(/^Достопримечательность (\d+)$/);
|
||||
@@ -1736,7 +1650,6 @@ class MapService {
|
||||
resourceName = `Достопримечательность ${nextSightNumber}`;
|
||||
break;
|
||||
case "route":
|
||||
// Используем полный список из mapStore, а не отфильтрованный
|
||||
const routeNumbers = mapStore.routes
|
||||
.map((route) => {
|
||||
const match = route.route_number?.match(/^Маршрут (\d+)$/);
|
||||
@@ -1778,11 +1691,8 @@ class MapService {
|
||||
private stopDrawing() {
|
||||
if (this.map && this.currentInteraction) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
this.currentInteraction.abortDrawing();
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
} catch (e) {}
|
||||
this.map.removeInteraction(this.currentInteraction);
|
||||
}
|
||||
this.currentInteraction = null;
|
||||
@@ -1793,7 +1703,6 @@ class MapService {
|
||||
public finishDrawing(): void {
|
||||
if (!this.currentInteraction) return;
|
||||
|
||||
// Блокируем завершение рисования, если идет процесс создания
|
||||
if (this.isCreating) {
|
||||
toast.warning("Дождитесь завершения создания предыдущего объекта.");
|
||||
return;
|
||||
@@ -1824,7 +1733,7 @@ class MapService {
|
||||
layerFilter,
|
||||
hitTolerance: 5,
|
||||
});
|
||||
// Устанавливаем курсор pointer для всей карты, чтобы показать возможность перетаскивания колёсиком
|
||||
|
||||
this.map.getTargetElement().style.cursor = hit ? "pointer" : "pointer";
|
||||
|
||||
const featureAtPixel: Feature<Geometry> | undefined =
|
||||
@@ -1838,7 +1747,7 @@ class MapService {
|
||||
if (featureAtPixel) {
|
||||
const originalFeatures = featureAtPixel.get("features");
|
||||
if (originalFeatures && originalFeatures.length > 0) {
|
||||
if (originalFeatures[0].get("isProxy")) return; // Ignore hover on proxy
|
||||
if (originalFeatures[0].get("isProxy")) return;
|
||||
finalFeature = originalFeatures[0];
|
||||
} else {
|
||||
finalFeature = featureAtPixel;
|
||||
@@ -2087,13 +1996,11 @@ class MapService {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
// Метод для сброса кешей карты
|
||||
public clearCaches() {
|
||||
this.clusterStyleCache = {};
|
||||
this.hoveredFeatureId = null;
|
||||
this.selectedIds.clear();
|
||||
|
||||
// Очищаем источники данных
|
||||
if (this.pointSource) {
|
||||
this.pointSource.clear();
|
||||
}
|
||||
@@ -2101,7 +2008,6 @@ class MapService {
|
||||
this.lineSource.clear();
|
||||
}
|
||||
|
||||
// Обновляем слои
|
||||
if (this.clusterLayer) {
|
||||
this.clusterLayer.changed();
|
||||
}
|
||||
@@ -2151,10 +2057,9 @@ class MapService {
|
||||
const featureType = feature.get("featureType") as FeatureType;
|
||||
if (!featureType || !this.map) return;
|
||||
|
||||
// Проверяем, не идет ли уже процесс создания
|
||||
if (this.isCreating) {
|
||||
toast.warning("Дождитесь завершения создания предыдущего объекта.");
|
||||
// Удаляем незавершенный объект с карты
|
||||
|
||||
if (feature.getGeometry()?.getType() === "LineString") {
|
||||
if (this.lineSource.hasFeature(feature as Feature<LineString>))
|
||||
this.lineSource.removeFeature(feature as Feature<LineString>);
|
||||
@@ -2181,37 +2086,28 @@ class MapService {
|
||||
);
|
||||
|
||||
const newFeatureId = `${featureType}-${createdFeatureData.id}`;
|
||||
// @ts-ignore
|
||||
|
||||
const displayName =
|
||||
featureType === "route"
|
||||
? // @ts-ignore
|
||||
createdFeatureData.route_number
|
||||
: // @ts-ignore
|
||||
createdFeatureData.name;
|
||||
? createdFeatureData.route_number
|
||||
: createdFeatureData.name;
|
||||
|
||||
if (featureType === "route") {
|
||||
// @ts-ignore
|
||||
const routeData = createdFeatureData as ApiRoute;
|
||||
const projection = this.map.getView().getProjection();
|
||||
|
||||
// Update existing line feature
|
||||
feature.setId(newFeatureId);
|
||||
feature.set("name", displayName);
|
||||
|
||||
// Optionally update geometry if server modified it
|
||||
const lineGeom = new LineString(
|
||||
routeData.path.map((c) =>
|
||||
transform([c[1], c[0]], "EPSG:4326", projection)
|
||||
)
|
||||
);
|
||||
feature.setGeometry(lineGeom);
|
||||
|
||||
// Не создаем прокси-точку для маршрута - только линия
|
||||
} else {
|
||||
// For points: update existing
|
||||
feature.setId(newFeatureId);
|
||||
feature.set("name", displayName);
|
||||
// No need to remove and re-add since it's already in the source
|
||||
}
|
||||
|
||||
this.updateFeaturesInReact();
|
||||
@@ -2233,7 +2129,6 @@ class MapService {
|
||||
}
|
||||
}
|
||||
|
||||
// --- MAP CONTROLS COMPONENT ---
|
||||
interface MapControlsProps {
|
||||
mapService: MapService | null;
|
||||
activeMode: string;
|
||||
@@ -2331,7 +2226,6 @@ const MapControls: React.FC<MapControlsProps> = ({
|
||||
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// --- MAP SIGHTBAR COMPONENT ---
|
||||
interface MapSightbarProps {
|
||||
mapService: MapService | null;
|
||||
mapFeatures: Feature<Geometry>[];
|
||||
@@ -2364,7 +2258,6 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
[mapFeatures]
|
||||
);
|
||||
|
||||
// Создаем объединенный список всех объектов для поиска
|
||||
const allFeatures = useMemo(() => {
|
||||
const stations = mapStore.filteredStations.map((station) => {
|
||||
const feature = new Feature({
|
||||
@@ -2437,7 +2330,6 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
const ctrlKey = event?.ctrlKey || event?.metaKey;
|
||||
|
||||
if (ctrlKey) {
|
||||
// Множественный выбор: добавляем к существующему
|
||||
const newSet = new Set(selectedIds);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
@@ -2447,7 +2339,6 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
setSelectedIds(newSet);
|
||||
mapService.setSelectedIds(newSet);
|
||||
} else {
|
||||
// Одиночный выбор: используем стандартный метод
|
||||
mapService.selectFeature(id);
|
||||
}
|
||||
},
|
||||
@@ -2505,32 +2396,19 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
if (isNaN(numericRouteId)) return;
|
||||
|
||||
const isHidden = mapStore.hiddenRoutes.has(numericRouteId);
|
||||
console.log(
|
||||
`[handleHideRoute] Route ${numericRouteId}, isHidden: ${isHidden}`
|
||||
);
|
||||
|
||||
try {
|
||||
if (isHidden) {
|
||||
console.log(`[handleHideRoute] Showing route ${numericRouteId}`);
|
||||
// Показываем маршрут обратно
|
||||
const route = mapStore.routes.find((r) => r.id === numericRouteId);
|
||||
if (!route) {
|
||||
console.warn(
|
||||
`[handleHideRoute] Route ${numericRouteId} not found in mapStore`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const projection = mapService.getMap()?.getView().getProjection();
|
||||
if (!projection) {
|
||||
console.error(`[handleHideRoute] Failed to get map projection`);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
`[handleHideRoute] Route ${numericRouteId} (${route.route_number}) found, showing`
|
||||
);
|
||||
|
||||
// Показываем сам маршрут
|
||||
const coordinates = route.path
|
||||
.filter((c) => c && c[0] != null && c[1] != null)
|
||||
.map((c: [number, number]) =>
|
||||
@@ -2546,33 +2424,19 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
lineFeature.setId(routeId);
|
||||
lineFeature.set("featureType", "route");
|
||||
mapService.lineSource.addFeature(lineFeature);
|
||||
console.log(`[handleHideRoute] Added route line to map`);
|
||||
} else {
|
||||
console.warn(
|
||||
`[handleHideRoute] No valid coordinates for route ${numericRouteId}`
|
||||
);
|
||||
}
|
||||
|
||||
// Получаем станции текущего маршрута из кэша
|
||||
const routeStationIds =
|
||||
mapStore.routeStationsCache.get(numericRouteId) || [];
|
||||
console.log(
|
||||
`[handleHideRoute] Route ${numericRouteId} has ${routeStationIds.length} stations`
|
||||
);
|
||||
|
||||
// Получаем все маршруты для проверки
|
||||
const allRouteIds = mapStore.routes.map((r) => r.id);
|
||||
|
||||
// Исключаем скрытые маршруты из проверки
|
||||
const visibleRouteIds = allRouteIds.filter(
|
||||
(id: number) =>
|
||||
id !== numericRouteId && !mapStore.hiddenRoutes.has(id)
|
||||
);
|
||||
console.log(
|
||||
`[handleHideRoute] Checking against ${visibleRouteIds.length} visible routes (excluding hidden)`
|
||||
);
|
||||
|
||||
// Собираем все станции видимых маршрутов из кэша
|
||||
const stationsInVisibleRoutes = new Set<number>();
|
||||
visibleRouteIds.forEach((otherRouteId) => {
|
||||
const stationIds =
|
||||
@@ -2582,16 +2446,10 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[handleHideRoute] Found ${stationsInVisibleRoutes.size} unique stations in visible routes`
|
||||
);
|
||||
|
||||
// Показываем станции, которые не используются в других ВИДИМЫХ маршрутах
|
||||
const stationsToShow = routeStationIds.filter(
|
||||
(id: number) => !stationsInVisibleRoutes.has(id)
|
||||
);
|
||||
|
||||
// Показываем станции на карте
|
||||
for (const stationId of stationsToShow) {
|
||||
const station = mapStore.stations.find((s) => s.id === stationId);
|
||||
if (!station) continue;
|
||||
@@ -2610,7 +2468,6 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
feature.setId(`station-${station.id}`);
|
||||
feature.set("featureType", "station");
|
||||
|
||||
// Добавляем станцию только если её еще нет на карте
|
||||
const existingFeature = mapService.pointSource.getFeatureById(
|
||||
`station-${station.id}`
|
||||
);
|
||||
@@ -2619,36 +2476,19 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем из скрытых
|
||||
mapStore.hiddenRoutes.delete(numericRouteId);
|
||||
saveHiddenRoutes(mapStore.hiddenRoutes); // Сохраняем в localStorage
|
||||
console.log(
|
||||
`[handleHideRoute] Removed ${numericRouteId} from hiddenRoutes, stations to show: ${stationsToShow.length}`
|
||||
);
|
||||
saveHiddenRoutes(mapStore.hiddenRoutes);
|
||||
} else {
|
||||
// Скрываем маршрут
|
||||
console.log(`[handleHideRoute] Hiding route ${numericRouteId}`);
|
||||
|
||||
// Получаем станции текущего маршрута из кэша
|
||||
const routeStationIds =
|
||||
mapStore.routeStationsCache.get(numericRouteId) || [];
|
||||
console.log(
|
||||
`[handleHideRoute] Route ${numericRouteId} has ${routeStationIds.length} stations`
|
||||
);
|
||||
|
||||
// Получаем все маршруты для проверки
|
||||
const allRouteIds = mapStore.routes.map((r) => r.id);
|
||||
|
||||
// Исключаем скрытые маршруты из проверки
|
||||
const visibleRouteIds = allRouteIds.filter(
|
||||
(id: number) =>
|
||||
id !== numericRouteId && !mapStore.hiddenRoutes.has(id)
|
||||
);
|
||||
console.log(
|
||||
`[handleHideRoute] Checking against ${visibleRouteIds.length} visible routes (excluding hidden)`
|
||||
);
|
||||
|
||||
// Собираем все станции видимых маршрутов из кэша
|
||||
const stationsInVisibleRoutes = new Set<number>();
|
||||
visibleRouteIds.forEach((otherRouteId) => {
|
||||
const stationIds =
|
||||
@@ -2658,16 +2498,10 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[handleHideRoute] Found ${stationsInVisibleRoutes.size} unique stations in visible routes`
|
||||
);
|
||||
|
||||
// Скрываем станции, которые не используются в других ВИДИМЫХ маршрутах
|
||||
const stationsToHide = routeStationIds.filter(
|
||||
(id: number) => !stationsInVisibleRoutes.has(id)
|
||||
);
|
||||
|
||||
// Скрываем станции с карты
|
||||
stationsToHide.forEach((stationId: number) => {
|
||||
const pointFeature = mapService.pointSource.getFeatureById(
|
||||
`station-${stationId}`
|
||||
@@ -2679,7 +2513,6 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
}
|
||||
});
|
||||
|
||||
// Скрываем сам маршрут с карты
|
||||
const lineFeature = mapService.lineSource.getFeatureById(routeId);
|
||||
if (lineFeature) {
|
||||
mapService.lineSource.removeFeature(
|
||||
@@ -2687,15 +2520,10 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
);
|
||||
}
|
||||
|
||||
// Добавляем в скрытые
|
||||
mapStore.hiddenRoutes.add(numericRouteId);
|
||||
saveHiddenRoutes(mapStore.hiddenRoutes); // Сохраняем в localStorage
|
||||
console.log(
|
||||
`[handleHideRoute] Added ${numericRouteId} to hiddenRoutes, stations to hide: ${stationsToHide.length}`
|
||||
);
|
||||
saveHiddenRoutes(mapStore.hiddenRoutes);
|
||||
}
|
||||
|
||||
// Снимаем выделение
|
||||
mapService.unselect();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -2801,12 +2629,11 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
{features.length > 0 ? (
|
||||
features.map((feature) => {
|
||||
const fId = feature.getId();
|
||||
if (fId === undefined) return null; // TypeScript-safe
|
||||
if (fId === undefined) return null;
|
||||
const fName = (feature.get("name") as string) || "Без названия";
|
||||
const isSelected = selectedFeature?.getId() === fId;
|
||||
const isChecked = selectedIds.has(fId);
|
||||
|
||||
// Проверяем, скрыт ли маршрут
|
||||
const numericRouteId =
|
||||
featureType === "route"
|
||||
? parseInt(String(fId).split("-")[1], 10)
|
||||
@@ -3146,7 +2973,7 @@ const MapSightbar: React.FC<MapSightbarProps> = observer(
|
||||
);
|
||||
}
|
||||
);
|
||||
// --- MAP PAGE COMPONENT ---
|
||||
|
||||
export const MapPage: React.FC = observer(() => {
|
||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -3177,7 +3004,6 @@ export const MapPage: React.FC = observer(() => {
|
||||
|
||||
const handleFeatureSelectForSidebar = useCallback(
|
||||
(feat: Feature<Geometry> | null) => {
|
||||
// Logic to sync sidebar selection with map
|
||||
setSelectedFeatureForSidebar(feat);
|
||||
if (feat) {
|
||||
const featureType = feat.get("featureType");
|
||||
@@ -3239,7 +3065,6 @@ export const MapPage: React.FC = observer(() => {
|
||||
);
|
||||
setMapServiceInstance(service);
|
||||
|
||||
// Делаем mapServiceInstance доступным глобально для сброса кешей
|
||||
if (typeof window !== "undefined") {
|
||||
(window as any).mapServiceInstance = service;
|
||||
}
|
||||
@@ -3257,15 +3082,12 @@ export const MapPage: React.FC = observer(() => {
|
||||
service?.destroy();
|
||||
setMapServiceInstance(null);
|
||||
|
||||
// Удаляем глобальную ссылку
|
||||
if (typeof window !== "undefined") {
|
||||
delete (window as any).mapServiceInstance;
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// --- ИСПРАВЛЕНИЕ: Этот хук отвечает ТОЛЬКО за клики по ПУСТОМУ месту
|
||||
useEffect(() => {
|
||||
const olMap = mapServiceInstance?.getMap();
|
||||
if (!olMap || !mapServiceInstance) return;
|
||||
@@ -3280,11 +3102,9 @@ export const MapPage: React.FC = observer(() => {
|
||||
hitTolerance: 5,
|
||||
});
|
||||
|
||||
// Если клик был НЕ по объекту, снимаем выделение
|
||||
if (!hit) {
|
||||
mapServiceInstance.unselect();
|
||||
}
|
||||
// Если клик был ПО объекту, НИЧЕГО не делаем. За это отвечает selectInteraction.
|
||||
};
|
||||
|
||||
olMap.on("click", handleMapClickForDeselect);
|
||||
@@ -3337,14 +3157,11 @@ export const MapPage: React.FC = observer(() => {
|
||||
saveActiveSection(activeSectionFromParent);
|
||||
}, [activeSectionFromParent]);
|
||||
|
||||
// Перезагружаем данные при изменении города
|
||||
useEffect(() => {
|
||||
if (mapServiceInstance && !isDataLoading) {
|
||||
// Очищаем текущие объекты на карте
|
||||
mapServiceInstance.pointSource.clear();
|
||||
mapServiceInstance.lineSource.clear();
|
||||
|
||||
// Загружаем новые данные с фильтрацией по городу
|
||||
mapServiceInstance.loadFeaturesFromApi(
|
||||
mapStore.stations,
|
||||
mapStore.routes,
|
||||
@@ -3353,14 +3170,11 @@ export const MapPage: React.FC = observer(() => {
|
||||
}
|
||||
}, [selectedCityId, mapServiceInstance, isDataLoading]);
|
||||
|
||||
// Перезагружаем данные при изменении настройки скрытия достопримечательностей
|
||||
useEffect(() => {
|
||||
if (mapServiceInstance && !isDataLoading) {
|
||||
// Очищаем текущие объекты на карте
|
||||
mapServiceInstance.pointSource.clear();
|
||||
mapServiceInstance.lineSource.clear();
|
||||
|
||||
// Загружаем новые данные с учетом фильтрации достопримечательностей
|
||||
mapServiceInstance.loadFeaturesFromApi(
|
||||
mapStore.stations,
|
||||
mapStore.routes,
|
||||
|
||||
@@ -22,10 +22,8 @@ interface ApiSight {
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
// Допуск для сравнения координат, чтобы избежать ошибок с точностью чисел.
|
||||
const COORDINATE_PRECISION_TOLERANCE = 1e-9;
|
||||
|
||||
// Вспомогательная функция, обновленная для сравнения с допуском.
|
||||
const arePathsEqual = (
|
||||
path1: [number, number][],
|
||||
path2: [number, number][]
|
||||
@@ -136,7 +134,6 @@ class MapStore {
|
||||
longitude: geometry.coordinates[0],
|
||||
};
|
||||
|
||||
// ИЗМЕНЕНИЕ: Сравнение координат с допуском
|
||||
if (
|
||||
originalStation.name !== currentStation.name ||
|
||||
Math.abs(originalStation.latitude - currentStation.latitude) >
|
||||
@@ -155,7 +152,6 @@ class MapStore {
|
||||
path: geometry.coordinates,
|
||||
};
|
||||
|
||||
// ИЗМЕНЕНИЕ: Используется новая функция arePathsEqual с допуском
|
||||
if (
|
||||
originalRoute.route_number !== currentRoute.route_number ||
|
||||
!arePathsEqual(originalRoute.path, currentRoute.path)
|
||||
@@ -173,7 +169,6 @@ class MapStore {
|
||||
longitude: geometry.coordinates[0],
|
||||
};
|
||||
|
||||
// ИЗМЕНЕНИЕ: Сравнение координат с допуском
|
||||
if (
|
||||
originalSight.name !== currentSight.name ||
|
||||
originalSight.description !== currentSight.description ||
|
||||
|
||||
@@ -33,7 +33,7 @@ export const MediaEditPage = observer(() => {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [newFile, setNewFile] = useState<File | null>(null);
|
||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false); // State for the upload dialog
|
||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||
|
||||
const media = id ? mediaStore.media.find((m) => m.id === id) : null;
|
||||
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
|
||||
@@ -55,51 +55,27 @@ export const MediaEditPage = observer(() => {
|
||||
setMediaFilename(media.filename);
|
||||
setMediaType(media.media_type);
|
||||
|
||||
// Set available media types based on current file extension
|
||||
const extension = media.filename.split(".").pop()?.toLowerCase();
|
||||
if (extension) {
|
||||
if (["glb", "gltf"].includes(extension)) {
|
||||
setAvailableMediaTypes([6]); // 3D model
|
||||
setAvailableMediaTypes([6]);
|
||||
} else if (
|
||||
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
||||
extension
|
||||
)
|
||||
) {
|
||||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
||||
setAvailableMediaTypes([1, 3, 4, 5]);
|
||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||
setAvailableMediaTypes([2]); // Video
|
||||
setAvailableMediaTypes([2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [media]);
|
||||
|
||||
useEffect(() => {
|
||||
// Устанавливаем русский язык при загрузке страницы
|
||||
languageStore.setLanguage("ru");
|
||||
}, []);
|
||||
|
||||
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
// setIsDragging(false);
|
||||
|
||||
// const files = Array.from(e.dataTransfer.files);
|
||||
// if (files.length > 0) {
|
||||
// setNewFile(files[0]);
|
||||
// setMediaFilename(files[0].name);
|
||||
// setUploadDialogOpen(true); // Open dialog on file drop
|
||||
// }
|
||||
// };
|
||||
|
||||
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
// setIsDragging(true);
|
||||
// };
|
||||
|
||||
// const handleDragLeave = () => {
|
||||
// setIsDragging(false);
|
||||
// };
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
@@ -107,26 +83,25 @@ export const MediaEditPage = observer(() => {
|
||||
setNewFile(file);
|
||||
setMediaFilename(file.name);
|
||||
|
||||
// Determine media type based on file extension
|
||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||
if (extension) {
|
||||
if (["glb", "gltf"].includes(extension)) {
|
||||
setAvailableMediaTypes([6]); // 3D model
|
||||
setAvailableMediaTypes([6]);
|
||||
setMediaType(6);
|
||||
} else if (
|
||||
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
|
||||
extension
|
||||
)
|
||||
) {
|
||||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
||||
setMediaType(1); // Default to Photo
|
||||
setAvailableMediaTypes([1, 3, 4, 5]);
|
||||
setMediaType(1);
|
||||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||||
setAvailableMediaTypes([2]); // Video
|
||||
setAvailableMediaTypes([2]);
|
||||
setMediaType(2);
|
||||
}
|
||||
}
|
||||
|
||||
setUploadDialogOpen(true); // Open dialog on file selection
|
||||
setUploadDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -143,11 +118,6 @@ export const MediaEditPage = observer(() => {
|
||||
type: mediaType,
|
||||
});
|
||||
|
||||
// If a new file was selected, the actual file upload will happen
|
||||
// via the UploadMediaDialog. We just need to make sure the metadata
|
||||
// is updated correctly before or after.
|
||||
// Since the dialog handles the actual upload, we don't call updateMediaFile here.
|
||||
|
||||
setSuccess(true);
|
||||
handleUploadSuccess();
|
||||
} catch (err) {
|
||||
@@ -158,17 +128,15 @@ export const MediaEditPage = observer(() => {
|
||||
};
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
// After successful upload in the dialog, refresh media data if needed
|
||||
if (id) {
|
||||
mediaStore.getOneMedia(id);
|
||||
}
|
||||
setNewFile(null); // Clear the new file state after successful upload
|
||||
setNewFile(null);
|
||||
setUploadDialogOpen(false);
|
||||
setSuccess(true);
|
||||
};
|
||||
|
||||
if (!media && id) {
|
||||
// Only show loading if an ID is present and media is not yet loaded
|
||||
return (
|
||||
<Box className="flex justify-center items-center h-screen">
|
||||
<CircularProgress />
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
} from "@shared";
|
||||
import { EditStationModal } from "../../widgets/modals/EditStationModal";
|
||||
|
||||
// Helper function to insert an item at a specific position (1-based index)
|
||||
function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
||||
const index = pos - 1;
|
||||
const result = [...arr];
|
||||
@@ -54,7 +53,6 @@ function insertAtPosition<T>(arr: T[], pos: number, value: T): T[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper function to reorder items after drag and drop
|
||||
const reorder = <T,>(list: T[], startIndex: number, endIndex: number): T[] => {
|
||||
const result = Array.from(list);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
@@ -152,13 +150,11 @@ const LinkedItemsContentsInner = <
|
||||
const availableItems = allItems
|
||||
.filter((item) => !linkedItems.some((linked) => linked.id === item.id))
|
||||
.filter((item) => {
|
||||
// Если направление маршрута не указано, показываем все станции
|
||||
if (routeDirection === undefined) return true;
|
||||
// Фильтруем станции по направлению маршрута
|
||||
|
||||
return item.direction === routeDirection;
|
||||
})
|
||||
.filter((item) => {
|
||||
// Фильтруем по городу из навбара
|
||||
const selectedCityId = selectedCityStore.selectedCityId;
|
||||
if (selectedCityId && "city_id" in item) {
|
||||
return item.city_id === selectedCityId;
|
||||
@@ -167,7 +163,6 @@ const LinkedItemsContentsInner = <
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Фильтрация по поиску для массового режима
|
||||
const filteredAvailableItems = availableItems.filter((item) => {
|
||||
if (!searchQuery.trim()) return true;
|
||||
return String(item.name).toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
@@ -113,7 +113,6 @@ export const RouteCreatePage = observer(() => {
|
||||
const handleArticleSelect = (articleId: number) => {
|
||||
setGovernorAppeal(articleId.toString());
|
||||
setIsSelectArticleDialogOpen(false);
|
||||
// Обновляем список статей после создания новой
|
||||
articlesStore.getArticleList();
|
||||
};
|
||||
|
||||
@@ -155,7 +154,6 @@ export const RouteCreatePage = observer(() => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Валидация обязательных полей
|
||||
if (!routeName.trim()) {
|
||||
toast.error("Заполните название маршрута");
|
||||
setIsLoading(false);
|
||||
@@ -189,10 +187,9 @@ export const RouteCreatePage = observer(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Валидация масштабов
|
||||
const scale_min = scaleMin ? Number(scaleMin) : null;
|
||||
const scale_max = scaleMax ? Number(scaleMax) : null;
|
||||
console.log(scale_min, scale_max);
|
||||
|
||||
if (
|
||||
scale_min === 0 ||
|
||||
scale_max === 0 ||
|
||||
@@ -215,7 +212,6 @@ export const RouteCreatePage = observer(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Преобразуем значения в нужные типы
|
||||
const carrier_id = Number(carrier);
|
||||
const governor_appeal = Number(governorAppeal);
|
||||
const rotate = turn ? Number(turn) : undefined;
|
||||
@@ -223,7 +219,6 @@ export const RouteCreatePage = observer(() => {
|
||||
const center_longitude = centerLng ? Number(centerLng) : undefined;
|
||||
const route_direction = direction === "forward";
|
||||
|
||||
// Координаты маршрута как массив массивов чисел
|
||||
const path = routeCoords
|
||||
.trim()
|
||||
.split("\n")
|
||||
@@ -235,7 +230,6 @@ export const RouteCreatePage = observer(() => {
|
||||
return [lat, lon];
|
||||
});
|
||||
|
||||
// Собираем объект маршрута
|
||||
const newRoute: Partial<Route> = {
|
||||
carrier:
|
||||
carrierStore.carriers[
|
||||
@@ -268,7 +262,6 @@ export const RouteCreatePage = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
// Получаем название выбранной статьи для отображения
|
||||
const selectedArticle = articlesStore.articleList.ru.data.find(
|
||||
(article) => article.id === Number(governorAppeal)
|
||||
);
|
||||
@@ -429,7 +422,6 @@ export const RouteCreatePage = observer(() => {
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setScaleMin(value);
|
||||
// Если максимальный масштаб стал меньше минимального, обновляем его
|
||||
if (value && scaleMax && Number(value) > Number(scaleMax)) {
|
||||
setScaleMax(value);
|
||||
}
|
||||
|
||||
@@ -47,10 +47,8 @@ export function InfiniteCanvas({
|
||||
const [startPosition, setStartPosition] = useState({ x: 0, y: 0 });
|
||||
const [isPointerDown, setIsPointerDown] = useState(false);
|
||||
|
||||
// Флаг для предотвращения конфликта между пользовательским вводом и данными маршрута
|
||||
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
||||
|
||||
// Реф для отслеживания последнего значения originalRouteData?.rotate
|
||||
const lastOriginalRotation = useRef<number | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,7 +66,7 @@ export function InfiniteCanvas({
|
||||
const handlePointerDown = (e: FederatedMouseEvent) => {
|
||||
setIsPointerDown(true);
|
||||
setIsDragging(false);
|
||||
setIsUserInteracting(true); // Устанавливаем флаг взаимодействия пользователя
|
||||
setIsUserInteracting(true);
|
||||
setStartPosition({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
@@ -81,13 +79,9 @@ export function InfiniteCanvas({
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
// Устанавливаем rotation только при изменении originalRouteData и отсутствии взаимодействия пользователя
|
||||
useEffect(() => {
|
||||
const newRotation = originalRouteData?.rotate ?? 0;
|
||||
|
||||
// Обновляем rotation только если:
|
||||
// 1. Пользователь не взаимодействует с канвасом
|
||||
// 2. Значение действительно изменилось
|
||||
if (!isUserInteracting && lastOriginalRotation.current !== newRotation) {
|
||||
setRotation((newRotation * Math.PI) / 180);
|
||||
lastOriginalRotation.current = newRotation;
|
||||
@@ -97,7 +91,6 @@ export function InfiniteCanvas({
|
||||
const handlePointerMove = (e: FederatedMouseEvent) => {
|
||||
if (!isPointerDown) return;
|
||||
|
||||
// Проверяем, началось ли перетаскивание
|
||||
if (!isDragging) {
|
||||
const dx = e.globalX - startMousePosition.x;
|
||||
const dy = e.globalY - startMousePosition.y;
|
||||
@@ -119,10 +112,8 @@ export function InfiniteCanvas({
|
||||
e.globalX - center.x
|
||||
);
|
||||
|
||||
// Calculate rotation difference in radians
|
||||
const rotationDiff = currentAngle - startAngle;
|
||||
|
||||
// Update rotation
|
||||
setRotation(startRotation + rotationDiff);
|
||||
|
||||
const cosDelta = Math.cos(rotationDiff);
|
||||
@@ -149,15 +140,13 @@ export function InfiniteCanvas({
|
||||
};
|
||||
|
||||
const handlePointerUp = (e: FederatedMouseEvent) => {
|
||||
// Если не было перетаскивания, то это простой клик - закрываем виджет
|
||||
if (!isDragging) {
|
||||
setSelectedSight(undefined);
|
||||
}
|
||||
|
||||
setIsPointerDown(false);
|
||||
setIsDragging(false);
|
||||
// Сбрасываем флаг взаимодействия через небольшую задержку
|
||||
// чтобы избежать немедленного срабатывания useEffect
|
||||
|
||||
setTimeout(() => {
|
||||
setIsUserInteracting(false);
|
||||
}, 100);
|
||||
@@ -166,29 +155,25 @@ export function InfiniteCanvas({
|
||||
|
||||
const handleWheel = (e: FederatedWheelEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsUserInteracting(true); // Устанавливаем флаг при зуме
|
||||
setIsUserInteracting(true);
|
||||
|
||||
// Get mouse position relative to canvas
|
||||
const mouseX = e.globalX - position.x;
|
||||
const mouseY = e.globalY - position.y;
|
||||
|
||||
// Calculate new scale
|
||||
const scaleMin = (routeData?.scale_min ?? 10) / SCALE_FACTOR;
|
||||
const scaleMax = (routeData?.scale_max ?? 20) / SCALE_FACTOR;
|
||||
|
||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; // Zoom out/in
|
||||
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
||||
const newScale = Math.max(scaleMin, Math.min(scaleMax, scale * zoomFactor));
|
||||
const actualZoomFactor = newScale / scale;
|
||||
|
||||
if (scale === newScale) {
|
||||
// Сбрасываем флаг, если зум не изменился
|
||||
setTimeout(() => {
|
||||
setIsUserInteracting(false);
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update position to zoom towards mouse cursor
|
||||
setPosition({
|
||||
x: position.x + mouseX * (1 - actualZoomFactor),
|
||||
y: position.y + mouseY * (1 - actualZoomFactor),
|
||||
@@ -196,7 +181,6 @@ export function InfiniteCanvas({
|
||||
|
||||
setScale(newScale);
|
||||
|
||||
// Сбрасываем флаг взаимодействия через задержку
|
||||
setTimeout(() => {
|
||||
setIsUserInteracting(false);
|
||||
}, 100);
|
||||
|
||||
@@ -141,7 +141,6 @@ export const MapDataProvider = observer(
|
||||
}, [routeId]);
|
||||
|
||||
useEffect(() => {
|
||||
// combine changes with original data
|
||||
if (originalRouteData)
|
||||
setRouteData({ ...originalRouteData, ...routeChanges });
|
||||
if (originalSightData) setSightData(originalSightData);
|
||||
|
||||
@@ -37,11 +37,9 @@ export function RightSidebar() {
|
||||
|
||||
useEffect(() => {
|
||||
if (originalRouteData) {
|
||||
// Проверяем и сбрасываем минимальный масштаб если нужно
|
||||
const originalMinScale = originalRouteData.scale_min ?? 1;
|
||||
const resetMinScale = originalMinScale < 1 ? 1 : originalMinScale;
|
||||
|
||||
// Проверяем и сбрасываем максимальный масштаб если нужно
|
||||
const originalMaxScale = originalRouteData.scale_max ?? 5;
|
||||
const resetMaxScale = originalMaxScale < 3 ? 3 : originalMaxScale;
|
||||
|
||||
@@ -130,7 +128,6 @@ export function RightSidebar() {
|
||||
onChange={(e) => {
|
||||
let newMinScale = Number(e.target.value);
|
||||
|
||||
// Сбрасываем к 1 если меньше
|
||||
if (newMinScale < 1) {
|
||||
newMinScale = 1;
|
||||
}
|
||||
@@ -139,10 +136,10 @@ export function RightSidebar() {
|
||||
|
||||
if (maxScale - newMinScale < 2) {
|
||||
let newMaxScale = newMinScale + 2;
|
||||
// Сбрасываем максимальный к 3 если меньше минимального
|
||||
|
||||
if (newMaxScale < 3) {
|
||||
newMaxScale = 3;
|
||||
setMinScale(1); // Сбрасываем минимальный к 1
|
||||
setMinScale(1);
|
||||
}
|
||||
setMaxScale(newMaxScale);
|
||||
}
|
||||
@@ -175,7 +172,6 @@ export function RightSidebar() {
|
||||
onChange={(e) => {
|
||||
let newMaxScale = Number(e.target.value);
|
||||
|
||||
// Сбрасываем к 3 если меньше минимального
|
||||
if (newMaxScale < 3) {
|
||||
newMaxScale = 3;
|
||||
}
|
||||
@@ -184,10 +180,10 @@ export function RightSidebar() {
|
||||
|
||||
if (newMaxScale - minScale < 2) {
|
||||
let newMinScale = newMaxScale - 2;
|
||||
// Сбрасываем минимальный к 1 если меньше
|
||||
|
||||
if (newMinScale < 1) {
|
||||
newMinScale = 1;
|
||||
setMaxScale(3); // Сбрасываем максимальный к минимальному значению
|
||||
setMaxScale(3);
|
||||
}
|
||||
setMinScale(newMinScale);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { FederatedMouseEvent, Graphics } from "pixi.js";
|
||||
import { useCallback, useState, useEffect, useRef, FC, useMemo } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
// --- Заглушки для зависимостей (замените на ваши реальные импорты) ---
|
||||
import {
|
||||
BACKGROUND_COLOR,
|
||||
PATH_COLOR,
|
||||
@@ -15,22 +14,16 @@ import { StationData } from "./types";
|
||||
import { useMapData } from "./MapDataContext";
|
||||
import { coordinatesToLocal } from "./utils";
|
||||
import { languageStore } from "@shared";
|
||||
// --- Конец заглушек ---
|
||||
|
||||
// --- Декларации для react-pixi ---
|
||||
// (В реальном проекте типы должны быть предоставлены библиотекой @pixi/react-pixi)
|
||||
declare const pixiContainer: any;
|
||||
declare const pixiGraphics: any;
|
||||
declare const pixiText: any;
|
||||
|
||||
// --- Типы ---
|
||||
type HorizontalAlign = "left" | "center" | "right";
|
||||
type VerticalAlign = "top" | "center" | "bottom";
|
||||
type TextAlign = HorizontalAlign | `${HorizontalAlign} ${VerticalAlign}`;
|
||||
type LabelAlign = "left" | "center" | "right";
|
||||
|
||||
// --- Утилиты ---
|
||||
|
||||
/**
|
||||
* Преобразует текстовое позиционирование в anchor координаты.
|
||||
*/
|
||||
@@ -39,8 +32,6 @@ type LabelAlign = "left" | "center" | "right";
|
||||
* Получает координату anchor.x из типа выравнивания.
|
||||
*/
|
||||
|
||||
// --- Интерфейсы пропсов ---
|
||||
|
||||
interface StationProps {
|
||||
station: StationData;
|
||||
ruLabel: string | null;
|
||||
@@ -83,10 +74,6 @@ const getAnchorFromOffset = (
|
||||
return { x: (1 - nx) / 2, y: (1 - ny) / 2 };
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Компонент: Панель управления выравниванием в стиле УрФУ
|
||||
// =========================================================================
|
||||
|
||||
const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
||||
scale,
|
||||
currentAlign,
|
||||
@@ -107,7 +94,6 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
||||
(g: Graphics) => {
|
||||
g.clear();
|
||||
|
||||
// Основной фон с градиентом
|
||||
g.roundRect(
|
||||
-controlWidth / 2,
|
||||
0,
|
||||
@@ -115,9 +101,8 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
||||
controlHeight,
|
||||
borderRadius
|
||||
);
|
||||
g.fill({ color: "#1a1a1a" }); // Темный фон как у УрФУ
|
||||
g.fill({ color: "#1a1a1a" });
|
||||
|
||||
// Тонкая рамка
|
||||
g.roundRect(
|
||||
-controlWidth / 2,
|
||||
0,
|
||||
@@ -127,7 +112,6 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
||||
);
|
||||
g.stroke({ color: "#333333", width: strokeWidth });
|
||||
|
||||
// Разделители между кнопками
|
||||
for (let i = 1; i < 3; i++) {
|
||||
const x = -controlWidth / 2 + buttonWidth * i;
|
||||
g.moveTo(x, strokeWidth);
|
||||
@@ -151,7 +135,7 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
||||
controlHeight - strokeWidth * 2,
|
||||
borderRadius / 2
|
||||
);
|
||||
g.fill({ color: "#0066cc", alpha: 0.8 }); // Синий акцент УрФУ
|
||||
g.fill({ color: "#0066cc", alpha: 0.8 });
|
||||
}
|
||||
},
|
||||
[controlWidth, controlHeight, buttonWidth, strokeWidth, borderRadius]
|
||||
@@ -230,10 +214,6 @@ const LabelAlignmentControl: FC<LabelAlignmentControlProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Компонент: Метка Станции (с логикой)
|
||||
// =========================================================================
|
||||
|
||||
const StationLabel = observer(
|
||||
({
|
||||
station,
|
||||
@@ -274,48 +254,45 @@ const StationLabel = observer(
|
||||
hideTimer.current = null;
|
||||
}
|
||||
setIsHovered(true);
|
||||
onTextHover?.(true); // Call the callback to indicate text is hovered
|
||||
onTextHover?.(true);
|
||||
};
|
||||
|
||||
const handleControlPointerEnter = () => {
|
||||
// Дополнительная обработка для панели управления
|
||||
if (hideTimer.current) {
|
||||
clearTimeout(hideTimer.current);
|
||||
hideTimer.current = null;
|
||||
}
|
||||
setIsControlHovered(true);
|
||||
setIsHovered(true);
|
||||
onTextHover?.(true); // Call the callback to indicate text/control is hovered
|
||||
onTextHover?.(true);
|
||||
};
|
||||
|
||||
const handleControlPointerLeave = () => {
|
||||
setIsControlHovered(false);
|
||||
// Если курсор не над основным контейнером, скрываем панель через некоторое время
|
||||
|
||||
if (!isHovered) {
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setIsHovered(false);
|
||||
onTextHover?.(false); // Call the callback to indicate neither text nor control is hovered
|
||||
onTextHover?.(false);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePointerLeave = () => {
|
||||
// Увеличиваем время до скрытия панели и добавляем проверку
|
||||
hideTimer.current = setTimeout(() => {
|
||||
setIsHovered(false);
|
||||
// Если курсор не над панелью управления, скрываем и её
|
||||
|
||||
if (!isControlHovered) {
|
||||
setIsControlHovered(false);
|
||||
}
|
||||
onTextHover?.(false); // Call the callback to indicate text is no longer hovered
|
||||
}, 100); // Увеличиваем время до скрытия панели
|
||||
onTextHover?.(false);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPosition({ x: station.offset_x ?? 0, y: station.offset_y ?? 0 });
|
||||
}, [station.offset_x, station.offset_y, station.id]);
|
||||
|
||||
// Функция для конвертации числового align в строковый
|
||||
const convertNumericAlign = (align: number): LabelAlign => {
|
||||
switch (align) {
|
||||
case 0:
|
||||
@@ -329,7 +306,6 @@ const StationLabel = observer(
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для конвертации строкового align в числовой
|
||||
const convertStringAlign = (align: LabelAlign): number => {
|
||||
switch (align) {
|
||||
case "left":
|
||||
@@ -353,7 +329,6 @@ const StationLabel = observer(
|
||||
const compensatedRuFontSize = (26 * 0.75) / scale;
|
||||
const compensatedNameFontSize = (16 * 0.75) / scale;
|
||||
|
||||
// Измеряем ширину верхнего лейбла
|
||||
useEffect(() => {
|
||||
if (ruLabelRef.current && ruLabel) {
|
||||
setRuLabelWidth(ruLabelRef.current.width);
|
||||
@@ -386,7 +361,6 @@ const StationLabel = observer(
|
||||
y: dragStartPos.current.y + dy_screen,
|
||||
};
|
||||
|
||||
// Проверяем, изменилась ли позиция
|
||||
if (
|
||||
Math.abs(newPosition.x - position.x) > 0.01 ||
|
||||
Math.abs(newPosition.y - position.y) > 0.01
|
||||
@@ -406,7 +380,7 @@ const StationLabel = observer(
|
||||
const handleAlignChange = async (align: LabelAlign) => {
|
||||
setCurrentLabelAlign(align);
|
||||
onLabelAlignChange?.(align);
|
||||
// Сохраняем в стор
|
||||
|
||||
const numericAlign = convertStringAlign(align);
|
||||
setStationAlign(station.id, numericAlign);
|
||||
};
|
||||
@@ -416,34 +390,29 @@ const StationLabel = observer(
|
||||
[position.x, position.y]
|
||||
);
|
||||
|
||||
// Функция для расчета позиции нижнего лейбла относительно ширины верхнего
|
||||
const getSecondLabelPosition = (): number => {
|
||||
if (!ruLabelWidth) return 0;
|
||||
|
||||
switch (currentLabelAlign) {
|
||||
case "left":
|
||||
// Позиционируем относительно левого края верхнего текста
|
||||
return -ruLabelWidth / 2;
|
||||
case "center":
|
||||
// Центрируем относительно центра верхнего текста
|
||||
return 0;
|
||||
case "right":
|
||||
// Позиционируем относительно правого края верхнего текста
|
||||
return ruLabelWidth / 2;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Функция для расчета anchor нижнего лейбла
|
||||
const getSecondLabelAnchor = (): number => {
|
||||
switch (currentLabelAlign) {
|
||||
case "left":
|
||||
return 0; // anchor.x = 0 (левый край)
|
||||
return 0;
|
||||
case "center":
|
||||
return 0.5; // anchor.x = 0.5 (центр)
|
||||
return 0.5;
|
||||
case "right":
|
||||
return 1; // anchor.x = 1 (правый край)
|
||||
return 1;
|
||||
default:
|
||||
return 0.5;
|
||||
}
|
||||
@@ -522,10 +491,6 @@ const StationLabel = observer(
|
||||
}
|
||||
);
|
||||
|
||||
// =========================================================================
|
||||
// Главный экспортируемый компонент: Станция
|
||||
// =========================================================================
|
||||
|
||||
export const Station = ({
|
||||
station,
|
||||
ruLabel,
|
||||
@@ -548,10 +513,9 @@ export const Station = ({
|
||||
|
||||
g.circle(coordinates.x * UP_SCALE, coordinates.y * UP_SCALE, radius);
|
||||
|
||||
// Change fill color when text is hovered
|
||||
if (isTextHovered) {
|
||||
g.fill({ color: 0x00aaff }); // Highlight color when hovered
|
||||
g.stroke({ color: 0xffffff, width: strokeWidth + 1 }); // Brighter outline when hovered
|
||||
g.fill({ color: 0x00aaff });
|
||||
g.stroke({ color: 0xffffff, width: strokeWidth + 1 });
|
||||
} else {
|
||||
g.fill({ color: PATH_COLOR });
|
||||
g.stroke({ color: BACKGROUND_COLOR, width: strokeWidth });
|
||||
|
||||
@@ -50,7 +50,6 @@ const TransformContext = createContext<{
|
||||
setScaleAtCenter: () => {},
|
||||
});
|
||||
|
||||
// Provider component
|
||||
export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [scale, setScale] = useState(1);
|
||||
@@ -59,12 +58,10 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const screenToLocal = useCallback(
|
||||
(screenX: number, screenY: number) => {
|
||||
// Translate point relative to current pan position
|
||||
const translatedX = (screenX - position.x) / scale;
|
||||
const translatedY = (screenY - position.y) / scale;
|
||||
|
||||
// Rotate point around center
|
||||
const cosRotation = Math.cos(-rotation); // Negative rotation to reverse transform
|
||||
const cosRotation = Math.cos(-rotation);
|
||||
const sinRotation = Math.sin(-rotation);
|
||||
const rotatedX = translatedX * cosRotation - translatedY * sinRotation;
|
||||
const rotatedY = translatedX * sinRotation + translatedY * cosRotation;
|
||||
@@ -77,7 +74,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
[position.x, position.y, scale, rotation]
|
||||
);
|
||||
|
||||
// Inverse of screenToLocal
|
||||
const localToScreen = useCallback(
|
||||
(localX: number, localY: number) => {
|
||||
const upscaledX = localX * UP_SCALE;
|
||||
@@ -120,7 +116,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
(currentFromPosition.x - center.x) * sinDelta,
|
||||
};
|
||||
|
||||
// Update both rotation and position in a single batch to avoid stale closure
|
||||
setRotation(to);
|
||||
setPosition(newPosition);
|
||||
},
|
||||
@@ -150,13 +145,11 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
const cosRot = Math.cos(selectedRotation);
|
||||
const sinRot = Math.sin(selectedRotation);
|
||||
|
||||
// Translate point relative to center, rotate, then translate back
|
||||
const dx = newPosition.x;
|
||||
const dy = newPosition.y;
|
||||
newPosition.x = dx * cosRot - dy * sinRot + center.x;
|
||||
newPosition.y = dx * sinRot + dy * cosRot + center.y;
|
||||
|
||||
// Batch state updates to avoid intermediate renders
|
||||
setPosition(newPosition);
|
||||
setRotation(selectedRotation);
|
||||
setScale(selectedScale);
|
||||
@@ -184,7 +177,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
|
||||
const setScaleOnly = useCallback((newScale: number) => {
|
||||
// Изменяем только масштаб, не трогая позицию и поворот
|
||||
setScale(newScale);
|
||||
}, []);
|
||||
|
||||
@@ -237,7 +229,6 @@ export const TransformProvider = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// Custom hook for easy access to transform values
|
||||
export const useTransform = () => {
|
||||
const context = useContext(TransformContext);
|
||||
if (!context) {
|
||||
|
||||
@@ -53,13 +53,11 @@ export const WebGLMap = observer(() => {
|
||||
|
||||
const cameraAnimationStore = useCameraAnimationStore();
|
||||
|
||||
// Ref для хранения ограничений масштаба
|
||||
const scaleLimitsRef = useRef({
|
||||
min: null as number | null,
|
||||
max: null as number | null,
|
||||
});
|
||||
|
||||
// Обновляем ограничения масштаба при изменении routeData
|
||||
useEffect(() => {
|
||||
if (
|
||||
routeData?.scale_min !== undefined &&
|
||||
@@ -72,7 +70,6 @@ export const WebGLMap = observer(() => {
|
||||
}
|
||||
}, [routeData?.scale_min, routeData?.scale_max]);
|
||||
|
||||
// Функция для ограничения масштаба значениями с бекенда
|
||||
const clampScale = useCallback((value: number) => {
|
||||
const { min, max } = scaleLimitsRef.current;
|
||||
|
||||
@@ -90,7 +87,6 @@ export const WebGLMap = observer(() => {
|
||||
const setPositionRef = useRef(setPosition);
|
||||
const setScaleRef = useRef(setScale);
|
||||
|
||||
// Обновляем refs при изменении функций
|
||||
useEffect(() => {
|
||||
setPositionRef.current = setPosition;
|
||||
}, [setPosition]);
|
||||
@@ -99,7 +95,6 @@ export const WebGLMap = observer(() => {
|
||||
setScaleRef.current = setScale;
|
||||
}, [setScale]);
|
||||
|
||||
// Логирование данных маршрута для отладки
|
||||
useEffect(() => {
|
||||
if (routeData) {
|
||||
}
|
||||
@@ -124,7 +119,6 @@ export const WebGLMap = observer(() => {
|
||||
setPositionImmediate: setYellowDotPositionImmediate,
|
||||
} = useAnimatedPolarPosition(0, 0, 800);
|
||||
|
||||
// Build transformed route path (map coords)
|
||||
const routePath = useMemo(() => {
|
||||
if (!routeData?.path || routeData?.path.length === 0)
|
||||
return new Float32Array();
|
||||
@@ -180,7 +174,6 @@ export const WebGLMap = observer(() => {
|
||||
rotationAngle,
|
||||
]);
|
||||
|
||||
// Настройка CameraAnimationStore callback - только один раз при монтировании
|
||||
useEffect(() => {
|
||||
const callback = (newPos: { x: number; y: number }, newZoom: number) => {
|
||||
setPosition(newPos);
|
||||
@@ -189,15 +182,13 @@ export const WebGLMap = observer(() => {
|
||||
|
||||
cameraAnimationStore.setUpdateCallback(callback);
|
||||
|
||||
// Синхронизируем начальное состояние только один раз
|
||||
cameraAnimationStore.syncState(position, scale);
|
||||
|
||||
return () => {
|
||||
cameraAnimationStore.setUpdateCallback(null);
|
||||
};
|
||||
}, []); // Пустой массив - выполняется только при монтировании
|
||||
}, []);
|
||||
|
||||
// Установка границ зума
|
||||
useEffect(() => {
|
||||
if (
|
||||
routeData?.scale_min !== undefined &&
|
||||
@@ -208,28 +199,23 @@ export const WebGLMap = observer(() => {
|
||||
}
|
||||
}, [routeData?.scale_min, routeData?.scale_max, cameraAnimationStore]);
|
||||
|
||||
// Автоматический режим - таймер для включения через 5 секунд бездействия
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const timeSinceActivity = Date.now() - userActivityTimestamp;
|
||||
if (timeSinceActivity >= 5000 && !isAutoMode) {
|
||||
// 5 секунд бездействия - включаем авто режим
|
||||
setIsAutoMode(true);
|
||||
}
|
||||
}, 1000); // Проверяем каждую секунду
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [userActivityTimestamp, isAutoMode, setIsAutoMode]);
|
||||
|
||||
// Следование за желтой точкой с зумом при включенном авто режиме
|
||||
useEffect(() => {
|
||||
// Пропускаем обновление если анимация уже идет
|
||||
if (cameraAnimationStore.isActivelyAnimating) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAutoMode && transformedTramCoords && screenCenter) {
|
||||
// Преобразуем станции в формат для CameraAnimationStore
|
||||
const transformedStations = stationData
|
||||
? stationData
|
||||
.map((station: any) => {
|
||||
@@ -270,10 +256,8 @@ export const WebGLMap = observer(() => {
|
||||
cameraAnimationStore.setMaxZoom(scaleLimitsRef.current!.max);
|
||||
cameraAnimationStore.setMinZoom(scaleLimitsRef.current!.min);
|
||||
|
||||
// Синхронизируем текущее состояние камеры перед запуском анимации
|
||||
cameraAnimationStore.syncState(positionRef.current, scaleRef.current);
|
||||
|
||||
// Запускаем анимацию к желтой точке
|
||||
cameraAnimationStore.followTram(
|
||||
transformedTramCoords,
|
||||
screenCenter,
|
||||
@@ -293,7 +277,6 @@ export const WebGLMap = observer(() => {
|
||||
rotationAngle,
|
||||
]);
|
||||
|
||||
// Station label overlay positions (DOM overlay)
|
||||
const stationLabels = useMemo(() => {
|
||||
if (!stationData || !routeData)
|
||||
return [] as Array<{ x: number; y: number; name: string; sub?: string }>;
|
||||
@@ -356,7 +339,6 @@ export const WebGLMap = observer(() => {
|
||||
selectedLanguage as any,
|
||||
]);
|
||||
|
||||
// Build transformed stations (map coords)
|
||||
const stationPoints = useMemo(() => {
|
||||
if (!stationData || !routeData) return new Float32Array();
|
||||
const centerLat = routeData.center_latitude;
|
||||
@@ -386,7 +368,6 @@ export const WebGLMap = observer(() => {
|
||||
rotationAngle,
|
||||
]);
|
||||
|
||||
// Build transformed sights (map coords)
|
||||
const sightPoints = useMemo(() => {
|
||||
if (!sightData || !routeData) return new Float32Array();
|
||||
const centerLat = routeData.center_latitude;
|
||||
@@ -530,8 +511,6 @@ export const WebGLMap = observer(() => {
|
||||
const handleResize = () => {
|
||||
const changed = resizeCanvasToDisplaySize(canvas);
|
||||
if (!gl) return;
|
||||
// Update screen center when canvas size changes
|
||||
// Use physical pixels (canvas.width) instead of CSS pixels
|
||||
setScreenCenter({
|
||||
x: canvas.width / 2,
|
||||
y: canvas.height / 2,
|
||||
@@ -567,7 +546,6 @@ export const WebGLMap = observer(() => {
|
||||
const rx = x * cos - y * sin;
|
||||
const ry = x * sin + y * cos;
|
||||
|
||||
// В авторежиме используем анимацию, иначе мгновенное обновление
|
||||
if (isAutoMode) {
|
||||
animateYellowDotTo(rx, ry);
|
||||
} else {
|
||||
@@ -666,21 +644,18 @@ export const WebGLMap = observer(() => {
|
||||
|
||||
const vertexCount = routePath.length / 2;
|
||||
if (vertexCount > 1) {
|
||||
// Generate thick line geometry using triangles with proper joins
|
||||
const generateThickLine = (points: Float32Array, width: number) => {
|
||||
const vertices: number[] = [];
|
||||
const halfWidth = width / 2;
|
||||
|
||||
if (points.length < 4) return new Float32Array();
|
||||
|
||||
// Process each segment
|
||||
for (let i = 0; i < points.length - 2; i += 2) {
|
||||
const x1 = points[i];
|
||||
const y1 = points[i + 1];
|
||||
const x2 = points[i + 2];
|
||||
const y2 = points[i + 3];
|
||||
|
||||
// Calculate perpendicular vector
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
@@ -689,18 +664,14 @@ export const WebGLMap = observer(() => {
|
||||
const perpX = (-dy / length) * halfWidth;
|
||||
const perpY = (dx / length) * halfWidth;
|
||||
|
||||
// Create quad (two triangles) for this line segment
|
||||
// Triangle 1
|
||||
vertices.push(x1 + perpX, y1 + perpY);
|
||||
vertices.push(x1 - perpX, y1 - perpY);
|
||||
vertices.push(x2 + perpX, y2 + perpY);
|
||||
|
||||
// Triangle 2
|
||||
vertices.push(x1 - perpX, y1 - perpY);
|
||||
vertices.push(x2 - perpX, y2 - perpY);
|
||||
vertices.push(x2 + perpX, y2 + perpY);
|
||||
|
||||
// Add simple join triangles to fill gaps
|
||||
if (i < points.length - 4) {
|
||||
const x3 = points[i + 4];
|
||||
const y3 = points[i + 5];
|
||||
@@ -712,7 +683,6 @@ export const WebGLMap = observer(() => {
|
||||
const perpX2 = (-dy2 / length2) * halfWidth;
|
||||
const perpY2 = (dx2 / length2) * halfWidth;
|
||||
|
||||
// Simple join - just connect the endpoints
|
||||
vertices.push(x2 + perpX, y2 + perpY);
|
||||
vertices.push(x2 - perpX, y2 - perpY);
|
||||
vertices.push(x2 + perpX2, y2 + perpY2);
|
||||
@@ -734,22 +704,18 @@ export const WebGLMap = observer(() => {
|
||||
gl.uniform4f(uniforms.u_color, r1, g1, b1, 1);
|
||||
|
||||
if (tramSegIndex >= 0) {
|
||||
// Используем точную позицию желтой точки для определения конца красной линии
|
||||
const animatedPos = animatedYellowDotPosition;
|
||||
if (
|
||||
animatedPos &&
|
||||
animatedPos.x !== undefined &&
|
||||
animatedPos.y !== undefined
|
||||
) {
|
||||
// Создаем массив точек от начала маршрута до позиции желтой точки
|
||||
const passedPoints: number[] = [];
|
||||
|
||||
// Добавляем все точки до текущего сегмента
|
||||
for (let i = 0; i <= tramSegIndex; i++) {
|
||||
passedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
|
||||
}
|
||||
|
||||
// Добавляем точную позицию желтой точки как конечную точку
|
||||
passedPoints.push(animatedPos.x, animatedPos.y);
|
||||
|
||||
if (passedPoints.length >= 4) {
|
||||
@@ -768,7 +734,6 @@ export const WebGLMap = observer(() => {
|
||||
const b2 = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
||||
gl.uniform4f(uniforms.u_color, r2, g2, b2, 1);
|
||||
|
||||
// Серая линия начинается точно от позиции желтой точки
|
||||
const animatedPos = animatedYellowDotPosition;
|
||||
if (
|
||||
animatedPos &&
|
||||
@@ -777,10 +742,8 @@ export const WebGLMap = observer(() => {
|
||||
) {
|
||||
const unpassedPoints: number[] = [];
|
||||
|
||||
// Добавляем позицию желтой точки как начальную точку серой линии
|
||||
unpassedPoints.push(animatedPos.x, animatedPos.y);
|
||||
|
||||
// Добавляем все точки после текущего сегмента
|
||||
for (let i = tramSegIndex + 1; i < vertexCount; i++) {
|
||||
unpassedPoints.push(routePath[i * 2], routePath[i * 2 + 1]);
|
||||
}
|
||||
@@ -796,7 +759,6 @@ export const WebGLMap = observer(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw stations
|
||||
if (stationPoints.length > 0) {
|
||||
gl.useProgram(pprog);
|
||||
const a_pos_pts = gl.getAttribLocation(pprog, "a_pos");
|
||||
@@ -814,7 +776,6 @@ export const WebGLMap = observer(() => {
|
||||
gl.enableVertexAttribArray(a_pos_pts);
|
||||
gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Draw station outlines (black background)
|
||||
gl.uniform1f(u_pointSize, 10 * scale * 1.5);
|
||||
const r_outline = ((BACKGROUND_COLOR >> 16) & 0xff) / 255;
|
||||
const g_outline = ((BACKGROUND_COLOR >> 8) & 0xff) / 255;
|
||||
@@ -822,15 +783,12 @@ export const WebGLMap = observer(() => {
|
||||
gl.uniform4f(u_color_pts, r_outline, g_outline, b_outline, 1);
|
||||
gl.drawArrays(gl.POINTS, 0, stationPoints.length / 2);
|
||||
|
||||
// Draw station cores (colored based on passed/unpassed)
|
||||
gl.uniform1f(u_pointSize, 8.0 * scale * 1.5);
|
||||
|
||||
// Draw passed stations (red)
|
||||
if (tramSegIndex >= 0) {
|
||||
const passedStations = [];
|
||||
for (let i = 0; i < stationData.length; i++) {
|
||||
if (i <= tramSegIndex) {
|
||||
// @ts-ignore
|
||||
passedStations.push(stationPoints[i * 2], stationPoints[i * 2 + 1]);
|
||||
}
|
||||
}
|
||||
@@ -848,13 +806,11 @@ export const WebGLMap = observer(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw unpassed stations (gray)
|
||||
if (tramSegIndex >= 0) {
|
||||
const unpassedStations = [];
|
||||
for (let i = 0; i < stationData.length; i++) {
|
||||
if (i > tramSegIndex) {
|
||||
unpassedStations.push(
|
||||
// @ts-ignore
|
||||
stationPoints[i * 2],
|
||||
stationPoints[i * 2 + 1]
|
||||
);
|
||||
@@ -873,7 +829,6 @@ export const WebGLMap = observer(() => {
|
||||
gl.drawArrays(gl.POINTS, 0, unpassedStations.length / 2);
|
||||
}
|
||||
} else {
|
||||
// If no tram position, draw all stations as unpassed
|
||||
const r_unpassed = ((UNPASSED_STATION_COLOR >> 16) & 0xff) / 255;
|
||||
const g_unpassed = ((UNPASSED_STATION_COLOR >> 8) & 0xff) / 255;
|
||||
const b_unpassed = (UNPASSED_STATION_COLOR & 0xff) / 255;
|
||||
@@ -1015,7 +970,6 @@ export const WebGLMap = observer(() => {
|
||||
if (passedStations.length)
|
||||
gl.drawArrays(gl.POINTS, 0, passedStations.length / 2);
|
||||
|
||||
// Draw black dots with white outline for terminal stations (startStopId and endStopId) - 5x larger
|
||||
if (
|
||||
stationData &&
|
||||
stationData.length > 0 &&
|
||||
@@ -1028,7 +982,6 @@ export const WebGLMap = observer(() => {
|
||||
const cos = Math.cos(rotationAngle);
|
||||
const sin = Math.sin(rotationAngle);
|
||||
|
||||
// Find terminal stations using startStopId and endStopId from context
|
||||
const startStationData = stationData.find(
|
||||
(station) => station.id.toString() === apiStore.context?.startStopId
|
||||
);
|
||||
@@ -1038,7 +991,6 @@ export const WebGLMap = observer(() => {
|
||||
|
||||
const terminalStations: number[] = [];
|
||||
|
||||
// Transform start station coordinates if found
|
||||
if (startStationData) {
|
||||
const startLocal = coordinatesToLocal(
|
||||
startStationData.latitude - centerLat,
|
||||
@@ -1051,7 +1003,6 @@ export const WebGLMap = observer(() => {
|
||||
terminalStations.push(startRx, startRy);
|
||||
}
|
||||
|
||||
// Transform end station coordinates if found
|
||||
if (endStationData) {
|
||||
const endLocal = coordinatesToLocal(
|
||||
endStationData.latitude - centerLat,
|
||||
@@ -1065,12 +1016,10 @@ export const WebGLMap = observer(() => {
|
||||
}
|
||||
|
||||
if (terminalStations.length > 0) {
|
||||
// Determine if each terminal station is passed
|
||||
const terminalStationData: any[] = [];
|
||||
if (startStationData) terminalStationData.push(startStationData);
|
||||
if (endStationData) terminalStationData.push(endStationData);
|
||||
|
||||
// Get tram segment index for comparison
|
||||
let tramSegIndex = -1;
|
||||
const coords: any = apiStore?.context?.currentCoordinates;
|
||||
if (coords && centerLat !== undefined && centerLon !== undefined) {
|
||||
@@ -1085,7 +1034,6 @@ export const WebGLMap = observer(() => {
|
||||
const tx = wx * cosR - wy * sinR;
|
||||
const ty = wx * sinR + wy * cosR;
|
||||
|
||||
// Find closest segment to tram position
|
||||
let best = -1;
|
||||
let bestD = Infinity;
|
||||
for (let i = 0; i < routePath.length - 2; i += 2) {
|
||||
@@ -1110,7 +1058,6 @@ export const WebGLMap = observer(() => {
|
||||
tramSegIndex = best;
|
||||
}
|
||||
|
||||
// Check if each terminal station is passed
|
||||
const isStartPassed = startStationData
|
||||
? (() => {
|
||||
const sx = terminalStations[0];
|
||||
@@ -1186,46 +1133,41 @@ export const WebGLMap = observer(() => {
|
||||
gl.enableVertexAttribArray(a_pos_pts);
|
||||
gl.vertexAttribPointer(a_pos_pts, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Draw colored outline based on passed status - 24 pixels (x2)
|
||||
gl.uniform1f(u_pointSize, 18.0 * scale);
|
||||
if (startStationData && endStationData) {
|
||||
// Both stations - draw each with its own color
|
||||
if (isStartPassed) {
|
||||
gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных
|
||||
gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0);
|
||||
} else {
|
||||
gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных
|
||||
gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0);
|
||||
}
|
||||
gl.drawArrays(gl.POINTS, 0, 1); // Draw start station
|
||||
gl.drawArrays(gl.POINTS, 0, 1);
|
||||
|
||||
if (isEndPassed) {
|
||||
gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных
|
||||
gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0);
|
||||
} else {
|
||||
gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных
|
||||
gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0);
|
||||
}
|
||||
gl.drawArrays(gl.POINTS, 1, 1); // Draw end station
|
||||
gl.drawArrays(gl.POINTS, 1, 1);
|
||||
} else {
|
||||
// Single station - use appropriate color
|
||||
const isPassed = startStationData ? isStartPassed : isEndPassed;
|
||||
if (isPassed) {
|
||||
gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0); // Ярко-красный для пройденных
|
||||
gl.uniform4f(u_color_pts, 1.0, 0.4, 0.4, 1.0);
|
||||
} else {
|
||||
gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0); // Светло-серый для непройденных
|
||||
gl.uniform4f(u_color_pts, 0.7, 0.7, 0.7, 1.0);
|
||||
}
|
||||
gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2);
|
||||
}
|
||||
|
||||
// Draw dark center - 12 pixels (x2)
|
||||
gl.uniform1f(u_pointSize, 11.0 * scale);
|
||||
const r_center = ((BACKGROUND_COLOR >> 16) & 0xff) / 255;
|
||||
const g_center = ((BACKGROUND_COLOR >> 8) & 0xff) / 255;
|
||||
const b_center = (BACKGROUND_COLOR & 0xff) / 255;
|
||||
gl.uniform4f(u_color_pts, r_center, g_center, b_center, 1.0); // Dark color
|
||||
gl.uniform4f(u_color_pts, r_center, g_center, b_center, 1.0);
|
||||
gl.drawArrays(gl.POINTS, 0, terminalStations.length / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw yellow dot for tram position
|
||||
if (animatedYellowDotPosition) {
|
||||
const rx = animatedYellowDotPosition.x;
|
||||
const ry = animatedYellowDotPosition.y;
|
||||
@@ -1327,7 +1269,6 @@ export const WebGLMap = observer(() => {
|
||||
});
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
// Отслеживаем активность пользователя
|
||||
updateUserActivity();
|
||||
if (isAutoMode) {
|
||||
setIsAutoMode(false);
|
||||
@@ -1360,7 +1301,6 @@ export const WebGLMap = observer(() => {
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
if (!activePointers.has(e.pointerId)) return;
|
||||
|
||||
// Отслеживаем активность пользователя
|
||||
updateUserActivity();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
@@ -1386,7 +1326,6 @@ export const WebGLMap = observer(() => {
|
||||
};
|
||||
}
|
||||
|
||||
// Process the pinch gesture
|
||||
if (pinchStart) {
|
||||
const currentDistance = getDistance(p1, p2);
|
||||
const zoomFactor = currentDistance / pinchStart.distance;
|
||||
@@ -1405,7 +1344,6 @@ export const WebGLMap = observer(() => {
|
||||
} else if (isDragging && activePointers.size === 1) {
|
||||
const p = Array.from(activePointers.values())[0];
|
||||
|
||||
// Проверяем валидность значений
|
||||
if (
|
||||
!startMouse ||
|
||||
!startPos ||
|
||||
@@ -1433,7 +1371,6 @@ export const WebGLMap = observer(() => {
|
||||
};
|
||||
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
// Отслеживаем активность пользователя
|
||||
updateUserActivity();
|
||||
|
||||
canvas.releasePointerCapture(e.pointerId);
|
||||
@@ -1453,7 +1390,6 @@ export const WebGLMap = observer(() => {
|
||||
};
|
||||
|
||||
const onPointerCancel = (e: PointerEvent) => {
|
||||
// Handle pointer cancellation (e.g., when touch is interrupted)
|
||||
updateUserActivity();
|
||||
canvas.releasePointerCapture(e.pointerId);
|
||||
activePointers.delete(e.pointerId);
|
||||
@@ -1467,7 +1403,6 @@ export const WebGLMap = observer(() => {
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Отслеживаем активность пользователя
|
||||
updateUserActivity();
|
||||
if (isAutoMode) {
|
||||
setIsAutoMode(false);
|
||||
@@ -1475,7 +1410,6 @@ export const WebGLMap = observer(() => {
|
||||
cameraAnimationStore.stopAnimation();
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
// Convert mouse coordinates from CSS pixels to physical canvas pixels
|
||||
const mouseX =
|
||||
(e.clientX - rect.left) * (canvas.width / canvas.clientWidth);
|
||||
const mouseY =
|
||||
@@ -1582,7 +1516,6 @@ export const WebGLMap = observer(() => {
|
||||
const sy = (ry * scale + position.y) / dpr;
|
||||
const size = 30;
|
||||
|
||||
// Обработчик клика для выбора достопримечательности
|
||||
const handleSightClick = () => {
|
||||
const {
|
||||
setSelectedSightId,
|
||||
|
||||
@@ -35,7 +35,7 @@ export const StationCreatePage = observer(() => {
|
||||
const { cities, getCities } = cityStore;
|
||||
const { selectedCityId, selectedCity } = useSelectedCity();
|
||||
const [coordinates, setCoordinates] = useState<string>("");
|
||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
||||
|
||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,7 +49,6 @@ export const StationCreatePage = observer(() => {
|
||||
}
|
||||
}, [createStationData.common.latitude, createStationData.common.longitude]);
|
||||
|
||||
// НОВАЯ ФУНКЦИЯ: Фактическое создание (вызывается после подтверждения)
|
||||
const executeCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -64,11 +63,13 @@ export const StationCreatePage = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или создания
|
||||
const handleCreate = async () => {
|
||||
const isCityMissing = !createStationData.common.city_id;
|
||||
// Проверяем названия на всех языках
|
||||
const isNameMissing = !createStationData.ru.name || !createStationData.en.name || !createStationData.zh.name;
|
||||
|
||||
const isNameMissing =
|
||||
!createStationData.ru.name ||
|
||||
!createStationData.en.name ||
|
||||
!createStationData.zh.name;
|
||||
|
||||
if (isCityMissing || isNameMissing) {
|
||||
setIsSaveWarningOpen(true);
|
||||
@@ -78,13 +79,11 @@ export const StationCreatePage = observer(() => {
|
||||
await executeCreate();
|
||||
};
|
||||
|
||||
// Обработчик "Да" в предупреждающем окне
|
||||
const handleConfirmCreate = async () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
await executeCreate();
|
||||
};
|
||||
|
||||
// Обработчик "Нет" в предупреждающем окне
|
||||
const handleCancelCreate = () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
};
|
||||
@@ -99,7 +98,6 @@ export const StationCreatePage = observer(() => {
|
||||
fetchCities();
|
||||
}, []);
|
||||
|
||||
// Автоматически устанавливаем выбранный город при загрузке страницы
|
||||
useEffect(() => {
|
||||
if (selectedCityId && selectedCity && !createStationData.common.city_id) {
|
||||
setCreateCommonData({
|
||||
@@ -237,7 +235,7 @@ export const StationCreatePage = observer(() => {
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleCreate
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
|
||||
@@ -32,7 +32,7 @@ export const StationEditPage = observer(() => {
|
||||
} = stationsStore;
|
||||
const { cities, getCities } = cityStore;
|
||||
const [coordinates, setCoordinates] = useState<string>("");
|
||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
||||
|
||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,7 +50,6 @@ export const StationEditPage = observer(() => {
|
||||
}
|
||||
}, [editStationData.common.latitude, editStationData.common.longitude]);
|
||||
|
||||
// НОВАЯ ФУНКЦИЯ: Фактическое редактирование (вызывается после подтверждения)
|
||||
const executeEdit = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -64,10 +63,9 @@ export const StationEditPage = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или редактирования
|
||||
const handleEdit = async () => {
|
||||
const isCityMissing = !editStationData.common.city_id;
|
||||
// Проверяем названия на всех языках
|
||||
|
||||
const isNameMissing =
|
||||
!editStationData.ru.name ||
|
||||
!editStationData.en.name ||
|
||||
@@ -81,13 +79,11 @@ export const StationEditPage = observer(() => {
|
||||
await executeEdit();
|
||||
};
|
||||
|
||||
// Обработчик "Да" в предупреждающем окне
|
||||
const handleConfirmEdit = async () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
await executeEdit();
|
||||
};
|
||||
|
||||
// Обработчик "Нет" в предупреждающем окне
|
||||
const handleCancelEdit = () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
};
|
||||
@@ -243,7 +239,7 @@ export const StationEditPage = observer(() => {
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleEdit}
|
||||
disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleEdit
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
|
||||
Reference in New Issue
Block a user