Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56bf955297 | ||
|
|
ef6c08dfe8 | ||
|
|
495c3e1cd7 | ||
|
|
9a5fe3d744 | ||
|
|
72607cae4d | ||
|
|
4891cdb04d | ||
|
|
d214881720 | ||
|
|
e66c125dbf | ||
|
|
9862bcfa05 | ||
|
|
0ac5451bef | ||
|
|
669479b815 | ||
|
|
2721130566 | ||
|
|
6e33ad943f | ||
|
|
f4b7357802 | ||
|
|
7a404a7e6a | ||
|
|
5158700a79 | ||
|
|
41d10c1b47 | ||
|
|
3aaac91f07 | ||
|
|
ea7ff080b8 | ||
|
|
b5270958eb | ||
|
|
b0a38eab8c | ||
|
|
0a478e82ba | ||
|
|
02d97a009c | ||
|
|
33940cdb9b | ||
|
|
7a63f9ee6b | ||
|
|
89b41fff59 | ||
|
|
7a15f71528 | ||
|
|
cdce2f8761 | ||
|
|
a2470bbe47 | ||
|
|
dbdf1ddce0 | ||
|
|
f767e42e6f | ||
|
|
3051eb6369 | ||
|
|
a41faddca3 | ||
|
|
469038730e | ||
|
|
b1fe5d2453 | ||
|
|
f43e717da0 | ||
|
|
95c8d4eef8 | ||
|
|
747ca0ee82 | ||
|
|
35a0184ec3 | ||
|
|
96e1e5328c | ||
|
|
7f98d6a0d8 | ||
|
|
0194e25696 | ||
|
|
189442e50f | ||
|
|
d9bed070ec | ||
|
|
a6193da8b5 | ||
|
|
50d35b207d | ||
|
|
19eb45bfa1 | ||
|
|
01e0b24d9d | ||
|
|
3c8024b16c | ||
|
|
4c0ad0dd66 | ||
|
|
501840086b | ||
|
|
240b180857 | ||
|
|
2bc5d0aaea | ||
|
|
df66aa79b8 | ||
|
|
6ea6a59ce3 | ||
|
|
f3afa4d9d2 | ||
|
|
e33d81eda1 | ||
|
|
0fc4863dc4 | ||
|
|
63471a5533 | ||
|
|
050e90cb7e | ||
|
|
b4643f2e45 | ||
|
|
2d740a76b7 | ||
|
|
0de9e3a02d | ||
|
|
bedaa37e08 | ||
|
|
ef9fa80b76 | ||
|
|
dc2ea625ec | ||
|
|
17df2c188a | ||
|
|
6e050bccdf | ||
|
|
8afbf3ce91 | ||
|
|
9f57bf46a1 | ||
|
|
4ce47da521 | ||
|
|
4dc6382402 | ||
|
|
cb8c1e32d9 | ||
|
|
bf534be128 | ||
|
|
a89c117612 | ||
|
|
501dbcd76b | ||
|
|
c0ceff1eec | ||
|
|
6039ac5d4f | ||
|
|
48e87f3c47 | ||
|
|
c7866a1270 | ||
|
|
4fadcd5964 | ||
|
|
6480154d1b | ||
|
|
9c064531b8 | ||
|
|
bee64c2570 | ||
|
|
5393321953 | ||
|
|
b44669da87 | ||
|
|
9d8bf1529a | ||
|
|
a514825eaf | ||
|
|
8abcacb517 | ||
|
|
a3fc55e63d | ||
|
|
a4c1d5bf98 | ||
|
|
b16bc615fa | ||
|
|
10ec8d6b97 | ||
|
|
b04f0c9183 | ||
|
|
45e9ebb0d4 | ||
|
|
ac3e68a49f | ||
|
|
b35ef1151a | ||
|
|
ebd01bebd4 | ||
|
|
7c7dd44805 | ||
|
|
152ee60576 | ||
|
|
9db568d726 | ||
|
|
a34805d3dd | ||
|
|
8105af7451 | ||
|
|
88fd0eb3dd | ||
|
|
0e406c7818 | ||
|
|
778fe4a998 | ||
|
|
17ffac57d4 | ||
|
|
6cedf235ca | ||
|
|
877b8c2338 | ||
|
|
973f8508a8 | ||
|
|
91e3bfacf2 | ||
|
|
9e2ad58c0f | ||
|
|
7ecd96b448 |
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: WandererLtd
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
7
.github/workflows/build.yml
vendored
@@ -112,8 +112,6 @@ jobs:
|
||||
git config --global user.name 'CI'
|
||||
git config --global user.email 'ci@users.noreply.github.com'
|
||||
mix git_ops.release --force-patch --yes
|
||||
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
|
||||
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
|
||||
git push --follow-tags
|
||||
|
||||
docker:
|
||||
@@ -145,6 +143,11 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare Changelog
|
||||
run: |
|
||||
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
|
||||
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
|
||||
2
.gitignore
vendored
@@ -12,6 +12,8 @@
|
||||
|
||||
# Ignore assets that are produced by build tools.
|
||||
/priv/static/assets/
|
||||
/priv/static/icons/
|
||||
/priv/static/images/
|
||||
/priv/static/*.js
|
||||
/priv/static/*.css
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
erlang 27.0.1
|
||||
elixir 1.17-otp-27
|
||||
erlang 25.3
|
||||
elixir 1.16-otp-25
|
||||
nodejs 18.0.0
|
||||
|
||||
230
CHANGELOG.md
@@ -2,12 +2,230 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Map: Fix default sort
|
||||
|
||||
* Map: Remove resizible and fix styles of column sorting
|
||||
|
||||
* Map: Revision of sorting from also adding ability to sort all columns
|
||||
|
||||
## [v1.2.10](https://github.com/wanderer-industries/wanderer/compare/v1.2.9...v1.2.10) (2024-10-07)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.2.9](https://github.com/wanderer-industries/wanderer/compare/v1.2.8...v1.2.9) (2024-10-07)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.2.8](https://github.com/wanderer-industries/wanderer/compare/v1.2.7...v1.2.8) (2024-10-06)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.2.7](https://github.com/wanderer-industries/wanderer/compare/v1.2.6...v1.2.7) (2024-10-05)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.2.6](https://github.com/wanderer-industries/wanderer/compare/v1.2.5...v1.2.6) (2024-10-05)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Stability & performance improvements
|
||||
|
||||
## [v1.2.5](https://github.com/wanderer-industries/wanderer/compare/v1.2.4...v1.2.5) (2024-10-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Add system "true security" correction
|
||||
|
||||
## [v1.2.4](https://github.com/wanderer-industries/wanderer/compare/v1.2.3...v1.2.4) (2024-10-03)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Remove duplicate connections
|
||||
|
||||
## [v1.2.3](https://github.com/wanderer-industries/wanderer/compare/v1.2.2...v1.2.3) (2024-10-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix map loading after select a different map.
|
||||
|
||||
## [v1.2.2](https://github.com/wanderer-industries/wanderer/compare/v1.2.1...v1.2.2) (2024-10-02)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.2.1](https://github.com/wanderer-industries/wanderer/compare/v1.2.0...v1.2.1) (2024-10-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* ACL: Fix allowing to save map/access list with empty owner set
|
||||
|
||||
## [v1.2.0](https://github.com/wanderer-industries/wanderer/compare/v1.1.0...v1.2.0) (2024-09-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Map: Add ability to open jump planner from routes
|
||||
|
||||
## [v1.1.0](https://github.com/wanderer-industries/wanderer/compare/v1.0.23...v1.1.0) (2024-09-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Map: Add highlighting for imperial space systems depends on faction
|
||||
|
||||
## [v1.0.23](https://github.com/wanderer-industries/wanderer/compare/v1.0.22...v1.0.23) (2024-09-25)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Main map doesn't load back after refreshing/switching pages
|
||||
|
||||
## [v1.0.22](https://github.com/wanderer-industries/wanderer/compare/v1.0.21...v1.0.22) (2024-09-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Map: Main map doesn't load back after refreshing/switching pages
|
||||
|
||||
## [v1.0.21](https://github.com/wanderer-industries/wanderer/compare/v1.0.20...v1.0.21) (2024-09-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Map: Main map doesn't load back after refreshing/switching pages
|
||||
|
||||
## [v1.0.20](https://github.com/wanderer-industries/wanderer/compare/v1.0.19...v1.0.20) (2024-09-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* core: Small fixes & improvements
|
||||
|
||||
## [v1.0.19](https://github.com/wanderer-industries/wanderer/compare/v1.0.18...v1.0.19) (2024-09-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ACL: Fix adding empty members list
|
||||
|
||||
## [v1.0.18](https://github.com/wanderer-industries/wanderer/compare/v1.0.17...v1.0.18) (2024-09-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* ACL: Cant delete ACL list after map deletion #5
|
||||
|
||||
## [v1.0.16](https://github.com/wanderer-industries/wanderer/compare/v1.0.15...v1.0.16) (2024-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Map: commented console log
|
||||
|
||||
* Map: add console log for check sys loading
|
||||
|
||||
* Map: add key for cache changes detecting
|
||||
|
||||
## [v1.0.15](https://github.com/wanderer-industries/wanderer/compare/v1.0.14...v1.0.15) (2024-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* map: Show a proper user notification if map was deleted/archived
|
||||
|
||||
## [v1.0.14](https://github.com/wanderer-industries/wanderer/compare/v1.0.13...v1.0.14) (2024-09-21)
|
||||
|
||||
## [v1.0.13](https://github.com/wanderer-industries/wanderer/compare/v1.0.12...v1.0.13) (2024-09-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* tracking: Ensure user has at least one character tracked to work with map
|
||||
|
||||
## [v1.0.12](https://github.com/wanderer-industries/wanderer/compare/v1.0.11...v1.0.12) (2024-09-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* audit: Hide character for non-character map activities
|
||||
|
||||
## [v1.0.11](https://github.com/wanderer-industries/wanderer/compare/v1.0.10...v1.0.11) (2024-09-20)
|
||||
|
||||
## [v1.0.10](https://github.com/wanderer-industries/wanderer/compare/v1.0.9...v1.0.10) (2024-09-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* signatures: Fix update signatures error if no character tracked on map
|
||||
|
||||
## [v1.0.9](https://github.com/wanderer-industries/wanderer/compare/v1.0.8...v1.0.9) (2024-09-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* core: Fix system add error if it's already added on map
|
||||
|
||||
## [v1.0.8](https://github.com/wanderer-industries/wanderer/compare/v1.0.7...v1.0.8) (2024-09-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* docker: Fix DB connection in docker-compose internal network
|
||||
|
||||
## [v1.0.7](https://github.com/wanderer-industries/wanderer/compare/v1.0.6...v1.0.7) (2024-09-19)
|
||||
|
||||
## [v1.0.6](https://github.com/wanderer-industries/wanderer/compare/v1.0.5...v1.0.6) (2024-09-18)
|
||||
|
||||
## [v1.0.5](https://github.com/wanderer-industries/wanderer/compare/v1.0.4...v1.0.5) (2024-09-18)
|
||||
|
||||
## [v1.0.4](https://github.com/wanderer-industries/wanderer/compare/v1.0.3...v1.0.4) (2024-09-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* core: skip search results for failed character info request
|
||||
|
||||
## [v1.0.3](https://github.com/wanderer-industries/wanderer/compare/v1.0.2...v1.0.3) (2024-09-18)
|
||||
|
||||
## [v1.0.2](https://github.com/wanderer-industries/wanderer/compare/v1.0.1...v1.0.2) (2024-09-18)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.0.1](https://github.com/wanderer-industries/wanderer/compare/v1.0.0...v1.0.1) (2024-09-18)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Interested to learn more? [Check more on our website](https://wanderer.ltd/news)
|
||||
|
||||
### Can Wanderer be self-hosted?
|
||||
|
||||
Wanderer is [open source](https://wanderer.ltd/open-source-website-analytics) and we have a free as in beer and self-hosted solution called [Wanderer Community Edition (CE)](https://wanderer.ltd/news/self-hosted). Here are the differences between Wanderer and Wanderer CE:
|
||||
Wanderer is open source project and we have a free as in beer and self-hosted solution called [Wanderer Community Edition (CE)](https://wanderer.ltd/news/community-edition). Here are the differences between Wanderer and Wanderer CE:
|
||||
|
||||
| | Wanderer Cloud | Wanderer Community Edition |
|
||||
| ------------- | ------------- | ------------- |
|
||||
|
||||
@@ -67,3 +67,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.p-sortable-column {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.p-selectable-row td {
|
||||
padding: 4px 4px;
|
||||
}
|
||||
|
||||
.p-sortable-column > .p-column-header-content > span:last-child {
|
||||
transform: scale(0.7);
|
||||
|
||||
& > svg {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,17 +8,21 @@ import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components';
|
||||
import { useJumpPlannerMenu } from '@/hooks/Mapper/components/contexts/hooks/useJumpPlannerMenu';
|
||||
import { Route } from '@/hooks/Mapper/types/routes.ts';
|
||||
|
||||
export interface ContextMenuSystemInfoProps {
|
||||
systemStatics: Map<number, SolarSystemStaticInfoRaw>;
|
||||
hubs: string[];
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
systemId: string | undefined;
|
||||
systemIdFrom?: string | undefined;
|
||||
systems: SolarSystemRawType[];
|
||||
onOpenSettings(): void;
|
||||
onHubToggle(): void;
|
||||
onAddSystem(): void;
|
||||
onWaypointSet: WaypointSetContextHandler;
|
||||
routes: Route[];
|
||||
}
|
||||
|
||||
export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
@@ -30,9 +34,12 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
onAddSystem,
|
||||
onWaypointSet,
|
||||
systemId,
|
||||
systemIdFrom,
|
||||
hubs,
|
||||
routes,
|
||||
}) => {
|
||||
const getWaypointMenu = useWaypointMenu(onWaypointSet);
|
||||
const getJumpPlannerMenu = useJumpPlannerMenu(systems, systemIdFrom);
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined;
|
||||
@@ -55,7 +62,9 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{ separator: true },
|
||||
...getJumpPlannerMenu(system, routes),
|
||||
...getWaypointMenu(systemId, system.system_class),
|
||||
{
|
||||
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes',
|
||||
@@ -72,7 +81,17 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}, [systemId, systemStatics, systems, getWaypointMenu, hubs, onHubToggle, onAddSystem, onOpenSettings]);
|
||||
}, [
|
||||
systemId,
|
||||
systemStatics,
|
||||
systems,
|
||||
getJumpPlannerMenu,
|
||||
getWaypointMenu,
|
||||
hubs,
|
||||
onHubToggle,
|
||||
onAddSystem,
|
||||
onOpenSettings,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Commands, MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Ma
|
||||
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import * as React from 'react';
|
||||
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
|
||||
interface UseContextMenuSystemHandlersProps {
|
||||
hubs: string[];
|
||||
@@ -15,16 +16,21 @@ export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand, mapRef }: U
|
||||
const contextMenuRef = useRef<ContextMenu | null>(null);
|
||||
|
||||
const [system, setSystem] = useState<string>();
|
||||
const routeRef = useRef<(SolarSystemStaticInfoRaw | undefined)[]>([]);
|
||||
|
||||
const ref = useRef({ hubs, system, outCommand, mapRef });
|
||||
ref.current = { hubs, system, outCommand, mapRef };
|
||||
|
||||
const open = useCallback((ev: React.SyntheticEvent, systemId: string) => {
|
||||
setSystem(systemId);
|
||||
ev.preventDefault();
|
||||
ctxManager.next('ctxSysInfo', contextMenuRef.current);
|
||||
contextMenuRef.current?.show(ev);
|
||||
}, []);
|
||||
const open = useCallback(
|
||||
(ev: React.SyntheticEvent, systemId: string, route: (SolarSystemStaticInfoRaw | undefined)[]) => {
|
||||
setSystem(systemId);
|
||||
routeRef.current = route;
|
||||
ev.preventDefault();
|
||||
ctxManager.next('ctxSysInfo', contextMenuRef.current);
|
||||
contextMenuRef.current?.show(ev);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onHubToggle = useCallback(() => {
|
||||
const { hubs, system, outCommand } = ref.current;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './useJumpPlannerMenu.tsx';
|
||||
@@ -0,0 +1,129 @@
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback } from 'react';
|
||||
import { isPossibleSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
|
||||
import { Route } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { SolarSystemRawType, SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { SOLAR_SYSTEM_CLASS_IDS } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
|
||||
const imperialSpace = [SOLAR_SYSTEM_CLASS_IDS.hs, SOLAR_SYSTEM_CLASS_IDS.ls, SOLAR_SYSTEM_CLASS_IDS.ns];
|
||||
const criminalSpace = [SOLAR_SYSTEM_CLASS_IDS.ls, SOLAR_SYSTEM_CLASS_IDS.ns];
|
||||
|
||||
enum JUMP_SHIP_TYPE {
|
||||
BLACK_OPS = 'Marshal',
|
||||
JUMP_FREIGHTER = 'Anshar',
|
||||
RORQUAL = 'Rorqual',
|
||||
CAPITAL = 'Thanatos',
|
||||
SUPER_CAPITAL = 'Avatar',
|
||||
}
|
||||
|
||||
export const openJumpPlan = (jumpShipType: JUMP_SHIP_TYPE, from: string, to: string) => {
|
||||
return window.open(`https://evemaps.dotlan.net/jump/${jumpShipType},544/${from}:${to}`, '_blank');
|
||||
};
|
||||
|
||||
const BRACKET_ICONS = {
|
||||
npcsuperCarrier_32: '/icons/brackets/npcsuperCarrier_32.png',
|
||||
carrier_32: '/icons/brackets/carrier_32.png',
|
||||
battleship_32: '/icons/brackets/battleship_32.png',
|
||||
freighter_32: '/icons/brackets/freighter_32.png',
|
||||
};
|
||||
|
||||
const renderIcon = (icon: string) => {
|
||||
return (
|
||||
<div className="flex justify-center items-center mr-1.5 pt-px">
|
||||
<img src={icon} style={{ width: 20, height: 20 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useJumpPlannerMenu = (
|
||||
systems: SolarSystemRawType[],
|
||||
systemIdFrom?: string | undefined,
|
||||
): ((systemId: SolarSystemStaticInfoRaw, routes: Route[]) => MenuItem[]) => {
|
||||
return useCallback(
|
||||
(destination: SolarSystemStaticInfoRaw) => {
|
||||
if (!destination || !systemIdFrom) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const origin = getSystemById(systems, systemIdFrom)?.system_static_info;
|
||||
|
||||
if (!origin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isShowBOorJumpFreighter =
|
||||
isPossibleSpace(imperialSpace, origin.system_class) && isPossibleSpace(criminalSpace, destination.system_class);
|
||||
|
||||
const isShowCapital =
|
||||
isPossibleSpace(criminalSpace, origin.system_class) && isPossibleSpace(criminalSpace, destination.system_class);
|
||||
|
||||
if (!isShowBOorJumpFreighter && !isShowCapital) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'In Jump Planner',
|
||||
icon: PrimeIcons.SEND,
|
||||
items: [
|
||||
...(isShowBOorJumpFreighter
|
||||
? [
|
||||
{
|
||||
label: 'Black Ops',
|
||||
icon: renderIcon(BRACKET_ICONS.battleship_32),
|
||||
command: () => {
|
||||
openJumpPlan(JUMP_SHIP_TYPE.BLACK_OPS, origin.solar_system_name, destination.solar_system_name);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Jump Freighter',
|
||||
icon: renderIcon(BRACKET_ICONS.freighter_32),
|
||||
command: () => {
|
||||
openJumpPlan(
|
||||
JUMP_SHIP_TYPE.JUMP_FREIGHTER,
|
||||
origin.solar_system_name,
|
||||
destination.solar_system_name,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Rorqual',
|
||||
icon: renderIcon(BRACKET_ICONS.freighter_32),
|
||||
command: () => {
|
||||
openJumpPlan(JUMP_SHIP_TYPE.RORQUAL, origin.solar_system_name, destination.solar_system_name);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
...(isShowCapital
|
||||
? [
|
||||
{
|
||||
label: 'Capital',
|
||||
icon: renderIcon(BRACKET_ICONS.carrier_32),
|
||||
command: () => {
|
||||
openJumpPlan(JUMP_SHIP_TYPE.CAPITAL, origin.solar_system_name, destination.solar_system_name);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Super Capital',
|
||||
icon: renderIcon(BRACKET_ICONS.npcsuperCarrier_32),
|
||||
command: () => {
|
||||
openJumpPlan(
|
||||
JUMP_SHIP_TYPE.SUPER_CAPITAL,
|
||||
origin.solar_system_name,
|
||||
destination.solar_system_name,
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
[systems, systemIdFrom],
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { ForwardedRef, forwardRef, MouseEvent, useCallback } from 'react';
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
ConnectionMode,
|
||||
@@ -94,6 +94,7 @@ interface MapCompProps {
|
||||
minimapClasses?: string;
|
||||
isShowMinimap?: boolean;
|
||||
onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void;
|
||||
showKSpaceBG?: boolean;
|
||||
}
|
||||
|
||||
const MapComp = ({
|
||||
@@ -105,6 +106,7 @@ const MapComp = ({
|
||||
onConnectionInfoClick,
|
||||
onSelectionContextMenu,
|
||||
isShowMinimap,
|
||||
showKSpaceBG,
|
||||
}: MapCompProps) => {
|
||||
const [nodes, , onNodesChange] = useNodesState<SolarSystemRawType>(initialNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>[]>(initialEdges);
|
||||
@@ -169,6 +171,13 @@ const MapComp = ({
|
||||
localStorage.setItem(SESSION_KEY.viewPort, JSON.stringify(viewport));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
update(x => ({
|
||||
...x,
|
||||
showKSpaceBG: showKSpaceBG,
|
||||
}));
|
||||
}, [showKSpaceBG, update]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.MapRoot}>
|
||||
|
||||
@@ -7,6 +7,7 @@ export type MapData = MapUnionTypes & {
|
||||
isConnecting: boolean;
|
||||
hoverNodeId: string | null;
|
||||
visibleNodes: Set<string>;
|
||||
showKSpaceBG: boolean;
|
||||
};
|
||||
|
||||
interface MapProviderProps {
|
||||
@@ -27,6 +28,7 @@ const INITIAL_DATA: MapData = {
|
||||
connections: [],
|
||||
hoverNodeId: null,
|
||||
visibleNodes: new Set(),
|
||||
showKSpaceBG: false,
|
||||
};
|
||||
|
||||
export interface MapContextProps {
|
||||
@@ -38,6 +40,7 @@ export interface MapContextProps {
|
||||
const MapContext = createContext<MapContextProps>({
|
||||
update: () => {},
|
||||
data: { ...INITIAL_DATA },
|
||||
// @ts-ignore
|
||||
outCommand: async () => void 0,
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,62 @@ $tooltip-bg: #202020; // Темный фон для подсказок
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
&.Mataria, &.Amarria, &.Gallente, &.Caldaria {
|
||||
&::Before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
z-index: -1;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Mataria {
|
||||
&::before {
|
||||
background-image: url("/images/mataria.png");
|
||||
opacity: 0.6;
|
||||
background-position-x: -28px;
|
||||
background-position-y: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Caldaria {
|
||||
&::before {
|
||||
background-image: url("/images/caldaria.png");
|
||||
opacity: 0.6;
|
||||
background-position-x: -16px;
|
||||
background-position-y: -17px;
|
||||
}
|
||||
}
|
||||
|
||||
&.Amarria {
|
||||
&::before {
|
||||
opacity: 0.45;
|
||||
background-image: url("/images/amarr.png");
|
||||
background-position-x: 0px;
|
||||
background-position-y: -1px;
|
||||
width: calc(100% + 10px)
|
||||
}
|
||||
}
|
||||
|
||||
&.Gallente {
|
||||
&::before {
|
||||
opacity: 0.6;
|
||||
background-image: url("/images/gallente.png");
|
||||
background-position-x: -1px;
|
||||
background-position-y: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
|
||||
@@ -19,6 +19,14 @@ import { PrimeIcons } from 'primereact/api';
|
||||
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick.ts';
|
||||
import { REGIONS_MAP, Spaces } from '@/hooks/Mapper/constants';
|
||||
|
||||
const SpaceToClass: Record<string, string> = {
|
||||
[Spaces.Caldari]: classes.Caldaria,
|
||||
[Spaces.Matar]: classes.Mataria,
|
||||
[Spaces.Amarr]: classes.Amarria,
|
||||
[Spaces.Gallente]: classes.Gallente,
|
||||
};
|
||||
|
||||
const sortedLabels = (labels: string[]) => {
|
||||
if (labels === null) {
|
||||
@@ -50,6 +58,7 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
|
||||
statics,
|
||||
effect_name,
|
||||
region_name,
|
||||
region_id,
|
||||
is_shattered,
|
||||
solar_system_name,
|
||||
} = data.system_static_info;
|
||||
@@ -69,6 +78,7 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
|
||||
isConnecting,
|
||||
hoverNodeId,
|
||||
visibleNodes,
|
||||
showKSpaceBG,
|
||||
},
|
||||
outCommand,
|
||||
} = useMapState();
|
||||
@@ -114,6 +124,9 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
|
||||
|
||||
const showHandlers = isConnecting || hoverNodeId === id;
|
||||
|
||||
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
|
||||
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{visible && (
|
||||
@@ -147,7 +160,11 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx(classes.RootCustomNode, classes[STATUS_CLASSES[status]], { [classes.selected]: selected })}>
|
||||
<div
|
||||
className={clsx(classes.RootCustomNode, regionClass, classes[STATUS_CLASSES[status]], {
|
||||
[classes.selected]: selected,
|
||||
})}
|
||||
>
|
||||
{visible && (
|
||||
<>
|
||||
<div className={classes.HeadRow}>
|
||||
@@ -183,7 +200,13 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
|
||||
)}
|
||||
|
||||
{!isWormhole && !customName && (
|
||||
<div className="text-stone-400 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5">
|
||||
<div
|
||||
className={clsx('text-stone-400 whitespace-nowrap overflow-hidden text-ellipsis mr-0.5', {
|
||||
['text-teal-100 font-bold']: space === Spaces.Caldari,
|
||||
['text-yellow-100 font-bold']: space === Spaces.Amarr || space === Spaces.Matar,
|
||||
['text-lime-200/80 font-bold']: space === Spaces.Gallente,
|
||||
})}
|
||||
>
|
||||
{region_name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,3 +11,7 @@ export const isKnownSpace = (wormholeClassID: number) => {
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isPossibleSpace = (spaces: number[], wormholeClassID: number) => {
|
||||
return spaces.includes(wormholeClassID);
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const RoutesWidgetContent = () => {
|
||||
|
||||
const { loading } = useLoadRoutes();
|
||||
|
||||
const { systems: systemStatics, loadSystems } = useLoadSystemStatic({ systems: hubs ?? [] });
|
||||
const { systems: systemStatics, loadSystems, lastUpdateKey } = useLoadSystemStatic({ systems: hubs ?? [] });
|
||||
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers({
|
||||
outCommand,
|
||||
hubs,
|
||||
@@ -51,9 +51,10 @@ export const RoutesWidgetContent = () => {
|
||||
|
||||
return { ...systemStatics.get(parseInt(x))!, ...(sys && { customName: sys.name ?? '' }) };
|
||||
});
|
||||
}, [hubs, systems, systemStatics]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hubs, systems, systemStatics, lastUpdateKey]);
|
||||
|
||||
const preparedRoutes = useMemo(() => {
|
||||
const preparedRoutes: Route[] = useMemo(() => {
|
||||
return (
|
||||
routes?.routes
|
||||
.sort(sortByDist)
|
||||
@@ -70,15 +71,17 @@ export const RoutesWidgetContent = () => {
|
||||
);
|
||||
}, [routes?.routes, routes?.systems_static_data, systemId]);
|
||||
|
||||
const refData = useRef({ open, loadSystems });
|
||||
refData.current = { open, loadSystems };
|
||||
const refData = useRef({ open, loadSystems, preparedRoutes });
|
||||
refData.current = { open, loadSystems, preparedRoutes };
|
||||
|
||||
useEffect(() => {
|
||||
(async () => await refData.current.loadSystems(hubs))();
|
||||
}, [hubs]);
|
||||
|
||||
const handleClick = useCallback((e: MouseEvent, systemId: string) => {
|
||||
refData.current.open(e, systemId);
|
||||
const route = refData.current.preparedRoutes.find(x => x.destination.toString() === systemId);
|
||||
|
||||
refData.current.open(e, systemId, route?.mapped_systems ?? []);
|
||||
}, []);
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
@@ -114,6 +117,10 @@ export const RoutesWidgetContent = () => {
|
||||
{preparedRoutes.map(route => {
|
||||
const sys = preparedHubs.find(x => x.solar_system_id === route.destination)!;
|
||||
|
||||
// TODO do not delte this console log
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('JOipP', `Check sys [${route.destination}]:`, sys);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2 items-center">
|
||||
@@ -141,7 +148,14 @@ export const RoutesWidgetContent = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ContextMenuSystemInfo hubs={hubs} systems={systems} systemStatics={systemStatics} {...systemCtxProps} />
|
||||
<ContextMenuSystemInfo
|
||||
hubs={hubs}
|
||||
routes={preparedRoutes}
|
||||
systems={systems}
|
||||
systemStatics={systemStatics}
|
||||
systemIdFrom={systemId}
|
||||
{...systemCtxProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,3 +4,7 @@
|
||||
font-size: 12px !important;
|
||||
line-height: 8px;
|
||||
}
|
||||
|
||||
.Table {
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { parseSignatures } from '@/hooks/Mapper/helpers';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
import { DataTable, DataTableRowMouseEvent } from 'primereact/datatable';
|
||||
import { DataTable, DataTableRowMouseEvent, SortOrder } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import useRefState from 'react-usestateref';
|
||||
@@ -25,6 +25,18 @@ import {
|
||||
renderName,
|
||||
renderTimeLeft,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
// import { PrimeIcons } from 'primereact/api';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
type SystemSignaturesSortSettings = {
|
||||
sortField: string;
|
||||
sortOrder: SortOrder;
|
||||
};
|
||||
|
||||
const SORT_DEFAULT_VALUES: SystemSignaturesSortSettings = {
|
||||
sortField: 'updated_at',
|
||||
sortOrder: -1,
|
||||
};
|
||||
|
||||
interface SystemSignaturesContentProps {
|
||||
systemId: string;
|
||||
@@ -39,6 +51,10 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
|
||||
|
||||
const [hoveredSig, setHoveredSig] = useState<SystemSignature | null>(null);
|
||||
|
||||
const [sortSettings, setSortSettings] = useLocalStorageState<SystemSignaturesSortSettings>('window:signatures:sort', {
|
||||
defaultValue: SORT_DEFAULT_VALUES,
|
||||
});
|
||||
|
||||
const tableRef = useRef<HTMLDivElement>(null);
|
||||
const compact = useMaxWidth(tableRef, 260);
|
||||
const medium = useMaxWidth(tableRef, 380);
|
||||
@@ -50,7 +66,7 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
|
||||
const handleResize = useCallback(() => {
|
||||
if (tableRef.current) {
|
||||
const tableWidth = tableRef.current.offsetWidth;
|
||||
const otherColumnsWidth = 265;
|
||||
const otherColumnsWidth = 276;
|
||||
const availableWidth = tableWidth - otherColumnsWidth;
|
||||
setNameColumnWidth(`${availableWidth}px`);
|
||||
}
|
||||
@@ -159,6 +175,14 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
|
||||
setHoveredSig(null);
|
||||
}, []);
|
||||
|
||||
// const renderToolbar = (/*row: SystemSignature*/) => {
|
||||
// return (
|
||||
// <div className="flex justify-end items-center gap-2">
|
||||
// <span className={clsx(PrimeIcons.PENCIL, 'text-[10px]')}></span>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
return (
|
||||
<div ref={tableRef} className="h-full">
|
||||
{filteredSignatures.length === 0 ? (
|
||||
@@ -168,19 +192,23 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
|
||||
) : (
|
||||
<>
|
||||
<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
|
||||
resizableColumns={false}
|
||||
rowHover
|
||||
selectAll
|
||||
showHeaders={false}
|
||||
onRowMouseEnter={handleEnterRow}
|
||||
onRowMouseLeave={handleLeaveRow}
|
||||
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');
|
||||
@@ -198,7 +226,7 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
|
||||
bodyClassName="p-0 px-1"
|
||||
field="group"
|
||||
body={renderIcon}
|
||||
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
|
||||
style={{ maxWidth: 26, minWidth: 26, width: 26, height: 25 }}
|
||||
></Column>
|
||||
|
||||
<Column
|
||||
@@ -206,12 +234,14 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
|
||||
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"
|
||||
@@ -220,6 +250,7 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
|
||||
body={renderName}
|
||||
style={{ maxWidth: nameColumnWidth }}
|
||||
hidden={compact || medium}
|
||||
sortable
|
||||
></Column>
|
||||
<Column
|
||||
field="updated_at"
|
||||
@@ -227,7 +258,16 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TimeLeft } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
export const renderTimeLeft = (row: SystemSignature) => {
|
||||
return (
|
||||
<div className="flex justify-end w-full items-center">
|
||||
<div className="flex w-full items-center">
|
||||
<TimeLeft cDate={row.updated_at ? new Date(row.updated_at) : undefined} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,8 @@ interface RightBarProps {
|
||||
export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
|
||||
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
|
||||
|
||||
const isShowMinimap = interfaceSettings.isShowMinimap === undefined ? true : interfaceSettings.isShowMinimap;
|
||||
|
||||
const handleAddCharacter = useCallback(() => {
|
||||
outCommand({
|
||||
type: OutCommand.addCharacter,
|
||||
@@ -27,6 +29,13 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
|
||||
}));
|
||||
}, [setInterfaceSettings]);
|
||||
|
||||
const toggleKSpace = useCallback(() => {
|
||||
setInterfaceSettings(x => ({
|
||||
...x,
|
||||
isShowKSpace: !x.isShowKSpace,
|
||||
}));
|
||||
}, [setInterfaceSettings]);
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setInterfaceSettings(x => ({
|
||||
...x,
|
||||
@@ -67,19 +76,31 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
|
||||
|
||||
<div className="flex flex-col items-center mb-2 gap-1">
|
||||
<WdTooltipWrapper
|
||||
content={interfaceSettings.isShowMinimap ? 'Hide minimap' : 'Show minimap'}
|
||||
content={
|
||||
interfaceSettings.isShowKSpace ? 'Hide highlighting Imperial Space' : 'Show highlighting Imperial Space'
|
||||
}
|
||||
position={TooltipPosition.left}
|
||||
>
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
type="button"
|
||||
onClick={toggleKSpace}
|
||||
>
|
||||
{interfaceSettings.isShowKSpace ? (
|
||||
<i className="pi pi-star-fill text-lg"></i>
|
||||
) : (
|
||||
<i className="pi pi-star text-lg"></i>
|
||||
)}
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
<WdTooltipWrapper content={isShowMinimap ? 'Hide minimap' : 'Show minimap'} position={TooltipPosition.left}>
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
type="button"
|
||||
onClick={toggleMinimap}
|
||||
>
|
||||
{interfaceSettings.isShowMinimap ? (
|
||||
<i className="pi pi-eye text-lg"></i>
|
||||
) : (
|
||||
<i className="pi pi-eye-slash text-lg"></i>
|
||||
)}
|
||||
{isShowMinimap ? <i className="pi pi-eye text-lg"></i> : <i className="pi pi-eye-slash text-lg"></i>}
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => {
|
||||
update,
|
||||
outCommand,
|
||||
data: { selectedConnections, selectedSystems, hubs, systems },
|
||||
interfaceSettings: { isShowMenu, isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap },
|
||||
interfaceSettings: { isShowMenu, isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap, isShowKSpace },
|
||||
} = useMapRootState();
|
||||
|
||||
const { open, ...systemContextProps } = useContextMenuSystemHandlers({ systems, hubs, outCommand });
|
||||
@@ -99,6 +99,7 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => {
|
||||
onSelectionContextMenu={handleSystemMultipleContext}
|
||||
minimapClasses={!isShowMenu ? classes.MiniMap : undefined}
|
||||
isShowMinimap={isShowMinimap}
|
||||
showKSpaceBG={isShowKSpace}
|
||||
/>
|
||||
|
||||
{openSettings != null && (
|
||||
|
||||
@@ -5,3 +5,62 @@ export enum SESSION_KEY {
|
||||
}
|
||||
|
||||
export const GRADIENT_MENU_ACTIVE_CLASSES = 'bg-gradient-to-br from-transparent/10 to-fuchsia-300/10';
|
||||
|
||||
export enum Regions {
|
||||
Derelik = 10000001,
|
||||
TheForge = 10000002,
|
||||
Lonetrek = 10000016,
|
||||
SinqLaison = 10000032,
|
||||
Aridia = 10000054,
|
||||
BlackRise = 10000069,
|
||||
TheBleakLands = 10000038,
|
||||
TheCitadel = 10000033,
|
||||
Devoid = 10000036,
|
||||
Domain = 10000043,
|
||||
Essence = 10000064,
|
||||
Everyshore = 10000037,
|
||||
Genesis = 10000067,
|
||||
Heimatar = 10000030,
|
||||
Kador = 10000052,
|
||||
Khanid = 10000049,
|
||||
KorAzor = 10000065,
|
||||
Metropolis = 10000042,
|
||||
MoldenHeath = 10000028,
|
||||
Placid = 10000048,
|
||||
Solitude = 10000044,
|
||||
TashMurkon = 10000020,
|
||||
VergeVendor = 10000068,
|
||||
}
|
||||
|
||||
export enum Spaces {
|
||||
'Caldari' = 'Caldari',
|
||||
'Gallente' = 'Gallente',
|
||||
'Matar' = 'Matar',
|
||||
'Amarr' = 'Amarr',
|
||||
}
|
||||
|
||||
export const REGIONS_MAP: Record<number, Spaces> = {
|
||||
[Regions.Derelik]: Spaces.Amarr,
|
||||
[Regions.TheForge]: Spaces.Caldari,
|
||||
[Regions.Lonetrek]: Spaces.Caldari,
|
||||
[Regions.SinqLaison]: Spaces.Gallente,
|
||||
[Regions.Aridia]: Spaces.Amarr,
|
||||
[Regions.BlackRise]: Spaces.Caldari,
|
||||
[Regions.TheBleakLands]: Spaces.Amarr,
|
||||
[Regions.TheCitadel]: Spaces.Caldari,
|
||||
[Regions.Devoid]: Spaces.Amarr,
|
||||
[Regions.Domain]: Spaces.Amarr,
|
||||
[Regions.Essence]: Spaces.Gallente,
|
||||
[Regions.Everyshore]: Spaces.Gallente,
|
||||
[Regions.Genesis]: Spaces.Amarr,
|
||||
[Regions.Heimatar]: Spaces.Matar,
|
||||
[Regions.Kador]: Spaces.Amarr,
|
||||
[Regions.Khanid]: Spaces.Amarr,
|
||||
[Regions.KorAzor]: Spaces.Amarr,
|
||||
[Regions.Metropolis]: Spaces.Matar,
|
||||
[Regions.MoldenHeath]: Spaces.Matar,
|
||||
[Regions.Placid]: Spaces.Gallente,
|
||||
[Regions.Solitude]: Spaces.Gallente,
|
||||
[Regions.TashMurkon]: Spaces.Amarr,
|
||||
[Regions.VergeVendor]: Spaces.Gallente,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import Mapper from './MapRoot';
|
||||
import { decompressToJson } from './utils';
|
||||
|
||||
export default {
|
||||
_rootEl: null,
|
||||
@@ -23,22 +22,17 @@ export default {
|
||||
onError: handleError,
|
||||
});
|
||||
|
||||
this.pushEvent('loaded');
|
||||
this.pushEvent('ui_loaded');
|
||||
},
|
||||
|
||||
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) {
|
||||
|
||||
@@ -30,11 +30,13 @@ const INITIAL_DATA: MapRootData = {
|
||||
type InterfaceStoredSettings = {
|
||||
isShowMenu: boolean;
|
||||
isShowMinimap: boolean;
|
||||
isShowKSpace: boolean;
|
||||
};
|
||||
|
||||
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
|
||||
isShowMenu: false,
|
||||
isShowMinimap: true,
|
||||
isShowKSpace: false,
|
||||
};
|
||||
|
||||
export interface MapRootContextProps {
|
||||
@@ -50,6 +52,7 @@ const MapRootContext = createContext<MapRootContextProps>({
|
||||
update: () => {},
|
||||
data: { ...INITIAL_DATA },
|
||||
mapRef: { current: null },
|
||||
// @ts-ignore
|
||||
outCommand: async () => void 0,
|
||||
interfaceSettings: STORED_INTERFACE_DEFAULT_VALUES,
|
||||
setInterfaceSettings: () => null,
|
||||
|
||||
@@ -27,12 +27,14 @@ interface UseLoadSystemStaticProps {
|
||||
export const useLoadSystemStatic = ({ systems }: UseLoadSystemStaticProps) => {
|
||||
const { outCommand } = useMapRootState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lastUpdateKey, setLastUpdateKey] = useState(0);
|
||||
|
||||
const ref = useRef({ outCommand });
|
||||
ref.current = { outCommand };
|
||||
|
||||
const addSystemStatic = useCallback((static_info: SolarSystemStaticInfoRaw) => {
|
||||
cache.set(static_info.solar_system_id, static_info);
|
||||
setLastUpdateKey(new Date().getTime());
|
||||
}, []);
|
||||
|
||||
const loadSystems = useCallback(async (systems: (number | string)[]) => {
|
||||
@@ -43,6 +45,7 @@ export const useLoadSystemStatic = ({ systems }: UseLoadSystemStaticProps) => {
|
||||
if (toLoad.length > 0) {
|
||||
const res = await loadSystemStaticInfo(ref.current.outCommand, toLoad);
|
||||
res.forEach(x => cache.set(x.solar_system_id, x));
|
||||
setLastUpdateKey(new Date().getTime());
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
@@ -52,5 +55,5 @@ export const useLoadSystemStatic = ({ systems }: UseLoadSystemStaticProps) => {
|
||||
// eslint-disable-next-line
|
||||
}, [systems]);
|
||||
|
||||
return { addSystemStatic, systems: cache, loading, loadSystems };
|
||||
return { addSystemStatic, systems: cache, lastUpdateKey, loading, loadSystems };
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
assets/static/icons/brackets/battleship_32.png
Normal file
|
After Width: | Height: | Size: 403 B |
BIN
assets/static/icons/brackets/carrier_32.png
Normal file
|
After Width: | Height: | Size: 340 B |
BIN
assets/static/icons/brackets/freighter_32.png
Normal file
|
After Width: | Height: | Size: 509 B |
BIN
assets/static/icons/brackets/npcsuperCarrier_32.png
Normal file
|
After Width: | Height: | Size: 603 B |
BIN
assets/static/images/amarr.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
assets/static/images/caldaria.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
assets/static/images/gallente.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
assets/static/images/mataria.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
assets/static/images/news/09-19-connection-info/connection.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
assets/static/images/news/09-19-connection-info/cover.png
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
assets/static/images/news/09-19-connection-info/info.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
assets/static/images/news/ce_logo_dark.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
@@ -187,7 +187,7 @@
|
||||
|
||||
"@babel/runtime@^7.12.5":
|
||||
version "7.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz"
|
||||
integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
@@ -484,7 +484,7 @@
|
||||
|
||||
"@reactflow/background@11.3.14":
|
||||
version "11.3.14"
|
||||
resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.3.14.tgz#778ca30174f3de77fc321459ab3789e66e71a699"
|
||||
resolved "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz"
|
||||
integrity sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==
|
||||
dependencies:
|
||||
"@reactflow/core" "11.11.4"
|
||||
@@ -493,7 +493,7 @@
|
||||
|
||||
"@reactflow/controls@11.2.14":
|
||||
version "11.2.14"
|
||||
resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.2.14.tgz#508ed2c40d23341b3b0919dd11e76fd49cf850c7"
|
||||
resolved "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz"
|
||||
integrity sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==
|
||||
dependencies:
|
||||
"@reactflow/core" "11.11.4"
|
||||
@@ -502,7 +502,7 @@
|
||||
|
||||
"@reactflow/core@11.11.4":
|
||||
version "11.11.4"
|
||||
resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.11.4.tgz#89bd86d1862aa1416f3f49926cede7e8c2aab6a7"
|
||||
resolved "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz"
|
||||
integrity sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==
|
||||
dependencies:
|
||||
"@types/d3" "^7.4.0"
|
||||
@@ -517,7 +517,7 @@
|
||||
|
||||
"@reactflow/minimap@11.7.14":
|
||||
version "11.7.14"
|
||||
resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.7.14.tgz#298d7a63cb1da06b2518c99744f716560c88ca73"
|
||||
resolved "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz"
|
||||
integrity sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==
|
||||
dependencies:
|
||||
"@reactflow/core" "11.11.4"
|
||||
@@ -530,7 +530,7 @@
|
||||
|
||||
"@reactflow/node-resizer@2.2.14":
|
||||
version "2.2.14"
|
||||
resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz#1810c0ce51aeb936f179466a6660d1e02c7a77a8"
|
||||
resolved "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz"
|
||||
integrity sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==
|
||||
dependencies:
|
||||
"@reactflow/core" "11.11.4"
|
||||
@@ -541,7 +541,7 @@
|
||||
|
||||
"@reactflow/node-toolbar@1.3.14":
|
||||
version "1.3.14"
|
||||
resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz#c6ffc76f82acacdce654f2160dc9852162d6e7c9"
|
||||
resolved "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz"
|
||||
integrity sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==
|
||||
dependencies:
|
||||
"@reactflow/core" "11.11.4"
|
||||
@@ -964,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.yarnpkg.com/@types/pako/-/pako-2.0.3.tgz#b6993334f3af27c158f3fe0dfeeba987c578afb1"
|
||||
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"
|
||||
@@ -2947,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.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
|
||||
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"
|
||||
@@ -3204,7 +3194,7 @@ react-draggable@^4.0.3, react-draggable@^4.4.5:
|
||||
|
||||
react-error-boundary@^4.0.13:
|
||||
version "4.0.13"
|
||||
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.13.tgz#80386b7b27b1131c5fbb7368b8c0d983354c7947"
|
||||
resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz"
|
||||
integrity sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
@@ -3282,7 +3272,7 @@ react@18.2.0:
|
||||
|
||||
reactflow@^11.10.4:
|
||||
version "11.11.4"
|
||||
resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.4.tgz#e3593e313420542caed81aecbd73fb9bc6576653"
|
||||
resolved "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz"
|
||||
integrity sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==
|
||||
dependencies:
|
||||
"@reactflow/background" "11.3.14"
|
||||
|
||||
@@ -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)$",
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
import Config
|
||||
import WandererApp.ConfigHelpers
|
||||
|
||||
# config/runtime.exs is executed for all environments, including
|
||||
# during releases. It is executed after compilation and before the
|
||||
# system starts, so it is typically used to load production configuration
|
||||
# and secrets from environment variables or elsewhere. Do not define
|
||||
# any compile-time configuration in here, as it won't be applied.
|
||||
# The block below contains prod specific runtime configuration.
|
||||
|
||||
# ## Using releases
|
||||
#
|
||||
# If you use `mix release`, you need to explicitly enable the server
|
||||
# by passing the PHX_SERVER=true when you start it:
|
||||
#
|
||||
# PHX_SERVER=true bin/wanderer_app start
|
||||
#
|
||||
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
|
||||
# script that automatically sets the env var above.
|
||||
if System.get_env("PHX_SERVER") do
|
||||
config :wanderer_app, WandererAppWeb.Endpoint, server: true
|
||||
end
|
||||
@@ -69,6 +53,14 @@ map_subscriptions_enabled =
|
||||
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
map_subscription_characters_limit =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_CHARACTERS_LIMIT", 100)
|
||||
|
||||
map_subscription_hubs_limit =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_HUBS_LIMIT", 10)
|
||||
|
||||
wallet_tracking_enabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_WALLET_TRACKING_ENABLED", "false")
|
||||
@@ -86,6 +78,8 @@ config :wanderer_app,
|
||||
git_sha: System.get_env("GIT_SHA", "111"),
|
||||
custom_route_base_url: System.get_env("CUSTOM_ROUTE_BASE_URL"),
|
||||
invites: System.get_env("WANDERER_INVITES", "false") == "true",
|
||||
admin_username: System.get_env("WANDERER_ADMIN_USERNAME", "admin"),
|
||||
admin_password: System.get_env("WANDERER_ADMIN_PASSWORD"),
|
||||
admins: admins,
|
||||
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
|
||||
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
|
||||
@@ -93,7 +87,13 @@ config :wanderer_app,
|
||||
wallet_tracking_enabled: wallet_tracking_enabled,
|
||||
subscription_settings: %{
|
||||
plans: [
|
||||
%{id: "alpha", characters_limit: 100, hubs_limit: 10, base_price: 0, monthly_discount: 0},
|
||||
%{
|
||||
id: "alpha",
|
||||
characters_limit: map_subscription_characters_limit,
|
||||
hubs_limit: map_subscription_hubs_limit,
|
||||
base_price: 0,
|
||||
monthly_discount: 0
|
||||
},
|
||||
%{
|
||||
id: "omega",
|
||||
characters_limit: 300,
|
||||
@@ -178,10 +178,34 @@ if config_env() == :prod do
|
||||
For example: ecto://USER:PASS@HOST/DATABASE
|
||||
"""
|
||||
|
||||
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
|
||||
maybe_ipv6 =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("ECTO_IPV6", "false")
|
||||
|> String.to_existing_atom()
|
||||
|> case do
|
||||
true -> [:inet6]
|
||||
_ -> []
|
||||
end
|
||||
|
||||
db_ssl_enabled =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("DATABASE_SSL_ENABLED", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
db_ssl_verify_none =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("DATABASE_SSL_VERIFY_NONE", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
client_opts =
|
||||
if db_ssl_verify_none do
|
||||
[verify: :verify_none]
|
||||
end
|
||||
|
||||
config :wanderer_app, WandererApp.Repo,
|
||||
url: database_url,
|
||||
ssl: db_ssl_enabled,
|
||||
ssl_opts: client_opts,
|
||||
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
|
||||
socket_options: maybe_ipv6
|
||||
|
||||
|
||||
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1726871744,
|
||||
"narHash": "sha256-V5LpfdHyQkUF7RfOaDPrZDP+oqz88lTJrMT1+stXNwo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a1d92660c6b3b7c26fb883500a80ea9d33321be2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -43,7 +43,6 @@ defmodule WandererApp.Api.AccessList do
|
||||
primary?(true)
|
||||
|
||||
argument :owner_id, :uuid, allow_nil?: false
|
||||
argument :owner_id_text_input, :string, allow_nil?: true
|
||||
|
||||
change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
|
||||
end
|
||||
@@ -51,8 +50,6 @@ defmodule WandererApp.Api.AccessList do
|
||||
update :update do
|
||||
accept [:name, :description, :owner_id]
|
||||
primary?(true)
|
||||
|
||||
argument :owner_id_text_input, :string, allow_nil?: true
|
||||
end
|
||||
|
||||
update :assign_owner do
|
||||
|
||||
@@ -104,13 +104,21 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
end
|
||||
end
|
||||
|
||||
postgres do
|
||||
references do
|
||||
reference :access_list, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :uniq_acl_character_id, [:access_list_id, :eve_character_id] do
|
||||
pre_check?(true)
|
||||
end
|
||||
identity :uniq_acl_corporation_id, [:access_list_id, :eve_corporation_id] do
|
||||
|
||||
identity :uniq_acl_corporation_id, [:access_list_id, :eve_corporation_id] do
|
||||
pre_check?(true)
|
||||
end
|
||||
|
||||
identity :uniq_acl_alliance_id, [:access_list_id, :eve_alliance_id] do
|
||||
pre_check?(true)
|
||||
end
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -63,7 +64,6 @@ defmodule WandererApp.Api.Map do
|
||||
primary?(true)
|
||||
|
||||
argument :owner_id, :uuid, allow_nil?: false
|
||||
argument :owner_id_text_input, :string, allow_nil?: true
|
||||
argument :create_default_acl, :boolean, allow_nil?: true
|
||||
argument :acls, {:array, :uuid}, allow_nil?: true
|
||||
argument :acls_text_input, :string, allow_nil?: true
|
||||
@@ -113,6 +113,10 @@ defmodule WandererApp.Api.Map do
|
||||
accept [:hubs]
|
||||
end
|
||||
|
||||
update :update_options do
|
||||
accept [:options]
|
||||
end
|
||||
|
||||
update :mark_as_deleted do
|
||||
accept([])
|
||||
|
||||
@@ -168,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
|
||||
|
||||
@@ -44,6 +44,13 @@ defmodule WandererApp.Api.MapAccessList do
|
||||
belongs_to :access_list, WandererApp.Api.AccessList, primary_key?: true, allow_nil?: false
|
||||
end
|
||||
|
||||
postgres do
|
||||
references do
|
||||
reference :map, on_delete: :delete
|
||||
reference :access_list, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
identities do
|
||||
identity :unique_map_acl, [:map_id, :access_list_id] do
|
||||
pre_check?(false)
|
||||
|
||||
@@ -18,10 +18,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
action: :read
|
||||
)
|
||||
|
||||
define(:by_locations,
|
||||
get_by: [:map_id, :solar_system_source, :solar_system_target],
|
||||
action: :read
|
||||
)
|
||||
define(:by_locations, action: :read_by_locations)
|
||||
|
||||
define(:read_by_map, action: :read_by_map)
|
||||
define(:get_link_pairs_advanced, action: :get_link_pairs_advanced)
|
||||
@@ -47,6 +44,19 @@ defmodule WandererApp.Api.MapConnection do
|
||||
filter(expr(map_id == ^arg(:map_id)))
|
||||
end
|
||||
|
||||
read :read_by_locations 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)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
read :get_link_pairs_advanced do
|
||||
argument(:map_id, :string, allow_nil?: false)
|
||||
argument(:include_mass_crit, :boolean, allow_nil?: false)
|
||||
|
||||
@@ -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)}
|
||||
|
||||
_ ->
|
||||
@@ -205,7 +218,13 @@ defmodule WandererApp.Character do
|
||||
end)
|
||||
# 145000 == Timeout in milliseconds
|
||||
|> Enum.map(fn task -> Task.await(task, 145_000) end)
|
||||
|> Enum.map(fn {:ok, result} -> map_function.(result) end)}
|
||||
|> Enum.map(fn result ->
|
||||
case result do
|
||||
{:ok, result} -> map_function.(result)
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(fn result -> not is_nil(result) end)}
|
||||
|
||||
defp _map_alliance_info(info) do
|
||||
%{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -68,13 +68,14 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
state
|
||||
|
||||
false ->
|
||||
WandererApp.Character.update_character_state(character_id, %{opts: opts})
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :started], %{count: 1})
|
||||
|
||||
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
tracked_characters = [character_id | state.characters] |> Enum.uniq()
|
||||
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)
|
||||
|
||||
%{state | characters: tracked_characters}
|
||||
@@ -177,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
|
||||
@@ -204,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
|
||||
@@ -225,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
|
||||
@@ -254,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
|
||||
@@ -282,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
|
||||
@@ -311,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
|
||||
@@ -355,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)
|
||||
)
|
||||
@@ -391,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)
|
||||
)
|
||||
@@ -401,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
|
||||
@@ -11,6 +11,8 @@ defmodule WandererApp.Env do
|
||||
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
|
||||
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
|
||||
def admins, do: get_key(:admins, [])
|
||||
def admin_username, do: get_key(:admin_username)
|
||||
def admin_password, do: get_key(:admin_password)
|
||||
def corp_wallet, do: get_key(:corp_wallet, "")
|
||||
def corp_eve_id, do: get_key(:corp_id, -1)
|
||||
def subscription_settings, do: get_key(:subscription_settings)
|
||||
|
||||
@@ -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"],
|
||||
@@ -229,7 +229,7 @@ defmodule WandererApp.EveDataService do
|
||||
constellation_id = row["constellationID"] |> Integer.parse() |> elem(0)
|
||||
|
||||
{:ok, wormhole_class_id} =
|
||||
_get_wormhole_class_id(
|
||||
get_wormhole_class_id(
|
||||
map_location_wormhole_classes,
|
||||
region_id,
|
||||
constellation_id,
|
||||
@@ -237,16 +237,16 @@ defmodule WandererApp.EveDataService do
|
||||
)
|
||||
|
||||
{:ok, constellation_name} =
|
||||
_get_constellation_name(map_constellations, constellation_id)
|
||||
get_constellation_name(map_constellations, constellation_id)
|
||||
|
||||
{:ok, region_name} = _get_region_name(map_regions, region_id)
|
||||
{:ok, region_name} = get_region_name(map_regions, region_id)
|
||||
|
||||
{:ok, wormhole_class} = _get_wormhole_class(wormhole_classes, wormhole_class_id)
|
||||
{:ok, wormhole_class} = get_wormhole_class(wormhole_classes, wormhole_class_id)
|
||||
|
||||
{:ok, security} = _get_security(row["security"])
|
||||
{:ok, security} = get_security(row["security"])
|
||||
|
||||
{:ok, class_title} =
|
||||
_get_class_title(
|
||||
get_class_title(
|
||||
wormhole_classes_info,
|
||||
wormhole_class_id,
|
||||
security,
|
||||
@@ -270,7 +270,7 @@ defmodule WandererApp.EveDataService do
|
||||
solar_system_id: solar_system_id,
|
||||
solar_system_name: row["solarSystemName"],
|
||||
solar_system_name_lc: row["solarSystemName"] |> String.downcase(),
|
||||
sun_type_id: _get_sun_type_id(row["sunTypeID"]),
|
||||
sun_type_id: get_sun_type_id(row["sunTypeID"]),
|
||||
constellation_name: constellation_name,
|
||||
region_name: region_name,
|
||||
security: security,
|
||||
@@ -279,8 +279,8 @@ defmodule WandererApp.EveDataService do
|
||||
type_description: wormhole_class.title,
|
||||
is_shattered: is_shattered
|
||||
}
|
||||
|> _get_wormhole_data(wormhole_systems, solar_system_id, wormhole_class)
|
||||
|> _get_triglavian_data(triglavian_systems, solar_system_id)
|
||||
|> get_wormhole_data(wormhole_systems, solar_system_id, wormhole_class)
|
||||
|> get_triglavian_data(triglavian_systems, solar_system_id)
|
||||
end
|
||||
)
|
||||
end
|
||||
@@ -332,14 +332,14 @@ defmodule WandererApp.EveDataService do
|
||||
)
|
||||
end
|
||||
|
||||
defp _get_sun_type_id(sun_type_id) do
|
||||
defp get_sun_type_id(sun_type_id) do
|
||||
case sun_type_id do
|
||||
"None" -> 0
|
||||
_ -> sun_type_id |> Integer.parse() |> elem(0)
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_wormhole_data(default_data, wormhole_systems, solar_system_id, wormhole_class) do
|
||||
defp get_wormhole_data(default_data, wormhole_systems, solar_system_id, wormhole_class) do
|
||||
case Enum.find(wormhole_systems, fn system -> system.solar_system_id == solar_system_id end) do
|
||||
nil ->
|
||||
default_data
|
||||
@@ -355,7 +355,7 @@ defmodule WandererApp.EveDataService do
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_triglavian_data(default_data, triglavian_systems, solar_system_id) do
|
||||
defp get_triglavian_data(default_data, triglavian_systems, solar_system_id) do
|
||||
case Enum.find(triglavian_systems, fn system -> system.solar_system_id == solar_system_id end) do
|
||||
nil ->
|
||||
default_data
|
||||
@@ -370,14 +370,30 @@ defmodule WandererApp.EveDataService do
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_security(security) do
|
||||
defp get_security(security) do
|
||||
case security do
|
||||
nil -> {:ok, ""}
|
||||
_ -> {:ok, Decimal.parse(security) |> elem(0) |> Decimal.round(1) |> Decimal.to_string()}
|
||||
_ -> {:ok, String.to_float(security) |> get_true_security() |> Float.to_string(decimals: 1)}
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_class_title(wormhole_classes_info, wormhole_class_id, security, wormhole_class) do
|
||||
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) 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 [
|
||||
wormhole_classes_info.names["hs"],
|
||||
wormhole_classes_info.names["ls"],
|
||||
@@ -391,7 +407,7 @@ defmodule WandererApp.EveDataService do
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_constellation_name(constellations, constellation_id) do
|
||||
defp get_constellation_name(constellations, constellation_id) do
|
||||
case Enum.find(constellations, fn constellation ->
|
||||
constellation.constellation_id == constellation_id
|
||||
end) do
|
||||
@@ -400,24 +416,24 @@ defmodule WandererApp.EveDataService do
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_region_name(regions, region_id) do
|
||||
defp get_region_name(regions, region_id) do
|
||||
case Enum.find(regions, fn region -> region.region_id == region_id end) do
|
||||
nil -> {:ok, ""}
|
||||
region -> {:ok, region.region_name}
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_wormhole_class(wormhole_classes, wormhole_class_id) do
|
||||
defp get_wormhole_class(wormhole_classes, wormhole_class_id) do
|
||||
{:ok,
|
||||
Enum.find(wormhole_classes, fn wormhole_class ->
|
||||
wormhole_class.wormhole_class_id == wormhole_class_id
|
||||
end)}
|
||||
end
|
||||
|
||||
defp _get_wormhole_class_id(_systems, _region_id, _constellation_id, 30_100_000),
|
||||
defp get_wormhole_class_id(_systems, _region_id, _constellation_id, 30_100_000),
|
||||
do: {:ok, 10_100}
|
||||
|
||||
defp _get_wormhole_class_id(systems, region_id, constellation_id, solar_system_id) do
|
||||
defp get_wormhole_class_id(systems, region_id, constellation_id, solar_system_id) do
|
||||
with region <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||
@@ -430,23 +446,23 @@ defmodule WandererApp.EveDataService do
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||
end),
|
||||
wormhole_class_id <- _get_wormhole_class_id(region, constellation, solar_system) do
|
||||
wormhole_class_id <- get_wormhole_class_id(region, constellation, solar_system) do
|
||||
{:ok, wormhole_class_id}
|
||||
else
|
||||
_ -> {:ok, -1}
|
||||
end
|
||||
end
|
||||
|
||||
defp _get_wormhole_class_id(_region, _constellation, solar_system)
|
||||
defp get_wormhole_class_id(_region, _constellation, solar_system)
|
||||
when not is_nil(solar_system),
|
||||
do: solar_system.wormhole_class_id |> Integer.parse() |> elem(0)
|
||||
|
||||
defp _get_wormhole_class_id(_region, constellation, _solar_system)
|
||||
defp get_wormhole_class_id(_region, constellation, _solar_system)
|
||||
when not is_nil(constellation),
|
||||
do: constellation.wormhole_class_id |> Integer.parse() |> elem(0)
|
||||
|
||||
defp _get_wormhole_class_id(region, _constellation, _solar_system) when not is_nil(region),
|
||||
defp get_wormhole_class_id(region, _constellation, _solar_system) when not is_nil(region),
|
||||
do: region.wormhole_class_id |> Integer.parse() |> elem(0)
|
||||
|
||||
defp _get_wormhole_class_id(_region, _constellation, _solar_system), do: -1
|
||||
defp get_wormhole_class_id(_region, _constellation, _solar_system), do: -1
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,8 +10,8 @@ defmodule WandererApp.Map.Manager do
|
||||
alias WandererApp.Map.Server
|
||||
alias WandererApp.Map.ServerSupervisor
|
||||
|
||||
@maps_start_per_second 100
|
||||
@maps_start_interval 1500
|
||||
@maps_start_per_second 5
|
||||
@maps_start_interval 1000
|
||||
@maps_queue :maps_queue
|
||||
@garbage_collection_interval :timer.hours(1)
|
||||
@check_maps_queue_interval :timer.seconds(1)
|
||||
|
||||
@@ -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} ->
|
||||
{
|
||||
|
||||
@@ -75,11 +75,11 @@ defmodule WandererApp.Map.Server do
|
||||
|> map_pid!
|
||||
|> GenServer.call({&Impl.get_characters/1, []}, :timer.minutes(1))
|
||||
|
||||
def add_character(map_id, character) when is_binary(map_id),
|
||||
def add_character(map_id, character, track_character \\ false) when is_binary(map_id),
|
||||
do:
|
||||
map_id
|
||||
|> map_pid!
|
||||
|> GenServer.cast({&Impl.add_character/2, [character]})
|
||||
|> GenServer.cast({&Impl.add_character/3, [character, track_character]})
|
||||
|
||||
def remove_character(map_id, character_id) when is_binary(map_id),
|
||||
do:
|
||||
|
||||
@@ -11,7 +11,8 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
defstruct [
|
||||
:map_id,
|
||||
:rtree_name,
|
||||
map: nil
|
||||
map: nil,
|
||||
map_opts: []
|
||||
]
|
||||
|
||||
# @ccp1 -1
|
||||
@@ -176,38 +177,46 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
def get_characters(%{map_id: map_id} = _state),
|
||||
do: {:ok, map_id |> WandererApp.Map.list_characters()}
|
||||
|
||||
def add_character(%{map_id: map_id} = state, %{id: character_id} = character) do
|
||||
with :ok <- map_id |> WandererApp.Map.add_character(character),
|
||||
{:ok, _} <-
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: character_id,
|
||||
map_id: map_id,
|
||||
tracked: false
|
||||
}),
|
||||
{:ok, character} <- WandererApp.Character.get_character(character_id) do
|
||||
broadcast!(map_id, :character_added, character)
|
||||
def add_character(%{map_id: map_id} = state, %{id: character_id} = character, track_character) do
|
||||
Task.start_link(fn ->
|
||||
with :ok <- map_id |> WandererApp.Map.add_character(character),
|
||||
{:ok, _} <-
|
||||
WandererApp.MapCharacterSettingsRepo.create(%{
|
||||
character_id: character_id,
|
||||
map_id: map_id,
|
||||
tracked: track_character
|
||||
}),
|
||||
{:ok, character} <- WandererApp.Character.get_character(character_id) do
|
||||
broadcast!(map_id, :character_added, character)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
|
||||
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
|
||||
|
||||
state
|
||||
else
|
||||
{:error, error} ->
|
||||
state
|
||||
end
|
||||
:ok
|
||||
else
|
||||
{:error, _error} ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def remove_character(%{map_id: map_id} = state, character_id) do
|
||||
with :ok <- WandererApp.Map.remove_character(map_id, character_id),
|
||||
{:ok, character} <- WandererApp.Character.get_character(character_id) do
|
||||
broadcast!(map_id, :character_removed, character)
|
||||
Task.start_link(fn ->
|
||||
with :ok <- WandererApp.Map.remove_character(map_id, character_id),
|
||||
{:ok, character} <- WandererApp.Character.get_character(character_id) do
|
||||
broadcast!(map_id, :character_removed, character)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :map, :character, :removed], %{count: 1})
|
||||
:telemetry.execute([:wanderer_app, :map, :character, :removed], %{count: 1})
|
||||
|
||||
state
|
||||
else
|
||||
{:error, error} ->
|
||||
state
|
||||
end
|
||||
:ok
|
||||
else
|
||||
{:error, _error} ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def untrack_characters(%{map_id: map_id} = state, characters_ids) do
|
||||
@@ -357,17 +366,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
connections_to_remove
|
||||
|> Enum.each(fn connection ->
|
||||
@logger.debug(fn -> "Removing connection from map: #{inspect(connection)}" end)
|
||||
|
||||
connection
|
||||
|> WandererApp.MapConnectionRepo.destroy!()
|
||||
|> case do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
@logger.error("Failed to remove connection from map: #{inspect(error, pretty: true)}")
|
||||
:ok
|
||||
end
|
||||
WandererApp.MapConnectionRepo.destroy(map_id, connection)
|
||||
end)
|
||||
|
||||
@ddrt.delete(removed_ids, rtree_name)
|
||||
@@ -377,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
|
||||
@@ -797,7 +800,10 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
}
|
||||
end
|
||||
|
||||
def handle_event({ref, _result}, %{map_id: map_id} = state) do
|
||||
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])
|
||||
|
||||
state
|
||||
@@ -836,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(
|
||||
@@ -851,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)
|
||||
|
||||
_ ->
|
||||
@@ -1099,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
|
||||
@@ -1115,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
|
||||
@@ -1167,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
|
||||
@@ -1257,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
|
||||
@@ -1567,8 +1578,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
old_location.solar_system_id
|
||||
) do
|
||||
{:ok, connection} ->
|
||||
connection
|
||||
|> WandererApp.MapConnectionRepo.destroy!()
|
||||
:ok = WandererApp.MapConnectionRepo.destroy(map_id, connection)
|
||||
|
||||
broadcast!(map_id, :remove_connections, [connection])
|
||||
map_id |> WandererApp.Map.remove_connection(connection)
|
||||
@@ -1589,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
|
||||
@@ -1617,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,
|
||||
@@ -1657,44 +1672,48 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
{:ok, solar_system_info} =
|
||||
WandererApp.Api.MapSolarSystem.by_solar_system_id(location.solar_system_id)
|
||||
|
||||
{:ok, new_system} =
|
||||
WandererApp.MapSystemRepo.create(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: location.solar_system_id,
|
||||
name: solar_system_info.solar_system_name,
|
||||
position_x: position.x,
|
||||
position_y: position.y
|
||||
})
|
||||
WandererApp.MapSystemRepo.create(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: location.solar_system_id,
|
||||
name: solar_system_info.solar_system_name,
|
||||
position_x: position.x,
|
||||
position_y: position.y
|
||||
})
|
||||
|> case do
|
||||
{:ok, new_system} ->
|
||||
@ddrt.insert(
|
||||
{new_system.solar_system_id,
|
||||
WandererApp.Map.PositionCalculator.get_system_bounding_rect(new_system)},
|
||||
rtree_name
|
||||
)
|
||||
|
||||
@ddrt.insert(
|
||||
{new_system.solar_system_id,
|
||||
WandererApp.Map.PositionCalculator.get_system_bounding_rect(new_system)},
|
||||
rtree_name
|
||||
)
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:system_#{new_system.id}:last_activity",
|
||||
DateTime.utc_now(),
|
||||
ttl: @system_inactive_timeout
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"map_#{map_id}:system_#{new_system.id}:last_activity",
|
||||
DateTime.utc_now(),
|
||||
ttl: @system_inactive_timeout
|
||||
)
|
||||
broadcast!(map_id, :add_system, new_system)
|
||||
WandererApp.Map.add_system(map_id, new_system)
|
||||
|
||||
broadcast!(map_id, :add_system, new_system)
|
||||
WandererApp.Map.add_system(map_id, new_system)
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
{:error, :already_exists} ->
|
||||
{:error, _} ->
|
||||
:ok
|
||||
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
|
||||
|
||||
@@ -17,7 +17,16 @@ defmodule WandererApp.Permissions do
|
||||
@delete_map 4096
|
||||
|
||||
@viewer_role [@view_system, @view_character, @view_connection]
|
||||
@member_role @viewer_role ++ [@add_system, @add_connection, @update_system, @track_character, @delete_connection, @delete_system, @lock_system]
|
||||
@member_role @viewer_role ++
|
||||
[
|
||||
@add_system,
|
||||
@add_connection,
|
||||
@update_system,
|
||||
@track_character,
|
||||
@delete_connection,
|
||||
@delete_system,
|
||||
@lock_system
|
||||
]
|
||||
@manager_role @member_role
|
||||
@admin_role @manager_role ++ [@add_acl, @delete_acl, @delete_map]
|
||||
|
||||
|
||||
@@ -1,13 +1,68 @@
|
||||
defmodule WandererApp.MapConnectionRepo do
|
||||
use WandererApp, :repository
|
||||
|
||||
require Logger
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
|
||||
def get_by_map(map_id),
|
||||
do: WandererApp.Api.MapConnection.read_by_map(%{map_id: map_id})
|
||||
|
||||
def get_by_locations(map_id, solar_system_source, solar_system_target) do
|
||||
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}
|
||||
|
||||
{:error, %Ash.Error.Query.NotFound{}} ->
|
||||
{:ok, []}
|
||||
|
||||
{:error, error} ->
|
||||
@logger.error("Failed to get connections: #{inspect(error, pretty: true)}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
[from_connections ++ to_connections]
|
||||
|> List.flatten()
|
||||
|> bulk_destroy!()
|
||||
|> case do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
error ->
|
||||
@logger.error("Failed to remove connections from map: #{inspect(error, pretty: true)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def destroy!(connection), do: connection |> WandererApp.Api.MapConnection.destroy!()
|
||||
|
||||
def bulk_destroy!(connections) do
|
||||
connections
|
||||
|> WandererApp.Api.MapConnection.destroy!()
|
||||
|> case do
|
||||
%Ash.BulkResult{status: :success} ->
|
||||
:ok
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def update_time_status(connection, update),
|
||||
do:
|
||||
connection
|
||||
|
||||
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
|
||||
|
||||
@@ -51,7 +51,7 @@ defmodule WandererAppWeb.Alerts do
|
||||
def delayed_fade_out_flash() do
|
||||
JS.hide(
|
||||
transition:
|
||||
{"transition-opacity ease-out delay-2000 duration-1000", "opacity-100", "opacity-0"},
|
||||
{"transition-opacity ease-out delay-5000 duration-6000", "opacity-100", "opacity-0"},
|
||||
time: 6000
|
||||
)
|
||||
|> JS.push("lv:clear-flash")
|
||||
|
||||
@@ -92,11 +92,11 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
<div class="absolute right-4">
|
||||
<button
|
||||
phx-click={JS.exec("data-cancel", to: "##{@id}")}
|
||||
type="button"
|
||||
class="p-link opacity-70 hover:opacity-100"
|
||||
aria-label={gettext("close")}
|
||||
type="button"
|
||||
class="p-link opacity-70 hover:opacity-100"
|
||||
aria-label={gettext("close")}
|
||||
>
|
||||
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
|
||||
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</h3>
|
||||
@@ -602,12 +602,10 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
<tr
|
||||
:for={row <- @rows}
|
||||
id={@row_id && @row_id.(row)}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
class={"hover #{if @row_selected && @row_selected.(row), do: "!bg-slate-600", else: ""} #{if @row_click, do: "cursor-pointer", else: ""}"}
|
||||
>
|
||||
<td
|
||||
:for={{col, _index} <- Enum.with_index(@col)}
|
||||
phx-click={@row_click && @row_click.(row)}
|
||||
>
|
||||
<td :for={{col, _index} <- Enum.with_index(@col)}>
|
||||
<%= render_slot(col, @row_item.(row)) %>
|
||||
</td>
|
||||
<td :if={@action != []}>
|
||||
|
||||
@@ -42,27 +42,28 @@
|
||||
integrity={integrity_hash("https://unpkg.com/react-dom@18/umd/react-dom.production.min.js")}
|
||||
>
|
||||
</script>
|
||||
|
||||
<script defer phx-track-static type="module" src={~p"/assets/app.js"} crossorigin="anonymous">
|
||||
</script>
|
||||
<!-- Appzi: Capture Insightful Feedback -->
|
||||
<script async src="https://w.appzi.io/w.js?token=yddv0">
|
||||
<script defer src="https://w.appzi.io/w.js?token=yddv0">
|
||||
</script>
|
||||
<!-- End Appzi -->
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script
|
||||
async
|
||||
defer
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-61PHLLS0LD"
|
||||
crossorigin="anonymous"
|
||||
>
|
||||
</script>
|
||||
<script>
|
||||
<script defer>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-61PHLLS0LD');
|
||||
</script>
|
||||
<script defer phx-track-static type="module" src={~p"/assets/app.js"} crossorigin="anonymous">
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<%= @inner_content %>
|
||||
|
||||
@@ -46,7 +46,10 @@ defmodule WandererAppWeb.UserActivity do
|
||||
<.local_time id={@activity.id} at={@activity.inserted_at} />
|
||||
</span>
|
||||
</p>
|
||||
<p class="flex shrink-0 items-center space-x-1 min-w-[200px]">
|
||||
<p
|
||||
:if={not is_nil(@activity.character)}
|
||||
class="flex shrink-0 items-center space-x-1 min-w-[200px]"
|
||||
>
|
||||
<.character_item character={@activity.character} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
17
lib/wanderer_app_web/controllers/basic_auth.ex
Executable file
@@ -0,0 +1,17 @@
|
||||
defmodule WandererAppWeb.BasicAuth do
|
||||
@moduledoc false
|
||||
|
||||
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
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -20,11 +20,8 @@
|
||||
<div class="h-10"></div>
|
||||
<div>
|
||||
<div class="inline-flex w-full flex-col items-stretch justify-center gap-2 px-4 md:flex-row xl:justify-start xl:px-0">
|
||||
<a
|
||||
<.link
|
||||
href="https://discord.gg/cafERvDD2k"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Discord"
|
||||
class="btn md:btn-lg group shrink-0 rounded-full [@media(min-width:768px)]:px-10 bg-[oklch(64.74%_0.124_270.62)] border-[oklch(64.74%_0.124_270.62)] hover:bg-[oklch(60%_0.124_270.62)] hover:border-[oklch(60%_0.124_270.62)]"
|
||||
>
|
||||
<svg
|
||||
@@ -66,7 +63,7 @@
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</a>
|
||||
</.link>
|
||||
|
||||
<a
|
||||
href="https://t.me/wanderer_mapper"
|
||||
|
||||
@@ -11,24 +11,22 @@
|
||||
</div>
|
||||
<!--Right Col-->
|
||||
<div :if={@invite_token_valid} class="overflow-hidden">
|
||||
<%!-- <img class="mx-auto w-full md:w-4/5 transform -rotate-6 transition hover:scale-105 duration-700 ease-in-out hover:rotate-6" src="macbook.svg" /> --%>
|
||||
<.link navigate={~p"/auth/eve?invite=#{@invite_token}"}>
|
||||
<img src="https://web.ccpgamescdn.com/eveonlineassets/developers/eve-sso-login-black-large.png" />
|
||||
</.link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel carousel-center !bg-neutral rounded-box max-w-4xl space-x-6 p-4">
|
||||
<div class="carousel carousel-center bg-neutral rounded-box max-w-[80%] space-x-4 p-4">
|
||||
<%= for post <- @posts do %>
|
||||
<.link class="carousel-item relative" navigate={~p"/news/#{post.id}"}>
|
||||
<.link class="group carousel-item relative" navigate={~p"/news/#{post.id}"}>
|
||||
<div class="artboard-horizontal phone-1 relative hover:text-white mt-10">
|
||||
<img
|
||||
class="rounded-lg shadow-lg block !w-[400px] !h-[200px] opacity-75"
|
||||
src={post.cover_image_uri}
|
||||
/>
|
||||
<p class="absolute bottom-24 left-14 text-sm font-normal ccp-font">
|
||||
<%= post.date %> - BY <span class="uppercase"><%= post.author %></span>
|
||||
</p>
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black opacity-75 group-hover:opacity-25 transition-opacity duration-300">
|
||||
</div>
|
||||
<h3 class="absolute bottom-4 left-14 font-bold break-normal pt-6 pb-2 ccp-font text-white">
|
||||
<%= post.title %>
|
||||
</h3>
|
||||
@@ -36,10 +34,8 @@
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex w-full justify-center gap-2 py-2">
|
||||
<%= for post <- @posts do %>
|
||||
<.link class="btn btn-xs" navigate={~p"/news/#{post.id}"}></.link>
|
||||
<% end %>
|
||||
</div>
|
||||
<%!-- <div class="carousel carousel-center !bg-neutral rounded-box max-w-4xl space-x-6 p-4">
|
||||
|
||||
</div> --%>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -99,40 +99,35 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
end
|
||||
|
||||
defp apply_action(socket, :add_members, %{"id" => acl_id} = _params) do
|
||||
{:ok, %{owner: %{id: _character_id}} = access_list} =
|
||||
socket.assigns.access_lists |> Enum.find(&(&1.id == acl_id)) |> Ash.load(:owner)
|
||||
with {:ok, %{owner: %{id: _character_id}} = access_list} <-
|
||||
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)
|
||||
end)
|
||||
|
||||
user_character_ids = socket.assigns.current_user.characters |> Enum.map(& &1.id)
|
||||
|
||||
user_character_ids
|
||||
|> Enum.each(fn user_character_id ->
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(user_character_id)
|
||||
end)
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :access_lists)
|
||||
|> assign(:page_title, "Access Lists - Add Members")
|
||||
|> assign(:selected_acl_id, acl_id)
|
||||
|> assign(:user_character_ids, user_character_ids)
|
||||
|> assign(
|
||||
member_search_options: socket.assigns.characters |> Enum.map(&map_user_character_info/1)
|
||||
)
|
||||
|> assign(:access_list, access_list)
|
||||
|> assign(
|
||||
:members,
|
||||
WandererApp.Api.AccessListMember.read_by_access_list!(%{access_list_id: acl_id})
|
||||
)
|
||||
|> assign(
|
||||
:member_form,
|
||||
%{} |> to_form()
|
||||
)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("set-default", %{"id" => id}, socket) do
|
||||
send_update(LiveSelect.Component, options: socket.assigns.characters, id: id)
|
||||
|
||||
{:noreply, socket}
|
||||
socket
|
||||
|> assign(:active_page, :access_lists)
|
||||
|> assign(:page_title, "Access Lists - Add Members")
|
||||
|> assign(:selected_acl_id, acl_id)
|
||||
|> assign(:user_character_ids, user_character_ids)
|
||||
|> assign(
|
||||
member_search_options: socket.assigns.characters |> Enum.map(&map_user_character_info/1)
|
||||
)
|
||||
|> assign(:access_list, access_list)
|
||||
|> assign(
|
||||
:members,
|
||||
WandererApp.Api.AccessListMember.read_by_access_list!(%{access_list_id: acl_id})
|
||||
)
|
||||
|> assign(
|
||||
:member_form,
|
||||
%{} |> to_form()
|
||||
)
|
||||
else
|
||||
_ ->
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
@@ -225,7 +220,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
"add_members",
|
||||
%{"member_id" => member_id} = _params,
|
||||
%{assigns: assigns} = socket
|
||||
) do
|
||||
)
|
||||
when is_binary(member_id) and member_id != "" do
|
||||
member_option =
|
||||
assigns.member_search_options
|
||||
|> Enum.find(&(&1.value == member_id))
|
||||
@@ -238,8 +234,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
def handle_event("delete-acl", %{"id" => acl_id} = _params, socket) do
|
||||
case socket.assigns.access_lists
|
||||
|> Enum.find(&(&1.id == acl_id))
|
||||
|> WandererApp.Api.AccessList.destroy() do
|
||||
{:ok, _acl} ->
|
||||
|> WandererApp.Api.AccessList.destroy!() do
|
||||
:ok ->
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
@@ -261,7 +257,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
)}
|
||||
end
|
||||
rescue
|
||||
_ ->
|
||||
_error ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(
|
||||
@@ -315,6 +311,12 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(event, body, socket) do
|
||||
Logger.warning(fn -> "unhandled event: #{event} #{inspect(body)}" end)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:search, text}, socket) do
|
||||
first_character_id =
|
||||
@@ -372,12 +374,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,
|
||||
@@ -487,12 +493,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}
|
||||
|
||||
@@ -514,12 +524,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}
|
||||
|
||||
@@ -542,12 +556,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}
|
||||
|
||||
@@ -646,6 +664,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
|
||||
|
||||
@@ -127,23 +127,13 @@
|
||||
<.form :let={f} for={@form} phx-change="validate" phx-submit={@live_action}>
|
||||
<.input type="text" field={f[:name]} placeholder="Name" />
|
||||
<.input type="textarea" field={f[:description]} placeholder="Public description" />
|
||||
<.live_select
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:owner_id]}
|
||||
dropdown_extra_class="max-h-64"
|
||||
available_option_class="w-full"
|
||||
value_mapper={&map_character/1}
|
||||
update_min_len={0}
|
||||
phx-focus="set-default"
|
||||
options={@characters}
|
||||
placeholder="Owner"
|
||||
>
|
||||
<:option :let={option}>
|
||||
<div class="flex items-center">
|
||||
<.avatar url={member_icon_url(option.eve_id)} label={option.label} />
|
||||
<%= option.label %>
|
||||
</div>
|
||||
</:option>
|
||||
</.live_select>
|
||||
class="p-dropdown p-component p-inputwrapper mt-8"
|
||||
placeholder="Select a map owner"
|
||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||
/>
|
||||
<div class="modal-action">
|
||||
<.button class="mt-2" type="submit" phx-disable-with="Saving...">
|
||||
<%= (@live_action == :create && "Create") || "Save" %>
|
||||
|
||||
@@ -42,8 +42,11 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
|
||||
{:ok, character_settings} =
|
||||
case WandererApp.Api.MapCharacterSettings.read_by_map(%{map_id: selected_map.id}) do
|
||||
{:ok, settings} -> {:ok, settings}
|
||||
_ -> {:ok, []}
|
||||
{:ok, settings} ->
|
||||
{:ok, settings}
|
||||
|
||||
_ ->
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
user_id = socket.assigns.user_id
|
||||
|
||||
@@ -169,19 +169,4 @@ defmodule WandererAppWeb.MapAuditLive do
|
||||
_ -> socket
|
||||
end
|
||||
end
|
||||
|
||||
defp map_ui_character(character) do
|
||||
character
|
||||
|> Map.take([
|
||||
:id,
|
||||
:eve_id,
|
||||
:name,
|
||||
:corporation_id,
|
||||
:corporation_name,
|
||||
:corporation_ticker,
|
||||
:alliance_id,
|
||||
:alliance_name,
|
||||
:alliance_ticker
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
id="map-loader"
|
||||
data-loading={show_loader("map-loader")}
|
||||
data-loaded={hide_loader("map-loader")}
|
||||
class="!z-100 absolute w-screen h-screen bg-transparent hidden"
|
||||
class="!z-100 w-screen h-screen hidden relative"
|
||||
>
|
||||
<div class="flex w-full h-full items-center justify-center">
|
||||
<div class="hs-overlay-backdrop transition duration absolute inset-0 blur" />
|
||||
<div class="flex !z-[150] w-full h-full items-center justify-center">
|
||||
<div class="Loader" data-text="Wanderer">
|
||||
<span class="Loader__Circle"></span>
|
||||
<span class="Loader__Circle"></span>
|
||||
|
||||
@@ -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})
|
||||
@@ -107,11 +109,18 @@ defmodule WandererAppWeb.MapsLive do
|
||||
subscription_form = %{
|
||||
"plan" => "omega",
|
||||
"period" => "1",
|
||||
"characters_limit" => "100",
|
||||
"characters_limit" => "300",
|
||||
"hubs_limit" => "10",
|
||||
"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,
|
||||
@@ -167,24 +181,6 @@ defmodule WandererAppWeb.MapsLive do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"live_select_change",
|
||||
%{"id" => "form_owner_id_live_select_component" = id, "text" => text} = _change_event,
|
||||
socket
|
||||
) do
|
||||
options =
|
||||
if text == "" do
|
||||
socket.assigns.characters
|
||||
else
|
||||
socket.assigns.characters
|
||||
end
|
||||
|
||||
send_update(LiveSelect.Component, options: options, id: id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"live_select_change",
|
||||
@@ -612,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(
|
||||
@@ -671,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}
|
||||
@@ -916,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"
|
||||
@@ -137,23 +136,17 @@
|
||||
<.input type="text" field={f[:name]} placeholder="Name" />
|
||||
<.input type="text" field={f[:slug]} prefix={@uri} placeholder="map-slug" />
|
||||
<.input type="textarea" field={f[:description]} placeholder="Public description" />
|
||||
<.live_select
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:owner_id]}
|
||||
value_mapper={&map_character/1}
|
||||
options={@characters}
|
||||
class="p-dropdown p-component p-inputwrapper mt-8"
|
||||
placeholder="Select a map owner"
|
||||
>
|
||||
<:option :let={option}>
|
||||
<div class="flex items-center">
|
||||
<.avatar url={member_icon_url(option.eve_id)} label={option.label} />
|
||||
<%= option.label %>
|
||||
</div>
|
||||
</:option>
|
||||
</.live_select>
|
||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||
/>
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:scope]}
|
||||
class="p-dropdown p-component p-inputwrapper"
|
||||
class="p-dropdown p-component p-inputwrapper mt-8"
|
||||
placeholder="Select a map scope"
|
||||
options={Enum.map(@scopes, fn scope -> {scope, scope} end)}
|
||||
/>
|
||||
@@ -196,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"
|
||||
@@ -207,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"
|
||||
@@ -233,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">
|
||||
|
||||
@@ -9,6 +9,10 @@ defmodule WandererAppWeb.Router do
|
||||
warn: false,
|
||||
only: [redirect_if_user_is_authenticated: 2]
|
||||
|
||||
import WandererAppWeb.BasicAuth,
|
||||
warn: false,
|
||||
only: [admin_basic_auth: 2]
|
||||
|
||||
@code_reloading Application.compile_env(
|
||||
:wanderer_app,
|
||||
[WandererAppWeb.Endpoint, :code_reloader],
|
||||
@@ -20,6 +24,10 @@ defmodule WandererAppWeb.Router do
|
||||
@font_src ~w('self' data: https://web.ccpgamescdn.com https://w.appzi.io)
|
||||
@script_src ~w('self' )
|
||||
|
||||
pipeline :admin_bauth do
|
||||
plug :admin_basic_auth
|
||||
end
|
||||
|
||||
pipeline :browser do
|
||||
plug(:accepts, ["html"])
|
||||
plug(:fetch_session)
|
||||
@@ -137,11 +145,9 @@ defmodule WandererAppWeb.Router do
|
||||
get "/:provider/callback", AuthController, :callback
|
||||
end
|
||||
|
||||
scope "/", WandererAppWeb do
|
||||
scope "/admin", WandererAppWeb do
|
||||
pipe_through(:browser)
|
||||
|
||||
get "/", RedirectController, :redirect_authenticated
|
||||
get("/last", MapsController, :last)
|
||||
pipe_through(:admin_bauth)
|
||||
|
||||
live_session :admin,
|
||||
on_mount: [
|
||||
@@ -149,9 +155,23 @@ defmodule WandererAppWeb.Router do
|
||||
{WandererAppWeb.UserAuth, :ensure_admin},
|
||||
WandererAppWeb.Nav
|
||||
] do
|
||||
live("/admin", AdminLive, :index)
|
||||
live("/", AdminLive, :index)
|
||||
end
|
||||
|
||||
error_tracker_dashboard("/errors",
|
||||
on_mount: [
|
||||
{WandererAppWeb.UserAuth, :ensure_authenticated},
|
||||
{WandererAppWeb.UserAuth, :ensure_admin}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
scope "/", WandererAppWeb do
|
||||
pipe_through(:browser)
|
||||
|
||||
get "/", RedirectController, :redirect_authenticated
|
||||
get("/last", MapsController, :last)
|
||||
|
||||
live_session :authenticated,
|
||||
on_mount: [
|
||||
{WandererAppWeb.UserAuth, :ensure_authenticated},
|
||||
@@ -180,17 +200,6 @@ defmodule WandererAppWeb.Router do
|
||||
end
|
||||
end
|
||||
|
||||
scope "/admin" do
|
||||
pipe_through(:browser)
|
||||
|
||||
error_tracker_dashboard("/errors",
|
||||
on_mount: [
|
||||
{WandererAppWeb.UserAuth, :ensure_authenticated},
|
||||
{WandererAppWeb.UserAuth, :ensure_admin}
|
||||
]
|
||||
)
|
||||
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
|
||||
@@ -206,7 +215,6 @@ defmodule WandererAppWeb.Router do
|
||||
error_tracker_dashboard("/errors", as: :error_tracker_dev_dashboard)
|
||||
|
||||
live_dashboard("/dashboard", metrics: WandererAppWeb.Telemetry)
|
||||
# forward("/mailbox", Plug.Swoosh.MailboxPreview)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
8
mix.exs
@@ -2,7 +2,7 @@ defmodule WandererApp.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
@version "1.0.2"
|
||||
@version "1.3.4"
|
||||
|
||||
def project do
|
||||
[
|
||||
@@ -56,7 +56,7 @@ defmodule WandererApp.MixProject do
|
||||
{:phoenix, "~> 1.7.12"},
|
||||
{:phoenix_ecto, "~> 4.6"},
|
||||
{:ecto_sql, "~> 3.10"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:postgrex, "~> 0.19.1"},
|
||||
{:phoenix_html, "~> 4.0"},
|
||||
{:phoenix_live_reload, "~> 1.5.3", only: :dev},
|
||||
{:phoenix_live_view, "~> 0.20.17"},
|
||||
@@ -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"},
|
||||
@@ -112,7 +112,7 @@ defmodule WandererApp.MixProject do
|
||||
{:git_ops, "~> 2.6.1"},
|
||||
{:version_tasks, "~> 0.12.0"},
|
||||
{:error_tracker, "~> 0.2"},
|
||||
{:sourceror, "~> 1.3.0", override: true}
|
||||
{:ddrt, "~> 0.2.1"}
|
||||
]
|
||||
end
|
||||
|
||||
|
||||
3
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"},
|
||||
@@ -105,7 +106,7 @@
|
||||
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
|
||||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
||||
"sourceror": {:hex, :sourceror, "1.3.0", "70ab9e8bf6df085a1effba4b49ad621b7153b065f69ef6cdb82e6088f2026029", [:mix], [], "hexpm", "1794c3ceeca4eb3f9437261721e4d9cbf846d7c64c7aee4f64062b18d5ce1eac"},
|
||||
"sourceror": {:hex, :sourceror, "1.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"},
|
||||
"spark": {:hex, :spark, "2.2.29", "a52733ff72b05a674e48d3ca7a4172fe7bec81e9116069da8b4db19030d581d9", [:mix], [{:igniter, ">= 0.3.36 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "111a0dadbb27537c7629bc03ac56fcab15056ab0b9ad985084b9adcdb48836c8"},
|
||||
"spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"},
|
||||
"splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"},
|
||||
|
||||
@@ -42,20 +42,17 @@ We prioritize your security and privacy. Our application uses advanced encryptio
|
||||
|
||||
We believe in the power of community and collaboration. Join our growing community of Eve Online players and take your gameplay to the next level. Share tips, strategies, and feedback on our forums and social media channels.
|
||||
|
||||
- **[Forums](#)**
|
||||
- **[Twitter](#)**
|
||||
- **[Discord](#)**
|
||||
- **[Discord](https://discord.gg/cafERvDD2k)**
|
||||
- **[Telegram](https://t.me/wanderer_mapper)**
|
||||
- **[Github](https://github.com/wanderer-industries)**
|
||||
|
||||
## Feedback and Support
|
||||
|
||||
We are committed to continuous improvement and value your feedback. If you encounter any issues or have suggestions for new features, please reach out to our support team. We're here to help!
|
||||
|
||||
- **[Support](#)**
|
||||
- **[Feedback Form](#)**
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Eve Online Shared Systems Map application is more than just a tool—it's a game-changer. Enhance your strategic planning, improve coordination with your team, and explore the vast universe of Eve Online like never before. Download the app today and start your journey towards galactic dominance!
|
||||
The Wanderer application is more than just a tool—it's a game-changer. Enhance your strategic planning, improve coordination with your team, and explore the vast universe of Eve Online like never before. Join today and start your journey towards galactic dominance!
|
||||
|
||||
Fly safe,
|
||||
WANDERER TEAM
|
||||
|
||||
20
priv/posts/2024/09-18-community-edition.md
Normal file
@@ -0,0 +1,20 @@
|
||||
%{
|
||||
title: "Introducing Wanderer Community Edition",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/ce_logo_dark.png",
|
||||
tags: ~w(community-edition open-source),
|
||||
description: "We’re introducing the free as in beer, self-hosted and MIT-licensed Wanderer Community Edition (CE)."
|
||||
}
|
||||
|
||||
---
|
||||

|
||||
|
||||
* [Wanderer CE installation instructions](https://github.com/wanderer-industries/community-edition)
|
||||
* [Wanderer CE release notes](https://github.com/wanderer-industries/wanderer/blob/main/CHANGELOG.md).
|
||||
|
||||
Fly safe,
|
||||
WANDERER TEAM
|
||||
|
||||
---
|
||||
|
||||
_Note: Eve Online is a trademark of CCP hf. This application is not affiliated with or endorsed by CCP hf._
|
||||
65
priv/posts/2024/09-19-connection-info.md
Normal file
@@ -0,0 +1,65 @@
|
||||
%{
|
||||
title: "User Guide: Map Connection Info",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/09-19-connection-info/cover.png",
|
||||
tags: ~w(interface guide map connection),
|
||||
description: "This guide provides instructions on how to access and interpret connection information on the map. The connection info feature helps users monitor traffic between systems and estimate the mass of ships that have passed through a connection."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Guide
|
||||
|
||||
### 1. Accessing Connection Information
|
||||
|
||||

|
||||
|
||||
- **Overview:** Users can view detailed information about system connections, such as wormholes or stargates, to track the ships that have passed through.
|
||||
- **How to Open Connection Info:**
|
||||
1. On the map, locate a connection line between two systems.
|
||||
2. Click on the connection to open the information panel.
|
||||
3. The panel will display key data about the connection, including ships passed, time and estimated mass.
|
||||
|
||||
### 2. Viewing Ships Passed
|
||||
|
||||

|
||||
|
||||
- **Tracked Characters Only:** The information provided about ships that passed through a connection is based on characters being tracked via the map's character tracking system.
|
||||
- **Details Provided:**
|
||||
1. Once the connection info is open, you will see a list of all ships that passed through the connection.
|
||||
2. The ships are displayed along with the names of the tracked characters that piloted them.
|
||||
3. This information allows you to track traffic patterns and analyze ship movements through the connection.
|
||||
|
||||
### 3. Viewing Estimated Ship Mass
|
||||
|
||||
- **Ship Mass Calculation:** The approximate sum of the mass of all ships that passed through the connection is also displayed.
|
||||
- **How the Mass is Calculated:**
|
||||
1. Ship mass is estimated based on the ship type only.
|
||||
2. The displayed mass may not be 100% precise, as factors like ship fittings or enabled propulsion modes can influence the actual mass.
|
||||
3. The total mass of all ships that passed through the connection is provided to help estimate how much mass the connection has handled, which is particularly important for wormhole stability.
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Accuracy of Values:** Please note that all values (such as ship mass and the number of ships) are approximations. Ship mass is calculated based on known ship types, but the actual mass may vary depending on fittings and propulsion mode settings.
|
||||
- **Tracked Characters Only:** The ships counted in the "Ships Passed" section are only those of characters being tracked by the map. Ships from non-tracked characters or unidentified sources will not be listed.
|
||||
|
||||
### Example Workflow
|
||||
|
||||
1. **Check Connection Info:**
|
||||
- Click on a connection between two systems (e.g., a wormhole connection).
|
||||
- Review the list of ships passed through the connection.
|
||||
|
||||
2. **Monitor Ship Mass:**
|
||||
- In the same panel, review the approximate total mass of the ships that have passed.
|
||||
- Use this information to assess the connection's usage or potential collapse (in the case of wormholes).
|
||||
|
||||
## Conclusion
|
||||
|
||||
The connection information feature is a valuable tool for tracking ship traffic and estimating mass through system connections. While the values are approximations, this feature provides useful insights for map users to monitor and manage connections more effectively. If you have any questions or need further assistance, please contact our support team.
|
||||
|
||||
Fly safe,
|
||||
WANDERER TEAM
|
||||
|
||||
---
|
||||
|
||||
_Note: Eve Online is a trademark of CCP hf. This application is not affiliated with or endorsed by CCP hf._
|
||||