mirror of
				https://github.com/wanderer-industries/wanderer
				synced 2025-10-30 22:17:19 +00:00 
			
		
		
		
	Compare commits
	
		
			21 Commits
		
	
	
		
			19-add-map
			...
			v1.6.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 8f20cd9863 | ||
|   | 4ed0e85680 | ||
|   | 8ce9eb9955 | ||
|   | 363330f3d1 | ||
|   | fbf9c5ddd6 | ||
|   | fbf2ee314c | ||
|   | c9f83fb419 | ||
|   | 9737d91e16 | ||
|   | 2f672ae970 | ||
|   | 25339546c6 | ||
|   | 912cad42ac | ||
|   | b3752c8d8f | ||
|   | e8a11333f2 | ||
|   | 8bb6d09e6e | ||
|   | 56bf955297 | ||
|   | ef6c08dfe8 | ||
|   | 495c3e1cd7 | ||
|   | 9a5fe3d744 | ||
|   | 72607cae4d | ||
|   | 4891cdb04d | ||
|   | d214881720 | 
							
								
								
									
										65
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										65
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -2,6 +2,71 @@ | ||||
|  | ||||
| <!-- changelog --> | ||||
|  | ||||
| ## [v1.6.0](https://github.com/wanderer-industries/wanderer/compare/v1.5.0...v1.6.0) (2024-10-13) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ### Features: | ||||
|  | ||||
| * Map: Link signature on splash | ||||
|  | ||||
| ## [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) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -28,6 +28,12 @@ body { | ||||
|   font-weight: 500; | ||||
| } | ||||
|  | ||||
| #bg-canvas { | ||||
|   position: absolute; | ||||
|   width: 100vw; | ||||
|   height: 100vh; | ||||
| } | ||||
|  | ||||
| .ccp-font { | ||||
|   font-family: 'Shentox', 'Rogan', sans-serif !important; | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ const Characters = ({ data }: { data: CharacterTypeRaw[] }) => { | ||||
|  | ||||
|   const handleSelect = useCallback( | ||||
|     (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], | ||||
|   ); | ||||
|   | ||||
| @@ -48,19 +48,19 @@ export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand, mapRef }: U | ||||
|   }, []); | ||||
|  | ||||
|   const onAddSystem = useCallback(() => { | ||||
|     const { system, outCommand, mapRef } = ref.current; | ||||
|     if (!system) { | ||||
|     const { system: solarSystemId, outCommand, mapRef } = ref.current; | ||||
|     if (!solarSystemId) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     outCommand({ | ||||
|       type: OutCommand.addSystem, | ||||
|       data: { | ||||
|         system_id: system, | ||||
|         system_id: solarSystemId, | ||||
|       }, | ||||
|     }); | ||||
|     setTimeout(() => { | ||||
|       mapRef.current?.command(Commands.selectSystem, system); | ||||
|       mapRef.current?.command(Commands.centerSystem, solarSystemId); | ||||
|       setSystem(undefined); | ||||
|     }, 200); | ||||
|   }, []); | ||||
|   | ||||
| @@ -5,5 +5,6 @@ export * from './useMapRemoveSystems'; | ||||
| export * from './useCommandsCharacters'; | ||||
| export * from './useCommandsConnections'; | ||||
| export * from './useCommandsConnections'; | ||||
| export * from './useCenterSystem'; | ||||
| export * from './useSelectSystem'; | ||||
| export * from './useMapCommands'; | ||||
|   | ||||
| @@ -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 }); | ||||
|   }, []); | ||||
| }; | ||||
| @@ -1,21 +1,21 @@ | ||||
| import { useReactFlow } from 'reactflow'; | ||||
| import { useCallback, useRef } from 'react'; | ||||
| import { useCallback } from 'react'; | ||||
| import { CommandSelectSystem } from '@/hooks/Mapper/types'; | ||||
|  | ||||
| export const useSelectSystem = () => { | ||||
|   const rf = useReactFlow(); | ||||
|   const ref = useRef({ rf }); | ||||
|   ref.current = { rf }; | ||||
|  | ||||
|   return useCallback((systemId: CommandSelectSystem) => { | ||||
|     if (!ref.current?.rf) { | ||||
|     if (!rf) { | ||||
|       return; | ||||
|     } | ||||
|     const systemNode = ref.current.rf.getNodes().find(x => x.data.id === systemId); | ||||
|     if (!systemNode) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     ref.current.rf.setCenter(systemNode.position.x, systemNode.position.y, { duration: 1000 }); | ||||
|     rf.setNodes(nds => | ||||
|       nds.map(node => { | ||||
|         return { | ||||
|           ...node, | ||||
|           selected: node.id === systemId, | ||||
|         }; | ||||
|       }), | ||||
|     ); | ||||
|   }, []); | ||||
| }; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { ForwardedRef, useImperativeHandle } from 'react'; | ||||
| import { ForwardedRef, useImperativeHandle, useRef } from 'react'; | ||||
| import { | ||||
|   CommandAddConnections, | ||||
|   CommandAddSystems, | ||||
| @@ -26,6 +26,7 @@ import { | ||||
|   useMapInit, | ||||
|   useMapRemoveSystems, | ||||
|   useMapUpdateSystems, | ||||
|   useCenterSystem, | ||||
|   useSelectSystem, | ||||
| } from './api'; | ||||
| 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 mapUpdateSystems = useMapUpdateSystems(); | ||||
|   const removeSystems = useMapRemoveSystems(onSelectionChange); | ||||
|   const centerSystem = useCenterSystem(); | ||||
|   const selectSystem = useSelectSystem(); | ||||
|  | ||||
|   const selectRef = useRef({ onSelectionChange }); | ||||
|   selectRef.current = { onSelectionChange }; | ||||
|  | ||||
|   const { addConnections, removeConnections, updateConnection } = useCommandsConnections(); | ||||
|   const { mapUpdated, killsUpdated } = useMapCommands(); | ||||
|   const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } = | ||||
| @@ -91,14 +96,32 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange | ||||
|               killsUpdated(data as CommandKillsUpdated); | ||||
|               break; | ||||
|  | ||||
|             case Commands.centerSystem: | ||||
|               setTimeout(() => { | ||||
|                 const systemId = `${data}`; | ||||
|                 centerSystem(systemId as CommandSelectSystem); | ||||
|               }, 100); | ||||
|               break; | ||||
|  | ||||
|             case Commands.selectSystem: | ||||
|               selectSystem(data as CommandSelectSystem); | ||||
|               setTimeout(() => { | ||||
|                 const systemId = `${data}`; | ||||
|                 selectRef.current.onSelectionChange({ | ||||
|                   systems: [systemId], | ||||
|                   connections: [], | ||||
|                 }); | ||||
|                 selectSystem(systemId as CommandSelectSystem); | ||||
|               }, 100); | ||||
|               break; | ||||
|  | ||||
|             case Commands.routes: | ||||
|               // do nothing here | ||||
|               break; | ||||
|  | ||||
|             case Commands.linkSignatureToSystem: | ||||
|               // do nothing here | ||||
|               break; | ||||
|  | ||||
|             default: | ||||
|               console.warn(`Map handlers: Unknown command: ${type}`, data); | ||||
|               break; | ||||
|   | ||||
| @@ -0,0 +1,70 @@ | ||||
| import { useCallback, useRef } from 'react'; | ||||
| import { Dialog } from 'primereact/dialog'; | ||||
|  | ||||
| import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts'; | ||||
| import { SystemSignature } from '@/hooks/Mapper/types'; | ||||
| import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
| import { CommandLinkSignatureToSystem } from '@/hooks/Mapper/types'; | ||||
| import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent'; | ||||
| import { | ||||
|   Setting, | ||||
|   COSMIC_SIGNATURE, | ||||
| } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog'; | ||||
|  | ||||
| interface SystemLinkSignatureDialogProps { | ||||
|   data: CommandLinkSignatureToSystem; | ||||
|   setVisible: (visible: boolean) => void; | ||||
| } | ||||
|  | ||||
| const signatureSettings: Setting[] = [{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true }]; | ||||
|  | ||||
| export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignatureDialogProps) => { | ||||
|   const { outCommand } = useMapRootState(); | ||||
|  | ||||
|   console.log(data); | ||||
|  | ||||
|   const ref = useRef({ outCommand }); | ||||
|   ref.current = { outCommand }; | ||||
|  | ||||
|   const handleHide = useCallback(() => { | ||||
|     setVisible(false); | ||||
|   }, [setVisible]); | ||||
|  | ||||
|   const handleSelect = useCallback( | ||||
|     (signatures: SystemSignature[]) => { | ||||
|       if (!signatures.length) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const { outCommand } = ref.current; | ||||
|  | ||||
|       outCommand({ | ||||
|         type: OutCommand.linkSignatureToSystem, | ||||
|         data: { | ||||
|           ...data, | ||||
|           signature_eve_id: signatures[0].eve_id, | ||||
|         }, | ||||
|       }); | ||||
|       setVisible(false); | ||||
|     }, | ||||
|     [setVisible], | ||||
|   ); | ||||
|  | ||||
|   return ( | ||||
|     <Dialog | ||||
|       header="Select signature to link" | ||||
|       visible | ||||
|       draggable={false} | ||||
|       style={{ width: '500px' }} | ||||
|       onHide={handleHide} | ||||
|       contentClassName="!p-0" | ||||
|     > | ||||
|       <SystemSignaturesContent | ||||
|         systemId={`${data.solar_system_source}`} | ||||
|         settings={signatureSettings} | ||||
|         onSelect={handleSelect} | ||||
|         selectable={true} | ||||
|       /> | ||||
|     </Dialog> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1 @@ | ||||
| export * from './SystemLinkSignatureDialog'; | ||||
| @@ -2,3 +2,4 @@ export * from './Widget'; | ||||
| export * from './WidgetsGrid'; | ||||
| export * from './SystemSettingsDialog'; | ||||
| export * from './SystemCustomLabelDialog'; | ||||
| export * from './SystemLinkSignatureDialog'; | ||||
|   | ||||
| @@ -91,7 +91,7 @@ export const RoutesList = ({ data, onContextMenu }: RoutesListProps) => { | ||||
|   const { mapRef } = useMapRootState(); | ||||
|  | ||||
|   const handleClick = useCallback( | ||||
|     (systemId: number) => mapRef.current?.command(Commands.selectSystem, systemId.toString()), | ||||
|     (systemId: number) => mapRef.current?.command(Commands.centerSystem, systemId.toString()), | ||||
|     [mapRef], | ||||
|   ); | ||||
|  | ||||
|   | ||||
| @@ -5,6 +5,14 @@ import { Checkbox } from 'primereact/checkbox'; | ||||
|  | ||||
| export type Setting = { key: string; name: string; value: boolean }; | ||||
|  | ||||
| export const COSMIC_SIGNATURE = 'Cosmic Signature'; | ||||
| export const COSMIC_ANOMALY = 'Cosmic Anomaly'; | ||||
| export const DEPLOYABLE = 'Deployable'; | ||||
| export const STRUCTURE = 'Structure'; | ||||
| export const STARBASE = 'Starbase'; | ||||
| export const SHIP = 'Ship'; | ||||
| export const DRONE = 'Drone'; | ||||
|  | ||||
| interface SystemSignatureSettingsDialogProps { | ||||
|   settings: Setting[]; | ||||
|   onSave: (settings: Setting[]) => void; | ||||
|   | ||||
| @@ -1,7 +1,17 @@ | ||||
| import { Widget } from '@/hooks/Mapper/components/mapInterface/components'; | ||||
| import { InfoDrawer, LayoutEventBlocker, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit'; | ||||
| import { SystemSignaturesContent } from './SystemSignaturesContent'; | ||||
| import { Setting, SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog'; | ||||
| import { | ||||
|   Setting, | ||||
|   SystemSignatureSettingsDialog, | ||||
|   COSMIC_SIGNATURE, | ||||
|   COSMIC_ANOMALY, | ||||
|   DEPLOYABLE, | ||||
|   STRUCTURE, | ||||
|   STARBASE, | ||||
|   SHIP, | ||||
|   DRONE, | ||||
| } from './SystemSignatureSettingsDialog'; | ||||
|  | ||||
| import React, { useCallback, useEffect, useState } from 'react'; | ||||
|  | ||||
| @@ -9,14 +19,6 @@ import { PrimeIcons } from 'primereact/api'; | ||||
|  | ||||
| import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
|  | ||||
| export const COSMIC_SIGNATURE = 'Cosmic Signature'; | ||||
| export const COSMIC_ANOMALY = 'Cosmic Anomaly'; | ||||
| export const DEPLOYABLE = 'Deployable'; | ||||
| export const STRUCTURE = 'Structure'; | ||||
| export const STARBASE = 'Starbase'; | ||||
| export const SHIP = 'Ship'; | ||||
| export const DRONE = 'Drone'; | ||||
|  | ||||
| const settings: Setting[] = [ | ||||
|   { key: COSMIC_ANOMALY, name: 'Show Anomalies', value: true }, | ||||
|   { key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true }, | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import { | ||||
|   renderIcon, | ||||
|   renderName, | ||||
|   renderTimeLeft, | ||||
|   renderLinkedSystem, | ||||
| } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders'; | ||||
| // import { PrimeIcons } from 'primereact/api'; | ||||
| import useLocalStorageState from 'use-local-storage-state'; | ||||
| @@ -41,13 +42,17 @@ const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = { | ||||
| interface SystemSignaturesContentProps { | ||||
|   systemId: string; | ||||
|   settings: Setting[]; | ||||
|   selectable?: boolean; | ||||
|   onSelect?: (signatures: SystemSignature[]) => void; | ||||
| } | ||||
| export const SystemSignaturesContent = ({ systemId, settings }: SystemSignaturesContentProps) => { | ||||
| export const SystemSignaturesContent = ({ systemId, settings, selectable, onSelect }: SystemSignaturesContentProps) => { | ||||
|   const { outCommand } = useMapRootState(); | ||||
|  | ||||
|   const [signatures, setSignatures, signaturesRef] = useRefState<SystemSignature[]>([]); | ||||
|   const [selectedSignatures, setSelectedSignatures] = useState<SystemSignature[]>([]); | ||||
|   const [nameColumnWidth, setNameColumnWidth] = useState('auto'); | ||||
|   const [parsedSignatures, setParsedSignatures] = useState<SystemSignature[]>([]); | ||||
|   const [askUser, setAskUser] = useState(false); | ||||
|  | ||||
|   const [hoveredSig, setHoveredSig] = useState<SystemSignature | null>(null); | ||||
|  | ||||
| @@ -86,12 +91,33 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures | ||||
|       data: { system_id: systemId }, | ||||
|     }); | ||||
|  | ||||
|     setAskUser(false); | ||||
|     setSignatures(signatures); | ||||
|   }, [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( | ||||
|     async (newSignatures: SystemSignature[]) => { | ||||
|       const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures); | ||||
|     async (newSignatures: SystemSignature[], updateOnly: boolean) => { | ||||
|       const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures, updateOnly); | ||||
|  | ||||
|       const { signatures: updatedSignatures } = await outCommand({ | ||||
|         type: OutCommand.updateSignatures, | ||||
| @@ -110,37 +136,70 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures | ||||
|   ); | ||||
|  | ||||
|   const handleDeleteSelected = useCallback(async () => { | ||||
|     if (selectable) { | ||||
|       return; | ||||
|     } | ||||
|     if (selectedSignatures.length === 0) { | ||||
|       return; | ||||
|     } | ||||
|     const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id); | ||||
|     await handleUpdateSignatures(signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id))); | ||||
|   }, [handleUpdateSignatures, signatures, selectedSignatures]); | ||||
|     await handleUpdateSignatures( | ||||
|       signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)), | ||||
|       false, | ||||
|     ); | ||||
|   }, [handleUpdateSignatures, selectable, signatures, selectedSignatures]); | ||||
|  | ||||
|   const handleSelectAll = useCallback(() => { | ||||
|     setSelectedSignatures(signatures); | ||||
|   }, [signatures]); | ||||
|  | ||||
|   const handleReplaceAll = useCallback(() => { | ||||
|     handleUpdateSignatures(parsedSignatures, false); | ||||
|     setAskUser(false); | ||||
|   }, [parsedSignatures, handleUpdateSignatures]); | ||||
|  | ||||
|   const handleUpdateOnly = useCallback(() => { | ||||
|     handleUpdateSignatures(parsedSignatures, true); | ||||
|     setAskUser(false); | ||||
|   }, [parsedSignatures, handleUpdateSignatures]); | ||||
|  | ||||
|   const handleSelectSignatures = useCallback(e => { | ||||
|     setSelectedSignatures(e.value); | ||||
|     onSelect?.(e.value); | ||||
|   }, []); | ||||
|  | ||||
|   useHotkey(true, ['a'], handleSelectAll); | ||||
|  | ||||
|   useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (selectable) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (!clipboardContent) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const signatures = parseSignatures( | ||||
|     const newSignatures = parseSignatures( | ||||
|       clipboardContent, | ||||
|       settings.map(x => x.key), | ||||
|     ); | ||||
|  | ||||
|     handleUpdateSignatures(signatures); | ||||
|   }, [clipboardContent]); | ||||
|     const { removed } = getActualSigs(signaturesRef.current, newSignatures, false); | ||||
|  | ||||
|     if (!signaturesRef.current || !signaturesRef.current.length || !removed.length) { | ||||
|       handleUpdateSignatures(newSignatures, false); | ||||
|     } else { | ||||
|       setParsedSignatures(newSignatures); | ||||
|       setAskUser(true); | ||||
|     } | ||||
|   }, [clipboardContent, selectable]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (!systemId) { | ||||
|       setSignatures([]); | ||||
|       setAskUser(false); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
| @@ -184,98 +243,132 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures | ||||
|   // }; | ||||
|  | ||||
|   return ( | ||||
|     <div ref={tableRef} className="h-full"> | ||||
|       {filteredSignatures.length === 0 ? ( | ||||
|         <div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm"> | ||||
|           No signatures | ||||
|         </div> | ||||
|       ) : ( | ||||
|         <> | ||||
|           <DataTable | ||||
|             className={classes.Table} | ||||
|             value={filteredSignatures} | ||||
|             size="small" | ||||
|             selectionMode="multiple" | ||||
|             selection={selectedSignatures} | ||||
|             metaKeySelection | ||||
|             onSelectionChange={e => setSelectedSignatures(e.value)} | ||||
|             dataKey="eve_id" | ||||
|             tableClassName="w-full select-none" | ||||
|             resizableColumns={false} | ||||
|             rowHover | ||||
|             selectAll | ||||
|             sortField={sortSettings.sortField} | ||||
|             sortOrder={sortSettings.sortOrder} | ||||
|             onSort={event => setSortSettings(() => ({ sortField: event.sortField, sortOrder: event.sortOrder }))} | ||||
|             onRowMouseEnter={compact || medium ? handleEnterRow : undefined} | ||||
|             onRowMouseLeave={compact || medium ? handleLeaveRow : undefined} | ||||
|             rowClassName={row => { | ||||
|               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'); | ||||
|               } | ||||
|     <> | ||||
|       <div ref={tableRef} className={'h-full '}> | ||||
|         {filteredSignatures.length === 0 ? ( | ||||
|           <div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm"> | ||||
|             No signatures | ||||
|           </div> | ||||
|         ) : ( | ||||
|           <> | ||||
|             <DataTable | ||||
|               className={classes.Table} | ||||
|               value={filteredSignatures} | ||||
|               size="small" | ||||
|               // selectionMode={selectable ? 'multiple' : 'single'} | ||||
|               selection={selectedSignatures} | ||||
|               metaKeySelection | ||||
|               onSelectionChange={handleSelectSignatures} | ||||
|               dataKey="eve_id" | ||||
|               tableClassName="w-full select-none" | ||||
|               resizableColumns={false} | ||||
|               rowHover | ||||
|               selectAll | ||||
|               sortField={sortSettings.sortField} | ||||
|               sortOrder={sortSettings.sortOrder} | ||||
|               onSort={event => setSortSettings(() => ({ sortField: event.sortField, sortOrder: event.sortOrder }))} | ||||
|               onRowMouseEnter={compact || medium ? handleEnterRow : undefined} | ||||
|               onRowMouseLeave={compact || medium ? handleLeaveRow : undefined} | ||||
|               rowClassName={row => { | ||||
|                 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); | ||||
|               if (!dateClass) { | ||||
|                 return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200'); | ||||
|               } | ||||
|                 const dateClass = getRowColorByTimeLeft(row.updated_at ? new Date(row.updated_at) : undefined); | ||||
|                 if (!dateClass) { | ||||
|                   return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200'); | ||||
|                 } | ||||
|  | ||||
|               return clsx(classes.TableRowCompact, dateClass); | ||||
|             }} | ||||
|           > | ||||
|             <Column | ||||
|               bodyClassName="p-0 px-1" | ||||
|               field="group" | ||||
|               body={renderIcon} | ||||
|               style={{ maxWidth: 26, minWidth: 26, width: 26, height: 25 }} | ||||
|             ></Column> | ||||
|                 return clsx(classes.TableRowCompact, dateClass); | ||||
|               }} | ||||
|             > | ||||
|               <Column | ||||
|                 bodyClassName="p-0 px-1" | ||||
|                 field="group" | ||||
|                 body={renderIcon} | ||||
|                 style={{ maxWidth: 26, minWidth: 26, width: 26, height: 25 }} | ||||
|               ></Column> | ||||
|  | ||||
|             <Column | ||||
|               field="eve_id" | ||||
|               header="Id" | ||||
|               bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" | ||||
|               style={{ maxWidth: 72, minWidth: 72, width: 72 }} | ||||
|               sortable | ||||
|             ></Column> | ||||
|             <Column | ||||
|               field="group" | ||||
|               header="Group" | ||||
|               bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" | ||||
|               hidden={compact} | ||||
|               sortable | ||||
|             ></Column> | ||||
|             <Column | ||||
|               field="name" | ||||
|               header="Name" | ||||
|               bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" | ||||
|               body={renderName} | ||||
|               style={{ maxWidth: nameColumnWidth }} | ||||
|               hidden={compact || medium} | ||||
|               sortable | ||||
|             ></Column> | ||||
|             <Column | ||||
|               field="updated_at" | ||||
|               header="Updated" | ||||
|               dataType="date" | ||||
|               bodyClassName="w-[80px] text-ellipsis overflow-hidden whitespace-nowrap" | ||||
|               body={renderTimeLeft} | ||||
|               sortable | ||||
|             ></Column> | ||||
|               <Column | ||||
|                 field="eve_id" | ||||
|                 header="Id" | ||||
|                 bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" | ||||
|                 style={{ maxWidth: 72, minWidth: 72, width: 72 }} | ||||
|                 sortable | ||||
|               ></Column> | ||||
|               <Column | ||||
|                 field="group" | ||||
|                 header="Group" | ||||
|                 bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" | ||||
|                 hidden={compact} | ||||
|                 sortable | ||||
|               ></Column> | ||||
|               <Column | ||||
|                 field="name" | ||||
|                 header="Name" | ||||
|                 bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" | ||||
|                 body={renderName} | ||||
|                 style={{ maxWidth: nameColumnWidth }} | ||||
|                 hidden={compact || medium} | ||||
|                 sortable | ||||
|               ></Column> | ||||
|               {!selectable && ( | ||||
|                 <Column | ||||
|                   field="linked_system" | ||||
|                   header="Linked System" | ||||
|                   bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" | ||||
|                   body={renderLinkedSystem} | ||||
|                   style={{ maxWidth: nameColumnWidth }} | ||||
|                   hidden={compact} | ||||
|                   sortable | ||||
|                 ></Column> | ||||
|               )} | ||||
|  | ||||
|             {/*<Column*/} | ||||
|             {/*  bodyClassName="p-0 pl-1 pr-2"*/} | ||||
|             {/*  field="group"*/} | ||||
|             {/*  body={renderToolbar}*/} | ||||
|             {/*  headerClassName={headerClasses}*/} | ||||
|             {/*  style={{ maxWidth: 26, minWidth: 26, width: 26 }}*/} | ||||
|             {/*></Column>*/} | ||||
|           </DataTable> | ||||
|         </> | ||||
|       )} | ||||
|       <WdTooltip | ||||
|         className="bg-stone-900/95 text-slate-50" | ||||
|         ref={tooltipRef} | ||||
|         content={hoveredSig ? <SignatureView {...hoveredSig} /> : null} | ||||
|       /> | ||||
|     </div> | ||||
|               <Column | ||||
|                 field="updated_at" | ||||
|                 header="Updated" | ||||
|                 dataType="date" | ||||
|                 bodyClassName="w-[80px] text-ellipsis overflow-hidden whitespace-nowrap" | ||||
|                 body={renderTimeLeft} | ||||
|                 sortable | ||||
|               ></Column> | ||||
|  | ||||
|               {/*<Column*/} | ||||
|               {/*  bodyClassName="p-0 pl-1 pr-2"*/} | ||||
|               {/*  field="group"*/} | ||||
|               {/*  body={renderToolbar}*/} | ||||
|               {/*  headerClassName={headerClasses}*/} | ||||
|               {/*  style={{ maxWidth: 26, minWidth: 26, width: 26 }}*/} | ||||
|               {/*></Column>*/} | ||||
|             </DataTable> | ||||
|           </> | ||||
|         )} | ||||
|         <WdTooltip | ||||
|           className="bg-stone-900/95 text-slate-50" | ||||
|           ref={tooltipRef} | ||||
|           content={hoveredSig ? <SignatureView {...hoveredSig} /> : null} | ||||
|         /> | ||||
|         {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> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { getState } from './getState.ts'; | ||||
| export const getActualSigs = ( | ||||
|   oldSignatures: SystemSignature[], | ||||
|   newSignatures: SystemSignature[], | ||||
|   updateOnly: boolean, | ||||
| ): { added: SystemSignature[]; updated: SystemSignature[]; removed: SystemSignature[] } => { | ||||
|   const updated: SystemSignature[] = []; | ||||
|   const removed: SystemSignature[] = []; | ||||
| @@ -20,7 +21,9 @@ export const getActualSigs = ( | ||||
|         updated.push({ ...oldSig, group: newSig.group, name: newSig.name }); | ||||
|       } | ||||
|     } else { | ||||
|       removed.push(oldSig); | ||||
|       if (!updateOnly) { | ||||
|         removed.push(oldSig); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   | ||||
| @@ -7,9 +7,9 @@ export const getState = (_: string[], newSig: SystemSignature) => { | ||||
|   let state = -1; | ||||
|   if (!newSig.group || newSig.group === '') { | ||||
|     state = 0; | ||||
|   } else if (!!newSig.group && newSig.group !== '' && newSig.name === '') { | ||||
|   } else if (!newSig.name || newSig.name === '') { | ||||
|     state = 1; | ||||
|   } else if (!!newSig.group && newSig.group !== '' && newSig.name !== '') { | ||||
|   } else if (newSig.name !== '') { | ||||
|     state = 2; | ||||
|   } | ||||
|   return state; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| export * from './renderIcon'; | ||||
| export * from './renderName'; | ||||
| export * from './renderTimeLeft'; | ||||
| export * from './renderLinkedSystem'; | ||||
|   | ||||
| @@ -0,0 +1,20 @@ | ||||
| import clsx from 'clsx'; | ||||
|  | ||||
| import { SystemSignature } from '@/hooks/Mapper/types'; | ||||
| import { SystemViewStandalone } from '@/hooks/Mapper/components/ui-kit'; | ||||
|  | ||||
| export const renderLinkedSystem = (row: SystemSignature) => { | ||||
|   if (!row.linked_system) { | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <span title={row.linked_system?.solar_system_name}> | ||||
|       <SystemViewStandalone | ||||
|         className={clsx('select-none text-center cursor-context-menu')} | ||||
|         hideRegion | ||||
|         {...row.linked_system} | ||||
|       /> | ||||
|     </span> | ||||
|   ); | ||||
| }; | ||||
| @@ -6,6 +6,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
| import { useCallback, useState } from 'react'; | ||||
| import { OnTheMap, RightBar } from '@/hooks/Mapper/components/mapRootContent/components'; | ||||
| import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/components/MapContextMenu/MapContextMenu.tsx'; | ||||
| import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu'; | ||||
|  | ||||
| export interface MapRootContentProps {} | ||||
|  | ||||
| @@ -19,6 +20,8 @@ export const MapRootContent = ({}: MapRootContentProps) => { | ||||
|  | ||||
|   const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []); | ||||
|  | ||||
|   useSkipContextMenu(); | ||||
|  | ||||
|   return ( | ||||
|     <Layout map={<MapWrapper refn={mapRef} />}> | ||||
|       {!isShowMenu ? ( | ||||
|   | ||||
| @@ -22,6 +22,13 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => { | ||||
|     }); | ||||
|   }, [outCommand]); | ||||
|  | ||||
|   const handleOpenUserSettings = useCallback(() => { | ||||
|     outCommand({ | ||||
|       type: OutCommand.openUserSettings, | ||||
|       data: null, | ||||
|     }); | ||||
|   }, [outCommand]); | ||||
|  | ||||
|   const toggleMinimap = useCallback(() => { | ||||
|     setInterfaceSettings(x => ({ | ||||
|       ...x, | ||||
| @@ -63,6 +70,16 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => { | ||||
|           </button> | ||||
|         </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}> | ||||
|           <button | ||||
|             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} | ||||
|           > | ||||
|             {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> | ||||
|         </WdTooltipWrapper> | ||||
|   | ||||
| @@ -5,13 +5,20 @@ import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider'; | ||||
| import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts'; | ||||
| import isEqual from 'lodash.isequal'; | ||||
| import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts'; | ||||
| import { SystemCustomLabelDialog, SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components'; | ||||
| import { | ||||
|   SystemCustomLabelDialog, | ||||
|   SystemSettingsDialog, | ||||
|   SystemLinkSignatureDialog, | ||||
| } from '@/hooks/Mapper/components/mapInterface/components'; | ||||
| import classes from './MapWrapper.module.scss'; | ||||
| import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections'; | ||||
| import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple'; | ||||
| import { getSystemById } from '@/hooks/Mapper/helpers'; | ||||
| import { Node } from 'reactflow'; | ||||
|  | ||||
| import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts'; | ||||
| import { useMapEventListener } from '@/hooks/Mapper/events'; | ||||
|  | ||||
| import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider'; | ||||
|  | ||||
| interface MapWrapperProps { | ||||
| @@ -53,6 +60,7 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => { | ||||
|   ); | ||||
|  | ||||
|   const [openSettings, setOpenSettings] = useState<string | null>(null); | ||||
|   const [openLinkSignatures, setOpenLinkSignatures] = useState<any | null>(null); | ||||
|   const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null); | ||||
|   const handleCommand: OutCommandHandler = useCallback( | ||||
|     event => { | ||||
| @@ -60,6 +68,9 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => { | ||||
|         case OutCommand.openSettings: | ||||
|           setOpenSettings(event.data.system_id); | ||||
|           break; | ||||
|         case OutCommand.linkSignatureToSystem: | ||||
|           setOpenLinkSignatures(event.data); | ||||
|           break; | ||||
|         default: | ||||
|           return outCommand(event); | ||||
|       } | ||||
| @@ -88,6 +99,14 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => { | ||||
|  | ||||
|   const handleConnectionDbClick = useCallback((e: SolarSystemConnection) => setSelectedConnection(e), []); | ||||
|  | ||||
|   useMapEventListener(event => { | ||||
|     switch (event.name) { | ||||
|       case Commands.linkSignatureToSystem: | ||||
|         setOpenLinkSignatures(event.data); | ||||
|         return true; | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <Map | ||||
| @@ -103,19 +122,15 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => { | ||||
|       /> | ||||
|  | ||||
|       {openSettings != null && ( | ||||
|         <SystemSettingsDialog | ||||
|           systemId={openSettings} | ||||
|           visible={openSettings != null} | ||||
|           setVisible={() => setOpenSettings(null)} | ||||
|         /> | ||||
|         <SystemSettingsDialog systemId={openSettings} visible setVisible={() => setOpenSettings(null)} /> | ||||
|       )} | ||||
|  | ||||
|       {openCustomLabel != null && ( | ||||
|         <SystemCustomLabelDialog | ||||
|           systemId={openCustomLabel} | ||||
|           visible={openCustomLabel != null} | ||||
|           setVisible={() => setOpenCustomLabel(null)} | ||||
|         /> | ||||
|         <SystemCustomLabelDialog systemId={openCustomLabel} visible setVisible={() => setOpenCustomLabel(null)} /> | ||||
|       )} | ||||
|  | ||||
|       {openLinkSignatures != null && ( | ||||
|         <SystemLinkSignatureDialog data={openLinkSignatures} setVisible={() => setOpenLinkSignatures(null)} /> | ||||
|       )} | ||||
|  | ||||
|       <Connections selectedConnection={selectedConnection} onHide={() => setSelectedConnection(null)} /> | ||||
|   | ||||
| @@ -37,7 +37,7 @@ export const CharacterCard = ({ | ||||
|   const { mapRef } = useMapRootState(); | ||||
|  | ||||
|   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]); | ||||
|  | ||||
|   return ( | ||||
|   | ||||
							
								
								
									
										13
									
								
								assets/js/hooks/Mapper/events/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								assets/js/hooks/Mapper/events/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { createEvent } from 'react-event-hook'; | ||||
|  | ||||
| export interface MapEvent { | ||||
|   name: string; | ||||
|   data: { | ||||
|     solar_system_source: number; | ||||
|     solar_system_target: number; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| const { useMapEventListener, emitMapEvent } = createEvent('map-event')<MapEvent>(); | ||||
|  | ||||
| export { useMapEventListener, emitMapEvent }; | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures'; | ||||
| import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog'; | ||||
| import { SystemSignature } from '@/hooks/Mapper/types'; | ||||
|  | ||||
| export const parseSignatures = (value: string, availableKeys: string[]): SystemSignature[] => { | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| export * from './usePageVisibility'; | ||||
| export * from './useClipboard'; | ||||
| export * from './useHotkey'; | ||||
| export * from './useSkipContextMenu'; | ||||
|   | ||||
							
								
								
									
										15
									
								
								assets/js/hooks/Mapper/hooks/useSkipContextMenu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								assets/js/hooks/Mapper/hooks/useSkipContextMenu.ts
									
									
									
									
									
										Normal 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); | ||||
|     }; | ||||
|   }, []); | ||||
| }; | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { createRoot } from 'react-dom/client'; | ||||
| import Mapper from './MapRoot'; | ||||
| import { decompressToJson } from './utils'; | ||||
|  | ||||
| export default { | ||||
|   _rootEl: null, | ||||
| @@ -28,17 +27,12 @@ export default { | ||||
|  | ||||
|   handleEventWrapper(event: string, handler: (payload: any) => void) { | ||||
|     this.handleEvent(event, (body: any) => { | ||||
|       if (event === 'map_event') { | ||||
|         const { type, body: data } = body; | ||||
|         handler({ type, body: decompressToJson(data) }); | ||||
|       } else { | ||||
|         handler(body); | ||||
|       } | ||||
|       handler(body); | ||||
|     }); | ||||
|   }, | ||||
|  | ||||
|   reconnected() { | ||||
|     this.pushEvent('reconnected'); | ||||
|     this.pushEvent('ui_loaded'); | ||||
|   }, | ||||
|  | ||||
|   async pushEventAsync(event: string, payload: any) { | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import { MapHandlers, MapUnionTypes, OutCommandHandler, SolarSystemConnection } | ||||
| import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks'; | ||||
| import { WithChildren } from '@/hooks/Mapper/types/common.ts'; | ||||
| import useLocalStorageState from 'use-local-storage-state'; | ||||
| import { DEFAULT_SETTINGS } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx'; | ||||
|  | ||||
| export type MapRootData = MapUnionTypes & { | ||||
|   selectedSystems: string[]; | ||||
|   | ||||
| @@ -27,6 +27,8 @@ import { | ||||
|   useRoutes, | ||||
| } from './api'; | ||||
|  | ||||
| import { emitMapEvent } from '@/hooks/Mapper/events'; | ||||
|  | ||||
| export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => { | ||||
|   const mapInit = useMapInit(); | ||||
|   const { addSystems, removeSystems, updateSystems } = useCommandsSystems(); | ||||
| @@ -85,10 +87,18 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => { | ||||
|               mapRoutes(data as CommandRoutes); | ||||
|               break; | ||||
|  | ||||
|             case Commands.centerSystem: | ||||
|               // do nothing here | ||||
|               break; | ||||
|  | ||||
|             case Commands.selectSystem: | ||||
|               // do nothing here | ||||
|               break; | ||||
|  | ||||
|             case Commands.linkSignatureToSystem: | ||||
|               emitMapEvent({ name: Commands.linkSignatureToSystem, data }); | ||||
|               break; | ||||
|  | ||||
|             case Commands.killsUpdated: | ||||
|               // do nothing here | ||||
|               break; | ||||
|   | ||||
| @@ -21,7 +21,9 @@ export enum Commands { | ||||
|   mapUpdated = 'map_updated', | ||||
|   killsUpdated = 'kills_updated', | ||||
|   routes = 'routes', | ||||
|   centerSystem = 'center_system', | ||||
|   selectSystem = 'select_system', | ||||
|   linkSignatureToSystem = 'link_signature_to_system', | ||||
| } | ||||
|  | ||||
| export type Command = | ||||
| @@ -40,7 +42,9 @@ export type Command = | ||||
|   | Commands.mapUpdated | ||||
|   | Commands.killsUpdated | ||||
|   | Commands.routes | ||||
|   | Commands.selectSystem; | ||||
|   | Commands.selectSystem | ||||
|   | Commands.centerSystem | ||||
|   | Commands.linkSignatureToSystem; | ||||
|  | ||||
| export type CommandInit = { | ||||
|   systems: SolarSystemRawType[]; | ||||
| @@ -72,6 +76,12 @@ export type CommandMapUpdated = Partial<CommandInit>; | ||||
| export type CommandRoutes = RoutesList; | ||||
| export type CommandKillsUpdated = Kill[]; | ||||
| export type CommandSelectSystem = string | undefined; | ||||
| export type CommandCenterSystem = string | undefined; | ||||
| export type CommandLinkSignatureToSystem = { | ||||
|   solar_system_source: number; | ||||
|   solar_system_target: number; | ||||
|   signatures: any[]; | ||||
| }; | ||||
|  | ||||
| export interface CommandData { | ||||
|   [Commands.init]: CommandInit; | ||||
| @@ -90,6 +100,8 @@ export interface CommandData { | ||||
|   [Commands.routes]: CommandRoutes; | ||||
|   [Commands.killsUpdated]: CommandKillsUpdated; | ||||
|   [Commands.selectSystem]: CommandSelectSystem; | ||||
|   [Commands.centerSystem]: CommandCenterSystem; | ||||
|   [Commands.linkSignatureToSystem]: CommandLinkSignatureToSystem; | ||||
| } | ||||
|  | ||||
| export interface MapHandlers { | ||||
| @@ -123,7 +135,9 @@ export enum OutCommand { | ||||
|   setAutopilotWaypoint = 'set_autopilot_waypoint', | ||||
|   addSystem = 'add_system', | ||||
|   addCharacter = 'add_character', | ||||
|   openUserSettings = 'open_user_settings', | ||||
|   getPassages = 'get_passages', | ||||
|   linkSignatureToSystem = 'link_signature_to_system', | ||||
|  | ||||
|   // Only UI commands | ||||
|   openSettings = 'open_settings', | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types'; | ||||
|  | ||||
| export type SystemSignature = { | ||||
|   eve_id: string; | ||||
|   kind: string; | ||||
|   name: string; | ||||
|   description?: string; | ||||
|   group: string; | ||||
|   linked_system?: SolarSystemStaticInfoRaw; | ||||
|   updated_at?: string; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
| }; | ||||
| @@ -1,3 +1,2 @@ | ||||
| export * from './contextStore'; | ||||
| export * from './decompressToJson'; | ||||
| export * from './getQueryVariable'; | ||||
|   | ||||
| @@ -3,7 +3,230 @@ import 'phoenix_html'; | ||||
|  | ||||
| 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 () { | ||||
|   // animage background | ||||
|   const canvas = document.getElementById('bg-canvas'); | ||||
|   if (canvas) { | ||||
|     animateBg(canvas); | ||||
|   } | ||||
|  | ||||
|   // Select all buttons with the 'share-link' class | ||||
|   const buttons = document.querySelectorAll('button.copy-link'); | ||||
|  | ||||
|   | ||||
| @@ -21,7 +21,6 @@ | ||||
|     "live_select": "file:../deps/live_select", | ||||
|     "lodash.debounce": "^4.0.8", | ||||
|     "lodash.isequal": "^4.5.0", | ||||
|     "pako": "^2.1.0", | ||||
|     "phoenix": "file:../deps/phoenix", | ||||
|     "phoenix_html": "file:../deps/phoenix_html", | ||||
|     "phoenix_live_view": "file:../deps/phoenix_live_view", | ||||
| @@ -29,6 +28,7 @@ | ||||
|     "primeicons": "^7.0.0", | ||||
|     "primereact": "^10.6.5", | ||||
|     "react-error-boundary": "^4.0.13", | ||||
|     "react-event-hook": "^3.1.2", | ||||
|     "react-flow-renderer": "^10.3.17", | ||||
|     "react-grid-layout": "^1.3.4", | ||||
|     "react-usestateref": "^1.0.9", | ||||
| @@ -44,7 +44,6 @@ | ||||
|     "@tailwindcss/typography": "^0.5.13", | ||||
|     "@types/lodash.debounce": "^4.0.9", | ||||
|     "@types/lodash.isequal": "^4.5.8", | ||||
|     "@types/pako": "^2.0.3", | ||||
|     "@types/react": "18.2.0", | ||||
|     "@types/react-dom": "18.2.1", | ||||
|     "@types/react-grid-layout": "^1.3.4", | ||||
|   | ||||
							
								
								
									
										349
									
								
								assets/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										349
									
								
								assets/yarn.lock
									
									
									
									
									
								
							| @@ -33,7 +33,7 @@ | ||||
|   resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz" | ||||
|   integrity sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA== | ||||
| @@ -240,11 +240,121 @@ | ||||
|     "@babel/helper-validator-identifier" "^7.24.5" | ||||
|     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": | ||||
|   version "0.20.2" | ||||
|   resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz" | ||||
|   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": | ||||
|   version "4.4.0" | ||||
|   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" | ||||
|     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" | ||||
|   resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" | ||||
|   integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== | ||||
| @@ -359,7 +469,7 @@ | ||||
|   resolved "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/@react-rxjs/core/-/core-0.10.7.tgz" | ||||
|   integrity sha512-dornp8pUs9OcdqFKKRh9+I2FVe21gWufNun6RYU1ddts7kUy9i4Thvl0iqcPFbGY61cJQMAJF7dxixWMSD/A/A== | ||||
| @@ -455,11 +565,91 @@ | ||||
|     estree-walker "^2.0.2" | ||||
|     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": | ||||
|   version "4.17.2" | ||||
|   resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz" | ||||
|   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": | ||||
|   version "0.1.4" | ||||
|   resolved "https://registry.npmjs.org/@rx-state/core/-/core-0.1.4.tgz" | ||||
| @@ -740,7 +930,7 @@ | ||||
|     "@types/d3-transition" "*" | ||||
|     "@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" | ||||
|   resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" | ||||
|   integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== | ||||
| @@ -774,11 +964,6 @@ | ||||
|   resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz" | ||||
|   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@*": | ||||
|   version "15.7.11" | ||||
|   resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz" | ||||
| @@ -805,7 +990,7 @@ | ||||
|   dependencies: | ||||
|     "@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" | ||||
|   resolved "https://registry.npmjs.org/@types/react/-/react-18.2.0.tgz" | ||||
|   integrity sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA== | ||||
| @@ -846,7 +1031,7 @@ | ||||
|     semver "^7.5.4" | ||||
|     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" | ||||
|   resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz" | ||||
|   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" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz" | ||||
|   integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== | ||||
| @@ -1147,7 +1332,7 @@ braces@^3.0.2, braces@~3.0.2: | ||||
|   dependencies: | ||||
|     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" | ||||
|   resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz" | ||||
|   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" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" | ||||
|   integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== | ||||
| @@ -1258,16 +1443,16 @@ color-convert@^2.0.1: | ||||
|   dependencies: | ||||
|     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: | ||||
|   version "1.1.3" | ||||
|   resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" | ||||
|   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: | ||||
|   version "4.1.1" | ||||
|   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" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz" | ||||
|   integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== | ||||
| @@ -1345,7 +1530,7 @@ d3-drag@^3.0.0, "d3-drag@2 - 3": | ||||
|   dependencies: | ||||
|     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" | ||||
|   resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz" | ||||
|   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" | ||||
|   integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== | ||||
|  | ||||
| eslint-config-prettier@*, eslint-config-prettier@^9.1.0: | ||||
| eslint-config-prettier@^9.1.0: | ||||
|   version "9.1.0" | ||||
|   resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz" | ||||
|   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" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz" | ||||
|   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" | ||||
|   integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== | ||||
|  | ||||
| estree-walker@^2.0.1: | ||||
|   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: | ||||
| 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== | ||||
| @@ -2010,18 +2190,6 @@ glob-parent@^6.0.2: | ||||
|   dependencies: | ||||
|     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: | ||||
|   version "7.1.6" | ||||
|   resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" | ||||
| @@ -2034,6 +2202,18 @@ glob@7.1.6: | ||||
|     once "^1.3.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: | ||||
|   version "11.12.0" | ||||
|   resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" | ||||
| @@ -2141,14 +2321,7 @@ hasown@^2.0.0: | ||||
|   dependencies: | ||||
|     function-bind "^1.1.2" | ||||
|  | ||||
| hasown@^2.0.1: | ||||
|   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: | ||||
| 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== | ||||
| @@ -2420,7 +2593,7 @@ iterator.prototype@^1.1.2: | ||||
|     reflect.getprototypeof "^1.0.4" | ||||
|     set-function-name "^2.0.1" | ||||
|  | ||||
| jiti@^1.19.1, jiti@>=1.21.0: | ||||
| jiti@^1.19.1: | ||||
|   version "1.21.0" | ||||
|   resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz" | ||||
|   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": | ||||
|   version "1.4.2" | ||||
|   resolved "file:../deps/live_select" | ||||
|  | ||||
| locate-path@^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" | ||||
|   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: | ||||
|   version "9.0.3" | ||||
|   resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" | ||||
| @@ -2626,6 +2791,13 @@ minimatch@9.0.3: | ||||
|   dependencies: | ||||
|     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: | ||||
|   version "2.1.2" | ||||
|   resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" | ||||
| @@ -2770,11 +2942,6 @@ p-locate@^5.0.0: | ||||
|   dependencies: | ||||
|     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: | ||||
|   version "1.0.1" | ||||
|   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" | ||||
|   integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== | ||||
|  | ||||
| "phoenix@file:../deps/phoenix": | ||||
|   version "1.7.14" | ||||
|  | ||||
| "phoenix_html@file:../deps/phoenix_html": | ||||
|   version "4.1.0" | ||||
|   resolved "file:../deps/phoenix_html" | ||||
|  | ||||
| "phoenix_live_view@file:../deps/phoenix_live_view": | ||||
|   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: | ||||
|   version "1.0.0" | ||||
| @@ -2923,14 +3087,6 @@ postcss-reporter@^7.0.0: | ||||
|     picocolors "^1.0.0" | ||||
|     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: | ||||
|   version "6.0.10" | ||||
|   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" | ||||
|     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: | ||||
|   version "4.2.0" | ||||
|   resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz" | ||||
|   integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== | ||||
| @@ -2965,7 +3129,7 @@ prettier-linter-helpers@^1.0.0: | ||||
|   dependencies: | ||||
|     fast-diff "^1.1.2" | ||||
|  | ||||
| prettier@^3.2.5, prettier@>=3.0.0: | ||||
| prettier@^3.2.5: | ||||
|   version "3.2.5" | ||||
|   resolved "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz" | ||||
|   integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== | ||||
| @@ -2993,7 +3157,7 @@ primereact@^10.6.5: | ||||
|     "@types/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" | ||||
|   resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" | ||||
|   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" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" | ||||
|   integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== | ||||
| @@ -3035,6 +3199,11 @@ react-error-boundary@^4.0.13: | ||||
|   dependencies: | ||||
|     "@babel/runtime" "^7.12.5" | ||||
|  | ||||
| react-event-hook@^3.1.2: | ||||
|   version "3.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/react-event-hook/-/react-event-hook-3.1.2.tgz#445e8f3b751f6abe4ef199f31bff47593c4c13d4" | ||||
|   integrity sha512-qQ9LXLdxmWRRZPlnqVjqlw7jovSvDosQEOyQ9cjPHhtDv8JIszjj0td1PuHJHrVW0LS8a1XeJhLe6i7S5u9SbQ== | ||||
|  | ||||
| react-flow-renderer@^10.3.17: | ||||
|   version "10.3.17" | ||||
|   resolved "https://registry.npmjs.org/react-flow-renderer/-/react-flow-renderer-10.3.17.tgz" | ||||
| @@ -3099,7 +3268,7 @@ react-usestateref@^1.0.9: | ||||
|   resolved "https://registry.npmjs.org/react-usestateref/-/react-usestateref-1.0.9.tgz" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" | ||||
|   integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== | ||||
| @@ -3215,7 +3384,7 @@ rollup-plugin-external-globals@^0.10.0: | ||||
|     is-reference "^3.0.2" | ||||
|     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" | ||||
|   resolved "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz" | ||||
|   integrity sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ== | ||||
| @@ -3247,7 +3416,7 @@ run-parallel@^1.1.9: | ||||
|   dependencies: | ||||
|     queue-microtask "^1.2.2" | ||||
|  | ||||
| rxjs@^7.8.1, rxjs@>=6, rxjs@>=7: | ||||
| rxjs@^7.8.1: | ||||
|   version "7.8.1" | ||||
|   resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz" | ||||
|   integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== | ||||
| @@ -3280,7 +3449,7 @@ sass-loader@^14.2.1: | ||||
|   dependencies: | ||||
|     neo-async "^2.6.2" | ||||
|  | ||||
| sass@*, sass@^1.3.0, sass@^1.77.2: | ||||
| sass@^1.77.2: | ||||
|   version "1.77.2" | ||||
|   resolved "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz" | ||||
|   integrity sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA== | ||||
| @@ -3362,7 +3531,7 @@ slash@^5.0.0, slash@^5.1.0: | ||||
|   resolved "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" | ||||
|   integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== | ||||
| @@ -3479,7 +3648,7 @@ synckit@^0.8.6: | ||||
|     "@pkgr/core" "^0.1.0" | ||||
|     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" | ||||
|   resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz" | ||||
|   integrity sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw== | ||||
| @@ -3619,7 +3788,7 @@ typed-array-length@^1.0.6: | ||||
|     is-typed-array "^1.1.13" | ||||
|     possible-typed-array-names "^1.0.0" | ||||
|  | ||||
| typescript@^5.2.2, typescript@>=4.2.0: | ||||
| typescript@^5.2.2: | ||||
|   version "5.4.5" | ||||
|   resolved "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz" | ||||
|   integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== | ||||
| @@ -3664,7 +3833,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" | ||||
|   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" | ||||
|   resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" | ||||
|   integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== | ||||
| @@ -3692,7 +3861,7 @@ vite-plugin-externals@^0.6.2: | ||||
|     fs-extra "^10.0.0" | ||||
|     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" | ||||
|   resolved "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz" | ||||
|   integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ== | ||||
|   | ||||
| @@ -60,15 +60,7 @@ config :dart_sass, :version, "1.54.5" | ||||
|  | ||||
| config :tailwind, :version, "3.2.7" | ||||
|  | ||||
| config :wanderer_app, WandererApp.PromEx, | ||||
|   manual_metrics_start_delay: :no_delay, | ||||
|   metrics_server: [ | ||||
|     port: 4021, | ||||
|     path: "/metrics", | ||||
|     protocol: :http, | ||||
|     pool_size: 5, | ||||
|     cowboy_opts: [ip: {0, 0, 0, 0}] | ||||
|   ] | ||||
| config :wanderer_app, WandererApp.PromEx, manual_metrics_start_delay: :no_delay | ||||
|  | ||||
| config :wanderer_app, | ||||
|   grafana_datasource_id: "wanderer" | ||||
|   | ||||
| @@ -55,7 +55,6 @@ config :wanderer_app, WandererAppWeb.Endpoint, | ||||
| config :wanderer_app, WandererAppWeb.Endpoint, | ||||
|   live_reload: [ | ||||
|     interval: 1000, | ||||
|     web_console_logger: true, | ||||
|     patterns: [ | ||||
|       ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", | ||||
|       ~r"priv/gettext/.*(po)$", | ||||
|   | ||||
| @@ -19,6 +19,7 @@ defmodule WandererApp.Api do | ||||
|     resource WandererApp.Api.MapCharacterSettings | ||||
|     resource WandererApp.Api.MapSubscription | ||||
|     resource WandererApp.Api.MapTransaction | ||||
|     resource WandererApp.Api.MapUserSettings | ||||
|     resource WandererApp.Api.User | ||||
|     resource WandererApp.Api.ShipTypeInfo | ||||
|     resource WandererApp.Api.UserActivity | ||||
|   | ||||
| @@ -30,80 +30,89 @@ defmodule WandererApp.Api.Calculations.CalcMapPermissions do | ||||
|  | ||||
|     result = | ||||
|       record.acls | ||||
|       |> Enum.filter(fn acl -> | ||||
|         acl.owner_id in character_ids or | ||||
|           acl.members |> Enum.any?(fn member -> member.eve_character_id in character_eve_ids end) or | ||||
|       |> Enum.reduce([0, 0], fn acl, acc -> | ||||
|         is_owner? = acl.owner_id in character_ids | ||||
|  | ||||
|         is_character_member? = | ||||
|           acl.members |> Enum.any?(fn member -> member.eve_character_id in character_eve_ids end) | ||||
|  | ||||
|         is_corporation_member? = | ||||
|           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 | ||||
|           |> 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] -> | ||||
|             char_acl_mask = | ||||
|               acl.members | ||||
|               |> Enum.filter(fn member -> | ||||
|                 member.eve_character_id in character_eve_ids | ||||
|               end) | ||||
|               |> Enum.reduce(0, fn member, acc -> | ||||
|                 case acc do | ||||
|         if is_owner? || is_character_member? || is_corporation_member? || is_alliance_member? do | ||||
|           case acc do | ||||
|             [_, -1] -> | ||||
|               [-1, -1] | ||||
|  | ||||
|             [-1, char_acc] -> | ||||
|               char_acl_mask = | ||||
|                 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 | ||||
|                   _ -> WandererApp.Permissions.calc_role_mask(member.role, acc) | ||||
|                   _ -> char_acc ||| char_acl_mask | ||||
|                 end | ||||
|               end) | ||||
|  | ||||
|             char_acc = | ||||
|               case char_acl_mask do | ||||
|                 -1 -> -1 | ||||
|                 _ -> char_acc ||| char_acl_mask | ||||
|               end | ||||
|               [-1, char_acc] | ||||
|  | ||||
|             [-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] -> | ||||
|             any_acl_mask = | ||||
|               acl.members | ||||
|               |> Enum.filter(fn member -> | ||||
|                 member.eve_character_id in character_eve_ids or | ||||
|                   member.eve_corporation_id in character_corporation_ids or | ||||
|                   member.eve_alliance_id in character_alliance_ids | ||||
|               end) | ||||
|               |> Enum.reduce(0, fn member, acc -> | ||||
|                 case acc do | ||||
|               char_acl_mask = | ||||
|                 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) | ||||
|  | ||||
|               any_acc = | ||||
|                 case any_acl_mask do | ||||
|                   -1 -> -1 | ||||
|                   _ -> WandererApp.Permissions.calc_role_mask(member.role, acc) | ||||
|                   _ -> any_acc ||| any_acl_mask | ||||
|                 end | ||||
|               end) | ||||
|  | ||||
|             char_acl_mask = | ||||
|               acl.members | ||||
|               |> Enum.filter(fn member -> | ||||
|                 member.eve_character_id in character_eve_ids | ||||
|               end) | ||||
|               |> Enum.reduce(0, fn member, acc -> | ||||
|                 case acc do | ||||
|               char_acc = | ||||
|                 case char_acl_mask do | ||||
|                   -1 -> -1 | ||||
|                   _ -> WandererApp.Permissions.calc_role_mask(member.role, acc) | ||||
|                   _ -> char_acc ||| char_acl_mask | ||||
|                 end | ||||
|               end) | ||||
|  | ||||
|             any_acc = | ||||
|               case any_acl_mask do | ||||
|                 -1 -> -1 | ||||
|                 _ -> any_acc ||| any_acl_mask | ||||
|               end | ||||
|  | ||||
|             char_acc = | ||||
|               case char_acl_mask do | ||||
|                 -1 -> -1 | ||||
|                 _ -> char_acc ||| char_acl_mask | ||||
|               end | ||||
|  | ||||
|             [any_acc, char_acc] | ||||
|               [any_acc, char_acc] | ||||
|           end | ||||
|         else | ||||
|           acc | ||||
|         end | ||||
|       end) | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,7 @@ defmodule WandererApp.Api.Map do | ||||
|     define(:update, action: :update) | ||||
|     define(:update_acls, action: :update_acls) | ||||
|     define(:update_hubs, action: :update_hubs) | ||||
|     define(:update_options, action: :update_options) | ||||
|     define(:assign_owner, action: :assign_owner) | ||||
|     define(:mark_as_deleted, action: :mark_as_deleted) | ||||
|  | ||||
| @@ -112,6 +113,10 @@ defmodule WandererApp.Api.Map do | ||||
|       accept [:hubs] | ||||
|     end | ||||
|  | ||||
|     update :update_options do | ||||
|       accept [:options] | ||||
|     end | ||||
|  | ||||
|     update :mark_as_deleted do | ||||
|       accept([]) | ||||
|  | ||||
| @@ -167,6 +172,10 @@ defmodule WandererApp.Api.Map do | ||||
|       allow_nil?(true) | ||||
|     end | ||||
|  | ||||
|     attribute :options, :string do | ||||
|       allow_nil? true | ||||
|     end | ||||
|  | ||||
|     create_timestamp(:inserted_at) | ||||
|     update_timestamp(:updated_at) | ||||
|   end | ||||
|   | ||||
| @@ -48,7 +48,13 @@ defmodule WandererApp.Api.MapConnection do | ||||
|       argument(:map_id, :string, allow_nil?: false) | ||||
|       argument(:solar_system_source, :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 | ||||
|  | ||||
|     read :get_link_pairs_advanced do | ||||
|   | ||||
| @@ -14,6 +14,7 @@ defmodule WandererApp.Api.MapSystemSignature do | ||||
|     define(:all_active, action: :all_active) | ||||
|     define(:create, action: :create) | ||||
|     define(:update, action: :update) | ||||
|     define(:update_linked_system, action: :update_linked_system) | ||||
|  | ||||
|     define(:by_id, | ||||
|       get_by: [:id], | ||||
| @@ -66,13 +67,18 @@ defmodule WandererApp.Api.MapSystemSignature do | ||||
|         :name, | ||||
|         :description, | ||||
|         :kind, | ||||
|         :group | ||||
|         :group, | ||||
|         :linked_system_id | ||||
|       ] | ||||
|  | ||||
|       primary? true | ||||
|       require_atomic? false | ||||
|     end | ||||
|  | ||||
|     update :update_linked_system do | ||||
|       accept [:linked_system_id] | ||||
|     end | ||||
|  | ||||
|     read :by_system_id do | ||||
|       argument(:system_id, :string, allow_nil?: false) | ||||
|  | ||||
| @@ -99,6 +105,10 @@ defmodule WandererApp.Api.MapSystemSignature do | ||||
|       allow_nil? true | ||||
|     end | ||||
|  | ||||
|     attribute :linked_system_id, :integer do | ||||
|       allow_nil? true | ||||
|     end | ||||
|  | ||||
|     attribute :kind, :string | ||||
|     attribute :group, :string | ||||
|  | ||||
|   | ||||
							
								
								
									
										54
									
								
								lib/wanderer_app/api/map_user_settings.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								lib/wanderer_app/api/map_user_settings.ex
									
									
									
									
									
										Normal 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 | ||||
| @@ -38,8 +38,6 @@ defmodule WandererApp.Application do | ||||
|         WandererApp.Character.TrackerManager, | ||||
|         WandererApp.Map.Manager, | ||||
|         WandererApp.Map.ZkbDataFetcher, | ||||
|         WandererApp.Character.ActivityTracker, | ||||
|         WandererApp.User.ActivityTracker, | ||||
|         WandererAppWeb.Presence, | ||||
|         WandererAppWeb.Endpoint | ||||
|       ] ++ maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?()) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ defmodule WandererApp.Character do | ||||
|   @read_character_wallet_scope "esi-wallet.read_character_wallet.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 | ||||
|       {:ok, nil} -> | ||||
|         case WandererApp.Api.Character.by_id(character_id) do | ||||
| @@ -23,6 +23,8 @@ defmodule WandererApp.Character do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def get_character(_character_id), do: {:ok, nil} | ||||
|  | ||||
|   def get_character!(character_id) do | ||||
|     case get_character(character_id) do | ||||
|       {:ok, character} -> | ||||
| @@ -71,11 +73,24 @@ defmodule WandererApp.Character do | ||||
|     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 | ||||
|     Cachex.get_and_update(:character_state_cache, character_id, fn character_state -> | ||||
|       case character_state do | ||||
|         nil -> | ||||
|           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)} | ||||
|  | ||||
|         _ -> | ||||
|   | ||||
| @@ -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 | ||||
| @@ -35,6 +35,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|   @online_error_timeout :timer.minutes(2) | ||||
|   @forbidden_ttl :timer.minutes(1) | ||||
|   @pubsub_client Application.compile_env(:wanderer_app, :pubsub_client) | ||||
|  | ||||
|   def new(), do: __struct__() | ||||
|   def new(args), do: __struct__(args) | ||||
| @@ -53,69 +54,55 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|     {:ok, | ||||
|      character_state | ||||
|      |> _maybe_update_active_maps(track_settings) | ||||
|      |> _maybe_stop_tracking(track_settings) | ||||
|      |> _maybe_start_online_tracking(track_settings) | ||||
|      |> _maybe_start_location_tracking(track_settings) | ||||
|      |> _maybe_start_ship_tracking(track_settings)} | ||||
|      |> maybe_update_active_maps(track_settings) | ||||
|      |> maybe_stop_tracking(track_settings) | ||||
|      |> maybe_start_online_tracking(track_settings) | ||||
|      |> maybe_start_location_tracking(track_settings) | ||||
|      |> maybe_start_ship_tracking(track_settings)} | ||||
|   end | ||||
|  | ||||
|   def update_info(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_info(character_state) | ||||
|   end | ||||
|     WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden") | ||||
|     |> case do | ||||
|       true -> | ||||
|         {:error, :skipped} | ||||
|  | ||||
|   def update_ship(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_ship(character_state) | ||||
|   end | ||||
|       false -> | ||||
|         {:ok, %{eve_id: eve_id}} = WandererApp.Character.get_character(character_id) | ||||
|  | ||||
|   def update_location(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_location(character_state) | ||||
|   end | ||||
|         case WandererApp.Esi.get_character_info(eve_id) do | ||||
|           {:ok, info} -> | ||||
|             {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|             update = maybe_update_corporation(character_state, info) | ||||
|             WandererApp.Character.update_character_state(character_id, update) | ||||
|  | ||||
|   def update_online(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_online(character_state) | ||||
|   end | ||||
|             :ok | ||||
|  | ||||
|   def check_online_errors(character_id) do | ||||
|     case(WandererApp.Cache.lookup!("character:#{character_id}:online_error_time")) do | ||||
|       nil -> | ||||
|         :skip | ||||
|           {:error, :forbidden} -> | ||||
|             Logger.warning("#{__MODULE__} failed to get_character_info: forbidden") | ||||
|  | ||||
|       error_time -> | ||||
|         duration = DateTime.diff(DateTime.utc_now(), error_time, :second) | ||||
|             WandererApp.Cache.put( | ||||
|               "character:#{character_id}:info_forbidden", | ||||
|               true, | ||||
|               ttl: @forbidden_ttl | ||||
|             ) | ||||
|  | ||||
|         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, %{ | ||||
|             character_state | ||||
|             | is_online: false, | ||||
|               track_ship: false, | ||||
|               track_location: false | ||||
|           }) | ||||
|  | ||||
|           :ok | ||||
|         else | ||||
|           :skip | ||||
|           {:error, error} -> | ||||
|             Logger.error("#{__MODULE__} failed to get_character_info: #{inspect(error)}") | ||||
|             {:error, error} | ||||
|         end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def update_wallet(character_id) do | ||||
|     {:ok, character_state} = WandererApp.Character.get_character_state(character_id) | ||||
|     _update_wallet(character_state) | ||||
|   def update_ship(character_id) when is_binary(character_id) do | ||||
|     character_id | ||||
|     |> WandererApp.Character.get_character_state!() | ||||
|     |> update_ship() | ||||
|   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 | ||||
|       {:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) -> | ||||
|         WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden") | ||||
| @@ -123,14 +110,14 @@ defmodule WandererApp.Character.Tracker do | ||||
|           true -> | ||||
|             {:error, :skipped} | ||||
|  | ||||
|           false -> | ||||
|           _ -> | ||||
|             case WandererApp.Esi.get_character_ship(eve_id, | ||||
|                    access_token: access_token, | ||||
|                    character_id: character_id, | ||||
|                    refresh_token?: true | ||||
|                  ) do | ||||
|               {:ok, ship} -> | ||||
|                 character_state |> _maybe_update_ship(ship) | ||||
|                 character_state |> maybe_update_ship(ship) | ||||
|  | ||||
|                 :ok | ||||
|  | ||||
| @@ -156,9 +143,68 @@ defmodule WandererApp.Character.Tracker do | ||||
|     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 | ||||
|       {:ok, %{eve_id: eve_id, access_token: access_token}} | ||||
|       when not is_nil(access_token) -> | ||||
| @@ -167,14 +213,14 @@ defmodule WandererApp.Character.Tracker do | ||||
|           true -> | ||||
|             {:error, :skipped} | ||||
|  | ||||
|           false -> | ||||
|           _ -> | ||||
|             case WandererApp.Esi.get_character_online(eve_id, | ||||
|                    access_token: access_token, | ||||
|                    character_id: character_id, | ||||
|                    refresh_token?: true | ||||
|                  ) do | ||||
|               {: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_error_time") | ||||
| @@ -240,57 +286,43 @@ defmodule WandererApp.Character.Tracker do | ||||
|     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 | ||||
|     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} | ||||
|   def check_online_errors(character_id) do | ||||
|     WandererApp.Cache.lookup!("character:#{character_id}:online_error_time") | ||||
|     |> case do | ||||
|       nil -> | ||||
|         :skip | ||||
|  | ||||
|           false -> | ||||
|             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) | ||||
|       error_time -> | ||||
|         duration = DateTime.diff(DateTime.utc_now(), error_time, :second) | ||||
|  | ||||
|                 :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} -> | ||||
|                 Logger.warning("#{__MODULE__} failed to update_location: forbidden") | ||||
|           WandererApp.Character.update_character_state(character_id, %{ | ||||
|             character_state | ||||
|             | is_online: false, | ||||
|               track_ship: false, | ||||
|               track_location: false | ||||
|           }) | ||||
|  | ||||
|                 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} | ||||
|           :ok | ||||
|         else | ||||
|           :skip | ||||
|         end | ||||
|  | ||||
|       _ -> | ||||
|         {:error, :skipped} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _update_location(_), do: {:error, :skipped} | ||||
|  | ||||
|   defp _update_wallet(%{character_id: character_id} = state) do | ||||
|     case WandererApp.Character.get_character(character_id) do | ||||
|   def update_wallet(character_id) do | ||||
|     character_id | ||||
|     |> WandererApp.Character.get_character() | ||||
|     |> case do | ||||
|       {:ok, %{eve_id: eve_id, access_token: access_token} = character} | ||||
|       when not is_nil(access_token) -> | ||||
|         character | ||||
| @@ -302,7 +334,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|               true -> | ||||
|                 {:error, :skipped} | ||||
|  | ||||
|               false -> | ||||
|               _ -> | ||||
|                 case WandererApp.Esi.get_character_wallet(eve_id, | ||||
|                        params: %{datasource: "tranquility"}, | ||||
|                        access_token: access_token, | ||||
| @@ -310,7 +342,8 @@ defmodule WandererApp.Character.Tracker do | ||||
|                        refresh_token?: true | ||||
|                      ) do | ||||
|                   {:ok, result} -> | ||||
|                     state |> _maybe_update_wallet(result) | ||||
|                     {:ok, state} = WandererApp.Character.get_character_state(character_id) | ||||
|                     maybe_update_wallet(state, result) | ||||
|  | ||||
|                     :ok | ||||
|  | ||||
| @@ -340,42 +373,10 @@ defmodule WandererApp.Character.Tracker do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _update_info(%{character_id: character_id} = character_state) do | ||||
|     {:ok, %{eve_id: eve_id}} = WandererApp.Character.get_character(character_id) | ||||
|  | ||||
|     WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden") | ||||
|   defp update_alliance(%{character_id: character_id} = state, alliance_id) do | ||||
|     alliance_id | ||||
|     |> WandererApp.Esi.get_alliance_info() | ||||
|     |> 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, character} = WandererApp.Character.get_character(character_id) | ||||
|  | ||||
| @@ -390,7 +391,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|         WandererApp.Character.update_character(character_id, character_update) | ||||
|  | ||||
|         Phoenix.PubSub.broadcast( | ||||
|         @pubsub_client.broadcast( | ||||
|           WandererApp.PubSub, | ||||
|           "character:#{character_id}:alliance", | ||||
|           {:character_alliance, {character_id, character_update}} | ||||
| @@ -404,8 +405,10 @@ defmodule WandererApp.Character.Tracker do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _update_corporation(%{character_id: character_id} = state, corporation_id) do | ||||
|     case WandererApp.Esi.get_corporation_info(corporation_id) do | ||||
|   defp update_corporation(%{character_id: character_id} = state, corporation_id) do | ||||
|     corporation_id | ||||
|     |> WandererApp.Esi.get_corporation_info() | ||||
|     |> case do | ||||
|       {:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} -> | ||||
|         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) | ||||
|  | ||||
|         Phoenix.PubSub.broadcast( | ||||
|         @pubsub_client.broadcast( | ||||
|           WandererApp.PubSub, | ||||
|           "character:#{character_id}:corporation", | ||||
|           {:character_corporation, | ||||
| @@ -438,7 +441,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|         state | ||||
|         |> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id}) | ||||
|         |> _maybe_update_alliance() | ||||
|         |> maybe_update_alliance() | ||||
|  | ||||
|       _error -> | ||||
|         Logger.warning("Failed to get corporation info for #{corporation_id}") | ||||
| @@ -446,7 +449,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_ship( | ||||
|   defp maybe_update_ship( | ||||
|          %{ | ||||
|            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} = | ||||
|       WandererApp.Character.get_character(character_id) | ||||
|  | ||||
|     case old_ship_type_id != ship_type_id or old_ship_name != ship_name do | ||||
|       true -> | ||||
|         character_update = %{ | ||||
|           ship: ship_type_id, | ||||
|           ship_name: ship_name | ||||
|         } | ||||
|     ship_updated = old_ship_type_id != ship_type_id || old_ship_name != ship_name | ||||
|  | ||||
|         {:ok, _character} = | ||||
|           WandererApp.Api.Character.update_ship(character, character_update) | ||||
|     if ship_updated do | ||||
|       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 | ||||
|  | ||||
|       _ -> | ||||
|         state | ||||
|       WandererApp.Character.update_character(character_id, character_update) | ||||
|     end | ||||
|  | ||||
|     state | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_location( | ||||
|   defp maybe_update_location( | ||||
|          %{ | ||||
|            character_id: character_id | ||||
|          } = | ||||
|            state, | ||||
|          location | ||||
|        ) do | ||||
|     location = _get_location(location) | ||||
|     location = get_location(location) | ||||
|  | ||||
|     if not WandererApp.Cache.lookup!( | ||||
|          "character:#{character_id}:location_started", | ||||
|          false | ||||
|        ) do | ||||
|     if not is_location_started?(character_id) do | ||||
|       WandererApp.Cache.lookup!("character:#{character_id}:start_solar_system_id", nil) | ||||
|       |> case do | ||||
|         nil -> | ||||
| @@ -512,58 +510,51 @@ defmodule WandererApp.Character.Tracker do | ||||
|     {:ok, %{solar_system_id: solar_system_id, structure_id: structure_id} = character} = | ||||
|       WandererApp.Character.get_character(character_id) | ||||
|  | ||||
|     WandererApp.Cache.lookup!( | ||||
|       "character:#{character_id}:location_started", | ||||
|       false | ||||
|     ) | ||||
|     (not is_location_started?(character_id) || | ||||
|        is_location_updated?(location, solar_system_id, structure_id)) | ||||
|     |> case do | ||||
|       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) | ||||
|  | ||||
|         WandererApp.Character.update_character(character_id, location) | ||||
|  | ||||
|         :ok | ||||
|  | ||||
|       _ -> | ||||
|         :ok | ||||
|     end | ||||
|  | ||||
|     state | ||||
|   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, | ||||
|          %{ | ||||
|            "corporation_id" => corporation_id | ||||
|          } = _info | ||||
|        ) do | ||||
|     case corporation_id do | ||||
|       nil -> | ||||
|         state | ||||
|        ) | ||||
|        when not is_nil(corporation_id), | ||||
|        do: update_corporation(state, corporation_id) | ||||
|  | ||||
|       _ -> | ||||
|         _update_corporation(state, corporation_id) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_corporation( | ||||
|   defp maybe_update_corporation( | ||||
|          state, | ||||
|          _info | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_update_alliance( | ||||
|   defp maybe_update_alliance( | ||||
|          %{character_id: character_id, alliance_id: alliance_id} = | ||||
|            state | ||||
|        ) do | ||||
| @@ -582,7 +573,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|  | ||||
|         WandererApp.Character.update_character(character_id, character_update) | ||||
|  | ||||
|         Phoenix.PubSub.broadcast( | ||||
|         @pubsub_client.broadcast( | ||||
|           WandererApp.PubSub, | ||||
|           "character:#{character_id}:alliance", | ||||
|           {:character_alliance, {character_id, character_update}} | ||||
| @@ -591,11 +582,11 @@ defmodule WandererApp.Character.Tracker do | ||||
|         state | ||||
|  | ||||
|       _ -> | ||||
|         _update_alliance(state, alliance_id) | ||||
|         update_alliance(state, alliance_id) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_wallet( | ||||
|   defp maybe_update_wallet( | ||||
|          %{character_id: character_id} = | ||||
|            state, | ||||
|          wallet_balance | ||||
| @@ -611,7 +602,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|       eve_wallet_balance: wallet_balance | ||||
|     }) | ||||
|  | ||||
|     Phoenix.PubSub.broadcast( | ||||
|     @pubsub_client.broadcast( | ||||
|       WandererApp.PubSub, | ||||
|       "character:#{character_id}", | ||||
|       {:character_wallet_balance} | ||||
| @@ -620,7 +611,7 @@ defmodule WandererApp.Character.Tracker do | ||||
|     state | ||||
|   end | ||||
|  | ||||
|   defp _maybe_start_online_tracking( | ||||
|   defp maybe_start_online_tracking( | ||||
|          state, | ||||
|          %{track_online: true} = _track_settings | ||||
|        ), | ||||
| @@ -631,38 +622,37 @@ defmodule WandererApp.Character.Tracker do | ||||
|            track_ship: true | ||||
|        } | ||||
|  | ||||
|   defp _maybe_start_online_tracking( | ||||
|   defp maybe_start_online_tracking( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_start_location_tracking( | ||||
|   defp maybe_start_location_tracking( | ||||
|          state, | ||||
|          %{track_location: true} = _track_settings | ||||
|        ) do | ||||
|     %{state | track_location: true} | ||||
|   end | ||||
|        ), | ||||
|        do: %{state | track_location: true} | ||||
|  | ||||
|   defp _maybe_start_location_tracking( | ||||
|   defp maybe_start_location_tracking( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_start_ship_tracking( | ||||
|   defp maybe_start_ship_tracking( | ||||
|          state, | ||||
|          %{track_ship: true} = _track_settings | ||||
|        ), | ||||
|        do: %{state | track_ship: true} | ||||
|  | ||||
|   defp _maybe_start_ship_tracking( | ||||
|   defp maybe_start_ship_tracking( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_update_active_maps( | ||||
|   defp maybe_update_active_maps( | ||||
|          %{character_id: character_id, active_maps: active_maps} = | ||||
|            state, | ||||
|          %{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()} | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_active_maps( | ||||
|   defp maybe_update_active_maps( | ||||
|          %{character_id: character_id, active_maps: active_maps} = state, | ||||
|          %{map_id: map_id, track: false} = _track_settings | ||||
|        ) 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) -> | ||||
|         duration = DateTime.diff(DateTime.utc_now(), start_time, :second) | ||||
|         :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))} | ||||
|   end | ||||
|  | ||||
|   defp _maybe_update_active_maps( | ||||
|   defp maybe_update_active_maps( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _maybe_stop_tracking( | ||||
|   defp maybe_stop_tracking( | ||||
|          %{active_maps: [], character_id: character_id, opts: opts} = state, | ||||
|          _track_settings | ||||
|        ) do | ||||
| @@ -722,25 +713,21 @@ defmodule WandererApp.Character.Tracker do | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   defp _maybe_stop_tracking( | ||||
|   defp maybe_stop_tracking( | ||||
|          state, | ||||
|          _track_settings | ||||
|        ), | ||||
|        do: state | ||||
|  | ||||
|   defp _get_location(%{"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, "structure_id" => structure_id}), | ||||
|     do: %{solar_system_id: solar_system_id, structure_id: structure_id} | ||||
|  | ||||
|   defp _get_location(%{"solar_system_id" => solar_system_id}) do | ||||
|     %{solar_system_id: solar_system_id, structure_id: nil} | ||||
|   end | ||||
|   defp get_location(%{"solar_system_id" => solar_system_id}), | ||||
|     do: %{solar_system_id: solar_system_id, structure_id: nil} | ||||
|  | ||||
|   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 | ||||
|     %{online: online} | ||||
|   end | ||||
|   defp get_online(%{"online" => online}), do: %{online: online} | ||||
|  | ||||
|   defp _get_online(_), do: %{} | ||||
|   defp get_online(_), do: %{} | ||||
| end | ||||
|   | ||||
| @@ -46,9 +46,7 @@ defmodule WandererApp.Character.TrackerManager do | ||||
|   def handle_call(:error, _, state), do: {:stop, :error, :ok, state} | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call(:stop, _, state) do | ||||
|     {:stop, :normal, :ok, state} | ||||
|   end | ||||
|   def handle_call(:stop, _, state), do: {:stop, :normal, :ok, state} | ||||
|  | ||||
|   @impl true | ||||
|   def handle_call( | ||||
|   | ||||
| @@ -70,12 +70,10 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|       false -> | ||||
|         Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end) | ||||
|  | ||||
|         Task.start_link(fn -> | ||||
|           WandererApp.Character.update_character_state(character_id, %{opts: opts}) | ||||
|           :telemetry.execute([:wanderer_app, :character, :tracker, :started], %{count: 1}) | ||||
|  | ||||
|           :ok | ||||
|         end) | ||||
|         WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [ | ||||
|           character_id, | ||||
|           %{opts: opts} | ||||
|         ]) | ||||
|  | ||||
|         tracked_characters = [character_id | state.characters] |> Enum.uniq() | ||||
|         WandererApp.Cache.insert("tracked_characters", tracked_characters) | ||||
| @@ -180,9 +178,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_online(character_id) | ||||
|       end) | ||||
|       WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [ | ||||
|         character_id | ||||
|       ]) | ||||
|     end) | ||||
|  | ||||
|     state | ||||
| @@ -207,10 +205,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|     Process.send_after(self(), :check_online_errors, @check_online_errors_interval) | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.check_online_errors(character_id) | ||||
|       end) | ||||
|     |> Task.async_stream( | ||||
|       fn character_id -> | ||||
|         WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :check_online_errors, [ | ||||
|           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) | ||||
|  | ||||
|     state | ||||
| @@ -228,9 +235,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_location(character_id) | ||||
|       end) | ||||
|       WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [ | ||||
|         character_id | ||||
|       ]) | ||||
|     end) | ||||
|  | ||||
|     state | ||||
| @@ -257,9 +264,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_ship(character_id) | ||||
|       end) | ||||
|       WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [ | ||||
|         character_id | ||||
|       ]) | ||||
|     end) | ||||
|  | ||||
|     state | ||||
| @@ -285,10 +292,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|     Process.send_after(self(), :update_info, @update_info_interval) | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_info(character_id) | ||||
|       end) | ||||
|     |> Task.async_stream( | ||||
|       fn character_id -> | ||||
|         WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [ | ||||
|           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) | ||||
|  | ||||
|     state | ||||
| @@ -314,10 +330,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|     Process.send_after(self(), :update_wallet, @update_wallet_interval) | ||||
|  | ||||
|     characters | ||||
|     |> Enum.map(fn character_id -> | ||||
|       Task.start_link(fn -> | ||||
|         WandererApp.Character.Tracker.update_wallet(character_id) | ||||
|       end) | ||||
|     |> Task.async_stream( | ||||
|       fn character_id -> | ||||
|         WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [ | ||||
|           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) | ||||
|  | ||||
|     state | ||||
| @@ -358,7 +383,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|             end | ||||
|         end | ||||
|       end, | ||||
|       max_concurrency: 20, | ||||
|       max_concurrency: System.schedulers_online(), | ||||
|       on_timeout: :kill_task, | ||||
|       timeout: :timer.seconds(15) | ||||
|     ) | ||||
| @@ -394,7 +419,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|  | ||||
|         WandererApp.Character.update_character_state(character_id, character_state) | ||||
|       end, | ||||
|       max_concurrency: 20, | ||||
|       max_concurrency: System.schedulers_online(), | ||||
|       on_timeout: :kill_task, | ||||
|       timeout: :timer.seconds(30) | ||||
|     ) | ||||
| @@ -404,7 +429,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do | ||||
|   end | ||||
|  | ||||
|   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) | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -1,7 +0,0 @@ | ||||
| defmodule DDRT.DynamicRtreeImpl.Node do | ||||
|   @moduledoc false | ||||
|  | ||||
|   def new(gen, seed) do | ||||
|     gen[:next].(seed) | ||||
|   end | ||||
| end | ||||
| @@ -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 | ||||
| @@ -274,7 +274,7 @@ defmodule WandererApp.Esi.ApiClient do | ||||
|             ) | ||||
|   def 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} | ||||
|     end | ||||
|   end | ||||
| @@ -286,7 +286,7 @@ defmodule WandererApp.Esi.ApiClient do | ||||
|             ) | ||||
|   def 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} | ||||
|     end | ||||
|   end | ||||
| @@ -301,7 +301,7 @@ defmodule WandererApp.Esi.ApiClient do | ||||
|            "/characters/#{eve_id}/", | ||||
|            opts |> _with_cache_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} | ||||
|     end | ||||
|   end | ||||
|   | ||||
| @@ -67,8 +67,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_wormhole_types() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholes.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholes.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         id: row["typeID"], | ||||
|         name: row["name"], | ||||
| @@ -85,8 +85,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_wormhole_classes() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeClasses.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeClasses.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         id: row["id"], | ||||
|         short_name: row["shortName"], | ||||
| @@ -98,8 +98,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_wormhole_systems() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeSystems.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/wormholeSystems.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         solar_system_id: row["solarSystemID"], | ||||
|         wanderers: row["wanderers"], | ||||
| @@ -111,8 +111,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_effects() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/effects.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/effects.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         id: row["name"] |> Slug.slugify(), | ||||
|         name: row["name"], | ||||
| @@ -130,8 +130,8 @@ defmodule WandererApp.EveDataService do | ||||
|   end | ||||
|  | ||||
|   def load_triglavian_systems() do | ||||
|     JSONUtil.read_json("#{:code.priv_dir(:wanderer_app)}/repo/data/triglavianSystems.json") | ||||
|     |> JSONUtil.map_json(fn row -> | ||||
|     JSONUtil.read_json!("#{:code.priv_dir(:wanderer_app)}/repo/data/triglavianSystems.json") | ||||
|     |> Enum.map(fn row -> | ||||
|       %{ | ||||
|         solar_system_id: row["solarSystemID"], | ||||
|         solar_system_name: row["solarSystemName"], | ||||
| @@ -377,9 +377,21 @@ defmodule WandererApp.EveDataService do | ||||
|     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 | ||||
|     case wormhole_class_id in [ | ||||
|   | ||||
| @@ -52,6 +52,15 @@ defmodule WandererApp.Map do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def get_map_options!(map) do | ||||
|     map | ||||
|     |> Map.get(:options) | ||||
|     |> case do | ||||
|       nil -> %{"layout" => "left_to_right"} | ||||
|       options -> Jason.decode!(options) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def update_map(map_id, map_update) do | ||||
|     Cachex.get_and_update(:map_cache, map_id, fn map -> | ||||
|       case map do | ||||
|   | ||||
| @@ -19,46 +19,47 @@ defmodule WandererApp.Map.PositionCalculator do | ||||
|  | ||||
|   def get_system_bounding_rect(_system), do: [{0, 0}, {0, 0}] | ||||
|  | ||||
|   def get_new_system_position(nil, rtree_name) do | ||||
|     {:ok, {x, y}} = rtree_name |> _check_system_available_positions(@start_x, @start_y, 1) | ||||
|   def get_new_system_position(nil, rtree_name, opts) do | ||||
|     {:ok, {x, y}} = rtree_name |> check_system_available_positions(@start_x, @start_y, 1, opts) | ||||
|     %{x: x, y: y} | ||||
|   end | ||||
|  | ||||
|   def get_new_system_position( | ||||
|         %{position_x: start_x, position_y: start_y} = _old_system, | ||||
|         rtree_name | ||||
|         rtree_name, | ||||
|         opts | ||||
|       ) do | ||||
|     {:ok, {x, y}} = rtree_name |> _check_system_available_positions(start_x, start_y, 1) | ||||
|     {:ok, {x, y}} = rtree_name |> check_system_available_positions(start_x, start_y, 1, opts) | ||||
|  | ||||
|     %{x: x, y: y} | ||||
|   end | ||||
|  | ||||
|   defp _check_system_available_positions(_rtree_name, _start_x, _start_y, 100) do | ||||
|     {:ok, {@start_x, @start_y}} | ||||
|   end | ||||
|   defp check_system_available_positions(_rtree_name, _start_x, _start_y, 100, _opts), | ||||
|     do: {:ok, {@start_x, @start_y}} | ||||
|  | ||||
|   defp _check_system_available_positions(rtree_name, start_x, start_y, level) do | ||||
|     possible_positions = _get_available_positions(level, start_x, start_y) | ||||
|   defp check_system_available_positions(rtree_name, start_x, start_y, level, opts) do | ||||
|     possible_positions = get_available_positions(level, start_x, start_y, opts) | ||||
|  | ||||
|     case _get_available_position(possible_positions, rtree_name) do | ||||
|     case get_available_position(possible_positions, rtree_name) do | ||||
|       {:ok, nil} -> | ||||
|         rtree_name |> _check_system_available_positions(start_x, start_y, level + 1) | ||||
|         rtree_name |> check_system_available_positions(start_x, start_y, level + 1, opts) | ||||
|  | ||||
|       {:ok, position} -> | ||||
|         {:ok, position} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _get_available_position([], _rtree_name), do: {:ok, nil} | ||||
|   defp get_available_position([], _rtree_name), do: {:ok, nil} | ||||
|  | ||||
|   defp _get_available_position([position | rest], rtree_name) do | ||||
|     if _is_available_position(position, rtree_name) do | ||||
|   defp get_available_position([position | rest], rtree_name) do | ||||
|     if is_available_position(position, rtree_name) do | ||||
|       {:ok, position} | ||||
|     else | ||||
|       _get_available_position(rest, rtree_name) | ||||
|       get_available_position(rest, rtree_name) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp _is_available_position({x, y} = _position, rtree_name) do | ||||
|   defp is_available_position({x, y} = _position, rtree_name) do | ||||
|     case DDRT.query(get_system_bounding_rect(%{position_x: x, position_y: y}), rtree_name) do | ||||
|       {:ok, []} -> | ||||
|         true | ||||
| @@ -71,9 +72,10 @@ defmodule WandererApp.Map.PositionCalculator do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def _get_available_positions(level, x, y), do: _adjusted_coordinates(1 + level * 2, x, y) | ||||
|   def get_available_positions(level, x, y, opts), | ||||
|     do: adjusted_coordinates(1 + level * 2, x, y, opts) | ||||
|  | ||||
|   defp _edge_coordinates(n) when n > 1 do | ||||
|   defp edge_coordinates(n, opts) when n > 1 do | ||||
|     min = -div(n, 2) | ||||
|     max = div(n, 2) | ||||
|     # Top edge | ||||
| @@ -90,16 +92,20 @@ defmodule WandererApp.Map.PositionCalculator do | ||||
|     |> Enum.uniq() | ||||
|   end | ||||
|  | ||||
|   defp _sorted_edge_coordinates(n) when n > 1 do | ||||
|     coordinates = _edge_coordinates(n) | ||||
|     middle_right_index = div(n, 2) | ||||
|   defp sorted_edge_coordinates(n, opts) when n > 1 do | ||||
|     coordinates = edge_coordinates(n, opts) | ||||
|     start_index = get_start_index(n, opts[:layout]) | ||||
|  | ||||
|     Enum.slice(coordinates, middle_right_index, length(coordinates) - middle_right_index) ++ | ||||
|       Enum.slice(coordinates, 0, middle_right_index) | ||||
|     Enum.slice(coordinates, start_index, length(coordinates) - start_index) ++ | ||||
|       Enum.slice(coordinates, 0, start_index) | ||||
|   end | ||||
|  | ||||
|   defp _adjusted_coordinates(n, start_x, start_y) when n > 1 do | ||||
|     sorted_coords = _sorted_edge_coordinates(n) | ||||
|   defp get_start_index(n, "left_to_right"), do: div(n, 2) | ||||
|  | ||||
|   defp get_start_index(n, "top_to_bottom"), do: div(n, 2) + n - 1 | ||||
|  | ||||
|   defp adjusted_coordinates(n, start_x, start_y, opts) when n > 1 do | ||||
|     sorted_coords = sorted_edge_coordinates(n, opts) | ||||
|  | ||||
|     Enum.map(sorted_coords, fn {x, y} -> | ||||
|       { | ||||
|   | ||||
| @@ -11,7 +11,8 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|   defstruct [ | ||||
|     :map_id, | ||||
|     :rtree_name, | ||||
|     map: nil | ||||
|     map: nil, | ||||
|     map_opts: [] | ||||
|   ] | ||||
|  | ||||
|   # @ccp1 -1 | ||||
| @@ -375,17 +376,21 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|  | ||||
|     case not is_nil(user_id) do | ||||
|       true -> | ||||
|         :telemetry.execute( | ||||
|           [:wanderer_app, :map, :systems, :remove], | ||||
|           %{count: removed_ids |> Enum.count()}, | ||||
|           %{ | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_map_event(:systems_removed, %{ | ||||
|             character_id: character_id, | ||||
|             user_id: user_id, | ||||
|             map_id: map_id, | ||||
|             solar_system_ids: removed_ids | ||||
|           } | ||||
|           }) | ||||
|  | ||||
|         :telemetry.execute( | ||||
|           [:wanderer_app, :map, :systems, :remove], | ||||
|           %{count: removed_ids |> Enum.count()} | ||||
|         ) | ||||
|  | ||||
|         :ok | ||||
|  | ||||
|       _ -> | ||||
|         :ok | ||||
|     end | ||||
| @@ -795,6 +800,9 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   def handle_event({:options_updated, options}, %{map: map, map_id: map_id} = state), | ||||
|     do: %{state | map_opts: [layout: options.layout]} | ||||
|  | ||||
|   def handle_event({ref, _result}, %{map_id: _map_id} = state) do | ||||
|     Process.demonitor(ref, [:flush]) | ||||
|  | ||||
| @@ -834,12 +842,12 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|          character_id, | ||||
|          location, | ||||
|          old_location, | ||||
|          %{map: map, map_id: map_id, rtree_name: rtree_name} = _state | ||||
|          %{map: map, map_id: map_id, rtree_name: rtree_name, map_opts: map_opts} = _state | ||||
|        ) do | ||||
|     case is_nil(old_location.solar_system_id) and | ||||
|            _can_add_location(map.scope, location.solar_system_id) do | ||||
|       true -> | ||||
|         :ok = maybe_add_system(map_id, location, nil, rtree_name) | ||||
|         :ok = maybe_add_system(map_id, location, nil, rtree_name, map_opts) | ||||
|  | ||||
|       _ -> | ||||
|         case _is_connection_valid( | ||||
| @@ -848,10 +856,9 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|                location.solar_system_id | ||||
|              ) do | ||||
|           true -> | ||||
|             {:ok, character} = WandererApp.Character.get_character(character_id) | ||||
|             :ok = maybe_add_system(map_id, location, old_location, rtree_name) | ||||
|             :ok = maybe_add_system(map_id, old_location, location, rtree_name) | ||||
|             :ok = maybe_add_connection(map_id, location, old_location, character) | ||||
|             :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_connection(map_id, location, old_location, character_id) | ||||
|  | ||||
|           _ -> | ||||
|             :ok | ||||
| @@ -1097,7 +1104,7 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|        end)} | ||||
|  | ||||
|   defp _add_system( | ||||
|          %{map_id: map_id, rtree_name: rtree_name} = state, | ||||
|          %{map_id: map_id, map_opts: map_opts, rtree_name: rtree_name} = state, | ||||
|          %{ | ||||
|            solar_system_id: solar_system_id, | ||||
|            coordinates: coordinates | ||||
| @@ -1113,7 +1120,7 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|  | ||||
|         _ -> | ||||
|           %{x: x, y: y} = | ||||
|             WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name) | ||||
|             WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts) | ||||
|  | ||||
|           %{"x" => x, "y" => y} | ||||
|       end | ||||
| @@ -1165,12 +1172,15 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|  | ||||
|     broadcast!(map_id, :add_system, system) | ||||
|  | ||||
|     :telemetry.execute([:wanderer_app, :map, :system, :add], %{count: 1}, %{ | ||||
|       character_id: character_id, | ||||
|       user_id: user_id, | ||||
|       map_id: map_id, | ||||
|       solar_system_id: solar_system_id | ||||
|     }) | ||||
|     {:ok, _} = | ||||
|       WandererApp.User.ActivityTracker.track_map_event(:system_added, %{ | ||||
|         character_id: character_id, | ||||
|         user_id: user_id, | ||||
|         map_id: map_id, | ||||
|         solar_system_id: solar_system_id | ||||
|       }) | ||||
|  | ||||
|     :telemetry.execute([:wanderer_app, :map, :system, :add], %{count: 1}) | ||||
|  | ||||
|     state | ||||
|   end | ||||
| @@ -1255,20 +1265,22 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|  | ||||
|   defp _init_map( | ||||
|          state, | ||||
|          %{characters: characters} = map, | ||||
|          %{characters: characters} = initial_map, | ||||
|          subscription_settings, | ||||
|          systems, | ||||
|          connections | ||||
|        ) do | ||||
|     map = | ||||
|       map | ||||
|       initial_map | ||||
|       |> WandererApp.Map.new() | ||||
|       |> WandererApp.Map.update_subscription_settings!(subscription_settings) | ||||
|       |> WandererApp.Map.add_systems!(systems) | ||||
|       |> WandererApp.Map.add_connections!(connections) | ||||
|       |> WandererApp.Map.add_characters!(characters) | ||||
|  | ||||
|     %{state | map: map} | ||||
|     map_options = WandererApp.Map.get_map_options!(initial_map) | ||||
|  | ||||
|     %{state | map: map, map_opts: [layout: map_options |> Map.get("layout")]} | ||||
|   end | ||||
|  | ||||
|   defp _init_map_systems(state, [] = _systems), do: state | ||||
| @@ -1577,20 +1589,32 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|  | ||||
|   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 | ||||
|               not is_nil(old_location.solar_system_id) and | ||||
|               location.solar_system_id != old_location.solar_system_id do | ||||
|     case character do | ||||
|     character_id | ||||
|     |> WandererApp.Character.get_character!() | ||||
|     |> case do | ||||
|       nil -> | ||||
|         :ok | ||||
|  | ||||
|       _ -> | ||||
|         :telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{ | ||||
|           map_id: map_id, | ||||
|           character: character, | ||||
|           solar_system_source_id: old_location.solar_system_id, | ||||
|           solar_system_target_id: location.solar_system_id | ||||
|       character -> | ||||
|         :telemetry.execute([:wanderer_app, :map, :character, :jump], %{count: 1}, %{}) | ||||
|  | ||||
|         {: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: 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 | ||||
|  | ||||
| @@ -1603,22 +1627,30 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|             solar_system_target: location.solar_system_id | ||||
|           }) | ||||
|  | ||||
|         broadcast!(map_id, :add_connection, connection) | ||||
|         WandererApp.Map.add_connection(map_id, connection) | ||||
|  | ||||
|         broadcast!(map_id, :add_connection, connection) | ||||
|         broadcast!(map_id, :maybe_link_signature, %{ | ||||
|           character_id: character_id, | ||||
|           solar_system_source: old_location.solar_system_id, | ||||
|           solar_system_target: location.solar_system_id, | ||||
|         }) | ||||
|  | ||||
|         :ok | ||||
|  | ||||
|       {:error, error} -> | ||||
|         @logger.debug(fn -> "Failed to add connection: #{inspect(error, pretty: true)}" end) | ||||
|         :ok | ||||
|     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) | ||||
|   defp maybe_add_system(map_id, location, old_location, rtree_name, opts) | ||||
|        when not is_nil(location) do | ||||
|     case WandererApp.Map.check_location(map_id, location) do | ||||
|       {:ok, location} -> | ||||
|         {:ok, position} = calc_new_system_position(map_id, old_location, rtree_name) | ||||
|         {:ok, position} = calc_new_system_position(map_id, old_location, rtree_name, opts) | ||||
|  | ||||
|         case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id( | ||||
|                map_id, | ||||
| @@ -1688,14 +1720,14 @@ defmodule WandererApp.Map.Server.Impl do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp maybe_add_system(_map_id, _location, _old_location, _rtree_name), do: :ok | ||||
|   defp maybe_add_system(_map_id, _location, _old_location, _rtree_name, _opts), do: :ok | ||||
|  | ||||
|   defp calc_new_system_position(map_id, old_location, rtree_name) do | ||||
|     {:ok, | ||||
|      map_id | ||||
|      |> WandererApp.Map.find_system_by_location(old_location) | ||||
|      |> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name)} | ||||
|   end | ||||
|   defp calc_new_system_position(map_id, old_location, rtree_name, opts), | ||||
|     do: | ||||
|       {:ok, | ||||
|        map_id | ||||
|        |> WandererApp.Map.find_system_by_location(old_location) | ||||
|        |> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name, opts)} | ||||
|  | ||||
|   defp _broadcast_acl_updates( | ||||
|          {:ok, | ||||
|   | ||||
| @@ -55,13 +55,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do | ||||
|   def handle_info({ref, result}, state) do | ||||
|     Process.demonitor(ref, [:flush]) | ||||
|  | ||||
|     case result do | ||||
|       :ok -> | ||||
|         {:noreply, state} | ||||
|  | ||||
|       _ -> | ||||
|         {:noreply, state} | ||||
|     end | ||||
|     {:noreply, state} | ||||
|   end | ||||
|  | ||||
|   defp _update_map_kills(map_id) do | ||||
| @@ -70,10 +64,9 @@ defmodule WandererApp.Map.ZkbDataFetcher do | ||||
|         map_id | ||||
|         |> WandererApp.Map.get_map!() | ||||
|         |> Map.get(:systems, Map.new()) | ||||
|         |> Map.keys() | ||||
|         |> Enum.reduce(Map.new(), fn solar_system_id, acc -> | ||||
|         |> Enum.reduce(Map.new(), fn {solar_system_id, _system}, acc -> | ||||
|           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) | ||||
|         |> _maybe_broadcast_map_kills(map_id) | ||||
|  | ||||
| @@ -87,28 +80,24 @@ defmodule WandererApp.Map.ZkbDataFetcher do | ||||
|  | ||||
|     updated_kills_system_ids = | ||||
|       new_kills_map | ||||
|       |> Map.keys() | ||||
|       |> 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) | ||||
|       |> Map.filter(fn {solar_system_id, new_kills_count} -> | ||||
|         old_kills_count = old_kills_map |> Map.get(solar_system_id, 0) | ||||
|  | ||||
|         new_kills_count != old_kills_count and | ||||
|           old_kills_count > 0 and new_kills_count == 0 | ||||
|           new_kills_count > 0 | ||||
|       end) | ||||
|       |> Map.keys() | ||||
|  | ||||
|     [updated_kills_system_ids | removed_kills_system_ids] | ||||
|     |> List.flatten() | ||||
|     removed_kills_system_ids = | ||||
|       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 | ||||
|       [] -> | ||||
|         :ok | ||||
|   | ||||
| @@ -9,7 +9,11 @@ defmodule WandererApp.MapConnectionRepo do | ||||
|     do: WandererApp.Api.MapConnection.read_by_map(%{map_id: map_id}) | ||||
|  | ||||
|   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 | ||||
|       {:ok, connections} -> | ||||
|         {:ok, connections} | ||||
| @@ -26,8 +30,11 @@ defmodule WandererApp.MapConnectionRepo do | ||||
|   def create!(connection), do: connection |> WandererApp.Api.MapConnection.create!() | ||||
|  | ||||
|   def destroy(map_id, connection) do | ||||
|     {:ok, from_connections} = 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) | ||||
|     {:ok, from_connections} = | ||||
|       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] | ||||
|     |> List.flatten() | ||||
| @@ -42,8 +49,7 @@ defmodule WandererApp.MapConnectionRepo do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def destroy!(connection), do: | ||||
|     connection |> WandererApp.Api.MapConnection.destroy!() | ||||
|   def destroy!(connection), do: connection |> WandererApp.Api.MapConnection.destroy!() | ||||
|  | ||||
|   def bulk_destroy!(connections) do | ||||
|     connections | ||||
| @@ -51,6 +57,7 @@ defmodule WandererApp.MapConnectionRepo do | ||||
|     |> case do | ||||
|       %Ash.BulkResult{status: :success} -> | ||||
|         :ok | ||||
|  | ||||
|       error -> | ||||
|         error | ||||
|     end | ||||
|   | ||||
							
								
								
									
										49
									
								
								lib/wanderer_app/repositories/map_user_settings_repo.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/wanderer_app/repositories/map_user_settings_repo.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| defmodule WandererApp.MapUserSettingsRepo do | ||||
|   use WandererApp, :repository | ||||
|  | ||||
|   @default_form_data %{"select_on_spash" => "false", "link_signature_on_splash" => "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 | ||||
							
								
								
									
										9
									
								
								lib/wanderer_app/task_wrapper.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/wanderer_app/task_wrapper.ex
									
									
									
									
									
										Normal 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 | ||||
| @@ -1,114 +1,16 @@ | ||||
| defmodule WandererApp.User.ActivityTracker do | ||||
|   @moduledoc false | ||||
|   use GenServer | ||||
|  | ||||
|   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 | ||||
|     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_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 | ||||
|   def track_acl_event( | ||||
|         event_type, | ||||
|         metadata | ||||
|       ), | ||||
|       do: WandererApp.Map.Audit.track_acl_event(event_type, metadata) | ||||
| end | ||||
|   | ||||
| @@ -6,14 +6,9 @@ defmodule WandererApp.Utils.JSONUtil do | ||||
|     Jason.decode(body) | ||||
|   end | ||||
|  | ||||
|   def map_json({:ok, json}, mapper) do | ||||
|     Enum.map(json, mapper) | ||||
|   end | ||||
|  | ||||
|   def compress(data) do | ||||
|     data | ||||
|     |> Jason.encode!() | ||||
|     |> :zlib.compress() | ||||
|     |> Base.encode64() | ||||
|   end | ||||
|   def read_json!(filename), | ||||
|     do: | ||||
|       filename | ||||
|       |> File.read!() | ||||
|       |> Jason.decode!() | ||||
| end | ||||
|   | ||||
| @@ -63,7 +63,7 @@ defmodule WandererApp.Zkb.KillsProvider do | ||||
|   end | ||||
|  | ||||
|   defp handle_websocket(message, state) do | ||||
|     case message |> _parse_message() do | ||||
|     case message |> parse_message() do | ||||
|       nil -> | ||||
|         {:ok, state} | ||||
|  | ||||
| @@ -109,7 +109,7 @@ defmodule WandererApp.Zkb.KillsProvider do | ||||
|     Logger.warning(fn -> "Terminating client process with reason : #{inspect(reason)}" end) | ||||
|   end | ||||
|  | ||||
|   defp _parse_message( | ||||
|   defp parse_message( | ||||
|          %{ | ||||
|            "solar_system_id" => solar_system_id, | ||||
|            "killmail_time" => killmail_time | ||||
| @@ -123,5 +123,5 @@ defmodule WandererApp.Zkb.KillsProvider do | ||||
|     } | ||||
|   end | ||||
|  | ||||
|   defp _parse_message(_message), do: nil | ||||
|   defp parse_message(_message), do: nil | ||||
| end | ||||
|   | ||||
| @@ -377,7 +377,7 @@ defmodule WandererAppWeb.CoreComponents do | ||||
|  | ||||
|     ~H""" | ||||
|     <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> | ||||
|         <input type="hidden" name={@name} value="false" /> | ||||
|         <input | ||||
| @@ -605,9 +605,7 @@ defmodule WandererAppWeb.CoreComponents do | ||||
|             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: ""}"} | ||||
|           > | ||||
|             <td | ||||
|               :for={{col, _index} <- Enum.with_index(@col)} | ||||
|             > | ||||
|             <td :for={{col, _index} <- Enum.with_index(@col)}> | ||||
|               <%= render_slot(col, @row_item.(row)) %> | ||||
|             </td> | ||||
|             <td :if={@action != []}> | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| <main class="bg-gradient-to-r from-stone-950 to-stone-900"> | ||||
| <main class="bg-stone-950"> | ||||
|   <%= @inner_content %> | ||||
| </main> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <main | ||||
|   class="main flex-col !min-h-screen justify-between flex z-0 focus:outline-none transition-all duration-500 opacity-0 phx-page-loading:opacity-0 bg-gradient-to-r from-stone-950 to-stone-900 ccp-font" | ||||
|   class="main flex-col !min-h-screen justify-between flex z-0 focus:outline-none transition-all duration-500 opacity-0 phx-page-loading:opacity-0 bg-stone-950 ccp-font" | ||||
|   phx-mounted={JS.remove_class("opacity-0")} | ||||
| > | ||||
|   <navbar class="navbar bg-base-100 !sticky top-0 z-50 bg-opacity-0 "> | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|   </.connection_status> | ||||
|  | ||||
|   <main | ||||
|     class="main flex-1 relative z-0 overflow-hidden focus:outline-none transition-all duration-500 opacity-0 phx-page-loading:opacity-0 bg-gradient-to-r from-stone-950 to-stone-900 maps_bg ccp-font" | ||||
|     class="main flex-1 relative z-0 overflow-hidden focus:outline-none transition-all duration-500 opacity-0 phx-page-loading:opacity-0 bg-stone-950 maps_bg ccp-font" | ||||
|     phx-mounted={JS.remove_class("opacity-0")} | ||||
|   > | ||||
|     <%= @inner_content %> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <div class="flex flex-col w-0 flex-1 overflow-hidden bg-gradient-to-r from-stone-950 to-stone-900"> | ||||
| <div class="flex flex-col w-0 flex-1 overflow-hidden bg-stone-950"> | ||||
|   <.connection_status> | ||||
|     Re-establishing connection... | ||||
|   </.connection_status> | ||||
|   | ||||
| @@ -43,6 +43,20 @@ | ||||
|     > | ||||
|     </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> | ||||
|     <!-- Appzi: Capture Insightful Feedback --> | ||||
|   | ||||
| @@ -3,11 +3,15 @@ defmodule WandererAppWeb.BasicAuth do | ||||
|  | ||||
|   def admin_basic_auth(conn, _opts) do | ||||
|     admin_password = WandererApp.Env.admin_password() | ||||
|  | ||||
|     if is_nil(admin_password) do | ||||
|       conn | ||||
|     else | ||||
|       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 | ||||
|   | ||||
| @@ -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"> | ||||
|   <canvas id="bg-canvas"></canvas> | ||||
|   <div class="h-full w-full flex flex-col items-center"> | ||||
|     <!--Main--> | ||||
|     <div class="artboard artboard-horizontal phone-3 pt-10 !h-40"> | ||||
| @@ -11,9 +12,17 @@ | ||||
|         </div> | ||||
|         <!--Right Col--> | ||||
|         <div :if={@invite_token_valid} class="overflow-hidden"> | ||||
|           <.link navigate={~p"/auth/eve?invite=#{@invite_token}"}> | ||||
|             <img src="https://web.ccpgamescdn.com/eveonlineassets/developers/eve-sso-login-black-large.png" /> | ||||
|           </.link> | ||||
|           <div class="!z-100 relative group alert items-center fade-in-scale text-white w-[224px] h-[44px] rounded p-px overflow-hidden"> | ||||
|             <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]" /> | ||||
|             <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> | ||||
|   | ||||
| @@ -99,8 +99,9 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|   end | ||||
|  | ||||
|   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), | ||||
|      user_character_ids <- socket.assigns.current_user.characters |> Enum.map(& &1.id) do | ||||
|     with {:ok, %{owner: %{id: _character_id}} = access_list} <- | ||||
|            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 | ||||
|       |> Enum.each(fn user_character_id -> | ||||
|         :ok = WandererApp.Character.TrackerManager.start_tracking(user_character_id) | ||||
| @@ -125,7 +126,7 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|       ) | ||||
|     else | ||||
|       _ -> | ||||
|       socket | ||||
|         socket | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -318,11 +319,13 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info({:search, text}, socket) do | ||||
|     first_character_id = | ||||
|       socket.assigns.user_character_ids | ||||
|     active_character_id = | ||||
|       socket.assigns.current_user.characters | ||||
|       |> Enum.filter(fn character -> not is_nil(character.refresh_token) end) | ||||
|       |> Enum.map(& &1.id) | ||||
|       |> 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) | ||||
|     {:noreply, socket |> assign(member_search_options: options)} | ||||
| @@ -373,12 +376,16 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|           member | ||||
|           |> WandererApp.Api.AccessListMember.update_role!(%{role: role_atom}) | ||||
|  | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :update], %{count: 1}, %{ | ||||
|           user_id: socket.assigns.current_user.id, | ||||
|           acl_id: socket.assigns.selected_acl_id, | ||||
|           member: | ||||
|             member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|         }) | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_updated, %{ | ||||
|             user_id: socket.assigns.current_user.id, | ||||
|             acl_id: socket.assigns.selected_acl_id, | ||||
|             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( | ||||
|           WandererApp.PubSub, | ||||
| @@ -488,12 +495,16 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|            eve_corporation_id: nil | ||||
|          }) do | ||||
|       {:ok, member} -> | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}, %{ | ||||
|           user_id: socket.assigns.current_user.id, | ||||
|           acl_id: access_list_id, | ||||
|           member: | ||||
|             member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|         }) | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_added, %{ | ||||
|             user_id: socket.assigns.current_user.id, | ||||
|             acl_id: access_list_id, | ||||
|             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} | ||||
|  | ||||
| @@ -515,12 +526,16 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|            eve_corporation_id: eve_id | ||||
|          }) do | ||||
|       {:ok, member} -> | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}, %{ | ||||
|           user_id: socket.assigns.current_user.id, | ||||
|           acl_id: access_list_id, | ||||
|           member: | ||||
|             member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|         }) | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_added, %{ | ||||
|             user_id: socket.assigns.current_user.id, | ||||
|             acl_id: access_list_id, | ||||
|             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} | ||||
|  | ||||
| @@ -543,12 +558,16 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|            role: :viewer | ||||
|          }) do | ||||
|       {:ok, member} -> | ||||
|         :telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1}, %{ | ||||
|           user_id: socket.assigns.current_user.id, | ||||
|           acl_id: access_list_id, | ||||
|           member: | ||||
|             member |> Map.take([:eve_character_id, :eve_corporation_id, :eve_alliance_id, :role]) | ||||
|         }) | ||||
|         {:ok, _} = | ||||
|           WandererApp.User.ActivityTracker.track_acl_event(:map_acl_member_added, %{ | ||||
|             user_id: socket.assigns.current_user.id, | ||||
|             acl_id: access_list_id, | ||||
|             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} | ||||
|  | ||||
| @@ -647,6 +666,6 @@ defmodule WandererAppWeb.AccessListsLive do | ||||
|   end | ||||
|  | ||||
|   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 | ||||
|   | ||||
| @@ -44,6 +44,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do | ||||
|       case WandererApp.Api.MapCharacterSettings.read_by_map(%{map_id: selected_map.id}) do | ||||
|         {:ok, settings} -> | ||||
|           {:ok, settings} | ||||
|  | ||||
|         _ -> | ||||
|           {:ok, []} | ||||
|       end | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -13,8 +13,6 @@ | ||||
|       <span class="Loader__Circle"></span> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|  | ||||
| </div> | ||||
|  | ||||
| <div class="w-full h-full" id="mapper" phx-hook="Mapper" phx-update="ignore"></div> | ||||
| @@ -151,3 +149,25 @@ | ||||
|     </.table> | ||||
|   </.async_result> | ||||
| </.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" /> | ||||
|     <.input | ||||
|       type="checkbox" | ||||
|       field={f[:link_signature_on_splash]} | ||||
|       label="Link splashed systems to signatures" | ||||
|     /> | ||||
|   </.form> | ||||
| </.modal> | ||||
|   | ||||
| @@ -5,6 +5,8 @@ defmodule WandererAppWeb.MapsLive do | ||||
|  | ||||
|   alias BetterNumber, as: Number | ||||
|  | ||||
|   @pubsub_client Application.compile_env(:wanderer_app, :pubsub_client) | ||||
|  | ||||
|   @impl true | ||||
|   def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do | ||||
|     {:ok, active_characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id}) | ||||
| @@ -112,6 +114,13 @@ defmodule WandererAppWeb.MapsLive do | ||||
|       "auto_renew?" => true | ||||
|     } | ||||
|  | ||||
|     options_form = | ||||
|       map.options | ||||
|       |> case do | ||||
|         nil -> %{"layout" => "left_to_right"} | ||||
|         options -> Jason.decode!(options) | ||||
|       end | ||||
|  | ||||
|     {:ok, estimated_price, discount} = | ||||
|       WandererApp.Map.SubscriptionManager.estimate_price(subscription_form, false) | ||||
|  | ||||
| @@ -130,6 +139,7 @@ defmodule WandererAppWeb.MapsLive do | ||||
|       active_settings_tab: "general", | ||||
|       is_adding_subscription?: false, | ||||
|       selected_subscription: nil, | ||||
|       options_form: options_form |> to_form(), | ||||
|       map_subscriptions: map_subscriptions, | ||||
|       subscription_form: subscription_form |> to_form(), | ||||
|       estimated_price: estimated_price, | ||||
| @@ -142,6 +152,10 @@ defmodule WandererAppWeb.MapsLive do | ||||
|         {"3 Months", "3"}, | ||||
|         {"6 Months", "6"}, | ||||
|         {"1 Year", "12"} | ||||
|       ], | ||||
|       layout_options: [ | ||||
|         {"Left To Right", "left_to_right"}, | ||||
|         {"Top To Bottom", "top_to_bottom"} | ||||
|       ] | ||||
|     ) | ||||
|     |> allow_upload(:settings, | ||||
| @@ -594,20 +608,12 @@ defmodule WandererAppWeb.MapsLive do | ||||
|  | ||||
|         added_acls | ||||
|         |> Enum.each(fn acl_id -> | ||||
|           :telemetry.execute([:wanderer_app, :map, :acl, :add], %{count: 1}, %{ | ||||
|             user_id: current_user.id, | ||||
|             map_id: map.id, | ||||
|             acl_id: acl_id | ||||
|           }) | ||||
|           :telemetry.execute([:wanderer_app, :map, :acl, :add], %{count: 1}) | ||||
|         end) | ||||
|  | ||||
|         removed_acls | ||||
|         |> Enum.each(fn acl_id -> | ||||
|           :telemetry.execute([:wanderer_app, :map, :acl, :remove], %{count: 1}, %{ | ||||
|             user_id: current_user.id, | ||||
|             map_id: map.id, | ||||
|             acl_id: acl_id | ||||
|           }) | ||||
|           :telemetry.execute([:wanderer_app, :map, :acl, :remove], %{count: 1}) | ||||
|         end) | ||||
|  | ||||
|         Phoenix.PubSub.broadcast( | ||||
| @@ -653,6 +659,28 @@ defmodule WandererAppWeb.MapsLive do | ||||
|      |> push_patch(to: ~p"/maps")} | ||||
|   end | ||||
|  | ||||
|   def handle_event( | ||||
|         "update_options", | ||||
|         %{ | ||||
|           "layout" => layout | ||||
|         } = options_form, | ||||
|         %{assigns: %{map_id: map_id, map: map}} = socket | ||||
|       ) do | ||||
|     options = %{layout: layout} | ||||
|  | ||||
|     updated_map = | ||||
|       map | ||||
|       |> WandererApp.Api.Map.update_options!(%{options: Jason.encode!(options)}) | ||||
|  | ||||
|     @pubsub_client.broadcast( | ||||
|       WandererApp.PubSub, | ||||
|       "maps:#{map_id}", | ||||
|       {:options_updated, options} | ||||
|     ) | ||||
|  | ||||
|     {:noreply, socket |> assign(map: updated_map, options_form: options_form)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("noop", _, socket) do | ||||
|     {:noreply, socket} | ||||
| @@ -898,6 +926,6 @@ defmodule WandererAppWeb.MapsLive do | ||||
|  | ||||
|   defp map_map(%{acls: acls} = map) do | ||||
|     map | ||||
|     |> Map.merge(%{acls: acls |> Enum.map(&map_acl/1)}) | ||||
|     |> Map.put(:acls, acls |> Enum.map(&map_acl/1)) | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -26,7 +26,6 @@ | ||||
|           > | ||||
|             <figure class="absolute z-10 h-200 avatar w-full h-full"> | ||||
|               <img :if={map.scope === :all} class="absolute h-200" src="/images/all_back.webp" /> | ||||
|  | ||||
|               <img | ||||
|                 :if={map.scope === :wormholes} | ||||
|                 class="absolute h-200" | ||||
| @@ -190,7 +189,6 @@ | ||||
| > | ||||
|   <div role="tablist" class="tabs tabs-bordered"> | ||||
|     <a | ||||
|       :if={@map_subscriptions_enabled?} | ||||
|       role="tab" | ||||
|       phx-click="change_settings_tab" | ||||
|       phx-value-tab="general" | ||||
| @@ -201,6 +199,17 @@ | ||||
|     > | ||||
|       <.icon name="hero-wrench-screwdriver-solid" class="w-4 h-4" /> General | ||||
|     </a> | ||||
|     <a | ||||
|       role="tab" | ||||
|       phx-click="change_settings_tab" | ||||
|       phx-value-tab="import" | ||||
|       class={[ | ||||
|         "tab", | ||||
|         classes("tab-active": @active_settings_tab == "import") | ||||
|       ]} | ||||
|     > | ||||
|       <.icon name="hero-document-arrow-down-solid" class="w-4 h-4" /> Import/Export | ||||
|     </a> | ||||
|     <a | ||||
|       :if={@map_subscriptions_enabled?} | ||||
|       role="tab" | ||||
| @@ -227,6 +236,27 @@ | ||||
|     </a> | ||||
|   </div> | ||||
|   <.header :if={@active_settings_tab == "general"} class="bordered border-1 border-zinc-800"> | ||||
|     <:actions> | ||||
|       <.form | ||||
|         :let={f} | ||||
|         :if={assigns |> Map.get(:options_form, false)} | ||||
|         for={@options_form} | ||||
|         phx-change="update_options" | ||||
|       > | ||||
|         <div class="stat-title">Map systems layout</div> | ||||
|         <div class="stat-value text-white"> | ||||
|           <.input | ||||
|             type="select" | ||||
|             field={f[:layout]} | ||||
|             class="p-dropdown p-component p-inputwrapper" | ||||
|             placeholder="Map default layout" | ||||
|             options={@layout_options} | ||||
|           /> | ||||
|         </div> | ||||
|       </.form> | ||||
|     </:actions> | ||||
|   </.header> | ||||
|   <.header :if={@active_settings_tab == "import"} class="bordered border-1 border-zinc-800"> | ||||
|     Import/Export Map Settings | ||||
|     <:actions> | ||||
|       <.form :if={assigns |> Map.get(:import_form, false)} for={@import_form} phx-change="import"> | ||||
|   | ||||
| @@ -58,7 +58,8 @@ defmodule WandererAppWeb.Router do | ||||
|           ~w('unsafe-inline'), | ||||
|           ~w(https://unpkg.com), | ||||
|           ~w(https://w.appzi.io), | ||||
|           ~w(https://www.googletagmanager.com) | ||||
|           ~w(https://www.googletagmanager.com), | ||||
|           ~w(https://cdnjs.cloudflare.com) | ||||
|         ], | ||||
|         style_src: @style_src, | ||||
|         img_src: @img_src, | ||||
| @@ -200,8 +201,6 @@ defmodule WandererAppWeb.Router do | ||||
|     end | ||||
|   end | ||||
|  | ||||
|  | ||||
|  | ||||
|   # Enable LiveDashboard and Swoosh mailbox preview in development | ||||
|   if Application.compile_env(:wanderer_app, :dev_routes) do | ||||
|     # If you want to use the LiveDashboard in production, you should put | ||||
|   | ||||
							
								
								
									
										7
									
								
								mix.exs
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								mix.exs
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ defmodule WandererApp.MixProject do | ||||
|   use Mix.Project | ||||
|  | ||||
|   @source_url "https://github.com/wanderer-industries/wanderer" | ||||
|   @version "1.3.0" | ||||
|   @version "1.6.0" | ||||
|  | ||||
|   def project do | ||||
|     [ | ||||
| @@ -100,7 +100,7 @@ defmodule WandererApp.MixProject do | ||||
|       {:makeup_elixir, ">= 0.0.0"}, | ||||
|       {:makeup_erlang, ">= 0.0.0"}, | ||||
|       {:better_number, "~> 1.0.0"}, | ||||
|       {:delta_crdt, "~> 0.6.5"}, | ||||
|       {:delta_crdt, "~> 0.6.5", override: true}, | ||||
|       {:qex, "~> 0.5"}, | ||||
|       {:site_encrypt, "~> 0.6.0"}, | ||||
|       {:bandit, "~> 1.0"}, | ||||
| @@ -111,7 +111,8 @@ defmodule WandererApp.MixProject do | ||||
|       {:mox, "~> 1.1", only: [:test, :integration]}, | ||||
|       {:git_ops, "~> 2.6.1"}, | ||||
|       {:version_tasks, "~> 0.12.0"}, | ||||
|       {:error_tracker, "~> 0.2"} | ||||
|       {:error_tracker, "~> 0.2"}, | ||||
|       {:ddrt, "~> 0.2.1"} | ||||
|     ] | ||||
|   end | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								mix.lock
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								mix.lock
									
									
									
									
									
								
							| @@ -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"}, | ||||
|   "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"}, | ||||
|   "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"}, | ||||
|   "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, | ||||
|   "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, | ||||
|   | ||||
							
								
								
									
										21
									
								
								priv/repo/migrations/20241006092351_add_map_options.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								priv/repo/migrations/20241006092351_add_map_options.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| defmodule WandererApp.Repo.Migrations.AddMapOptions 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 | ||||
|     alter table(:maps_v1) do | ||||
|       add :options, :text | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def down do | ||||
|     alter table(:maps_v1) do | ||||
|       remove :options | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -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 | ||||
| @@ -0,0 +1,21 @@ | ||||
| defmodule WandererApp.Repo.Migrations.AddSignatureLinkedSystem 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 | ||||
|     alter table(:map_system_signatures_v1) do | ||||
|       add :linked_system_id, :bigint | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def down do | ||||
|     alter table(:map_system_signatures_v1) do | ||||
|       remove :linked_system_id | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -0,0 +1,167 @@ | ||||
| { | ||||
|   "attributes": [ | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "fragment(\"gen_random_uuid()\")", | ||||
|       "generated?": false, | ||||
|       "primary_key?": true, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "id", | ||||
|       "type": "uuid" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "eve_id", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "character_eve_id", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "name", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "description", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "linked_system_id", | ||||
|       "type": "bigint" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "kind", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "group", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "inserted_at", | ||||
|       "type": "utc_datetime_usec" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "updated_at", | ||||
|       "type": "utc_datetime_usec" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "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_system_signatures_v1_system_id_fkey", | ||||
|         "on_delete": null, | ||||
|         "on_update": null, | ||||
|         "primary_key?": true, | ||||
|         "schema": "public", | ||||
|         "table": "map_system_v1" | ||||
|       }, | ||||
|       "size": null, | ||||
|       "source": "system_id", | ||||
|       "type": "uuid" | ||||
|     } | ||||
|   ], | ||||
|   "base_filter": null, | ||||
|   "check_constraints": [], | ||||
|   "custom_indexes": [], | ||||
|   "custom_statements": [], | ||||
|   "has_create_action": true, | ||||
|   "hash": "B437BC9E9F4607EBD235CBB39B814710E223D532CE09B952C9371257159008F4", | ||||
|   "identities": [ | ||||
|     { | ||||
|       "all_tenants?": false, | ||||
|       "base_filter": null, | ||||
|       "index_name": "map_system_signatures_v1_uniq_system_eve_id_index", | ||||
|       "keys": [ | ||||
|         { | ||||
|           "type": "atom", | ||||
|           "value": "system_id" | ||||
|         }, | ||||
|         { | ||||
|           "type": "atom", | ||||
|           "value": "eve_id" | ||||
|         } | ||||
|       ], | ||||
|       "name": "uniq_system_eve_id", | ||||
|       "nils_distinct?": true, | ||||
|       "where": null | ||||
|     } | ||||
|   ], | ||||
|   "multitenancy": { | ||||
|     "attribute": null, | ||||
|     "global": null, | ||||
|     "strategy": null | ||||
|   }, | ||||
|   "repo": "Elixir.WandererApp.Repo", | ||||
|   "schema": null, | ||||
|   "table": "map_system_signatures_v1" | ||||
| } | ||||
| @@ -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" | ||||
| } | ||||
							
								
								
									
										186
									
								
								priv/resource_snapshots/repo/maps_v1/20241006092351.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								priv/resource_snapshots/repo/maps_v1/20241006092351.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| { | ||||
|   "attributes": [ | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "fragment(\"gen_random_uuid()\")", | ||||
|       "generated?": false, | ||||
|       "primary_key?": true, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "id", | ||||
|       "type": "uuid" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "name", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "slug", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "description", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "personal_note", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "[]", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "hubs", | ||||
|       "type": [ | ||||
|         "array", | ||||
|         "text" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "\"wormholes\"", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "scope", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "false", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "deleted", | ||||
|       "type": "boolean" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "false", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "only_tracked_characters", | ||||
|       "type": "boolean" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "options", | ||||
|       "type": "text" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "inserted_at", | ||||
|       "type": "utc_datetime_usec" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": false, | ||||
|       "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "references": null, | ||||
|       "size": null, | ||||
|       "source": "updated_at", | ||||
|       "type": "utc_datetime_usec" | ||||
|     }, | ||||
|     { | ||||
|       "allow_nil?": true, | ||||
|       "default": "nil", | ||||
|       "generated?": false, | ||||
|       "primary_key?": false, | ||||
|       "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": "maps_v1_owner_id_fkey", | ||||
|         "on_delete": null, | ||||
|         "on_update": null, | ||||
|         "primary_key?": true, | ||||
|         "schema": "public", | ||||
|         "table": "character_v1" | ||||
|       }, | ||||
|       "size": null, | ||||
|       "source": "owner_id", | ||||
|       "type": "uuid" | ||||
|     } | ||||
|   ], | ||||
|   "base_filter": null, | ||||
|   "check_constraints": [], | ||||
|   "custom_indexes": [], | ||||
|   "custom_statements": [], | ||||
|   "has_create_action": true, | ||||
|   "hash": "E5FC6B5F1B9AD5E23163494C7C93A8002F9C812AFC7A26A8C33A344877086A03", | ||||
|   "identities": [ | ||||
|     { | ||||
|       "all_tenants?": false, | ||||
|       "base_filter": null, | ||||
|       "index_name": "maps_v1_unique_slug_index", | ||||
|       "keys": [ | ||||
|         { | ||||
|           "type": "atom", | ||||
|           "value": "slug" | ||||
|         } | ||||
|       ], | ||||
|       "name": "unique_slug", | ||||
|       "nils_distinct?": true, | ||||
|       "where": null | ||||
|     } | ||||
|   ], | ||||
|   "multitenancy": { | ||||
|     "attribute": null, | ||||
|     "global": null, | ||||
|     "strategy": null | ||||
|   }, | ||||
|   "repo": "Elixir.WandererApp.Repo", | ||||
|   "schema": null, | ||||
|   "table": "maps_v1" | ||||
| } | ||||
		Reference in New Issue
	
	Block a user