mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-01-31 19:16:08 +00:00
Compare commits
5 Commits
routes-by
...
auto-layou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
294e718b4f | ||
|
|
527c927bb8 | ||
|
|
fd04f64634 | ||
|
|
34ae21b7c3 | ||
|
|
797cda2577 |
@@ -8,15 +8,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ContextMenu {
|
||||
width: max-content;
|
||||
min-width: unset;
|
||||
|
||||
:global {
|
||||
.p-submenu-list {
|
||||
width: max-content;
|
||||
min-width: unset !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import React, { RefObject, useCallback, useMemo } from 'react';
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { CharacterTypeRaw, SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import classes from './ContextMenuSystemInfo.module.scss';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
|
||||
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { Route, RouteStationSummary } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { Route } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { MapAddIcon, MapDeleteIcon } from '@/hooks/Mapper/icons';
|
||||
import { useRouteProvider } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
|
||||
import { useGetOwnOnlineCharacters } from '@/hooks/Mapper/components/hooks/useGetOwnOnlineCharacters.ts';
|
||||
|
||||
export interface ContextMenuSystemInfoProps {
|
||||
systemStatics: Map<number, SolarSystemStaticInfoRaw>;
|
||||
hubs: string[];
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
systemId: string | undefined;
|
||||
systemIdFrom?: string | undefined;
|
||||
@@ -38,104 +37,11 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
onWaypointSet,
|
||||
systemId,
|
||||
systemIdFrom,
|
||||
hubs,
|
||||
routes,
|
||||
}) => {
|
||||
const getWaypointMenu = useWaypointMenu(onWaypointSet);
|
||||
const getJumpPlannerMenu = useJumpPlannerMenu(systems, systemIdFrom);
|
||||
const { toggleHubCommand, hubs } = useRouteProvider();
|
||||
const getOwnOnlineCharacters = useGetOwnOnlineCharacters();
|
||||
|
||||
const getStationWaypointItems = useCallback(
|
||||
(destinationId: string, chars: CharacterTypeRaw[]): MenuItem[] => [
|
||||
{
|
||||
label: 'Set Destination',
|
||||
icon: PrimeIcons.SEND,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: true,
|
||||
clearWay: true,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Add Waypoint',
|
||||
icon: PrimeIcons.DIRECTIONS_ALT,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: false,
|
||||
clearWay: false,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Add Waypoint Front',
|
||||
icon: PrimeIcons.DIRECTIONS,
|
||||
command: () => {
|
||||
onWaypointSet({
|
||||
fromBeginning: true,
|
||||
clearWay: false,
|
||||
destination: destinationId,
|
||||
charIds: chars.map(char => char.eve_id),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
[onWaypointSet],
|
||||
);
|
||||
|
||||
const getStationsMenu = useCallback(
|
||||
(stations: RouteStationSummary[]) => {
|
||||
const chars = getOwnOnlineCharacters().filter(x => x.online);
|
||||
if (chars.length === 0) {
|
||||
return [
|
||||
{
|
||||
label: 'Stations',
|
||||
icon: PrimeIcons.MAP_MARKER,
|
||||
items: [{ label: 'No online characters', disabled: true }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Stations',
|
||||
icon: PrimeIcons.MAP_MARKER,
|
||||
items: stations.map(station => {
|
||||
const destinationId = station.station_id.toString();
|
||||
|
||||
if (chars.length === 1) {
|
||||
return {
|
||||
label: station.station_name,
|
||||
items: getStationWaypointItems(destinationId, chars.slice(0, 1)),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: station.station_name,
|
||||
className: 'w-[500px]',
|
||||
items: [
|
||||
{
|
||||
label: 'All',
|
||||
icon: PrimeIcons.USERS,
|
||||
items: getStationWaypointItems(destinationId, chars),
|
||||
},
|
||||
...chars.map(char => ({
|
||||
label: char.name,
|
||||
icon: PrimeIcons.USER,
|
||||
items: getStationWaypointItems(destinationId, [char]),
|
||||
})),
|
||||
],
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
[getOwnOnlineCharacters, getStationWaypointItems],
|
||||
);
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined;
|
||||
@@ -144,10 +50,6 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
if (!systemId || !system) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const route = routes.find(x => x.destination?.toString() === systemId);
|
||||
const stationItems = route?.stations?.length ? getStationsMenu(route.stations) : [];
|
||||
|
||||
return [
|
||||
{
|
||||
className: classes.FastActions,
|
||||
@@ -167,20 +69,15 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
{ separator: true },
|
||||
...getJumpPlannerMenu(system, routes),
|
||||
...getWaypointMenu(systemId, system.system_class),
|
||||
...stationItems,
|
||||
...(toggleHubCommand
|
||||
? [
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onHubToggle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add Route' : 'Remove Route',
|
||||
icon: !hubs.includes(systemId) ? (
|
||||
<MapAddIcon className="mr-1 relative left-[-2px]" />
|
||||
) : (
|
||||
<MapDeleteIcon className="mr-1 relative left-[-2px]" />
|
||||
),
|
||||
command: onHubToggle,
|
||||
},
|
||||
...(!systemOnMap
|
||||
? [
|
||||
{
|
||||
@@ -197,18 +94,15 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
systems,
|
||||
getJumpPlannerMenu,
|
||||
getWaypointMenu,
|
||||
getStationsMenu,
|
||||
hubs,
|
||||
onHubToggle,
|
||||
onAddSystem,
|
||||
onOpenSettings,
|
||||
toggleHubCommand,
|
||||
routes,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu className={classes.ContextMenu} model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
<ContextMenu model={items} ref={contextMenuRef} breakpoint="767px" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useContextMenuSystemInfoHandlers = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current.toggleHubCommand?.(system);
|
||||
ref.current.toggleHubCommand(system);
|
||||
setSystem(undefined);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -281,9 +281,9 @@ const MapComp = ({
|
||||
deleteKeyCode={['']}
|
||||
{...(isPanAndDrag
|
||||
? {
|
||||
selectionOnDrag: true,
|
||||
panOnDrag: [2],
|
||||
}
|
||||
selectionOnDrag: true,
|
||||
panOnDrag: [2],
|
||||
}
|
||||
: {})}
|
||||
// TODO need create clear example with problem with that flag
|
||||
// if system is not visible edge not drawing (and any render in Custom node is not happening)
|
||||
|
||||
@@ -13,12 +13,14 @@ export interface ContextMenuRootProps {
|
||||
pasteSystemsAndConnections: PasteSystemsAndConnections | undefined;
|
||||
onAddSystem(): void;
|
||||
onPasteSystemsAnsConnections(): void;
|
||||
onAutoLayout(): void;
|
||||
}
|
||||
|
||||
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
|
||||
contextMenuRef,
|
||||
onAddSystem,
|
||||
onPasteSystemsAnsConnections,
|
||||
onAutoLayout,
|
||||
pasteSystemsAndConnections,
|
||||
}) => {
|
||||
const {
|
||||
@@ -34,35 +36,40 @@ export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
|
||||
icon: PrimeIcons.PLUS,
|
||||
command: onAddSystem,
|
||||
},
|
||||
{
|
||||
label: 'Auto Layout',
|
||||
icon: PrimeIcons.REFRESH,
|
||||
command: onAutoLayout,
|
||||
},
|
||||
...(pasteSystemsAndConnections != null
|
||||
? [
|
||||
{
|
||||
icon: 'pi pi-clipboard',
|
||||
disabled: !allowPaste,
|
||||
command: onPasteSystemsAnsConnections,
|
||||
template: () => {
|
||||
if (allowPaste) {
|
||||
return (
|
||||
<WdMenuItem icon="pi pi-clipboard">
|
||||
Paste
|
||||
</WdMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
icon: 'pi pi-clipboard',
|
||||
disabled: !allowPaste,
|
||||
command: onPasteSystemsAnsConnections,
|
||||
template: () => {
|
||||
if (allowPaste) {
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Action is blocked because you don’t have permission to Paste."
|
||||
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
|
||||
tooltipWrapperClassName="flex"
|
||||
>
|
||||
<WdMenuItem disabled icon="pi pi-clipboard">
|
||||
Paste
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
<WdMenuItem icon="pi pi-clipboard">
|
||||
Paste
|
||||
</WdMenuItem>
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Action is blocked because you don’t have permission to Paste."
|
||||
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
|
||||
tooltipWrapperClassName="flex"
|
||||
>
|
||||
<WdMenuItem disabled icon="pi pi-clipboard">
|
||||
Paste
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
);
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}, [userPermissions, options, onAddSystem, pasteSystemsAndConnections, onPasteSystemsAnsConnections]);
|
||||
|
||||
@@ -46,6 +46,22 @@ export const useContextMenuRootHandlers = ({ onAddSystem, onCommand }: UseContex
|
||||
ref.current.onAddSystem?.({ coordinates: position });
|
||||
}, [position]);
|
||||
|
||||
const onAutoLayout = useCallback(async () => {
|
||||
const { onCommand } = ref.current;
|
||||
if (!onCommand) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Auto layouting systems');
|
||||
|
||||
await onCommand({
|
||||
type: OutCommand.layoutSystems,
|
||||
data: {
|
||||
system_ids: [], // Layout all systems
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onPasteSystemsAnsConnections = useCallback(async () => {
|
||||
const { pasteSystemsAndConnections, onCommand, position } = ref.current;
|
||||
if (!position || !onCommand || !pasteSystemsAndConnections) {
|
||||
@@ -72,5 +88,6 @@ export const useContextMenuRootHandlers = ({ onAddSystem, onCommand }: UseContex
|
||||
contextMenuRef,
|
||||
onAddSystem: onAddSystemCallback,
|
||||
onPasteSystemsAnsConnections,
|
||||
onAutoLayout,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
stroke-dasharray: 10 5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
&.CrossList {
|
||||
stroke: #ff0000;
|
||||
}
|
||||
}
|
||||
|
||||
.EdgePathFront {
|
||||
@@ -93,6 +97,10 @@
|
||||
stroke-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.CrossList {
|
||||
stroke: #ff0000;
|
||||
}
|
||||
}
|
||||
|
||||
.ClickPath {
|
||||
|
||||
@@ -85,6 +85,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
[classes.Hovered]: hovered,
|
||||
[classes.Gate]: isGate,
|
||||
[classes.Bridge]: isBridge,
|
||||
[classes.CrossList]: data.is_cross_list,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
@@ -100,6 +101,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
|
||||
[classes.Gate]: isGate,
|
||||
[classes.Bridge]: isBridge,
|
||||
[classes.CrossList]: data.is_cross_list,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
|
||||
@@ -17,8 +17,15 @@ export const useCommandsConnections = () => {
|
||||
}, []);
|
||||
|
||||
const updateConnection = useCallback((value: CommandUpdateConnection) => {
|
||||
ref.current.rf.deleteElements({ edges: [value] });
|
||||
ref.current.rf.addEdges([convertConnection2Edge(value)]);
|
||||
const newEdge = convertConnection2Edge(value);
|
||||
ref.current.rf.setEdges(eds => {
|
||||
const exists = eds.find(e => e.id === newEdge.id);
|
||||
if (exists) {
|
||||
return eds.map(e => e.id === newEdge.id ? newEdge : e);
|
||||
} else {
|
||||
return [...eds, newEdge];
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { addConnections, removeConnections, updateConnection };
|
||||
|
||||
@@ -62,7 +62,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
|
||||
break;
|
||||
case Commands.updateSystems:
|
||||
mapUpdateSystems(data as CommandUpdateSystems);
|
||||
setTimeout(() => mapUpdateSystems(data as CommandUpdateSystems), 100);
|
||||
break;
|
||||
case Commands.removeSystems:
|
||||
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
|
||||
@@ -89,7 +89,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.updateConnection:
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
setTimeout(() => updateConnection(data as CommandUpdateConnection), 100);
|
||||
break;
|
||||
case Commands.mapUpdated:
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
SystemStructures,
|
||||
WRoutesPublic,
|
||||
WRoutesUser,
|
||||
WRoutesBy,
|
||||
WSystemKills,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
|
||||
@@ -19,7 +18,6 @@ export enum WidgetsIds {
|
||||
signatures = 'signatures',
|
||||
local = 'local',
|
||||
routes = 'routes',
|
||||
routesBy = 'routesBy',
|
||||
structures = 'structures',
|
||||
kills = 'kills',
|
||||
comments = 'comments',
|
||||
@@ -62,13 +60,6 @@ export const DEFAULT_WIDGETS: WindowProps[] = [
|
||||
zIndex: 0,
|
||||
content: () => <WRoutesPublic />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.routesBy,
|
||||
position: { x: 10, y: 740 },
|
||||
size: { width: 510, height: 200 },
|
||||
zIndex: 0,
|
||||
content: () => <WRoutesBy />,
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.userRoutes,
|
||||
position: { x: 10, y: 10 },
|
||||
@@ -121,10 +112,6 @@ export const WIDGETS_CHECKBOXES_PROPS: WidgetsCheckboxesType = [
|
||||
id: WidgetsIds.routes,
|
||||
label: 'Routes',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.routesBy,
|
||||
label: 'Routes By',
|
||||
},
|
||||
{
|
||||
id: WidgetsIds.userRoutes,
|
||||
label: 'User Routes',
|
||||
|
||||
@@ -86,13 +86,6 @@ export const RoutesWidgetContent = () => {
|
||||
[handleClick],
|
||||
);
|
||||
|
||||
|
||||
// useEffect(() => {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log('JOipP', `loading`, loading);
|
||||
// }, [loading]);
|
||||
|
||||
|
||||
if (isRestricted && !isSubscriptionActive) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
@@ -115,7 +108,6 @@ export const RoutesWidgetContent = () => {
|
||||
return <div className="w-full h-full flex justify-center items-center select-none">Routes not set</div>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingWrapper loading={loading}>
|
||||
@@ -137,6 +129,7 @@ export const RoutesWidgetContent = () => {
|
||||
offset: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
<SystemView
|
||||
systemId={route.destination.toString()}
|
||||
className={clsx('select-none text-center cursor-context-menu')}
|
||||
@@ -145,7 +138,7 @@ export const RoutesWidgetContent = () => {
|
||||
showCustomName
|
||||
/>
|
||||
</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? (route.systems?.length ?? 2) : ''}</div>
|
||||
<div className="text-right pl-1">{route.has_connection ? route.systems?.length ?? 2 : ''}</div>
|
||||
<div className="pl-2 pb-0.5">
|
||||
<RoutesList data={route} onContextMenu={handleContextMenu} />
|
||||
</div>
|
||||
@@ -154,7 +147,9 @@ export const RoutesWidgetContent = () => {
|
||||
})}
|
||||
</div>
|
||||
</LoadingWrapper>
|
||||
|
||||
<ContextMenuSystemInfo
|
||||
hubs={hubs}
|
||||
routes={preparedRoutes}
|
||||
systems={systems}
|
||||
systemStatics={systemStatics}
|
||||
@@ -167,10 +162,9 @@ export const RoutesWidgetContent = () => {
|
||||
|
||||
type RoutesWidgetCompProps = {
|
||||
title: ReactNode | string;
|
||||
renderContent?: (content: ReactNode, compact: boolean) => ReactNode;
|
||||
};
|
||||
|
||||
export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps) => {
|
||||
export const RoutesWidgetComp = ({ title }: RoutesWidgetCompProps) => {
|
||||
const [routeSettingsVisible, setRouteSettingsVisible] = useState(false);
|
||||
const { data, update, addHubCommand } = useRouteProvider();
|
||||
|
||||
@@ -189,7 +183,7 @@ export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps
|
||||
const onAddSystem = useCallback(() => setOpenAddSystem(true), []);
|
||||
|
||||
const handleSubmitAddSystem: SearchOnSubmitCallback = useCallback(
|
||||
async item => addHubCommand?.(item.value.toString()),
|
||||
async item => addHubCommand(item.value.toString()),
|
||||
[addHubCommand],
|
||||
);
|
||||
|
||||
@@ -197,17 +191,15 @@ export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps
|
||||
<Widget
|
||||
label={
|
||||
<div className="flex justify-between items-center text-xs w-full" ref={ref}>
|
||||
<div className="select-none flex items-center gap-2">{title}</div>
|
||||
<span className="select-none">{title}</span>
|
||||
<LayoutEventBlocker className="flex items-center gap-2">
|
||||
{addHubCommand && (
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={onAddSystem}
|
||||
tooltip={{
|
||||
content: 'Click here to add new system to routes',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<WdImgButton
|
||||
className={PrimeIcons.PLUS_CIRCLE}
|
||||
onClick={onAddSystem}
|
||||
tooltip={{
|
||||
content: 'Click here to add new system to routes',
|
||||
}}
|
||||
/>
|
||||
|
||||
<WdTooltipWrapper content="Show shortest route" position={TooltipPosition.top}>
|
||||
<WdCheckbox
|
||||
@@ -231,38 +223,24 @@ export const RoutesWidgetComp = ({ title, renderContent }: RoutesWidgetCompProps
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{renderContent ? (
|
||||
renderContent(
|
||||
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
|
||||
<RoutesWidgetContent />
|
||||
</div>,
|
||||
compact,
|
||||
)
|
||||
) : (
|
||||
<div className="h-full overflow-auto bg-opacity-5 custom-scrollbar">
|
||||
<RoutesWidgetContent />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<RoutesWidgetContent />
|
||||
<RoutesSettingsDialog visible={routeSettingsVisible} setVisible={setRouteSettingsVisible} />
|
||||
|
||||
{addHubCommand && (
|
||||
<AddSystemDialog
|
||||
title="Add system to routes"
|
||||
visible={openAddSystem}
|
||||
setVisible={() => setOpenAddSystem(false)}
|
||||
onSubmit={handleSubmitAddSystem}
|
||||
/>
|
||||
)}
|
||||
<AddSystemDialog
|
||||
title="Add system to routes"
|
||||
visible={openAddSystem}
|
||||
setVisible={() => setOpenAddSystem(false)}
|
||||
onSubmit={handleSubmitAddSystem}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export const RoutesWidget = forwardRef<RoutesImperativeHandle, RoutesWidgetProps & RoutesWidgetCompProps>(
|
||||
({ title, renderContent, ...props }, ref) => {
|
||||
({ title, ...props }, ref) => {
|
||||
return (
|
||||
<RoutesProvider {...props} ref={ref}>
|
||||
<RoutesWidgetComp title={title} renderContent={renderContent} />
|
||||
<RoutesWidgetComp title={title} />
|
||||
</RoutesProvider>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './useLoadRoutes';
|
||||
export * from './useLoadRoutesBy';
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { flattenValues } from '@/hooks/Mapper/utils/flattenValues.ts';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { Commands } from '@/hooks/Mapper/types';
|
||||
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
type UseLoadRoutesByProps = {
|
||||
loadRoutesCommand: LoadRoutesCommand;
|
||||
routesList: RoutesList | undefined;
|
||||
data: RoutesType;
|
||||
deps?: unknown[];
|
||||
};
|
||||
|
||||
export const useLoadRoutesBy = ({
|
||||
data: routesSettings,
|
||||
loadRoutesCommand,
|
||||
routesList,
|
||||
deps = [],
|
||||
}: UseLoadRoutesByProps) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
} = useMapRootState();
|
||||
|
||||
const prevSys = usePrevious(selectedSystems);
|
||||
const ref = useRef({ prevSys, selectedSystems });
|
||||
ref.current = { prevSys, selectedSystems };
|
||||
|
||||
const loadRoutes = useCallback(
|
||||
(systemId: string, settings: RoutesType) => {
|
||||
loadRoutesCommand(systemId, settings);
|
||||
setLoading(true);
|
||||
},
|
||||
[loadRoutesCommand],
|
||||
);
|
||||
|
||||
useMapEventListener(event => {
|
||||
if (event.name === Commands.routesListBy) {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, [routesList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSystems.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
loadRoutes(systemId, routesSettings);
|
||||
}, [loadRoutes, selectedSystems, ...flattenValues(routesSettings), ...deps]);
|
||||
|
||||
return { loading, loadRoutes, setLoading };
|
||||
};
|
||||
@@ -12,8 +12,8 @@ export type RoutesWidgetProps = {
|
||||
routesList: RoutesList | undefined;
|
||||
loading: boolean;
|
||||
|
||||
addHubCommand?: AddHubCommand;
|
||||
toggleHubCommand?: ToggleHubCommand;
|
||||
addHubCommand: AddHubCommand;
|
||||
toggleHubCommand: ToggleHubCommand;
|
||||
isRestricted?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { RoutesWidget } from '@/hooks/Mapper/components/mapInterface/widgets';
|
||||
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { useLoadRoutesBy } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/hooks';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { SelectItemOptionsType } from 'primereact/selectitem';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth.ts';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export type RoutesByType = 'blueLoot' | 'redLoot';
|
||||
export type RoutesBySecurityType = 'both' | 'low' | 'high';
|
||||
|
||||
type WRoutesByProps = {
|
||||
type?: RoutesByType;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const ROUTES_BY_OPTIONS: SelectItemOptionsType = [
|
||||
{
|
||||
label: 'Blue Loot',
|
||||
value: 'blueLoot',
|
||||
icon: 'images/30747_64.png',
|
||||
},
|
||||
{
|
||||
label: 'Red Loot',
|
||||
value: 'redLoot',
|
||||
icon: 'images/89219_64.png',
|
||||
},
|
||||
];
|
||||
const ROUTES_BY_SECURITY_OPTIONS = [
|
||||
{ label: 'All', value: 'both' },
|
||||
{ label: 'High', value: 'high' },
|
||||
{ label: 'Low', value: 'low' },
|
||||
];
|
||||
|
||||
export const WRoutesBy = ({ type = 'blueLoot', title = 'Routes By' }: WRoutesByProps) => {
|
||||
const {
|
||||
outCommand,
|
||||
storedSettings: { settingsRoutes, settingsRoutesUpdate },
|
||||
data,
|
||||
} = useMapRootState();
|
||||
|
||||
const [criteriaType, setCriteriaType] = useState<RoutesByType>(type);
|
||||
const [securityType, setSecurityType] = useState<RoutesBySecurityType>('both');
|
||||
const routesListBy = data.routesListBy;
|
||||
|
||||
const loadRoutesCommand: LoadRoutesCommand = useCallback(
|
||||
async (systemId, routesSettings) => {
|
||||
await outCommand({
|
||||
type: OutCommand.getRoutesBy,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
type: criteriaType,
|
||||
securityType: securityType || 'both',
|
||||
routes_settings: routesSettings,
|
||||
},
|
||||
});
|
||||
},
|
||||
[outCommand, criteriaType, securityType],
|
||||
);
|
||||
|
||||
const hubs = useMemo(() => routesListBy?.routes?.map(route => route.destination.toString()) ?? [], [routesListBy]);
|
||||
|
||||
const { loading: internalLoading } = useLoadRoutesBy({
|
||||
data: settingsRoutes,
|
||||
loadRoutesCommand,
|
||||
routesList: routesListBy,
|
||||
deps: [criteriaType, securityType],
|
||||
});
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const compactSmall = useMaxWidth(ref, 180);
|
||||
const compactMiddle = useMaxWidth(ref, 245);
|
||||
|
||||
return (
|
||||
<RoutesWidget
|
||||
title={title}
|
||||
renderContent={(content /*, compact*/) => (
|
||||
<div className="h-full grid grid-rows-[1fr_auto]" ref={ref}>
|
||||
{content}
|
||||
<div className="flex items-center gap-2 justify-end mb-2 px-2 pt-2">
|
||||
{!compactSmall && (
|
||||
<Dropdown
|
||||
value={securityType}
|
||||
options={ROUTES_BY_SECURITY_OPTIONS}
|
||||
onChange={e => setSecurityType(e.value)}
|
||||
className="w-[90px] [&_span]:!text-[12px]"
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
value={criteriaType}
|
||||
itemTemplate={e => {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={e.icon} height="18" width="18"></img>
|
||||
<span className="text-[12px]">{e.label}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
valueTemplate={e => {
|
||||
if (compactMiddle) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 min-w-[50px]">
|
||||
<img src={e.icon} height="18" width="18"></img>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={e.icon} height="18" width="18"></img>
|
||||
<span className="text-[12px]">{e.label}</span>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
options={ROUTES_BY_OPTIONS}
|
||||
onChange={e => setCriteriaType(e.value)}
|
||||
className={clsx({
|
||||
['w-[130px]']: !compactMiddle,
|
||||
['w-[65px]']: compactMiddle,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
data={settingsRoutes}
|
||||
update={settingsRoutesUpdate}
|
||||
hubs={hubs}
|
||||
routesList={routesListBy}
|
||||
loading={internalLoading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export { WRoutesBy } from './WRoutesBy';
|
||||
export type { RoutesByType } from './WRoutesBy';
|
||||
@@ -6,5 +6,4 @@ export * from './SystemStructures';
|
||||
export * from './WSystemKills';
|
||||
export * from './WRoutesUser';
|
||||
export * from './WRoutesPublic';
|
||||
export * from './WRoutesBy';
|
||||
export * from './CommentsWidget';
|
||||
|
||||
@@ -13,7 +13,7 @@ export type SystemViewProps = {
|
||||
|
||||
export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomName, ...rest }: SystemViewProps) => {
|
||||
const memSystems = useMemo(() => [systemId], [systemId]);
|
||||
const { systems, lastUpdateKey, loading } = useLoadSystemStatic({ systems: memSystems });
|
||||
const { systems, loading } = useLoadSystemStatic({ systems: memSystems });
|
||||
|
||||
const {
|
||||
data: { systems: mapSystems },
|
||||
@@ -23,10 +23,9 @@ export const SystemView = ({ systemId, systemInfo: customSystemInfo, showCustomN
|
||||
if (!systemId) {
|
||||
return customSystemInfo;
|
||||
}
|
||||
|
||||
return systems.get(parseInt(systemId));
|
||||
// eslint-disable-next-line
|
||||
}, [customSystemInfo, systemId, systems, lastUpdateKey, loading]);
|
||||
}, [customSystemInfo, systemId, systems, loading]);
|
||||
|
||||
const mapSystemInfo = useMemo(() => {
|
||||
if (!showCustomName) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MapUnionTypes,
|
||||
OutCommandHandler,
|
||||
SolarSystemConnection,
|
||||
StringBoolean,
|
||||
TrackingCharacter,
|
||||
UseCharactersCacheData,
|
||||
UseCommentsData,
|
||||
@@ -75,7 +76,6 @@ const INITIAL_DATA: MapRootData = {
|
||||
userHubs: [],
|
||||
routes: undefined,
|
||||
userRoutes: undefined,
|
||||
routesListBy: undefined,
|
||||
kills: [],
|
||||
connections: [],
|
||||
detailedKills: {},
|
||||
|
||||
@@ -112,23 +112,3 @@ export const useUserRoutes = () => {
|
||||
update({ userRoutes: value });
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const useRoutesListBy = () => {
|
||||
const {
|
||||
update,
|
||||
data: { routesListBy },
|
||||
} = useMapRootState();
|
||||
|
||||
const ref = useRef({ update, routesListBy });
|
||||
ref.current = { update, routesListBy };
|
||||
|
||||
return useCallback((value: CommandRoutes) => {
|
||||
const { update, routesListBy } = ref.current;
|
||||
|
||||
if (areRoutesListsEqual(routesListBy, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
update({ routesListBy: value });
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
useMapInit,
|
||||
useMapUpdated,
|
||||
useRoutes,
|
||||
useRoutesListBy,
|
||||
useUserRoutes,
|
||||
} from './api';
|
||||
|
||||
@@ -62,7 +61,6 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapUpdated = useMapUpdated();
|
||||
const mapRoutes = useRoutes();
|
||||
const mapUserRoutes = useUserRoutes();
|
||||
const mapRoutesListBy = useRoutesListBy();
|
||||
const { addComment, removeComment } = useCommandComments();
|
||||
const { pingAdded, pingCancelled } = useCommandPings();
|
||||
const { pingBlocked } = useCommandPingBlocked();
|
||||
@@ -117,9 +115,6 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.routesListBy:
|
||||
mapRoutesListBy(data as CommandRoutes);
|
||||
break;
|
||||
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
|
||||
@@ -41,4 +41,5 @@ export type SolarSystemConnection = {
|
||||
target: string;
|
||||
|
||||
type?: ConnectionType;
|
||||
is_cross_list?: boolean;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,6 @@ export enum Commands {
|
||||
detailedKillsUpdated = 'detailed_kills_updated',
|
||||
routes = 'routes',
|
||||
userRoutes = 'user_routes',
|
||||
routesListBy = 'routes_list_by',
|
||||
centerSystem = 'center_system',
|
||||
selectSystem = 'select_system',
|
||||
selectSystems = 'select_systems',
|
||||
@@ -63,7 +62,6 @@ export type Command =
|
||||
| Commands.detailedKillsUpdated
|
||||
| Commands.routes
|
||||
| Commands.userRoutes
|
||||
| Commands.routesListBy
|
||||
| Commands.selectSystem
|
||||
| Commands.selectSystems
|
||||
| Commands.centerSystem
|
||||
@@ -123,7 +121,6 @@ export type CommandSignaturesUpdated = string;
|
||||
export type CommandMapUpdated = Partial<CommandInit>;
|
||||
export type CommandRoutes = RoutesList;
|
||||
export type CommandUserRoutes = RoutesList;
|
||||
export type CommandRoutesListBy = RoutesList;
|
||||
export type CommandKillsUpdated = Kill[];
|
||||
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
|
||||
export type CommandSelectSystem = string | undefined;
|
||||
@@ -202,7 +199,6 @@ export interface CommandData {
|
||||
[Commands.mapUpdated]: CommandMapUpdated;
|
||||
[Commands.routes]: CommandRoutes;
|
||||
[Commands.userRoutes]: CommandUserRoutes;
|
||||
[Commands.routesListBy]: CommandRoutesListBy;
|
||||
[Commands.killsUpdated]: CommandKillsUpdated;
|
||||
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
|
||||
[Commands.selectSystem]: CommandSelectSystem;
|
||||
@@ -236,7 +232,6 @@ export enum OutCommand {
|
||||
deleteUserHub = 'delete_user_hub',
|
||||
getRoutes = 'get_routes',
|
||||
getUserRoutes = 'get_user_routes',
|
||||
getRoutesBy = 'get_routes_by',
|
||||
getCharacterJumps = 'get_character_jumps',
|
||||
getStructures = 'get_structures',
|
||||
getSignatures = 'get_signatures',
|
||||
@@ -286,6 +281,7 @@ export enum OutCommand {
|
||||
addPing = 'add_ping',
|
||||
cancelPing = 'cancel_ping',
|
||||
startTracking = 'startTracking',
|
||||
layoutSystems = 'layout_systems',
|
||||
|
||||
// Only UI commands
|
||||
openSettings = 'open_settings',
|
||||
|
||||
@@ -20,7 +20,6 @@ export type MapUnionTypes = {
|
||||
systemSignatures: Record<string, SystemSignature[]>;
|
||||
routes?: RoutesList;
|
||||
userRoutes?: RoutesList;
|
||||
routesListBy?: RoutesList;
|
||||
kills: Record<number, number>;
|
||||
connections: SolarSystemConnection[];
|
||||
userPermissions: Partial<UserPermissions>;
|
||||
|
||||
@@ -13,18 +13,12 @@ export type SystemStaticInfoShort = Pick<
|
||||
|
||||
type MappedSystem = SolarSystemStaticInfoRaw | undefined;
|
||||
|
||||
export type RouteStationSummary = {
|
||||
station_id: number;
|
||||
station_name: string;
|
||||
};
|
||||
|
||||
export type Route = {
|
||||
destination: number;
|
||||
has_connection: boolean;
|
||||
origin: number;
|
||||
systems?: number[];
|
||||
mapped_systems?: MappedSystem[];
|
||||
stations?: RouteStationSummary[];
|
||||
success?: boolean;
|
||||
};
|
||||
|
||||
|
||||
12939
assets/package-lock.json
generated
Normal file
12939
assets/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
1762
assets/yarn.lock
1762
assets/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -67,10 +67,6 @@ wanderer_kills_base_url =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_KILLS_BASE_URL", "ws://wanderer-kills:4004")
|
||||
|
||||
route_builder_base_url =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_ROUTE_BUILDER_BASE_URL", "http://localhost:2001")
|
||||
|
||||
map_subscriptions_enabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
|
||||
@@ -178,7 +174,6 @@ config :wanderer_app,
|
||||
character_api_disabled: character_api_disabled,
|
||||
wanderer_kills_service_enabled: wanderer_kills_service_enabled,
|
||||
wanderer_kills_base_url: wanderer_kills_base_url,
|
||||
route_builder_base_url: route_builder_base_url,
|
||||
map_subscriptions_enabled: map_subscriptions_enabled,
|
||||
map_connection_auto_expire_hours: map_connection_auto_expire_hours,
|
||||
map_connection_auto_eol_hours: map_connection_auto_eol_hours,
|
||||
|
||||
@@ -14,7 +14,6 @@ defmodule WandererApp.Env do
|
||||
def base_url(), do: get_key(:web_app_url, "<BASE_URL>")
|
||||
def base_metrics_only(), do: get_key(:base_metrics_only, false)
|
||||
def custom_route_base_url(), do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
|
||||
def route_builder_base_url(), do: get_key(:route_builder_base_url, "http://localhost:2001")
|
||||
def invites(), do: get_key(:invites, false)
|
||||
|
||||
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
|
||||
|
||||
@@ -119,4 +119,325 @@ defmodule WandererApp.Map.PositionCalculator do
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
# Layout systems
|
||||
|
||||
def layout_systems(systems, connections, opts) do
|
||||
Logger.info("Layouting systems with #{length(systems)} systems and #{length(connections)} connections")
|
||||
|
||||
system_ids = Enum.map(systems, & &1.solar_system_id)
|
||||
system_props = systems |> Enum.map(&{&1.solar_system_id, &1}) |> Map.new()
|
||||
|
||||
# Build undirected adjacency list for component finding and traversal
|
||||
undirected_adj = connections
|
||||
|> Enum.reduce(%{}, fn %{solar_system_source: s, solar_system_target: t}, acc ->
|
||||
if s in system_ids and t in system_ids do
|
||||
acc
|
||||
|> Map.update(s, [t], &[t | &1])
|
||||
|> Map.update(t, [s], &[s | &1])
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
# Find connected components
|
||||
components = find_components(system_ids, undirected_adj)
|
||||
|
||||
# 1. Identify all roots for each component
|
||||
all_roots = components
|
||||
|> Enum.flat_map(&find_roots(&1, system_props))
|
||||
|> Enum.uniq()
|
||||
|> Enum.sort_by(&get_system_name(Map.get(system_props, &1)))
|
||||
|
||||
all_roots_set = MapSet.new(all_roots)
|
||||
|
||||
# 2. Pre-calculate root claims using Priority-based expansion (Oldest connections first)
|
||||
{root_claims, tree_edge_ids} = build_root_claims(all_roots_set, undirected_adj, connections)
|
||||
|
||||
# 3. Build tree adjacency list for traversal to ensure naming/positioning strictly follow tree logic
|
||||
tree_adj = connections
|
||||
|> Enum.reduce(%{}, fn conn, acc ->
|
||||
if MapSet.member?(tree_edge_ids, conn.id) do
|
||||
s = conn.solar_system_source
|
||||
t = conn.solar_system_target
|
||||
acc
|
||||
|> Map.update(s, [t], &[t | &1])
|
||||
|> Map.update(t, [s], &[s | &1])
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
# 4. Layout each root sequentially
|
||||
layout_type = (opts |> Keyword.get(:layout, "left_to_right")) |> String.to_atom()
|
||||
|
||||
{final_positions, hierarchical_names, _next_breadth, _visited} = all_roots
|
||||
|> Enum.reduce({%{}, %{}, 0.0, MapSet.new()}, fn root_id, {pos_acc, name_acc, cur_breadth, visited_acc} ->
|
||||
if MapSet.member?(visited_acc, root_id) do
|
||||
{pos_acc, name_acc, cur_breadth, visited_acc}
|
||||
else
|
||||
{start_x, start_y} = case layout_type do
|
||||
:top_to_bottom -> {cur_breadth, 0.0}
|
||||
_ -> {0.0, cur_breadth}
|
||||
end
|
||||
|
||||
# Recursive layout starting from this root, strictly following its claimed tree edges
|
||||
{subtree_pos, subtree_names, subtree_breadth, new_visited} = do_recursive_layout(root_id, start_x, start_y, tree_adj, system_props, visited_acc, root_claims, "0", layout_type)
|
||||
|
||||
# Use a larger margin between root subtrees
|
||||
margin = case layout_type do
|
||||
:top_to_bottom -> @m_x * 3
|
||||
_ -> @m_y * 3
|
||||
end
|
||||
|
||||
{Map.merge(pos_acc, subtree_pos), Map.merge(name_acc, subtree_names), cur_breadth + subtree_breadth + margin, new_visited}
|
||||
end
|
||||
end)
|
||||
|
||||
# 5. Detect special connections (cross-list or cycle) and affected systems
|
||||
{special_conn_ids, affected_roots} = connections
|
||||
|> Enum.reduce({[], MapSet.new()}, fn conn, {ids_acc, roots_acc} ->
|
||||
if not MapSet.member?(tree_edge_ids, conn.id) do
|
||||
# This is a special connection (cycle or cross-list)
|
||||
root_s = Map.get(root_claims, conn.solar_system_source)
|
||||
root_t = Map.get(root_claims, conn.solar_system_target)
|
||||
|
||||
new_roots_acc = roots_acc
|
||||
new_roots_acc = if root_s, do: MapSet.put(new_roots_acc, root_s), else: new_roots_acc
|
||||
new_roots_acc = if root_t, do: MapSet.put(new_roots_acc, root_t), else: new_roots_acc
|
||||
|
||||
Logger.info("[PositionCalculator] Special connection detected (Cycle/Cross-List): #{get_system_name(system_props[conn.solar_system_source])} <-> #{get_system_name(system_props[conn.solar_system_target])}. Skipping affected components.")
|
||||
{[conn.id | ids_acc], new_roots_acc}
|
||||
else
|
||||
{ids_acc, roots_acc}
|
||||
end
|
||||
end)
|
||||
|
||||
# Find all systems that belong to any affected root subtree
|
||||
skipped_system_ids = root_claims
|
||||
|> Enum.filter(fn {_, root_id} -> MapSet.member?(affected_roots, root_id) end)
|
||||
|> Enum.map(fn {sid, _} -> sid end)
|
||||
|> MapSet.new()
|
||||
|
||||
updated_systems = systems
|
||||
|> Enum.map(fn %{solar_system_id: id} = system ->
|
||||
is_skipped = MapSet.member?(skipped_system_ids, id)
|
||||
system = if is_skipped do
|
||||
# Skip: keep original positions
|
||||
system
|
||||
else
|
||||
{x, y} = Map.get(final_positions, id, {float(system.position_x), float(system.position_y)})
|
||||
%{system | position_x: round(x), position_y: round(y)}
|
||||
end
|
||||
|
||||
# Always attach the hierarchical name if it was calculated AND not skipped
|
||||
case Map.get(hierarchical_names, id) do
|
||||
h_name when not is_nil(h_name) and not is_skipped -> Map.put(system, :hierarchical_name, h_name)
|
||||
_ -> system
|
||||
end
|
||||
end)
|
||||
|
||||
{updated_systems, special_conn_ids}
|
||||
end
|
||||
|
||||
defp find_roots(component_ids, system_props) do
|
||||
component_systems = Enum.map(component_ids, &Map.get(system_props, &1))
|
||||
locked_roots = component_systems
|
||||
|> Enum.filter(&Map.get(&1, :locked, false))
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
if Enum.empty?(locked_roots) do
|
||||
# Fallback: take alphabetical first according to criteria
|
||||
component_systems
|
||||
|> Enum.sort_by(&get_system_name/1)
|
||||
|> List.first()
|
||||
|> Map.get(:solar_system_id)
|
||||
|> List.wrap()
|
||||
else
|
||||
locked_roots
|
||||
end
|
||||
end
|
||||
|
||||
defp get_system_name(nil), do: ""
|
||||
defp get_system_name(system) do
|
||||
Map.get(system, :name) || Map.get(system, :temporary_name) || (system.solar_system_id |> Integer.to_string())
|
||||
end
|
||||
|
||||
defp find_components(ids, adj) do
|
||||
ids
|
||||
|> Enum.reduce({[], MapSet.new()}, fn id, {components, visited} ->
|
||||
if MapSet.member?(visited, id) do
|
||||
{components, visited}
|
||||
else
|
||||
{component, new_visited} = bfs_component(id, adj)
|
||||
{[component | components], MapSet.union(visited, new_visited)}
|
||||
end
|
||||
end)
|
||||
|> elem(0)
|
||||
end
|
||||
|
||||
defp bfs_component(start_id, adj) do
|
||||
queue = :queue.from_list([start_id])
|
||||
do_bfs_component(queue, adj, MapSet.new())
|
||||
end
|
||||
|
||||
defp do_bfs_component(queue, adj, visited) do
|
||||
case :queue.out(queue) do
|
||||
{{:value, id}, q} ->
|
||||
if MapSet.member?(visited, id) do
|
||||
do_bfs_component(q, adj, visited)
|
||||
else
|
||||
visited = MapSet.put(visited, id)
|
||||
neighbors = Map.get(adj, id, [])
|
||||
q = Enum.reduce(neighbors, q, &:queue.in(&1, &2))
|
||||
do_bfs_component(q, adj, visited)
|
||||
end
|
||||
{:empty, _} ->
|
||||
{MapSet.to_list(visited), visited}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
defp do_recursive_layout(id, x, y, adj, system_props, visited, root_claims, path, layout \\ :left_to_right) do
|
||||
if MapSet.member?(visited, id) do
|
||||
{%{}, %{}, 0.0, visited}
|
||||
else
|
||||
system = Map.get(system_props, id)
|
||||
|
||||
# Determine actual axes based on orientation
|
||||
{actual_x, actual_y} = case layout do
|
||||
:top_to_bottom ->
|
||||
ay = if Map.get(system, :locked, false) and path != "0", do: float(system.position_y), else: y
|
||||
{x, ay}
|
||||
_ ->
|
||||
ax = if Map.get(system, :locked, false) and path != "0", do: float(system.position_x), else: x
|
||||
{ax, y}
|
||||
end
|
||||
|
||||
visited = MapSet.put(visited, id)
|
||||
|
||||
# Determine current root for this node
|
||||
root_id = Map.get(root_claims, id)
|
||||
|
||||
# Follow neighbors that belong to the SAME root claim and are not yet visited
|
||||
# (adj already only contains tree edges)
|
||||
children = Map.get(adj, id, [])
|
||||
|> Enum.filter(&(Map.get(root_claims, &1) == root_id))
|
||||
|> Enum.reject(&MapSet.member?(visited, &1))
|
||||
|> Enum.sort_by(&get_system_name(Map.get(system_props, &1)))
|
||||
|
||||
current_name_map = %{id => path}
|
||||
|
||||
if Enum.empty?(children) do
|
||||
branch_breadth = case layout do
|
||||
:top_to_bottom -> float(@w)
|
||||
_ -> float(@h)
|
||||
end
|
||||
{%{id => {actual_x, actual_y}}, current_name_map, branch_breadth, visited}
|
||||
else
|
||||
# Layout children sequentially based on orientation
|
||||
{children_pos, children_names, total_children_breadth, new_visited} = children
|
||||
|> Enum.with_index(1)
|
||||
|> Enum.reduce({%{}, %{}, 0.0, visited}, fn {child_id, index}, {acc_pos, acc_names, acc_b, acc_visited} ->
|
||||
child_path = if path == "0", do: "#{index}", else: "#{path}-#{index}"
|
||||
|
||||
{child_x, child_y} = case layout do
|
||||
:top_to_bottom -> {actual_x + acc_b, actual_y + @h + @m_y}
|
||||
_ -> {actual_x + @w + @m_x, actual_y + acc_b}
|
||||
end
|
||||
|
||||
{c_pos, c_names, c_b, c_v} = do_recursive_layout(child_id, child_x, child_y, adj, system_props, acc_visited, root_claims, child_path, layout)
|
||||
|
||||
step_margin = case layout do
|
||||
:top_to_bottom -> @m_x
|
||||
_ -> @m_y
|
||||
end
|
||||
|
||||
{Map.merge(acc_pos, c_pos), Map.merge(acc_names, c_names), acc_b + c_b + step_margin, c_v}
|
||||
end)
|
||||
|
||||
margin_correction = case layout do
|
||||
:top_to_bottom -> @m_x
|
||||
_ -> @m_y
|
||||
end
|
||||
|
||||
total_children_breadth = if total_children_breadth > 0, do: total_children_breadth - margin_correction, else: 0.0
|
||||
|
||||
node_pos = %{id => {actual_x, actual_y}}
|
||||
node_breadth = case layout do
|
||||
:top_to_bottom -> float(@w)
|
||||
_ -> float(@h)
|
||||
end
|
||||
|
||||
result_breadth = Enum.max([node_breadth, total_children_breadth])
|
||||
|
||||
{Map.merge(node_pos, children_pos), Map.merge(current_name_map, children_names), result_breadth, new_visited}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp build_root_claims(all_roots_set, adj, connections) do
|
||||
# Map connections to neutral keys {s, t} for quick age lookup
|
||||
conn_map = connections
|
||||
|> Enum.flat_map(fn c ->
|
||||
s = c.solar_system_source
|
||||
t = c.solar_system_target
|
||||
key = if s < t, do: {s, t}, else: {t, s}
|
||||
[{key, c}]
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
initial_claims = all_roots_set |> MapSet.to_list() |> Enum.map(&{&1, &1}) |> Map.new()
|
||||
|
||||
# Priority-based search frontier
|
||||
# We sort all roots by name to have deterministic start
|
||||
sorted_roots = all_roots_set |> MapSet.to_list() |> Enum.sort_by(& Integer.to_string(&1))
|
||||
|
||||
initial_frontier = sorted_roots
|
||||
|> Enum.flat_map(fn rid ->
|
||||
Map.get(adj, rid, [])
|
||||
|> Enum.reject(&Map.has_key?(initial_claims, &1))
|
||||
|> Enum.map(fn neighbor_id ->
|
||||
key = if rid < neighbor_id, do: {rid, neighbor_id}, else: {neighbor_id, rid}
|
||||
conn = Map.get(conn_map, key)
|
||||
|
||||
inserted_at = Map.get(conn, :inserted_at) || ~U[2099-01-01 00:00:00Z]
|
||||
sort_key = {inserted_at, Map.get(conn, :id, "")}
|
||||
{sort_key, neighbor_id, rid, conn.id}
|
||||
end)
|
||||
end)
|
||||
|
||||
do_build_claims(initial_frontier, adj, conn_map, initial_claims, MapSet.new())
|
||||
end
|
||||
|
||||
# Simple priority-based expansion search
|
||||
defp do_build_claims([], _adj, _conn_map, claims, tree_edge_ids), do: {claims, tree_edge_ids}
|
||||
defp do_build_claims(frontier, adj, conn_map, claims, tree_edge_ids) do
|
||||
# Sort frontier by sort_key (age ASC, then ID ASC)
|
||||
[{_key, node_id, root_id, conn_id} | rest_frontier] = Enum.sort_by(frontier, fn {k, _, _, _} -> k end)
|
||||
|
||||
if Map.has_key?(claims, node_id) do
|
||||
do_build_claims(rest_frontier, adj, conn_map, claims, tree_edge_ids)
|
||||
else
|
||||
new_claims = Map.put(claims, node_id, root_id)
|
||||
new_tree_edge_ids = MapSet.put(tree_edge_ids, conn_id)
|
||||
|
||||
# Add neighbors to frontier
|
||||
new_neighbors = Map.get(adj, node_id, [])
|
||||
|> Enum.reject(&Map.has_key?(new_claims, &1))
|
||||
|> Enum.map(fn neighbor_id ->
|
||||
key = if node_id < neighbor_id, do: {node_id, neighbor_id}, else: {neighbor_id, node_id}
|
||||
conn = Map.get(conn_map, key)
|
||||
|
||||
inserted_at = Map.get(conn, :inserted_at) || ~U[2099-01-01 00:00:00Z]
|
||||
sort_key = {inserted_at, Map.get(conn, :id, "")}
|
||||
{sort_key, neighbor_id, root_id, conn.id}
|
||||
end)
|
||||
|
||||
do_build_claims(rest_frontier ++ new_neighbors, adj, conn_map, new_claims, new_tree_edge_ids)
|
||||
end
|
||||
end
|
||||
|
||||
defp float(v) when is_integer(v), do: v * 1.0
|
||||
defp float(v), do: v
|
||||
end
|
||||
|
||||
@@ -72,6 +72,8 @@ defmodule WandererApp.Map.Server do
|
||||
|
||||
defdelegate delete_systems(map_id, solar_system_ids, user_id, character_id), to: Impl
|
||||
|
||||
defdelegate layout_systems(map_id, system_ids), to: Impl
|
||||
|
||||
defdelegate add_connection(map_id, connection_info), to: Impl
|
||||
|
||||
defdelegate delete_connection(map_id, connection_info), to: Impl
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
defmodule WandererApp.Map.RoutesBy do
|
||||
@moduledoc """
|
||||
Routes-by helper that uses the local route builder service.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@minimum_route_attrs [
|
||||
:system_class,
|
||||
:class_title,
|
||||
:security,
|
||||
:triglavian_invasion_status,
|
||||
:solar_system_id,
|
||||
:solar_system_name,
|
||||
:region_name,
|
||||
:is_shattered
|
||||
]
|
||||
|
||||
@default_routes_settings %{
|
||||
path_type: "shortest",
|
||||
include_mass_crit: true,
|
||||
include_eol: false,
|
||||
include_frig: true,
|
||||
include_cruise: true,
|
||||
avoid_wormholes: false,
|
||||
avoid_pochven: false,
|
||||
avoid_edencom: false,
|
||||
avoid_triglavian: false,
|
||||
include_thera: true,
|
||||
avoid: []
|
||||
}
|
||||
|
||||
@zarzakh_system 30_100_000
|
||||
@default_avoid_systems [@zarzakh_system]
|
||||
@get_link_pairs_advanced_params [
|
||||
:include_mass_crit,
|
||||
:include_eol,
|
||||
:include_frig
|
||||
]
|
||||
|
||||
def find(map_id, origin, routes_settings, type) do
|
||||
origin = parse_origin(origin)
|
||||
routes_settings = @default_routes_settings |> Map.merge(routes_settings || %{})
|
||||
|
||||
connections = build_connections(map_id, routes_settings)
|
||||
|
||||
avoidance_list = build_avoidance_list(routes_settings)
|
||||
|
||||
security_type =
|
||||
routes_settings
|
||||
|> Map.get(:security_type, "both")
|
||||
|> normalize_security_type()
|
||||
|
||||
payload = %{
|
||||
origin: origin,
|
||||
flag: routes_settings.path_type,
|
||||
connections: connections,
|
||||
avoid: avoidance_list,
|
||||
count: 40,
|
||||
type: type,
|
||||
security_type: security_type
|
||||
}
|
||||
|
||||
stations_by_system = WandererApp.RouteBuilderClient.stations_for(type)
|
||||
|
||||
case WandererApp.RouteBuilderClient.find_closest(payload) do
|
||||
{:ok, body} ->
|
||||
routes = normalize_routes(body, origin)
|
||||
routes = attach_stations(routes, stations_by_system)
|
||||
systems_static_data = fetch_systems_static_data(routes)
|
||||
{:ok, %{routes: routes, systems_static_data: systems_static_data}}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[RoutesBy] Failed to fetch routes by: #{inspect(reason)}")
|
||||
{:ok, %{routes: [], systems_static_data: []}}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_origin(origin) when is_integer(origin), do: origin
|
||||
|
||||
defp parse_origin(origin) when is_binary(origin) do
|
||||
case Integer.parse(origin) do
|
||||
{id, _} -> id
|
||||
:error -> 0
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_origin(_), do: 0
|
||||
|
||||
defp normalize_routes(%{"routes" => routes}, origin) when is_list(routes),
|
||||
do: normalize_routes(routes, origin)
|
||||
|
||||
defp normalize_routes(routes, _origin) when is_list(routes) do
|
||||
routes
|
||||
|> Enum.map(&map_route_info/1)
|
||||
|> Enum.filter(fn route_info -> not is_nil(route_info) end)
|
||||
end
|
||||
|
||||
defp normalize_routes(_body, _origin), do: []
|
||||
|
||||
defp attach_stations(routes, stations_by_system) do
|
||||
Enum.map(routes, fn route ->
|
||||
system_key = to_string(route.destination)
|
||||
stations = Map.get(stations_by_system, system_key, [])
|
||||
|
||||
normalized_stations =
|
||||
stations
|
||||
|> Enum.filter(&is_map/1)
|
||||
|> Enum.map(fn station ->
|
||||
%{
|
||||
station_id: Map.get(station, "station_id") || Map.get(station, :station_id),
|
||||
station_name: Map.get(station, "name") || Map.get(station, :name)
|
||||
}
|
||||
end)
|
||||
|> Enum.filter(fn station ->
|
||||
is_integer(station.station_id) and is_binary(station.station_name)
|
||||
end)
|
||||
|
||||
Map.put(route, :stations, normalized_stations)
|
||||
end)
|
||||
end
|
||||
|
||||
defp map_route_info(%{
|
||||
"origin" => origin,
|
||||
"destination" => destination,
|
||||
"systems" => result_systems,
|
||||
"success" => success
|
||||
}) do
|
||||
map_route_info(%{
|
||||
origin: origin,
|
||||
destination: destination,
|
||||
systems: result_systems,
|
||||
success: success
|
||||
})
|
||||
end
|
||||
|
||||
defp map_route_info(
|
||||
%{origin: origin, destination: destination, systems: result_systems, success: success} =
|
||||
_route_info
|
||||
) do
|
||||
systems =
|
||||
case result_systems do
|
||||
[] -> []
|
||||
_ -> result_systems |> Enum.reject(fn system_id -> system_id == origin end)
|
||||
end
|
||||
|
||||
%{
|
||||
has_connection: result_systems != [],
|
||||
systems: systems,
|
||||
origin: origin,
|
||||
destination: destination,
|
||||
success: success
|
||||
}
|
||||
end
|
||||
|
||||
defp map_route_info(_), do: nil
|
||||
|
||||
defp fetch_systems_static_data(routes) do
|
||||
routes
|
||||
|> Enum.map(fn route_info -> route_info.systems end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|> Task.async_stream(
|
||||
fn system_id ->
|
||||
case WandererApp.CachedInfo.get_system_static_info(system_id) do
|
||||
{:ok, nil} -> nil
|
||||
{:ok, system} -> system |> Map.take(@minimum_route_attrs)
|
||||
end
|
||||
end,
|
||||
max_concurrency: System.schedulers_online() * 4
|
||||
)
|
||||
|> Enum.map(fn {:ok, val} -> val end)
|
||||
end
|
||||
|
||||
defp build_avoidance_list(routes_settings) do
|
||||
{:ok, trig_systems} = WandererApp.CachedInfo.get_trig_systems()
|
||||
|
||||
pochven_solar_systems =
|
||||
trig_systems
|
||||
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Final" end)
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
triglavian_solar_systems =
|
||||
trig_systems
|
||||
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Triglavian" end)
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
edencom_solar_systems =
|
||||
trig_systems
|
||||
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Edencom" end)
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
avoidance_list =
|
||||
case routes_settings.avoid_edencom do
|
||||
true -> edencom_solar_systems
|
||||
false -> []
|
||||
end
|
||||
|
||||
avoidance_list =
|
||||
case routes_settings.avoid_triglavian do
|
||||
true -> [avoidance_list | triglavian_solar_systems]
|
||||
false -> avoidance_list
|
||||
end
|
||||
|
||||
avoidance_list =
|
||||
case routes_settings.avoid_pochven do
|
||||
true -> [avoidance_list | pochven_solar_systems]
|
||||
false -> avoidance_list
|
||||
end
|
||||
|
||||
(@default_avoid_systems ++ [routes_settings.avoid | avoidance_list])
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp normalize_security_type("high"), do: "high"
|
||||
defp normalize_security_type(:high), do: "high"
|
||||
defp normalize_security_type("low"), do: "low"
|
||||
defp normalize_security_type(:low), do: "low"
|
||||
defp normalize_security_type(_), do: "both"
|
||||
|
||||
defp build_connections(map_id, routes_settings) do
|
||||
if routes_settings.avoid_wormholes do
|
||||
[]
|
||||
else
|
||||
map_chains =
|
||||
routes_settings
|
||||
|> Map.take(@get_link_pairs_advanced_params)
|
||||
|> Map.put_new(:map_id, map_id)
|
||||
|> WandererApp.Api.MapConnection.get_link_pairs_advanced!()
|
||||
|> Enum.map(fn %{
|
||||
solar_system_source: solar_system_source,
|
||||
solar_system_target: solar_system_target
|
||||
} ->
|
||||
%{
|
||||
first: solar_system_source,
|
||||
second: solar_system_target
|
||||
}
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, thera_chains} =
|
||||
case routes_settings.include_thera do
|
||||
true ->
|
||||
WandererApp.Server.TheraDataFetcher.get_chain_pairs(routes_settings)
|
||||
|
||||
false ->
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
chains = remove_intersection([map_chains | thera_chains] |> List.flatten())
|
||||
|
||||
chains =
|
||||
case routes_settings.include_cruise do
|
||||
false ->
|
||||
{:ok, wh_class_a_systems} = WandererApp.CachedInfo.get_wh_class_a_systems()
|
||||
|
||||
chains
|
||||
|> Enum.filter(fn x ->
|
||||
not Enum.member?(wh_class_a_systems, x.first) and
|
||||
not Enum.member?(wh_class_a_systems, x.second)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
chains
|
||||
end
|
||||
|
||||
chains
|
||||
|> Enum.map(fn chain ->
|
||||
["#{chain.first}|#{chain.second}", "#{chain.second}|#{chain.first}"]
|
||||
end)
|
||||
|> List.flatten()
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_intersection(pairs_arr) do
|
||||
tuples = pairs_arr |> Enum.map(fn x -> {x.first, x.second} end)
|
||||
|
||||
tuples
|
||||
|> Enum.reduce([], fn {first, second} = x, acc ->
|
||||
if Enum.member?(tuples, {second, first}) do
|
||||
acc
|
||||
else
|
||||
[x | acc]
|
||||
end
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.map(fn {first, second} ->
|
||||
%{
|
||||
first: first,
|
||||
second: second
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -151,7 +151,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id,
|
||||
character_id: character_id
|
||||
} = connection_info
|
||||
} = connection_info,
|
||||
retrigger_layout \\ true
|
||||
),
|
||||
do:
|
||||
maybe_add_connection(
|
||||
@@ -162,7 +163,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
},
|
||||
character_id,
|
||||
true,
|
||||
connection_info |> Map.get(:extra_info)
|
||||
connection_info |> Map.get(:extra_info),
|
||||
retrigger_layout
|
||||
)
|
||||
|
||||
def paste_connections(
|
||||
@@ -179,13 +181,20 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
solar_system_source_id = source |> String.to_integer()
|
||||
solar_system_target_id = target |> String.to_integer()
|
||||
|
||||
# Disable retrigger_layout for each individual connection
|
||||
add_connection(map_id, %{
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id,
|
||||
character_id: character_id,
|
||||
extra_info: connection
|
||||
})
|
||||
}, false)
|
||||
end)
|
||||
|
||||
# Retrigger layout once at the end if auto_layout is on
|
||||
{:ok, %{map_opts: map_opts}} = WandererApp.Map.get_map_state(map_id)
|
||||
if Keyword.get(map_opts, :auto_layout, false) do
|
||||
SystemsImpl.layout_systems(map_id, nil)
|
||||
end
|
||||
end
|
||||
|
||||
def delete_connection(
|
||||
@@ -534,7 +543,18 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
old_location,
|
||||
character_id,
|
||||
is_manual,
|
||||
extra_info
|
||||
extra_info,
|
||||
retrigger_layout \\ true
|
||||
)
|
||||
|
||||
def maybe_add_connection(
|
||||
map_id,
|
||||
location,
|
||||
old_location,
|
||||
character_id,
|
||||
is_manual,
|
||||
extra_info,
|
||||
retrigger_layout
|
||||
)
|
||||
when not is_nil(location) and not is_nil(old_location) and
|
||||
not is_nil(old_location.solar_system_id) and
|
||||
@@ -646,6 +666,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
solar_system_target_id: location.solar_system_id
|
||||
})
|
||||
|
||||
if retrigger_layout do
|
||||
{:ok, %{map_opts: map_opts}} = WandererApp.Map.get_map_state(map_id)
|
||||
if Keyword.get(map_opts, :auto_layout, false) do
|
||||
SystemsImpl.layout_systems(map_id, nil)
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
{:error, :already_exists} ->
|
||||
@@ -670,7 +697,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
_old_location,
|
||||
_character_id,
|
||||
_is_manual,
|
||||
_connection_extra_info
|
||||
_connection_extra_info,
|
||||
_retrigger_layout
|
||||
),
|
||||
do: :ok
|
||||
|
||||
|
||||
@@ -257,6 +257,8 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
defdelegate update_connection_custom_info(map_id, connection_update), to: ConnectionsImpl
|
||||
defdelegate update_signatures(map_id, signatures_update), to: SignaturesImpl
|
||||
|
||||
defdelegate layout_systems(map_id, system_ids), to: SystemsImpl
|
||||
|
||||
def import_settings(map_id, settings, user_id) do
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:importing",
|
||||
@@ -477,7 +479,8 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
restrict_offline_showing:
|
||||
options |> Map.get("restrict_offline_showing", "false") |> String.to_existing_atom(),
|
||||
allowed_copy_for: options |> Map.get("allowed_copy_for", "admin"),
|
||||
allowed_paste_for: options |> Map.get("allowed_paste_for", "member")
|
||||
allowed_paste_for: options |> Map.get("allowed_paste_for", "member"),
|
||||
auto_layout: options |> Map.get("auto_layout", "false") |> String.to_existing_atom()
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
@@ -283,6 +283,72 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end
|
||||
)
|
||||
|
||||
def layout_systems(map_id, system_ids) do
|
||||
{:ok, all_systems} = WandererApp.Map.list_systems(map_id)
|
||||
{:ok, connections} = WandererApp.Map.list_connections(map_id)
|
||||
|
||||
Logger.info("Layouting systems for map #{map_id} with system_ids #{inspect(system_ids)}")
|
||||
|
||||
systems_to_layout =
|
||||
case system_ids do
|
||||
nil -> all_systems
|
||||
[] -> all_systems
|
||||
ids -> all_systems |> Enum.filter(fn %{solar_system_id: sid} -> sid in ids end)
|
||||
end
|
||||
|
||||
{:ok, %{map_opts: map_opts}} = WandererApp.Map.get_map_state(map_id)
|
||||
|
||||
{updated_systems, cross_list_conn_ids} =
|
||||
WandererApp.Map.PositionCalculator.layout_systems(
|
||||
systems_to_layout,
|
||||
connections,
|
||||
map_opts
|
||||
)
|
||||
|
||||
show_temp_system_name = map_opts |> Keyword.get(:show_temp_system_name, false)
|
||||
|
||||
updated_systems
|
||||
|> Enum.each(fn updated_system ->
|
||||
hierarchical_name = Map.get(updated_system, :hierarchical_name)
|
||||
|
||||
if show_temp_system_name and not is_nil(hierarchical_name) and hierarchical_name != "0" do
|
||||
update_system_temporary_name(map_id, %{
|
||||
solar_system_id: updated_system.solar_system_id,
|
||||
temporary_name: hierarchical_name
|
||||
})
|
||||
end
|
||||
|
||||
update_system_position(map_id, %{
|
||||
solar_system_id: updated_system.solar_system_id,
|
||||
position_x: updated_system.position_x,
|
||||
position_y: updated_system.position_y
|
||||
})
|
||||
end)
|
||||
|
||||
connections
|
||||
|> Enum.each(fn conn ->
|
||||
is_cross_list = conn.id in cross_list_conn_ids
|
||||
current_info = conn.custom_info || "{}"
|
||||
|
||||
new_info =
|
||||
case Jason.decode(current_info) do
|
||||
{:ok, info_map} when is_map(info_map) ->
|
||||
info_map |> Map.put("is_cross_list", is_cross_list) |> Jason.encode!()
|
||||
|
||||
_ ->
|
||||
Jason.encode!(%{"is_cross_list" => is_cross_list})
|
||||
end
|
||||
|
||||
if new_info != current_info do
|
||||
{:ok, updated_conn} =
|
||||
WandererApp.MapConnectionRepo.update_custom_info(conn, %{custom_info: new_info})
|
||||
|
||||
WandererApp.Map.update_connection(map_id, updated_conn)
|
||||
Impl.broadcast!(map_id, :update_connection, updated_conn)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def add_hub(
|
||||
map_id,
|
||||
hub_info
|
||||
|
||||
@@ -11,7 +11,8 @@ defmodule WandererApp.MapRepo do
|
||||
"show_temp_system_name" => "false",
|
||||
"restrict_offline_showing" => "false",
|
||||
"allowed_copy_for" => "admin_map",
|
||||
"allowed_paste_for" => "add_system"
|
||||
"allowed_paste_for" => "add_system",
|
||||
"auto_layout" => "false"
|
||||
}
|
||||
|
||||
def get(map_id, relationships \\ []) do
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
defmodule WandererApp.RouteBuilderClient do
|
||||
@moduledoc """
|
||||
HTTP client for the local route builder service.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@timeout_opts [pool_timeout: 5_000, receive_timeout: :timer.seconds(30)]
|
||||
@loot_dir Path.join(["repo", "data", "route_by_systems"])
|
||||
|
||||
def find_closest(%{
|
||||
origin: origin,
|
||||
flag: flag,
|
||||
connections: connections,
|
||||
avoid: avoid,
|
||||
count: count,
|
||||
type: type,
|
||||
security_type: security_type
|
||||
}) do
|
||||
url = "#{WandererApp.Env.route_builder_base_url()}/route/findClosest"
|
||||
|
||||
destinations = destinations_for(type, security_type)
|
||||
|
||||
payload = %{
|
||||
origin: origin,
|
||||
flag: flag,
|
||||
connections: connections || [],
|
||||
avoid: avoid || [],
|
||||
destinations: destinations,
|
||||
count: count || 1
|
||||
}
|
||||
|
||||
case Req.post(url, Keyword.merge([json: payload], @timeout_opts)) do
|
||||
{:ok, %{status: status, body: body}} when status in [200, 201] ->
|
||||
{:ok, body}
|
||||
|
||||
{:ok, %{status: status, body: body}} ->
|
||||
Logger.warning("[RouteBuilderClient] Unexpected status: #{status}")
|
||||
{:error, {:unexpected_status, status, body}}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[RouteBuilderClient] Request failed: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp destinations_for(type, security_type) do
|
||||
case load_loot_data(type) do
|
||||
{:ok, %{"system_ids_by_band" => by_band}} ->
|
||||
high = Map.get(by_band, "high", [])
|
||||
low = Map.get(by_band, "low", [])
|
||||
pick_by_band(high, low, security_type)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[RouteBuilderClient] Failed to load loot data: #{inspect(reason)}")
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def stations_for(type) do
|
||||
case load_loot_data(type) do
|
||||
{:ok, %{"system_stations" => system_stations}} when is_map(system_stations) ->
|
||||
system_stations
|
||||
|
||||
{:ok, _} ->
|
||||
%{}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("[RouteBuilderClient] Failed to load loot stations: #{inspect(reason)}")
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
defp pick_by_band(high, _low, "high"), do: high
|
||||
defp pick_by_band(high, _low, :high), do: high
|
||||
defp pick_by_band(high, _low, "hight"), do: high
|
||||
defp pick_by_band(high, _low, :hight), do: high
|
||||
defp pick_by_band(_high, low, "low"), do: low
|
||||
defp pick_by_band(_high, low, :low), do: low
|
||||
defp pick_by_band(high, low, _), do: high ++ low
|
||||
|
||||
defp load_loot_data("blueLoot"), do: load_loot_file("blueloot.json")
|
||||
defp load_loot_data(:blueLoot), do: load_loot_file("blueloot.json")
|
||||
defp load_loot_data("redLoot"), do: load_loot_file("redloot.json")
|
||||
defp load_loot_data(:redLoot), do: load_loot_file("redloot.json")
|
||||
defp load_loot_data(_), do: load_loot_file("blueloot.json")
|
||||
|
||||
defp load_loot_file(filename) do
|
||||
key = {__MODULE__, :loot_data, filename}
|
||||
|
||||
case :persistent_term.get(key, :missing) do
|
||||
:missing ->
|
||||
path = Path.join([:code.priv_dir(:wanderer_app), @loot_dir, filename])
|
||||
|
||||
with {:ok, body} <- File.read(path),
|
||||
{:ok, json} <- Jason.decode(body) do
|
||||
:persistent_term.put(key, json)
|
||||
{:ok, json}
|
||||
else
|
||||
error -> error
|
||||
end
|
||||
|
||||
cached ->
|
||||
{:ok, cached}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,27 +0,0 @@
|
||||
defmodule WandererAppWeb.RouteBuilderController do
|
||||
use WandererAppWeb, :controller
|
||||
|
||||
require Logger
|
||||
|
||||
def find_closest(conn, params) do
|
||||
payload = %{
|
||||
origin: Map.get(params, "origin") || Map.get(params, :origin),
|
||||
flag: Map.get(params, "flag") || Map.get(params, :flag) || "shortest",
|
||||
connections: Map.get(params, "connections") || Map.get(params, :connections) || [],
|
||||
avoid: Map.get(params, "avoid") || Map.get(params, :avoid) || [],
|
||||
count: Map.get(params, "count") || Map.get(params, :count) || 1,
|
||||
type: Map.get(params, "type") || Map.get(params, :type) || "blueLoot"
|
||||
}
|
||||
|
||||
case WandererApp.RouteBuilderClient.find_closest(payload) do
|
||||
{:ok, body} ->
|
||||
json(conn, body)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("[RouteBuilderController] find_closest failed: #{inspect(reason)}")
|
||||
conn
|
||||
|> put_status(:bad_gateway)
|
||||
|> json(%{error: "route_builder_failed"})
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -43,25 +43,6 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
}
|
||||
)
|
||||
|
||||
def handle_server_event(
|
||||
%{
|
||||
event: :routes_list_by,
|
||||
payload: {solar_system_id, %{routes: routes, systems_static_data: systems_static_data}}
|
||||
},
|
||||
socket
|
||||
),
|
||||
do:
|
||||
socket
|
||||
|> MapEventHandler.push_map_event(
|
||||
"routes_list_by",
|
||||
%{
|
||||
solar_system_id: solar_system_id,
|
||||
loading: false,
|
||||
routes: routes,
|
||||
systems_static_data: systems_static_data
|
||||
}
|
||||
)
|
||||
|
||||
def handle_server_event(event, socket),
|
||||
do: MapCoreEventHandler.handle_server_event(event, socket)
|
||||
|
||||
@@ -161,33 +142,6 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"get_routes_by",
|
||||
%{"system_id" => solar_system_id, "routes_settings" => routes_settings} = event,
|
||||
%{assigns: %{map_id: map_id, map_loaded?: true}} = socket
|
||||
) do
|
||||
routes_type = Map.get(event, "type", "blueLoot")
|
||||
security_type = Map.get(event, "securityType", "both")
|
||||
routes_settings =
|
||||
routes_settings
|
||||
|> get_routes_settings()
|
||||
|> Map.put(:security_type, security_type)
|
||||
|
||||
Task.async(fn ->
|
||||
{:ok, routes} =
|
||||
WandererApp.Map.RoutesBy.find(
|
||||
map_id,
|
||||
solar_system_id,
|
||||
routes_settings,
|
||||
routes_type
|
||||
)
|
||||
|
||||
{:routes_list_by, {solar_system_id, routes}}
|
||||
end)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"add_hub",
|
||||
%{"system_id" => solar_system_id} = _event,
|
||||
|
||||
@@ -163,6 +163,25 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"layout_systems",
|
||||
%{"system_ids" => system_ids},
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
main_character_id: main_character_id,
|
||||
has_tracked_characters?: true,
|
||||
user_permissions: %{update_system: true}
|
||||
}
|
||||
} = socket
|
||||
)
|
||||
when not is_nil(main_character_id) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.layout_systems(system_ids)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"update_system_position",
|
||||
position,
|
||||
|
||||
@@ -58,7 +58,8 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
"update_system_tag",
|
||||
"update_system_temporary_name",
|
||||
"update_system_status",
|
||||
"manual_paste_systems_and_connections"
|
||||
"manual_paste_systems_and_connections",
|
||||
"layout_systems"
|
||||
]
|
||||
|
||||
@map_system_comments_events [
|
||||
@@ -101,13 +102,11 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
|
||||
@map_routes_events [
|
||||
:routes,
|
||||
:user_routes,
|
||||
:routes_list_by
|
||||
:user_routes
|
||||
]
|
||||
|
||||
@map_routes_ui_events [
|
||||
"get_routes",
|
||||
"get_routes_by",
|
||||
"get_user_routes",
|
||||
"set_autopilot_waypoint",
|
||||
"add_hub",
|
||||
@@ -341,19 +340,34 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
time_status: time_status,
|
||||
type: type,
|
||||
ship_size_type: ship_size_type,
|
||||
locked: locked
|
||||
locked: locked,
|
||||
custom_info: custom_info
|
||||
} = _connection
|
||||
),
|
||||
do: %{
|
||||
id: "#{solar_system_source}_#{solar_system_target}",
|
||||
mass_status: mass_status,
|
||||
time_status: time_status,
|
||||
type: type,
|
||||
ship_size_type: ship_size_type,
|
||||
locked: locked,
|
||||
source: "#{solar_system_source}",
|
||||
target: "#{solar_system_target}"
|
||||
}
|
||||
) do
|
||||
is_cross_list =
|
||||
case custom_info do
|
||||
nil ->
|
||||
false
|
||||
|
||||
info ->
|
||||
case Jason.decode(info) do
|
||||
{:ok, %{"is_cross_list" => val}} -> val
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
%{
|
||||
id: "#{solar_system_source}_#{solar_system_target}",
|
||||
mass_status: mass_status,
|
||||
time_status: time_status,
|
||||
type: type,
|
||||
ship_size_type: ship_size_type,
|
||||
locked: locked,
|
||||
source: "#{solar_system_source}",
|
||||
target: "#{solar_system_target}",
|
||||
is_cross_list: is_cross_list
|
||||
}
|
||||
end
|
||||
|
||||
def map_ui_system(
|
||||
%{
|
||||
|
||||
@@ -574,7 +574,8 @@ defmodule WandererAppWeb.MapsLive do
|
||||
"show_temp_system_name",
|
||||
"restrict_offline_showing",
|
||||
"allowed_copy_for",
|
||||
"allowed_paste_for"
|
||||
"allowed_paste_for",
|
||||
"auto_layout"
|
||||
])
|
||||
|
||||
{:ok, updated_map} = WandererApp.MapRepo.update_options(map, options)
|
||||
|
||||
@@ -439,55 +439,79 @@
|
||||
for={@options_form}
|
||||
phx-change="update_options"
|
||||
>
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:layout]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
label="Map systems layout"
|
||||
placeholder="Map default layout"
|
||||
options={@layout_options}
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:store_custom_labels]}
|
||||
label="Store system custom labels"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:show_temp_system_name]}
|
||||
label="Allow temporary system names"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:show_linked_signature_id]}
|
||||
label="Show linked signature ID as custom label part"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:show_linked_signature_id_temp_name]}
|
||||
label="Show linked signature ID as temporary name part"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:restrict_offline_showing]}
|
||||
label="Show offline characters to admins & managers only"
|
||||
/>
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:allowed_copy_for]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
label="Copy map data allowed for"
|
||||
placeholder="Select role to allow map data copy"
|
||||
options={@allowed_copy_for_options}
|
||||
/>
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:allowed_paste_for]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
label="Paste map data allowed for"
|
||||
placeholder="Select role to allow map data paste"
|
||||
options={@allowed_paste_for_options}
|
||||
/>
|
||||
<div class="flex flex-col gap-2 p-1">
|
||||
<div class="border border-dashed border-stone-600 rounded p-3">
|
||||
<p class="text-xs text-stone-400 mb-2 uppercase tracking-wider font-bold">
|
||||
Layout Settings
|
||||
</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:layout]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
label="Map systems layout"
|
||||
placeholder="Map default layout"
|
||||
options={@layout_options}
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:auto_layout]}
|
||||
label="Retrigger layout on new connection added"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:show_temp_system_name]}
|
||||
label="Allow hierarchical numbering (Temporary Name)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-dashed border-stone-600 rounded p-3 mt-2">
|
||||
<p class="text-xs text-stone-400 mb-2 uppercase tracking-wider font-bold">
|
||||
General Settings
|
||||
</p>
|
||||
<div class="flex flex-col gap-1">
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:store_custom_labels]}
|
||||
label="Store system custom labels"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:show_linked_signature_id]}
|
||||
label="Show linked signature ID as custom label part"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:show_linked_signature_id_temp_name]}
|
||||
label="Show linked signature ID as temporary name part"
|
||||
/>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:restrict_offline_showing]}
|
||||
label="Show offline characters to admins & managers only"
|
||||
/>
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:allowed_copy_for]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
label="Copy map data allowed for"
|
||||
placeholder="Select role to allow map data copy"
|
||||
options={@allowed_copy_for_options}
|
||||
wrapper_class="mt-2"
|
||||
/>
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:allowed_paste_for]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
label="Paste map data allowed for"
|
||||
placeholder="Select role to allow map data paste"
|
||||
options={@allowed_paste_for_options}
|
||||
wrapper_class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</.form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -341,11 +341,6 @@ defmodule WandererAppWeb.Router do
|
||||
get "/system-static-info", CommonAPIController, :show_system_static
|
||||
end
|
||||
|
||||
scope "/route", WandererAppWeb do
|
||||
pipe_through [:api]
|
||||
post "/findClosest", RouteBuilderController, :find_closest
|
||||
end
|
||||
|
||||
scope "/api" do
|
||||
pipe_through [:api_spec]
|
||||
get "/openapi", OpenApiSpex.Plug.RenderSpec, :show
|
||||
|
||||
@@ -1,947 +0,0 @@
|
||||
{
|
||||
"type_id": 30747,
|
||||
"generated_at": "2026-01-30T13:53:23.834Z",
|
||||
"bands": [
|
||||
"high",
|
||||
"low"
|
||||
],
|
||||
"system_ids_by_band": {
|
||||
"high": [
|
||||
30000055,
|
||||
30000053,
|
||||
30000159,
|
||||
30000160,
|
||||
30000187,
|
||||
30000133,
|
||||
30000181,
|
||||
30001359,
|
||||
30001391,
|
||||
30001395,
|
||||
30001397,
|
||||
30001676,
|
||||
30001679,
|
||||
30001677,
|
||||
30002558,
|
||||
30002569,
|
||||
30002571,
|
||||
30002568,
|
||||
30002572,
|
||||
30002771,
|
||||
30002800,
|
||||
30002815,
|
||||
30002816,
|
||||
30002988,
|
||||
30002992,
|
||||
30002993,
|
||||
30003017,
|
||||
30003018,
|
||||
30003024,
|
||||
30003025,
|
||||
30003029,
|
||||
30003030,
|
||||
30003048,
|
||||
30003053,
|
||||
30003055,
|
||||
30003389,
|
||||
30003394,
|
||||
30003402,
|
||||
30003409,
|
||||
30003412,
|
||||
30003447,
|
||||
30003449,
|
||||
30003469,
|
||||
30003404,
|
||||
30003413,
|
||||
30002191,
|
||||
30002193,
|
||||
30002252,
|
||||
30003553,
|
||||
30003554,
|
||||
30003555,
|
||||
30002190,
|
||||
30002187,
|
||||
30004077,
|
||||
30004078,
|
||||
30004079,
|
||||
30004083,
|
||||
30004084,
|
||||
30004111,
|
||||
30004112,
|
||||
30004114,
|
||||
30005009,
|
||||
30005011,
|
||||
30005017,
|
||||
30005018,
|
||||
30005039,
|
||||
30005040,
|
||||
30005043,
|
||||
30005052,
|
||||
30005054,
|
||||
30005204,
|
||||
30005199
|
||||
],
|
||||
"low": [
|
||||
30000017,
|
||||
30000040,
|
||||
30000041,
|
||||
30000072,
|
||||
30000074,
|
||||
30000162,
|
||||
30000163,
|
||||
30000164,
|
||||
30000196,
|
||||
30000197,
|
||||
30001390,
|
||||
30001361,
|
||||
30002414,
|
||||
30002415,
|
||||
30002418,
|
||||
30002559,
|
||||
30002560,
|
||||
30002769,
|
||||
30002975,
|
||||
30002977,
|
||||
30002980,
|
||||
30002059,
|
||||
30002065,
|
||||
30003467,
|
||||
30002058,
|
||||
30002067,
|
||||
30003556,
|
||||
30004239,
|
||||
30004240,
|
||||
30004241,
|
||||
30004288,
|
||||
30004291,
|
||||
30004296,
|
||||
30005010,
|
||||
30005020,
|
||||
30005030,
|
||||
30005031,
|
||||
30005034,
|
||||
30005035,
|
||||
30005275,
|
||||
30005276
|
||||
]
|
||||
},
|
||||
"system_stations": {
|
||||
"30000017": [
|
||||
{
|
||||
"station_id": 60014071,
|
||||
"name": "Futzchag IX - Moon 9 - Thukker Mix Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60014074,
|
||||
"name": "Futzchag II - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000040": [
|
||||
{
|
||||
"station_id": 60014095,
|
||||
"name": "Uzistoon VII - Moon 2 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000041": [
|
||||
{
|
||||
"station_id": 60014098,
|
||||
"name": "Bairshir IV - Moon 11 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000053": [
|
||||
{
|
||||
"station_id": 60014137,
|
||||
"name": "Ibaria III - Thukker Mix Warehouse"
|
||||
}
|
||||
],
|
||||
"30000055": [
|
||||
{
|
||||
"station_id": 60014140,
|
||||
"name": "Zemalu IX - Moon 2 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000072": [
|
||||
{
|
||||
"station_id": 60014068,
|
||||
"name": "Nakah I - Moon 1 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000074": [
|
||||
{
|
||||
"station_id": 60014065,
|
||||
"name": "Hasateem VI - Moon 12 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30000133": [
|
||||
{
|
||||
"station_id": 60001810,
|
||||
"name": "Hirtamon VII - Moon 6 - Zainou Biotech Production"
|
||||
},
|
||||
{
|
||||
"station_id": 60001807,
|
||||
"name": "Hirtamon VII - Moon 5 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30000159": [
|
||||
{
|
||||
"station_id": 60010195,
|
||||
"name": "Ikami X - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30000160": [
|
||||
{
|
||||
"station_id": 60010192,
|
||||
"name": "Reisen VI - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30000162": [
|
||||
{
|
||||
"station_id": 60001801,
|
||||
"name": "Maila IV - Zainou Biotech Production"
|
||||
},
|
||||
{
|
||||
"station_id": 60001804,
|
||||
"name": "Maila VI - Moon 1 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30000163": [
|
||||
{
|
||||
"station_id": 60010198,
|
||||
"name": "Akora IX - Moon 19 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30000164": [
|
||||
{
|
||||
"station_id": 60010201,
|
||||
"name": "Messoya VIII - Moon 6 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30000181": [
|
||||
{
|
||||
"station_id": 60001783,
|
||||
"name": "Korsiki III - Moon 15 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30000187": [
|
||||
{
|
||||
"station_id": 60001786,
|
||||
"name": "Wuos VI - Zainou Biotech Research Center"
|
||||
}
|
||||
],
|
||||
"30000196": [
|
||||
{
|
||||
"station_id": 60001798,
|
||||
"name": "Otosela V - Moon 13 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30000197": [
|
||||
{
|
||||
"station_id": 60001795,
|
||||
"name": "Uemon VIII - Moon 10 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001359": [
|
||||
{
|
||||
"station_id": 60001780,
|
||||
"name": "Semiki IV - Zainou Biohazard Containment Facility"
|
||||
}
|
||||
],
|
||||
"30001361": [
|
||||
{
|
||||
"station_id": 60001777,
|
||||
"name": "Aurohunen III - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001390": [
|
||||
{
|
||||
"station_id": 60001816,
|
||||
"name": "Pakkonen IV - Moon 11 - Zainou Biotech Research Center"
|
||||
}
|
||||
],
|
||||
"30001391": [
|
||||
{
|
||||
"station_id": 60001813,
|
||||
"name": "Piekura VIII - Moon 15 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001395": [
|
||||
{
|
||||
"station_id": 60001768,
|
||||
"name": "Ylandoki II - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001397": [
|
||||
{
|
||||
"station_id": 60001765,
|
||||
"name": "Isseras IV - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30001676": [
|
||||
{
|
||||
"station_id": 60008527,
|
||||
"name": "Mimen X - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008521,
|
||||
"name": "Mimen VIII - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30001677": [
|
||||
{
|
||||
"station_id": 60008518,
|
||||
"name": "Thashkarai VII - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30001679": [
|
||||
{
|
||||
"station_id": 60008524,
|
||||
"name": "Unkah VI - Moon 7 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002058": [
|
||||
{
|
||||
"station_id": 60014134,
|
||||
"name": "Ardar IV - Moon 2 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002059": [
|
||||
{
|
||||
"station_id": 60014131,
|
||||
"name": "Auner VIII - Moon 10 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002065": [
|
||||
{
|
||||
"station_id": 60014143,
|
||||
"name": "Lasleinur IV - Moon 16 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002067": [
|
||||
{
|
||||
"station_id": 60014146,
|
||||
"name": "Brin V - Moon 7 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002187": [
|
||||
{
|
||||
"station_id": 60008494,
|
||||
"name": "Amarr VIII (Oris) - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30002190": [
|
||||
{
|
||||
"station_id": 60008500,
|
||||
"name": "Mabnen IV - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002191": [
|
||||
{
|
||||
"station_id": 60008503,
|
||||
"name": "Toshabia VI - Moon 6 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002193": [
|
||||
{
|
||||
"station_id": 60008497,
|
||||
"name": "Kehour VIII - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002252": [
|
||||
{
|
||||
"station_id": 60010240,
|
||||
"name": "Bika III - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010243,
|
||||
"name": "Bika VIII - Moon 11 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010246,
|
||||
"name": "Bika V - Moon 1 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010249,
|
||||
"name": "Bika VII - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002414": [
|
||||
{
|
||||
"station_id": 60010183,
|
||||
"name": "Klingt IX - Moon 11 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002415": [
|
||||
{
|
||||
"station_id": 60010180,
|
||||
"name": "Weld IV - Moon 4 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002418": [
|
||||
{
|
||||
"station_id": 60010186,
|
||||
"name": "Hegfunden VIII - Moon 14 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010189,
|
||||
"name": "Hegfunden VIII - Moon 26 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002558": [
|
||||
{
|
||||
"station_id": 60010168,
|
||||
"name": "Endrulf VIII - Moon 1 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010171,
|
||||
"name": "Endrulf IV - Moon 1 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30002559": [
|
||||
{
|
||||
"station_id": 60010174,
|
||||
"name": "Ingunn V - Moon 21 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002560": [
|
||||
{
|
||||
"station_id": 60010177,
|
||||
"name": "Gultratren V - Moon 22 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002568": [
|
||||
{
|
||||
"station_id": 60010297,
|
||||
"name": "Onga X - Moon 11 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010291,
|
||||
"name": "Onga VI - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002569": [
|
||||
{
|
||||
"station_id": 60010294,
|
||||
"name": "Osaumuni VII - Moon 16 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30002571": [
|
||||
{
|
||||
"station_id": 60010288,
|
||||
"name": "Oremmulf IX - Moon 6 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60014110,
|
||||
"name": "Oremmulf V - Moon 20 - Thukker Mix Warehouse"
|
||||
}
|
||||
],
|
||||
"30002572": [
|
||||
{
|
||||
"station_id": 60014107,
|
||||
"name": "Hurjafren VII - Moon 25 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30002769": [
|
||||
{
|
||||
"station_id": 60001819,
|
||||
"name": "Enderailen IV - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002771": [
|
||||
{
|
||||
"station_id": 60001822,
|
||||
"name": "Kulelen V - Moon 8 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002800": [
|
||||
{
|
||||
"station_id": 60001792,
|
||||
"name": "Haatomo VI - Moon 6 - Zainou Biotech Production"
|
||||
},
|
||||
{
|
||||
"station_id": 60001789,
|
||||
"name": "Haatomo VII - Moon 7 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002815": [
|
||||
{
|
||||
"station_id": 60001774,
|
||||
"name": "Isenairos V - Moon 7 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002816": [
|
||||
{
|
||||
"station_id": 60001771,
|
||||
"name": "Saila VIII - Moon 16 - Zainou Biotech Production"
|
||||
}
|
||||
],
|
||||
"30002975": [
|
||||
{
|
||||
"station_id": 60008551,
|
||||
"name": "Roushzar II - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002977": [
|
||||
{
|
||||
"station_id": 60008542,
|
||||
"name": "Arayar VII - Moon 16 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002980": [
|
||||
{
|
||||
"station_id": 60008545,
|
||||
"name": "Sosan II - Emperor Family Academy"
|
||||
},
|
||||
{
|
||||
"station_id": 60008548,
|
||||
"name": "Sosan III - Moon 4 - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30002988": [
|
||||
{
|
||||
"station_id": 60008536,
|
||||
"name": "Nakatre II - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30002992": [
|
||||
{
|
||||
"station_id": 60008530,
|
||||
"name": "Akes V - Moon 2 - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30002993": [
|
||||
{
|
||||
"station_id": 60008533,
|
||||
"name": "Riavayed IX - Moon 2 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008539,
|
||||
"name": "Riavayed II - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30003017": [
|
||||
{
|
||||
"station_id": 60010258,
|
||||
"name": "Harerget VIII - Moon 1 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010252,
|
||||
"name": "Harerget V - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003018": [
|
||||
{
|
||||
"station_id": 60010261,
|
||||
"name": "Azer III - Moon 6 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010255,
|
||||
"name": "Azer VI - Moon 1 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30003024": [
|
||||
{
|
||||
"station_id": 60010219,
|
||||
"name": "Marosier IV - Moon 2 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003025": [
|
||||
{
|
||||
"station_id": 60010216,
|
||||
"name": "Lirsautton I - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003029": [
|
||||
{
|
||||
"station_id": 60010222,
|
||||
"name": "Jaschercis IV - Moon 2 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003030": [
|
||||
{
|
||||
"station_id": 60010225,
|
||||
"name": "Ardallabier III - Moon 14 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003048": [
|
||||
{
|
||||
"station_id": 60010120,
|
||||
"name": "Carirgnottin VIII - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003053": [
|
||||
{
|
||||
"station_id": 60010126,
|
||||
"name": "Avele V - Moon 11 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30003055": [
|
||||
{
|
||||
"station_id": 60010129,
|
||||
"name": "Aydoteaux II - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010123,
|
||||
"name": "Aydoteaux VIII - Moon 12 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30003389": [
|
||||
{
|
||||
"station_id": 60014077,
|
||||
"name": "Altrinur XI - Moon 3 - Thukker Mix Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60014080,
|
||||
"name": "Altrinur XII - Moon 2 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003394": [
|
||||
{
|
||||
"station_id": 60014125,
|
||||
"name": "Freatlidur V - Moon 4 - Thukker Mix Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60014128,
|
||||
"name": "Freatlidur VII - Moon 3 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003402": [
|
||||
{
|
||||
"station_id": 60014083,
|
||||
"name": "Totkubad III - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003404": [
|
||||
{
|
||||
"station_id": 60014086,
|
||||
"name": "Agtver VI - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003409": [
|
||||
{
|
||||
"station_id": 60014101,
|
||||
"name": "Leurtmar III - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003412": [
|
||||
{
|
||||
"station_id": 60005719,
|
||||
"name": "Elgoi VI - Moon 1 - Eifyr and Co. Biotech Production"
|
||||
},
|
||||
{
|
||||
"station_id": 60014104,
|
||||
"name": "Elgoi VIII - Moon 19 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003413": [
|
||||
{
|
||||
"station_id": 60005722,
|
||||
"name": "Eram V - Moon 2 - Eifyr and Co. Biotech Production"
|
||||
}
|
||||
],
|
||||
"30003447": [
|
||||
{
|
||||
"station_id": 60010144,
|
||||
"name": "Josekorn IV - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010150,
|
||||
"name": "Josekorn VIII - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010153,
|
||||
"name": "Josekorn X - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003449": [
|
||||
{
|
||||
"station_id": 60010147,
|
||||
"name": "Hakeri XI - Moon 5 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003467": [
|
||||
{
|
||||
"station_id": 60014116,
|
||||
"name": "Frulegur IX - Moon 5 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003469": [
|
||||
{
|
||||
"station_id": 60014113,
|
||||
"name": "Hodrold VII - Moon 8 - Thukker Mix Factory"
|
||||
}
|
||||
],
|
||||
"30003553": [
|
||||
{
|
||||
"station_id": 60010141,
|
||||
"name": "Warouh VII - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003554": [
|
||||
{
|
||||
"station_id": 60010132,
|
||||
"name": "Jambu VI - Moon 3 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003555": [
|
||||
{
|
||||
"station_id": 60010138,
|
||||
"name": "Bittanshal VII - Moon 9 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30003556": [
|
||||
{
|
||||
"station_id": 60010135,
|
||||
"name": "Arton II - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004077": [
|
||||
{
|
||||
"station_id": 60008509,
|
||||
"name": "Hiroudeh VIII - Moon 3 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004078": [
|
||||
{
|
||||
"station_id": 60008515,
|
||||
"name": "Dresi I - Moon 18 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004079": [
|
||||
{
|
||||
"station_id": 60008506,
|
||||
"name": "Aphend VII - Moon 4 - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30004083": [
|
||||
{
|
||||
"station_id": 60008512,
|
||||
"name": "Gensela X - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004084": [
|
||||
{
|
||||
"station_id": 60010228,
|
||||
"name": "Ghesis V - Moon 2 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010231,
|
||||
"name": "Ghesis V - Moon 9 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010234,
|
||||
"name": "Ghesis V - Moon 13 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010237,
|
||||
"name": "Ghesis V - Moon 3 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004111": [
|
||||
{
|
||||
"station_id": 60010279,
|
||||
"name": "Yarebap VII - Moon 8 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004112": [
|
||||
{
|
||||
"station_id": 60010276,
|
||||
"name": "Mandoo III - Moon 11 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010282,
|
||||
"name": "Mandoo III - Moon 5 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004114": [
|
||||
{
|
||||
"station_id": 60010285,
|
||||
"name": "Peyiri XI - Moon 21 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30004239": [
|
||||
{
|
||||
"station_id": 60008566,
|
||||
"name": "Kamih VII - Moon 4 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004240": [
|
||||
{
|
||||
"station_id": 60008569,
|
||||
"name": "Hier IV - Moon 3 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008572,
|
||||
"name": "Hier VII - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004241": [
|
||||
{
|
||||
"station_id": 60008575,
|
||||
"name": "Jasson I - Moon 4 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30004288": [
|
||||
{
|
||||
"station_id": 60010267,
|
||||
"name": "Ghekon V - Moon 5 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010273,
|
||||
"name": "Ghekon II - Moon 2 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004291": [
|
||||
{
|
||||
"station_id": 60010270,
|
||||
"name": "Anohel VI - Moon 14 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30004296": [
|
||||
{
|
||||
"station_id": 60010264,
|
||||
"name": "Bapraya IV - Moon 1 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30005009": [
|
||||
{
|
||||
"station_id": 60010159,
|
||||
"name": "Allebin VIII - Moon 4 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30005010": [
|
||||
{
|
||||
"station_id": 60010162,
|
||||
"name": "Atlulle VIII - Moon 6 - CreoDron Factory"
|
||||
},
|
||||
{
|
||||
"station_id": 60010165,
|
||||
"name": "Atlulle III - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30005011": [
|
||||
{
|
||||
"station_id": 60010156,
|
||||
"name": "Droselory VI - Moon 17 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30005017": [
|
||||
{
|
||||
"station_id": 60010207,
|
||||
"name": "Yona VI - Moon 5 - CreoDron Factory"
|
||||
}
|
||||
],
|
||||
"30005018": [
|
||||
{
|
||||
"station_id": 60010210,
|
||||
"name": "Noghere VII - Moon 15 - CreoDron Warehouse"
|
||||
},
|
||||
{
|
||||
"station_id": 60010213,
|
||||
"name": "Noghere VIII - Moon 18 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30005020": [
|
||||
{
|
||||
"station_id": 60010204,
|
||||
"name": "Seyllin VIII - Moon 14 - CreoDron Warehouse"
|
||||
}
|
||||
],
|
||||
"30005030": [
|
||||
{
|
||||
"station_id": 60008578,
|
||||
"name": "Fensi V - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005031": [
|
||||
{
|
||||
"station_id": 60008584,
|
||||
"name": "Nebian VIII - Moon 4 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005034": [
|
||||
{
|
||||
"station_id": 60008581,
|
||||
"name": "Bridi II - Moon 1 - Emperor Family Academy"
|
||||
}
|
||||
],
|
||||
"30005035": [
|
||||
{
|
||||
"station_id": 60008587,
|
||||
"name": "Ami XI - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005039": [
|
||||
{
|
||||
"station_id": 60008611,
|
||||
"name": "Leva II - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008602,
|
||||
"name": "Leva XI - Moon 8 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005040": [
|
||||
{
|
||||
"station_id": 60008608,
|
||||
"name": "Nishah VII - Moon 5 - Emperor Family Treasury"
|
||||
}
|
||||
],
|
||||
"30005043": [
|
||||
{
|
||||
"station_id": 60008605,
|
||||
"name": "Nakregde VII - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005052": [
|
||||
{
|
||||
"station_id": 60008554,
|
||||
"name": "Soumi V - Moon 4 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008557,
|
||||
"name": "Soumi I - Moon 1 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008563,
|
||||
"name": "Soumi VII - Moon 1 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005054": [
|
||||
{
|
||||
"station_id": 60008560,
|
||||
"name": "Nare VI - Moon 16 - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005199": [
|
||||
{
|
||||
"station_id": 60012739,
|
||||
"name": "Tar III - Secure Commerce Commission Depository"
|
||||
}
|
||||
],
|
||||
"30005204": [
|
||||
{
|
||||
"station_id": 60012736,
|
||||
"name": "Yulai III - Moon 1 - Secure Commerce Commission Depository"
|
||||
}
|
||||
],
|
||||
"30005275": [
|
||||
{
|
||||
"station_id": 60008596,
|
||||
"name": "Azedi III - Emperor Family Bureau"
|
||||
}
|
||||
],
|
||||
"30005276": [
|
||||
{
|
||||
"station_id": 60008599,
|
||||
"name": "Sharza VII - Moon 3 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008590,
|
||||
"name": "Sharza VII - Moon 5 - Emperor Family Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60008593,
|
||||
"name": "Sharza VI - Moon 4 - Emperor Family Bureau"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,839 +0,0 @@
|
||||
{
|
||||
"type_id": 89219,
|
||||
"generated_at": "2026-01-30T13:54:13.047Z",
|
||||
"bands": [
|
||||
"high",
|
||||
"low"
|
||||
],
|
||||
"system_ids_by_band": {
|
||||
"high": [
|
||||
30000009,
|
||||
30000010,
|
||||
30000024,
|
||||
30000025,
|
||||
30000030,
|
||||
30000051,
|
||||
30000052,
|
||||
30000084,
|
||||
30000087,
|
||||
30000201,
|
||||
30000202,
|
||||
30001380,
|
||||
30001384,
|
||||
30001644,
|
||||
30001646,
|
||||
30001669,
|
||||
30001671,
|
||||
30001674,
|
||||
30001689,
|
||||
30001690,
|
||||
30001693,
|
||||
30002724,
|
||||
30002762,
|
||||
30002763,
|
||||
30002764,
|
||||
30002766,
|
||||
30003058,
|
||||
30003374,
|
||||
30003375,
|
||||
30003376,
|
||||
30003378,
|
||||
30003428,
|
||||
30003429,
|
||||
30003430,
|
||||
30003431,
|
||||
30003432,
|
||||
30002242,
|
||||
30002259,
|
||||
30002260,
|
||||
30002262,
|
||||
30003859,
|
||||
30003860,
|
||||
30003861,
|
||||
30003862,
|
||||
30004248,
|
||||
30004249,
|
||||
30004250,
|
||||
30004251,
|
||||
30004253,
|
||||
30005069,
|
||||
30005078,
|
||||
30005198,
|
||||
30005199,
|
||||
30005200,
|
||||
30005204,
|
||||
30005205,
|
||||
30005206,
|
||||
30005315,
|
||||
30005319,
|
||||
30005322,
|
||||
30005323
|
||||
],
|
||||
"low": [
|
||||
30000012,
|
||||
30000014,
|
||||
30000015,
|
||||
30000205,
|
||||
30001385,
|
||||
30002402,
|
||||
30002404,
|
||||
30002406,
|
||||
30002407,
|
||||
30002414,
|
||||
30002419,
|
||||
30002420,
|
||||
30002725,
|
||||
30002726,
|
||||
30002728,
|
||||
30002730,
|
||||
30003057,
|
||||
30003059,
|
||||
30003061,
|
||||
30002060,
|
||||
30002062,
|
||||
30002065,
|
||||
30002246,
|
||||
30002249,
|
||||
30003818,
|
||||
30003819,
|
||||
30004280,
|
||||
30004281,
|
||||
30004284,
|
||||
30005079,
|
||||
30005080,
|
||||
30005328
|
||||
]
|
||||
},
|
||||
"system_stations": {
|
||||
"30000009": [
|
||||
{
|
||||
"station_id": 60012295,
|
||||
"name": "Sooma X - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30000010": [
|
||||
{
|
||||
"station_id": 60012304,
|
||||
"name": "Chidah V - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012307,
|
||||
"name": "Chidah VIII - Moon 17 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000012": [
|
||||
{
|
||||
"station_id": 60012292,
|
||||
"name": "Asabona IX - Moon 5 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30000014": [
|
||||
{
|
||||
"station_id": 60012298,
|
||||
"name": "Shamahi IX - Moon 12 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30000015": [
|
||||
{
|
||||
"station_id": 60012301,
|
||||
"name": "Sendaya V - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30000024": [
|
||||
{
|
||||
"station_id": 60013027,
|
||||
"name": "Kiereend VII - Moon 3 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000025": [
|
||||
{
|
||||
"station_id": 60013030,
|
||||
"name": "Rashy VI - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000030": [
|
||||
{
|
||||
"station_id": 60013024,
|
||||
"name": "Kasrasi IX - Moon 7 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000051": [
|
||||
{
|
||||
"station_id": 60012949,
|
||||
"name": "Juddi VII - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30000052": [
|
||||
{
|
||||
"station_id": 60012943,
|
||||
"name": "Maspah V - Moon 6 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012946,
|
||||
"name": "Maspah IV - Moon 7 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000084": [
|
||||
{
|
||||
"station_id": 60013012,
|
||||
"name": "Asghatil IX - Moon 3 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000087": [
|
||||
{
|
||||
"station_id": 60013006,
|
||||
"name": "Gelhan V - Moon 10 - DED Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60013009,
|
||||
"name": "Gelhan V - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000201": [
|
||||
{
|
||||
"station_id": 60012313,
|
||||
"name": "Uchoshi I - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012322,
|
||||
"name": "Uchoshi IX - Moon 2 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000202": [
|
||||
{
|
||||
"station_id": 60012325,
|
||||
"name": "Mastakomon IX - Moon 2 - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60013042,
|
||||
"name": "Mastakomon IX - Moon 3 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012310,
|
||||
"name": "Mastakomon V - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012316,
|
||||
"name": "Mastakomon IX - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012319,
|
||||
"name": "Mastakomon VIII - Moon 1 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60013048,
|
||||
"name": "Mastakomon XI - Moon 2 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30000205": [
|
||||
{
|
||||
"station_id": 60013045,
|
||||
"name": "Obe VI - Moon 2 - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30001380": [
|
||||
{
|
||||
"station_id": 60012328,
|
||||
"name": "Vellaine V - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30001384": [
|
||||
{
|
||||
"station_id": 60012331,
|
||||
"name": "Autaris VIII - Moon 5 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012340,
|
||||
"name": "Autaris IV - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012343,
|
||||
"name": "Autaris I - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001385": [
|
||||
{
|
||||
"station_id": 60012334,
|
||||
"name": "Jan VI - Moon 21 - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30001644": [
|
||||
{
|
||||
"station_id": 60013033,
|
||||
"name": "Tividu IV - Moon 10 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60013036,
|
||||
"name": "Tividu IV - Moon 3 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001646": [
|
||||
{
|
||||
"station_id": 60013039,
|
||||
"name": "Goram VII - Moon 4 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001669": [
|
||||
{
|
||||
"station_id": 60012982,
|
||||
"name": "Pimebeka VII - Moon 13 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30001671": [
|
||||
{
|
||||
"station_id": 60012985,
|
||||
"name": "Tash-Murkon Prime III - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001674": [
|
||||
{
|
||||
"station_id": 60012979,
|
||||
"name": "Hilaban II - Moon 5 - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30001689": [
|
||||
{
|
||||
"station_id": 60012964,
|
||||
"name": "Asesamy VI - Moon 8 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001690": [
|
||||
{
|
||||
"station_id": 60012967,
|
||||
"name": "Hostni VII - Moon 18 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30001693": [
|
||||
{
|
||||
"station_id": 60012961,
|
||||
"name": "Perdan VI - Moon 16 - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002060": [
|
||||
{
|
||||
"station_id": 60012970,
|
||||
"name": "Evati IX - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002062": [
|
||||
{
|
||||
"station_id": 60012976,
|
||||
"name": "Todifrauan VII - Moon 8 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002065": [
|
||||
{
|
||||
"station_id": 60012973,
|
||||
"name": "Lasleinur VI - Moon 17 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002242": [
|
||||
{
|
||||
"station_id": 60012937,
|
||||
"name": "Mamenkhanar IX - Moon 11 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002246": [
|
||||
{
|
||||
"station_id": 60012940,
|
||||
"name": "Neziel I - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002249": [
|
||||
{
|
||||
"station_id": 60012934,
|
||||
"name": "Ruchy I - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002259": [
|
||||
{
|
||||
"station_id": 60012958,
|
||||
"name": "Sahdil III - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002260": [
|
||||
{
|
||||
"station_id": 60012952,
|
||||
"name": "Esteban VIII - Moon 4 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002262": [
|
||||
{
|
||||
"station_id": 60012955,
|
||||
"name": "Nalu VII - Moon 7 - DED Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002402": [
|
||||
{
|
||||
"station_id": 60012457,
|
||||
"name": "Istodard IX - Moon 5 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012460,
|
||||
"name": "Istodard IX - Moon 16 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002404": [
|
||||
{
|
||||
"station_id": 60012469,
|
||||
"name": "Half VII - Moon 4 - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012454,
|
||||
"name": "Half VII - Moon 1 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002406": [
|
||||
{
|
||||
"station_id": 60012463,
|
||||
"name": "Hedaleolfarber VII - Moon 17 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002407": [
|
||||
{
|
||||
"station_id": 60012466,
|
||||
"name": "Altbrard IX - Moon 8 - CONCORD Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30002414": [
|
||||
{
|
||||
"station_id": 60012430,
|
||||
"name": "Klingt VIII - CONCORD Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60012433,
|
||||
"name": "Klingt IX - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012418,
|
||||
"name": "Klingt III - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002419": [
|
||||
{
|
||||
"station_id": 60012421,
|
||||
"name": "Aeditide V - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002420": [
|
||||
{
|
||||
"station_id": 60012424,
|
||||
"name": "Egbinger XII - CONCORD Academy"
|
||||
},
|
||||
{
|
||||
"station_id": 60012427,
|
||||
"name": "Egbinger V - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002724": [
|
||||
{
|
||||
"station_id": 60012502,
|
||||
"name": "Assiettes IV - Moon 1 - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002725": [
|
||||
{
|
||||
"station_id": 60012499,
|
||||
"name": "Goinard III - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012490,
|
||||
"name": "Goinard IV - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002726": [
|
||||
{
|
||||
"station_id": 60012505,
|
||||
"name": "Raeghoscon VIII - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30002728": [
|
||||
{
|
||||
"station_id": 60012496,
|
||||
"name": "Lermireve VIII - Moon 15 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002730": [
|
||||
{
|
||||
"station_id": 60012493,
|
||||
"name": "Esmes IV - Moon 2 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30002762": [
|
||||
{
|
||||
"station_id": 60012511,
|
||||
"name": "Yashunen VII - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012514,
|
||||
"name": "Yashunen VII - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002763": [
|
||||
{
|
||||
"station_id": 60012523,
|
||||
"name": "Tennen VIII - Moon 4 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30002764": [
|
||||
{
|
||||
"station_id": 60012517,
|
||||
"name": "Hatakani VI - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012508,
|
||||
"name": "Hatakani VI - Moon 10 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30002766": [
|
||||
{
|
||||
"station_id": 60012520,
|
||||
"name": "Iivinen VIII - Moon 10 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003057": [
|
||||
{
|
||||
"station_id": 60012373,
|
||||
"name": "Groothese X - Moon 13 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003058": [
|
||||
{
|
||||
"station_id": 60012367,
|
||||
"name": "Olide VI - Moon 10 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012370,
|
||||
"name": "Olide VI - Moon 14 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012379,
|
||||
"name": "Olide IV - Moon 7 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003059": [
|
||||
{
|
||||
"station_id": 60012364,
|
||||
"name": "Adeel VIII - Moon 1 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30003061": [
|
||||
{
|
||||
"station_id": 60012376,
|
||||
"name": "Mormelot I - CONCORD Testing Facilities"
|
||||
}
|
||||
],
|
||||
"30003374": [
|
||||
{
|
||||
"station_id": 60012355,
|
||||
"name": "Arlulf III - Moon 10 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012358,
|
||||
"name": "Arlulf VI - Moon 1 - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012361,
|
||||
"name": "Arlulf III - Moon 11 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003375": [
|
||||
{
|
||||
"station_id": 60012346,
|
||||
"name": "Brundakur IV - Moon 1 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003376": [
|
||||
{
|
||||
"station_id": 60012349,
|
||||
"name": "Stirht VII - Moon 14 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003378": [
|
||||
{
|
||||
"station_id": 60012352,
|
||||
"name": "Nedegulf VII - Moon 4 - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30003428": [
|
||||
{
|
||||
"station_id": 60012286,
|
||||
"name": "Hilfhurmur VIII - Moon 6 - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30003429": [
|
||||
{
|
||||
"station_id": 60012277,
|
||||
"name": "Geffur VII - Moon 8 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003430": [
|
||||
{
|
||||
"station_id": 60012289,
|
||||
"name": "Oppold III - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003431": [
|
||||
{
|
||||
"station_id": 60012280,
|
||||
"name": "Tratokard II - Moon 1 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003432": [
|
||||
{
|
||||
"station_id": 60012283,
|
||||
"name": "Lumegen III - CONCORD Academy"
|
||||
},
|
||||
{
|
||||
"station_id": 60012274,
|
||||
"name": "Lumegen IV - Moon 2 - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30003818": [
|
||||
{
|
||||
"station_id": 60012439,
|
||||
"name": "Aulbres X - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012442,
|
||||
"name": "Aulbres VII - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012445,
|
||||
"name": "Aulbres X - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012448,
|
||||
"name": "Aulbres VII - Moon 16 - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012451,
|
||||
"name": "Aulbres V - Moon 4 - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30003819": [
|
||||
{
|
||||
"station_id": 60012436,
|
||||
"name": "Barleguet IV - Moon 2 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003859": [
|
||||
{
|
||||
"station_id": 60012403,
|
||||
"name": "Neyi VII - Moon 7 - CONCORD Academy"
|
||||
}
|
||||
],
|
||||
"30003860": [
|
||||
{
|
||||
"station_id": 60012406,
|
||||
"name": "Kihtaled VIII - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012409,
|
||||
"name": "Kihtaled VIII - Moon 17 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30003861": [
|
||||
{
|
||||
"station_id": 60012400,
|
||||
"name": "Ipref IV - Moon 5 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012415,
|
||||
"name": "Ipref II - Moon 1 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30003862": [
|
||||
{
|
||||
"station_id": 60012412,
|
||||
"name": "Agil VI - Moon 2 - CONCORD Logistic Support"
|
||||
}
|
||||
],
|
||||
"30004248": [
|
||||
{
|
||||
"station_id": 60012382,
|
||||
"name": "Haimeh IX - Moon 16 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30004249": [
|
||||
{
|
||||
"station_id": 60012394,
|
||||
"name": "Avada V - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30004250": [
|
||||
{
|
||||
"station_id": 60012391,
|
||||
"name": "Chibi VI - Moon 15 - CONCORD Treasury"
|
||||
}
|
||||
],
|
||||
"30004251": [
|
||||
{
|
||||
"station_id": 60012397,
|
||||
"name": "Mishi VIII - CONCORD Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012385,
|
||||
"name": "Mishi VII - Moon 4 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30004253": [
|
||||
{
|
||||
"station_id": 60012388,
|
||||
"name": "Pahineh V - Moon 1 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30004280": [
|
||||
{
|
||||
"station_id": 60013003,
|
||||
"name": "Nalnifan IV - Moon 2 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30004281": [
|
||||
{
|
||||
"station_id": 60013000,
|
||||
"name": "Jerhesh VI - Moon 11 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30004284": [
|
||||
{
|
||||
"station_id": 60012997,
|
||||
"name": "Defsunun IV - Moon 1 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005069": [
|
||||
{
|
||||
"station_id": 60013015,
|
||||
"name": "Nahol X - Moon 2 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60013018,
|
||||
"name": "Nahol II - Moon 1 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60013021,
|
||||
"name": "Nahol IV - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005078": [
|
||||
{
|
||||
"station_id": 60012484,
|
||||
"name": "Keproh VIII - Moon 3 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005079": [
|
||||
{
|
||||
"station_id": 60012478,
|
||||
"name": "Zatamaka VII - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012481,
|
||||
"name": "Zatamaka X - Moon 2 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012472,
|
||||
"name": "Zatamaka XI - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30005080": [
|
||||
{
|
||||
"station_id": 60012475,
|
||||
"name": "Rannoze V - Moon 8 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012487,
|
||||
"name": "Rannoze VII - Moon 2 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005198": [
|
||||
{
|
||||
"station_id": 60012265,
|
||||
"name": "Pakhshi IX - Moon 20 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30005199": [
|
||||
{
|
||||
"station_id": 60012739,
|
||||
"name": "Tar III - Secure Commerce Commission Depository"
|
||||
}
|
||||
],
|
||||
"30005200": [
|
||||
{
|
||||
"station_id": 60012259,
|
||||
"name": "Tekaima I - Moon 1 - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012268,
|
||||
"name": "Tekaima V - Moon 9 - CONCORD Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005204": [
|
||||
{
|
||||
"station_id": 60012271,
|
||||
"name": "Yulai VIII - Moon 10 - CONCORD Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60012736,
|
||||
"name": "Yulai III - Moon 1 - Secure Commerce Commission Depository"
|
||||
},
|
||||
{
|
||||
"station_id": 60012916,
|
||||
"name": "Yulai VIII - Moon 12 - DED Logistic Support"
|
||||
},
|
||||
{
|
||||
"station_id": 60012256,
|
||||
"name": "Yulai IX (Kjarval) - CONCORD Bureau"
|
||||
},
|
||||
{
|
||||
"station_id": 60012922,
|
||||
"name": "Yulai X - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005205": [
|
||||
{
|
||||
"station_id": 60012919,
|
||||
"name": "Tarta IX - Moon 14 - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005206": [
|
||||
{
|
||||
"station_id": 60012262,
|
||||
"name": "Kemerk V - Moon 10 - CONCORD Bureau"
|
||||
}
|
||||
],
|
||||
"30005315": [
|
||||
{
|
||||
"station_id": 60012994,
|
||||
"name": "Eletta VII - Moon 7 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30005319": [
|
||||
{
|
||||
"station_id": 60012988,
|
||||
"name": "Raneilles V - Moon 2 - DED Assembly Plant"
|
||||
},
|
||||
{
|
||||
"station_id": 60012991,
|
||||
"name": "Raneilles III - DED Assembly Plant"
|
||||
}
|
||||
],
|
||||
"30005322": [
|
||||
{
|
||||
"station_id": 60012931,
|
||||
"name": "Scolluzer VI - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30005323": [
|
||||
{
|
||||
"station_id": 60012925,
|
||||
"name": "Sortet VI - Moon 5 - DED Logistic Support"
|
||||
}
|
||||
],
|
||||
"30005328": [
|
||||
{
|
||||
"station_id": 60012928,
|
||||
"name": "Reblier VIII - Moon 7 - DED Logistic Support"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
283
test/wanderer_app/map/map_position_calculator_test.exs
Normal file
283
test/wanderer_app/map/map_position_calculator_test.exs
Normal file
@@ -0,0 +1,283 @@
|
||||
defmodule WandererApp.Map.PositionCalculatorTest do
|
||||
use ExUnit.Case, async: true
|
||||
alias WandererApp.Map.PositionCalculator
|
||||
|
||||
test "layout_systems rearranges systems" do
|
||||
systems = [
|
||||
%{solar_system_id: 1, position_x: 0, position_y: 0},
|
||||
%{solar_system_id: 2, position_x: 10, position_y: 10},
|
||||
%{solar_system_id: 3, position_x: -10, position_y: -10}
|
||||
]
|
||||
|
||||
connections = [
|
||||
%{id: "1-2", solar_system_source: 1, solar_system_target: 2},
|
||||
%{id: "1-3", solar_system_source: 1, solar_system_target: 3}
|
||||
]
|
||||
|
||||
{updated_systems, _cross_list_ids} = PositionCalculator.layout_systems(systems, connections, [])
|
||||
|
||||
assert length(updated_systems) == 3
|
||||
|
||||
# Sort by ID to compare
|
||||
updated_1 = Enum.find(updated_systems, & &1.solar_system_id == 1)
|
||||
updated_2 = Enum.find(updated_systems, & &1.solar_system_id == 2)
|
||||
updated_3 = Enum.find(updated_systems, & &1.solar_system_id == 3)
|
||||
|
||||
# Node 1 is root (layer 0)
|
||||
# Node 2 and 3 are in layer 1
|
||||
assert updated_1.position_x < updated_2.position_x
|
||||
assert updated_1.position_x < updated_3.position_x
|
||||
assert updated_2.position_x == updated_3.position_x
|
||||
|
||||
# Vertically centered: node 2 and 3 should be above/below each other
|
||||
assert updated_2.position_y != updated_3.position_y
|
||||
end
|
||||
|
||||
test "layout_systems prevents overlaps even with locked systems" do
|
||||
systems = [
|
||||
%{solar_system_id: 1, position_x: 0, position_y: 0, locked: true},
|
||||
%{solar_system_id: 2, position_x: 0, position_y: 0, locked: true}, # Locked at same spot!
|
||||
%{solar_system_id: 3, position_x: 100, position_y: 100}
|
||||
]
|
||||
|
||||
connections = [
|
||||
%{id: "1-3", solar_system_source: 1, solar_system_target: 3}
|
||||
]
|
||||
|
||||
{updated_systems, _cross_list_ids} = PositionCalculator.layout_systems(systems, connections, [])
|
||||
|
||||
# Check for overlaps
|
||||
# A system [x, x+130], [y, y+34]
|
||||
for s1 <- updated_systems, s2 <- updated_systems, s1.solar_system_id < s2.solar_system_id do
|
||||
assert not overlap?(s1, s2), "Systems #{s1.solar_system_id} and #{s2.solar_system_id} overlap"
|
||||
end
|
||||
end
|
||||
|
||||
defp overlap?(s1, s2) do
|
||||
w = 130
|
||||
h = 34
|
||||
# Horizontal overlap
|
||||
x_overlap = s1.position_x < s2.position_x + w and s1.position_x + w > s2.position_x
|
||||
# Vertical overlap
|
||||
y_overlap = s1.position_y < s2.position_y + h and s1.position_y + h > s2.position_y
|
||||
|
||||
x_overlap and y_overlap
|
||||
end
|
||||
|
||||
test "layout_systems correctly handles multiple roots in a component" do
|
||||
# System 1 and 2 are connected via 3, both 1 and 2 are locked (roots)
|
||||
systems = [
|
||||
%{solar_system_id: 1, position_x: 0, position_y: 0, locked: true, name: "A-Root"},
|
||||
%{solar_system_id: 2, position_x: 0, position_y: 500, locked: true, name: "B-Root"},
|
||||
%{solar_system_id: 3, position_x: 100, position_y: 100, name: "C-Node"}
|
||||
]
|
||||
|
||||
connections = [
|
||||
%{id: "1-3", solar_system_source: 1, solar_system_target: 3},
|
||||
%{id: "2-3", solar_system_source: 2, solar_system_target: 3}
|
||||
]
|
||||
|
||||
{updated_systems, _cross_list_ids} = PositionCalculator.layout_systems(systems, connections, [])
|
||||
|
||||
updated_1 = Enum.find(updated_systems, & &1.solar_system_id == 1)
|
||||
updated_2 = Enum.find(updated_systems, & &1.solar_system_id == 2)
|
||||
|
||||
assert updated_1.position_y == 0
|
||||
# Root 2 (B-Root) should be shifted below Root 1's subtree
|
||||
assert updated_2.position_y > updated_1.position_y
|
||||
end
|
||||
|
||||
test "layout_systems skips layout for systems involved in cross-list connections" do
|
||||
# System 1 is root, connected to 3.
|
||||
# System 2 is root, connected to 4.
|
||||
# Connection (3, 4) is a cross-list connection.
|
||||
# Systems 1, 3, 2, 4 should keep original positions because of the bridge.
|
||||
systems = [
|
||||
%{solar_system_id: 1, position_x: 100, position_y: 100, locked: true, name: "Root-A"},
|
||||
%{solar_system_id: 2, position_x: 500, position_y: 500, locked: true, name: "Root-B"},
|
||||
%{solar_system_id: 3, position_x: 200, position_y: 200, name: "Node-A3"},
|
||||
%{solar_system_id: 4, position_x: 600, position_y: 600, name: "Node-B4"}
|
||||
]
|
||||
|
||||
connections = [
|
||||
%{id: "conn-1-3", solar_system_source: 1, solar_system_target: 3},
|
||||
%{id: "conn-2-4", solar_system_source: 2, solar_system_target: 4},
|
||||
%{id: "cross-bridge", solar_system_source: 3, solar_system_target: 4}
|
||||
]
|
||||
|
||||
{updated_systems, cross_list_ids} = PositionCalculator.layout_systems(systems, connections, [])
|
||||
|
||||
# Either "cross-bridge" or "conn-2-4" (or even "conn-1-3" depending on order)
|
||||
# will be detected as cross-list because roots greedily claim nodes.
|
||||
assert not Enum.empty?(cross_list_ids)
|
||||
|
||||
for original <- systems do
|
||||
updated = Enum.find(updated_systems, & &1.solar_system_id == original.solar_system_id)
|
||||
assert updated.position_x == original.position_x
|
||||
assert updated.position_y == original.position_y
|
||||
end
|
||||
end
|
||||
|
||||
test "layout_systems prioritizes older connections for root claims" do
|
||||
# Root A connects to S (NEW).
|
||||
# Root B connects to S (OLD).
|
||||
# S should stay with Root B subtree.
|
||||
old_time = ~U[2020-01-01 00:00:00Z]
|
||||
new_time = ~U[2024-01-01 00:00:00Z]
|
||||
|
||||
systems = [
|
||||
%{solar_system_id: 1, position_x: 0, position_y: 0, locked: true, name: "Root-Hek"},
|
||||
%{solar_system_id: 2, position_x: 0, position_y: 500, locked: true, name: "Root-J220546"},
|
||||
%{solar_system_id: 3, position_x: 100, position_y: 200, name: "System-S"}
|
||||
]
|
||||
|
||||
connections = [
|
||||
%{id: "hek-s", solar_system_source: 1, solar_system_target: 3, inserted_at: new_time},
|
||||
%{id: "j-s", solar_system_source: 2, solar_system_target: 3, inserted_at: old_time}
|
||||
]
|
||||
|
||||
{_updated, cross_list_ids} = PositionCalculator.layout_systems(systems, connections, [])
|
||||
|
||||
# "hek-s" should be the cross-list connection because "j-s" was older and claimed S first.
|
||||
assert "hek-s" in cross_list_ids
|
||||
end
|
||||
|
||||
test "layout_systems treats nil inserted_at as newer than existing connections" do
|
||||
old_time = ~U[2020-01-01 00:00:00Z]
|
||||
|
||||
systems = [
|
||||
%{solar_system_id: 1, position_x: 0, position_y: 0, locked: true, name: "Root-Hek"},
|
||||
%{solar_system_id: 2, position_x: 0, position_y: 500, locked: true, name: "Root-J220546"},
|
||||
%{solar_system_id: 3, position_x: 100, position_y: 200, name: "System-S"}
|
||||
]
|
||||
|
||||
connections = [
|
||||
%{id: "hek-s", solar_system_source: 1, solar_system_target: 3, inserted_at: nil},
|
||||
%{id: "j-s", solar_system_source: 2, solar_system_target: 3, inserted_at: old_time}
|
||||
]
|
||||
|
||||
{_updated, cross_list_ids} = PositionCalculator.layout_systems(systems, connections, [])
|
||||
|
||||
# "hek-s" with nil should be treated as new, so "j-s" (old) wins the claim for S.
|
||||
# Therefore, hek-s is the cross-list connection.
|
||||
assert "hek-s" in cross_list_ids
|
||||
end
|
||||
|
||||
test "layout_systems generates hierarchical names" do
|
||||
# Root (1)
|
||||
# -> Child (2)
|
||||
# -> Grandchild (4)
|
||||
# -> Child (3)
|
||||
systems = [
|
||||
%{solar_system_id: 1, name: "Root", locked: true, position_x: 0, position_y: 0},
|
||||
%{solar_system_id: 2, name: "A-Child", position_x: 0, position_y: 0},
|
||||
%{solar_system_id: 3, name: "B-Child", position_x: 0, position_y: 0},
|
||||
%{solar_system_id: 4, name: "A-Grandchild", position_x: 0, position_y: 0}
|
||||
]
|
||||
|
||||
connections = [
|
||||
%{id: "1-2", solar_system_source: 1, solar_system_target: 2, inserted_at: ~U[2020-01-01 00:00:00Z]},
|
||||
%{id: "1-3", solar_system_source: 1, solar_system_target: 3, inserted_at: ~U[2020-01-01 00:00:00Z]},
|
||||
%{id: "2-4", solar_system_source: 2, solar_system_target: 4, inserted_at: ~U[2020-01-01 00:00:00Z]}
|
||||
]
|
||||
|
||||
{updated_systems, _cross_list_ids} = PositionCalculator.layout_systems(systems, connections, [])
|
||||
|
||||
# Sort children by name: A-Child (index 1), B-Child (index 2)
|
||||
# Root -> "0"
|
||||
# A-Child -> "1"
|
||||
# B-Child -> "2"
|
||||
# A-Grandchild -> "1-1"
|
||||
|
||||
s1 = Enum.find(updated_systems, & &1.solar_system_id == 1)
|
||||
s2 = Enum.find(updated_systems, & &1.solar_system_id == 2)
|
||||
s3 = Enum.find(updated_systems, & &1.solar_system_id == 3)
|
||||
s4 = Enum.find(updated_systems, & &1.solar_system_id == 4)
|
||||
|
||||
assert s1.hierarchical_name == "0"
|
||||
assert s2.hierarchical_name == "1"
|
||||
assert s3.hierarchical_name == "2"
|
||||
assert s4.hierarchical_name == "1-1"
|
||||
end
|
||||
|
||||
test "layout_systems aligns multiple locked roots to the same X axis" do
|
||||
systems = [
|
||||
%{solar_system_id: 1, name: "Root-A", locked: true, position_x: 100, position_y: 100},
|
||||
%{solar_system_id: 2, name: "Root-B", locked: true, position_x: 500, position_y: 500}
|
||||
]
|
||||
|
||||
# No connections, so they are independent roots
|
||||
{updated_systems, _cross_list_ids} = PositionCalculator.layout_systems(systems, [], [])
|
||||
|
||||
s1 = Enum.find(updated_systems, & &1.solar_system_id == 1)
|
||||
s2 = Enum.find(updated_systems, & &1.solar_system_id == 2)
|
||||
|
||||
# Both should be forced to X = 0.0 (the root axis)
|
||||
assert s1.position_x == 0
|
||||
assert s2.position_x == 0
|
||||
end
|
||||
|
||||
test "layout_systems top_to_bottom anchors roots to Y axis and arranges children vertically" do
|
||||
# Root (1)
|
||||
# -> Child (2)
|
||||
systems = [
|
||||
%{solar_system_id: 1, name: "Root", locked: true, position_x: 100, position_y: 100},
|
||||
%{solar_system_id: 2, name: "Child", position_x: 0, position_y: 0}
|
||||
]
|
||||
|
||||
connections = [
|
||||
%{id: "1-2", solar_system_source: 1, solar_system_target: 2, inserted_at: ~U[2020-01-01 00:00:00Z]}
|
||||
]
|
||||
|
||||
# Use top_to_bottom layout
|
||||
{updated_systems, _cross_list_ids} = PositionCalculator.layout_systems(systems, connections, [layout: "top_to_bottom"])
|
||||
|
||||
s1 = Enum.find(updated_systems, & &1.solar_system_id == 1)
|
||||
s2 = Enum.find(updated_systems, & &1.solar_system_id == 2)
|
||||
|
||||
# Root should be forced to Y = 0
|
||||
assert s1.position_y == 0
|
||||
# Child should be below root (Y = s1.y + @h + @m_y)
|
||||
# @h = 34, @m_y = 41 -> s2.y = 0 + 34 + 41 = 75
|
||||
assert s2.position_y == 75
|
||||
# Since there's only one root at (0, 0), the child should have same X (0)
|
||||
assert s2.position_x == 0
|
||||
end
|
||||
|
||||
test "layout_systems detects cycles and excludes affected subtrees from layout" do
|
||||
# Root (1) -> Child (2) -> Grandchild (3) -> Root (1) [Cycle]
|
||||
systems = [
|
||||
%{solar_system_id: 1, name: "Root", locked: true, position_x: 0, position_y: 0},
|
||||
%{solar_system_id: 2, name: "Child", position_x: 500, position_y: 500},
|
||||
%{solar_system_id: 3, name: "Grandchild", position_x: 1000, position_y: 1000}
|
||||
]
|
||||
|
||||
connections = [
|
||||
%{id: "1-2", solar_system_source: 1, solar_system_target: 2, inserted_at: ~U[2020-01-01 00:00:00Z]},
|
||||
%{id: "2-3", solar_system_source: 2, solar_system_target: 3, inserted_at: ~U[2020-01-01 00:00:00Z]},
|
||||
%{id: "3-1", solar_system_source: 3, solar_system_target: 1, inserted_at: ~U[2020-01-01 00:00:00Z]}
|
||||
]
|
||||
|
||||
{updated_systems, special_ids} = PositionCalculator.layout_systems(systems, connections, [])
|
||||
|
||||
# The 3-1 connection completes the cycle and should be identified as special
|
||||
assert "3-1" in special_ids
|
||||
|
||||
# Since the root (1) is part of a special connection, its entire subtree should be skipped
|
||||
s1 = Enum.find(updated_systems, & &1.solar_system_id == 1)
|
||||
s2 = Enum.find(updated_systems, & &1.solar_system_id == 2)
|
||||
s3 = Enum.find(updated_systems, & &1.solar_system_id == 3)
|
||||
|
||||
assert s1.position_x == 0
|
||||
assert s1.position_y == 0
|
||||
assert s2.position_x == 500
|
||||
assert s2.position_y == 500
|
||||
assert s3.position_x == 1000
|
||||
assert s3.position_y == 1000
|
||||
|
||||
# Ensure hierarchical_name is NOT added when layout is skipped due to cycle
|
||||
refute Map.has_key?(s1, :hierarchical_name)
|
||||
refute Map.has_key?(s2, :hierarchical_name)
|
||||
refute Map.has_key?(s3, :hierarchical_name)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user