mirror of
				https://github.com/wanderer-industries/wanderer
				synced 2025-11-04 08:24:51 +00:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			19-add-map
			...
			v1.3.5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e8a11333f2 | ||
| 
						 | 
					8bb6d09e6e | ||
| 
						 | 
					56bf955297 | ||
| 
						 | 
					ef6c08dfe8 | ||
| 
						 | 
					495c3e1cd7 | ||
| 
						 | 
					9a5fe3d744 | ||
| 
						 | 
					72607cae4d | ||
| 
						 | 
					4891cdb04d | ||
| 
						 | 
					d214881720 | 
							
								
								
									
										29
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -2,6 +2,35 @@
 | 
			
		||||
 | 
			
		||||
<!-- changelog -->
 | 
			
		||||
 | 
			
		||||
## [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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,8 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
 | 
			
		||||
  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);
 | 
			
		||||
 | 
			
		||||
@@ -90,8 +92,8 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
 | 
			
		||||
  }, [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,
 | 
			
		||||
@@ -114,13 +116,26 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id);
 | 
			
		||||
    await handleUpdateSignatures(signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)));
 | 
			
		||||
    await handleUpdateSignatures(
 | 
			
		||||
      signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)),
 | 
			
		||||
      false,
 | 
			
		||||
    );
 | 
			
		||||
  }, [handleUpdateSignatures, signatures, selectedSignatures]);
 | 
			
		||||
 | 
			
		||||
  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]);
 | 
			
		||||
 | 
			
		||||
  useHotkey(true, ['a'], handleSelectAll);
 | 
			
		||||
 | 
			
		||||
  useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected);
 | 
			
		||||
@@ -135,7 +150,12 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
 | 
			
		||||
      settings.map(x => x.key),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    handleUpdateSignatures(signatures);
 | 
			
		||||
    if (!signaturesRef.current || !signaturesRef.current.length) {
 | 
			
		||||
      handleUpdateSignatures(signatures, false);
 | 
			
		||||
    } else {
 | 
			
		||||
      setParsedSignatures(signatures);
 | 
			
		||||
      setAskUser(true);
 | 
			
		||||
    }
 | 
			
		||||
  }, [clipboardContent]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
