Compare commits

..

18 Commits

Author SHA1 Message Date
CI
363330f3d1 chore: release version v1.5.0 2024-10-11 09:06:20 +00:00
Dmitry Popov
fbf9c5ddd6 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-11 13:05:52 +04:00
Dmitry Popov
fbf2ee314c feat(Map): Follow Character on Map and auto select their current system
fixes #34
2024-10-11 13:05:48 +04:00
CI
c9f83fb419 chore: release version v1.4.0 2024-10-11 08:12:17 +00:00
Dmitry Popov
9737d91e16 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-11 12:11:38 +04:00
Dmitry Popov
2f672ae970 feat(Map): Follow Character on Map and auto select their current system
fixes #34
2024-10-11 12:11:33 +04:00
CI
25339546c6 chore: release version v1.3.6 2024-10-09 21:44:05 +00:00
Dmitry Popov
912cad42ac Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-10 01:43:36 +04:00
Dmitry Popov
b3752c8d8f fix(Signatures): Signatures update fixes
fixes #25
2024-10-10 01:43:32 +04:00
CI
e8a11333f2 chore: release version v1.3.5 2024-10-09 13:41:38 +00:00
Dmitry Popov
8bb6d09e6e fix(Signatures): Signatures update fixes
fixes #25
2024-10-09 17:41:02 +04:00
CI
56bf955297 chore: release version v1.3.4 2024-10-09 09:24:17 +00:00
Dmitry Popov
ef6c08dfe8 Refactoring (#27)
* chore: release version v1.3.1

* fix(Core): Add system "true security" correction

* chore: release version v1.3.1
2024-10-09 13:23:35 +04:00
CI
495c3e1cd7 chore: release version v1.3.3 2024-10-08 07:30:12 +00:00
Dmitry Popov
9a5fe3d744 chore: release version v1.3.2 2024-10-08 11:29:36 +04:00
CI
72607cae4d chore: release version v1.3.2 2024-10-07 19:34:37 +00:00
Dmitry Popov
4891cdb04d 19 add map custom options (#24)
* feat(Core): Support map default layout option
2024-10-07 23:33:52 +04:00
CI
d214881720 chore: release version v1.3.1 2024-10-07 09:58:06 +00:00
69 changed files with 2033 additions and 2697 deletions

View File

@@ -2,6 +2,62 @@
<!-- changelog --> <!-- changelog -->
## [v1.5.0](https://github.com/wanderer-industries/wanderer/compare/v1.4.0...v1.5.0) (2024-10-11)
### Features:
* Map: Follow Character on Map and auto select their current system
## [v1.4.0](https://github.com/wanderer-industries/wanderer/compare/v1.3.6...v1.4.0) (2024-10-11)
### Features:
* Map: Follow Character on Map and auto select their current system
## [v1.3.6](https://github.com/wanderer-industries/wanderer/compare/v1.3.5...v1.3.6) (2024-10-09)
### Bug Fixes:
* Signatures: Signatures update fixes
## [v1.3.5](https://github.com/wanderer-industries/wanderer/compare/v1.3.4...v1.3.5) (2024-10-09)
### Bug Fixes:
* Signatures: Signatures update fixes
## [v1.3.4](https://github.com/wanderer-industries/wanderer/compare/v1.3.3...v1.3.4) (2024-10-09)
## [v1.3.3](https://github.com/wanderer-industries/wanderer/compare/v1.3.2...v1.3.3) (2024-10-08)
## [v1.3.2](https://github.com/wanderer-industries/wanderer/compare/v1.3.1...v1.3.2) (2024-10-07)
## [v1.3.1](https://github.com/wanderer-industries/wanderer/compare/v1.3.0...v1.3.1) (2024-10-07)
## [v1.3.0](https://github.com/wanderer-industries/wanderer/compare/v1.2.10...v1.3.0) (2024-10-07) ## [v1.3.0](https://github.com/wanderer-industries/wanderer/compare/v1.2.10...v1.3.0) (2024-10-07)

View File

@@ -28,6 +28,12 @@ body {
font-weight: 500; font-weight: 500;
} }
#bg-canvas {
position: absolute;
width: 100vw;
height: 100vh;
}
.ccp-font { .ccp-font {
font-family: 'Shentox', 'Rogan', sans-serif !important; font-family: 'Shentox', 'Rogan', sans-serif !important;
} }

View File

@@ -11,7 +11,7 @@ const Characters = ({ data }: { data: CharacterTypeRaw[] }) => {
const handleSelect = useCallback( const handleSelect = useCallback(
(character: CharacterTypeRaw) => { (character: CharacterTypeRaw) => {
mapRef.current?.command(Commands.selectSystem, character?.location?.solar_system_id?.toString()); mapRef.current?.command(Commands.centerSystem, character?.location?.solar_system_id?.toString());
}, },
[mapRef], [mapRef],
); );

View File

@@ -48,19 +48,19 @@ export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand, mapRef }: U
}, []); }, []);
const onAddSystem = useCallback(() => { const onAddSystem = useCallback(() => {
const { system, outCommand, mapRef } = ref.current; const { system: solarSystemId, outCommand, mapRef } = ref.current;
if (!system) { if (!solarSystemId) {
return; return;
} }
outCommand({ outCommand({
type: OutCommand.addSystem, type: OutCommand.addSystem,
data: { data: {
system_id: system, system_id: solarSystemId,
}, },
}); });
setTimeout(() => { setTimeout(() => {
mapRef.current?.command(Commands.selectSystem, system); mapRef.current?.command(Commands.centerSystem, solarSystemId);
setSystem(undefined); setSystem(undefined);
}, 200); }, 200);
}, []); }, []);

View File

@@ -5,5 +5,6 @@ export * from './useMapRemoveSystems';
export * from './useCommandsCharacters'; export * from './useCommandsCharacters';
export * from './useCommandsConnections'; export * from './useCommandsConnections';
export * from './useCommandsConnections'; export * from './useCommandsConnections';
export * from './useCenterSystem';
export * from './useSelectSystem'; export * from './useSelectSystem';
export * from './useMapCommands'; export * from './useMapCommands';

View File

@@ -0,0 +1,18 @@
import { useReactFlow } from 'reactflow';
import { useCallback } from 'react';
import { CommandCenterSystem } from '@/hooks/Mapper/types';
export const useCenterSystem = () => {
const rf = useReactFlow();
return useCallback((systemId: CommandCenterSystem) => {
if (!rf) {
return;
}
const systemNode = rf.getNodes().find(x => x.data.id === systemId);
if (!systemNode) {
return;
}
rf.setCenter(systemNode.position.x, systemNode.position.y, { duration: 1000 });
}, []);
};

View File

@@ -1,21 +1,21 @@
import { useReactFlow } from 'reactflow'; import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react'; import { useCallback } from 'react';
import { CommandSelectSystem } from '@/hooks/Mapper/types'; import { CommandSelectSystem } from '@/hooks/Mapper/types';
export const useSelectSystem = () => { export const useSelectSystem = () => {
const rf = useReactFlow(); const rf = useReactFlow();
const ref = useRef({ rf });
ref.current = { rf };
return useCallback((systemId: CommandSelectSystem) => { return useCallback((systemId: CommandSelectSystem) => {
if (!ref.current?.rf) { if (!rf) {
return; return;
} }
const systemNode = ref.current.rf.getNodes().find(x => x.data.id === systemId); rf.setNodes(nds =>
if (!systemNode) { nds.map(node => {
return; return {
} ...node,
selected: node.id === systemId,
ref.current.rf.setCenter(systemNode.position.x, systemNode.position.y, { duration: 1000 }); };
}),
);
}, []); }, []);
}; };

View File

@@ -1,4 +1,4 @@
import { ForwardedRef, useImperativeHandle } from 'react'; import { ForwardedRef, useImperativeHandle, useRef } from 'react';
import { import {
CommandAddConnections, CommandAddConnections,
CommandAddSystems, CommandAddSystems,
@@ -26,6 +26,7 @@ import {
useMapInit, useMapInit,
useMapRemoveSystems, useMapRemoveSystems,
useMapUpdateSystems, useMapUpdateSystems,
useCenterSystem,
useSelectSystem, useSelectSystem,
} from './api'; } from './api';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts'; import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
@@ -35,8 +36,12 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
const mapAddSystems = useMapAddSystems(); const mapAddSystems = useMapAddSystems();
const mapUpdateSystems = useMapUpdateSystems(); const mapUpdateSystems = useMapUpdateSystems();
const removeSystems = useMapRemoveSystems(onSelectionChange); const removeSystems = useMapRemoveSystems(onSelectionChange);
const centerSystem = useCenterSystem();
const selectSystem = useSelectSystem(); const selectSystem = useSelectSystem();
const selectRef = useRef({ onSelectionChange });
selectRef.current = { onSelectionChange };
const { addConnections, removeConnections, updateConnection } = useCommandsConnections(); const { addConnections, removeConnections, updateConnection } = useCommandsConnections();
const { mapUpdated, killsUpdated } = useMapCommands(); const { mapUpdated, killsUpdated } = useMapCommands();
const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } = const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } =
@@ -91,8 +96,22 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
killsUpdated(data as CommandKillsUpdated); killsUpdated(data as CommandKillsUpdated);
break; break;
case Commands.centerSystem:
setTimeout(() => {
const systemId = `${data}`;
centerSystem(systemId as CommandSelectSystem);
}, 100);
break;
case Commands.selectSystem: case Commands.selectSystem:
selectSystem(data as CommandSelectSystem); setTimeout(() => {
const systemId = `${data}`;
selectRef.current.onSelectionChange({
systems: [systemId],
connections: [],
});
selectSystem(systemId as CommandSelectSystem);
}, 100);
break; break;
case Commands.routes: case Commands.routes:

View File

@@ -91,7 +91,7 @@ export const RoutesList = ({ data, onContextMenu }: RoutesListProps) => {
const { mapRef } = useMapRootState(); const { mapRef } = useMapRootState();
const handleClick = useCallback( const handleClick = useCallback(
(systemId: number) => mapRef.current?.command(Commands.selectSystem, systemId.toString()), (systemId: number) => mapRef.current?.command(Commands.centerSystem, systemId.toString()),
[mapRef], [mapRef],
); );

View File

@@ -48,6 +48,8 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
const [signatures, setSignatures, signaturesRef] = useRefState<SystemSignature[]>([]); const [signatures, setSignatures, signaturesRef] = useRefState<SystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<SystemSignature[]>([]); const [selectedSignatures, setSelectedSignatures] = useState<SystemSignature[]>([]);
const [nameColumnWidth, setNameColumnWidth] = useState('auto'); const [nameColumnWidth, setNameColumnWidth] = useState('auto');
const [parsedSignatures, setParsedSignatures] = useState<SystemSignature[]>([]);
const [askUser, setAskUser] = useState(false);
const [hoveredSig, setHoveredSig] = useState<SystemSignature | null>(null); const [hoveredSig, setHoveredSig] = useState<SystemSignature | null>(null);
@@ -86,12 +88,33 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
data: { system_id: systemId }, data: { system_id: systemId },
}); });
setAskUser(false);
setSignatures(signatures); setSignatures(signatures);
}, [outCommand, systemId]); }, [outCommand, systemId]);
const updateSignatures = useCallback(
async (newSignatures: SystemSignature[], updateOnly: boolean) => {
const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures, updateOnly);
const { signatures: updatedSignatures } = await outCommand({
type: OutCommand.updateSignatures,
data: {
system_id: systemId,
added,
updated,
removed,
},
});
setSignatures(() => updatedSignatures);
setSelectedSignatures([]);
},
[outCommand, systemId],
);
const handleUpdateSignatures = useCallback( const handleUpdateSignatures = useCallback(
async (newSignatures: SystemSignature[]) => { async (newSignatures: SystemSignature[], updateOnly: boolean) => {
const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures); const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures, updateOnly);
const { signatures: updatedSignatures } = await outCommand({ const { signatures: updatedSignatures } = await outCommand({
type: OutCommand.updateSignatures, type: OutCommand.updateSignatures,
@@ -114,13 +137,26 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
return; return;
} }
const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id); const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id);
await handleUpdateSignatures(signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id))); await handleUpdateSignatures(
signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)),
false,
);
}, [handleUpdateSignatures, signatures, selectedSignatures]); }, [handleUpdateSignatures, signatures, selectedSignatures]);
const handleSelectAll = useCallback(() => { const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures); setSelectedSignatures(signatures);
}, [signatures]); }, [signatures]);
const handleReplaceAll = useCallback(() => {
handleUpdateSignatures(parsedSignatures, false);
setAskUser(false);
}, [parsedSignatures, handleUpdateSignatures]);
const handleUpdateOnly = useCallback(() => {
handleUpdateSignatures(parsedSignatures, true);
setAskUser(false);
}, [parsedSignatures, handleUpdateSignatures]);
useHotkey(true, ['a'], handleSelectAll); useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected); useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected);
@@ -130,17 +166,25 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
return; return;
} }
const signatures = parseSignatures( const newSignatures = parseSignatures(
clipboardContent, clipboardContent,
settings.map(x => x.key), settings.map(x => x.key),
); );
handleUpdateSignatures(signatures); const { removed } = getActualSigs(signaturesRef.current, newSignatures, false);
if (!signaturesRef.current || !signaturesRef.current.length || !removed.length) {
handleUpdateSignatures(newSignatures, false);
} else {
setParsedSignatures(newSignatures);
setAskUser(true);
}
}, [clipboardContent]); }, [clipboardContent]);
useEffect(() => { useEffect(() => {
if (!systemId) { if (!systemId) {
setSignatures([]); setSignatures([]);
setAskUser(false);
return; return;
} }
@@ -184,98 +228,120 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
// }; // };
return ( return (
<div ref={tableRef} className="h-full"> <>
{filteredSignatures.length === 0 ? ( <div ref={tableRef} className={'h-full '}>
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm"> {filteredSignatures.length === 0 ? (
No signatures <div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
</div> No signatures
) : ( </div>
<> ) : (
<DataTable <>
className={classes.Table} <DataTable
value={filteredSignatures} className={classes.Table}
size="small" value={filteredSignatures}
selectionMode="multiple" size="small"
selection={selectedSignatures} selectionMode="multiple"
metaKeySelection selection={selectedSignatures}
onSelectionChange={e => setSelectedSignatures(e.value)} metaKeySelection
dataKey="eve_id" onSelectionChange={e => setSelectedSignatures(e.value)}
tableClassName="w-full select-none" dataKey="eve_id"
resizableColumns={false} tableClassName="w-full select-none"
rowHover resizableColumns={false}
selectAll rowHover
sortField={sortSettings.sortField} selectAll
sortOrder={sortSettings.sortOrder} sortField={sortSettings.sortField}
onSort={event => setSortSettings(() => ({ sortField: event.sortField, sortOrder: event.sortOrder }))} sortOrder={sortSettings.sortOrder}
onRowMouseEnter={compact || medium ? handleEnterRow : undefined} onSort={event => setSortSettings(() => ({ sortField: event.sortField, sortOrder: event.sortOrder }))}
onRowMouseLeave={compact || medium ? handleLeaveRow : undefined} onRowMouseEnter={compact || medium ? handleEnterRow : undefined}
rowClassName={row => { onRowMouseLeave={compact || medium ? handleLeaveRow : undefined}
if (selectedSignatures.some(x => x.eve_id === row.eve_id)) { rowClassName={row => {
return clsx(classes.TableRowCompact, 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200'); if (selectedSignatures.some(x => x.eve_id === row.eve_id)) {
} return clsx(classes.TableRowCompact, 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200');
}
const dateClass = getRowColorByTimeLeft(row.updated_at ? new Date(row.updated_at) : undefined); const dateClass = getRowColorByTimeLeft(row.updated_at ? new Date(row.updated_at) : undefined);
if (!dateClass) { if (!dateClass) {
return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200'); return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200');
} }
return clsx(classes.TableRowCompact, dateClass); return clsx(classes.TableRowCompact, dateClass);
}} }}
> >
<Column <Column
bodyClassName="p-0 px-1" bodyClassName="p-0 px-1"
field="group" field="group"
body={renderIcon} body={renderIcon}
style={{ maxWidth: 26, minWidth: 26, width: 26, height: 25 }} style={{ maxWidth: 26, minWidth: 26, width: 26, height: 25 }}
></Column> ></Column>
<Column <Column
field="eve_id" field="eve_id"
header="Id" header="Id"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 72, minWidth: 72, width: 72 }} style={{ maxWidth: 72, minWidth: 72, width: 72 }}
sortable sortable
></Column> ></Column>
<Column <Column
field="group" field="group"
header="Group" header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={compact} hidden={compact}
sortable sortable
></Column> ></Column>
<Column <Column
field="name" field="name"
header="Name" header="Name"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
body={renderName} body={renderName}
style={{ maxWidth: nameColumnWidth }} style={{ maxWidth: nameColumnWidth }}
hidden={compact || medium} hidden={compact || medium}
sortable sortable
></Column> ></Column>
<Column <Column
field="updated_at" field="updated_at"
header="Updated" header="Updated"
dataType="date" dataType="date"
bodyClassName="w-[80px] text-ellipsis overflow-hidden whitespace-nowrap" bodyClassName="w-[80px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderTimeLeft} body={renderTimeLeft}
sortable sortable
></Column> ></Column>
{/*<Column*/} {/*<Column*/}
{/* bodyClassName="p-0 pl-1 pr-2"*/} {/* bodyClassName="p-0 pl-1 pr-2"*/}
{/* field="group"*/} {/* field="group"*/}
{/* body={renderToolbar}*/} {/* body={renderToolbar}*/}
{/* headerClassName={headerClasses}*/} {/* headerClassName={headerClasses}*/}
{/* style={{ maxWidth: 26, minWidth: 26, width: 26 }}*/} {/* style={{ maxWidth: 26, minWidth: 26, width: 26 }}*/}
{/*></Column>*/} {/*></Column>*/}
</DataTable> </DataTable>
</> </>
)} )}
<WdTooltip <WdTooltip
className="bg-stone-900/95 text-slate-50" className="bg-stone-900/95 text-slate-50"
ref={tooltipRef} ref={tooltipRef}
content={hoveredSig ? <SignatureView {...hoveredSig} /> : null} content={hoveredSig ? <SignatureView {...hoveredSig} /> : null}
/> />
</div> {askUser && (
<div className="absolute left-[1px] top-[29px] h-[calc(100%-30px)] w-[calc(100%-3px)] bg-stone-900/10 backdrop-blur-sm">
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center">
<div className="text-stone-400/80 text-sm">
<div className="flex flex-col text-center gap-2">
<button className="p-button p-component p-button-outlined p-button-sm btn-wide">
<span className="p-button-label p-c" onClick={handleUpdateOnly}>
Update
</span>
</button>
<button className="p-button p-component p-button-outlined p-button-sm btn-wide">
<span className="p-button-label p-c" onClick={handleReplaceAll}>
Update & Delete
</span>
</button>
</div>
</div>
</div>
</div>
)}
</div>
</>
); );
}; };

View File

@@ -5,6 +5,7 @@ import { getState } from './getState.ts';
export const getActualSigs = ( export const getActualSigs = (
oldSignatures: SystemSignature[], oldSignatures: SystemSignature[],
newSignatures: SystemSignature[], newSignatures: SystemSignature[],
updateOnly: boolean,
): { added: SystemSignature[]; updated: SystemSignature[]; removed: SystemSignature[] } => { ): { added: SystemSignature[]; updated: SystemSignature[]; removed: SystemSignature[] } => {
const updated: SystemSignature[] = []; const updated: SystemSignature[] = [];
const removed: SystemSignature[] = []; const removed: SystemSignature[] = [];
@@ -20,7 +21,9 @@ export const getActualSigs = (
updated.push({ ...oldSig, group: newSig.group, name: newSig.name }); updated.push({ ...oldSig, group: newSig.group, name: newSig.name });
} }
} else { } else {
removed.push(oldSig); if (!updateOnly) {
removed.push(oldSig);
}
} }
}); });

View File

