diff --git a/src/pages/MapPage/index.tsx b/src/pages/MapPage/index.tsx index dc46b93..2df54b4 100644 --- a/src/pages/MapPage/index.tsx +++ b/src/pages/MapPage/index.tsx @@ -18,13 +18,92 @@ import { Style, Fill, Stroke, Circle as CircleStyle } from "ol/style"; import { Point, LineString, Geometry } from "ol/geom"; import { transform } from "ol/proj"; import { GeoJSON } from "ol/format"; -import { Bus, RouteIcon, MapPin, Trash2, ArrowRightLeft } from "lucide-react"; -import { altKeyOnly, primaryAction, singleClick } from "ol/events/condition"; +import { + Bus, + RouteIcon, + MapPin, + Trash2, + ArrowRightLeft, + Landmark, +} from "lucide-react"; +import { singleClick, doubleClick } from "ol/events/condition"; import { Feature } from "ol"; import Layer from "ol/layer/Layer"; import Source from "ol/source/Source"; -import LayerRenderer from "ol/renderer/Layer"; import { FeatureLike } from "ol/Feature"; +import { authInstance } from "@shared"; + +// --- API INTERFACES --- +interface ApiRoute { + id: number; + route_number: string; + path: [number, number][]; // [longitude, latitude][] +} + +interface ApiStation { + id: number; + name: string; + latitude: number; + longitude: number; +} + +interface ApiSight { + id: number; + name: string; + description: string; + latitude: number; + longitude: number; +} + +// --- MOCK API (для имитации запросов к серверу) --- +const mockApi = { + getRoutes: async (): Promise => { + console.log("Fetching routes..."); + await new Promise((res) => setTimeout(res, 500)); // Имитация задержки сети + return [ + { + id: 1, + route_number: "А-78", + path: [ + [30.315, 59.934], + [30.32, 59.936], + [30.325, 59.938], + [30.33, 59.94], + ], + }, + ]; + }, + getStations: async (): Promise => { + console.log("Fetching stations..."); + // const stations = await authInstance.get("/station"); + + await new Promise((res) => setTimeout(res, 400)); + return [ + { id: 101, name: "Гостиный двор", latitude: 59.934, longitude: 30.332 }, + { id: 102, name: "Пл. Восстания", latitude: 59.931, longitude: 30.362 }, + ]; + }, + getSights: async (): Promise => { + console.log("Fetching sights..."); + await new Promise((res) => setTimeout(res, 600)); + return [ + { + id: 201, + name: "Спас на Крови", + description: "Храм Воскресения Христова", + latitude: 59.94, + longitude: 30.329, + }, + { + id: 202, + name: "Казанский собор", + description: "Кафедральный собор", + latitude: 59.934, + longitude: 30.325, + }, + ]; + }, +}; // --- CONFIGURATION --- export const mapConfig = { @@ -49,22 +128,6 @@ const EditIcon = () => ( /> ); -const StatsIcon = () => ( - - - -); const LineIconSvg = () => ( >; @@ -107,7 +168,6 @@ class MapService { private currentInteraction: Draw | null; private modifyInteraction: Modify; private selectInteraction: Select; - private infoSelectedFeatureId: string | number | null; private hoveredFeatureId: string | number | null; private history: HistoryState[]; private historyIndex: number; @@ -123,17 +183,15 @@ class MapService { private defaultStyle: Style; private selectedStyle: Style; private drawStyle: Style; - private infoSelectedLineStyle: Style; private busIconStyle: Style; private selectedBusIconStyle: Style; private drawBusIconStyle: Style; - private infoSelectedBusIconStyle: Style; + private sightIconStyle: Style; + private selectedSightIconStyle: Style; private universalHoverStylePoint: Style; private universalHoverStyleLine: Style; // Callbacks - private setCoordinatesPanelContent: (content: string) => void; - private setShowCoordinatesPanel: (show: boolean) => void; private setLoading: (loading: boolean) => void; private setError: (error: string | null) => void; private onModeChangeCallback: (mode: string) => void; @@ -142,8 +200,6 @@ class MapService { constructor( config: MapServiceConfig, - setCoordinatesPanelContent: (content: string) => void, - setShowCoordinatesPanel: (show: boolean) => void, setLoading: (loading: boolean) => void, setError: (error: string | null) => void, onModeChangeCallback: (mode: string) => void, @@ -157,14 +213,11 @@ class MapService { this.mode = null; this.currentDrawingType = null; this.currentInteraction = null; - this.infoSelectedFeatureId = null; this.hoveredFeatureId = null; this.history = []; this.historyIndex = -1; this.beforeModifyState = null; - this.setCoordinatesPanelContent = setCoordinatesPanelContent; - this.setShowCoordinatesPanel = setShowCoordinatesPanel; this.setLoading = setLoading; this.setError = setError; this.onModeChangeCallback = onModeChangeCallback; @@ -196,10 +249,6 @@ class MapService { fill: new Fill({ color: "rgba(34, 197, 94, 0.7)" }), }), }); - this.infoSelectedLineStyle = new Style({ - fill: new Fill({ color: "rgba(255, 0, 0, 0.2)" }), - stroke: new Stroke({ color: "rgba(255, 0, 0, 0.9)", width: 3 }), - }); this.busIconStyle = new Style({ image: new CircleStyle({ radius: 8, @@ -221,10 +270,17 @@ class MapService { stroke: new Stroke({ color: "#ffffff", width: 1.5 }), }), }); - this.infoSelectedBusIconStyle = new Style({ + this.sightIconStyle = new Style({ + image: new CircleStyle({ + radius: 8, + fill: new Fill({ color: "rgba(139, 92, 246, 0.8)" }), // Purple + stroke: new Stroke({ color: "#ffffff", width: 1.5 }), + }), + }); + this.selectedSightIconStyle = new Style({ image: new CircleStyle({ radius: 10, - fill: new Fill({ color: "rgba(255, 0, 0, 0.9)" }), + fill: new Fill({ color: "rgba(221, 107, 32, 0.9)" }), stroke: new Stroke({ color: "#ffffff", width: 2 }), }), }); @@ -234,39 +290,56 @@ class MapService { fill: new Fill({ color: "rgba(255, 165, 0, 0.7)" }), stroke: new Stroke({ color: "rgba(255,255,255,0.8)", width: 2 }), }), + zIndex: Infinity, }); this.universalHoverStyleLine = new Style({ - fill: new Fill({ color: "rgba(255, 165, 0, 0.3)" }), - stroke: new Stroke({ color: "rgba(255, 165, 0, 0.8)", width: 3.5 }), + stroke: new Stroke({ color: "rgba(255, 165, 0, 0.8)", width: 4.5 }), + zIndex: Infinity, }); this.vectorSource = new VectorSource>(); this.vectorLayer = new VectorLayer({ source: this.vectorSource, style: (featureLike: FeatureLike) => { - const feature = featureLike as Feature; // We know our source has Features + const feature = featureLike as Feature; + if (!feature) return this.defaultStyle; + const geometryType = feature.getGeometry()?.getType(); const fId = feature.getId(); + const featureType = feature.get("featureType"); + const isEditSelected = this.selectInteraction ?.getFeatures() .getArray() .includes(feature); - const isInfoSelected = - this.infoSelectedFeatureId === fId && this.mode === "statistics"; const isHovered = this.hoveredFeatureId === fId; if (geometryType === "Point") { - if (isHovered && !isEditSelected && !isInfoSelected) + const defaultPointStyle = + featureType === "sight" ? this.sightIconStyle : this.busIconStyle; + const selectedPointStyle = + featureType === "sight" + ? this.selectedSightIconStyle + : this.selectedBusIconStyle; + + if (isEditSelected) { + return selectedPointStyle; + } + if (isHovered) { return this.universalHoverStylePoint; - if (isInfoSelected) return this.infoSelectedBusIconStyle; - return isEditSelected ? this.selectedBusIconStyle : this.busIconStyle; + } + return defaultPointStyle; } else if (geometryType === "LineString") { - if (isHovered && !isEditSelected && !isInfoSelected) + if (isEditSelected) { + return this.selectedStyle; + } + if (isHovered) { return this.universalHoverStyleLine; - if (isInfoSelected) return this.infoSelectedLineStyle; - return isEditSelected ? this.selectedStyle : this.defaultStyle; + } + return this.defaultStyle; } - return this.defaultStyle; + + return this.defaultStyle; // Fallback }, }); @@ -277,16 +350,12 @@ class MapService { this.vectorSource.on( "addfeature", - this.handleFeatureEvent.bind(this) as ( - event: VectorSourceEvent> - ) => void + this.handleFeatureEvent.bind(this) as any ); this.vectorSource.on("removefeature", () => this.updateFeaturesInReact()); this.vectorSource.on( "changefeature", - this.handleFeatureChange.bind(this) as ( - event: VectorSourceEvent> - ) => void + this.handleFeatureChange.bind(this) as any ); let renderCompleteHandled = false; @@ -335,11 +404,7 @@ class MapService { f.getGeometry()?.getType() === "Point" ? this.selectedBusIconStyle : this.selectedStyle, - deleteCondition: ( - e: MapBrowserEvent - ) => - altKeyOnly(e as MapBrowserEvent) && - primaryAction(e as MapBrowserEvent), + deleteCondition: (e: MapBrowserEvent) => doubleClick(e), }); this.selectInteraction = new Select({ @@ -351,11 +416,9 @@ class MapService { ? this.selectedBusIconStyle : this.selectedStyle; }, - condition: ( - e: MapBrowserEvent - ) => { + condition: (e: MapBrowserEvent) => { const isEdit = this.mode === "edit"; - const isSingle = singleClick(e as MapBrowserEvent); + const isSingle = singleClick(e); if (!isEdit || !isSingle) return false; let clickModify = false; @@ -363,9 +426,7 @@ class MapService { const px = e.pixel; const internalModify = this.modifyInteraction as Modify; const sketchFs: Feature[] | undefined = ( - internalModify as unknown as { - overlay_?: VectorLayer>>; - } + internalModify as any ).overlay_ ?.getSource() ?.getFeatures(); @@ -380,8 +441,7 @@ class MapService { const cppx = this.map.getPixelFromCoordinate(cp); if (!cppx) continue; const pixelTolerance = - (internalModify as unknown as { pixelTolerance_?: number }) - .pixelTolerance_ || 10; + (internalModify as any).pixelTolerance_ || 10; if ( Math.sqrt((px[0] - cppx[0]) ** 2 + (px[1] - cppx[1]) ** 2) < pixelTolerance + 2 @@ -395,13 +455,8 @@ class MapService { } return !clickModify; }, - filter: ( - _: FeatureLike, - l: Layer< - Source, - LayerRenderer>>> - > | null - ) => l === this.vectorLayer, + filter: (_: FeatureLike, l: Layer | null) => + l === this.vectorLayer, }); this.modifyInteraction.on("modifystart", () => { @@ -422,12 +477,6 @@ class MapService { this.beforeModifyState = null; } this.updateFeaturesInReact(); - event.features.forEach((f) => { - if (f instanceof Feature) { - // Ensure f is a Feature instance - this.getFeatureCoordinates(f as Feature); - } - }); }); if (this.map) { @@ -438,22 +487,15 @@ class MapService { this.selectInteraction.on("select", (e: SelectEvent) => { if (this.mode === "edit") { - this.infoSelectedFeatureId = null; - this.vectorLayer.changed(); - const selFs = e.selected as Feature[]; // Selected features are always ol/Feature + const selFs = e.selected as Feature[]; this.modifyInteraction.setActive(selFs.length > 0); if (selFs.length > 0) { - this.getFeatureCoordinates(selFs[0]); this.onFeatureSelect(selFs[0]); } else { - this.hideCoordinatesPanel(); this.onFeatureSelect(null); } - } else if (this.mode === "statistics") { - this.onFeatureSelect(null); // Clear selection when switching to stats mode via click } }); - this.map.on("click", this.handleClick.bind(this) as any); this.map.on("pointermove", this.boundHandlePointerMove as any); const targetEl = this.map.getTargetElement(); if (targetEl instanceof HTMLElement) { @@ -465,6 +507,59 @@ class MapService { } } + public loadFeaturesFromApi( + apiStations: ApiStation[], + apiRoutes: ApiRoute[], + apiSights: ApiSight[] + ): void { + if (!this.map) return; + + const projection = this.map.getView().getProjection(); + const featuresToAdd: Feature[] = []; + + apiStations.forEach((station) => { + const point = new Point( + transform( + [station.longitude, station.latitude], + "EPSG:4326", + projection + ) + ); + const feature = new Feature({ geometry: point, name: station.name }); + feature.setId(`station-${station.id}`); + feature.set("featureType", "station"); + featuresToAdd.push(feature); + }); + + apiRoutes.forEach((route) => { + const coordinates = route.path.map((coord) => + transform(coord, "EPSG:4326", projection) + ); + const line = new LineString(coordinates); + const feature = new Feature({ geometry: line, name: route.route_number }); + feature.setId(`route-${route.id}`); + feature.set("featureType", "route"); + featuresToAdd.push(feature); + }); + + apiSights.forEach((sight) => { + const point = new Point( + transform([sight.longitude, sight.latitude], "EPSG:4326", projection) + ); + const feature = new Feature({ + geometry: point, + name: sight.name, + description: sight.description, + }); + feature.setId(`sight-${sight.id}`); + feature.set("featureType", "sight"); + featuresToAdd.push(feature); + }); + + this.vectorSource.addFeatures(featuresToAdd); + this.updateFeaturesInReact(); + } + private addStateToHistory( actionDescription: string, stateToSave: string @@ -492,7 +587,6 @@ class MapService { this.vectorSource.clear(); this.updateFeaturesInReact(); this.onFeatureSelect(null); - this.hideCoordinatesPanel(); } } @@ -514,9 +608,7 @@ class MapService { if (features.length > 0) this.vectorSource.addFeatures(features); this.updateFeaturesInReact(); this.onFeatureSelect(null); - this.hideCoordinatesPanel(); this.selectInteraction.getFeatures().clear(); - this.infoSelectedFeatureId = null; this.vectorLayer.changed(); } @@ -552,14 +644,6 @@ class MapService { this.selectInteraction?.getFeatures().getLength() > 0 ) { this.selectInteraction.getFeatures().clear(); - // Firing an event for the selection change might be good here - // So that the UI (like coordinates panel) updates - this.onFeatureSelect(null); - this.hideCoordinatesPanel(); - } else if (this.mode === "statistics" && this.infoSelectedFeatureId) { - this.infoSelectedFeatureId = null; - this.vectorLayer.changed(); - this.hideCoordinatesPanel(); this.onFeatureSelect(null); } } @@ -576,11 +660,6 @@ class MapService { if (this.tooltipOverlay) this.tooltipOverlay.setPosition(undefined); } - if (newMode !== "statistics" && this.infoSelectedFeatureId) { - this.infoSelectedFeatureId = null; - this.vectorLayer.changed(); - } - if (this.onModeChangeCallback) this.onModeChangeCallback(newMode); if (this.currentInteraction instanceof Draw) { @@ -595,16 +674,6 @@ class MapService { this.selectInteraction.setActive(false); this.modifyInteraction.setActive(false); } - - const isEditWithSelection = - newMode === "edit" && - this.selectInteraction?.getFeatures().getLength() > 0; - const isStatsWithSelection = - newMode === "statistics" && this.infoSelectedFeatureId; - - if (!isEditWithSelection && !isStatsWithSelection) { - this.hideCoordinatesPanel(); - } } public activateEditMode(): void { @@ -614,25 +683,9 @@ class MapService { const firstSelectedFeature = this.selectInteraction .getFeatures() .item(0) as Feature; - this.getFeatureCoordinates(firstSelectedFeature); + this.onFeatureSelect(firstSelectedFeature); } else { - this.hideCoordinatesPanel(); - } - } - - public activateStatisticsMode(): void { - this.currentDrawingType = null; - this.setMode("statistics"); - if (this.selectInteraction?.getFeatures().getLength() > 0) { - this.selectInteraction.getFeatures().clear(); - } - if (this.infoSelectedFeatureId) { - const feature = this.vectorSource.getFeatureById( - this.infoSelectedFeatureId - ); - if (feature) this.getFeatureCoordinates(feature); - } else { - this.hideCoordinatesPanel(); + this.onFeatureSelect(null); } } @@ -661,7 +714,7 @@ class MapService { if (stateBeforeDraw) { this.addStateToHistory("draw-before", stateBeforeDraw); } - const feature = event.feature as Feature; // DrawEvent feature is always an ol/Feature + const feature = event.feature as Feature; const geometry = feature.getGeometry(); if (!geometry) return; const geometryType = geometry.getType(); @@ -671,9 +724,11 @@ class MapService { if (geometryType === "Point") { baseName = "Станция"; namePrefix = "Станция "; + feature.set("featureType", "station"); } else if (geometryType === "LineString") { baseName = "Маршрут"; namePrefix = "Маршрут "; + feature.set("featureType", "route"); } else { baseName = "Объект"; namePrefix = "Объект "; @@ -695,17 +750,19 @@ class MapService { if (name) { const numStr = name.substring(namePrefix.length); const num = parseInt(numStr, 10); - if (!isNaN(num) && num > maxNumber) { - maxNumber = num; - } + if (!isNaN(num) && num > maxNumber) maxNumber = num; } }); const newNumber = maxNumber + 1; feature.set("name", `${baseName} ${newNumber}`); - feature.setStyle( - type === "Point" ? this.busIconStyle : this.defaultStyle - ); + + // DO NOT set style directly on the feature, so it uses the layer's style function + // which handles hover effects. + // feature.setStyle( + // type === "Point" ? this.busIconStyle : this.defaultStyle + // ); + if (type === "LineString") this.finishDrawing(); }); this.map.addInteraction(this.currentInteraction); @@ -714,26 +771,18 @@ class MapService { public startDrawingMarker(): void { this.startDrawing("Point"); } - public startDrawingLine(): void { this.startDrawing("LineString"); } public finishDrawing(): void { if (!this.map || !this.currentInteraction) return; - const drawInteraction = this.currentInteraction as Draw; - if ( - (drawInteraction as unknown as { sketchFeature_?: Feature }) - .sketchFeature_ - ) { + if ((drawInteraction as any).sketchFeature_) { try { - // This can throw if not enough points for geometry (e.g. LineString) this.currentInteraction.finishDrawing(); } catch (e) { - // console.warn("Could not finish drawing programmatically:", e); - // If finishDrawing fails (e.g. LineString with one point), - // we still want to remove interaction and reset mode. + // Drawing could not be finished (e.g., LineString with 1 point) } } this.map.removeInteraction(this.currentInteraction); @@ -754,17 +803,15 @@ class MapService { ) { return; } - const drawInteraction = this.currentInteraction as Draw; if ( this.currentDrawingType === "LineString" && - (drawInteraction as unknown as { sketchFeature_?: Feature }) - .sketchFeature_ + (drawInteraction as any).sketchFeature_ ) { try { this.currentInteraction.finishDrawing(); } catch (e) { - this.finishDrawing(); // Fallback to ensure cleanup + this.finishDrawing(); } } else { this.finishDrawing(); @@ -785,25 +832,20 @@ class MapService { const featureAtPixel: Feature | undefined = this.map.forEachFeatureAtPixel( pixel, - (f: FeatureLike) => f as Feature, // Cast is okay due to layerFilter - { - layerFilter: (l) => l === this.vectorLayer, - hitTolerance: 5, - } + (f: FeatureLike) => f as Feature, + { layerFilter: (l) => l === this.vectorLayer, hitTolerance: 5 } ); - const newHoveredFeatureId = featureAtPixel - ? featureAtPixel.getId() - : (null as string | number | null); + const newHoveredFeatureId = featureAtPixel ? featureAtPixel.getId() : null; if (this.tooltipOverlay && this.tooltipElement) { + const featureType = featureAtPixel?.get("featureType"); if ( - this.mode === "statistics" && featureAtPixel && - featureAtPixel.getGeometry()?.getType() === "Point" + (featureType === "station" || featureType === "sight") ) { this.tooltipElement.innerHTML = - (featureAtPixel.get("name") as string) || "Станция"; + (featureAtPixel.get("name") as string) || "Объект"; this.tooltipOverlay.setPosition(event.coordinate); } else { this.tooltipOverlay.setPosition(undefined); @@ -816,99 +858,25 @@ class MapService { } } - private handleClick(event: MapBrowserEvent): void { - if (!this.map || (this.mode && this.mode.startsWith("drawing-"))) return; - - const featureAtPixel: Feature | undefined = - this.map.forEachFeatureAtPixel( - event.pixel, - (f: FeatureLike) => f as Feature, - { - layerFilter: (l) => l === this.vectorLayer, - hitTolerance: 2, // Lower tolerance for click - } - ); - - if (this.mode === "statistics") { - this.selectInteraction.getFeatures().clear(); // Ensure no edit selection in stats mode - if (featureAtPixel) { - const featureId = featureAtPixel.getId(); - if (this.infoSelectedFeatureId !== featureId) { - this.infoSelectedFeatureId = featureId as string | number | null; - this.getFeatureCoordinates(featureAtPixel); - this.onFeatureSelect(featureAtPixel); - } else { - // Clicked same feature again, deselect - this.infoSelectedFeatureId = null; - this.hideCoordinatesPanel(); - this.onFeatureSelect(null); - } - } else { - // Clicked on map, not on a feature - this.infoSelectedFeatureId = null; - this.hideCoordinatesPanel(); - this.onFeatureSelect(null); - } - this.vectorLayer.changed(); // Re-render for style changes - } else if (this.mode === "edit") { - // Selection in edit mode is handled by the Select interaction's 'select' event - // This click handler can ensure that if a click happens outside any feature - // (and doesn't trigger a new selection by the Select interaction), - // any info selection from a previous mode is cleared. - if (this.infoSelectedFeatureId) { - this.infoSelectedFeatureId = null; - this.vectorLayer.changed(); - } - // If click was on a feature, Select interaction handles it. - // If click was on map, Select interaction's 'select' event with e.deselected might clear panel. - // If no feature is selected after this click, the coordinates panel should be hidden by Select's logic. - if ( - !featureAtPixel && - this.selectInteraction.getFeatures().getLength() === 0 - ) { - this.hideCoordinatesPanel(); - this.onFeatureSelect(null); - } - } - } - public selectFeature(featureId: string | number | undefined): void { if (!this.map || featureId === undefined) { this.onFeatureSelect(null); if (this.mode === "edit") this.selectInteraction.getFeatures().clear(); - if (this.mode === "statistics" && this.infoSelectedFeatureId) { - this.infoSelectedFeatureId = null; - this.vectorLayer.changed(); - } - this.hideCoordinatesPanel(); + this.vectorLayer.changed(); return; } const feature = this.vectorSource.getFeatureById(featureId); - if (!feature) { this.onFeatureSelect(null); if (this.mode === "edit") this.selectInteraction.getFeatures().clear(); - if ( - this.mode === "statistics" && - this.infoSelectedFeatureId === featureId - ) { - this.infoSelectedFeatureId = null; - } - this.hideCoordinatesPanel(); this.vectorLayer.changed(); return; } if (this.mode === "edit") { - this.infoSelectedFeatureId = null; // Clear any info selection this.selectInteraction.getFeatures().clear(); this.selectInteraction.getFeatures().push(feature); - // The 'select' event on selectInteraction should handle getFeatureCoordinates and onFeatureSelect - } else if (this.mode === "statistics") { - this.selectInteraction.getFeatures().clear(); // Clear any edit selection - this.infoSelectedFeatureId = featureId; - this.getFeatureCoordinates(feature); this.onFeatureSelect(feature); } this.vectorLayer.changed(); @@ -916,10 +884,9 @@ class MapService { const view = this.map.getView(); const geometry = feature.getGeometry(); if (geometry) { - if (geometry.getType() === "Point") { - const pointGeom = geometry as Point; + if (geometry instanceof Point) { view.animate({ - center: pointGeom.getCoordinates(), + center: geometry.getCoordinates(), duration: 500, zoom: Math.max(view.getZoom() || mapConfig.zoom, 14), }); @@ -938,121 +905,32 @@ class MapService { const feature = this.vectorSource.getFeatureById(featureId); if (feature) { const currentState = this.getCurrentStateAsGeoJSON(); - if (currentState) { - this.addStateToHistory("delete", currentState); - } - - if (this.infoSelectedFeatureId === featureId) { - this.infoSelectedFeatureId = null; - this.onFeatureSelect(null); // Notify React that selection changed - } + if (currentState) this.addStateToHistory("delete", currentState); const selectedFeaturesCollection = this.selectInteraction?.getFeatures(); if (selectedFeaturesCollection?.getArray().includes(feature)) { selectedFeaturesCollection.clear(); - // This will trigger selectInteraction's 'select' event with deselected, - // which should handle UI updates like hiding coordinates panel. - this.onFeatureSelect(null); // Explicitly notify too + this.onFeatureSelect(null); } - this.vectorSource.removeFeature(feature); // This will trigger 'removefeature' event - - // If after deletion, no feature is selected in any mode, hide panel - if ( - !this.infoSelectedFeatureId && - selectedFeaturesCollection?.getLength() === 0 - ) { - this.hideCoordinatesPanel(); - } - this.vectorLayer.changed(); // Ensure map re-renders + this.vectorSource.removeFeature(feature); + this.vectorLayer.changed(); } } - private getFeatureCoordinates(feature: Feature): void { - if (!feature || !feature.getGeometry()) { - this.hideCoordinatesPanel(); - return; - } - const geometry = feature.getGeometry(); - if (!geometry) { - this.hideCoordinatesPanel(); - return; - } - - const toGeoCoords = (c: number[]) => transform(c, "EPSG:3857", "EPSG:4326"); - const fmt = new Intl.NumberFormat("ru-RU", { - minimumFractionDigits: 5, - maximumFractionDigits: 5, - }); - let txt = ""; - const fType = geometry.getType(); - const fName = - (feature.get("name") as string) || - (fType === "Point" - ? "Станция" - : fType === "LineString" - ? "Маршрут" - : "Объект"); - - let nameClr = "text-blue-600"; - if ( - this.mode === "statistics" && - this.infoSelectedFeatureId === feature.getId() - ) { - nameClr = "text-red-600"; - } else if ( - this.mode === "edit" && - this.selectInteraction?.getFeatures().getArray().includes(feature) - ) { - nameClr = "text-orange-600"; - } - txt += `${fName}:
`; - - if (geometry instanceof Point) { - const crds = geometry.getCoordinates(); - const [lon, lat] = toGeoCoords(crds); - txt += `Шир: ${fmt.format(lat)}, Дол: ${fmt.format(lon)}`; - } else if (geometry instanceof LineString) { - const crdsArr = geometry.getCoordinates(); - txt += crdsArr - .map((c, i) => { - const [lon, lat] = toGeoCoords(c); - return `Т${i + 1}: Ш: ${fmt.format( - lat - )}, Д: ${fmt.format(lon)}`; - }) - .join("
"); - const lenM = geometry.getLength(); - txt += `
Длина: ${lenM.toFixed(2)} м`; - } else { - txt += "Координаты недоступны."; - } - - this.setCoordinatesPanelContent(txt); - this.setShowCoordinatesPanel(true); - } - - private hideCoordinatesPanel(): void { - this.setCoordinatesPanelContent(""); - this.setShowCoordinatesPanel(false); - } - public getAllFeaturesAsGeoJSON(): string | null { if (!this.vectorSource || !this.map) return null; const feats = this.vectorSource.getFeatures(); if (feats.length === 0) return null; const geoJSONFmt = new GeoJSON(); - // Clone features to avoid transforming original geometries const featsExp = feats.map((f) => { const cF = f.clone(); - // Make sure to copy properties and ID - cF.setProperties(f.getProperties(), true); // true to suppress change event + cF.setProperties(f.getProperties(), true); cF.setId(f.getId()); const geom = cF.getGeometry(); - if (geom) { + if (geom) geom.transform(this.map!.getView().getProjection(), "EPSG:4326"); - } return cF; }); return geoJSONFmt.writeFeatures(featsExp); @@ -1075,7 +953,7 @@ class MapService { this.map.un("pointermove", this.boundHandlePointerMove as any); if (this.tooltipOverlay) this.map.removeOverlay(this.tooltipOverlay); this.vectorSource.clear(); - this.map.setTarget(undefined); // Use undefined instead of null + this.map.setTarget(undefined); this.map = null; } } @@ -1095,21 +973,7 @@ class MapService { event: VectorSourceEvent> ): void { if (!event.feature) return; - const feature = event.feature; this.updateFeaturesInReact(); - - const selectedInEdit = this.selectInteraction - ?.getFeatures() - .getArray() - .includes(feature); - const selectedInInfo = this.infoSelectedFeatureId === feature.getId(); - - if ( - (this.mode === "edit" && selectedInEdit) || - (this.mode === "statistics" && selectedInInfo) - ) { - this.getFeatureCoordinates(feature); - } } } @@ -1133,13 +997,6 @@ const MapControls: React.FC = ({ icon: , action: () => mapService.activateEditMode(), }, - { - mode: "statistics", - title: "Информация", - longTitle: "Информация", - icon: , - action: () => mapService.activateStatisticsMode(), - }, { mode: "drawing-point", title: "Станция", @@ -1156,7 +1013,7 @@ const MapControls: React.FC = ({ }, ]; return ( -
+
{controls.map((c) => (
), }, + { + id: "sights", + title: `Достопримечательности (${sights.length})`, + icon: , + content: ( +
+ {sights.length > 0 ? ( + sights.map((s) => { + const sId = s.getId(); + const sName = (s.get("name") as string) || "Без названия"; + const isSelected = selectedFeature?.getId() === sId; + return ( +
handleFeatureClick(sId)} + > +
+ + + {sName} + +
+ +
+ ); + }) + ) : ( +

+ Нет данных о достопримечательностях. +

+ )} +
+ ), + }, ]; if (!sections.length && activeSection) { @@ -1437,7 +1334,7 @@ const MapSightbar: React.FC = ({
@@ -1458,43 +1355,32 @@ export const MapPage: React.FC = () => { const tooltipRef = useRef(null); const [mapServiceInstance, setMapServiceInstance] = useState(null); - const [coordinatesPanelContent, setCoordinatesPanelContent] = - useState(""); - const [showCoordinatesPanel, setShowCoordinatesPanel] = - useState(false); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [currentMapMode, setCurrentMapMode] = useState("edit"); // Default mode + const [currentMapMode, setCurrentMapMode] = useState("edit"); const [mapFeatures, setMapFeatures] = useState[]>([]); const [selectedFeatureForSidebar, setSelectedFeatureForSidebar] = useState | null>(null); const handleFeaturesChange = useCallback( - (feats: Feature[]) => setMapFeatures([...feats]), // Create new array instance + (feats: Feature[]) => setMapFeatures([...feats]), [] ); - const handleFeatureSelectForSidebar = useCallback( (feat: Feature | null) => { setSelectedFeatureForSidebar(feat); - if (!feat && showCoordinatesPanel) { - // Check showCoordinatesPanel state before calling setter - setShowCoordinatesPanel(false); - } }, - [showCoordinatesPanel] // Dependency on showCoordinatesPanel as it's read + [] ); useEffect(() => { - let service: MapService | null = null; // Initialize to null + let service: MapService | null = null; if (mapRef.current && tooltipRef.current && !mapServiceInstance) { setIsLoading(true); setError(null); try { service = new MapService( { ...mapConfig, target: mapRef.current }, - setCoordinatesPanelContent, - setShowCoordinatesPanel, setIsLoading, setError, setCurrentMapMode, @@ -1503,6 +1389,25 @@ export const MapPage: React.FC = () => { tooltipRef.current ); setMapServiceInstance(service); + + const loadInitialData = async (mapService: MapService) => { + console.log("Starting data load..."); + setIsLoading(true); + try { + // Замените mockApi на реальные fetch запросы к вашему API + const [routes, stations, sights] = await Promise.all([ + mockApi.getRoutes(), + mockApi.getStations(), + mockApi.getSights(), + ]); + mapService.loadFeaturesFromApi(stations, routes, sights); + } catch (e) { + console.error("Failed to load initial map data:", e); + setError("Не удалось загрузить данные для карты."); + } + }; + + loadInitialData(service); } catch (e: any) { console.error("MapPage useEffect error:", e); setError( @@ -1516,14 +1421,14 @@ export const MapPage: React.FC = () => { return () => { if (service) { service.destroy(); - setMapServiceInstance(null); // Ensure instance is cleared on unmount + setMapServiceInstance(null); } }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // Added stable useCallback refs + }, []); return ( -
+
{ style={{ position: "absolute", pointerEvents: "none", - // Ensure it's hidden initially if empty or not positioned by OL visibility: "hidden", }} >
- {isLoading && (
@@ -1568,25 +1471,12 @@ export const MapPage: React.FC = () => { activeMode={currentMapMode} /> )} -
-
-
{mapServiceInstance && !isLoading && !error && ( )}
diff --git a/src/shared/modals/UploadMediaDialog/index.tsx b/src/shared/modals/UploadMediaDialog/index.tsx index aaa2978..b5678ab 100644 --- a/src/shared/modals/UploadMediaDialog/index.tsx +++ b/src/shared/modals/UploadMediaDialog/index.tsx @@ -106,7 +106,9 @@ export const UploadMediaDialog = observer( try { const media = await uploadMedia( mediaFilename, - mediaType, + hardcodeType + ? (MEDIA_TYPE_VALUES[hardcodeType] as number) + : mediaType, mediaFile, mediaName ); diff --git a/src/shared/store/CityStore/index.ts b/src/shared/store/CityStore/index.ts index 8e57b6f..a83a950 100644 --- a/src/shared/store/CityStore/index.ts +++ b/src/shared/store/CityStore/index.ts @@ -40,6 +40,16 @@ class CityStore { makeAutoObservable(this); } + ruCities: City[] = []; + + getRuCities = async () => { + const response = await languageInstance("ru").get(`/city`); + + runInAction(() => { + this.ruCities = response.data; + }); + }; + getCities = async (language: keyof CashedCities) => { if (this.cities[language] && this.cities[language].length > 0) { return;