@@ -184,98 +204,125 @@ 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
 | 
			
		||||
    <>
 | 
			
		||||
      {askUser && (
 | 
			
		||||
        <div className="flex w-full h-full bg-stone-900/95 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={handleReplaceAll}>
 | 
			
		||||
                    Replace
 | 
			
		||||
                  </span>
 | 
			
		||||
                </button>
 | 
			
		||||
                <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>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </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');
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              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>
 | 
			
		||||
 | 
			
		||||
            <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*/}
 | 
			
		||||
            {/*  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>
 | 
			
		||||
      {!askUser && (
 | 
			
		||||
        <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',
 | 
			
		||||
                    );
 | 
			
		||||
                  }
 | 
			
		||||
 | 
			
		||||
                  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>
 | 
			
		||||
 | 
			
		||||
                <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*/}
 | 
			
		||||
                {/*  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>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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,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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
@@ -44,7 +43,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",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										344
									
								
								assets/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										344
									
								
								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==
 | 
			
		||||
@@ -3099,7 +3263,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 +3379,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 +3411,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 +3444,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 +3526,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 +3643,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 +3783,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 +3828,7 @@ use-local-storage-state@^19.3.1:
 | 
			
		||||
  resolved "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-19.3.1.tgz"
 | 
			
		||||
  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 +3856,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)$",
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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?())
 | 
			
		||||
 
 | 
			
		||||
@@ -71,11 +71,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(
 | 
			
		||||
@@ -849,8 +857,8 @@ defmodule WandererApp.Map.Server.Impl do
 | 
			
		||||
             ) 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_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)
 | 
			
		||||
 | 
			
		||||
          _ ->
 | 
			
		||||
@@ -1097,7 +1105,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 +1121,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 +1173,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 +1266,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
 | 
			
		||||
@@ -1586,12 +1599,17 @@ defmodule WandererApp.Map.Server.Impl do
 | 
			
		||||
        :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
 | 
			
		||||
        })
 | 
			
		||||
        :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
 | 
			
		||||
          })
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    case WandererApp.Map.check_connection(map_id, location, old_location) do
 | 
			
		||||
@@ -1614,11 +1632,11 @@ defmodule WandererApp.Map.Server.Impl do
 | 
			
		||||
 | 
			
		||||
  defp maybe_add_connection(_map_id, _location, _old_location, _character), 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 +1706,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
 | 
			
		||||
  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)}
 | 
			
		||||
  end
 | 
			
		||||
     |> 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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 != []}>
 | 
			
		||||
 
 | 
			
		||||
@@ -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,14 @@ 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 +377,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 +496,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 +527,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 +559,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 +667,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
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,6 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
         id: map_id,
 | 
			
		||||
         deleted: false
 | 
			
		||||
       } = map} ->
 | 
			
		||||
 | 
			
		||||
        Process.send_after(self(), {:init_map, map}, 10)
 | 
			
		||||
 | 
			
		||||
        socket
 | 
			
		||||
@@ -130,28 +129,28 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
  def handle_info(%{event: :add_system, payload: system}, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event("add_systems", [map_ui_system(system)])}
 | 
			
		||||
     |> push_map_event("add_systems", [map_ui_system(system)])}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_info(%{event: :update_system, payload: system}, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event("update_systems", [map_ui_system(system)])}
 | 
			
		||||
     |> push_map_event("update_systems", [map_ui_system(system)])}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_info(%{event: :update_connection, payload: connection}, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event("update_connection", map_ui_connection(connection))}
 | 
			
		||||
     |> push_map_event("update_connection", map_ui_connection(connection))}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_info(%{event: :systems_removed, payload: solar_system_ids}, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event("remove_systems", solar_system_ids)}
 | 
			
		||||
     |> push_map_event("remove_systems", solar_system_ids)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
@@ -160,7 +159,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event(
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "remove_connections",
 | 
			
		||||
       connection_ids
 | 
			
		||||
     )}
 | 
			
		||||
@@ -172,7 +171,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event(
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "add_connections",
 | 
			
		||||
       connections
 | 
			
		||||
     )}
 | 
			
		||||
@@ -182,7 +181,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
  def handle_info(%{event: :update_map, payload: map_diff}, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event(
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "map_updated",
 | 
			
		||||
       map_diff
 | 
			
		||||
     )}
 | 
			
		||||
@@ -196,7 +195,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event(
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "kills_updated",
 | 
			
		||||
       kills
 | 
			
		||||
     )}
 | 
			
		||||
@@ -218,7 +217,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event(
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "characters_updated",
 | 
			
		||||
       characters
 | 
			
		||||
     )}
 | 
			
		||||
@@ -228,7 +227,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
  def handle_info(%{event: :character_added, payload: character}, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event(
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "character_added",
 | 
			
		||||
       character |> map_ui_character()
 | 
			
		||||
     )}
 | 
			
		||||
@@ -238,7 +237,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
  def handle_info(%{event: :character_removed, payload: character}, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event(
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "character_removed",
 | 
			
		||||
       character |> map_ui_character()
 | 
			
		||||
     )}
 | 
			
		||||
@@ -248,7 +247,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
  def handle_info(%{event: :character_updated, payload: character}, socket) do
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> _push_map_event(
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "character_updated",
 | 
			
		||||
       character |> map_ui_character()
 | 
			
		||||
     )}
 | 
			
		||||
@@ -262,7 +261,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
      do:
 | 
			
		||||
        {:noreply,
 | 
			
		||||
         socket
 | 
			
		||||
         |> _push_map_event(
 | 
			
		||||
         |> push_map_event(
 | 
			
		||||
           "present_characters",
 | 
			
		||||
           present_character_eve_ids
 | 
			
		||||
         )}
 | 
			
		||||
@@ -336,7 +335,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
 | 
			
		||||
          socket
 | 
			
		||||
          |> assign(user_permissions: user_permissions)
 | 
			
		||||
          |> _push_map_event(
 | 
			
		||||
          |> push_map_event(
 | 
			
		||||
            "user_permissions",
 | 
			
		||||
            user_permissions
 | 
			
		||||
          )
 | 
			
		||||
@@ -376,17 +375,24 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
      tracked_character_ids =
 | 
			
		||||
        availaible_map_characters |> Enum.filter(& &1.tracked) |> Enum.map(& &1.id)
 | 
			
		||||
 | 
			
		||||
      all_character_tracked? = (not (availaible_map_characters |> Enum.empty?())) and availaible_map_characters |> Enum.all?(& &1.tracked)
 | 
			
		||||
      all_character_tracked? =
 | 
			
		||||
        not (availaible_map_characters |> Enum.empty?()) and
 | 
			
		||||
          availaible_map_characters |> Enum.all?(& &1.tracked)
 | 
			
		||||
 | 
			
		||||
      cond do
 | 
			
		||||
        (only_tracked_characters and can_track? and all_character_tracked?) or
 | 
			
		||||
            (not only_tracked_characters and can_view?) ->
 | 
			
		||||
            Process.send_after(self(), {:map_init, %{
 | 
			
		||||
              map_id: map_id,
 | 
			
		||||
              page_title: map_name,
 | 
			
		||||
              user_permissions: user_permissions,
 | 
			
		||||
              tracked_character_ids: tracked_character_ids
 | 
			
		||||
            }}, 10)
 | 
			
		||||
          Process.send_after(
 | 
			
		||||
            self(),
 | 
			
		||||
            {:map_init,
 | 
			
		||||
             %{
 | 
			
		||||
               map_id: map_id,
 | 
			
		||||
               page_title: map_name,
 | 
			
		||||
               user_permissions: user_permissions,
 | 
			
		||||
               tracked_character_ids: tracked_character_ids
 | 
			
		||||
             }},
 | 
			
		||||
            10
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
        only_tracked_characters and can_track? and not all_character_tracked? ->
 | 
			
		||||
          Process.send_after(self(), :not_all_characters_tracked, 10)
 | 
			
		||||
@@ -406,17 +412,20 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
    Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id)
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
      socket
 | 
			
		||||
      |> assign(initial_data)}
 | 
			
		||||
     socket
 | 
			
		||||
     |> assign(initial_data)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info({:map_start,
 | 
			
		||||
   %{
 | 
			
		||||
     map_id: map_id,
 | 
			
		||||
     user_characters: user_character_eve_ids,
 | 
			
		||||
     initial_data: initial_data,
 | 
			
		||||
     events: events
 | 
			
		||||
   } = _started_data}, socket) do
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        {:map_start,
 | 
			
		||||
         %{
 | 
			
		||||
           map_id: map_id,
 | 
			
		||||
           user_characters: user_character_eve_ids,
 | 
			
		||||
           initial_data: initial_data,
 | 
			
		||||
           events: events
 | 
			
		||||
         } = _started_data},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    socket =
 | 
			
		||||
      events
 | 
			
		||||
      |> Enum.reduce(socket, fn event, socket ->
 | 
			
		||||
@@ -452,66 +461,78 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
        end
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    Process.send_after(self(), {:map_loaded,
 | 
			
		||||
      %{
 | 
			
		||||
        map_id: map_id,
 | 
			
		||||
        user_characters: user_character_eve_ids,
 | 
			
		||||
        initial_data: initial_data
 | 
			
		||||
      }}, 10)
 | 
			
		||||
    Process.send_after(
 | 
			
		||||
      self(),
 | 
			
		||||
      {:map_loaded,
 | 
			
		||||
       %{
 | 
			
		||||
         map_id: map_id,
 | 
			
		||||
         user_characters: user_character_eve_ids,
 | 
			
		||||
         initial_data: initial_data
 | 
			
		||||
       }},
 | 
			
		||||
      10
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info({:map_loaded,
 | 
			
		||||
   %{
 | 
			
		||||
     map_id: map_id,
 | 
			
		||||
     user_characters: user_character_eve_ids,
 | 
			
		||||
     initial_data: initial_data
 | 
			
		||||
   } = _loaded_data}, socket) do
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        {:map_loaded,
 | 
			
		||||
         %{
 | 
			
		||||
           map_id: map_id,
 | 
			
		||||
           user_characters: user_character_eve_ids,
 | 
			
		||||
           initial_data: initial_data
 | 
			
		||||
         } = _loaded_data},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    map_characters = map_id |> WandererApp.Map.list_characters()
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
      socket
 | 
			
		||||
      |> assign(
 | 
			
		||||
        map_loaded?: true,
 | 
			
		||||
        user_characters: user_character_eve_ids,
 | 
			
		||||
        has_tracked_characters?: _has_tracked_characters?(user_character_eve_ids)
 | 
			
		||||
      )
 | 
			
		||||
      |> _push_map_event("init", initial_data |> Map.merge(%{
 | 
			
		||||
        characters: map_characters |> Enum.map(&map_ui_character/1)
 | 
			
		||||
      }))
 | 
			
		||||
      |> push_event("js-exec", %{
 | 
			
		||||
        to: "#map-loader",
 | 
			
		||||
        attr: "data-loaded"
 | 
			
		||||
      })}
 | 
			
		||||
     socket
 | 
			
		||||
     |> assign(
 | 
			
		||||
       map_loaded?: true,
 | 
			
		||||
       user_characters: user_character_eve_ids,
 | 
			
		||||
       has_tracked_characters?: _has_tracked_characters?(user_character_eve_ids)
 | 
			
		||||
     )
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "init",
 | 
			
		||||
       initial_data |> Map.put(:characters, map_characters |> Enum.map(&map_ui_character/1))
 | 
			
		||||
     )
 | 
			
		||||
     |> push_event("js-exec", %{
 | 
			
		||||
       to: "#map-loader",
 | 
			
		||||
       attr: "data-loaded"
 | 
			
		||||
     })}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info(:no_access, socket), do:
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> put_flash(:error, "You don't have an access to this map.")
 | 
			
		||||
     |> push_navigate(to: ~p"/maps")}
 | 
			
		||||
  def handle_info(:no_access, socket),
 | 
			
		||||
    do:
 | 
			
		||||
      {:noreply,
 | 
			
		||||
       socket
 | 
			
		||||
       |> put_flash(:error, "You don't have an access to this map.")
 | 
			
		||||
       |> push_navigate(to: ~p"/maps")}
 | 
			
		||||
 | 
			
		||||
  def handle_info(:no_permissions, socket), do:
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> put_flash(:error, "You don't have permissions to use this map.")
 | 
			
		||||
     |> push_navigate(to: ~p"/maps")}
 | 
			
		||||
  def handle_info(:no_permissions, socket),
 | 
			
		||||
    do:
 | 
			
		||||
      {:noreply,
 | 
			
		||||
       socket
 | 
			
		||||
       |> put_flash(:error, "You don't have permissions to use this map.")
 | 
			
		||||
       |> push_navigate(to: ~p"/maps")}
 | 
			
		||||
 | 
			
		||||
  def handle_info(:not_all_characters_tracked, socket), do:
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> put_flash(
 | 
			
		||||
       :error,
 | 
			
		||||
       "You should enable tracking for all characters that have access to this map first!"
 | 
			
		||||
     )
 | 
			
		||||
     |> push_navigate(to: ~p"/tracking/#{socket.assigns.map_slug}")}
 | 
			
		||||
  def handle_info(:not_all_characters_tracked, socket),
 | 
			
		||||
    do:
 | 
			
		||||
      {:noreply,
 | 
			
		||||
       socket
 | 
			
		||||
       |> put_flash(
 | 
			
		||||
         :error,
 | 
			
		||||
         "You should enable tracking for all characters that have access to this map first!"
 | 
			
		||||
       )
 | 
			
		||||
       |> push_navigate(to: ~p"/tracking/#{socket.assigns.map_slug}")}
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        {ref, result},
 | 
			
		||||
        socket
 | 
			
		||||
      ) when is_reference(ref) do
 | 
			
		||||
      )
 | 
			
		||||
      when is_reference(ref) do
 | 
			
		||||
    Process.demonitor(ref, [:flush])
 | 
			
		||||
 | 
			
		||||
    case result do
 | 
			
		||||
@@ -539,7 +560,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
      {:routes, {solar_system_id, %{routes: routes, systems_static_data: systems_static_data}}} ->
 | 
			
		||||
        {:noreply,
 | 
			
		||||
         socket
 | 
			
		||||
         |> _push_map_event(
 | 
			
		||||
         |> push_map_event(
 | 
			
		||||
           "routes",
 | 
			
		||||
           %{
 | 
			
		||||
             solar_system_id: solar_system_id,
 | 
			
		||||
@@ -571,12 +592,11 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("reconnected", _body, socket) do
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("change_map", %{"map_slug" => map_slug} = _event, %{assigns: %{map_id: map_id}} = socket) do
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "change_map",
 | 
			
		||||
        %{"map_slug" => map_slug} = _event,
 | 
			
		||||
        %{assigns: %{map_id: map_id}} = socket
 | 
			
		||||
      ) do
 | 
			
		||||
    Phoenix.PubSub.unsubscribe(WandererApp.PubSub, map_id)
 | 
			
		||||
    {:noreply, push_navigate(socket, to: ~p"/#{map_slug}")}
 | 
			
		||||
  end
 | 
			
		||||
@@ -637,13 +657,16 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
              solar_system_target_id: solar_system_target_id |> String.to_integer()
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :connection, :add], %{count: 1}, %{
 | 
			
		||||
              character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
              user_id: current_user.id,
 | 
			
		||||
              map_id: map_id,
 | 
			
		||||
              solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
 | 
			
		||||
              solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer()
 | 
			
		||||
            })
 | 
			
		||||
            {:ok, _} =
 | 
			
		||||
              WandererApp.User.ActivityTracker.track_map_event(:map_connection_added, %{
 | 
			
		||||
                character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
                user_id: current_user.id,
 | 
			
		||||
                map_id: map_id,
 | 
			
		||||
                solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
 | 
			
		||||
                solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer()
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :connection, :add], %{count: 1})
 | 
			
		||||
 | 
			
		||||
            {:noreply, socket}
 | 
			
		||||
 | 
			
		||||
@@ -682,18 +705,21 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
              socket
 | 
			
		||||
              |> map_id()
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :hub, :add], %{count: 1}, %{
 | 
			
		||||
              character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
              user_id: current_user.id,
 | 
			
		||||
              map_id: map_id,
 | 
			
		||||
              solar_system_id: solar_system_id
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            map_id
 | 
			
		||||
            |> WandererApp.Map.Server.add_hub(%{
 | 
			
		||||
              solar_system_id: solar_system_id
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            {:ok, _} =
 | 
			
		||||
              WandererApp.User.ActivityTracker.track_map_event(:hub_added, %{
 | 
			
		||||
                character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
                user_id: current_user.id,
 | 
			
		||||
                map_id: map_id,
 | 
			
		||||
                solar_system_id: solar_system_id
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :hub, :add], %{count: 1})
 | 
			
		||||
 | 
			
		||||
            {:noreply, socket}
 | 
			
		||||
 | 
			
		||||
          _ ->
 | 
			
		||||
@@ -731,18 +757,21 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
              socket
 | 
			
		||||
              |> map_id()
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :hub, :remove], %{count: 1}, %{
 | 
			
		||||
              character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
              user_id: current_user.id,
 | 
			
		||||
              map_id: map_id,
 | 
			
		||||
              solar_system_id: solar_system_id
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            map_id
 | 
			
		||||
            |> WandererApp.Map.Server.remove_hub(%{
 | 
			
		||||
              solar_system_id: solar_system_id
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            {:ok, _} =
 | 
			
		||||
              WandererApp.User.ActivityTracker.track_map_event(:hub_removed, %{
 | 
			
		||||
                character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
                user_id: current_user.id,
 | 
			
		||||
                map_id: map_id,
 | 
			
		||||
                solar_system_id: solar_system_id
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :hub, :remove], %{count: 1})
 | 
			
		||||
 | 
			
		||||
            {:noreply, socket}
 | 
			
		||||
 | 
			
		||||
          _ ->
 | 
			
		||||
@@ -802,15 +831,6 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
              socket
 | 
			
		||||
              |> map_id()
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :system, :update], %{count: 1}, %{
 | 
			
		||||
              character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
              user_id: current_user.id,
 | 
			
		||||
              map_id: map_id,
 | 
			
		||||
              solar_system_id: "#{solar_system_id}" |> String.to_integer(),
 | 
			
		||||
              key: key_atom,
 | 
			
		||||
              value: value
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            apply(WandererApp.Map.Server, method_atom, [
 | 
			
		||||
              map_id,
 | 
			
		||||
              %{
 | 
			
		||||
@@ -819,6 +839,18 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
              |> Map.put_new(key_atom, value)
 | 
			
		||||
            ])
 | 
			
		||||
 | 
			
		||||
            {:ok, _} =
 | 
			
		||||
              WandererApp.User.ActivityTracker.track_map_event(:system_updated, %{
 | 
			
		||||
                character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
                user_id: current_user.id,
 | 
			
		||||
                map_id: map_id,
 | 
			
		||||
                solar_system_id: "#{solar_system_id}" |> String.to_integer(),
 | 
			
		||||
                key: key_atom,
 | 
			
		||||
                value: value
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :system, :update], %{count: 1})
 | 
			
		||||
 | 
			
		||||
            {:noreply, socket}
 | 
			
		||||
 | 
			
		||||
          _ ->
 | 
			
		||||
@@ -878,15 +910,18 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
              socket
 | 
			
		||||
              |> map_id()
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :connection, :update], %{count: 1}, %{
 | 
			
		||||
              character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
              user_id: current_user.id,
 | 
			
		||||
              map_id: map_id,
 | 
			
		||||
              solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
 | 
			
		||||
              solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer(),
 | 
			
		||||
              key: key_atom,
 | 
			
		||||
              value: value
 | 
			
		||||
            })
 | 
			
		||||
            {:ok, _} =
 | 
			
		||||
              WandererApp.User.ActivityTracker.track_map_event(:map_connection_updated, %{
 | 
			
		||||
                character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
                user_id: current_user.id,
 | 
			
		||||
                map_id: map_id,
 | 
			
		||||
                solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
 | 
			
		||||
                solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer(),
 | 
			
		||||
                key: key_atom,
 | 
			
		||||
                value: value
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :connection, :update], %{count: 1})
 | 
			
		||||
 | 
			
		||||
            apply(WandererApp.Map.Server, method_atom, [
 | 
			
		||||
              map_id,
 | 
			
		||||
@@ -1230,13 +1265,16 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
              solar_system_target_id: solar_system_target_id |> String.to_integer()
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :connection, :remove], %{count: 1}, %{
 | 
			
		||||
              character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
              user_id: current_user.id,
 | 
			
		||||
              map_id: map_id,
 | 
			
		||||
              solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
 | 
			
		||||
              solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer()
 | 
			
		||||
            })
 | 
			
		||||
            {:ok, _} =
 | 
			
		||||
              WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{
 | 
			
		||||
                character_id: tracked_character_ids |> List.first(),
 | 
			
		||||
                user_id: current_user.id,
 | 
			
		||||
                map_id: map_id,
 | 
			
		||||
                solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
 | 
			
		||||
                solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer()
 | 
			
		||||
              })
 | 
			
		||||
 | 
			
		||||
            :telemetry.execute([:wanderer_app, :map, :connection, :remove], %{count: 1})
 | 
			
		||||
 | 
			
		||||
            {:noreply, socket}
 | 
			
		||||
 | 
			
		||||
@@ -1424,7 +1462,7 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
     |> assign_async(:characters, fn ->
 | 
			
		||||
       {:ok, %{characters: characters}}
 | 
			
		||||
     end)
 | 
			
		||||
     |> _push_map_event(
 | 
			
		||||
     |> push_map_event(
 | 
			
		||||
       "init",
 | 
			
		||||
       %{
 | 
			
		||||
         user_characters: user_character_eve_ids,
 | 
			
		||||
@@ -1564,16 +1602,20 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
          )
 | 
			
		||||
          |> Map.put(:reset, true)
 | 
			
		||||
 | 
			
		||||
        Process.send_after(self(), {:map_start, %{
 | 
			
		||||
          map_id: map_id,
 | 
			
		||||
          user_characters: user_character_eve_ids,
 | 
			
		||||
          initial_data: initial_data,
 | 
			
		||||
          events: events
 | 
			
		||||
        }}, 10)
 | 
			
		||||
        Process.send_after(
 | 
			
		||||
          self(),
 | 
			
		||||
          {:map_start,
 | 
			
		||||
           %{
 | 
			
		||||
             map_id: map_id,
 | 
			
		||||
             user_characters: user_character_eve_ids,
 | 
			
		||||
             initial_data: initial_data,
 | 
			
		||||
             events: events
 | 
			
		||||
           }},
 | 
			
		||||
          10
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        Process.send_after(self(), :no_access, 10)
 | 
			
		||||
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -2079,14 +2121,13 @@ defmodule WandererAppWeb.MapLive do
 | 
			
		||||
    :ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp _push_map_event(socket, type, event_body)
 | 
			
		||||
    do
 | 
			
		||||
  defp push_map_event(socket, type, body),
 | 
			
		||||
    do:
 | 
			
		||||
      socket
 | 
			
		||||
      |> push_event("map_event", %{
 | 
			
		||||
        type: type,
 | 
			
		||||
        body: event_body |> WandererApp.Utils.JSONUtil.compress()
 | 
			
		||||
        body: body
 | 
			
		||||
      })
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
  defp map_id(%{assigns: %{map_id: map_id}} = _socket), do: map_id
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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, current_user: current_user}} = 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.3.5"
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
							
								
								
									
										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