@@ -7,9 +7,9 @@ export const getState = (_: string[], newSig: SystemSignature) => {
let state = -1; let state = -1;
if (!newSig.group || newSig.group === '') { if (!newSig.group || newSig.group === '') {
state = 0; state = 0;
} else if (!!newSig.group && newSig.group !== '' && newSig.name === '') { } else if (!newSig.name || newSig.name === '') {
state = 1; state = 1;
} else if (!!newSig.group && newSig.group !== '' && newSig.name !== '') { } else if (newSig.name !== '') {
state = 2; state = 2;
} }
return state; return state;

View File

@@ -6,6 +6,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { OnTheMap, RightBar } from '@/hooks/Mapper/components/mapRootContent/components'; import { OnTheMap, RightBar } from '@/hooks/Mapper/components/mapRootContent/components';
import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/components/MapContextMenu/MapContextMenu.tsx'; import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/components/MapContextMenu/MapContextMenu.tsx';
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
export interface MapRootContentProps {} export interface MapRootContentProps {}
@@ -19,6 +20,8 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []); const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
useSkipContextMenu();
return ( return (
<Layout map={<MapWrapper refn={mapRef} />}> <Layout map={<MapWrapper refn={mapRef} />}>
{!isShowMenu ? ( {!isShowMenu ? (

View File

@@ -22,6 +22,13 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
}); });
}, [outCommand]); }, [outCommand]);
const handleOpenUserSettings = useCallback(() => {
outCommand({
type: OutCommand.openUserSettings,
data: null,
});
}, [outCommand]);
const toggleMinimap = useCallback(() => { const toggleMinimap = useCallback(() => {
setInterfaceSettings(x => ({ setInterfaceSettings(x => ({
...x, ...x,
@@ -63,6 +70,16 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
</button> </button>
</WdTooltipWrapper> </WdTooltipWrapper>
<WdTooltipWrapper content="User settings" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={handleOpenUserSettings}
>
<i className="pi pi-cog text-lg"></i>
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}> <WdTooltipWrapper content="Show on the map" position={TooltipPosition.left}>
<button <button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto" className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
@@ -87,9 +104,9 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
onClick={toggleKSpace} onClick={toggleKSpace}
> >
{interfaceSettings.isShowKSpace ? ( {interfaceSettings.isShowKSpace ? (
<i className="pi pi-star-fill text-lg"></i> <i className="pi pi-heart-fill text-lg"></i>
) : ( ) : (
<i className="pi pi-star text-lg"></i> <i className="pi pi-heart text-lg"></i>
)} )}
</button> </button>
</WdTooltipWrapper> </WdTooltipWrapper>

View File

@@ -37,7 +37,7 @@ export const CharacterCard = ({
const { mapRef } = useMapRootState(); const { mapRef } = useMapRootState();
const handleSelect = useCallback(() => { const handleSelect = useCallback(() => {
mapRef.current?.command(Commands.selectSystem, char?.location?.solar_system_id?.toString()); mapRef.current?.command(Commands.centerSystem, char?.location?.solar_system_id?.toString());
}, [mapRef, char]); }, [mapRef, char]);
return ( return (

View File

@@ -1,3 +1,4 @@
export * from './usePageVisibility'; export * from './usePageVisibility';
export * from './useClipboard'; export * from './useClipboard';
export * from './useHotkey'; export * from './useHotkey';
export * from './useSkipContextMenu';

View File

@@ -0,0 +1,15 @@
import { useEffect } from 'react';
export const useSkipContextMenu = () => {
useEffect(() => {
function handleContextMenu(e) {
e.preventDefault();
}
window.addEventListener(`contextmenu`, handleContextMenu);
return () => {
window.removeEventListener(`contextmenu`, handleContextMenu);
};
}, []);
};

View File

@@ -1,6 +1,5 @@
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import Mapper from './MapRoot'; import Mapper from './MapRoot';
import { decompressToJson } from './utils';
export default { export default {
_rootEl: null, _rootEl: null,
@@ -28,17 +27,12 @@ export default {
handleEventWrapper(event: string, handler: (payload: any) => void) { handleEventWrapper(event: string, handler: (payload: any) => void) {
this.handleEvent(event, (body: any) => { this.handleEvent(event, (body: any) => {
if (event === 'map_event') { handler(body);
const { type, body: data } = body;
handler({ type, body: decompressToJson(data) });
} else {
handler(body);
}
}); });
}, },
reconnected() { reconnected() {
this.pushEvent('reconnected'); this.pushEvent('ui_loaded');
}, },
async pushEventAsync(event: string, payload: any) { async pushEventAsync(event: string, payload: any) {

View File

@@ -4,7 +4,6 @@ import { MapHandlers, MapUnionTypes, OutCommandHandler, SolarSystemConnection }
import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks'; import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
import { WithChildren } from '@/hooks/Mapper/types/common.ts'; import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import useLocalStorageState from 'use-local-storage-state'; import useLocalStorageState from 'use-local-storage-state';
import { DEFAULT_SETTINGS } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
export type MapRootData = MapUnionTypes & { export type MapRootData = MapUnionTypes & {
selectedSystems: string[]; selectedSystems: string[];

View File

@@ -85,6 +85,10 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
mapRoutes(data as CommandRoutes); mapRoutes(data as CommandRoutes);
break; break;
case Commands.centerSystem:
// do nothing here
break;
case Commands.selectSystem: case Commands.selectSystem:
// do nothing here // do nothing here
break; break;

View File

@@ -21,6 +21,7 @@ export enum Commands {
mapUpdated = 'map_updated', mapUpdated = 'map_updated',
killsUpdated = 'kills_updated', killsUpdated = 'kills_updated',
routes = 'routes', routes = 'routes',
centerSystem = 'center_system',
selectSystem = 'select_system', selectSystem = 'select_system',
} }
@@ -40,7 +41,8 @@ export type Command =
| Commands.mapUpdated | Commands.mapUpdated
| Commands.killsUpdated | Commands.killsUpdated
| Commands.routes | Commands.routes
| Commands.selectSystem; | Commands.selectSystem
| Commands.centerSystem;
export type CommandInit = { export type CommandInit = {
systems: SolarSystemRawType[]; systems: SolarSystemRawType[];
@@ -72,6 +74,7 @@ export type CommandMapUpdated = Partial<CommandInit>;
export type CommandRoutes = RoutesList; export type CommandRoutes = RoutesList;
export type CommandKillsUpdated = Kill[]; export type CommandKillsUpdated = Kill[];
export type CommandSelectSystem = string | undefined; export type CommandSelectSystem = string | undefined;
export type CommandCenterSystem = string | undefined;
export interface CommandData { export interface CommandData {
[Commands.init]: CommandInit; [Commands.init]: CommandInit;
@@ -90,6 +93,7 @@ export interface CommandData {
[Commands.routes]: CommandRoutes; [Commands.routes]: CommandRoutes;
[Commands.killsUpdated]: CommandKillsUpdated; [Commands.killsUpdated]: CommandKillsUpdated;
[Commands.selectSystem]: CommandSelectSystem; [Commands.selectSystem]: CommandSelectSystem;
[Commands.centerSystem]: CommandCenterSystem;
} }
export interface MapHandlers { export interface MapHandlers {
@@ -123,6 +127,7 @@ export enum OutCommand {
setAutopilotWaypoint = 'set_autopilot_waypoint', setAutopilotWaypoint = 'set_autopilot_waypoint',
addSystem = 'add_system', addSystem = 'add_system',
addCharacter = 'add_character', addCharacter = 'add_character',
openUserSettings = 'open_user_settings',
getPassages = 'get_passages', getPassages = 'get_passages',
// Only UI commands // Only UI commands

View File

@@ -1,14 +0,0 @@
import pako from 'pako';
export const decompressToJson = (base64string: string) => {
const base64_decoded = atob(base64string);
const charData = base64_decoded.split('').map(function (x) {
return x.charCodeAt(0);
});
const zlibData = new Uint8Array(charData);
const inflatedData = pako.inflate(zlibData, {
to: 'string',
});
return JSON.parse(inflatedData);
};

View File

@@ -1,3 +1,2 @@
export * from './contextStore'; export * from './contextStore';
export * from './decompressToJson';
export * from './getQueryVariable'; export * from './getQueryVariable';

View File

@@ -3,7 +3,230 @@ import 'phoenix_html';
import './live_reload.css'; import './live_reload.css';
const animateBg = function (bgCanvas) {
const { TweenMax, _ } = window;
/**
* Utility function for returning a random integer in a given range
* @param {Int} max
* @param {Int} min
*/
const randomInRange = (max, min) => Math.floor(Math.random() * (max - min + 1)) + min;
const BASE_SIZE = 1;
const VELOCITY_INC = 1.01;
const VELOCITY_INIT_INC = 0.525;
const JUMP_VELOCITY_INC = 0.55;
const JUMP_SIZE_INC = 1.15;
const SIZE_INC = 1.01;
const RAD = Math.PI / 180;
const WARP_COLORS = [
[197, 239, 247],
[25, 181, 254],
[77, 5, 232],
[165, 55, 253],
[255, 255, 255],
];
/**
* Class for storing the particle metadata
* position, size, length, speed etc.
*/
class Star {
STATE = {
alpha: Math.random(),
angle: randomInRange(0, 360) * RAD,
};
reset = () => {
const angle = randomInRange(0, 360) * (Math.PI / 180);
const vX = Math.cos(angle);
const vY = Math.sin(angle);
const travelled =
Math.random() > 0.5
? Math.random() * Math.max(window.innerWidth, window.innerHeight) + Math.random() * (window.innerWidth * 0.24)
: Math.random() * (window.innerWidth * 0.25);
this.STATE = {
...this.STATE,
iX: undefined,
iY: undefined,
active: travelled ? true : false,
x: Math.floor(vX * travelled) + window.innerWidth / 2,
vX,
y: Math.floor(vY * travelled) + window.innerHeight / 2,
vY,
size: BASE_SIZE,
};
};
constructor() {
this.reset();
}
}
const generateStarPool = size => new Array(size).fill().map(() => new Star());
// Class for the actual app
// Not too much happens in here
// Initiate the drawing process and listen for user interactions 👍
class JumpToHyperspace {
STATE = {
stars: generateStarPool(300),
bgAlpha: 0,
sizeInc: SIZE_INC,
velocity: VELOCITY_INC,
};
canvas = null;
context = null;
constructor(canvas) {
this.canvas = canvas;
this.context = canvas.getContext('2d');
this.bind();
this.setup();
this.render();
}
render = () => {
const {
STATE: { bgAlpha, velocity, sizeInc, initiating, jumping, stars },
context,
render,
} = this;
// Clear the canvas
context.clearRect(0, 0, window.innerWidth, window.innerHeight);
if (bgAlpha > 0) {
context.fillStyle = `rgba(31, 58, 157, ${bgAlpha})`;
context.fillRect(0, 0, window.innerWidth, window.innerHeight);
}
// 1. Shall we add a new star
const nonActive = stars.filter(s => !s.STATE.active);
if (!initiating && nonActive.length > 0) {
// Introduce a star
nonActive[0].STATE.active = true;
}
// 2. Update the stars and draw them.
for (const star of stars.filter(s => s.STATE.active)) {
const { active, x, y, iX, iY, iVX, iVY, size, vX, vY } = star.STATE;
// Check if the star needs deactivating
if (
((iX || x) < 0 || (iX || x) > window.innerWidth || (iY || y) < 0 || (iY || y) > window.innerHeight) &&
active &&
!initiating
) {
star.reset(true);
} else if (active) {
const newIX = initiating ? iX : iX + iVX;
const newIY = initiating ? iY : iY + iVY;
const newX = x + vX;
const newY = y + vY;
// Just need to work out if it overtakes the original line that's all
const caught =
(vX < 0 && newIX < x) || (vX > 0 && newIX > x) || (vY < 0 && newIY < y) || (vY > 0 && newIY > y);
star.STATE = {
...star.STATE,
iX: caught ? undefined : newIX,
iY: caught ? undefined : newIY,
iVX: caught ? undefined : iVX * VELOCITY_INIT_INC,
iVY: caught ? undefined : iVY * VELOCITY_INIT_INC,
x: newX,
vX: star.STATE.vX * velocity,
y: newY,
vY: star.STATE.vY * velocity,
size: initiating ? size : size * (iX || iY ? SIZE_INC : sizeInc),
};
let color = `rgba(255, 255, 255, ${star.STATE.alpha})`;
if (jumping) {
const [r, g, b] = WARP_COLORS[randomInRange(0, WARP_COLORS.length)];
color = `rgba(${r}, ${g}, ${b}, ${star.STATE.alpha})`;
}
context.strokeStyle = color;
context.lineWidth = size;
context.beginPath();
context.moveTo(star.STATE.iX || x, star.STATE.iY || y);
context.lineTo(star.STATE.x, star.STATE.y);
context.stroke();
}
}
requestAnimationFrame(render);
};
initiate = () => {
if (this.STATE.jumping || this.STATE.initiating) return;
this.STATE = {
...this.STATE,
initiating: true,
initiateTimestamp: new Date().getTime(),
};
TweenMax.to(this.STATE, 0.25, { velocity: VELOCITY_INIT_INC, bgAlpha: 0.3 });
// When we initiate, stop the XY origin from moving so that we draw
// longer lines until the jump
for (const star of this.STATE.stars.filter(s => s.STATE.active)) {
star.STATE = {
...star.STATE,
iX: star.STATE.x,
iY: star.STATE.y,
iVX: star.STATE.vX,
iVY: star.STATE.vY,
};
}
};
jump = () => {
this.STATE = {
...this.STATE,
bgAlpha: 0,
jumping: true,
};
TweenMax.to(this.STATE, 0.25, { velocity: JUMP_VELOCITY_INC, bgAlpha: 0.75, sizeInc: JUMP_SIZE_INC });
setTimeout(() => {
this.STATE = {
...this.STATE,
jumping: false,
};
TweenMax.to(this.STATE, 0.25, { bgAlpha: 0, velocity: VELOCITY_INC, sizeInc: SIZE_INC });
}, 5000);
};
enter = () => {
if (this.STATE.jumping) return;
const { initiateTimestamp } = this.STATE;
this.STATE = {
...this.STATE,
initiating: false,
initiateTimestamp: undefined,
};
if (new Date().getTime() - initiateTimestamp > 600) {
this.jump();
} else {
TweenMax.to(this.STATE, 0.25, { velocity: VELOCITY_INC, bgAlpha: 0 });
}
};
bind = () => {
this.canvas.addEventListener('mousedown', this.initiate);
this.canvas.addEventListener('touchstart', this.initiate);
this.canvas.addEventListener('mouseup', this.enter);
this.canvas.addEventListener('touchend', this.enter);
};
setup = () => {
this.context.lineCap = 'round';
this.canvas.height = window.innerHeight;
this.canvas.width = window.innerWidth;
};
reset = () => {
this.STATE = {
...this.STATE,
stars: generateStarPool(300),
};
this.setup();
};
}
window.myJump = new JumpToHyperspace(bgCanvas);
window.addEventListener(
'resize',
_.debounce(() => {
window.myJump.reset();
}, 250),
);
};
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// animage background
const canvas = document.getElementById('bg-canvas');
if (canvas) {
animateBg(canvas);
}
// Select all buttons with the 'share-link' class // Select all buttons with the 'share-link' class
const buttons = document.querySelectorAll('button.copy-link'); const buttons = document.querySelectorAll('button.copy-link');

View File

@@ -21,7 +21,6 @@
"live_select": "file:../deps/live_select", "live_select": "file:../deps/live_select",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"pako": "^2.1.0",
"phoenix": "file:../deps/phoenix", "phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html", "phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view", "phoenix_live_view": "file:../deps/phoenix_live_view",
@@ -44,7 +43,6 @@
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"@types/lodash.debounce": "^4.0.9", "@types/lodash.debounce": "^4.0.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/pako": "^2.0.3",
"@types/react": "18.2.0", "@types/react": "18.2.0",
"@types/react-dom": "18.2.1", "@types/react-dom": "18.2.1",
"@types/react-grid-layout": "^1.3.4", "@types/react-grid-layout": "^1.3.4",

View File

@@ -33,7 +33,7 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz"
integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ== integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.14.8", "@babel/core@^7.24.5": "@babel/core@^7.14.8", "@babel/core@^7.24.5":
version "7.24.5" version "7.24.5"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz" resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz"
integrity sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA== integrity sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==
@@ -240,11 +240,121 @@
"@babel/helper-validator-identifier" "^7.24.5" "@babel/helper-validator-identifier" "^7.24.5"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@esbuild/aix-ppc64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537"
integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==
"@esbuild/android-arm64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9"
integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==
"@esbuild/android-arm@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995"
integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==
"@esbuild/android-x64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98"
integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==
"@esbuild/darwin-arm64@0.20.2": "@esbuild/darwin-arm64@0.20.2":
version "0.20.2" version "0.20.2"
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz"
integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==
"@esbuild/darwin-x64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0"
integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==
"@esbuild/freebsd-arm64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911"
integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==
"@esbuild/freebsd-x64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c"
integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==
"@esbuild/linux-arm64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5"
integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==
"@esbuild/linux-arm@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c"
integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==
"@esbuild/linux-ia32@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa"
integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==
"@esbuild/linux-loong64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5"
integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==
"@esbuild/linux-mips64el@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa"
integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==
"@esbuild/linux-ppc64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20"
integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==
"@esbuild/linux-riscv64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300"
integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==
"@esbuild/linux-s390x@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685"
integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==
"@esbuild/linux-x64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff"
integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==
"@esbuild/netbsd-x64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6"
integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==
"@esbuild/openbsd-x64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf"
integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==
"@esbuild/sunos-x64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f"
integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==
"@esbuild/win32-arm64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90"
integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==
"@esbuild/win32-ia32@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23"
integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==
"@esbuild/win32-x64@0.20.2":
version "0.20.2"
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc"
integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0" version "4.4.0"
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz"
@@ -341,7 +451,7 @@
"@nodelib/fs.stat" "2.0.5" "@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9" run-parallel "^1.1.9"
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5" version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -359,7 +469,7 @@
resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz" resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz"
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
"@react-rxjs/core@^0.10.7", "@react-rxjs/core@>=0.1.0": "@react-rxjs/core@^0.10.7":
version "0.10.7" version "0.10.7"
resolved "https://registry.npmjs.org/@react-rxjs/core/-/core-0.10.7.tgz" resolved "https://registry.npmjs.org/@react-rxjs/core/-/core-0.10.7.tgz"
integrity sha512-dornp8pUs9OcdqFKKRh9+I2FVe21gWufNun6RYU1ddts7kUy9i4Thvl0iqcPFbGY61cJQMAJF7dxixWMSD/A/A== integrity sha512-dornp8pUs9OcdqFKKRh9+I2FVe21gWufNun6RYU1ddts7kUy9i4Thvl0iqcPFbGY61cJQMAJF7dxixWMSD/A/A==
@@ -455,11 +565,91 @@
estree-walker "^2.0.2" estree-walker "^2.0.2"
picomatch "^2.3.1" picomatch "^2.3.1"
"@rollup/rollup-android-arm-eabi@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz#1a32112822660ee104c5dd3a7c595e26100d4c2d"
integrity sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==
"@rollup/rollup-android-arm64@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz#5aeef206d65ff4db423f3a93f71af91b28662c5b"
integrity sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==
"@rollup/rollup-darwin-arm64@4.17.2": "@rollup/rollup-darwin-arm64@4.17.2":
version "4.17.2" version "4.17.2"
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz" resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz"
integrity sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw== integrity sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==
"@rollup/rollup-darwin-x64@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz#f64fc51ed12b19f883131ccbcea59fc68cbd6c0b"
integrity sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==
"@rollup/rollup-linux-arm-gnueabihf@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz#1a7641111be67c10111f7122d1e375d1226cbf14"
integrity sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==
"@rollup/rollup-linux-arm-musleabihf@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz#c93fd632923e0fee25aacd2ae414288d0b7455bb"
integrity sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==
"@rollup/rollup-linux-arm64-gnu@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz#fa531425dd21d058a630947527b4612d9d0b4a4a"
integrity sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==
"@rollup/rollup-linux-arm64-musl@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz#8acc16f095ceea5854caf7b07e73f7d1802ac5af"
integrity sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==
"@rollup/rollup-linux-powerpc64le-gnu@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz#94e69a8499b5cf368911b83a44bb230782aeb571"
integrity sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==
"@rollup/rollup-linux-riscv64-gnu@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz#7ef1c781c7e59e85a6ce261cc95d7f1e0b56db0f"
integrity sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==
"@rollup/rollup-linux-s390x-gnu@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz#f15775841c3232fca9b78cd25a7a0512c694b354"
integrity sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==
"@rollup/rollup-linux-x64-gnu@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz#b521d271798d037ad70c9f85dd97d25f8a52e811"
integrity sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==
"@rollup/rollup-linux-x64-gnu@4.9.5":
version "4.9.5"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz#85946ee4d068bd12197aeeec2c6f679c94978a49"
integrity sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==
"@rollup/rollup-linux-x64-musl@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz#9254019cc4baac35800991315d133cc9fd1bf385"
integrity sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==
"@rollup/rollup-win32-arm64-msvc@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz#27f65a89f6f52ee9426ec11e3571038e4671790f"
integrity sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==
"@rollup/rollup-win32-ia32-msvc@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz#a2fbf8246ed0bb014f078ca34ae6b377a90cb411"
integrity sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==
"@rollup/rollup-win32-x64-msvc@4.17.2":
version "4.17.2"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz#5a2d08b81e8064b34242d5cc9973ef8dd1e60503"
integrity sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==
"@rx-state/core@0.1.4": "@rx-state/core@0.1.4":
version "0.1.4" version "0.1.4"
resolved "https://registry.npmjs.org/@rx-state/core/-/core-0.1.4.tgz" resolved "https://registry.npmjs.org/@rx-state/core/-/core-0.1.4.tgz"
@@ -740,7 +930,7 @@
"@types/d3-transition" "*" "@types/d3-transition" "*"
"@types/d3-zoom" "*" "@types/d3-zoom" "*"
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@1.0.5": "@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0":
version "1.0.5" version "1.0.5"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
@@ -774,11 +964,6 @@
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz"
integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==
"@types/pako@^2.0.3":
version "2.0.3"
resolved "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz"
integrity sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.11" version "15.7.11"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz"
@@ -805,7 +990,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@^17.0.0 || ^18.0.0", "@types/react@>=16.8", "@types/react@18.2.0": "@types/react@*", "@types/react@18.2.0":
version "18.2.0" version "18.2.0"
resolved "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz" resolved "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz"
integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA== integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==
@@ -846,7 +1031,7 @@
semver "^7.5.4" semver "^7.5.4"
ts-api-utils "^1.0.1" ts-api-utils "^1.0.1"
"@typescript-eslint/parser@^6.0.0 || ^6.0.0-alpha", "@typescript-eslint/parser@^6.21.0": "@typescript-eslint/parser@^6.21.0":
version "6.21.0" version "6.21.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz"
integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==
@@ -947,7 +1132,7 @@ acorn-jsx@^5.3.2:
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.4.0, acorn@^8.9.0: acorn@^8.4.0, acorn@^8.9.0:
version "8.11.3" version "8.11.3"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
@@ -1147,7 +1332,7 @@ braces@^3.0.2, braces@~3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
browserslist@^4.22.2, browserslist@^4.23.0, "browserslist@>= 4.21.0": browserslist@^4.22.2, browserslist@^4.23.0:
version "4.23.0" version "4.23.0"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz"
integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==
@@ -1205,7 +1390,7 @@ child_process@^1.0.2:
resolved "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz" resolved "https://registry.npmjs.org/child_process/-/child_process-1.0.2.tgz"
integrity sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g== integrity sha512-Wmza/JzL0SiWz7kl6MhIKT5ceIlnFPJX+lwUGj7Clhy5MMldsSoJR0+uvRzOS5Kv45Mq7t1PoE8TsOA9bzvb6g==
chokidar@^3.3.0, chokidar@^3.5.3, "chokidar@>=3.0.0 <4.0.0": "chokidar@>=3.0.0 <4.0.0", chokidar@^3.3.0, chokidar@^3.5.3:
version "3.6.0" version "3.6.0"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
@@ -1258,16 +1443,16 @@ color-convert@^2.0.1:
dependencies: dependencies:
color-name "~1.1.4" color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-name@1.1.3: color-name@1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
commander@^4.0.0: commander@^4.0.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" resolved "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz"
@@ -1325,7 +1510,7 @@ culori@^3:
resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz" resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz"
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
d3-drag@^3.0.0, "d3-drag@2 - 3": "d3-drag@2 - 3", d3-drag@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz" resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz"
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
@@ -1345,7 +1530,7 @@ d3-drag@^3.0.0, "d3-drag@2 - 3":
dependencies: dependencies:
d3-color "1 - 3" d3-color "1 - 3"
d3-selection@^3.0.0, "d3-selection@2 - 3", d3-selection@3: "d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz"
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
@@ -1663,7 +1848,7 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-prettier@*, eslint-config-prettier@^9.1.0: eslint-config-prettier@^9.1.0:
version "9.1.0" version "9.1.0"
resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz" resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz"
integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==
@@ -1723,7 +1908,7 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
"eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.0.0 || ^8.0.0", eslint@^8.57.0, eslint@>=7, eslint@>=7.0.0, eslint@>=8.0.0: eslint@^8.57.0:
version "8.57.0" version "8.57.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz" resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz"
integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==
@@ -1795,12 +1980,7 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^2.0.1: estree-walker@^2.0.1, estree-walker@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
estree-walker@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
@@ -2010,18 +2190,6 @@ glob-parent@^6.0.2:
dependencies: dependencies:
is-glob "^4.0.3" is-glob "^4.0.3"
glob@^7.1.3:
version "7.2.3"
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.6: glob@7.1.6:
version "7.1.6" version "7.1.6"
resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz"
@@ -2034,6 +2202,18 @@ glob@7.1.6:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
glob@^7.1.3:
version "7.2.3"
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^11.1.0: globals@^11.1.0:
version "11.12.0" version "11.12.0"
resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz"
@@ -2141,14 +2321,7 @@ hasown@^2.0.0:
dependencies: dependencies:
function-bind "^1.1.2" function-bind "^1.1.2"
hasown@^2.0.1: hasown@^2.0.1, hasown@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
hasown@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
@@ -2420,7 +2593,7 @@ iterator.prototype@^1.1.2:
reflect.getprototypeof "^1.0.4" reflect.getprototypeof "^1.0.4"
set-function-name "^2.0.1" set-function-name "^2.0.1"
jiti@^1.19.1, jiti@>=1.21.0: jiti@^1.19.1:
version "1.21.0" version "1.21.0"
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz" resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
@@ -2518,7 +2691,6 @@ lines-and-columns@^1.1.6:
"live_select@file:../deps/live_select": "live_select@file:../deps/live_select":
version "1.4.2" version "1.4.2"
resolved "file:../deps/live_select"
locate-path@^6.0.0: locate-path@^6.0.0:
version "6.0.0" version "6.0.0"
@@ -2612,13 +2784,6 @@ mini-svg-data-uri@^1.2.3:
resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz" resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz"
integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimatch@9.0.3: minimatch@9.0.3:
version "9.0.3" version "9.0.3"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz"
@@ -2626,6 +2791,13 @@ minimatch@9.0.3:
dependencies: dependencies:
brace-expansion "^2.0.1" brace-expansion "^2.0.1"
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
ms@2.1.2: ms@2.1.2:
version "2.1.2" version "2.1.2"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
@@ -2770,11 +2942,6 @@ p-locate@^5.0.0:
dependencies: dependencies:
p-limit "^3.0.2" p-limit "^3.0.2"
pako@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
parent-module@^1.0.0: parent-module@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
@@ -2812,17 +2979,14 @@ path-type@^5.0.0:
resolved "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz" resolved "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz"
integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==
"phoenix@file:../deps/phoenix":
version "1.7.14"
"phoenix_html@file:../deps/phoenix_html": "phoenix_html@file:../deps/phoenix_html":
version "4.1.0" version "4.1.0"
resolved "file:../deps/phoenix_html"
"phoenix_live_view@file:../deps/phoenix_live_view": "phoenix_live_view@file:../deps/phoenix_live_view":
version "0.20.17" version "0.20.17"
resolved "file:../deps/phoenix_live_view"
"phoenix@file:../deps/phoenix":
version "1.7.14"
resolved "file:../deps/phoenix"
picocolors@^1, picocolors@^1.0.0: picocolors@^1, picocolors@^1.0.0:
version "1.0.0" version "1.0.0"
@@ -2923,14 +3087,6 @@ postcss-reporter@^7.0.0:
picocolors "^1.0.0" picocolors "^1.0.0"
thenby "^1.3.4" thenby "^1.3.4"
postcss-selector-parser@^6.0.11:
version "6.0.13"
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz"
integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-selector-parser@6.0.10: postcss-selector-parser@6.0.10:
version "6.0.10" version "6.0.10"
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz" resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz"
@@ -2939,12 +3095,20 @@ postcss-selector-parser@6.0.10:
cssesc "^3.0.0" cssesc "^3.0.0"
util-deprecate "^1.0.2" util-deprecate "^1.0.2"
postcss-selector-parser@^6.0.11:
version "6.0.13"
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz"
integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.0.0, postcss@^8.1.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.38, postcss@>=8.0.9: postcss@^8.4.23, postcss@^8.4.38:
version "8.4.38" version "8.4.38"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz" resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz"
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
@@ -2965,7 +3129,7 @@ prettier-linter-helpers@^1.0.0:
dependencies: dependencies:
fast-diff "^1.1.2" fast-diff "^1.1.2"
prettier@^3.2.5, prettier@>=3.0.0: prettier@^3.2.5:
version "3.2.5" version "3.2.5"
resolved "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz" resolved "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz"
integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==
@@ -2993,7 +3157,7 @@ primereact@^10.6.5:
"@types/react-transition-group" "^4.4.1" "@types/react-transition-group" "^4.4.1"
react-transition-group "^4.4.1" react-transition-group "^4.4.1"
prop-types@^15.6.2, prop-types@^15.8.1, prop-types@15.x: prop-types@15.x, prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -3012,7 +3176,7 @@ queue-microtask@^1.2.2:
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
"react-dom@^17.0.0 || ^18.0.0", "react-dom@>= 16.3.0", react-dom@>=16.6.0, react-dom@>=17, react-dom@>=18, "react-dom@16 || 17 || 18", react-dom@18.2.0: react-dom@18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
@@ -3099,7 +3263,7 @@ react-usestateref@^1.0.9:
resolved "https://registry.npmjs.org/react-usestateref/-/react-usestateref-1.0.9.tgz" resolved "https://registry.npmjs.org/react-usestateref/-/react-usestateref-1.0.9.tgz"
integrity sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw== integrity sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw==
"react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.0 || ^18.0.0", react@^18.2.0, "react@>= 16.3", "react@>= 16.3.0", react@>=16.13.1, react@>=16.6.0, react@>=16.8, react@>=16.8.0, react@>=17, react@>=18, react@>16.0.0, "react@16 || 17 || 18", react@18.2.0: react@18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
@@ -3215,7 +3379,7 @@ rollup-plugin-external-globals@^0.10.0:
is-reference "^3.0.2" is-reference "^3.0.2"
magic-string "^0.30.5" magic-string "^0.30.5"
rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, "rollup@^2.25.0 || ^3.3.0 || ^4.1.4", rollup@^4.13.0: rollup@^4.13.0:
version "4.17.2" version "4.17.2"
resolved "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz" resolved "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz"
integrity sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ== integrity sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==
@@ -3247,7 +3411,7 @@ run-parallel@^1.1.9:
dependencies: dependencies:
queue-microtask "^1.2.2" queue-microtask "^1.2.2"
rxjs@^7.8.1, rxjs@>=6, rxjs@>=7: rxjs@^7.8.1:
version "7.8.1" version "7.8.1"
resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
@@ -3280,7 +3444,7 @@ sass-loader@^14.2.1:
dependencies: dependencies:
neo-async "^2.6.2" neo-async "^2.6.2"
sass@*, sass@^1.3.0, sass@^1.77.2: sass@^1.77.2:
version "1.77.2" version "1.77.2"
resolved "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz" resolved "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz"
integrity sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA== integrity sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==
@@ -3362,7 +3526,7 @@ slash@^5.0.0, slash@^5.1.0:
resolved "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz" resolved "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz"
integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==
source-map-js@^1.2.0, "source-map-js@>=0.6.2 <2.0.0": "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
@@ -3479,7 +3643,7 @@ synckit@^0.8.6:
"@pkgr/core" "^0.1.0" "@pkgr/core" "^0.1.0"
tslib "^2.6.2" tslib "^2.6.2"
tailwindcss@^3.3.6, "tailwindcss@>=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1", "tailwindcss@>=3.0.0 || >= 3.0.0-alpha.1", "tailwindcss@>=3.0.0 || insiders": tailwindcss@^3.3.6:
version "3.3.6" version "3.3.6"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz" resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz"
integrity sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw== integrity sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw==
@@ -3619,7 +3783,7 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13" is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0" possible-typed-array-names "^1.0.0"
typescript@^5.2.2, typescript@>=4.2.0: typescript@^5.2.2:
version "5.4.5" version "5.4.5"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz" resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz"
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
@@ -3664,7 +3828,7 @@ use-local-storage-state@^19.3.1:
resolved "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-19.3.1.tgz" resolved "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-19.3.1.tgz"
integrity sha512-y3Z1dODXvZXZB4qtLDNN8iuXbsYD6TAxz61K58GWB9/yKwrNG9ynI0GzCTHi/Je1rMiyOwMimz0oyFsZn+Kj7Q== integrity sha512-y3Z1dODXvZXZB4qtLDNN8iuXbsYD6TAxz61K58GWB9/yKwrNG9ynI0GzCTHi/Je1rMiyOwMimz0oyFsZn+Kj7Q==
use-sync-external-store@^1.0.0, use-sync-external-store@1.2.0: use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz"
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
@@ -3692,7 +3856,7 @@ vite-plugin-externals@^0.6.2:
fs-extra "^10.0.0" fs-extra "^10.0.0"
magic-string "^0.25.7" magic-string "^0.25.7"
"vite@^4.2.0 || ^5.0.0", vite@^5.0.5, vite@>=2.0.0: vite@^5.0.5:
version "5.2.11" version "5.2.11"
resolved "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz" resolved "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz"
integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ== integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==

View File

@@ -55,7 +55,6 @@ config :wanderer_app, WandererAppWeb.Endpoint,
config :wanderer_app, WandererAppWeb.Endpoint, config :wanderer_app, WandererAppWeb.Endpoint,
live_reload: [ live_reload: [
interval: 1000, interval: 1000,
web_console_logger: true,
patterns: [ patterns: [
~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
~r"priv/gettext/.*(po)$", ~r"priv/gettext/.*(po)$",

View File

@@ -19,6 +19,7 @@ defmodule WandererApp.Api do
resource WandererApp.Api.MapCharacterSettings resource WandererApp.Api.MapCharacterSettings
resource WandererApp.Api.MapSubscription resource WandererApp.Api.MapSubscription
resource WandererApp.Api.MapTransaction resource WandererApp.Api.MapTransaction
resource WandererApp.Api.MapUserSettings
resource WandererApp.Api.User resource WandererApp.Api.User
resource WandererApp.Api.ShipTypeInfo resource WandererApp.Api.ShipTypeInfo
resource WandererApp.Api.UserActivity resource WandererApp.Api.UserActivity

View File

@@ -30,80 +30,89 @@ defmodule WandererApp.Api.Calculations.CalcMapPermissions do
result = result =
record.acls record.acls
|> Enum.filter(fn acl -> |> Enum.reduce([0, 0], fn acl, acc ->
acl.owner_id in character_ids or is_owner? = acl.owner_id in character_ids
acl.members |> Enum.any?(fn member -> member.eve_character_id in character_eve_ids end) or
is_character_member? =
acl.members |> Enum.any?(fn member -> member.eve_character_id in character_eve_ids end)
is_corporation_member? =
acl.members acl.members
|> Enum.any?(fn member -> member.eve_corporation_id in character_corporation_ids end) or |> Enum.any?(fn member -> member.eve_corporation_id in character_corporation_ids end)
is_alliance_member? =
acl.members acl.members
|> Enum.any?(fn member -> member.eve_alliance_id in character_alliance_ids end) |> Enum.any?(fn member -> member.eve_alliance_id in character_alliance_ids end)
end)
|> Enum.reduce([0, 0], fn acl, acc ->
case acc do
[_, -1] ->
[-1, -1]
[-1, char_acc] -> if is_owner? || is_character_member? || is_corporation_member? || is_alliance_member? do
char_acl_mask = case acc do
acl.members [_, -1] ->
|> Enum.filter(fn member -> [-1, -1]
member.eve_character_id in character_eve_ids
end) [-1, char_acc] ->
|> Enum.reduce(0, fn member, acc -> char_acl_mask =
case acc do acl.members
|> Enum.filter(fn member ->
member.eve_character_id in character_eve_ids
end)
|> Enum.reduce(0, fn member, acc ->
case acc do
-1 -> -1
_ -> WandererApp.Permissions.calc_role_mask(member.role, acc)
end
end)
char_acc =
case char_acl_mask do
-1 -> -1 -1 -> -1
_ -> WandererApp.Permissions.calc_role_mask(member.role, acc) _ -> char_acc ||| char_acl_mask
end end
end)
char_acc = [-1, char_acc]
case char_acl_mask do
-1 -> -1
_ -> char_acc ||| char_acl_mask
end
[-1, char_acc] [any_acc, char_acc] ->
any_acl_mask =
acl.members
|> Enum.filter(fn member ->
member.eve_character_id in character_eve_ids ||
member.eve_corporation_id in character_corporation_ids ||
member.eve_alliance_id in character_alliance_ids
end)
|> Enum.reduce(0, fn member, acc ->
case acc do
-1 -> -1
_ -> WandererApp.Permissions.calc_role_mask(member.role, acc)
end
end)
[any_acc, char_acc] -> char_acl_mask =
any_acl_mask = acl.members
acl.members |> Enum.filter(fn member ->
|> Enum.filter(fn member -> member.eve_character_id in character_eve_ids
member.eve_character_id in character_eve_ids or end)
member.eve_corporation_id in character_corporation_ids or |> Enum.reduce(0, fn member, acc ->
member.eve_alliance_id in character_alliance_ids case acc do
end) -1 -> -1
|> Enum.reduce(0, fn member, acc -> _ -> WandererApp.Permissions.calc_role_mask(member.role, acc)
case acc do end
end)
any_acc =
case any_acl_mask do
-1 -> -1 -1 -> -1
_ -> WandererApp.Permissions.calc_role_mask(member.role, acc) _ -> any_acc ||| any_acl_mask
end end
end)
char_acl_mask = char_acc =
acl.members case char_acl_mask do
|> Enum.filter(fn member ->
member.eve_character_id in character_eve_ids
end)
|> Enum.reduce(0, fn member, acc ->
case acc do
-1 -> -1 -1 -> -1
_ -> WandererApp.Permissions.calc_role_mask(member.role, acc) _ -> char_acc ||| char_acl_mask
end end
end)
any_acc = [any_acc, char_acc]
case any_acl_mask do end
-1 -> -1 else
_ -> any_acc ||| any_acl_mask acc
end
char_acc =
case char_acl_mask do
-1 -> -1
_ -> char_acc ||| char_acl_mask
end
[any_acc, char_acc]
end end
end) end)

View File

@@ -48,7 +48,13 @@ defmodule WandererApp.Api.MapConnection do
argument(:map_id, :string, allow_nil?: false) argument(:map_id, :string, allow_nil?: false)
argument(:solar_system_source, :integer, allow_nil?: false) argument(:solar_system_source, :integer, allow_nil?: false)
argument(:solar_system_target, :integer, allow_nil?: false) argument(:solar_system_target, :integer, allow_nil?: false)
filter(expr(map_id == ^arg(:map_id) and solar_system_source == ^arg(:solar_system_source) and solar_system_target == ^arg(:solar_system_target)))
filter(
expr(
map_id == ^arg(:map_id) and solar_system_source == ^arg(:solar_system_source) and
solar_system_target == ^arg(:solar_system_target)
)
)
end end
read :get_link_pairs_advanced do read :get_link_pairs_advanced do

View File

@@ -0,0 +1,54 @@
defmodule WandererApp.Api.MapUserSettings do
@moduledoc false
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer
postgres do
repo(WandererApp.Repo)
table("map_user_settings_v1")
end
code_interface do
define(:create, action: :create)
define(:by_user_id,
get_by: [:map_id, :user_id],
action: :read
)
define(:update_settings, action: :update_settings)
end
actions do
default_accept [
:map_id,
:user_id,
:settings
]
defaults [:create, :read, :update, :destroy]
update :update_settings do
accept [:settings]
end
end
attributes do
uuid_primary_key :id
attribute :settings, :string do
allow_nil? true
end
end
relationships do
belongs_to :map, WandererApp.Api.Map, primary_key?: true, allow_nil?: false
belongs_to :user, WandererApp.Api.User, primary_key?: true, allow_nil?: false
end
identities do
identity :uniq_map_user, [:map_id, :user_id]
end
end

View File

@@ -38,8 +38,6 @@ defmodule WandererApp.Application do
WandererApp.Character.TrackerManager, WandererApp.Character.TrackerManager,
WandererApp.Map.Manager, WandererApp.Map.Manager,
WandererApp.Map.ZkbDataFetcher, WandererApp.Map.ZkbDataFetcher,
WandererApp.Character.ActivityTracker,
WandererApp.User.ActivityTracker,
WandererAppWeb.Presence, WandererAppWeb.Presence,
WandererAppWeb.Endpoint WandererAppWeb.Endpoint
] ++ maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?()) ] ++ maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?())

View File

@@ -6,7 +6,7 @@ defmodule WandererApp.Character do
@read_character_wallet_scope "esi-wallet.read_character_wallet.v1" @read_character_wallet_scope "esi-wallet.read_character_wallet.v1"
@read_corp_wallet_scope "esi-wallet.read_corporation_wallets.v1" @read_corp_wallet_scope "esi-wallet.read_corporation_wallets.v1"
def get_character(character_id) do def get_character(character_id) when not is_nil(character_id) do
case Cachex.get(:character_cache, character_id) do case Cachex.get(:character_cache, character_id) do
{:ok, nil} -> {:ok, nil} ->
case WandererApp.Api.Character.by_id(character_id) do case WandererApp.Api.Character.by_id(character_id) do
@@ -23,6 +23,8 @@ defmodule WandererApp.Character do
end end
end end
def get_character(_character_id), do: {:ok, nil}
def get_character!(character_id) do def get_character!(character_id) do
case get_character(character_id) do case get_character(character_id) do
{:ok, character} -> {:ok, character} ->
@@ -71,11 +73,24 @@ defmodule WandererApp.Character do
end end
end end
def get_character_state!(character_id) do
case get_character_state(character_id) do
{:ok, character_state} ->
character_state
_ ->
Logger.error("Failed to get character_state #{character_id}")
throw("Failed to get character_state #{character_id}")
end
end
def update_character_state(character_id, character_state_update) do def update_character_state(character_id, character_state_update) do
Cachex.get_and_update(:character_state_cache, character_id, fn character_state -> Cachex.get_and_update(:character_state_cache, character_id, fn character_state ->
case character_state do case character_state do
nil -> nil ->
new_state = WandererApp.Character.Tracker.init(character_id: character_id) new_state = WandererApp.Character.Tracker.init(character_id: character_id)
:telemetry.execute([:wanderer_app, :character, :tracker, :started], %{count: 1})
{:commit, Map.merge(new_state, character_state_update)} {:commit, Map.merge(new_state, character_state_update)}
_ -> _ ->

View File

@@ -1,60 +0,0 @@
defmodule WandererApp.Character.ActivityTracker do
@moduledoc false
use GenServer
require Logger
@name __MODULE__
def start_link(args) do
GenServer.start(__MODULE__, args, name: @name)
end
@impl true
def init(_args) do
Logger.info("#{__MODULE__} started")
{:ok, %{}, {:continue, :start}}
end
@impl true
def handle_continue(:start, state) do
:telemetry.attach_many(
"map_character_activity_handler",
[
[:wanderer_app, :map, :character, :jump]
],
&WandererApp.Character.ActivityTracker.handle_event/4,
nil
)
{:noreply, state}
end
@impl true
def terminate(_reason, _state) do
:ok
end
def handle_event(
[:wanderer_app, :map, :character, :jump],
_event_data,
%{
character: character,
map_id: map_id,
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
} = _metadata,
_config
) do
{:ok, _} =
WandererApp.Api.MapChainPassages.new(%{
map_id: map_id,
character_id: character.id,
ship_type_id: character.ship,
ship_name: character.ship_name,
solar_system_source_id: solar_system_source_id,
solar_system_target_id: solar_system_target_id
})
end
end

View File

@@ -35,6 +35,7 @@ defmodule WandererApp.Character.Tracker do
@online_error_timeout :timer.minutes(2) @online_error_timeout :timer.minutes(2)
@forbidden_ttl :timer.minutes(1) @forbidden_ttl :timer.minutes(1)
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
def new(), do: __struct__() def new(), do: __struct__()
def new(args), do: __struct__(args) def new(args), do: __struct__(args)
@@ -53,69 +54,55 @@ defmodule WandererApp.Character.Tracker do
{:ok, {:ok,
character_state character_state
|> _maybe_update_active_maps(track_settings) |> maybe_update_active_maps(track_settings)
|> _maybe_stop_tracking(track_settings) |> maybe_stop_tracking(track_settings)
|> _maybe_start_online_tracking(track_settings) |> maybe_start_online_tracking(track_settings)
|> _maybe_start_location_tracking(track_settings) |> maybe_start_location_tracking(track_settings)
|> _maybe_start_ship_tracking(track_settings)} |> maybe_start_ship_tracking(track_settings)}
end end
def update_info(character_id) do def update_info(character_id) do
{:ok, character_state} = WandererApp.Character.get_character_state(character_id) WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden")
_update_info(character_state) |> case do
end true ->
{:error, :skipped}
def update_ship(character_id) do false ->
{:ok, character_state} = WandererApp.Character.get_character_state(character_id) {:ok, %{eve_id: eve_id}} = WandererApp.Character.get_character(character_id)
_update_ship(character_state)
end
def update_location(character_id) do case WandererApp.Esi.get_character_info(eve_id) do
{:ok, character_state} = WandererApp.Character.get_character_state(character_id) {:ok, info} ->
_update_location(character_state) {:ok, character_state} = WandererApp.Character.get_character_state(character_id)
end update = maybe_update_corporation(character_state, info)
WandererApp.Character.update_character_state(character_id, update)
def update_online(character_id) do :ok
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
_update_online(character_state)
end
def check_online_errors(character_id) do {:error, :forbidden} ->
case(WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")) do Logger.warning("#{__MODULE__} failed to get_character_info: forbidden")
nil ->
:skip
error_time -> WandererApp.Cache.put(
duration = DateTime.diff(DateTime.utc_now(), error_time, :second) "character:#{character_id}:info_forbidden",
true,
ttl: @forbidden_ttl
)
if duration >= @online_error_timeout do {:error, :forbidden}
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Cache.delete("character:#{character_id}:location_started")
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
WandererApp.Character.update_character_state(character_id, %{ {:error, error} ->
character_state Logger.error("#{__MODULE__} failed to get_character_info: #{inspect(error)}")
| is_online: false, {:error, error}
track_ship: false,
track_location: false
})
:ok
else
:skip
end end
end end
end end
def update_wallet(character_id) do def update_ship(character_id) when is_binary(character_id) do
{:ok, character_state} = WandererApp.Character.get_character_state(character_id) character_id
_update_wallet(character_state) |> WandererApp.Character.get_character_state!()
|> update_ship()
end end
defp _update_ship(%{character_id: character_id, track_ship: true} = character_state) do def update_ship(%{character_id: character_id, track_ship: true} = character_state) do
case WandererApp.Character.get_character(character_id) do case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) -> {:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) ->
WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden") WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden")
@@ -123,14 +110,14 @@ defmodule WandererApp.Character.Tracker do
true -> true ->
{:error, :skipped} {:error, :skipped}
false -> _ ->
case WandererApp.Esi.get_character_ship(eve_id, case WandererApp.Esi.get_character_ship(eve_id,
access_token: access_token, access_token: access_token,
character_id: character_id, character_id: character_id,
refresh_token?: true refresh_token?: true
) do ) do
{:ok, ship} -> {:ok, ship} ->
character_state |> _maybe_update_ship(ship) character_state |> maybe_update_ship(ship)
:ok :ok
@@ -156,9 +143,68 @@ defmodule WandererApp.Character.Tracker do
end end
end end
defp _update_ship(_), do: {:error, :skipped} def update_ship(_), do: {:error, :skipped}
defp _update_online(%{track_online: true, character_id: character_id} = character_state) do def update_location(character_id) when is_binary(character_id) do
character_id
|> WandererApp.Character.get_character_state!()
|> update_location()
end
def update_location(%{track_location: true, character_id: character_id} = character_state) do
case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) ->
WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden")
|> case do
true ->
{:error, :skipped}
_ ->
case WandererApp.Esi.get_character_location(eve_id,
access_token: access_token,
character_id: character_id,
refresh_token?: true
) do
{:ok, location} ->
character_state
|> maybe_update_location(location)
:ok
{:error, :forbidden} ->
Logger.warning("#{__MODULE__} failed to update_location: forbidden")
WandererApp.Cache.put(
"character:#{character_id}:location_forbidden",
true,
ttl: @forbidden_ttl
)
{:error, :forbidden}
{:error, error} ->
Logger.error("#{__MODULE__} failed to update_location: #{inspect(error)}")
{:error, error}
end
_ ->
{:error, :skipped}
end
_ ->
{:error, :skipped}
end
end
def update_location(_), do: {:error, :skipped}
def update_online(character_id) when is_binary(character_id) do
character_id
|> WandererApp.Character.get_character_state!()
|> update_online()
end
def update_online(%{track_online: true, character_id: character_id} = character_state) do
case WandererApp.Character.get_character(character_id) do case WandererApp.Character.get_character(character_id) do
{:ok, %{eve_id: eve_id, access_token: access_token}} {:ok, %{eve_id: eve_id, access_token: access_token}}
when not is_nil(access_token) -> when not is_nil(access_token) ->
@@ -167,14 +213,14 @@ defmodule WandererApp.Character.Tracker do
true -> true ->
{:error, :skipped} {:error, :skipped}
false -> _ ->
case WandererApp.Esi.get_character_online(eve_id, case WandererApp.Esi.get_character_online(eve_id,
access_token: access_token, access_token: access_token,
character_id: character_id, character_id: character_id,
refresh_token?: true refresh_token?: true
) do ) do
{:ok, online} -> {:ok, online} ->
online = _get_online(online) online = get_online(online)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden") WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time") WandererApp.Cache.delete("character:#{character_id}:online_error_time")
@@ -240,57 +286,43 @@ defmodule WandererApp.Character.Tracker do
end end
end end
defp _update_online(_), do: {:error, :skipped} def update_online(_), do: {:error, :skipped}
defp _update_location(%{track_location: true, character_id: character_id} = character_state) do def check_online_errors(character_id) do
case WandererApp.Character.get_character(character_id) do WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")
{:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) -> |> case do
WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden") nil ->
|> case do :skip
true ->
{:error, :skipped}
false -> error_time ->
case WandererApp.Esi.get_character_location(eve_id, duration = DateTime.diff(DateTime.utc_now(), error_time, :second)
access_token: access_token,
character_id: character_id,
refresh_token?: true
) do
{:ok, location} ->
character_state
|> _maybe_update_location(location)
:ok if duration >= @online_error_timeout do
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Cache.delete("character:#{character_id}:location_started")
WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
{:error, :forbidden} -> WandererApp.Character.update_character_state(character_id, %{
Logger.warning("#{__MODULE__} failed to update_location: forbidden") character_state
| is_online: false,
track_ship: false,
track_location: false
})
WandererApp.Cache.put( :ok
"character:#{character_id}:location_forbidden", else
true, :skip
ttl: @forbidden_ttl
)
{:error, :forbidden}
{:error, error} ->
Logger.error("#{__MODULE__} failed to update_location: #{inspect(error)}")
{:error, error}
end
_ ->
{:error, :skipped}
end end
_ ->
{:error, :skipped}
end end
end end
defp _update_location(_), do: {:error, :skipped} def update_wallet(character_id) do
character_id
defp _update_wallet(%{character_id: character_id} = state) do |> WandererApp.Character.get_character()
case WandererApp.Character.get_character(character_id) do |> case do
{:ok, %{eve_id: eve_id, access_token: access_token} = character} {:ok, %{eve_id: eve_id, access_token: access_token} = character}
when not is_nil(access_token) -> when not is_nil(access_token) ->
character character
@@ -302,7 +334,7 @@ defmodule WandererApp.Character.Tracker do
true -> true ->
{:error, :skipped} {:error, :skipped}
false -> _ ->
case WandererApp.Esi.get_character_wallet(eve_id, case WandererApp.Esi.get_character_wallet(eve_id,
params: %{datasource: "tranquility"}, params: %{datasource: "tranquility"},
access_token: access_token, access_token: access_token,
@@ -310,7 +342,8 @@ defmodule WandererApp.Character.Tracker do
refresh_token?: true refresh_token?: true
) do ) do
{:ok, result} -> {:ok, result} ->
state |> _maybe_update_wallet(result) {:ok, state} = WandererApp.Character.get_character_state(character_id)
maybe_update_wallet(state, result)
:ok :ok
@@ -340,42 +373,10 @@ defmodule WandererApp.Character.Tracker do
end end
end end
defp _update_info(%{character_id: character_id} = character_state) do defp update_alliance(%{character_id: character_id} = state, alliance_id) do
{:ok, %{eve_id: eve_id}} = WandererApp.Character.get_character(character_id) alliance_id
|> WandererApp.Esi.get_alliance_info()
WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden")
|> case do |> case do
true ->
{:error, :skipped}
false ->
case WandererApp.Esi.get_character_info(eve_id) do
{:ok, info} ->
update = character_state |> _maybe_update_corporation(info)
WandererApp.Character.update_character_state(character_id, update)
:ok
{:error, :forbidden} ->
Logger.warning("#{__MODULE__} failed to get_character_info: forbidden")
WandererApp.Cache.put(
"character:#{character_id}:info_forbidden",
true,
ttl: @forbidden_ttl
)
{:error, :forbidden}
{:error, error} ->
Logger.error("#{__MODULE__} failed to get_character_info: #{inspect(error)}")
{:error, error}
end
end
end
defp _update_alliance(%{character_id: character_id} = state, alliance_id) do
case WandererApp.Esi.get_alliance_info(alliance_id) do
{:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} -> {:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} ->
{:ok, character} = WandererApp.Character.get_character(character_id) {:ok, character} = WandererApp.Character.get_character(character_id)
@@ -390,7 +391,7 @@ defmodule WandererApp.Character.Tracker do
WandererApp.Character.update_character(character_id, character_update) WandererApp.Character.update_character(character_id, character_update)
Phoenix.PubSub.broadcast( @pubsub_client.broadcast(
WandererApp.PubSub, WandererApp.PubSub,
"character:#{character_id}:alliance", "character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}} {:character_alliance, {character_id, character_update}}
@@ -404,8 +405,10 @@ defmodule WandererApp.Character.Tracker do
end end
end end
defp _update_corporation(%{character_id: character_id} = state, corporation_id) do defp update_corporation(%{character_id: character_id} = state, corporation_id) do
case WandererApp.Esi.get_corporation_info(corporation_id) do corporation_id
|> WandererApp.Esi.get_corporation_info()
|> case do
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} -> {:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
alliance_id = Map.get(corporation_info, "alliance_id") alliance_id = Map.get(corporation_info, "alliance_id")
@@ -424,7 +427,7 @@ defmodule WandererApp.Character.Tracker do
WandererApp.Character.update_character(character_id, character_update) WandererApp.Character.update_character(character_id, character_update)
Phoenix.PubSub.broadcast( @pubsub_client.broadcast(
WandererApp.PubSub, WandererApp.PubSub,
"character:#{character_id}:corporation", "character:#{character_id}:corporation",
{:character_corporation, {:character_corporation,
@@ -438,7 +441,7 @@ defmodule WandererApp.Character.Tracker do
state state
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id}) |> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|> _maybe_update_alliance() |> maybe_update_alliance()
_error -> _error ->
Logger.warning("Failed to get corporation info for #{corporation_id}") Logger.warning("Failed to get corporation info for #{corporation_id}")
@@ -446,7 +449,7 @@ defmodule WandererApp.Character.Tracker do
end end
end end
defp _maybe_update_ship( defp maybe_update_ship(
%{ %{
character_id: character_id character_id: character_id
} = } =
@@ -459,38 +462,33 @@ defmodule WandererApp.Character.Tracker do
{:ok, %{ship: old_ship_type_id, ship_name: old_ship_name} = character} = {:ok, %{ship: old_ship_type_id, ship_name: old_ship_name} = character} =
WandererApp.Character.get_character(character_id) WandererApp.Character.get_character(character_id)
case old_ship_type_id != ship_type_id or old_ship_name != ship_name do ship_updated = old_ship_type_id != ship_type_id || old_ship_name != ship_name
true ->
character_update = %{
ship: ship_type_id,
ship_name: ship_name
}
{:ok, _character} = if ship_updated do
WandererApp.Api.Character.update_ship(character, character_update) character_update = %{
ship: ship_type_id,
ship_name: ship_name
}
WandererApp.Character.update_character(character_id, character_update) {:ok, _character} =
WandererApp.Api.Character.update_ship(character, character_update)
state WandererApp.Character.update_character(character_id, character_update)
_ ->
state
end end
state
end end
defp _maybe_update_location( defp maybe_update_location(
%{ %{
character_id: character_id character_id: character_id
} = } =
state, state,
location location
) do ) do
location = _get_location(location) location = get_location(location)
if not WandererApp.Cache.lookup!( if not is_location_started?(character_id) do
"character:#{character_id}:location_started",
false
) do
WandererApp.Cache.lookup!("character:#{character_id}:start_solar_system_id", nil) WandererApp.Cache.lookup!("character:#{character_id}:start_solar_system_id", nil)
|> case do |> case do
nil -> nil ->
@@ -512,58 +510,51 @@ defmodule WandererApp.Character.Tracker do
{:ok, %{solar_system_id: solar_system_id, structure_id: structure_id} = character} = {:ok, %{solar_system_id: solar_system_id, structure_id: structure_id} = character} =
WandererApp.Character.get_character(character_id) WandererApp.Character.get_character(character_id)
WandererApp.Cache.lookup!( (not is_location_started?(character_id) ||
"character:#{character_id}:location_started", is_location_updated?(location, solar_system_id, structure_id))
false
)
|> case do |> case do
true -> true ->
case solar_system_id != location.solar_system_id or
structure_id != location.structure_id do
true ->
{:ok, _character} = WandererApp.Api.Character.update_location(character, location)
WandererApp.Character.update_character(character_id, location)
:ok
_ ->
:ok
end
false ->
{:ok, _character} = WandererApp.Api.Character.update_location(character, location) {:ok, _character} = WandererApp.Api.Character.update_location(character, location)
WandererApp.Character.update_character(character_id, location) WandererApp.Character.update_character(character_id, location)
:ok :ok
_ ->
:ok
end end
state state
end end
defp _maybe_update_corporation( defp is_location_started?(character_id),
do:
WandererApp.Cache.lookup!(
"character:#{character_id}:location_started",
false
)
defp is_location_updated?(location, solar_system_id, structure_id),
do:
solar_system_id != location.solar_system_id ||
structure_id != location.structure_id
defp maybe_update_corporation(
state, state,
%{ %{
"corporation_id" => corporation_id "corporation_id" => corporation_id
} = _info } = _info
) do )
case corporation_id do when not is_nil(corporation_id),
nil -> do: update_corporation(state, corporation_id)
state
_ -> defp maybe_update_corporation(
_update_corporation(state, corporation_id)
end
end
defp _maybe_update_corporation(
state, state,
_info _info
), ),
do: state do: state
defp _maybe_update_alliance( defp maybe_update_alliance(
%{character_id: character_id, alliance_id: alliance_id} = %{character_id: character_id, alliance_id: alliance_id} =
state state
) do ) do
@@ -582,7 +573,7 @@ defmodule WandererApp.Character.Tracker do
WandererApp.Character.update_character(character_id, character_update) WandererApp.Character.update_character(character_id, character_update)
Phoenix.PubSub.broadcast( @pubsub_client.broadcast(
WandererApp.PubSub, WandererApp.PubSub,
"character:#{character_id}:alliance", "character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}} {:character_alliance, {character_id, character_update}}
@@ -591,11 +582,11 @@ defmodule WandererApp.Character.Tracker do
state state
_ -> _ ->
_update_alliance(state, alliance_id) update_alliance(state, alliance_id)
end end
end end
defp _maybe_update_wallet( defp maybe_update_wallet(
%{character_id: character_id} = %{character_id: character_id} =
state, state,
wallet_balance wallet_balance
@@ -611,7 +602,7 @@ defmodule WandererApp.Character.Tracker do
eve_wallet_balance: wallet_balance eve_wallet_balance: wallet_balance
}) })
Phoenix.PubSub.broadcast( @pubsub_client.broadcast(
WandererApp.PubSub, WandererApp.PubSub,
"character:#{character_id}", "character:#{character_id}",
{:character_wallet_balance} {:character_wallet_balance}
@@ -620,7 +611,7 @@ defmodule WandererApp.Character.Tracker do
state state
end end
defp _maybe_start_online_tracking( defp maybe_start_online_tracking(
state, state,
%{track_online: true} = _track_settings %{track_online: true} = _track_settings
), ),
@@ -631,38 +622,37 @@ defmodule WandererApp.Character.Tracker do
track_ship: true track_ship: true
} }
defp _maybe_start_online_tracking( defp maybe_start_online_tracking(
state, state,
_track_settings _track_settings
), ),
do: state do: state
defp _maybe_start_location_tracking( defp maybe_start_location_tracking(
state, state,
%{track_location: true} = _track_settings %{track_location: true} = _track_settings
) do ),
%{state | track_location: true} do: %{state | track_location: true}
end
defp _maybe_start_location_tracking( defp maybe_start_location_tracking(
state, state,
_track_settings _track_settings
), ),
do: state do: state
defp _maybe_start_ship_tracking( defp maybe_start_ship_tracking(
state, state,
%{track_ship: true} = _track_settings %{track_ship: true} = _track_settings
), ),
do: %{state | track_ship: true} do: %{state | track_ship: true}
defp _maybe_start_ship_tracking( defp maybe_start_ship_tracking(
state, state,
_track_settings _track_settings
), ),
do: state do: state
defp _maybe_update_active_maps( defp maybe_update_active_maps(
%{character_id: character_id, active_maps: active_maps} = %{character_id: character_id, active_maps: active_maps} =
state, state,
%{map_id: map_id, track: true} = _track_settings %{map_id: map_id, track: true} = _track_settings
@@ -677,11 +667,12 @@ defmodule WandererApp.Character.Tracker do
%{state | active_maps: [map_id | active_maps] |> Enum.uniq()} %{state | active_maps: [map_id | active_maps] |> Enum.uniq()}
end end
defp _maybe_update_active_maps( defp maybe_update_active_maps(
%{character_id: character_id, active_maps: active_maps} = state, %{character_id: character_id, active_maps: active_maps} = state,
%{map_id: map_id, track: false} = _track_settings %{map_id: map_id, track: false} = _track_settings
) do ) do
case WandererApp.Cache.take("character:#{character_id}:map:#{map_id}:tracking_start_time") do WandererApp.Cache.take("character:#{character_id}:map:#{map_id}:tracking_start_time")
|> case do
start_time when not is_nil(start_time) -> start_time when not is_nil(start_time) ->
duration = DateTime.diff(DateTime.utc_now(), start_time, :second) duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
:telemetry.execute([:wanderer_app, :character, :tracker], %{duration: duration}) :telemetry.execute([:wanderer_app, :character, :tracker], %{duration: duration})
@@ -695,13 +686,13 @@ defmodule WandererApp.Character.Tracker do
%{state | active_maps: Enum.filter(active_maps, &(&1 != map_id))} %{state | active_maps: Enum.filter(active_maps, &(&1 != map_id))}
end end
defp _maybe_update_active_maps( defp maybe_update_active_maps(
state, state,
_track_settings _track_settings
), ),
do: state do: state
defp _maybe_stop_tracking( defp maybe_stop_tracking(
%{active_maps: [], character_id: character_id, opts: opts} = state, %{active_maps: [], character_id: character_id, opts: opts} = state,
_track_settings _track_settings
) do ) do
@@ -722,25 +713,21 @@ defmodule WandererApp.Character.Tracker do
} }
end end
defp _maybe_stop_tracking( defp maybe_stop_tracking(
state, state,
_track_settings _track_settings
), ),
do: state do: state
defp _get_location(%{"solar_system_id" => solar_system_id, "structure_id" => structure_id}) do defp get_location(%{"solar_system_id" => solar_system_id, "structure_id" => structure_id}),
%{solar_system_id: solar_system_id, structure_id: structure_id} do: %{solar_system_id: solar_system_id, structure_id: structure_id}
end
defp _get_location(%{"solar_system_id" => solar_system_id}) do defp get_location(%{"solar_system_id" => solar_system_id}),
%{solar_system_id: solar_system_id, structure_id: nil} do: %{solar_system_id: solar_system_id, structure_id: nil}
end
defp _get_location(_), do: %{solar_system_id: nil, structure_id: nil} defp get_location(_), do: %{solar_system_id: nil, structure_id: nil}
defp _get_online(%{"online" => online}) do defp get_online(%{"online" => online}), do: %{online: online}
%{online: online}
end
defp _get_online(_), do: %{} defp get_online(_), do: %{}
end end

View File

@@ -46,9 +46,7 @@ defmodule WandererApp.Character.TrackerManager do
def handle_call(:error, _, state), do: {:stop, :error, :ok, state} def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
@impl true @impl true
def handle_call(:stop, _, state) do def handle_call(:stop, _, state), do: {:stop, :normal, :ok, state}
{:stop, :normal, :ok, state}
end
@impl true @impl true
def handle_call( def handle_call(

View File

@@ -70,12 +70,10 @@ defmodule WandererApp.Character.TrackerManager.Impl do
false -> false ->
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end) Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
Task.start_link(fn -> WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
WandererApp.Character.update_character_state(character_id, %{opts: opts}) character_id,
:telemetry.execute([:wanderer_app, :character, :tracker, :started], %{count: 1}) %{opts: opts}
])
:ok
end)
tracked_characters = [character_id | state.characters] |> Enum.uniq() tracked_characters = [character_id | state.characters] |> Enum.uniq()
WandererApp.Cache.insert("tracked_characters", tracked_characters) WandererApp.Cache.insert("tracked_characters", tracked_characters)
@@ -180,9 +178,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
characters characters
|> Enum.map(fn character_id -> |> Enum.map(fn character_id ->
Task.start_link(fn -> WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [
WandererApp.Character.Tracker.update_online(character_id) character_id
end) ])
end) end)
state state
@@ -207,10 +205,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
Process.send_after(self(), :check_online_errors, @check_online_errors_interval) Process.send_after(self(), :check_online_errors, @check_online_errors_interval)
characters characters
|> Enum.map(fn character_id -> |> Task.async_stream(
Task.start_link(fn -> fn character_id ->
WandererApp.Character.Tracker.check_online_errors(character_id) WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :check_online_errors, [
end) character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_online_errors: #{inspect(reason)}")
end) end)
state state
@@ -228,9 +235,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
characters characters
|> Enum.map(fn character_id -> |> Enum.map(fn character_id ->
Task.start_link(fn -> WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [
WandererApp.Character.Tracker.update_location(character_id) character_id
end) ])
end) end)
state state
@@ -257,9 +264,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
characters characters
|> Enum.map(fn character_id -> |> Enum.map(fn character_id ->
Task.start_link(fn -> WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [
WandererApp.Character.Tracker.update_ship(character_id) character_id
end) ])
end) end)
state state
@@ -285,10 +292,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
Process.send_after(self(), :update_info, @update_info_interval) Process.send_after(self(), :update_info, @update_info_interval)
characters characters
|> Enum.map(fn character_id -> |> Task.async_stream(
Task.start_link(fn -> fn character_id ->
WandererApp.Character.Tracker.update_info(character_id) WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [
end) character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in update_info: #{inspect(reason)}")
end) end)
state state
@@ -314,10 +330,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
Process.send_after(self(), :update_wallet, @update_wallet_interval) Process.send_after(self(), :update_wallet, @update_wallet_interval)
characters characters
|> Enum.map(fn character_id -> |> Task.async_stream(
Task.start_link(fn -> fn character_id ->
WandererApp.Character.Tracker.update_wallet(character_id) WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [
end) character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in update_wallet: #{inspect(reason)}")
end) end)
state state
@@ -358,7 +383,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end end
end end
end, end,
max_concurrency: 20, max_concurrency: System.schedulers_online(),
on_timeout: :kill_task, on_timeout: :kill_task,
timeout: :timer.seconds(15) timeout: :timer.seconds(15)
) )
@@ -394,7 +419,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
WandererApp.Character.update_character_state(character_id, character_state) WandererApp.Character.update_character_state(character_id, character_state)
end, end,
max_concurrency: 20, max_concurrency: System.schedulers_online(),
on_timeout: :kill_task, on_timeout: :kill_task,
timeout: :timer.seconds(30) timeout: :timer.seconds(30)
) )
@@ -404,7 +429,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end end
def handle_info({:stop_track, character_id}, state) do def handle_info({:stop_track, character_id}, state) do
Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end) @logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
stop_tracking(state, character_id) stop_tracking(state, character_id)
end end

View File

@@ -1,42 +0,0 @@
defmodule DDRT do
use DDRT.DynamicRtree
alias DDRT.DynamicRtree
@moduledoc """
This is the top-level `DDRT` module. Use this to create a distributed r-tree. If you're only interested in using this package for the r-tree implementation, you should instead use `DDRT.DynamicRtree`
Please refer to `DDRT.DynamicRtree` module documentation for complete function specs and examples for general usage of the core API methods.
"""
# DDRT party begins.
@spec start_link(DynamicRtree.tree_config()) :: {:ok, pid}
@doc "See `DDRT.DynamicRtree.start_link/1` for documentation and configuration parameters"
def start_link(opts) do
name = Keyword.get(opts, :name, DynamicRtree)
children = [
{DeltaCrdt,
[
crdt: DeltaCrdt.AWLWWMap,
name: Module.concat([name, Crdt]),
on_diffs: &on_diffs(&1, DynamicRtree, name)
]},
{DynamicRtree,
[
conf: Keyword.put_new(opts, :mode, :distributed),
crdt: Module.concat([name, Crdt]),
name: name
]}
]
Supervisor.start_link(children,
strategy: :one_for_one,
name: Module.concat([name, Supervisor])
)
end
@doc false
def on_diffs(diffs, mod, name) do
mod.merge_diffs(diffs, name)
end
end

View File

@@ -1,725 +0,0 @@
defmodule DDRT.DynamicRtree do
use GenServer, restart: :transient
use DDRT.DynamicRtreeImpl
@type tree_init :: [
name: GenServer.name(),
crdt: module(),
conf: tree_config()
]
@type tree_config :: [
name: GenServer.name(),
width: integer(),
type: module(),
verbose: boolean(),
seed: integer(),
mode: ddrt_mode()
]
@type ddrt_mode :: :standalone | :distributed
@type coord_range :: {number(), number()}
@type bounding_box :: list(coord_range())
@type id :: number() | String.t()
@type leaf :: {id(), bounding_box()}
@type member :: GenServer.name() | {GenServer.name(), node()}
@callback delete(ids :: id() | [id()], name :: GenServer.name()) ::
{:ok, map()} | {:badtree, map()}
@callback insert(leaves :: leaf() | [leaf()], name :: GenServer.name()) ::
{:ok, map()} | {:badtree, map()}
@callback metadata(name :: GenServer.name()) :: map()
@callback pquery(box :: bounding_box(), depth :: integer(), name :: GenServer.name()) ::
{:ok, [id()]} | {:badtree, map()}
@callback query(box :: bounding_box(), name :: GenServer.name()) ::
{:ok, [id()]} | {:badtree, map()}
@callback update(
ids :: id(),
box :: bounding_box() | {bounding_box(), bounding_box()},
name :: GenServer.name()
) :: {:ok, map()} | {:badtree, map()}
@callback bulk_update(leaves :: [leaf()], name :: GenServer.name()) ::
{:ok, map()} | {:badtree, map()}
@callback new(opts :: Keyword.t(), name :: GenServer.name()) :: {:ok, map()}
@callback tree(name :: GenServer.name()) :: map()
@callback set_members(name :: GenServer.name(), [member()]) :: :ok
@doc false
defmacro doc_referral({name, arity}) do
"See `DDRT.DynamicRtree.#{name}/#{arity}` for documentation and usage examples."
end
defmacro __using__(_) do
quote do
alias DDRT.DynamicRtree
@behaviour DynamicRtree
@doc unquote(doc_referral({:delete, 2}))
defdelegate delete(ids, name), to: DynamicRtree
@doc unquote(doc_referral({:insert, 2}))
defdelegate insert(leaves, name), to: DynamicRtree
@doc unquote(doc_referral({:metadata, 1}))
defdelegate metadata(name), to: DynamicRtree
@doc unquote(doc_referral({:pquery, 3}))
defdelegate pquery(box, depth, name), to: DynamicRtree
@doc unquote(doc_referral({:query, 2}))
defdelegate query(box, name), to: DynamicRtree
@doc unquote(doc_referral({:update, 3}))
defdelegate update(ids, box, name), to: DynamicRtree
@doc unquote(doc_referral({:bulk_update, 2}))
defdelegate bulk_update(leaves, name), to: DynamicRtree
@doc unquote(doc_referral({:new, 2}))
defdelegate new(opts, name), to: DynamicRtree
@doc unquote(doc_referral({:tree, 1}))
defdelegate tree(name), to: DynamicRtree
@doc unquote(doc_referral({:set_members, 2}))
defdelegate set_members(name, members), to: DynamicRtree
end
end
defstruct metadata: nil,
tree: nil,
listeners: [],
crdt: nil,
name: nil
@moduledoc """
Use this module if you're interested in creating an R-Tree optimized to run on a single machine. If you'd instead like to run a distributed R-Tree on a cluster of Elixir nodes, use the `DDRT` module.
"""
@doc """
These are all of the possible configuration parameters for `opts` and their default values:
- **name**: The name of the DDRT process. Defaults to `DDRT`
- **width**: The max number of children a node may have. Defaults to `6`
- **verbose**: allows `Logger` to report console logs. (Also decreases performance). Defaults to `false`.
- **seed**: Sets the seed value for the pseudo-random number generator which generates the unique IDs for each node in the tree. This is a deterministic process; so the same seed value will guarantee the same pseudo-random unique IDs being generated for your tree in the same order each time. Defaults to `0`
"""
@spec start_link(opts :: tree_init()) :: {:ok, pid()} | {:error, term()}
def start_link(opts) do
name = Keyword.get(opts, :name, DDRT)
GenServer.start_link(__MODULE__, opts, name: name)
end
@impl true
def init(opts) do
conf = filter_conf(opts[:conf])
{t, meta} = tree_new(conf)
listeners = Node.list()
t =
if %{metadata: meta} |> is_distributed? do
DeltaCrdt.set_neighbours(opts[:crdt], Enum.map(Node.list(), fn x -> {opts[:crdt], x} end))
crdt_value = DeltaCrdt.to_map(opts[:crdt])
:net_kernel.monitor_nodes(true, node_type: :visible)
if crdt_value != %{}, do: reconstruct_from_crdt(crdt_value, t), else: t
else
t
end
{:ok,
%__MODULE__{
name: opts[:name],
metadata: meta,
tree: t,
listeners: listeners,
crdt: opts[:crdt]
}}
end
@opt_values %{
type: [Map, MerkleMap],
mode: [:standalone, :distributed]
}
@defopts [
width: 6,
type: Map,
mode: :standalone,
verbose: false,
seed: 0
]
@spec new(opts :: Keyword.t(), name :: GenServer.name()) :: {:ok, map()}
def new(opts \\ @defopts, name \\ DDRT) when is_list(opts) do
GenServer.call(name, {:new, opts})
end
@spec insert(leaves :: leaf() | [leaf()], name :: GenServer.name()) ::
{:ok, map()} | {:badtree, map()}
def insert(_a, name \\ DDRT)
@doc """
Insert `leaves` into the r-tree with process with name `name`
Returns `{:ok,map()}`
## Parameters
- `leaves`: the data to insert.
- `name`: the r-tree name where you want to insert.
## Examples
Individual insertion:
```
iex> DynamicRtree.insert({"Griffin", [{4,5},{6,7}]}, :my_rtree)
iex> DynamicRtree.insert({"Parker", [{14,15},{16,17}]}, :my_rtree)
{:ok,
%{
43143342109176739 => {["Parker", "Griffin"], nil, [{4, 15}, {6, 17}]},
:root => 43143342109176739,
:ticket => [19125803434255161 | 82545666616502197],
"Griffin" => {:leaf, 43143342109176739, [{4, 5}, {6, 7}]},
"Parker" => {:leaf, 43143342109176739, [{14, 15}, {16, 17}]}
}}
```
Bulk Insertion:
```
iex> DynamicRtree.insert([{"Griffin", [{4,5},{6,7}]}, {"Parker", [{14,15},{16,17}]}], :my_rtree)
{:ok,
%{
43143342109176739 => {["Parker", "Griffin"], nil, [{4, 15}, {6, 17}]},
:root => 43143342109176739,
:ticket => [19125803434255161 | 82545666616502197],
"Griffin" => {:leaf, 43143342109176739, [{4, 5}, {6, 7}]},
"Parker" => {:leaf, 43143342109176739, [{14, 15}, {16, 17}]}
}}
```
"""
def insert(leaves, name) when is_list(leaves) do
GenServer.call(name, {:bulk_insert, leaves}, :infinity)
end
def insert(leaf, name) do
GenServer.call(name, {:insert, leaf}, :infinity)
end
@doc """
Query to get every leaf id overlapped by `box`.
Returns `[id's]`.
## Examples
iex> DynamicRtree.query([{0,7},{4,8}],:my_rtree)
{:ok, ["Griffin"]}
"""
@spec query(box :: bounding_box(), name :: GenServer.name()) ::
{:ok, [id()]} | {:badtree, map()}
def query(box, name \\ DDRT) do
GenServer.call(name, {:query, box})
end
@doc """
Query to get every node id overlapped by `box` at the defined `depth`.
Returns `[id's]`.
"""
@spec pquery(box :: bounding_box(), depth :: integer(), name :: GenServer.name()) ::
{:ok, [id()]} | {:badtree, map()}
def pquery(box, depth, name \\ DDRT) do
GenServer.call(name, {:query_depth, {box, depth}})
end
@spec delete(ids :: id() | [id()], name :: GenServer.name()) ::
{:ok, map()} | {:badtree, map()}
def delete(_a, name \\ DDRT)
@doc """
Delete the leaves with the given `ids`.
Returns `{:ok,map()}`
## Parameters
- `ids`: Id or list of Id that you want to delete.
- `name`: the name of the rtree process.
## Examples
Individual deletion:
```
iex> DynamicRtree.delete("Griffin",:my_rtree)
iex> DynamicRtree.delete("Parker",:my_rtree)
```
Bulk Deletion:
```
iex> DynamicRtree.delete(["Griffin","Parker"],:my_rtree)
```
"""
def delete(ids, name) when is_list(ids) do
GenServer.call(name, {:bulk_delete, ids}, :infinity)
end
def delete(id, name) do
GenServer.call(name, {:delete, id})
end
@doc """
Update a bunch of r-tree leaves to the new bounding boxes defined.
Returns `{:ok,map()}`
## Examples
```
iex> DynamicRtree.bulk_update([{"Griffin",[{0,1},{0,1}]},{"Parker",[{10,11},{10,11}]}],:my_rtree)
{:ok,
%{
43143342109176739 => {["Parker", "Griffin"], nil, [{0, 11}, {0, 11}]},
:root => 43143342109176739,
:ticket => [19125803434255161 | 82545666616502197],
"Griffin" => {:leaf, 43143342109176739, [{0, 1}, {0, 1}]},
"Parker" => {:leaf, 43143342109176739, [{10, 11}, {10, 11}]}
}}
```
"""
@spec bulk_update(leaves :: [leaf()], name :: GenServer.name()) ::
{:ok, map()} | {:badtree, map()}
def bulk_update(updates, name \\ DDRT) when is_list(updates) do
GenServer.call(name, {:bulk_update, updates}, :infinity)
end
@doc """
Update a single leaf bounding box
Returns `{:ok,map()}`
## Examples
```
iex> DynamicRtree.update({"Griffin",[{0,1},{0,1}]},:my_rtree)
{:ok,
%{
43143342109176739 => {["Parker", "Griffin"], nil, [{0, 11}, {0, 11}]},
:root => 43143342109176739,
:ticket => [19125803434255161 | 82545666616502197],
"Griffin" => {:leaf, 43143342109176739, [{0, 1}, {0, 1}]},
"Parker" => {:leaf, 43143342109176739, [{10, 11}, {16, 17}]}
}}
```
"""
@spec update(
ids :: id(),
box :: bounding_box() | {bounding_box(), bounding_box()},
name :: GenServer.name()
) :: {:ok, map()} | {:badtree, map()}
def update(id, update, name \\ DDRT) do
GenServer.call(name, {:update, {id, update}})
end
@doc """
Get the r-tree metadata
Returns `map()`
## Examples
iex> DynamicRtree.metadata(:my_rtree)
%{
params: %{mode: :standalone, seed: 0, type: Map, verbose: false, width: 6},
seeding: %{
bits: 58,
jump: #Function<3.53802439/1 in :rand.mk_alg/1>,
next: #Function<0.53802439/1 in :rand.mk_alg/1>,
type: :exrop,
uniform: #Function<1.53802439/1 in :rand.mk_alg/1>,
uniform_n: #Function<2.53802439/2 in :rand.mk_alg/1>,
weak_low_bits: 1
}
}
"""
@spec metadata(name :: GenServer.name()) :: map()
def metadata(name \\ DDRT)
def metadata(name) do
GenServer.call(name, :metadata)
end
@doc """
Get the r-tree representation
Returns `map()`
## Examples
```
iex> DynamicRtree.metadata(:my_rtree)
%{
43143342109176739 => {["Parker", "Griffin"], nil, [{0, 11}, {0, 11}]},
:root => 43143342109176739,
:ticket => [19125803434255161 | 82545666616502197],
"Griffin" => {:leaf, 43143342109176739, [{0, 1}, {0, 1}]},
"Parker" => {:leaf, 43143342109176739, [{10, 11}, {10, 11}]}
}
```
"""
@spec tree(name :: GenServer.name()) :: map()
def tree(name \\ DDRT)
def tree(name) do
GenServer.call(name, :tree)
end
@doc """
Set the members of the `DDRT` cluster.
`members` should be in the format `{GenServer.name(), node()}`.
## Examples
```
DDRT.set_members(DDRT, [{DDRT.A, :yournode@foreignhost}, {DDRT.B, :yournode@foreignhost}])
```
"""
@spec set_members(name :: GenServer.name(), [member()]) :: :ok
def set_members(name, members) do
:ok = GenServer.call(name, {:set_members, members})
:ok
end
def merge_diffs(_a, name \\ DDRT)
@doc false
def merge_diffs(diffs, name) do
send(name, {:merge_diff, diffs})
end
## PRIVATE METHODS
defp fully_qualified_name({_name, _node} = fq_pair), do: fq_pair
defp fully_qualified_name(name) do
{name, Node.self()}
end
defp is_distributed?(state) do
state.metadata[:params][:mode] == :distributed
end
defp constraints() do
%{
width: fn v -> v > 0 end,
type: fn v -> v in (@opt_values |> Map.get(:type)) end,
mode: fn v -> v in (@opt_values |> Map.get(:mode)) end,
verbose: fn v -> is_boolean(v) end,
seed: fn v -> is_integer(v) end
}
end
defp filter_conf(opts) do
# set default :mode to :standalone
opts = Keyword.put_new(opts, :mode, :standalone)
new_opts =
case opts[:mode] do
:distributed -> Keyword.put(opts, :type, MerkleMap)
_ -> opts
end
good_keys =
new_opts
|> Keyword.keys()
|> Enum.filter(fn k ->
constraints() |> Map.has_key?(k) and constraints()[k].(new_opts[k])
end)
good_keys
|> Enum.reduce(@defopts, fn k, acc ->
acc |> Keyword.put(k, new_opts[k])
end)
end
defp get_rbundle(state) do
meta = state.metadata
params = meta.params
%{
tree: state.tree,
width: params[:width],
verbose: params[:verbose],
type: params[:type],
seeding: meta[:seeding]
}
end
@impl true
def handle_call({:set_members, members}, _from, state) do
self_crdt =
Module.concat([state.name, Crdt])
|> fully_qualified_name()
member_crdts =
members
|> Enum.map(&fully_qualified_name(&1))
|> Enum.map(fn {pname, node} ->
{Module.concat([pname, Crdt]), node}
end)
result = DeltaCrdt.set_neighbours(self_crdt, member_crdts)
{:reply, result, state}
end
@impl true
def handle_call({:new, config}, _from, state) do
conf = config |> filter_conf
{t, meta} = tree_new(conf)
{:reply, {:ok, t}, %__MODULE__{state | metadata: meta, tree: t}}
end
@impl true
def handle_call({:insert, leaf}, _from, state) do
r =
{_atom, t} =
case state.tree do
nil -> {:badtree, state.tree}
_ -> {:ok, get_rbundle(state) |> tree_insert(leaf)}
end
if is_distributed?(state) do
diffs = tree_diffs(state.tree, t)
sync_crdt(diffs, state.crdt)
end
{:reply, r, %__MODULE__{state | tree: t}}
end
@impl true
def handle_call({:bulk_insert, leaves}, _from, state) do
r =
{_atom, t} =
case state.tree do
nil ->
{:badtree, state.tree}
_ ->
final_rbundle =
leaves
|> Enum.reduce(get_rbundle(state), fn l, acc ->
%{acc | tree: acc |> tree_insert(l)}
end)
{:ok, final_rbundle.tree}
end
if is_distributed?(state) do
diffs = tree_diffs(state.tree, t)
sync_crdt(diffs, state.crdt)
end
{:reply, r, %__MODULE__{state | tree: t}}
end
@impl true
def handle_call({:query, box}, _from, state) do
r =
{_atom, _t} =
case state.tree do
nil -> {:badtree, state.tree}
_ -> {:ok, get_rbundle(state) |> tree_query(box)}
end
{:reply, r, state}
end
@impl true
def handle_call({:query_depth, {box, depth}}, _from, state) do
r =
{_atom, _t} =
case state.tree do
nil -> {:badtree, state.tree}
_ -> {:ok, get_rbundle(state) |> tree_query(box, depth)}
end
{:reply, r, state}
end
@impl true
def handle_call({:delete, id}, _from, state) do
r =
{_atom, t} =
case state.tree do
nil -> {:badtree, state.tree}
_ -> {:ok, get_rbundle(state) |> tree_delete(id)}
end
if is_distributed?(state) do
diffs = tree_diffs(state.tree, t)
sync_crdt(diffs, state.crdt)
end
{:reply, r, %__MODULE__{state | tree: t}}
end
@impl true
def handle_call({:bulk_delete, ids}, _from, state) do
r =
{_atom, t} =
case state.tree do
nil ->
{:badtree, state.tree}
_ ->
final_rbundle =
ids
|> Enum.reduce(get_rbundle(state), fn id, acc ->
%{acc | tree: acc |> tree_delete(id)}
end)
{:ok, final_rbundle.tree}
end
if is_distributed?(state) do
diffs = tree_diffs(state.tree, t)
sync_crdt(diffs, state.crdt)
end
{:reply, r, %__MODULE__{state | tree: t}}
end
@impl true
def handle_call({:update, {id, update}}, _from, state) do
r =
{_atom, t} =
case state.tree do
nil -> {:badtree, state.tree}
_ -> {:ok, get_rbundle(state) |> tree_update_leaf(id, update)}
end
if is_distributed?(state) do
diffs = tree_diffs(state.tree, t)
sync_crdt(diffs, state.crdt)
end
{:reply, r, %__MODULE__{state | tree: t}}
end
@impl true
def handle_call({:bulk_update, updates}, _from, state) do
r =
{_atom, t} =
case state.tree do
nil ->
{:badtree, state.tree}
_ ->
final_rbundle =
updates
|> Enum.reduce(get_rbundle(state), fn {id, update} = _u, acc ->
%{acc | tree: acc |> tree_update_leaf(id, update)}
end)
{:ok, final_rbundle.tree}
end
if is_distributed?(state) do
diffs = tree_diffs(state.tree, t)
sync_crdt(diffs, state.crdt)
end
{:reply, r, %__MODULE__{state | tree: t}}
end
@impl true
def handle_call(:metadata, _from, state) do
{:reply, state.metadata, state}
end
@impl true
def handle_call(:tree, _from, state) do
{:reply, state.tree, state}
end
# Distributed things
@impl true
def handle_info({:merge_diff, diff}, state) do
new_tree =
diff
|> Enum.reduce(state.tree, fn x, acc ->
case x do
{:add, k, v} -> acc |> MerkleMap.put(k, v)
{:remove, k} -> acc |> MerkleMap.delete(k)
end
end)
{:noreply, %__MODULE__{state | tree: new_tree}}
end
def handle_info({:nodeup, _node, _opts}, state) do
DeltaCrdt.set_neighbours(state.crdt, Enum.map(Node.list(), fn x -> {state.crdt, x} end))
{:noreply, %__MODULE__{state | listeners: Node.list()}}
end
def handle_info({:nodedown, _node, _opts}, state) do
DeltaCrdt.set_neighbours(state.crdt, Enum.map(Node.list(), fn x -> {state.crdt, x} end))
{:noreply, %__MODULE__{state | listeners: Node.list()}}
end
@doc false
def sync_crdt(diffs, crdt) when length(diffs) > 0 do
diffs
|> Enum.each(fn {k, v} ->
if v do
DeltaCrdt.put(crdt, k, v)
else
DeltaCrdt.delete(crdt, k)
end
end)
end
@doc false
def sync_crdt(_diffs, _crdt) do
end
@doc false
def reconstruct_from_crdt(map, t) do
map
|> Enum.reduce(t, fn {x, y}, acc ->
acc |> MerkleMap.put(x, y)
end)
end
@doc false
def tree_diffs(old_tree, new_tree) when not is_nil(old_tree) and not is_nil(new_tree) do
case MerkleMap.diff_keys(
old_tree |> MerkleMap.update_hashes(),
new_tree |> MerkleMap.update_hashes()
) do
{:ok, keys} -> keys |> Enum.map(fn x -> {x, new_tree |> MerkleMap.get(x)} end)
_ -> []
end
end
def tree_diffs(_old_tree, _new_tree), do: []
end

View File

@@ -1,687 +0,0 @@
defmodule DDRT.DynamicRtreeImpl do
alias DDRT.DynamicRtreeImpl.{Node, Utils}
require Logger
import IO.ANSI
# Between 1 y 64800. Bigger value => ^ updates speed, ~v query speed.
@max_area 20000
defmacro __using__(_) do
quote do
alias DDRT.DynamicRtreeImpl
@doc false
defdelegate tree_new(opts), to: DynamicRtreeImpl
@doc false
defdelegate tree_insert(tree, leaf), to: DynamicRtreeImpl
@doc false
defdelegate tree_query(tree, box), to: DynamicRtreeImpl
@doc false
defdelegate tree_query(tree, box, depth), to: DynamicRtreeImpl
@doc false
defdelegate tree_delete(tree, id), to: DynamicRtreeImpl
@doc false
defdelegate tree_update_leaf(tree, id, update), to: DynamicRtreeImpl
end
end
# PUBLIC METHODS
def tree_new(opts) do
{f, s} = :rand.seed(:exrop, opts[:seed])
{node, new_ticket} = Node.new(f, s)
tree_init =
case opts[:type] do
Map -> %{}
MerkleMap -> %MerkleMap{}
end
tree =
tree_init
|> opts[:type].put(:ticket, new_ticket)
|> opts[:type].put(:root, node)
|> opts[:type].put(node, {[], nil, [{0, 0}, {0, 0}]})
{tree, %{params: opts, seeding: f}}
end
def tree_insert(rbundle, {id, _box} = leaf) do
if rbundle.tree |> rbundle[:type].get(id) do
if rbundle.verbose,
do:
Logger.debug(
cyan() <>
"[" <>
green() <>
"Insertion" <>
cyan() <>
"] failed:" <>
yellow() <>
" [#{id}] " <>
cyan() <>
"already exists at tree." <>
yellow() <> " [Tip]" <> cyan() <> " use " <> yellow() <> "update_leaf/3"
)
rbundle.tree
else
path = best_subtree(rbundle, leaf)
t1 = :os.system_time(:microsecond)
r =
insertion(rbundle, path, leaf)
|> recursive_update(tl(path), leaf, :insertion)
t2 = :os.system_time(:microsecond)
if rbundle.verbose,
do:
Logger.debug(
cyan() <>
"[" <>
green() <>
"Insertion" <>
cyan() <>
"] success: " <>
yellow() <>
"[#{id}]" <> cyan() <> " was inserted at" <> yellow() <> " ['#{hd(path)}']"
)
if rbundle.verbose,
do:
Logger.info(
cyan() <>
"[" <> green() <> "Insertion" <> cyan() <> "] took" <> yellow() <> " #{t2 - t1} µs"
)
r
end
end
def tree_query(rbundle, box) do
t1 = :os.system_time(:microsecond)
r = find_match_leaves(rbundle, box, [get_root(rbundle)], [], [])
t2 = :os.system_time(:microsecond)
if rbundle.verbose,
do:
Logger.info(
cyan() <>
"[" <>
color(201) <>
"Query" <>
cyan() <>
"] box " <>
yellow() <>
"#{box |> Kernel.inspect()} " <> cyan() <> "took " <> yellow() <> "#{t2 - t1} µs"
)
r
end
def tree_query(rbundle, box, depth) do
find_match_depth(rbundle, box, [{get_root(rbundle), 0}], [], depth)
end
def tree_delete(rbundle, id) do
t1 = :os.system_time(:microsecond)
r =
if rbundle.tree |> rbundle[:type].get(id) do
remove(rbundle, id)
else
rbundle.tree
end
t2 = :os.system_time(:microsecond)
if rbundle.verbose,
do:
Logger.info(
cyan() <>
"[" <>
color(124) <>
"Delete" <>
cyan() <>
"] leaf " <>
yellow() <> "[#{id}]" <> cyan() <> " took " <> yellow() <> "#{t2 - t1} µs"
)
r
end
def tree_update_leaf(rbundle, id, {old_box, new_box} = boxes) do
if rbundle.tree |> rbundle[:type].get(id) do
t1 = :os.system_time(:microsecond)
r = update(rbundle, id, boxes)
t2 = :os.system_time(:microsecond)
if rbundle.verbose,
do:
Logger.info(
cyan() <>
"[" <>
color(195) <>
"Update" <>
cyan() <>
"] " <>
yellow() <>
"[#{id}]" <>
cyan() <>
" from " <>
yellow() <>
"#{old_box |> Kernel.inspect()}" <>
cyan() <>
" to " <>
yellow() <>
"#{new_box |> Kernel.inspect()}" <>
cyan() <> " took " <> yellow() <> "#{t2 - t1} µs"
)
r
else
if rbundle.verbose,
do:
Logger.warning(
cyan() <>
"[" <>
color(195) <>
"Update" <> cyan() <> "] " <> yellow() <> "[#{id}] doesn't exists" <> cyan()
)
rbundle.tree
end
end
# You dont need to know old_box but is a BIT slower
def tree_update_leaf(rbundle, id, new_box) do
tree_update_leaf(
rbundle,
id,
{rbundle.tree |> rbundle[:type].get(id) |> Utils.tuple_value(:bbox), new_box}
)
end
### PRIVATE METHODS
# Helpers
defp get_root(rbundle) do
rbundle.tree |> rbundle[:type].get(:root)
end
defp is_root?(rbundle, node) do
get_root(rbundle) == node
end
## Internal actions
## Insert
# triple - S (Structure Swifty Shift)
defp triple_s(rbundle, old_node, new_node, {id, box}) do
tuple_entry =
{old_node_childs_update, _daddy, _bbox} =
rbundle.tree |> rbundle[:type].get(old_node) |> (fn {n, d, b} -> {n -- [id], d, b} end).()
tree_update =
rbundle.tree
|> rbundle[:type].update!(new_node, fn {ch, d, b} -> {[id] ++ ch, d, b} end)
|> rbundle[:type].update!(id, fn {ch, _d, b} -> {ch, new_node, b} end)
if length(old_node_childs_update) > 0 do
%{rbundle | tree: tree_update |> rbundle[:type].put(old_node, tuple_entry)}
|> recursive_update(old_node, box, :deletion)
else
%{rbundle | tree: tree_update} |> remove(old_node)
end
end
defp insertion(rbundle, branch, {_id, _box} = leaf) do
tree_update = add_entry(rbundle, hd(branch), leaf)
childs = tree_update |> rbundle[:type].get(hd(branch)) |> Utils.tuple_value(:childs)
final_tree =
if length(childs) > rbundle.width do
handle_overflow(%{rbundle | tree: tree_update}, branch)
else
tree_update
end
%{rbundle | tree: final_tree}
end
defp add_entry(rbundle, node, {id, box} = _leaf) do
rbundle.tree
|> rbundle[:type].update!(node, fn {ch, daddy, b} ->
{[id] ++ ch, daddy, Utils.combine_multiple([box, b])}
end)
|> rbundle[:type].put(id, {:leaf, node, box})
end
defp handle_overflow(rbundle, branch) do
n = hd(branch)
{node_n, new_node} = split(rbundle, n)
treeck = rbundle.tree |> rbundle[:type].put(:ticket, new_node.next_ticket)
if is_root?(rbundle, n) do
{new_root, ticket} = Node.new(rbundle.seeding, treeck |> rbundle[:type].get(:ticket))
treeck = treeck |> rbundle[:type].put(:ticket, ticket)
root_bbox = Utils.combine_multiple([node_n.bbox, new_node.bbox])
treeck =
treeck
|> rbundle[:type].put(new_node.id, {new_node.childs, new_root, new_node.bbox})
|> rbundle[:type].replace!(node_n.id, {node_n.childs, new_root, node_n.bbox})
|> rbundle[:type].replace!(:root, new_root)
|> rbundle[:type].put(new_root, {[node_n.id, new_node.id], nil, root_bbox})
new_node.childs
|> Enum.reduce(treeck, fn c, acc ->
acc |> rbundle[:type].update!(c, fn {ch, _d, b} -> {ch, new_node.id, b} end)
end)
else
parent = hd(tl(branch))
treeck =
treeck
|> rbundle[:type].put(new_node.id, {new_node.childs, parent, new_node.bbox})
|> rbundle[:type].replace!(node_n.id, {node_n.childs, parent, node_n.bbox})
|> rbundle[:type].update!(parent, fn {ch, d, b} ->
{[new_node.id] ++ ch, d, Utils.combine_multiple([b, new_node.bbox])}
end)
updated_tree =
new_node.childs
|> Enum.reduce(treeck, fn c, acc ->
acc |> rbundle[:type].update!(c, fn {ch, _d, b} -> {ch, new_node.id, b} end)
end)
if length(updated_tree |> rbundle[:type].get(parent) |> elem(0)) > rbundle.width,
do: handle_overflow(%{rbundle | tree: updated_tree}, tl(branch)),
else: updated_tree
end
end
defp split(rbundle, node) do
sorted_nodes =
rbundle.tree
|> rbundle[:type].get(node)
|> Utils.tuple_value(:childs)
|> Enum.map(fn n ->
box = rbundle.tree |> rbundle[:type].get(n) |> Utils.tuple_value(:bbox)
{box |> Utils.middle_value(), n, box}
end)
|> Enum.sort()
|> Enum.map(fn {_x, y, z} -> {y, z} end)
{n_id, n_bbox} =
sorted_nodes |> Enum.slice(0..((rbundle.width / 2 - 1) |> Kernel.trunc())) |> Enum.unzip()
{dn_id, dn_bbox} =
sorted_nodes
|> Enum.slice(((rbundle.width / 2) |> Kernel.trunc())..(length(sorted_nodes) - 1))
|> Enum.unzip()
{new_node, next_ticket} =
Node.new(rbundle.seeding, rbundle.tree |> rbundle[:type].get(:ticket))
n_bounds = n_bbox |> Utils.combine_multiple()
dn_bounds = dn_bbox |> Utils.combine_multiple()
{%{id: node, childs: n_id, bbox: n_bounds},
%{id: new_node, childs: dn_id, bbox: dn_bounds, next_ticket: next_ticket}}
end
defp best_subtree(rbundle, leaf) do
find_best_subtree(rbundle, get_root(rbundle), leaf, [])
end
defp find_best_subtree(rbundle, root, {_id, box} = leaf, track) do
childs = rbundle.tree |> rbundle[:type].get(root) |> Utils.tuple_value(:childs)
if is_list(childs) and length(childs) > 0 do
winner = get_best_candidate(rbundle, childs, box)
new_track = [root] ++ track
find_best_subtree(rbundle, winner, leaf, new_track)
else
if is_atom(childs), do: track, else: [root] ++ track
end
end
defp get_best_candidate(rbundle, candidates, box) do
win_entry =
candidates
|> Enum.reduce_while(%{id: :not_id, cost: :infinity}, fn c, acc ->
cbox = rbundle.tree |> rbundle[:type].get(c) |> Utils.tuple_value(:bbox)
if Utils.contained?(cbox, box) do
{:halt, %{id: c, cost: 0}}
else
enlargement = Utils.enlargement_area(cbox, box)
if enlargement < acc |> Map.get(:cost) do
{:cont, %{id: c, cost: enlargement}}
else
{:cont, acc}
end
end
end)
win_entry[:id]
end
## Query
defp find_match_leaves(rbundle, box, dig, leaves, flood) do
f = hd(dig)
tail = if length(dig) > 1, do: tl(dig), else: []
{content, _dad, fbox} = rbundle.tree |> rbundle[:type].get(f)
{new_dig, new_leaves, new_flood} =
if Utils.overlap?(fbox, box) do
if is_atom(content) do
{tail, [f] ++ leaves, flood}
else
if Utils.contained?(box, fbox),
do: {tail, leaves, [f] ++ flood},
else: {content ++ tail, leaves, flood}
end
else
{tail, leaves, flood}
end
if length(new_dig) > 0 do
find_match_leaves(rbundle, box, new_dig, new_leaves, new_flood)
else
new_leaves ++ explore_flood(rbundle, new_flood)
end
end
defp explore_flood(rbundle, flood) do
next_floor =
flood
|> Enum.flat_map(fn x ->
case rbundle.tree |> rbundle[:type].get(x) |> Utils.tuple_value(:childs) do
:leaf -> []
any -> any
end
end)
if length(next_floor) > 0, do: explore_flood(rbundle, next_floor), else: flood
end
defp find_match_depth(rbundle, box, dig, leaves, depth) do
{f, cdepth} = hd(dig)
tail = if length(dig) > 1, do: tl(dig), else: []
{content, _dad, fbox} = rbundle.tree |> rbundle[:type].get(f)
{new_dig, new_leaves} =
if Utils.overlap?(fbox, box) do
if cdepth < depth and is_list(content) do
childs = content |> Enum.map(fn c -> {c, cdepth + 1} end)
{childs ++ tail, leaves}
else
{tail, [f] ++ leaves}
end
else
{tail, leaves}
end
if length(new_dig) > 0,
do: find_match_depth(rbundle, box, new_dig, new_leaves, depth),
else: new_leaves
end
## Delete
defp remove(rbundle, id) do
{_ch, parent, removed_bbox} = rbundle.tree |> rbundle[:type].get(id)
if parent do
tree_updated =
rbundle.tree
|> rbundle[:type].delete(id)
|> rbundle[:type].update!(parent, fn {ch, daddy, b} -> {ch -- [id], daddy, b} end)
parent_childs = tree_updated |> rbundle[:type].get(parent) |> elem(0)
if length(parent_childs) > 0 do
%{rbundle | tree: tree_updated} |> recursive_update(parent, removed_bbox, :deletion)
else
remove(%{rbundle | tree: tree_updated}, parent)
end
else
rbundle.tree
|> rbundle[:type].update!(id, fn {ch, daddy, _b} -> {ch, daddy, [{0, 0}, {0, 0}]} end)
end
end
## Hard update
defp update(rbundle, id, {old_box, new_box}) do
parent = rbundle.tree |> rbundle[:type].get(id) |> Utils.tuple_value(:dad)
parent_box = rbundle.tree |> rbundle[:type].get(parent) |> Utils.tuple_value(:bbox)
updated_tree =
rbundle.tree |> rbundle[:type].update!(id, fn {ch, d, _b} -> {ch, d, new_box} end)
local_rbundle = %{rbundle | tree: updated_tree}
if Utils.contained?(parent_box, new_box) do
if Utils.in_border?(parent_box, old_box) do
if rbundle.verbose,
do:
Logger.debug(
cyan() <>
"[" <>
color(195) <>
"Update" <>
cyan() <>
"] Good case: new box " <>
yellow() <>
"(#{new_box |> Kernel.inspect()})" <>
cyan() <>
" of " <>
yellow() <>
"[#{id}]" <>
cyan() <>
" reduce the parent " <> yellow() <> "(['#{parent}'])" <> cyan() <> " box"
)
local_rbundle |> recursive_update(parent, old_box, :deletion)
else
if rbundle.verbose,
do:
Logger.debug(
cyan() <>
"[" <>
color(195) <>
"Update" <>
cyan() <>
"] Best case: new box " <>
yellow() <>
"(#{new_box |> Kernel.inspect()})" <>
cyan() <>
" of " <>
yellow() <>
"[#{id}]" <>
cyan() <> " was contained by his parent " <> yellow() <> "(['#{parent}'])"
)
local_rbundle.tree
end
else
case local_rbundle
|> node_brothers(parent)
|> (fn b -> good_slot?(local_rbundle, b, new_box) end).() do
{new_parent, _new_brothers, _new_parent_box} ->
if rbundle.verbose,
do:
Logger.debug(
cyan() <>
"[" <>
color(195) <>
"Update" <>
cyan() <>
"] Neutral case: new box " <>
yellow() <>
"(#{new_box |> Kernel.inspect()})" <>
cyan() <>
" of " <>
yellow() <>
"[#{id}]" <>
cyan() <>
" increases the parent box but there is an available slot at one uncle " <>
yellow() <> "(['#{new_parent}'])"
)
triple_s(local_rbundle, parent, new_parent, {id, old_box})
nil ->
if Utils.area(parent_box) >= @max_area do
if rbundle.verbose,
do:
Logger.debug(
cyan() <>
"[" <>
color(195) <>
"Update" <>
cyan() <>
"] Worst case: new box " <>
yellow() <>
"(#{new_box |> Kernel.inspect()})" <>
cyan() <>
" of " <>
yellow() <>
"[#{id}]" <>
cyan() <>
" increases the parent box which was so big " <>
yellow() <>
"#{((Utils.area(parent_box) |> Kernel.trunc()) / @max_area * 100) |> Kernel.trunc()} %. " <>
cyan() <>
"So we proceed to delete " <>
yellow() <> "[#{id}]" <> cyan() <> " and reinsert at tree"
)
local_rbundle |> top_down({id, new_box})
else
if rbundle.verbose,
do:
Logger.debug(
cyan() <>
"[" <>
color(195) <>
"Update" <>
cyan() <>
"] Bad case: new box " <>
yellow() <>
"(#{new_box |> Kernel.inspect()})" <>
cyan() <>
" of " <>
yellow() <>
"[#{id}]" <>
cyan() <>
" increases the parent box which isn't that big yet " <>
yellow() <>
"#{((Utils.area(parent_box) |> Kernel.trunc()) / @max_area * 100) |> Kernel.trunc()} %. " <>
cyan() <>
"So we proceed to increase parent " <>
yellow() <> "(['#{parent}'])" <> cyan() <> " box"
)
local_rbundle |> recursive_update(parent, new_box, :insertion)
end
end
end
end
## Common updates
defp top_down(rbundle, {id, box}) do
%{rbundle | tree: rbundle |> remove(id)} |> tree_insert({id, box})
end
# Recursive bbox updates when you have node path from root (at insertion)
defp recursive_update(rbundle, path, {_id, box} = leaf, :insertion) when length(path) > 0 do
{modified, t} = update_node_bbox(rbundle, hd(path), box, :insertion)
if modified and length(path) > 1,
do: recursive_update(%{rbundle | tree: t}, tl(path), leaf, :insertion),
else: rbundle.tree
end
# Recursive bbox updates when u dont have node path from root, so you have to query parents map... (at delete)
defp recursive_update(rbundle, node, box, mode) when is_list(node) |> Kernel.not() do
{modified, t} = update_node_bbox(rbundle, node, box, mode)
next = rbundle.tree |> rbundle[:type].get(node) |> Utils.tuple_value(:dad)
if modified and next, do: recursive_update(%{rbundle | tree: t}, next, box, mode), else: t
end
# Typical dumbass safe method
defp recursive_update(rbundle, _path, _leaf, :insertion) do
rbundle.tree
end
defp update_node_bbox(rbundle, node, the_box, action) do
node_box = rbundle.tree |> rbundle[:type].get(node) |> Utils.tuple_value(:bbox)
new_bbox =
case action do
:insertion ->
Utils.combine(node_box, the_box)
:deletion ->
if Utils.in_border?(node_box, the_box) do
rbundle.tree
|> rbundle[:type].get(node)
|> Utils.tuple_value(:childs)
|> Enum.map(fn c ->
rbundle.tree |> rbundle[:type].get(c) |> Utils.tuple_value(:bbox)
end)
|> Utils.combine_multiple()
else
node_box
end
end
bbox_mutation(rbundle, node, new_bbox, node_box)
end
defp bbox_mutation(rbundle, node, new_bbox, node_box) do
if new_bbox == node_box do
{false, rbundle.tree}
else
t = rbundle.tree |> rbundle[:type].update!(node, fn {ch, d, _b} -> {ch, d, new_bbox} end)
{true, t}
end
end
# Return the brothers of the node [{brother_id, brother_childs, brother_box},...]
defp node_brothers(rbundle, node) do
parent = rbundle.tree |> rbundle[:type].get(node) |> Utils.tuple_value(:dad)
rbundle.tree
|> rbundle[:type].get(parent)
|> Utils.tuple_value(:childs)
|> (fn c -> if c, do: c -- [node], else: [] end).()
|> Enum.map(fn b ->
tuple = rbundle.tree |> rbundle[:type].get(b)
{b, tuple |> Utils.tuple_value(:childs), tuple |> Utils.tuple_value(:bbox)}
end)
end
# Find a good slot (at bros/brothers list) for the box, it means that the brother hasnt the max childs and the box is at the limits of his own
defp good_slot?(rbundle, bros, box) do
bros
|> Enum.find(fn {_bid, bchilds, bbox} ->
length(bchilds) < rbundle.width and Utils.contained?(bbox, box)
end)
end
end

View File

@@ -1,13 +0,0 @@
defmodule DDRT.DynamicRtreeImpl.BoundingBoxGenerator do
@moduledoc false
def generate(n, size, result) do
s = size / 2
x = Enum.random(-180..180)
y = Enum.random(-90..90)
if n > 0,
do: generate(n - 1, size, [[{x - s, x + s}, {y - s, y + s}]] ++ result),
else: result
end
end

View File

@@ -1,7 +0,0 @@
defmodule DDRT.DynamicRtreeImpl.Node do
@moduledoc false
def new(gen, seed) do
gen[:next].(seed)
end
end

View File

@@ -1,118 +0,0 @@
defmodule DDRT.DynamicRtreeImpl.Utils do
@moduledoc false
def format_bbox([{min_x, max_x} = x, {min_y, max_y} = y]) do
%{
x: x,
y: y,
xm: min_x,
xM: max_x,
ym: min_y,
yM: max_y
}
end
def tuple_value(raw, _atom) when raw == nil do
nil
end
def tuple_value(raw, atom) do
case atom do
:childs -> raw |> elem(0)
:dad -> raw |> elem(1)
:bbox -> raw |> elem(2)
end
end
# Combine two bounding boxes into one
def combine(box1, box2) do
a = box1 |> format_bbox
b = box2 |> format_bbox
xm = Kernel.min(a.xm, b.xm)
xM = Kernel.max(a.xM, b.xM)
ym = Kernel.min(a.ym, b.ym)
yM = Kernel.max(a.yM, b.yM)
result = [{xm, xM}, {ym, yM}]
result = if area(box1) === 0, do: box2, else: result
if area(box2) === 0, do: box1, else: result
end
# Combine multiple bbox
def combine_multiple(list) when length(list) > 1 do
real_list = list |> Enum.filter(fn x -> area(x) > 0 end)
tl(real_list)
|> Enum.reduce(hd(real_list), fn [{a, b}, {c, d}] = _e, [{x, y}, {z, w}] = _acc ->
[{Kernel.min(a, x), Kernel.max(b, y)}, {Kernel.min(c, z), Kernel.max(d, w)}]
end)
end
def combine_multiple(list) do
hd(list)
end
# Returns de percent of the overlap area (of the box1) between box1 and box2
def overlap_area(box1, box2) do
a = box1 |> format_bbox
b = box2 |> format_bbox
x_overlap = Kernel.max(0, Kernel.min(a.xM, b.xM) - Kernel.max(a.xm, b.xm))
y_overlap = Kernel.max(0, Kernel.min(a.yM, b.yM) - Kernel.max(a.ym, b.ym))
x_overlap * y_overlap / area(box1) * 100
end
# Return if those 2 boxes are overlapping
def overlap?(box1, box2) do
if overlap_area(box1, box2) > 0, do: true, else: false
end
# Return if box 1 contains box 2
def contained?(box1, box2) do
a = box1 |> format_bbox
b = box2 |> format_bbox
a.xm <= b.xm and a.xM >= b.xM and a.ym <= b.ym and a.yM >= b.yM
end
# Enlargement area after adding new box
def enlargement_area(box, new_box) do
a1 = area(box)
a2 = combine_multiple([box, new_box]) |> area
a2 - a1
end
# Checks if box is at some border of parent_box
def in_border?(parent_box, box) do
p = parent_box |> format_bbox
b = box |> format_bbox
p.xm == b.xm or p.xM == b.xM or p.ym == b.ym or p.yM == b.yM
end
# Return the area of a bounding box
def area([{a, b}, {c, d}]) do
ab = b - a
cd = d - c
cond do
ab == 0 and cd != 0 -> cd
ab != 0 and cd == 0 -> ab
ab != 0 and cd != 0 -> ab * cd
ab == 0 and cd == 0 -> -1
end
end
# Return the middle bounding box value
def middle_value([{a, b}, {c, d}]) do
(a + b + c + d) / 2
end
def get_posxy([{a, b}, {c, d}]) do
%{x: (b + a) / 2, y: (c + d) / 2}
end
def box_move([{a, b}, {c, d}], move) do
x = move[:x]
y = move[:y]
[{a + x, b + x}, {c + y, d + y}]
end
end

View File

@@ -274,7 +274,7 @@ defmodule WandererApp.Esi.ApiClient do
) )
def get_alliance_info(eve_id, opts \\ []) do def get_alliance_info(eve_id, opts \\ []) do
case _get_alliance_info(eve_id, "", opts) do case _get_alliance_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.merge(%{"eve_id" => eve_id})} {:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error} {:error, error} -> {:error, error}
end end
end end
@@ -286,7 +286,7 @@ defmodule WandererApp.Esi.ApiClient do
) )
def get_corporation_info(eve_id, opts \\ []) do def get_corporation_info(eve_id, opts \\ []) do
case _get_corporation_info(eve_id, "", opts) do case _get_corporation_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.merge(%{"eve_id" => eve_id})} {:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error} {:error, error} -> {:error, error}
end end
end end
@@ -301,7 +301,7 @@ defmodule WandererApp.Esi.ApiClient do
"/characters/#{eve_id}/", "/characters/#{eve_id}/",
opts |> _with_cache_opts() opts |> _with_cache_opts()
) do ) do
{:ok, result} -> {:ok, result |> Map.merge(%{"eve_id" => eve_id})} {:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error} {:error, error} -> {:error, error}
end end
end end

View File

@@ -67,8 +67,8 @@ defmodule WandererApp.EveDataService do
end end
def load_wormhole_types() do def load_wormhole_types() do
JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholes.json") JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholes.json")
|> JSONUtil.map_json(fn row -> |> Enum.map(fn row ->
%{ %{
id: row["typeID"], id: row["typeID"],
name: row["name"], name: row["name"],
@@ -85,8 +85,8 @@ defmodule WandererApp.EveDataService do
end end
def load_wormhole_classes() do def load_wormhole_classes() do
JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeClasses.json") JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeClasses.json")
|> JSONUtil.map_json(fn row -> |> Enum.map(fn row ->
%{ %{
id: row["id"], id: row["id"],
short_name: row["shortName"], short_name: row["shortName"],
@@ -98,8 +98,8 @@ defmodule WandererApp.EveDataService do
end end
def load_wormhole_systems() do def load_wormhole_systems() do
JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeSystems.json") JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeSystems.json")
|> JSONUtil.map_json(fn row -> |> Enum.map(fn row ->
%{ %{
solar_system_id: row["solarSystemID"], solar_system_id: row["solarSystemID"],
wanderers: row["wanderers"], wanderers: row["wanderers"],
@@ -111,8 +111,8 @@ defmodule WandererApp.EveDataService do
end end
def load_effects() do def load_effects() do
JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/effects.json") JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/effects.json")
|> JSONUtil.map_json(fn row -> |> Enum.map(fn row ->
%{ %{
id: row["name"] |> Slug.slugify(), id: row["name"] |> Slug.slugify(),
name: row["name"], name: row["name"],
@@ -130,8 +130,8 @@ defmodule WandererApp.EveDataService do
end end
def load_triglavian_systems() do def load_triglavian_systems() do
JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/triglavianSystems.json") JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/triglavianSystems.json")
|> JSONUtil.map_json(fn row -> |> Enum.map(fn row ->
%{ %{
solar_system_id: row["solarSystemID"], solar_system_id: row["solarSystemID"],
solar_system_name: row["solarSystemName"], solar_system_name: row["solarSystemName"],
@@ -377,9 +377,21 @@ defmodule WandererApp.EveDataService do
end end
end end
defp get_true_security(security) when is_float(security) and security > 0.0 and security < 0.05, do: security |> Float.ceil(1) defp truncate_to_two_digits(value) when is_float(value), do: Float.floor(value * 100) / 100
defp get_true_security(security) when is_float(security), do: security |> Float.floor(1) defp get_true_security(security) when is_float(security) and security > 0.0 and security < 0.05,
do: security |> Float.ceil(1)
defp get_true_security(security) when is_float(security) do
truncated_value = security |> truncate_to_two_digits()
floor_value = truncated_value |> Float.floor(1)
if Float.round(truncated_value - floor_value, 2) < 0.05 do
floor_value
else
Float.ceil(truncated_value, 1)
end
end
defp get_class_title(wormhole_classes_info, wormhole_class_id, security, wormhole_class) do defp get_class_title(wormhole_classes_info, wormhole_class_id, security, wormhole_class) do
case wormhole_class_id in [ case wormhole_class_id in [

View File

@@ -56,9 +56,9 @@ defmodule WandererApp.Map do
map map
|> Map.get(:options) |> Map.get(:options)
|> case do |> case do
nil -> %{"layout" => "left_to_right"} nil -> %{"layout" => "left_to_right"}
options -> Jason.decode!(options) options -> Jason.decode!(options)
end end
end end
def update_map(map_id, map_update) do def update_map(map_id, map_update) do

View File

@@ -376,17 +376,21 @@ defmodule WandererApp.Map.Server.Impl do
case not is_nil(user_id) do case not is_nil(user_id) do
true -> true ->
:telemetry.execute( {:ok, _} =
[:wanderer_app, :map, :systems, :remove], WandererApp.User.ActivityTracker.track_map_event(:systems_removed, %{
%{count: removed_ids |> Enum.count()},
%{
character_id: character_id, character_id: character_id,
user_id: user_id, user_id: user_id,
map_id: map_id, map_id: map_id,
solar_system_ids: removed_ids solar_system_ids: removed_ids
} })
:telemetry.execute(
[:wanderer_app, :map, :systems, :remove],
%{count: removed_ids |> Enum.count()}
) )
:ok
_ -> _ ->
:ok :ok
end end
@@ -852,10 +856,9 @@ defmodule WandererApp.Map.Server.Impl do
location.solar_system_id location.solar_system_id
) do ) do
true -> true ->
{:ok, character} = WandererApp.Character.get_character(character_id)
:ok = maybe_add_system(map_id, location, old_location, rtree_name, map_opts) :ok = maybe_add_system(map_id, location, old_location, rtree_name, map_opts)
:ok = maybe_add_system(map_id, old_location, location, rtree_name, map_opts) :ok = maybe_add_system(map_id, old_location, location, rtree_name, map_opts)
:ok = maybe_add_connection(map_id, location, old_location, character) :ok = maybe_add_connection(map_id, location, old_location, character_id)
_ -> _ ->
:ok :ok
@@ -1169,12 +1172,15 @@ defmodule WandererApp.Map.Server.Impl do
broadcast!(map_id, :add_system, system) broadcast!(map_id, :add_system, system)
:telemetry.execute([:wanderer_app, :map, :system, :add], %{count: 1}, %{ {:ok, _} =
character_id: character_id, WandererApp.User.ActivityTracker.track_map_event(:system_added, %{
user_id: user_id, character_id: character_id,
map_id: map_id, user_id: user_id,
solar_system_id: solar_system_id map_id: map_id,
}) solar_system_id: solar_system_id
})
:telemetry.execute([:wanderer_app, :map, :system, :add], %{count: 1})
state state
end end
@@ -1583,20 +1589,32 @@ defmodule WandererApp.Map.Server.Impl do
defp maybe_remove_connection(_map_id, _location, _old_location), do: :ok defp maybe_remove_connection(_map_id, _location, _old_location), do: :ok
defp maybe_add_connection(map_id, location, old_location, character) defp maybe_add_connection(map_id, location, old_location, character_id)
when not is_nil(location) and not is_nil(old_location) and when not is_nil(location) and not is_nil(old_location) and
not is_nil(old_location.solar_system_id) and not is_nil(old_location.solar_system_id) and
location.solar_system_id != old_location.solar_system_id do location.solar_system_id != old_location.solar_system_id do
case character do character_id
|> WandererApp.Character.get_character!()
|> case do
nil -> nil ->
:ok :ok
_ -> character ->
:telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{ :telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{})
map_id: map_id,
character: character, {:ok, _} =
solar_system_source_id: old_location.solar_system_id, WandererApp.Api.MapChainPassages.new(%{
solar_system_target_id: location.solar_system_id map_id: map_id,
character_id: character_id,
ship_type_id: character.ship,
ship_name: character.ship_name,
solar_system_source_id: old_location.solar_system_id,
solar_system_target_id: location.solar_system_id
})
broadcast!(map_id, :maybe_select_system, %{
character_id: character_id,
solar_system_id: location.solar_system_id
}) })
end end
@@ -1618,7 +1636,7 @@ defmodule WandererApp.Map.Server.Impl do
end end
end end
defp maybe_add_connection(_map_id, _location, _old_location, _character), do: :ok defp maybe_add_connection(_map_id, _location, _old_location, _character_id), do: :ok
defp maybe_add_system(map_id, location, old_location, rtree_name, opts) defp maybe_add_system(map_id, location, old_location, rtree_name, opts)
when not is_nil(location) do when not is_nil(location) do
@@ -1698,10 +1716,10 @@ defmodule WandererApp.Map.Server.Impl do
defp calc_new_system_position(map_id, old_location, rtree_name, opts), defp calc_new_system_position(map_id, old_location, rtree_name, opts),
do: do:
{:ok, {:ok,
map_id map_id
|> WandererApp.Map.find_system_by_location(old_location) |> WandererApp.Map.find_system_by_location(old_location)
|> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name, opts)} |> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name, opts)}
defp _broadcast_acl_updates( defp _broadcast_acl_updates(
{:ok, {:ok,

View File

@@ -55,13 +55,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
def handle_info({ref, result}, state) do def handle_info({ref, result}, state) do
Process.demonitor(ref, [:flush]) Process.demonitor(ref, [:flush])
case result do {:noreply, state}
:ok ->
{:noreply, state}
_ ->
{:noreply, state}
end
end end
defp _update_map_kills(map_id) do defp _update_map_kills(map_id) do
@@ -70,10 +64,9 @@ defmodule WandererApp.Map.ZkbDataFetcher do
map_id map_id
|> WandererApp.Map.get_map!() |> WandererApp.Map.get_map!()
|> Map.get(:systems, Map.new()) |> Map.get(:systems, Map.new())
|> Map.keys() |> Enum.reduce(Map.new(), fn {solar_system_id, _system}, acc ->
|> Enum.reduce(Map.new(), fn solar_system_id, acc ->
kills_count = WandererApp.Cache.get("zkb_kills_#{solar_system_id}") kills_count = WandererApp.Cache.get("zkb_kills_#{solar_system_id}")
acc |> Map.put_new(solar_system_id, kills_count || 0) acc |> Map.put(solar_system_id, kills_count || 0)
end) end)
|> _maybe_broadcast_map_kills(map_id) |> _maybe_broadcast_map_kills(map_id)
@@ -87,28 +80,24 @@ defmodule WandererApp.Map.ZkbDataFetcher do
updated_kills_system_ids = updated_kills_system_ids =
new_kills_map new_kills_map
|> Map.keys() |> Map.filter(fn {solar_system_id, new_kills_count} ->
|> Enum.filter(fn solar_system_id ->
kills_count = new_kills_map |> Map.get(solar_system_id, 0)
old_kills_count = old_kills_map |> Map.get(solar_system_id, 0)
kills_count != old_kills_count and
kills_count > 0
end)
removed_kills_system_ids =
old_kills_map
|> Map.keys()
|> Enum.filter(fn solar_system_id ->
new_kills_count = new_kills_map |> Map.get(solar_system_id, 0)
old_kills_count = old_kills_map |> Map.get(solar_system_id, 0) old_kills_count = old_kills_map |> Map.get(solar_system_id, 0)
new_kills_count != old_kills_count and new_kills_count != old_kills_count and
old_kills_count > 0 and new_kills_count == 0 new_kills_count > 0
end) end)
|> Map.keys()
[updated_kills_system_ids | removed_kills_system_ids] removed_kills_system_ids =
|> List.flatten() old_kills_map
|> Map.filter(fn {solar_system_id, old_kills_count} ->
new_kills_count = new_kills_map |> Map.get(solar_system_id, 0)
old_kills_count > 0 and new_kills_count == 0
end)
|> Map.keys()
(updated_kills_system_ids ++ removed_kills_system_ids)
|> case do |> case do
[] -> [] ->
:ok :ok

View File

@@ -9,7 +9,11 @@ defmodule WandererApp.MapConnectionRepo do
do: WandererApp.Api.MapConnection.read_by_map(%{map_id: map_id}) do: WandererApp.Api.MapConnection.read_by_map(%{map_id: map_id})
def get_by_locations(map_id, solar_system_source, solar_system_target) do def get_by_locations(map_id, solar_system_source, solar_system_target) do
WandererApp.Api.MapConnection.by_locations(%{map_id: map_id, solar_system_source: solar_system_source, solar_system_target: solar_system_target}) WandererApp.Api.MapConnection.by_locations(%{
map_id: map_id,
solar_system_source: solar_system_source,
solar_system_target: solar_system_target
})
|> case do |> case do
{:ok, connections} -> {:ok, connections} ->
{:ok, connections} {:ok, connections}
@@ -26,8 +30,11 @@ defmodule WandererApp.MapConnectionRepo do
def create!(connection), do: connection |> WandererApp.Api.MapConnection.create!() def create!(connection), do: connection |> WandererApp.Api.MapConnection.create!()
def destroy(map_id, connection) do def destroy(map_id, connection) do
{:ok, from_connections} = get_by_locations(map_id, connection.solar_system_source, connection.solar_system_target) {:ok, from_connections} =
{:ok, to_connections} = get_by_locations(map_id, connection.solar_system_target, connection.solar_system_source) get_by_locations(map_id, connection.solar_system_source, connection.solar_system_target)
{:ok, to_connections} =
get_by_locations(map_id, connection.solar_system_target, connection.solar_system_source)
[from_connections ++ to_connections] [from_connections ++ to_connections]
|> List.flatten() |> List.flatten()
@@ -42,8 +49,7 @@ defmodule WandererApp.MapConnectionRepo do
end end
end end
def destroy!(connection), do: def destroy!(connection), do: connection |> WandererApp.Api.MapConnection.destroy!()
connection |> WandererApp.Api.MapConnection.destroy!()
def bulk_destroy!(connections) do def bulk_destroy!(connections) do
connections connections
@@ -51,6 +57,7 @@ defmodule WandererApp.MapConnectionRepo do
|> case do |> case do
%Ash.BulkResult{status: :success} -> %Ash.BulkResult{status: :success} ->
:ok :ok
error -> error ->
error error
end end

View File

@@ -0,0 +1,49 @@
defmodule WandererApp.MapUserSettingsRepo do
use WandererApp, :repository
@default_form_data %{"select_on_spash" => "false"}
def get(map_id, user_id) do
map_id
|> WandererApp.Api.MapUserSettings.by_user_id(user_id)
|> case do
{:ok, settings} ->
{:ok, settings}
_ ->
{:ok, nil}
end
end
def get!(map_id, user_id) do
WandererApp.Api.MapUserSettings.by_user_id(map_id, user_id)
|> case do
{:ok, user_settings} -> user_settings
_ -> nil
end
end
def create_or_update(map_id, user_id, settings) do
get!(map_id, user_id)
|> case do
user_settings when not is_nil(user_settings) ->
user_settings
|> WandererApp.Api.MapUserSettings.update_settings(%{settings: settings})
_ ->
WandererApp.Api.MapUserSettings.create(%{
map_id: map_id,
user_id: user_id,
settings: settings
})
end
end
def to_form_data(nil), do: {:ok, @default_form_data}
def to_form_data(%{settings: settings} = _user_settings), do: {:ok, Jason.decode!(settings)}
def to_form_data!(user_settings) do
{:ok, data} = to_form_data(user_settings)
data
end
end

View File

@@ -0,0 +1,9 @@
defmodule WandererApp.TaskWrapper do
def start_link(module, func, args) do
if Mix.env() == :test do
apply(module, func, args)
else
Task.start_link(module, func, args)
end
end
end

View File

@@ -1,114 +1,16 @@
defmodule WandererApp.User.ActivityTracker do defmodule WandererApp.User.ActivityTracker do
@moduledoc false @moduledoc false
use GenServer
require Logger require Logger
@name __MODULE__ def track_map_event(
event_type,
metadata
),
do: WandererApp.Map.Audit.track_map_event(event_type, metadata)
def start_link(args) do def track_acl_event(
GenServer.start(__MODULE__, args, name: @name) event_type,
end metadata
),
@impl true do: WandererApp.Map.Audit.track_acl_event(event_type, metadata)
def init(_args) do
Logger.info("#{__MODULE__} started")
{:ok, %{}, {:continue, :start}}
end
@impl true
def handle_continue(:start, state) do
:telemetry.attach_many(
"map_user_activity",
[
[:wanderer_app, :map, :hub, :add],
[:wanderer_app, :map, :hub, :remove],
[:wanderer_app, :map, :system, :add],
[:wanderer_app, :map, :system, :update],
[:wanderer_app, :map, :systems, :remove],
[:wanderer_app, :map, :connection, :add],
[:wanderer_app, :map, :connection, :update],
[:wanderer_app, :map, :connection, :remove],
[:wanderer_app, :map, :acl, :add],
[:wanderer_app, :map, :acl, :remove],
[:wanderer_app, :acl, :member, :add],
[:wanderer_app, :acl, :member, :remove],
[:wanderer_app, :acl, :member, :update]
],
&handle_event/4,
nil
)
{:noreply, state}
end
def handle_event([:wanderer_app, :map, :system, :add], _event_data, metadata, _config) do
{:ok, _} = _track_map_event(:system_added, metadata)
end
def handle_event([:wanderer_app, :map, :hub, :add], _event_data, metadata, _config) do
{:ok, _} = _track_map_event(:hub_added, metadata)
end
def handle_event([:wanderer_app, :map, :hub, :remove], _event_data, metadata, _config) do
{:ok, _} = _track_map_event(:hub_removed, metadata)
end
def handle_event([:wanderer_app, :map, :system, :update], _event_data, metadata, _config) do
{:ok, _} = _track_map_event(:system_updated, metadata)
end
def handle_event([:wanderer_app, :map, :systems, :remove], _event_data, metadata, _config) do
{:ok, _} = _track_map_event(:systems_removed, metadata)
end
def handle_event([:wanderer_app, :map, :connection, :add], _event_data, metadata, _config) do
{:ok, _} = _track_map_event(:map_connection_added, metadata)
end
def handle_event([:wanderer_app, :map, :connection, :update], _event_data, metadata, _config) do
{:ok, _} = _track_map_event(:map_connection_updated, metadata)
end
def handle_event([:wanderer_app, :map, :connection, :remove], _event_data, metadata, _config) do
{:ok, _} = _track_map_event(:map_connection_removed, metadata)
end
def handle_event([:wanderer_app, :acl, :member, :add], _event_data, metadata, _config) do
{:ok, _} = _track_acl_event(:map_acl_member_added, metadata)
end
def handle_event([:wanderer_app, :acl, :member, :remove], _event_data, metadata, _config) do
{:ok, _} = _track_acl_event(:map_acl_member_removed, metadata)
end
def handle_event([:wanderer_app, :acl, :member, :update], _event_data, metadata, _config) do
{:ok, _} = _track_acl_event(:map_acl_member_updated, metadata)
end
def handle_event([:wanderer_app, :map, :acl, :add], _event_data, _metadata, _config) do
# {:ok, _} = _track_map_event(:map_acl_added, metadata)
end
def handle_event([:wanderer_app, :map, :acl, :remove], _event_data, _metadata, _config) do
# {:ok, _} = _track_map_event(:map_acl_removed, metadata)
end
defp _track_map_event(
event_type,
metadata
),
do: WandererApp.Map.Audit.track_map_event(event_type, metadata)
defp _track_acl_event(
event_type,
metadata
),
do: WandererApp.Map.Audit.track_acl_event(event_type, metadata)
@impl true
def terminate(_reason, _state) do
:ok
end
end end

View File

@@ -6,14 +6,9 @@ defmodule WandererApp.Utils.JSONUtil do
Jason.decode(body) Jason.decode(body)
end end
def map_json({:ok, json}, mapper) do def read_json!(filename),
Enum.map(json, mapper) do:
end filename
|> File.read!()
def compress(data) do |> Jason.decode!()
data
|> Jason.encode!()
|> :zlib.compress()
|> Base.encode64()
end
end end

View File

@@ -63,7 +63,7 @@ defmodule WandererApp.Zkb.KillsProvider do
end end
defp handle_websocket(message, state) do defp handle_websocket(message, state) do
case message |> _parse_message() do case message |> parse_message() do
nil -> nil ->
{:ok, state} {:ok, state}
@@ -109,7 +109,7 @@ defmodule WandererApp.Zkb.KillsProvider do
Logger.warning(fn -> "Terminating client process with reason : #{inspect(reason)}" end) Logger.warning(fn -> "Terminating client process with reason : #{inspect(reason)}" end)
end end
defp _parse_message( defp parse_message(
%{ %{
"solar_system_id" => solar_system_id, "solar_system_id" => solar_system_id,
"killmail_time" => killmail_time "killmail_time" => killmail_time
@@ -123,5 +123,5 @@ defmodule WandererApp.Zkb.KillsProvider do
} }
end end
defp _parse_message(_message), do: nil defp parse_message(_message), do: nil
end end

View File

@@ -377,7 +377,7 @@ defmodule WandererAppWeb.CoreComponents do
~H""" ~H"""
<div phx-feedback-for={@name} class="form-control mt-8"> <div phx-feedback-for={@name} class="form-control mt-8">
<label class="label cursor-pointer"> <label class="label cursor-pointer gap-2">
<span class="label-text"><%= @label %></span> <span class="label-text"><%= @label %></span>
<input type="hidden" name={@name} value="false" /> <input type="hidden" name={@name} value="false" />
<input <input
@@ -605,9 +605,7 @@ defmodule WandererAppWeb.CoreComponents do
phx-click={@row_click && @row_click.(row)} phx-click={@row_click && @row_click.(row)}
class={"hover #{if @row_selected && @row_selected.(row), do: "!bg-slate-600", else: ""} #{if @row_click, do: "cursor-pointer", else: ""}"} class={"hover #{if @row_selected && @row_selected.(row), do: "!bg-slate-600", else: ""} #{if @row_click, do: "cursor-pointer", else: ""}"}
> >
<td <td :for={{col, _index} <- Enum.with_index(@col)}>
:for={{col, _index} <- Enum.with_index(@col)}
>
<%= render_slot(col, @row_item.(row)) %> <%= render_slot(col, @row_item.(row)) %>
</td> </td>
<td :if={@action != []}> <td :if={@action != []}>

View File

@@ -43,6 +43,20 @@
> >
</script> </script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.js"
crossorigin="anonymous"
referrerpolicy="no-referrer"
>
</script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"
crossorigin="anonymous"
referrerpolicy="no-referrer"
>
</script>
<script defer phx-track-static type="module" src={~p"/assets/app.js"} crossorigin="anonymous"> <script defer phx-track-static type="module" src={~p"/assets/app.js"} crossorigin="anonymous">
</script> </script>
<!-- Appzi: Capture Insightful Feedback --> <!-- Appzi: Capture Insightful Feedback -->

View File

@@ -3,11 +3,15 @@ defmodule WandererAppWeb.BasicAuth do
def admin_basic_auth(conn, _opts) do def admin_basic_auth(conn, _opts) do
admin_password = WandererApp.Env.admin_password() admin_password = WandererApp.Env.admin_password()
if is_nil(admin_password) do if is_nil(admin_password) do
conn conn
else else
conn conn
|> Plug.BasicAuth.basic_auth(username: WandererApp.Env.admin_username(), password: admin_password) |> Plug.BasicAuth.basic_auth(
username: WandererApp.Env.admin_username(),
password: admin_password
)
end end
end end
end end

View File

@@ -1,4 +1,5 @@
<section class="prose prose-lg max-w-full w-full leading-normal tracking-normal text-indigo-400 bg-cover bg-fixed flex items-center justify-center"> <section class="prose prose-lg max-w-full w-full leading-normal tracking-normal text-indigo-400 bg-cover bg-fixed flex items-center justify-center">
<canvas id="bg-canvas"></canvas>
<div class="h-full w-full flex flex-col items-center"> <div class="h-full w-full flex flex-col items-center">
<!--Main--> <!--Main-->
<div class="artboard artboard-horizontal phone-3 pt-10 !h-40"> <div class="artboard artboard-horizontal phone-3 pt-10 !h-40">
@@ -11,9 +12,17 @@
</div> </div>
<!--Right Col--> <!--Right Col-->
<div :if={@invite_token_valid} class="overflow-hidden"> <div :if={@invite_token_valid} class="overflow-hidden">
<.link navigate={~p"/auth/eve?invite=#{@invite_token}"}> <div class="!z-100 relative group alert items-center fade-in-scale text-white w-[224px] h-[44px] rounded p-px overflow-hidden">
<img src="https://web.ccpgamescdn.com/eveonlineassets/developers/eve-sso-login-black-large.png" /> <div class="group animate-rotate absolute inset-0 h-full w-full rounded-full bg-[conic-gradient(#0ea5e9_20deg,transparent_120deg)] group-hover:bg-[#0ea5e9]" />
</.link> <div class="!bg-black rounded w-[220px] h-[40px] flex items-center justify-center relative z-20">
<.link navigate={~p"/auth/eve?invite=#{@invite_token}"} class="opacity-100">
<img
src="https://web.ccpgamescdn.com/eveonlineassets/developers/eve-sso-login-black-large.png"
class="w-[220px] h-[40px]"
/>
</.link>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -99,8 +99,9 @@ defmodule WandererAppWeb.AccessListsLive do
end end
defp apply_action(socket, :add_members, %{"id" => acl_id} = _params) do defp apply_action(socket, :add_members, %{"id" => acl_id} = _params) do
with {:ok, %{owner: %{id: _character_id}} = access_list} <- socket.assigns.access_lists |> Enum.find(&(&1.id == acl_id)) |> Ash.load(:owner), with {:ok, %{owner: %{id: _character_id}} = access_list} <-
user_character_ids <- socket.assigns.current_user.characters |> Enum.map(& &1.id) do socket.assigns.access_lists |> Enum.find(&(&1.id == acl_id)) |> Ash.load(:owner),
user_character_ids <- socket.assigns.current_user.characters |> Enum.map(& &1.id) do
user_character_ids user_character_ids
|> Enum.each(fn user_character_id -> |> Enum.each(fn user_character_id ->
:ok = WandererApp.Character.TrackerManager.start_tracking(user_character_id) :ok = WandererApp.Character.TrackerManager.start_tracking(user_character_id)
@@ -125,7 +126,7 @@ defmodule WandererAppWeb.AccessListsLive do
) )
else else
_ -> _ ->
socket socket
end end
end end
@@ -318,11 +319,13 @@ defmodule WandererAppWeb.AccessListsLive do
@impl true @impl true
def handle_info({:search, text}, socket) do def handle_info({:search, text}, socket) do
first_character_id = active_character_id =
socket.assigns.user_character_ids socket.assigns.current_user.characters
|> Enum.filter(fn character -> not is_nil(character.refresh_token) end)
|> Enum.map(& &1.id)
|> Enum.at(0) |> Enum.at(0)
{:ok, options} = search(first_character_id, text) {:ok, options} = search(active_character_id, text)
send_update(LiveSelect.Component, options: options, id: socket.assigns.member_search_id) send_update(LiveSelect.Component, options: options, id: socket.assigns.member_search_id)
{:noreply, socket |> assign(member_search_options: options)} {:noreply, socket |> assign(member_search_options: options)}
@@ -373,12 +376,16 @@ defmodule WandererAppWeb.AccessListsLive do
member member
|> WandererApp.Api.AccessListMember.update_role!(%{role: role_atom}) |> WandererApp.Api.AccessListMember.update_role!(%{role: role_atom})
:telemetry.execute([:wanderer_app, :acl, :member, :update], %{count: 1}, %{ {:ok, _} =
user_id: socket.assigns.current_user.id, WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_updated, %{
acl_id: socket.assigns.selected_acl_id, user_id: socket.assigns.current_user.id,
member: acl_id: socket.assigns.selected_acl_id,
member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) member:
}) member
|> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role])
})
:telemetry.execute([:wanderer_app, :acl, :member, :update], %{count: 1})
Phoenix.PubSub.broadcast( Phoenix.PubSub.broadcast(
WandererApp.PubSub, WandererApp.PubSub,
@@ -488,12 +495,16 @@ defmodule WandererAppWeb.AccessListsLive do
eve_corporation_id: nil eve_corporation_id: nil
}) do }) do
{:ok, member} -> {:ok, member} ->
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}, %{ {:ok, _} =
user_id: socket.assigns.current_user.id, WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_added, %{
acl_id: access_list_id, user_id: socket.assigns.current_user.id,
member: acl_id: access_list_id,
member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) member:
}) member
|> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role])
})
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
{:ok, member} {:ok, member}
@@ -515,12 +526,16 @@ defmodule WandererAppWeb.AccessListsLive do
eve_corporation_id: eve_id eve_corporation_id: eve_id
}) do }) do
{:ok, member} -> {:ok, member} ->
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}, %{ {:ok, _} =
user_id: socket.assigns.current_user.id, WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_added, %{
acl_id: access_list_id, user_id: socket.assigns.current_user.id,
member: acl_id: access_list_id,
member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) member:
}) member
|> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role])
})
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
{:ok, member} {:ok, member}
@@ -543,12 +558,16 @@ defmodule WandererAppWeb.AccessListsLive do
role: :viewer role: :viewer
}) do }) do
{:ok, member} -> {:ok, member} ->
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}, %{ {:ok, _} =
user_id: socket.assigns.current_user.id, WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_added, %{
acl_id: access_list_id, user_id: socket.assigns.current_user.id,
member: acl_id: access_list_id,
member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) member:
}) member
|> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role])
})
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
{:ok, member} {:ok, member}
@@ -647,6 +666,6 @@ defmodule WandererAppWeb.AccessListsLive do
end end
defp map_ui_acl(acl, selected_id) do defp map_ui_acl(acl, selected_id) do
acl |> Map.merge(%{selected: acl.id == selected_id}) acl |> Map.put(:selected, acl.id == selected_id)
end end
end end

View File

@@ -44,6 +44,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do
case WandererApp.Api.MapCharacterSettings.read_by_map(%{map_id: selected_map.id}) do case WandererApp.Api.MapCharacterSettings.read_by_map(%{map_id: selected_map.id}) do
{:ok, settings} -> {:ok, settings} ->
{:ok, settings} {:ok, settings}
_ -> _ ->
{:ok, []} {:ok, []}
end end

View File

@@ -44,7 +44,6 @@ defmodule WandererAppWeb.MapLive do
id: map_id, id: map_id,
deleted: false deleted: false
} = map} -> } = map} ->
Process.send_after(self(), {:init_map, map}, 10) Process.send_after(self(), {:init_map, map}, 10)
socket socket
@@ -114,45 +113,45 @@ defmodule WandererAppWeb.MapLive do
} }
} = socket } = socket
) do ) do
_on_map_started(map_id, current_user, user_permissions) on_map_started(map_id, current_user, user_permissions)
{:noreply, socket} {:noreply, socket}
end end
@impl true @impl true
def handle_info(:character_token_invalid, socket) do def handle_info(:character_token_invalid, socket),
{:noreply, do:
socket {:noreply,
|> _put_invalid_token_message()} socket
end |> _put_invalid_token_message()}
@impl true @impl true
def handle_info(%{event: :add_system, payload: system}, socket) do def handle_info(%{event: :add_system, payload: system}, socket),
{:noreply, do:
socket {:noreply,
|> _push_map_event("add_systems", [map_ui_system(system)])} socket
end |> push_map_event("add_systems", [map_ui_system(system)])}
@impl true @impl true
def handle_info(%{event: :update_system, payload: system}, socket) do def handle_info(%{event: :update_system, payload: system}, socket),
{:noreply, do:
socket {:noreply,
|> _push_map_event("update_systems", [map_ui_system(system)])} socket
end |> push_map_event("update_systems", [map_ui_system(system)])}
@impl true @impl true
def handle_info(%{event: :update_connection, payload: connection}, socket) do def handle_info(%{event: :update_connection, payload: connection}, socket),
{:noreply, do:
socket {:noreply,
|> _push_map_event("update_connection", map_ui_connection(connection))} socket
end |> push_map_event("update_connection", map_ui_connection(connection))}
@impl true @impl true
def handle_info(%{event: :systems_removed, payload: solar_system_ids}, socket) do def handle_info(%{event: :systems_removed, payload: solar_system_ids}, socket),
{:noreply, do:
socket {:noreply,
|> _push_map_event("remove_systems", solar_system_ids)} socket
end |> push_map_event("remove_systems", solar_system_ids)}
@impl true @impl true
def handle_info(%{event: :remove_connections, payload: connections}, socket) do def handle_info(%{event: :remove_connections, payload: connections}, socket) do
@@ -160,7 +159,7 @@ defmodule WandererAppWeb.MapLive do
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"remove_connections", "remove_connections",
connection_ids connection_ids
)} )}
@@ -172,17 +171,51 @@ defmodule WandererAppWeb.MapLive do
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"add_connections", "add_connections",
connections connections
)} )}
end end
@impl true
def handle_info(
%{
event: :maybe_select_system,
payload: %{
character_id: character_id,
solar_system_id: solar_system_id
}
},
%{assigns: %{current_user: current_user, map_user_settings: map_user_settings}} = socket
) do
is_user_character? =
current_user.characters |> Enum.map(& &1.id) |> Enum.member?(character_id)
select_on_spash? =
map_user_settings
|> WandererApp.MapUserSettingsRepo.to_form_data!()
|> Map.get("select_on_spash", "false")
|> String.to_existing_atom()
socket =
(is_user_character? && select_on_spash?)
|> case do
true ->
socket
|> push_map_event("select_system", solar_system_id)
false ->
socket
end
{:noreply, socket}
end
@impl true @impl true
def handle_info(%{event: :update_map, payload: map_diff}, socket) do def handle_info(%{event: :update_map, payload: map_diff}, socket) do
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"map_updated", "map_updated",
map_diff map_diff
)} )}
@@ -196,7 +229,7 @@ defmodule WandererAppWeb.MapLive do
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"kills_updated", "kills_updated",
kills kills
)} )}
@@ -218,7 +251,7 @@ defmodule WandererAppWeb.MapLive do
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"characters_updated", "characters_updated",
characters characters
)} )}
@@ -228,7 +261,7 @@ defmodule WandererAppWeb.MapLive do
def handle_info(%{event: :character_added, payload: character}, socket) do def handle_info(%{event: :character_added, payload: character}, socket) do
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"character_added", "character_added",
character |> map_ui_character() character |> map_ui_character()
)} )}
@@ -238,7 +271,7 @@ defmodule WandererAppWeb.MapLive do
def handle_info(%{event: :character_removed, payload: character}, socket) do def handle_info(%{event: :character_removed, payload: character}, socket) do
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"character_removed", "character_removed",
character |> map_ui_character() character |> map_ui_character()
)} )}
@@ -248,7 +281,7 @@ defmodule WandererAppWeb.MapLive do
def handle_info(%{event: :character_updated, payload: character}, socket) do def handle_info(%{event: :character_updated, payload: character}, socket) do
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"character_updated", "character_updated",
character |> map_ui_character() character |> map_ui_character()
)} )}
@@ -262,7 +295,7 @@ defmodule WandererAppWeb.MapLive do
do: do:
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"present_characters", "present_characters",
present_character_eve_ids present_character_eve_ids
)} )}
@@ -336,7 +369,7 @@ defmodule WandererAppWeb.MapLive do
socket socket
|> assign(user_permissions: user_permissions) |> assign(user_permissions: user_permissions)
|> _push_map_event( |> push_map_event(
"user_permissions", "user_permissions",
user_permissions user_permissions
) )
@@ -376,17 +409,24 @@ defmodule WandererAppWeb.MapLive do
tracked_character_ids = tracked_character_ids =
availaible_map_characters |> Enum.filter(& &1.tracked) |> Enum.map(& &1.id) availaible_map_characters |> Enum.filter(& &1.tracked) |> Enum.map(& &1.id)
all_character_tracked? = (not (availaible_map_characters |> Enum.empty?())) and availaible_map_characters |> Enum.all?(& &1.tracked) all_character_tracked? =
not (availaible_map_characters |> Enum.empty?()) and
availaible_map_characters |> Enum.all?(& &1.tracked)
cond do cond do
(only_tracked_characters and can_track? and all_character_tracked?) or (only_tracked_characters and can_track? and all_character_tracked?) or
(not only_tracked_characters and can_view?) -> (not only_tracked_characters and can_view?) ->
Process.send_after(self(), {:map_init, %{ Process.send_after(
map_id: map_id, self(),
page_title: map_name, {:map_init,
user_permissions: user_permissions, %{
tracked_character_ids: tracked_character_ids map_id: map_id,
}}, 10) page_title: map_name,
user_permissions: user_permissions,
tracked_character_ids: tracked_character_ids
}},
10
)
only_tracked_characters and can_track? and not all_character_tracked? -> only_tracked_characters and can_track? and not all_character_tracked? ->
Process.send_after(self(), :not_all_characters_tracked, 10) Process.send_after(self(), :not_all_characters_tracked, 10)
@@ -406,17 +446,21 @@ defmodule WandererAppWeb.MapLive do
Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id) Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id)
{:noreply, {:noreply,
socket socket
|> assign(initial_data)} |> assign(initial_data)}
end end
def handle_info({:map_start, def handle_info(
%{ {:map_start,
map_id: map_id, %{
user_characters: user_character_eve_ids, map_id: map_id,
initial_data: initial_data, map_user_settings: map_user_settings,
events: events user_characters: user_character_eve_ids,
} = _started_data}, socket) do initial_data: initial_data,
events: events
} = _started_data},
socket
) do
socket = socket =
events events
|> Enum.reduce(socket, fn event, socket -> |> Enum.reduce(socket, fn event, socket ->
@@ -452,66 +496,78 @@ defmodule WandererAppWeb.MapLive do
end end
end) end)
Process.send_after(self(), {:map_loaded, Process.send_after(
%{ self(),
map_id: map_id, {:map_loaded,
user_characters: user_character_eve_ids, %{
initial_data: initial_data map_id: map_id,
}}, 10) initial_data: initial_data
}},
10
)
{:noreply, socket} {:noreply,
socket
|> assign(
map_user_settings: map_user_settings,
user_characters: user_character_eve_ids,
has_tracked_characters?: _has_tracked_characters?(user_character_eve_ids)
)}
end end
def handle_info({:map_loaded, def handle_info(
%{ {:map_loaded,
map_id: map_id, %{
user_characters: user_character_eve_ids, map_id: map_id,
initial_data: initial_data initial_data: initial_data
} = _loaded_data}, socket) do } = _loaded_data},
socket
) do
map_characters = map_id |> WandererApp.Map.list_characters() map_characters = map_id |> WandererApp.Map.list_characters()
{:noreply, {:noreply,
socket socket
|> assign( |> assign(map_loaded?: true)
map_loaded?: true, |> push_map_event(
user_characters: user_character_eve_ids, "init",
has_tracked_characters?: _has_tracked_characters?(user_character_eve_ids) initial_data |> Map.put(:characters, map_characters |> Enum.map(&map_ui_character/1))
) )
|> _push_map_event("init", initial_data |> Map.merge(%{ |> push_event("js-exec", %{
characters: map_characters |> Enum.map(&map_ui_character/1) to: "#map-loader",
})) attr: "data-loaded"
|> push_event("js-exec", %{ })}
to: "#map-loader",
attr: "data-loaded"
})}
end end
def handle_info(:no_access, socket), do: def handle_info(:no_access, socket),
{:noreply, do:
socket {:noreply,
|> put_flash(:error, "You don't have an access to this map.") socket
|> push_navigate(to: ~p"/maps")} |> put_flash(:error, "You don't have an access to this map.")
|> push_navigate(to: ~p"/maps")}
def handle_info(:no_permissions, socket), do: def handle_info(:no_permissions, socket),
{:noreply, do:
socket {:noreply,
|> put_flash(:error, "You don't have permissions to use this map.") socket
|> push_navigate(to: ~p"/maps")} |> put_flash(:error, "You don't have permissions to use this map.")
|> push_navigate(to: ~p"/maps")}
def handle_info(:not_all_characters_tracked, socket), do: def handle_info(:not_all_characters_tracked, socket),
{:noreply, do:
socket {:noreply,
|> put_flash( socket
:error, |> put_flash(
"You should enable tracking for all characters that have access to this map first!" :error,
) "You should enable tracking for all characters that have access to this map first!"
|> push_navigate(to: ~p"/tracking/#{socket.assigns.map_slug}")} )
|> push_navigate(to: ~p"/tracking/#{socket.assigns.map_slug}")}
@impl true @impl true
def handle_info( def handle_info(
{ref, result}, {ref, result},
socket socket
) when is_reference(ref) do )
when is_reference(ref) do
Process.demonitor(ref, [:flush]) Process.demonitor(ref, [:flush])
case result do case result do
@@ -539,7 +595,7 @@ defmodule WandererAppWeb.MapLive do
{:routes, {solar_system_id, %{routes: routes, systems_static_data: systems_static_data}}} -> {:routes, {solar_system_id, %{routes: routes, systems_static_data: systems_static_data}}} ->
{:noreply, {:noreply,
socket socket
|> _push_map_event( |> push_map_event(
"routes", "routes",
%{ %{
solar_system_id: solar_system_id, solar_system_id: solar_system_id,
@@ -571,12 +627,11 @@ defmodule WandererAppWeb.MapLive do
end end
@impl true @impl true
def handle_event("reconnected", _body, socket) do def handle_event(
{:noreply, socket} "change_map",
end %{"map_slug" => map_slug} = _event,
%{assigns: %{map_id: map_id}} = socket
@impl true ) do
def handle_event("change_map", %{"map_slug" => map_slug} = _event, %{assigns: %{map_id: map_id}} = socket) do
Phoenix.PubSub.unsubscribe(WandererApp.PubSub, map_id) Phoenix.PubSub.unsubscribe(WandererApp.PubSub, map_id)
{:noreply, push_navigate(socket, to: ~p"/#{map_slug}")} {:noreply, push_navigate(socket, to: ~p"/#{map_slug}")}
end end
@@ -637,13 +692,16 @@ defmodule WandererAppWeb.MapLive do
solar_system_target_id: solar_system_target_id |> String.to_integer() solar_system_target_id: solar_system_target_id |> String.to_integer()
}) })
:telemetry.execute([:wanderer_app, :map, :connection, :add], %{count: 1}, %{ {:ok, _} =
character_id: tracked_character_ids |> List.first(), WandererApp.User.ActivityTracker.track_map_event(:map_connection_added, %{
user_id: current_user.id, character_id: tracked_character_ids |> List.first(),
map_id: map_id, user_id: current_user.id,
solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(), map_id: map_id,
solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer() solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
}) solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer()
})
:telemetry.execute([:wanderer_app, :map, :connection, :add], %{count: 1})
{:noreply, socket} {:noreply, socket}
@@ -682,18 +740,21 @@ defmodule WandererAppWeb.MapLive do
socket socket
|> map_id() |> map_id()
:telemetry.execute([:wanderer_app, :map, :hub, :add], %{count: 1}, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: solar_system_id
})
map_id map_id
|> WandererApp.Map.Server.add_hub(%{ |> WandererApp.Map.Server.add_hub(%{
solar_system_id: solar_system_id solar_system_id: solar_system_id
}) })
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:hub_added, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: solar_system_id
})
:telemetry.execute([:wanderer_app, :map, :hub, :add], %{count: 1})
{:noreply, socket} {:noreply, socket}
_ -> _ ->
@@ -731,18 +792,21 @@ defmodule WandererAppWeb.MapLive do
socket socket
|> map_id() |> map_id()
:telemetry.execute([:wanderer_app, :map, :hub, :remove], %{count: 1}, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: solar_system_id
})
map_id map_id
|> WandererApp.Map.Server.remove_hub(%{ |> WandererApp.Map.Server.remove_hub(%{
solar_system_id: solar_system_id solar_system_id: solar_system_id
}) })
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:hub_removed, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: solar_system_id
})
:telemetry.execute([:wanderer_app, :map, :hub, :remove], %{count: 1})
{:noreply, socket} {:noreply, socket}
_ -> _ ->
@@ -802,15 +866,6 @@ defmodule WandererAppWeb.MapLive do
socket socket
|> map_id() |> map_id()
:telemetry.execute([:wanderer_app, :map, :system, :update], %{count: 1}, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: "#{solar_system_id}" |> String.to_integer(),
key: key_atom,
value: value
})
apply(WandererApp.Map.Server, method_atom, [ apply(WandererApp.Map.Server, method_atom, [
map_id, map_id,
%{ %{
@@ -819,6 +874,18 @@ defmodule WandererAppWeb.MapLive do
|> Map.put_new(key_atom, value) |> Map.put_new(key_atom, value)
]) ])
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:system_updated, %{
character_id: tracked_character_ids |> List.first(),
user_id: current_user.id,
map_id: map_id,
solar_system_id: "#{solar_system_id}" |> String.to_integer(),
key: key_atom,
value: value
})
:telemetry.execute([:wanderer_app, :map, :system, :update], %{count: 1})
{:noreply, socket} {:noreply, socket}
_ -> _ ->
@@ -878,15 +945,18 @@ defmodule WandererAppWeb.MapLive do
socket socket
|> map_id() |> map_id()
:telemetry.execute([:wanderer_app, :map, :connection, :update], %{count: 1}, %{ {:ok, _} =
character_id: tracked_character_ids |> List.first(), WandererApp.User.ActivityTracker.track_map_event(:map_connection_updated, %{
user_id: current_user.id, character_id: tracked_character_ids |> List.first(),
map_id: map_id, user_id: current_user.id,
solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(), map_id: map_id,
solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer(), solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
key: key_atom, solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer(),
value: value key: key_atom,
}) value: value
})
:telemetry.execute([:wanderer_app, :map, :connection, :update], %{count: 1})
apply(WandererApp.Map.Server, method_atom, [ apply(WandererApp.Map.Server, method_atom, [
map_id, map_id,
@@ -1230,13 +1300,16 @@ defmodule WandererAppWeb.MapLive do
solar_system_target_id: solar_system_target_id |> String.to_integer() solar_system_target_id: solar_system_target_id |> String.to_integer()
}) })
:telemetry.execute([:wanderer_app, :map, :connection, :remove], %{count: 1}, %{ {:ok, _} =
character_id: tracked_character_ids |> List.first(), WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{
user_id: current_user.id, character_id: tracked_character_ids |> List.first(),
map_id: map_id, user_id: current_user.id,
solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(), map_id: map_id,
solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer() solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
}) solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer()
})
:telemetry.execute([:wanderer_app, :map, :connection, :remove], %{count: 1})
{:noreply, socket} {:noreply, socket}
@@ -1424,7 +1497,7 @@ defmodule WandererAppWeb.MapLive do
|> assign_async(:characters, fn -> |> assign_async(:characters, fn ->
{:ok, %{characters: characters}} {:ok, %{characters: characters}}
end) end)
|> _push_map_event( |> push_map_event(
"init", "init",
%{ %{
user_characters: user_character_eve_ids, user_characters: user_character_eve_ids,
@@ -1433,6 +1506,39 @@ defmodule WandererAppWeb.MapLive do
)} )}
end end
@impl true
def handle_event(
"open_user_settings",
_,
%{assigns: %{map_id: map_id, current_user: current_user}} = socket
) do
{:ok, user_settings_form} =
WandererApp.MapUserSettingsRepo.get!(map_id, current_user.id)
|> WandererApp.MapUserSettingsRepo.to_form_data()
{:noreply,
socket
|> assign(
show_user_settings?: true,
user_settings_form: user_settings_form |> to_form()
)}
end
@impl true
def handle_event(
"update_user_settings",
user_settings_form,
%{assigns: %{map_id: map_id, current_user: current_user}} = socket
) do
settings = user_settings_form |> Map.take(["select_on_spash"]) |> Jason.encode!()
{:ok, user_settings} =
WandererApp.MapUserSettingsRepo.create_or_update(map_id, current_user.id, settings)
{:noreply,
socket |> assign(user_settings_form: user_settings_form, map_user_settings: user_settings)}
end
@impl true @impl true
def handle_event("noop", _, socket), do: {:noreply, socket} def handle_event("noop", _, socket), do: {:noreply, socket}
@@ -1457,6 +1563,10 @@ defmodule WandererAppWeb.MapLive do
def handle_event("hide_tracking", _, socket), def handle_event("hide_tracking", _, socket),
do: {:noreply, socket |> assign(show_tracking?: false)} do: {:noreply, socket |> assign(show_tracking?: false)}
@impl true
def handle_event("hide_user_settings", _, socket),
do: {:noreply, socket |> assign(show_user_settings?: false)}
@impl true @impl true
def handle_event( def handle_event(
"log_map_error", "log_map_error",
@@ -1481,102 +1591,110 @@ defmodule WandererAppWeb.MapLive do
{:noreply, socket} {:noreply, socket}
end end
defp _on_map_started(map_id, current_user, user_permissions) do defp on_map_started(
case user_permissions do map_id,
%{view_system: true, track_character: track_character} -> current_user,
{:ok, _} = current_user |> WandererApp.Api.User.update_last_map(%{last_map_id: map_id}) %{view_system: true, track_character: track_character} = user_permissions
) do
with {:ok, _} <- current_user |> WandererApp.Api.User.update_last_map(%{last_map_id: map_id}),
{:ok, map_user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user.id),
{:ok, tracked_map_characters} <- _get_tracked_map_characters(map_id, current_user),
{:ok, characters_limit} <- map_id |> WandererApp.Map.get_characters_limit(),
{:ok, present_character_ids} <-
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", []),
{:ok, kills} <- WandererApp.Cache.lookup("map_#{map_id}:zkb_kills", Map.new()) do
user_character_eve_ids = tracked_map_characters |> Enum.map(& &1.eve_id)
{:ok, tracked_map_characters} = _get_tracked_map_characters(map_id, current_user) events =
case tracked_map_characters |> Enum.any?(&(&1.access_token == nil)) do
true ->
[:invalid_token_message]
user_character_eve_ids = tracked_map_characters |> Enum.map(& &1.eve_id) _ ->
[]
end
events = events =
case tracked_map_characters |> Enum.any?(&(&1.access_token == nil)) do case tracked_map_characters |> Enum.empty?() do
true -> true ->
[:invalid_token_message] events ++ [:empty_tracked_characters]
_ -> _ ->
[] events
end end
events = events =
case tracked_map_characters |> Enum.empty?() do case present_character_ids |> Enum.count() < characters_limit do
true -> true ->
events ++ [:empty_tracked_characters] events ++ [{:track_characters, tracked_map_characters, track_character}]
_ -> _ ->
events events ++ [:map_character_limit]
end end
{:ok, characters_limit} = map_id |> WandererApp.Map.get_characters_limit() initial_data =
map_id
{:ok, present_character_ids} = |> _get_map_data()
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", []) |> Map.merge(%{
kills:
events = kills
case present_character_ids |> Enum.count() < characters_limit do |> Enum.filter(fn {_, kills} -> kills > 0 end)
true -> |> Enum.map(&map_ui_kill/1),
events ++ [{:track_characters, tracked_map_characters, track_character}] present_characters:
present_character_ids
_ -> |> WandererApp.Character.get_character_eve_ids!(),
events ++ [:map_character_limit]
end
{:ok, kills} = WandererApp.Cache.lookup("map_#{map_id}:zkb_kills", Map.new())
initial_data =
map_id
|> _get_map_data()
|> Map.merge(%{
kills:
kills
|> Enum.filter(fn {_, kills} -> kills > 0 end)
|> Enum.map(&map_ui_kill/1),
present_characters:
present_character_ids
|> WandererApp.Character.get_character_eve_ids!(),
user_characters: user_character_eve_ids,
user_permissions: user_permissions,
system_static_infos: nil,
wormhole_types: nil,
effects: nil,
reset: false
})
system_static_infos =
map_id
|> WandererApp.Map.list_systems!()
|> Enum.map(&WandererApp.CachedInfo.get_system_static_info!(&1.solar_system_id))
initial_data =
initial_data
|> Map.put(
:wormholes,
WandererApp.CachedInfo.get_wormhole_types!()
)
|> Map.put(
:effects,
WandererApp.CachedInfo.get_effects!()
)
|> Map.put(
:system_static_infos,
system_static_infos |> Enum.map(&map_ui_system_static_info/1)
)
|> Map.put(:reset, true)
Process.send_after(self(), {:map_start, %{
map_id: map_id,
user_characters: user_character_eve_ids, user_characters: user_character_eve_ids,
initial_data: initial_data, user_permissions: user_permissions,
events: events system_static_infos: nil,
}}, 10) wormhole_types: nil,
effects: nil,
reset: false
})
_ -> system_static_infos =
map_id
|> WandererApp.Map.list_systems!()
|> Enum.map(&WandererApp.CachedInfo.get_system_static_info!(&1.solar_system_id))
|> Enum.map(&map_ui_system_static_info/1)
initial_data =
initial_data
|> Map.put(
:wormholes,
WandererApp.CachedInfo.get_wormhole_types!()
)
|> Map.put(
:effects,
WandererApp.CachedInfo.get_effects!()
)
|> Map.put(
:system_static_infos,
system_static_infos
)
|> Map.put(:reset, true)
Process.send_after(
self(),
{:map_start,
%{
map_id: map_id,
map_user_settings: map_user_settings,
user_characters: user_character_eve_ids,
initial_data: initial_data,
events: events
}},
10
)
else
error ->
Logger.error(fn -> "map_start_error: #{error}" end)
Process.send_after(self(), :no_access, 10) Process.send_after(self(), :no_access, 10)
end end
end end
defp on_map_started(_map_id, _current_user, _user_permissions),
do: Process.send_after(self(), :no_access, 10)
defp _set_autopilot_waypoint( defp _set_autopilot_waypoint(
current_user, current_user,
character_eve_id, character_eve_id,
@@ -2079,14 +2197,13 @@ defmodule WandererAppWeb.MapLive do
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id) :ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
end end
defp _push_map_event(socket, type, event_body) defp push_map_event(socket, type, body),
do do:
socket socket
|> push_event("map_event", %{ |> push_event("map_event", %{
type: type, type: type,
body: event_body |> WandererApp.Utils.JSONUtil.compress() body: body
}) })
end
defp map_id(%{assigns: %{map_id: map_id}} = _socket), do: map_id defp map_id(%{assigns: %{map_id: map_id}} = _socket), do: map_id
end end

View File

@@ -13,8 +13,6 @@
<span class="Loader__Circle"></span> <span class="Loader__Circle"></span>
</div> </div>
</div> </div>
</div> </div>
<div class="w-full h-full" id="mapper" phx-hook="Mapper" phx-update="ignore"></div> <div class="w-full h-full" id="mapper" phx-hook="Mapper" phx-update="ignore"></div>
@@ -151,3 +149,20 @@
</.table> </.table>
</.async_result> </.async_result>
</.modal> </.modal>
<.modal
:if={assigns |> Map.get(:show_user_settings?, false)}
id="map-user-settings-modal"
title="Map user settings"
show
on_cancel={JS.push("hide_user_settings")}
>
<.form
:let={f}
:if={assigns |> Map.get(:user_settings_form, false)}
for={@user_settings_form}
phx-change="update_user_settings"
>
<.input type="checkbox" field={f[:select_on_spash]} label="Auto select splashed systems" />
</.form>
</.modal>

View File

@@ -608,20 +608,12 @@ defmodule WandererAppWeb.MapsLive do
added_acls added_acls
|> Enum.each(fn acl_id -> |> Enum.each(fn acl_id ->
:telemetry.execute([:wanderer_app, :map, :acl, :add], %{count: 1}, %{ :telemetry.execute([:wanderer_app, :map, :acl, :add], %{count: 1})
user_id: current_user.id,
map_id: map.id,
acl_id: acl_id
})
end) end)
removed_acls removed_acls
|> Enum.each(fn acl_id -> |> Enum.each(fn acl_id ->
:telemetry.execute([:wanderer_app, :map, :acl, :remove], %{count: 1}, %{ :telemetry.execute([:wanderer_app, :map, :acl, :remove], %{count: 1})
user_id: current_user.id,
map_id: map.id,
acl_id: acl_id
})
end) end)
Phoenix.PubSub.broadcast( Phoenix.PubSub.broadcast(
@@ -672,7 +664,7 @@ defmodule WandererAppWeb.MapsLive do
%{ %{
"layout" => layout "layout" => layout
} = options_form, } = options_form,
%{assigns: %{map_id: map_id, map: map, current_user: current_user}} = socket %{assigns: %{map_id: map_id, map: map}} = socket
) do ) do
options = %{layout: layout} options = %{layout: layout}
@@ -934,6 +926,6 @@ defmodule WandererAppWeb.MapsLive do
defp map_map(%{acls: acls} = map) do defp map_map(%{acls: acls} = map) do
map map
|> Map.merge(%{acls: acls |> Enum.map(&map_acl/1)}) |> Map.put(:acls, acls |> Enum.map(&map_acl/1))
end end
end end

View File

@@ -208,7 +208,7 @@
classes("tab-active": @active_settings_tab == "import") classes("tab-active": @active_settings_tab == "import")
]} ]}
> >
<.icon name="hero-wrench-screwdriver-solid" class="w-4 h-4" />&nbsp;Import/Export <.icon name="hero-document-arrow-down-solid" class="w-4 h-4" />&nbsp;Import/Export
</a> </a>
<a <a
:if={@map_subscriptions_enabled?} :if={@map_subscriptions_enabled?}

View File

@@ -58,7 +58,8 @@ defmodule WandererAppWeb.Router do
~w('unsafe-inline'), ~w('unsafe-inline'),
~w(https://unpkg.com), ~w(https://unpkg.com),
~w(https://w.appzi.io), ~w(https://w.appzi.io),
~w(https://www.googletagmanager.com) ~w(https://www.googletagmanager.com),
~w(https://cdnjs.cloudflare.com)
], ],
style_src: @style_src, style_src: @style_src,
img_src: @img_src, img_src: @img_src,
@@ -200,8 +201,6 @@ defmodule WandererAppWeb.Router do
end end
end end
# Enable LiveDashboard and Swoosh mailbox preview in development # Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:wanderer_app, :dev_routes) do if Application.compile_env(:wanderer_app, :dev_routes) do
# If you want to use the LiveDashboard in production, you should put # If you want to use the LiveDashboard in production, you should put

View File

@@ -2,7 +2,7 @@ defmodule WandererApp.MixProject do
use Mix.Project use Mix.Project
@source_url "https://github.com/wanderer-industries/wanderer" @source_url "https://github.com/wanderer-industries/wanderer"
@version "1.3.0" @version "1.5.0"
def project do def project do
[ [
@@ -100,7 +100,7 @@ defmodule WandererApp.MixProject do
{:makeup_elixir, ">= 0.0.0"}, {:makeup_elixir, ">= 0.0.0"},
{:makeup_erlang, ">= 0.0.0"}, {:makeup_erlang, ">= 0.0.0"},
{:better_number, "~> 1.0.0"}, {:better_number, "~> 1.0.0"},
{:delta_crdt, "~> 0.6.5"}, {:delta_crdt, "~> 0.6.5", override: true},
{:qex, "~> 0.5"}, {:qex, "~> 0.5"},
{:site_encrypt, "~> 0.6.0"}, {:site_encrypt, "~> 0.6.0"},
{:bandit, "~> 1.0"}, {:bandit, "~> 1.0"},
@@ -111,7 +111,8 @@ defmodule WandererApp.MixProject do
{:mox, "~> 1.1", only: [:test, :integration]}, {:mox, "~> 1.1", only: [:test, :integration]},
{:git_ops, "~> 2.6.1"}, {:git_ops, "~> 2.6.1"},
{:version_tasks, "~> 0.12.0"}, {:version_tasks, "~> 0.12.0"},
{:error_tracker, "~> 0.2"} {:error_tracker, "~> 0.2"},
{:ddrt, "~> 0.2.1"}
] ]
end end

View File

@@ -18,6 +18,7 @@
"crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"}, "crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"},
"dart_sass": {:hex, :dart_sass, "0.5.1", "d45f20a8e324313689fb83287d4702352793ce8c9644bc254155d12656ade8b6", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "24f8a1c67e8b5267c51a33cbe6c0b5ebf12c2c83ace88b5ac04947d676b4ec81"}, "dart_sass": {:hex, :dart_sass, "0.5.1", "d45f20a8e324313689fb83287d4702352793ce8c9644bc254155d12656ade8b6", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "24f8a1c67e8b5267c51a33cbe6c0b5ebf12c2c83ace88b5ac04947d676b4ec81"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"ddrt": {:hex, :ddrt, "0.2.1", "c4e4bddcef36add5de6599ec72ec822699932413ece0ad310e4be4ab2b3ab6d3", [:mix], [{:delta_crdt, "~> 0.5.0", [hex: :delta_crdt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:merkle_map, "~> 0.2.0", [hex: :merkle_map, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "1efcd60cf4ca4a4352e752d7f41ed9d696560e5860ee07d5bf31c16950100365"},
"debounce_and_throttle": {:hex, :debounce_and_throttle, "0.9.0", "fa86c982963e00365cc9808afa496e82ca2b48f8905c6c79f8edd304800d0892", [:mix], [], "hexpm", "573a7cff4032754023d8e6874f3eff5354864c90b39b692f1fc4a44b3eb7517b"}, "debounce_and_throttle": {:hex, :debounce_and_throttle, "0.9.0", "fa86c982963e00365cc9808afa496e82ca2b48f8905c6c79f8edd304800d0892", [:mix], [], "hexpm", "573a7cff4032754023d8e6874f3eff5354864c90b39b692f1fc4a44b3eb7517b"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"},

View File

@@ -0,0 +1,52 @@
defmodule WandererApp.Repo.Migrations.AddMapUserSettings do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
create table(:map_user_settings_v1, primary_key: false) do
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
add :settings, :text
add :map_id,
references(:maps_v1,
column: :id,
name: "map_user_settings_v1_map_id_fkey",
type: :uuid,
prefix: "public"
),
primary_key: true,
null: false
add :user_id,
references(:user_v1,
column: :id,
name: "map_user_settings_v1_user_id_fkey",
type: :uuid,
prefix: "public"
),
primary_key: true,
null: false
end
create unique_index(:map_user_settings_v1, [:map_id, :user_id],
name: "map_user_settings_v1_uniq_map_user_index"
)
end
def down do
drop_if_exists unique_index(:map_user_settings_v1, [:map_id, :user_id],
name: "map_user_settings_v1_uniq_map_user_index"
)
drop constraint(:map_user_settings_v1, "map_user_settings_v1_map_id_fkey")
drop constraint(:map_user_settings_v1, "map_user_settings_v1_user_id_fkey")
drop table(:map_user_settings_v1)
end
end

View File

@@ -0,0 +1,116 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "settings",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": true,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "map_user_settings_v1_map_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "maps_v1"
},
"size": null,
"source": "map_id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": true,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "map_user_settings_v1_user_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "user_v1"
},
"size": null,
"source": "user_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "88FB044C6F66793E2247BF2CEE17F2E4ED52C007C3DDCE02B6EF583EDDD44D85",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "map_user_settings_v1_uniq_map_user_index",
"keys": [
{
"type": "atom",
"value": "map_id"
},
{
"type": "atom",
"value": "user_id"
}
],
"name": "uniq_map_user",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "map_user_settings_v1"
}