Compare commits

..

134 Commits

Author SHA1 Message Date
CI
4835dfcc42 chore: release version v1.12.4 2024-10-21 11:11:42 +00:00
Dmitry Popov
15bceb09a2 chore: release version v1.12.3 2024-10-21 13:11:13 +02:00
Dmitry Popov
13e818abfd fix(Map): Fix systems cleanup 2024-10-21 13:02:42 +02:00
CI
9c5f6049b5 chore: release version v1.12.3 2024-10-18 07:10:35 +00:00
Dmitry Popov
2095b619a4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-18 11:10:06 +04:00
Dmitry Popov
df155cbc1b fix(Map): Fix regression issues 2024-10-18 11:10:02 +04:00
CI
3781729fd1 chore: release version v1.12.2 2024-10-16 21:12:27 +00:00
Dmitry Popov
d03c634ec0 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-17 01:11:41 +04:00
Dmitry Popov
93c979c218 chore: release version v1.12.0 2024-10-17 01:11:37 +04:00
CI
90fef94583 chore: release version v1.12.1 2024-10-16 20:11:16 +00:00
Dmitry Popov
0b8eec2263 fix(Map): Fix system add error after map page refresh 2024-10-17 00:10:36 +04:00
CI
9511af4e6d chore: release version v1.12.0 2024-10-16 14:12:30 +00:00
Aleksei Chichenkov
7deaf1fd9f Merge pull request #36 from wanderer-industries/refactor-settings
feat(Map): Prettify user settings
2024-10-16 17:12:04 +03:00
achichenkov
43cc5bd520 feat(Map): Prettify user settings
Fixes #35
2024-10-16 17:04:50 +03:00
CI
68b58aa520 chore: release version v1.11.5 2024-10-16 12:46:16 +00:00
Dmitry Popov
dbadd09af3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-16 16:45:48 +04:00
Dmitry Popov
fcbe2c754f chore: release version v1.11.1 2024-10-16 16:45:44 +04:00
CI
ad4580677b chore: release version v1.11.4 2024-10-16 11:48:48 +00:00
Dmitry Popov
01a6cc7d92 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-16 15:48:20 +04:00
Dmitry Popov
95ce95a187 chore: release version v1.11.1 2024-10-16 15:48:16 +04:00
CI
ce8e6fbfb0 chore: release version v1.11.3 2024-10-16 05:51:27 +00:00
Dmitry Popov
a20eaed76b Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-16 09:50:49 +04:00
Dmitry Popov
419af72028 chore: release version v1.11.1 2024-10-16 09:50:45 +04:00
CI
8e499522f6 chore: release version v1.11.2 2024-10-15 22:13:53 +00:00
Dmitry Popov
84321b847e chore: release version v1.11.1 2024-10-16 02:13:18 +04:00
CI
c969a4d465 chore: release version v1.11.1 2024-10-14 14:31:41 +00:00
Dmitry Popov
0e12c850b6 chore: release version v1.11.0 2024-10-14 18:31:12 +04:00
CI
442835dd9b chore: release version v1.11.0 2024-10-14 14:24:17 +00:00
Dmitry Popov
b4ff99cb2e Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-14 18:23:43 +04:00
Dmitry Popov
aa0ecbc998 feat(Map): Add map level option to store custom labels 2024-10-14 18:23:39 +04:00
CI
cc412e93c0 chore: release version v1.10.0 2024-10-13 10:18:23 +00:00
Dmitry Popov
1d36fadbfa Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-13 14:17:43 +04:00
Dmitry Popov
56182bd87d feat(Map): Link signature on splash 2024-10-13 14:17:40 +04:00
CI
d290ff92b3 chore: release version v1.9.0 2024-10-13 10:10:38 +00:00
Dmitry Popov
298c5fd3b8 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-13 14:09:56 +04:00
Dmitry Popov
e365c43781 feat(Map): Link signature on splash 2024-10-13 14:09:53 +04:00
CI
23a9f22ef4 chore: release version v1.8.0 2024-10-13 10:04:22 +00:00
Dmitry Popov
242f437237 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-13 14:03:53 +04:00
Dmitry Popov
2eae8cffdb feat(Map): Link signature on splash 2024-10-13 14:03:48 +04:00
CI
68ab3d4f72 chore: release version v1.7.0 2024-10-13 09:40:06 +00:00
Dmitry Popov
1ea805aff0 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-13 13:39:39 +04:00
Dmitry Popov
6ce45349dc feat(Map): Link signature on splash 2024-10-13 13:39:35 +04:00
CI
8f20cd9863 chore: release version v1.6.0 2024-10-13 08:58:05 +00:00
Dmitry Popov
4ed0e85680 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-13 12:57:37 +04:00
Dmitry Popov
8ce9eb9955 feat(Map): Link signature on splash 2024-10-13 12:57:33 +04:00
CI
363330f3d1 chore: release version v1.5.0 2024-10-11 09:06:20 +00:00
Dmitry Popov
fbf9c5ddd6 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-11 13:05:52 +04:00
Dmitry Popov
fbf2ee314c feat(Map): Follow Character on Map and auto select their current system
fixes #34
2024-10-11 13:05:48 +04:00
CI
c9f83fb419 chore: release version v1.4.0 2024-10-11 08:12:17 +00:00
Dmitry Popov
9737d91e16 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-11 12:11:38 +04:00
Dmitry Popov
2f672ae970 feat(Map): Follow Character on Map and auto select their current system
fixes #34
2024-10-11 12:11:33 +04:00
CI
25339546c6 chore: release version v1.3.6 2024-10-09 21:44:05 +00:00
Dmitry Popov
912cad42ac Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-10 01:43:36 +04:00
Dmitry Popov
b3752c8d8f fix(Signatures): Signatures update fixes
fixes #25
2024-10-10 01:43:32 +04:00
CI
e8a11333f2 chore: release version v1.3.5 2024-10-09 13:41:38 +00:00
Dmitry Popov
8bb6d09e6e fix(Signatures): Signatures update fixes
fixes #25
2024-10-09 17:41:02 +04:00
CI
56bf955297 chore: release version v1.3.4 2024-10-09 09:24:17 +00:00
Dmitry Popov
ef6c08dfe8 Refactoring (#27)
* chore: release version v1.3.1

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

* chore: release version v1.3.1
2024-10-09 13:23:35 +04:00
CI
495c3e1cd7 chore: release version v1.3.3 2024-10-08 07:30:12 +00:00
Dmitry Popov
9a5fe3d744 chore: release version v1.3.2 2024-10-08 11:29:36 +04:00
CI
72607cae4d chore: release version v1.3.2 2024-10-07 19:34:37 +00:00
Dmitry Popov
4891cdb04d 19 add map custom options (#24)
* feat(Core): Support map default layout option
2024-10-07 23:33:52 +04:00
CI
d214881720 chore: release version v1.3.1 2024-10-07 09:58:06 +00:00
Dmitry Popov
e66c125dbf chore: release version v1.3.0 2024-10-07 13:57:34 +04:00
CI
9862bcfa05 chore: release version v1.3.0 2024-10-07 07:54:23 +00:00
Aleksei Chichenkov
0ac5451bef Merge pull request #23 from wanderer-industries/fix-signatures-sort
Fix signatures sort
2024-10-07 10:53:45 +03:00
CI
669479b815 chore: release version v1.2.10 2024-10-07 07:51:56 +00:00
Ryan Olds
2721130566 Added DATABASE_SSL_VERIFY_NONE env var (#21) 2024-10-07 11:51:26 +04:00
achichenkov
6e33ad943f feat(Map): Fix default sort
Fixes #22
2024-10-07 10:24:54 +03:00
achichenkov
f4b7357802 feat(Map): Remove resizible and fix styles of column sorting
Fixes #22
2024-10-07 09:31:01 +03:00
achichenkov
7a404a7e6a Merge branch 'refs/heads/main' into fix-signatures-sort
# Conflicts:
#	assets/js/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent/SystemSignaturesContent.tsx
2024-10-07 09:13:35 +03:00
CI
5158700a79 chore: release version v1.2.9 2024-10-07 06:11:35 +00:00
Aleksei Chichenkov
41d10c1b47 Merge pull request #20 from ryanrolds/sig_sort_local_storage
Persist the signature sort between sessions and header improvements
2024-10-07 09:11:09 +03:00
achichenkov
3aaac91f07 feat(Map): Revision of sorting from also adding ability to sort all columns
Fixes #2
2024-10-07 09:08:54 +03:00
Ryan R. Olds
ea7ff080b8 Undid some formatting changes 2024-10-06 15:26:42 -07:00
Ryan R. Olds
b5270958eb Undid some formatting changes 2024-10-06 15:26:21 -07:00
Ryan R. Olds
b0a38eab8c Signature header improvements 2024-10-06 15:19:58 -07:00
Ryan R. Olds
0a478e82ba Persist the signature sort between sessions 2024-10-06 14:46:04 -07:00
CI
02d97a009c chore: release version v1.2.8 2024-10-06 13:13:12 +00:00
Ryan Olds
33940cdb9b Sortable signatures (#18)
feat(Signatures): Make signatures sortable
2024-10-06 17:12:47 +04:00
CI
7a63f9ee6b chore: release version v1.2.7 2024-10-05 07:55:07 +00:00
Dmitry Popov
89b41fff59 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-05 11:54:38 +04:00
Dmitry Popov
7a15f71528 chore: release version v1.2.5 2024-10-05 11:54:35 +04:00
CI
cdce2f8761 chore: release version v1.2.6 2024-10-05 07:39:43 +00:00
Dmitry Popov
a2470bbe47 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-05 11:39:17 +04:00
Dmitry Popov
dbdf1ddce0 fix(Core): Stability & performance improvements 2024-10-05 11:39:13 +04:00
CI
f767e42e6f chore: release version v1.2.5 2024-10-04 21:56:47 +00:00
Dmitry Popov
3051eb6369 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-05 01:56:19 +04:00
Dmitry Popov
a41faddca3 fix(Core): Add system "true security" correction 2024-10-05 01:56:16 +04:00
CI
469038730e chore: release version v1.2.4 2024-10-03 09:27:53 +00:00
Dmitry Popov
b1fe5d2453 fix(Map): Remove duplicate connections 2024-10-03 13:27:21 +04:00
CI
f43e717da0 chore: release version v1.2.3 2024-10-02 17:52:44 +00:00
Dmitry Popov
95c8d4eef8 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-02 21:52:08 +04:00
Dmitry Popov
747ca0ee82 fix(Map): Fix map loading after select a different map. 2024-10-02 21:52:04 +04:00
CI
35a0184ec3 chore: release version v1.2.2 2024-10-02 12:50:16 +00:00
Dmitry Popov
96e1e5328c Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-02 16:49:27 +04:00
Dmitry Popov
7f98d6a0d8 chore: release version v1.2.0 2024-10-02 16:49:23 +04:00
CI
0194e25696 chore: release version v1.2.1 2024-10-02 12:48:06 +00:00
Dmitry Popov
189442e50f Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-10-02 16:47:34 +04:00
Dmitry Popov
d9bed070ec fix(ACL): Fix allowing to save map/access list with empty owner set 2024-10-02 16:47:29 +04:00
CI
a6193da8b5 chore: release version v1.2.0 2024-09-29 19:01:16 +00:00
Aleksei Chichenkov
50d35b207d Merge pull request #14 from wanderer-industries/feat-wnd-13
feat(Map): Add ability to open jump planner from routes
2024-09-29 22:00:44 +03:00
achichenkov
19eb45bfa1 feat(Map): Add ability to open jump planner from routes
Fixes #13
2024-09-29 21:47:35 +03:00
CI
01e0b24d9d chore: release version v1.1.0 2024-09-29 15:07:13 +00:00
Aleksei Chichenkov
3c8024b16c Merge pull request #12 from wanderer-industries/feat-wnd-11
feat(Map): Add highlighting for imperial space systems depends on fac…
2024-09-29 18:06:41 +03:00
achichenkov
4c0ad0dd66 feat(Map): Add highlighting for imperial space systems depends on faction
Fixes #11
2024-09-29 17:57:07 +03:00
CI
501840086b chore: release version v1.0.23 2024-09-25 09:52:47 +00:00
Dmitry Popov
240b180857 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-09-25 13:52:14 +04:00
Dmitry Popov
2bc5d0aaea fix(Map): Main map doesn't load back after refreshing/switching pages
fixes #8
2024-09-25 13:52:10 +04:00
CI
df66aa79b8 chore: release version v1.0.22 2024-09-25 08:24:50 +00:00
Dmitry Popov
6ea6a59ce3 fix(Map): Main map doesn't load back after refreshing/switching pages
fixes #8
2024-09-25 12:24:14 +04:00
CI
f3afa4d9d2 chore: release version v1.0.21 2024-09-24 20:21:54 +00:00
Dmitry Popov
e33d81eda1 Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-09-25 00:21:17 +04:00
Dmitry Popov
0fc4863dc4 fix(Map): Main map doesn't load back after refreshing/switching pages
fixes #8
2024-09-25 00:21:14 +04:00
CI
63471a5533 chore: release version v1.0.20 2024-09-23 17:42:36 +00:00
Dmitry Popov
050e90cb7e Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-09-23 21:41:55 +04:00
Dmitry Popov
b4643f2e45 fix(core): Small fixes & improvements 2024-09-23 21:41:51 +04:00
CI
2d740a76b7 chore: release version v1.0.19 2024-09-23 07:01:18 +00:00
Dmitry Popov
0de9e3a02d Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-09-23 11:00:44 +04:00
Dmitry Popov
bedaa37e08 fix(ACL): Fix adding empty members list 2024-09-23 11:00:40 +04:00
CI
ef9fa80b76 chore: release version v1.0.18 2024-09-22 09:14:52 +00:00
Dmitry Popov
dc2ea625ec fix(ACL): Cant delete ACL list after map deletion #5
Fixes #6
2024-09-22 13:11:03 +04:00
Dmitry Popov
17df2c188a chore: release version v1.0.16 2024-09-22 12:11:46 +04:00
Dmitry Popov
6e050bccdf Merge branch 'main' of github.com:wanderer-industries/wanderer 2024-09-22 11:48:10 +04:00
Dmitry Popov
8afbf3ce91 chore: release version v1.0.16 2024-09-22 11:48:07 +04:00
CI
9f57bf46a1 chore: release version v1.0.17 2024-09-21 11:23:48 +00:00
Dmitry Popov
4ce47da521 chore: release version v1.0.16 2024-09-21 15:23:09 +04:00
CI
4dc6382402 chore: release version v1.0.16 2024-09-21 09:24:55 +00:00
Aleksei Chichenkov
cb8c1e32d9 Merge pull request #10 from wanderer-industries/fix-wnd-6
fix(Map): add key for cache changes detecting
2024-09-21 12:24:30 +03:00
achichenkov
bf534be128 fix(Map): commented console log
Fixes #6
2024-09-21 12:19:48 +03:00
CI
a89c117612 chore: release version v1.0.15 2024-09-21 09:16:33 +00:00
Dmitry Popov
501dbcd76b fix(map): Show a proper user notification if map was deleted/archived 2024-09-21 13:16:04 +04:00
achichenkov
c0ceff1eec fix(Map): add console log for check sys loading
Fixes #6
2024-09-21 11:47:19 +03:00
achichenkov
c7866a1270 fix(Map): add key for cache changes detecting
Fixes #6
2024-09-21 11:26:25 +03:00
160 changed files with 111300 additions and 3792 deletions

2
.gitignore vendored
View File

@@ -12,6 +12,8 @@
# Ignore assets that are produced by build tools. # Ignore assets that are produced by build tools.
/priv/static/assets/ /priv/static/assets/
/priv/static/icons/
/priv/static/images/
/priv/static/*.js /priv/static/*.js
/priv/static/*.css /priv/static/*.css

View File

@@ -1,3 +1,3 @@
erlang 27.0.1 erlang 25.3
elixir 1.17-otp-27 elixir 1.16-otp-25
nodejs 18.0.0 nodejs 18.0.0

View File

@@ -2,96 +2,277 @@
<!-- changelog --> <!-- changelog -->
## [v1.0.14](https://github.com/wanderer-industries/wanderer/compare/v1.0.13...v1.0.14) (2024-09-21) ## [v1.12.4](https://github.com/wanderer-industries/wanderer/compare/v1.12.3...v1.12.4) (2024-10-21)
## [v1.0.13](https://github.com/wanderer-industries/wanderer/compare/v1.0.12...v1.0.13) (2024-09-21)
### Bug Fixes: ### Bug Fixes:
* Map: Fix systems cleanup
## [v1.12.3](https://github.com/wanderer-industries/wanderer/compare/v1.12.2...v1.12.3) (2024-10-18)
### Bug Fixes:
* Map: Fix regression issues
## [v1.12.2](https://github.com/wanderer-industries/wanderer/compare/v1.12.1...v1.12.2) (2024-10-16)
## [v1.12.1](https://github.com/wanderer-industries/wanderer/compare/v1.12.0...v1.12.1) (2024-10-16)
### Bug Fixes:
* Map: Fix system add error after map page refresh
## [v1.12.0](https://github.com/wanderer-industries/wanderer/compare/v1.11.5...v1.12.0) (2024-10-16)
### Features:
* Map: Prettify user settings
## [v1.11.5](https://github.com/wanderer-industries/wanderer/compare/v1.11.4...v1.11.5) (2024-10-16)
## [v1.11.4](https://github.com/wanderer-industries/wanderer/compare/v1.11.3...v1.11.4) (2024-10-16)
## [v1.11.3](https://github.com/wanderer-industries/wanderer/compare/v1.11.2...v1.11.3) (2024-10-16)
## [v1.11.2](https://github.com/wanderer-industries/wanderer/compare/v1.11.1...v1.11.2) (2024-10-15)
## [v1.11.1](https://github.com/wanderer-industries/wanderer/compare/v1.11.0...v1.11.1) (2024-10-14)
## [v1.11.0](https://github.com/wanderer-industries/wanderer/compare/v1.10.0...v1.11.0) (2024-10-14)
### Features:
* Map: Add map level option to store custom labels
## [v1.10.0](https://github.com/wanderer-industries/wanderer/compare/v1.9.0...v1.10.0) (2024-10-13)
### Features:
* Map: Link signature on splash
## [v1.5.0](https://github.com/wanderer-industries/wanderer/compare/v1.4.0...v1.5.0) (2024-10-11)
### Features:
* Map: Follow Character on Map and auto select their current system
## [v1.3.6](https://github.com/wanderer-industries/wanderer/compare/v1.3.5...v1.3.6) (2024-10-09)
### Bug Fixes:
* Signatures: Signatures update fixes
## [v1.3.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.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.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 * 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) ## [v1.0.12](https://github.com/wanderer-industries/wanderer/compare/v1.0.11...v1.0.12) (2024-09-20)
### Bug Fixes
### Bug Fixes:
* audit: Hide character for non-character map activities * 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.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) ## [v1.0.10](https://github.com/wanderer-industries/wanderer/compare/v1.0.9...v1.0.10) (2024-09-19)
### Bug Fixes
### Bug Fixes:
* signatures: Fix update signatures error if no character tracked on map * 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) ## [v1.0.9](https://github.com/wanderer-industries/wanderer/compare/v1.0.8...v1.0.9) (2024-09-19)
### Bug Fixes
### Bug Fixes:
* core: Fix system add error if it's already added on map * 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) ## [v1.0.8](https://github.com/wanderer-industries/wanderer/compare/v1.0.7...v1.0.8) (2024-09-19)
### Bug Fixes
### Bug Fixes:
* docker: Fix DB connection in docker-compose internal network * 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) ## [v1.0.4](https://github.com/wanderer-industries/wanderer/compare/v1.0.3...v1.0.4) (2024-09-18)
### Bug Fixes
### Bug Fixes:
* core: skip search results for failed character info request * 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) ## [v1.0.1](https://github.com/wanderer-industries/wanderer/compare/v1.0.0...v1.0.1) (2024-09-18)

View File

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

View File

@@ -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;
}
}

View File

@@ -1,5 +1,5 @@
/* Основной класс диалога */ /* Основной класс диалога */
.p-dialog { body .p-dialog {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
//position: absolute; //position: absolute;
@@ -7,11 +7,26 @@
left: 0; left: 0;
//visibility: hidden; //visibility: hidden;
overflow: hidden; overflow: hidden;
border-radius: 6px; border-radius: 2px;
box-shadow: 0 2px 10px 0 rgba(0,0,0,0.2); box-shadow: 0 2px 10px 0 rgba(0,0,0,0.2);
transition: box-shadow 0.3s; transition: box-shadow 0.3s;
background: #fff;
z-index: 1000; z-index: 1000;
border: 1px solid #212121;
background: var(--surface-h);
color: var(--text-color);
.p-dialog-header {
background: #171717 !important;
color: var(--text-color);
.p-dialog-header-icon:focus-visible {
box-shadow: none !important;
}
}
.p-dialog-footer {
border-top: 1px solid var(--surface-d);
}
} }
/* Стиль видимого диалога */ /* Стиль видимого диалога */
@@ -45,12 +60,12 @@
justify-content: space-between; justify-content: space-between;
padding: 1rem; padding: 1rem;
background: #f4f4f4; background: #f4f4f4;
border-bottom: 1px solid #ddd; //border-bottom: 1px solid #ddd;
} }
/* Содержимое диалога */ /* Содержимое диалога */
.p-dialog-content { .p-dialog-content {
padding: 1rem; padding: 0.5rem;
overflow-y: auto; overflow-y: auto;
flex: 1; flex: 1;
} }
@@ -78,23 +93,3 @@
.p-dialog-header-close .pi { .p-dialog-header-close .pi {
font-size: 1.25rem; font-size: 1.25rem;
} }
/* Тема Saga Blue (пример) */
body .p-dialog {
background: var(--surface-a);
color: var(--text-color);
}
body .p-dialog .p-dialog-header,
body .p-dialog .p-dialog-footer {
background: var(--surface-b);
color: var(--text-color);
}
body .p-dialog .p-dialog-header {
border-bottom: 1px solid var(--surface-d);
}
body .p-dialog .p-dialog-footer {
border-top: 1px solid var(--surface-d);
}

View File

@@ -9,6 +9,7 @@
--surface-d: #3f4b5b; --surface-d: #3f4b5b;
--surface-e: #2a323d; --surface-e: #2a323d;
--surface-f: #2a323d; --surface-f: #2a323d;
--surface-h: #171717;
--text-color: rgba(255, 255, 255, 0.87); --text-color: rgba(255, 255, 255, 0.87);
--text-color-secondary: rgba(255, 255, 255, 0.6); --text-color-secondary: rgba(255, 255, 255, 0.6);
--primary-color: #8dd0ff; --primary-color: #8dd0ff;

View File

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

View File

@@ -46,7 +46,7 @@ export const useLabelsMenu = (
} }
// const labels = getLabels(system.labels); // const labels = getLabels(system.labels);
const hasLabels = labels.list.length > 0; const hasLabels = labels?.list?.length > 0;
const statusList = hasLabels ? LABELS_ORDER : LABELS_ORDER.slice(1); const statusList = hasLabels ? LABELS_ORDER : LABELS_ORDER.slice(1);
return [ return [

View File

@@ -8,17 +8,21 @@ import { getSystemById } from '@/hooks/Mapper/helpers';
import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks'; import { useWaypointMenu } from '@/hooks/Mapper/components/contexts/hooks';
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts'; import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { FastSystemActions } from '@/hooks/Mapper/components/contexts/components'; 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 { export interface ContextMenuSystemInfoProps {
systemStatics: Map<number, SolarSystemStaticInfoRaw>; systemStatics: Map<number, SolarSystemStaticInfoRaw>;
hubs: string[]; hubs: string[];
contextMenuRef: RefObject<ContextMenu>; contextMenuRef: RefObject<ContextMenu>;
systemId: string | undefined; systemId: string | undefined;
systemIdFrom?: string | undefined;
systems: SolarSystemRawType[]; systems: SolarSystemRawType[];
onOpenSettings(): void; onOpenSettings(): void;
onHubToggle(): void; onHubToggle(): void;
onAddSystem(): void; onAddSystem(): void;
onWaypointSet: WaypointSetContextHandler; onWaypointSet: WaypointSetContextHandler;
routes: Route[];
} }
export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
@@ -30,9 +34,12 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
onAddSystem, onAddSystem,
onWaypointSet, onWaypointSet,
systemId, systemId,
systemIdFrom,
hubs, hubs,
routes,
}) => { }) => {
const getWaypointMenu = useWaypointMenu(onWaypointSet); const getWaypointMenu = useWaypointMenu(onWaypointSet);
const getJumpPlannerMenu = useJumpPlannerMenu(systems, systemIdFrom);
const items: MenuItem[] = useMemo(() => { const items: MenuItem[] = useMemo(() => {
const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined; const system = systemId ? systemStatics.get(parseInt(systemId)) : undefined;
@@ -55,7 +62,9 @@ export const ContextMenuSystemInfo: React.FC<ContextMenuSystemInfoProps> = ({
); );
}, },
}, },
{ separator: true }, { separator: true },
...getJumpPlannerMenu(system, routes),
...getWaypointMenu(systemId, system.system_class), ...getWaypointMenu(systemId, system.system_class),
{ {
label: !hubs.includes(systemId) ? 'Add in Routes' : 'Remove from Routes', 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 ( return (
<> <>

View File

@@ -4,6 +4,7 @@ import { Commands, MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Ma
import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts'; import { WaypointSetContextHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts'; import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import * as React from 'react'; import * as React from 'react';
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
interface UseContextMenuSystemHandlersProps { interface UseContextMenuSystemHandlersProps {
hubs: string[]; hubs: string[];
@@ -15,16 +16,21 @@ export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand, mapRef }: U
const contextMenuRef = useRef<ContextMenu | null>(null); const contextMenuRef = useRef<ContextMenu | null>(null);
const [system, setSystem] = useState<string>(); const [system, setSystem] = useState<string>();
const routeRef = useRef<(SolarSystemStaticInfoRaw | undefined)[]>([]);
const ref = useRef({ hubs, system, outCommand, mapRef }); const ref = useRef({ hubs, system, outCommand, mapRef });
ref.current = { hubs, system, outCommand, mapRef }; ref.current = { hubs, system, outCommand, mapRef };
const open = useCallback((ev: React.SyntheticEvent, systemId: string) => { const open = useCallback(
setSystem(systemId); (ev: React.SyntheticEvent, systemId: string, route: (SolarSystemStaticInfoRaw | undefined)[]) => {
ev.preventDefault(); setSystem(systemId);
ctxManager.next('ctxSysInfo', contextMenuRef.current); routeRef.current = route;
contextMenuRef.current?.show(ev); ev.preventDefault();
}, []); ctxManager.next('ctxSysInfo', contextMenuRef.current);
contextMenuRef.current?.show(ev);
},
[],
);
const onHubToggle = useCallback(() => { const onHubToggle = useCallback(() => {
const { hubs, system, outCommand } = ref.current; const { hubs, system, outCommand } = ref.current;
@@ -42,19 +48,19 @@ export const useContextMenuSystemInfoHandlers = ({ hubs, outCommand, mapRef }: U
}, []); }, []);
const onAddSystem = useCallback(() => { const onAddSystem = useCallback(() => {
const { system, outCommand, mapRef } = ref.current; const { system: solarSystemId, outCommand, mapRef } = ref.current;
if (!system) { if (!solarSystemId) {
return; return;
} }
outCommand({ outCommand({
type: OutCommand.addSystem, type: OutCommand.addSystem,
data: { data: {
system_id: system, system_id: solarSystemId,
}, },
}); });
setTimeout(() => { setTimeout(() => {
mapRef.current?.command(Commands.selectSystem, system); mapRef.current?.command(Commands.centerSystem, solarSystemId);
setSystem(undefined); setSystem(undefined);
}, 200); }, 200);
}, []); }, []);

View File

@@ -0,0 +1 @@
export * from './useJumpPlannerMenu.tsx';

View File

@@ -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],
);
};

View File

@@ -1,4 +1,4 @@
import React, { ForwardedRef, forwardRef, MouseEvent, useCallback } from 'react'; import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect } from 'react';
import ReactFlow, { import ReactFlow, {
Background, Background,
ConnectionMode, ConnectionMode,
@@ -94,6 +94,7 @@ interface MapCompProps {
minimapClasses?: string; minimapClasses?: string;
isShowMinimap?: boolean; isShowMinimap?: boolean;
onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void; onSystemContextMenu: (event: MouseEvent<Element>, systemId: string) => void;
showKSpaceBG?: boolean;
} }
const MapComp = ({ const MapComp = ({
@@ -105,6 +106,7 @@ const MapComp = ({
onConnectionInfoClick, onConnectionInfoClick,
onSelectionContextMenu, onSelectionContextMenu,
isShowMinimap, isShowMinimap,
showKSpaceBG,
}: MapCompProps) => { }: MapCompProps) => {
const [nodes, , onNodesChange] = useNodesState<SolarSystemRawType>(initialNodes); const [nodes, , onNodesChange] = useNodesState<SolarSystemRawType>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>[]>(initialEdges); const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>[]>(initialEdges);
@@ -169,6 +171,13 @@ const MapComp = ({
localStorage.setItem(SESSION_KEY.viewPort, JSON.stringify(viewport)); localStorage.setItem(SESSION_KEY.viewPort, JSON.stringify(viewport));
}; };
useEffect(() => {
update(x => ({
...x,
showKSpaceBG: showKSpaceBG,
}));
}, [showKSpaceBG, update]);
return ( return (
<> <>
<div className={classes.MapRoot}> <div className={classes.MapRoot}>

View File

@@ -7,6 +7,7 @@ export type MapData = MapUnionTypes & {
isConnecting: boolean; isConnecting: boolean;
hoverNodeId: string | null; hoverNodeId: string | null;
visibleNodes: Set<string>; visibleNodes: Set<string>;
showKSpaceBG: boolean;
}; };
interface MapProviderProps { interface MapProviderProps {
@@ -27,6 +28,7 @@ const INITIAL_DATA: MapData = {
connections: [], connections: [],
hoverNodeId: null, hoverNodeId: null,
visibleNodes: new Set(), visibleNodes: new Set(),
showKSpaceBG: false,
}; };
export interface MapContextProps { export interface MapContextProps {
@@ -38,6 +40,7 @@ export interface MapContextProps {
const MapContext = createContext<MapContextProps>({ const MapContext = createContext<MapContextProps>({
update: () => {}, update: () => {},
data: { ...INITIAL_DATA }, data: { ...INITIAL_DATA },
// @ts-ignore
outCommand: async () => void 0, outCommand: async () => void 0,
}); });

View File

@@ -23,6 +23,62 @@ $tooltip-bg: #202020; // Темный фон для подсказок
border-radius: 5px; border-radius: 5px;
position: relative; position: relative;
z-index: 1; 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 { &.selected {
border-color: $pastel-pink; border-color: $pastel-pink;

View File

@@ -19,9 +19,17 @@ import { PrimeIcons } from 'primereact/api';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts'; import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { OutCommand } from '@/hooks/Mapper/types'; import { OutCommand } from '@/hooks/Mapper/types';
import { useDoubleClick } from '@/hooks/Mapper/hooks/useDoubleClick.ts'; 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[]) => { const sortedLabels = (labels: string[]) => {
if (labels === null) { if (!labels) {
return []; return [];
} }
@@ -50,6 +58,7 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
statics, statics,
effect_name, effect_name,
region_name, region_name,
region_id,
is_shattered, is_shattered,
solar_system_name, solar_system_name,
} = data.system_static_info; } = data.system_static_info;
@@ -69,6 +78,7 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
isConnecting, isConnecting,
hoverNodeId, hoverNodeId,
visibleNodes, visibleNodes,
showKSpaceBG,
}, },
outCommand, outCommand,
} = useMapState(); } = useMapState();
@@ -114,6 +124,9 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
const showHandlers = isConnecting || hoverNodeId === id; const showHandlers = isConnecting || hoverNodeId === id;
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
return ( return (
<> <>
{visible && ( {visible && (
@@ -147,7 +160,11 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
</div> </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 && ( {visible && (
<> <>
<div className={classes.HeadRow}> <div className={classes.HeadRow}>
@@ -183,7 +200,13 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
)} )}
{!isWormhole && !customName && ( {!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} {region_name}
</div> </div>
)} )}

View File

@@ -11,3 +11,7 @@ export const isKnownSpace = (wormholeClassID: number) => {
return false; return false;
}; };
export const isPossibleSpace = (spaces: number[], wormholeClassID: number) => {
return spaces.includes(wormholeClassID);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types'; import { OutCommand } from '@/hooks/Mapper/types';
import { IconField } from 'primereact/iconfield'; import { IconField } from 'primereact/iconfield';
import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts'; import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit'; import { WdImageSize, WdImgButton, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
interface SystemCustomLabelDialog { interface SystemCustomLabelDialog {
systemId: string; systemId: string;
@@ -79,14 +79,14 @@ export const SystemCustomLabelDialog = ({ systemId, visible, setVisible }: Syste
// @ts-ignore // @ts-ignore
const handleInput = useCallback(e => { const handleInput = useCallback(e => {
e.target.value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''); e.target.value = e.target.value.toUpperCase().replace(/[^A-Z0-9[\](){}]/g, '');
}, []); }, []);
return ( return (
<Dialog <Dialog
header="Edit label" header="Edit label"
visible={visible} visible={visible}
draggable={false} draggable={true}
style={{ width: '250px' }} style={{ width: '250px' }}
onHide={onHide} onHide={onHide}
onShow={onShow} onShow={onShow}
@@ -100,9 +100,13 @@ export const SystemCustomLabelDialog = ({ systemId, visible, setVisible }: Syste
<IconField> <IconField>
{label !== '' && ( {label !== '' && (
<WdImgButton <WdImgButton
className="pi pi-trash p-input-icon" className="pi pi-trash text-red-400"
textSize={WdImageSize.large} textSize={WdImageSize.large}
tooltip={{ content: 'Reset label' }} tooltip={{
content: 'Remove custom label',
className: 'pi p-input-icon',
position: TooltipPosition.top,
}}
onClick={handleReset} onClick={handleReset}
/> />
)} )}
@@ -111,7 +115,7 @@ export const SystemCustomLabelDialog = ({ systemId, visible, setVisible }: Syste
aria-describedby="username-help" aria-describedby="username-help"
autoComplete="off" autoComplete="off"
value={label} value={label}
maxLength={3} maxLength={5}
onChange={e => setLabel(e.target.value)} onChange={e => setLabel(e.target.value)}
// @ts-expect-error // @ts-expect-error
ref={inputRef} ref={inputRef}

View File

@@ -0,0 +1,68 @@
import { useCallback, useRef } from 'react';
import { Dialog } from 'primereact/dialog';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { SystemSignature } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { CommandLinkSignatureToSystem } from '@/hooks/Mapper/types';
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
import {
Setting,
COSMIC_SIGNATURE,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog';
interface SystemLinkSignatureDialogProps {
data: CommandLinkSignatureToSystem;
setVisible: (visible: boolean) => void;
}
const signatureSettings: Setting[] = [{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true }];
export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignatureDialogProps) => {
const { outCommand } = useMapRootState();
const ref = useRef({ outCommand });
ref.current = { outCommand };
const handleHide = useCallback(() => {
setVisible(false);
}, [setVisible]);
const handleSelect = useCallback(
(signature: SystemSignature) => {
if (!signature) {
return;
}
const { outCommand } = ref.current;
outCommand({
type: OutCommand.linkSignatureToSystem,
data: {
...data,
signature_eve_id: signature.eve_id,
},
});
setVisible(false);
},
[data, setVisible],
);
return (
<Dialog
header="Select signature to link"
visible
draggable={false}
style={{ width: '500px' }}
onHide={handleHide}
contentClassName="!p-0"
>
<SystemSignaturesContent
systemId={`${data.solar_system_source}`}
settings={signatureSettings}
onSelect={handleSelect}
selectable={true}
/>
</Dialog>
);
};

View File

@@ -0,0 +1 @@
export * from './SystemLinkSignatureDialog';

View File

@@ -90,7 +90,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
}, []); }, []);
const handleInput = useCallback((e: any) => { const handleInput = useCallback((e: any) => {
e.target.value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''); e.target.value = e.target.value.toUpperCase().replace(/[^A-Z0-9[\](){}]/g, '');
}, []); }, []);
return ( return (
@@ -160,7 +160,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
aria-describedby="label" aria-describedby="label"
autoComplete="off" autoComplete="off"
value={label} value={label}
maxLength={3} maxLength={5}
onChange={e => setLabel(e.target.value)} onChange={e => setLabel(e.target.value)}
onInput={handleInput} onInput={handleInput}
/> />

View File

@@ -2,3 +2,4 @@ export * from './Widget';
export * from './WidgetsGrid'; export * from './WidgetsGrid';
export * from './SystemSettingsDialog'; export * from './SystemSettingsDialog';
export * from './SystemCustomLabelDialog'; export * from './SystemCustomLabelDialog';
export * from './SystemLinkSignatureDialog';

View File

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

View File

@@ -38,7 +38,7 @@ export const RoutesWidgetContent = () => {
const { loading } = useLoadRoutes(); const { loading } = useLoadRoutes();
const { systems: systemStatics, loadSystems } = useLoadSystemStatic({ systems: hubs ?? [] }); const { systems: systemStatics, loadSystems, lastUpdateKey } = useLoadSystemStatic({ systems: hubs ?? [] });
const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers({ const { open, ...systemCtxProps } = useContextMenuSystemInfoHandlers({
outCommand, outCommand,
hubs, hubs,
@@ -51,9 +51,10 @@ export const RoutesWidgetContent = () => {
return { ...systemStatics.get(parseInt(x))!, ...(sys && { customName: sys.name ?? '' }) }; 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 ( return (
routes?.routes routes?.routes
.sort(sortByDist) .sort(sortByDist)
@@ -70,15 +71,17 @@ export const RoutesWidgetContent = () => {
); );
}, [routes?.routes, routes?.systems_static_data, systemId]); }, [routes?.routes, routes?.systems_static_data, systemId]);
const refData = useRef({ open, loadSystems }); const refData = useRef({ open, loadSystems, preparedRoutes });
refData.current = { open, loadSystems }; refData.current = { open, loadSystems, preparedRoutes };
useEffect(() => { useEffect(() => {
(async () => await refData.current.loadSystems(hubs))(); (async () => await refData.current.loadSystems(hubs))();
}, [hubs]); }, [hubs]);
const handleClick = useCallback((e: MouseEvent, systemId: string) => { 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( const handleContextMenu = useCallback(
@@ -114,6 +117,10 @@ export const RoutesWidgetContent = () => {
{preparedRoutes.map(route => { {preparedRoutes.map(route => {
const sys = preparedHubs.find(x => x.solar_system_id === route.destination)!; 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 ( return (
<> <>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
@@ -141,7 +148,14 @@ export const RoutesWidgetContent = () => {
</div> </div>
)} )}
<ContextMenuSystemInfo hubs={hubs} systems={systems} systemStatics={systemStatics} {...systemCtxProps} /> <ContextMenuSystemInfo
hubs={hubs}
routes={preparedRoutes}
systems={systems}
systemStatics={systemStatics}
systemIdFrom={systemId}
{...systemCtxProps}
/>
</> </>
); );
}; };

View File

@@ -5,6 +5,14 @@ import { Checkbox } from 'primereact/checkbox';
export type Setting = { key: string; name: string; value: boolean }; export type Setting = { key: string; name: string; value: boolean };
export const COSMIC_SIGNATURE = 'Cosmic Signature';
export const COSMIC_ANOMALY = 'Cosmic Anomaly';
export const DEPLOYABLE = 'Deployable';
export const STRUCTURE = 'Structure';
export const STARBASE = 'Starbase';
export const SHIP = 'Ship';
export const DRONE = 'Drone';
interface SystemSignatureSettingsDialogProps { interface SystemSignatureSettingsDialogProps {
settings: Setting[]; settings: Setting[];
onSave: (settings: Setting[]) => void; onSave: (settings: Setting[]) => void;

View File

@@ -1,7 +1,17 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components'; import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { InfoDrawer, LayoutEventBlocker, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit'; import { InfoDrawer, LayoutEventBlocker, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemSignaturesContent } from './SystemSignaturesContent'; import { SystemSignaturesContent } from './SystemSignaturesContent';
import { Setting, SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog'; import {
Setting,
SystemSignatureSettingsDialog,
COSMIC_SIGNATURE,
COSMIC_ANOMALY,
DEPLOYABLE,
STRUCTURE,
STARBASE,
SHIP,
DRONE,
} from './SystemSignatureSettingsDialog';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
@@ -9,14 +19,6 @@ import { PrimeIcons } from 'primereact/api';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider'; import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const COSMIC_SIGNATURE = 'Cosmic Signature';
export const COSMIC_ANOMALY = 'Cosmic Anomaly';
export const DEPLOYABLE = 'Deployable';
export const STRUCTURE = 'Structure';
export const STARBASE = 'Starbase';
export const SHIP = 'Ship';
export const DRONE = 'Drone';
const settings: Setting[] = [ const settings: Setting[] = [
{ key: COSMIC_ANOMALY, name: 'Show Anomalies', value: true }, { key: COSMIC_ANOMALY, name: 'Show Anomalies', value: true },
{ key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true }, { key: COSMIC_SIGNATURE, name: 'Show Cosmic Signatures', value: true },

View File

@@ -4,3 +4,7 @@
font-size: 12px !important; font-size: 12px !important;
line-height: 8px; line-height: 8px;
} }
.Table {
}

View File

@@ -4,7 +4,7 @@ import { parseSignatures } from '@/hooks/Mapper/helpers';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts'; import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
import { WdTooltip, WdTooltipHandlers } from '@/hooks/Mapper/components/ui-kit'; 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 { Column } from 'primereact/column';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useRefState from 'react-usestateref'; import useRefState from 'react-usestateref';
@@ -24,21 +24,42 @@ import {
renderIcon, renderIcon,
renderName, renderName,
renderTimeLeft, renderTimeLeft,
renderLinkedSystem,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders'; } 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 { interface SystemSignaturesContentProps {
systemId: string; systemId: string;
settings: Setting[]; settings: Setting[];
selectable?: boolean;
onSelect?: (signature: SystemSignature) => void;
} }
export const SystemSignaturesContent = ({ systemId, settings }: SystemSignaturesContentProps) => { export const SystemSignaturesContent = ({ systemId, settings, selectable, onSelect }: SystemSignaturesContentProps) => {
const { outCommand } = useMapRootState(); const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<SystemSignature[]>([]); const [signatures, setSignatures, signaturesRef] = useRefState<SystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<SystemSignature[]>([]); const [selectedSignatures, setSelectedSignatures] = useState<SystemSignature[]>([]);
const [nameColumnWidth, setNameColumnWidth] = useState('auto'); const [nameColumnWidth, setNameColumnWidth] = useState('auto');
const [parsedSignatures, setParsedSignatures] = useState<SystemSignature[]>([]);
const [askUser, setAskUser] = useState(false);
const [hoveredSig, setHoveredSig] = useState<SystemSignature | null>(null); const [hoveredSig, setHoveredSig] = useState<SystemSignature | null>(null);
const [sortSettings, setSortSettings] = useLocalStorageState<SystemSignaturesSortSettings>('window:signatures:sort', {
defaultValue: SORT_DEFAULT_VALUES,
});
const tableRef = useRef<HTMLDivElement>(null); const tableRef = useRef<HTMLDivElement>(null);
const compact = useMaxWidth(tableRef, 260); const compact = useMaxWidth(tableRef, 260);
const medium = useMaxWidth(tableRef, 380); const medium = useMaxWidth(tableRef, 380);
@@ -50,7 +71,7 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
const handleResize = useCallback(() => { const handleResize = useCallback(() => {
if (tableRef.current) { if (tableRef.current) {
const tableWidth = tableRef.current.offsetWidth; const tableWidth = tableRef.current.offsetWidth;
const otherColumnsWidth = 265; const otherColumnsWidth = 276;
const availableWidth = tableWidth - otherColumnsWidth; const availableWidth = tableWidth - otherColumnsWidth;
setNameColumnWidth(`${availableWidth}px`); setNameColumnWidth(`${availableWidth}px`);
} }
@@ -70,12 +91,33 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
data: { system_id: systemId }, data: { system_id: systemId },
}); });
setAskUser(false);
setSignatures(signatures); setSignatures(signatures);
}, [outCommand, systemId]); }, [outCommand, systemId]);
// const updateSignatures = useCallback(
// async (newSignatures: SystemSignature[], updateOnly: boolean) => {
// const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures, updateOnly);
// const { signatures: updatedSignatures } = await outCommand({
// type: OutCommand.updateSignatures,
// data: {
// system_id: systemId,
// added,
// updated,
// removed,
// },
// });
// setSignatures(() => updatedSignatures);
// setSelectedSignatures([]);
// },
// [outCommand, systemId],
// );
const handleUpdateSignatures = useCallback( const handleUpdateSignatures = useCallback(
async (newSignatures: SystemSignature[]) => { async (newSignatures: SystemSignature[], updateOnly: boolean) => {
const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures); const { added, updated, removed } = getActualSigs(signaturesRef.current, newSignatures, updateOnly);
const { signatures: updatedSignatures } = await outCommand({ const { signatures: updatedSignatures } = await outCommand({
type: OutCommand.updateSignatures, type: OutCommand.updateSignatures,
@@ -94,37 +136,76 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
); );
const handleDeleteSelected = useCallback(async () => { const handleDeleteSelected = useCallback(async () => {
if (selectable) {
return;
}
if (selectedSignatures.length === 0) { if (selectedSignatures.length === 0) {
return; return;
} }
const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id); const selectedSignaturesEveIds = selectedSignatures.map(x => x.eve_id);
await handleUpdateSignatures(signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id))); await handleUpdateSignatures(
}, [handleUpdateSignatures, signatures, selectedSignatures]); signatures.filter(x => !selectedSignaturesEveIds.includes(x.eve_id)),
false,
);
}, [handleUpdateSignatures, selectable, signatures, selectedSignatures]);
const handleSelectAll = useCallback(() => { const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures); setSelectedSignatures(signatures);
}, [signatures]); }, [signatures]);
const handleReplaceAll = useCallback(() => {
handleUpdateSignatures(parsedSignatures, false);
setAskUser(false);
}, [parsedSignatures, handleUpdateSignatures]);
const handleUpdateOnly = useCallback(() => {
handleUpdateSignatures(parsedSignatures, true);
setAskUser(false);
}, [parsedSignatures, handleUpdateSignatures]);
const handleSelectSignatures = useCallback(
e => {
if (selectable) {
onSelect?.(e.value);
} else {
setSelectedSignatures(e.value);
}
},
[onSelect, selectable],
);
useHotkey(true, ['a'], handleSelectAll); useHotkey(true, ['a'], handleSelectAll);
useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected); useHotkey(false, ['Backspace', 'Delete'], handleDeleteSelected);
useEffect(() => { useEffect(() => {
if (selectable) {
return;
}
if (!clipboardContent) { if (!clipboardContent) {
return; return;
} }
const signatures = parseSignatures( const newSignatures = parseSignatures(
clipboardContent, clipboardContent,
settings.map(x => x.key), settings.map(x => x.key),
); );
handleUpdateSignatures(signatures); const { removed } = getActualSigs(signaturesRef.current, newSignatures, false);
}, [clipboardContent]);
if (!signaturesRef.current || !signaturesRef.current.length || !removed.length) {
handleUpdateSignatures(newSignatures, false);
} else {
setParsedSignatures(newSignatures);
setAskUser(true);
}
}, [clipboardContent, selectable]);
useEffect(() => { useEffect(() => {
if (!systemId) { if (!systemId) {
setSignatures([]); setSignatures([]);
setAskUser(false);
return; return;
} }
@@ -159,83 +240,140 @@ export const SystemSignaturesContent = ({ systemId, settings }: SystemSignatures
setHoveredSig(null); 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 ( return (
<div ref={tableRef} className="h-full"> <>
{filteredSignatures.length === 0 ? ( <div ref={tableRef} className={'h-full '}>
<div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm"> {filteredSignatures.length === 0 ? (
No signatures <div className="w-full h-full flex justify-center items-center select-none text-stone-400/80 text-sm">
</div> No signatures
) : ( </div>
<> ) : (
<DataTable <>
value={filteredSignatures} <DataTable
size="small" className={classes.Table}
selectionMode="multiple" value={filteredSignatures}
selection={selectedSignatures} size="small"
onSelectionChange={e => setSelectedSignatures(e.value)} selectionMode={selectable ? 'single' : 'multiple'}
dataKey="eve_id" selection={selectedSignatures}
tableClassName="w-full select-none" metaKeySelection
resizableColumns onSelectionChange={handleSelectSignatures}
rowHover dataKey="eve_id"
selectAll tableClassName="w-full select-none"
showHeaders={false} resizableColumns={false}
onRowMouseEnter={handleEnterRow} rowHover
onRowMouseLeave={handleLeaveRow} selectAll
rowClassName={row => { sortField={sortSettings.sortField}
if (selectedSignatures.some(x => x.eve_id === row.eve_id)) { sortOrder={sortSettings.sortOrder}
return clsx(classes.TableRowCompact, 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200'); onSort={event => setSortSettings(() => ({ sortField: event.sortField, sortOrder: event.sortOrder }))}
} onRowMouseEnter={compact || medium ? handleEnterRow : undefined}
onRowMouseLeave={compact || medium ? handleLeaveRow : undefined}
rowClassName={row => {
if (selectedSignatures.some(x => x.eve_id === row.eve_id)) {
return clsx(classes.TableRowCompact, 'bg-amber-500/50 hover:bg-amber-500/70 transition duration-200');
}
const dateClass = getRowColorByTimeLeft(row.updated_at ? new Date(row.updated_at) : undefined); const dateClass = getRowColorByTimeLeft(row.updated_at ? new Date(row.updated_at) : undefined);
if (!dateClass) { if (!dateClass) {
return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200'); return clsx(classes.TableRowCompact, 'hover:bg-purple-400/20 transition duration-200');
} }
return clsx(classes.TableRowCompact, dateClass); return clsx(classes.TableRowCompact, dateClass);
}} }}
> >
<Column <Column
bodyClassName="p-0 px-1" bodyClassName="p-0 px-1"
field="group" field="group"
body={renderIcon} body={renderIcon}
style={{ maxWidth: 26, minWidth: 26, width: 26 }} style={{ maxWidth: 26, minWidth: 26, width: 26, height: 25 }}
></Column> ></Column>
<Column <Column
field="eve_id" field="eve_id"
header="Id" header="Id"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 72, minWidth: 72, width: 72 }} style={{ maxWidth: 72, minWidth: 72, width: 72 }}
></Column> sortable
<Column ></Column>
field="group" <Column
header="Group" field="group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" header="Group"
hidden={compact} bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
></Column> hidden={compact}
<Column sortable
field="name" ></Column>
header="Name" <Column
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap" field="name"
body={renderName} header="Name"
style={{ maxWidth: nameColumnWidth }} bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={compact || medium} body={renderName}
></Column> style={{ maxWidth: nameColumnWidth }}
<Column hidden={compact || medium}
field="updated_at" sortable
header="Updated" ></Column>
dataType="date" <Column
bodyClassName="w-[80px] text-ellipsis overflow-hidden whitespace-nowrap" field="linked_system"
body={renderTimeLeft} header="Leads To"
></Column> headerClassName="whitespace-nowrap"
</DataTable> bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
</> body={renderLinkedSystem}
)} style={{ maxWidth: nameColumnWidth }}
<WdTooltip hidden={compact}
className="bg-stone-900/95 text-slate-50" sortable
ref={tooltipRef} ></Column>
content={hoveredSig ? <SignatureView {...hoveredSig} /> : null}
/> <Column
</div> field="updated_at"
header="Updated"
dataType="date"
bodyClassName="w-[80px] text-ellipsis overflow-hidden whitespace-nowrap"
body={renderTimeLeft}
sortable
></Column>
{/*<Column*/}
{/* bodyClassName="p-0 pl-1 pr-2"*/}
{/* field="group"*/}
{/* body={renderToolbar}*/}
{/* headerClassName={headerClasses}*/}
{/* style={{ maxWidth: 26, minWidth: 26, width: 26 }}*/}
{/*></Column>*/}
</DataTable>
</>
)}
<WdTooltip
className="bg-stone-900/95 text-slate-50"
ref={tooltipRef}
content={hoveredSig ? <SignatureView {...hoveredSig} /> : null}
/>
{askUser && (
<div className="absolute left-[1px] top-[29px] h-[calc(100%-30px)] w-[calc(100%-3px)] bg-stone-900/10 backdrop-blur-sm">
<div className="absolute top-0 left-0 w-full h-full flex flex-col items-center justify-center">
<div className="text-stone-400/80 text-sm">
<div className="flex flex-col text-center gap-2">
<button className="p-button p-component p-button-outlined p-button-sm btn-wide">
<span className="p-button-label p-c" onClick={handleUpdateOnly}>
Update
</span>
</button>
<button className="p-button p-component p-button-outlined p-button-sm btn-wide">
<span className="p-button-label p-c" onClick={handleReplaceAll}>
Update & Delete missing
</span>
</button>
</div>
</div>
</div>
</div>
)}
</div>
</>
); );
}; };

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
export * from './renderIcon'; export * from './renderIcon';
export * from './renderName'; export * from './renderName';
export * from './renderTimeLeft'; export * from './renderTimeLeft';
export * from './renderLinkedSystem';

View File

@@ -0,0 +1,20 @@
import clsx from 'clsx';
import { SystemSignature } from '@/hooks/Mapper/types';
import { SystemViewStandalone } from '@/hooks/Mapper/components/ui-kit';
export const renderLinkedSystem = (row: SystemSignature) => {
if (!row.linked_system) {
return null;
}
return (
<span title={row.linked_system?.solar_system_name}>
<SystemViewStandalone
className={clsx('select-none text-center cursor-context-menu')}
hideRegion
{...row.linked_system}
/>
</span>
);
};

View File

@@ -3,7 +3,7 @@ import { TimeLeft } from '@/hooks/Mapper/components/ui-kit';
export const renderTimeLeft = (row: SystemSignature) => { export const renderTimeLeft = (row: SystemSignature) => {
return ( 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} /> <TimeLeft cDate={row.updated_at ? new Date(row.updated_at) : undefined} />
</div> </div>
); );

View File

@@ -6,6 +6,8 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { OnTheMap, RightBar } from '@/hooks/Mapper/components/mapRootContent/components'; import { OnTheMap, RightBar } from '@/hooks/Mapper/components/mapRootContent/components';
import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/components/MapContextMenu/MapContextMenu.tsx'; import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/components/MapContextMenu/MapContextMenu.tsx';
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
import { MapSettings } from "@/hooks/Mapper/components/mapRootContent/components/MapSettings";
export interface MapRootContentProps {} export interface MapRootContentProps {}
@@ -15,9 +17,13 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const { isShowMenu } = interfaceSettings; const { isShowMenu } = interfaceSettings;
const [showOnTheMap, setShowOnTheMap] = useState(false); const [showOnTheMap, setShowOnTheMap] = useState(false);
const [showMapSettings, setShowMapSettings] = useState(false);
const mapInterface = <MapInterface />; const mapInterface = <MapInterface />;
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []); const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
useSkipContextMenu();
return ( return (
<Layout map={<MapWrapper refn={mapRef} />}> <Layout map={<MapWrapper refn={mapRef} />}>
@@ -28,18 +34,19 @@ export const MapRootContent = ({}: MapRootContentProps) => {
{mapInterface} {mapInterface}
</div> </div>
<div className="absolute top-0 right-0 w-14 h-[calc(100%+3.5rem)] pointer-events-auto"> <div className="absolute top-0 right-0 w-14 h-[calc(100%+3.5rem)] pointer-events-auto">
<RightBar onShowOnTheMap={handleShowOnTheMap} /> <RightBar onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
</div> </div>
</div> </div>
) : ( ) : (
<div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none"> <div className="absolute top-0 left-14 w-[calc(100%-3.5rem)] h-[calc(100%-3.5rem)] pointer-events-none">
<Topbar> <Topbar>
<MapContextMenu onShowOnTheMap={handleShowOnTheMap} /> <MapContextMenu onShowOnTheMap={handleShowOnTheMap} onShowMapSettings={handleShowMapSettings} />
</Topbar> </Topbar>
{mapInterface} {mapInterface}
</div> </div>
)} )}
<OnTheMap show={showOnTheMap} onHide={() => setShowOnTheMap(false)} /> <OnTheMap show={showOnTheMap} onHide={() => setShowOnTheMap(false)} />
<MapSettings show={showMapSettings} onHide={() => setShowMapSettings(false)} />
</Layout> </Layout>
); );
}; };

View File

@@ -8,10 +8,11 @@ import { MenuItem } from 'primereact/menuitem';
export interface MapContextMenuProps { export interface MapContextMenuProps {
onShowOnTheMap?: () => void; onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
} }
export const MapContextMenu = ({ onShowOnTheMap }: MapContextMenuProps) => { export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings }: MapContextMenuProps) => {
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState(); const { outCommand, setInterfaceSettings } = useMapRootState();
const menuRight = useRef<Menu>(null); const menuRight = useRef<Menu>(null);
@@ -22,13 +23,6 @@ export const MapContextMenu = ({ onShowOnTheMap }: MapContextMenuProps) => {
}); });
}, [outCommand]); }, [outCommand]);
const toggleMinimap = useCallback(() => {
setInterfaceSettings(x => ({
...x,
isShowMinimap: !x.isShowMinimap,
}));
}, [setInterfaceSettings]);
const items = useMemo(() => { const items = useMemo(() => {
return [ return [
{ {
@@ -43,9 +37,9 @@ export const MapContextMenu = ({ onShowOnTheMap }: MapContextMenuProps) => {
}, },
{ separator: true }, { separator: true },
{ {
label: interfaceSettings.isShowMinimap ? 'Hide minimap' : 'Show minimap', label: 'Settings',
icon: `pi ${interfaceSettings.isShowMinimap ? 'pi-eye-slash' : 'pi-eye'}`, icon: `pi pi-cog`,
command: toggleMinimap, command: onShowMapSettings,
}, },
{ {
label: 'Dock menu', label: 'Dock menu',
@@ -57,7 +51,7 @@ export const MapContextMenu = ({ onShowOnTheMap }: MapContextMenuProps) => {
})), })),
}, },
] as MenuItem[]; ] as MenuItem[];
}, [handleAddCharacter, interfaceSettings.isShowMinimap, onShowOnTheMap, setInterfaceSettings, toggleMinimap]); }, [handleAddCharacter, onShowMapSettings, onShowOnTheMap, setInterfaceSettings]);
return ( return (
<div className="ml-1"> <div className="ml-1">

View File

@@ -0,0 +1,127 @@
.verticalTabsContainer {
display: flex;
width: 100%;
min-height: 300px;
:global {
.p-tabview {
width: 100%;
display: flex;
align-items: flex-start;
}
.p-tabview-panels {
padding: 6px 1rem !important;
flex-grow: 1;
}
.p-tabview-nav-container {
border-right: none;
height: 100%;
}
.p-tabview-nav {
flex-direction: column;
width: 150px;
min-height: 100%;
border: none;
li {
width: 100%;
border-right: 4px solid var(--surface-hover);
background-color: var(--surface-card);
transition: background-color 200ms, border-right-color 200ms;
&:hover {
background-color: var(--surface-hover);
border-right: 4px solid var(--surface-100);
}
.p-tabview-nav-link {
transition: color 200ms;
justify-content: flex-end;
padding: 10px;
//background-color: var(--surface-card);
background-color: initial;
border: none;
color: var(--gray-400);
border-radius: initial;
font-weight: 400;
margin: 0;
}
&.p-tabview-selected {
background-color: var(--surface-50);
border-right: 4px solid var(--primary-color);
.p-tabview-nav-link {
font-weight: 600;
color: var(--primary-color);
}
&:hover {
//background-color: var(--surface-hover);
border-right: 4px solid var(--primary-color);
}
}
}
}
.p-tabview-panel {
flex-grow: 1;
}
}
}
.CheckboxContainer {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
& > span:nth-child(1) {
color: var(--gray-200);
font-size: 13px;
}
& > :nth-child(2){
border-bottom: 2px dotted #3f3f3f;
height: 2px;
margin: 0 12px;
}
}
/* Уменьшение размеров InputSwitch с использованием глобальных стилей */
.smallInputSwitch {
height: 100%;
display: flex;
align-items: center;
:global {
.p-inputswitch {
height: 1rem;
width: 2rem;
&.p-inputswitch-checked {
.p-inputswitch-slider::before {
transform: translateX(1rem);
}
}
&.p-highlight .p-inputswitch-slider:before {
transform: translateX(1rem);
}
.p-inputswitch-slider::before {
width: 0.8rem;
height: 0.8rem;
margin-top: -0.4rem;
margin-left: -3px;
}
}
}
}

View File

@@ -0,0 +1,167 @@
import styles from './MapSettings.module.scss';
import { Dialog } from 'primereact/dialog';
import { useCallback, useMemo, useState } from 'react';
import { TabPanel, TabView } from 'primereact/tabview';
import { PrettySwitchbox } from './components';
import { InterfaceStoredSettings, InterfaceStoredSettingsProps, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
export enum UserSettingsRemoteProps {
link_signature_on_splash = 'link_signature_on_splash',
select_on_spash = 'select_on_spash',
}
export const DEFAULT_REMOTE_SETTINGS = {
[UserSettingsRemoteProps.link_signature_on_splash]: false,
[UserSettingsRemoteProps.select_on_spash]: false,
};
export const UserSettingsRemoteList = [
UserSettingsRemoteProps.link_signature_on_splash,
UserSettingsRemoteProps.select_on_spash,
];
export type UserSettingsRemote = {
link_signature_on_splash: boolean;
select_on_spash: boolean;
};
export type UserSettings = UserSettingsRemote & InterfaceStoredSettings;
export interface MapSettingsProps {
show: boolean;
onHide: () => void;
}
type CheckboxesList = {
prop: keyof UserSettings;
label: string;
}[];
const COMMON_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: InterfaceStoredSettingsProps.isShowMinimap, label: 'Show Minimap' },
];
const SYSTEMS_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: InterfaceStoredSettingsProps.isShowKSpace, label: 'Highlight Low/High-security systems' },
{ prop: UserSettingsRemoteProps.select_on_spash, label: 'Auto-select splashed' },
];
const SIGNATURES_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: UserSettingsRemoteProps.link_signature_on_splash, label: 'Link signature on splash' },
];
const UI_CHECKBOXES_PROPS: CheckboxesList = [
{ prop: InterfaceStoredSettingsProps.isShowMenu, label: 'Enable compact map menu bar' },
];
export const MapSettings = ({ show, onHide }: MapSettingsProps) => {
const [activeIndex, setActiveIndex] = useState(0);
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
const [userRemoteSettings, setUserRemoteSettings] = useState<UserSettingsRemote>({ ...DEFAULT_REMOTE_SETTINGS });
const mergedSettings = useMemo(() => {
return {
...interfaceSettings,
...userRemoteSettings,
};
}, [userRemoteSettings, interfaceSettings]);
const handleShow = async () => {
const { user_settings } = await outCommand({
type: OutCommand.getUserSettings,
data: null,
});
setUserRemoteSettings({
...user_settings,
});
};
const handleChangeChecked = useCallback(
(prop: keyof UserSettings) => async (checked: boolean) => {
// @ts-ignore
if (UserSettingsRemoteList.includes(prop)) {
const newRemoteSettings = {
...userRemoteSettings,
[prop]: checked,
};
await outCommand({
type: OutCommand.updateUserSettings,
data: newRemoteSettings,
});
setUserRemoteSettings(newRemoteSettings);
return;
}
setInterfaceSettings({
...interfaceSettings,
[prop]: checked,
});
},
[interfaceSettings, outCommand, setInterfaceSettings, userRemoteSettings],
);
const renderCheckboxesList = (list: CheckboxesList) => {
return list.map(x => {
return (
<PrettySwitchbox
key={x.prop}
label={x.label}
checked={mergedSettings[x.prop]}
setChecked={handleChangeChecked(x.prop)}
/>
);
});
};
return (
<Dialog
header="Map settings"
visible={show}
draggable={false}
style={{ width: '550px' }}
onShow={handleShow}
onHide={() => {
if (!show) {
return;
}
setActiveIndex(0);
onHide();
}}
>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<div className={styles.verticalTabsContainer}>
<TabView
activeIndex={activeIndex}
onTabChange={e => setActiveIndex(e.index)}
className={styles.verticalTabView}
>
<TabPanel header="Common" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">{renderCheckboxesList(COMMON_CHECKBOXES_PROPS)}</div>
</TabPanel>
<TabPanel header="Systems" headerClassName={styles.verticalTabHeader}>
<div className="w-full h-full flex flex-col gap-1">
{renderCheckboxesList(SYSTEMS_CHECKBOXES_PROPS)}
</div>
</TabPanel>
<TabPanel disabled header="Connections" headerClassName={styles.verticalTabHeader}>
<p>Connections</p>
</TabPanel>
<TabPanel header="Signatures" headerClassName={styles.verticalTabHeader}>
{renderCheckboxesList(SIGNATURES_CHECKBOXES_PROPS)}
</TabPanel>
<TabPanel header="User Interface" headerClassName={styles.verticalTabHeader}>
{renderCheckboxesList(UI_CHECKBOXES_PROPS)}
</TabPanel>
</TabView>
</div>
</div>
</div>
</Dialog>
);
};

View File

@@ -0,0 +1,48 @@
.CheckboxContainer {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
& > span:nth-child(1) {
color: var(--gray-200);
font-size: 13px;
user-select: none;
}
& > :nth-child(2){
border-bottom: 2px dotted #3f3f3f;
height: 1px;
margin: 0 12px;
}
}
/* Уменьшение размеров InputSwitch с использованием глобальных стилей */
.smallInputSwitch {
height: 100%;
display: flex;
align-items: center;
:global {
.p-inputswitch {
height: 1rem;
width: 2rem;
&.p-inputswitch-checked {
.p-inputswitch-slider::before {
transform: translateX(1rem);
}
}
&.p-highlight .p-inputswitch-slider:before {
transform: translateX(1rem);
}
.p-inputswitch-slider::before {
width: 0.8rem;
height: 0.8rem;
margin-top: -0.4rem;
margin-left: -3px;
}
}
}
}

View File

@@ -0,0 +1,20 @@
import styles from './MapSettings.module.scss';
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit';
interface PrettySwitchboxProps {
checked: boolean;
setChecked: (checked: boolean) => void;
label: string;
}
export const PrettySwitchbox = ({ checked, setChecked, label }: PrettySwitchboxProps) => {
return (
<label className={styles.CheckboxContainer}>
<span>{label}</span>
<div />
<div className={styles.smallInputSwitch}>
<WdCheckbox size="m" label={''} value={checked} onChange={e => setChecked(e.checked ?? false)} />
</div>
</label>
);
};

View File

@@ -0,0 +1 @@
export * from './PrettySwitchbox';

View File

@@ -0,0 +1 @@
export * from './PrettySwitchbox';

View File

@@ -0,0 +1 @@
export * from './MapSettings';

View File

@@ -8,11 +8,14 @@ import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
interface RightBarProps { interface RightBarProps {
onShowOnTheMap?: () => void; onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
} }
export const RightBar = ({ onShowOnTheMap }: RightBarProps) => { export const RightBar = ({ onShowOnTheMap, onShowMapSettings }: RightBarProps) => {
const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState(); const { outCommand, interfaceSettings, setInterfaceSettings } = useMapRootState();
const isShowMinimap = interfaceSettings.isShowMinimap === undefined ? true : interfaceSettings.isShowMinimap;
const handleAddCharacter = useCallback(() => { const handleAddCharacter = useCallback(() => {
outCommand({ outCommand({
type: OutCommand.addCharacter, type: OutCommand.addCharacter,
@@ -27,6 +30,13 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
})); }));
}, [setInterfaceSettings]); }, [setInterfaceSettings]);
const toggleKSpace = useCallback(() => {
setInterfaceSettings(x => ({
...x,
isShowKSpace: !x.isShowKSpace,
}));
}, [setInterfaceSettings]);
const toggleMenu = useCallback(() => { const toggleMenu = useCallback(() => {
setInterfaceSettings(x => ({ setInterfaceSettings(x => ({
...x, ...x,
@@ -50,7 +60,7 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
type="button" type="button"
onClick={handleAddCharacter} onClick={handleAddCharacter}
> >
<i className="pi pi-user-plus text-lg"></i> <i className="pi pi-user-plus"></i>
</button> </button>
</WdTooltipWrapper> </WdTooltipWrapper>
@@ -60,26 +70,44 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
type="button" type="button"
onClick={onShowOnTheMap} onClick={onShowOnTheMap}
> >
<i className="pi pi-hashtag text-lg"></i> <i className="pi pi-hashtag"></i>
</button> </button>
</WdTooltipWrapper> </WdTooltipWrapper>
</div> </div>
<div className="flex flex-col items-center mb-2 gap-1"> <div className="flex flex-col items-center mb-2 gap-1">
<WdTooltipWrapper content="User settings" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowMapSettings}
>
<i className="pi pi-cog"></i>
</button>
</WdTooltipWrapper>
<WdTooltipWrapper <WdTooltipWrapper
content={interfaceSettings.isShowMinimap ? 'Hide minimap' : 'Show minimap'} content={
interfaceSettings.isShowKSpace ? 'Hide highlighting Imperial Space' : 'Show highlighting Imperial Space'
}
position={TooltipPosition.left} 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}
>
<i className={interfaceSettings.isShowKSpace ? 'hero-cloud-solid' : 'hero-cloud'}></i>
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content={isShowMinimap ? 'Hide minimap' : 'Show minimap'} position={TooltipPosition.left}>
<button <button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto" className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button" type="button"
onClick={toggleMinimap} onClick={toggleMinimap}
> >
{interfaceSettings.isShowMinimap ? ( <i className={isShowMinimap ? 'pi pi-eye' : 'pi pi-eye-slash'}></i>
<i className="pi pi-eye text-lg"></i>
) : (
<i className="pi pi-eye-slash text-lg"></i>
)}
</button> </button>
</WdTooltipWrapper> </WdTooltipWrapper>
@@ -89,7 +117,7 @@ export const RightBar = ({ onShowOnTheMap }: RightBarProps) => {
type="button" type="button"
onClick={toggleMenu} onClick={toggleMenu}
> >
<i className="pi pi-window-minimize text-lg"></i> <i className="pi pi-window-minimize"></i>
</button> </button>
</WdTooltipWrapper> </WdTooltipWrapper>
</div> </div>

View File

@@ -5,13 +5,20 @@ import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts'; import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts'; import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts';
import { SystemCustomLabelDialog, SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components'; import {
SystemCustomLabelDialog,
SystemSettingsDialog,
SystemLinkSignatureDialog,
} from '@/hooks/Mapper/components/mapInterface/components';
import classes from './MapWrapper.module.scss'; import classes from './MapWrapper.module.scss';
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections'; import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple'; import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
import { getSystemById } from '@/hooks/Mapper/helpers'; import { getSystemById } from '@/hooks/Mapper/helpers';
import { Node } from 'reactflow'; import { Node } from 'reactflow';
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
import { useMapEventListener } from '@/hooks/Mapper/events';
import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider'; import { STORED_INTERFACE_DEFAULT_VALUES } from '@/hooks/Mapper/mapRootProvider/MapRootProvider';
interface MapWrapperProps { interface MapWrapperProps {
@@ -24,7 +31,7 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => {
update, update,
outCommand, outCommand,
data: { selectedConnections, selectedSystems, hubs, systems }, data: { selectedConnections, selectedSystems, hubs, systems },
interfaceSettings: { isShowMenu, isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap }, interfaceSettings: { isShowMenu, isShowMinimap = STORED_INTERFACE_DEFAULT_VALUES.isShowMinimap, isShowKSpace },
} = useMapRootState(); } = useMapRootState();
const { open, ...systemContextProps } = useContextMenuSystemHandlers({ systems, hubs, outCommand }); const { open, ...systemContextProps } = useContextMenuSystemHandlers({ systems, hubs, outCommand });
@@ -53,6 +60,7 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => {
); );
const [openSettings, setOpenSettings] = useState<string | null>(null); const [openSettings, setOpenSettings] = useState<string | null>(null);
const [openLinkSignatures, setOpenLinkSignatures] = useState<any | null>(null);
const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null); const [openCustomLabel, setOpenCustomLabel] = useState<string | null>(null);
const handleCommand: OutCommandHandler = useCallback( const handleCommand: OutCommandHandler = useCallback(
event => { event => {
@@ -60,6 +68,9 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => {
case OutCommand.openSettings: case OutCommand.openSettings:
setOpenSettings(event.data.system_id); setOpenSettings(event.data.system_id);
break; break;
case OutCommand.linkSignatureToSystem:
setOpenLinkSignatures(event.data);
break;
default: default:
return outCommand(event); return outCommand(event);
} }
@@ -88,6 +99,14 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => {
const handleConnectionDbClick = useCallback((e: SolarSystemConnection) => setSelectedConnection(e), []); const handleConnectionDbClick = useCallback((e: SolarSystemConnection) => setSelectedConnection(e), []);
useMapEventListener(event => {
switch (event.name) {
case Commands.linkSignatureToSystem:
setOpenLinkSignatures(event.data);
return true;
}
});
return ( return (
<> <>
<Map <Map
@@ -99,22 +118,19 @@ export const MapWrapper = ({ refn }: MapWrapperProps) => {
onSelectionContextMenu={handleSystemMultipleContext} onSelectionContextMenu={handleSystemMultipleContext}
minimapClasses={!isShowMenu ? classes.MiniMap : undefined} minimapClasses={!isShowMenu ? classes.MiniMap : undefined}
isShowMinimap={isShowMinimap} isShowMinimap={isShowMinimap}
showKSpaceBG={isShowKSpace}
/> />
{openSettings != null && ( {openSettings != null && (
<SystemSettingsDialog <SystemSettingsDialog systemId={openSettings} visible setVisible={() => setOpenSettings(null)} />
systemId={openSettings}
visible={openSettings != null}
setVisible={() => setOpenSettings(null)}
/>
)} )}
{openCustomLabel != null && ( {openCustomLabel != null && (
<SystemCustomLabelDialog <SystemCustomLabelDialog systemId={openCustomLabel} visible setVisible={() => setOpenCustomLabel(null)} />
systemId={openCustomLabel} )}
visible={openCustomLabel != null}
setVisible={() => setOpenCustomLabel(null)} {openLinkSignatures != null && (
/> <SystemLinkSignatureDialog data={openLinkSignatures} setVisible={() => setOpenLinkSignatures(null)} />
)} )}
<Connections selectedConnection={selectedConnection} onHide={() => setSelectedConnection(null)} /> <Connections selectedConnection={selectedConnection} onHide={() => setSelectedConnection(null)} />

View File

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

View File

@@ -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 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,
};

View File

@@ -0,0 +1,13 @@
import { createEvent } from 'react-event-hook';
export interface MapEvent {
name: string;
data: {
solar_system_source: number;
solar_system_target: number;
};
}
const { useMapEventListener, emitMapEvent } = createEvent('map-event')<MapEvent>();
export { useMapEventListener, emitMapEvent };

View File

@@ -1,4 +1,4 @@
import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures'; import { COSMIC_SIGNATURE } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignatureSettingsDialog';
import { SystemSignature } from '@/hooks/Mapper/types'; import { SystemSignature } from '@/hooks/Mapper/types';
export const parseSignatures = (value: string, availableKeys: string[]): SystemSignature[] => { export const parseSignatures = (value: string, availableKeys: string[]): SystemSignature[] => {

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import { MapHandlers, MapUnionTypes, OutCommandHandler, SolarSystemConnection }
import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks'; import { useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
import { WithChildren } from '@/hooks/Mapper/types/common.ts'; import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import useLocalStorageState from 'use-local-storage-state'; import useLocalStorageState from 'use-local-storage-state';
import { DEFAULT_SETTINGS } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/RoutesProvider.tsx';
export type MapRootData = MapUnionTypes & { export type MapRootData = MapUnionTypes & {
selectedSystems: string[]; selectedSystems: string[];
@@ -27,14 +26,22 @@ const INITIAL_DATA: MapRootData = {
selectedConnections: [], selectedConnections: [],
}; };
type InterfaceStoredSettings = { export enum InterfaceStoredSettingsProps {
isShowMenu = 'isShowMenu',
isShowMinimap = 'isShowMinimap',
isShowKSpace = 'isShowKSpace',
}
export type InterfaceStoredSettings = {
isShowMenu: boolean; isShowMenu: boolean;
isShowMinimap: boolean; isShowMinimap: boolean;
isShowKSpace: boolean;
}; };
export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = { export const STORED_INTERFACE_DEFAULT_VALUES: InterfaceStoredSettings = {
isShowMenu: false, isShowMenu: false,
isShowMinimap: true, isShowMinimap: true,
isShowKSpace: false,
}; };
export interface MapRootContextProps { export interface MapRootContextProps {
@@ -50,6 +57,7 @@ const MapRootContext = createContext<MapRootContextProps>({
update: () => {}, update: () => {},
data: { ...INITIAL_DATA }, data: { ...INITIAL_DATA },
mapRef: { current: null }, mapRef: { current: null },
// @ts-ignore
outCommand: async () => void 0, outCommand: async () => void 0,
interfaceSettings: STORED_INTERFACE_DEFAULT_VALUES, interfaceSettings: STORED_INTERFACE_DEFAULT_VALUES,
setInterfaceSettings: () => null, setInterfaceSettings: () => null,

View File

@@ -27,12 +27,14 @@ interface UseLoadSystemStaticProps {
export const useLoadSystemStatic = ({ systems }: UseLoadSystemStaticProps) => { export const useLoadSystemStatic = ({ systems }: UseLoadSystemStaticProps) => {
const { outCommand } = useMapRootState(); const { outCommand } = useMapRootState();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [lastUpdateKey, setLastUpdateKey] = useState(0);
const ref = useRef({ outCommand }); const ref = useRef({ outCommand });
ref.current = { outCommand }; ref.current = { outCommand };
const addSystemStatic = useCallback((static_info: SolarSystemStaticInfoRaw) => { const addSystemStatic = useCallback((static_info: SolarSystemStaticInfoRaw) => {
cache.set(static_info.solar_system_id, static_info); cache.set(static_info.solar_system_id, static_info);
setLastUpdateKey(new Date().getTime());
}, []); }, []);
const loadSystems = useCallback(async (systems: (number | string)[]) => { const loadSystems = useCallback(async (systems: (number | string)[]) => {
@@ -43,6 +45,7 @@ export const useLoadSystemStatic = ({ systems }: UseLoadSystemStaticProps) => {
if (toLoad.length > 0) { if (toLoad.length > 0) {
const res = await loadSystemStaticInfo(ref.current.outCommand, toLoad); const res = await loadSystemStaticInfo(ref.current.outCommand, toLoad);
res.forEach(x => cache.set(x.solar_system_id, x)); res.forEach(x => cache.set(x.solar_system_id, x));
setLastUpdateKey(new Date().getTime());
} }
setLoading(false); setLoading(false);
}, []); }, []);
@@ -52,5 +55,5 @@ export const useLoadSystemStatic = ({ systems }: UseLoadSystemStaticProps) => {
// eslint-disable-next-line // eslint-disable-next-line
}, [systems]); }, [systems]);
return { addSystemStatic, systems: cache, loading, loadSystems }; return { addSystemStatic, systems: cache, lastUpdateKey, loading, loadSystems };
}; };

View File

@@ -27,6 +27,8 @@ import {
useRoutes, useRoutes,
} from './api'; } from './api';
import { emitMapEvent } from '@/hooks/Mapper/events';
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => { export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const mapInit = useMapInit(); const mapInit = useMapInit();
const { addSystems, removeSystems, updateSystems } = useCommandsSystems(); const { addSystems, removeSystems, updateSystems } = useCommandsSystems();
@@ -85,10 +87,18 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
mapRoutes(data as CommandRoutes); mapRoutes(data as CommandRoutes);
break; break;
case Commands.centerSystem:
// do nothing here
break;
case Commands.selectSystem: case Commands.selectSystem:
// do nothing here // do nothing here
break; break;
case Commands.linkSignatureToSystem:
emitMapEvent({ name: Commands.linkSignatureToSystem, data });
break;
case Commands.killsUpdated: case Commands.killsUpdated:
// do nothing here // do nothing here
break; break;

View File

@@ -21,7 +21,9 @@ export enum Commands {
mapUpdated = 'map_updated', mapUpdated = 'map_updated',
killsUpdated = 'kills_updated', killsUpdated = 'kills_updated',
routes = 'routes', routes = 'routes',
centerSystem = 'center_system',
selectSystem = 'select_system', selectSystem = 'select_system',
linkSignatureToSystem = 'link_signature_to_system',
} }
export type Command = export type Command =
@@ -40,7 +42,9 @@ export type Command =
| Commands.mapUpdated | Commands.mapUpdated
| Commands.killsUpdated | Commands.killsUpdated
| Commands.routes | Commands.routes
| Commands.selectSystem; | Commands.selectSystem
| Commands.centerSystem
| Commands.linkSignatureToSystem;
export type CommandInit = { export type CommandInit = {
systems: SolarSystemRawType[]; systems: SolarSystemRawType[];
@@ -72,6 +76,11 @@ export type CommandMapUpdated = Partial<CommandInit>;
export type CommandRoutes = RoutesList; export type CommandRoutes = RoutesList;
export type CommandKillsUpdated = Kill[]; export type CommandKillsUpdated = Kill[];
export type CommandSelectSystem = string | undefined; export type CommandSelectSystem = string | undefined;
export type CommandCenterSystem = string | undefined;
export type CommandLinkSignatureToSystem = {
solar_system_source: number;
solar_system_target: number;
};
export interface CommandData { export interface CommandData {
[Commands.init]: CommandInit; [Commands.init]: CommandInit;
@@ -90,6 +99,8 @@ export interface CommandData {
[Commands.routes]: CommandRoutes; [Commands.routes]: CommandRoutes;
[Commands.killsUpdated]: CommandKillsUpdated; [Commands.killsUpdated]: CommandKillsUpdated;
[Commands.selectSystem]: CommandSelectSystem; [Commands.selectSystem]: CommandSelectSystem;
[Commands.centerSystem]: CommandCenterSystem;
[Commands.linkSignatureToSystem]: CommandLinkSignatureToSystem;
} }
export interface MapHandlers { export interface MapHandlers {
@@ -123,10 +134,15 @@ export enum OutCommand {
setAutopilotWaypoint = 'set_autopilot_waypoint', setAutopilotWaypoint = 'set_autopilot_waypoint',
addSystem = 'add_system', addSystem = 'add_system',
addCharacter = 'add_character', addCharacter = 'add_character',
openUserSettings = 'open_user_settings',
getPassages = 'get_passages', getPassages = 'get_passages',
linkSignatureToSystem = 'link_signature_to_system',
// Only UI commands // Only UI commands
openSettings = 'open_settings', openSettings = 'open_settings',
getUserSettings = 'get_user_settings',
updateUserSettings = 'update_user_settings',
} }
export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>; export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>;

View File

@@ -1,9 +1,12 @@
import { SolarSystemStaticInfoRaw } from '@/hooks/Mapper/types';
export type SystemSignature = { export type SystemSignature = {
eve_id: string; eve_id: string;
kind: string; kind: string;
name: string; name: string;
description?: string; description?: string;
group: string; group: string;
linked_system?: SolarSystemStaticInfoRaw;
updated_at?: string; updated_at?: string;
}; };

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ export class LabelsManager {
} }
hasLabel(label: string) { hasLabel(label: string) {
return this.parsedLabels.labels.includes(label); return this.parsedLabels.labels?.includes(label);
} }
toggleLabel(label: string) { toggleLabel(label: string) {

View File

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

View File

@@ -21,7 +21,6 @@
"live_select": "file:../deps/live_select", "live_select": "file:../deps/live_select",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"pako": "^2.1.0",
"phoenix": "file:../deps/phoenix", "phoenix": "file:../deps/phoenix",
"phoenix_html": "file:../deps/phoenix_html", "phoenix_html": "file:../deps/phoenix_html",
"phoenix_live_view": "file:../deps/phoenix_live_view", "phoenix_live_view": "file:../deps/phoenix_live_view",
@@ -29,6 +28,7 @@
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.6.5", "primereact": "^10.6.5",
"react-error-boundary": "^4.0.13", "react-error-boundary": "^4.0.13",
"react-event-hook": "^3.1.2",
"react-flow-renderer": "^10.3.17", "react-flow-renderer": "^10.3.17",
"react-grid-layout": "^1.3.4", "react-grid-layout": "^1.3.4",
"react-usestateref": "^1.0.9", "react-usestateref": "^1.0.9",
@@ -44,7 +44,6 @@
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"@types/lodash.debounce": "^4.0.9", "@types/lodash.debounce": "^4.0.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/pako": "^2.0.3",
"@types/react": "18.2.0", "@types/react": "18.2.0",
"@types/react-dom": "18.2.1", "@types/react-dom": "18.2.1",
"@types/react-grid-layout": "^1.3.4", "@types/react-grid-layout": "^1.3.4",

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

View File

@@ -187,7 +187,7 @@
"@babel/runtime@^7.12.5": "@babel/runtime@^7.12.5":
version "7.25.0" 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== integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.14.0"
@@ -484,7 +484,7 @@
"@reactflow/background@11.3.14": "@reactflow/background@11.3.14":
version "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== integrity sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==
dependencies: dependencies:
"@reactflow/core" "11.11.4" "@reactflow/core" "11.11.4"
@@ -493,7 +493,7 @@
"@reactflow/controls@11.2.14": "@reactflow/controls@11.2.14":
version "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== integrity sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==
dependencies: dependencies:
"@reactflow/core" "11.11.4" "@reactflow/core" "11.11.4"
@@ -502,7 +502,7 @@
"@reactflow/core@11.11.4": "@reactflow/core@11.11.4":
version "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== integrity sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==
dependencies: dependencies:
"@types/d3" "^7.4.0" "@types/d3" "^7.4.0"
@@ -517,7 +517,7 @@
"@reactflow/minimap@11.7.14": "@reactflow/minimap@11.7.14":
version "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== integrity sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==
dependencies: dependencies:
"@reactflow/core" "11.11.4" "@reactflow/core" "11.11.4"
@@ -530,7 +530,7 @@
"@reactflow/node-resizer@2.2.14": "@reactflow/node-resizer@2.2.14":
version "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== integrity sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==
dependencies: dependencies:
"@reactflow/core" "11.11.4" "@reactflow/core" "11.11.4"
@@ -541,7 +541,7 @@
"@reactflow/node-toolbar@1.3.14": "@reactflow/node-toolbar@1.3.14":
version "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== integrity sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==
dependencies: dependencies:
"@reactflow/core" "11.11.4" "@reactflow/core" "11.11.4"
@@ -964,11 +964,6 @@
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz"
integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==
"@types/pako@^2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-2.0.3.tgz#b6993334f3af27c158f3fe0dfeeba987c578afb1"
integrity sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.11" version "15.7.11"
resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz" resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz"
@@ -2947,11 +2942,6 @@ p-locate@^5.0.0:
dependencies: dependencies:
p-limit "^3.0.2" p-limit "^3.0.2"
pako@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
parent-module@^1.0.0: parent-module@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
@@ -3204,11 +3194,16 @@ react-draggable@^4.0.3, react-draggable@^4.4.5:
react-error-boundary@^4.0.13: react-error-boundary@^4.0.13:
version "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== integrity sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
react-event-hook@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/react-event-hook/-/react-event-hook-3.1.2.tgz#445e8f3b751f6abe4ef199f31bff47593c4c13d4"
integrity sha512-qQ9LXLdxmWRRZPlnqVjqlw7jovSvDosQEOyQ9cjPHhtDv8JIszjj0td1PuHJHrVW0LS8a1XeJhLe6i7S5u9SbQ==
react-flow-renderer@^10.3.17: react-flow-renderer@^10.3.17:
version "10.3.17" version "10.3.17"
resolved "https://registry.npmjs.org/react-flow-renderer/-/react-flow-renderer-10.3.17.tgz" resolved "https://registry.npmjs.org/react-flow-renderer/-/react-flow-renderer-10.3.17.tgz"
@@ -3282,7 +3277,7 @@ react@18.2.0:
reactflow@^11.10.4: reactflow@^11.10.4:
version "11.11.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== integrity sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==
dependencies: dependencies:
"@reactflow/background" "11.3.14" "@reactflow/background" "11.3.14"

View File

@@ -60,15 +60,7 @@ config :dart_sass, :version, "1.54.5"
config :tailwind, :version, "3.2.7" config :tailwind, :version, "3.2.7"
config :wanderer_app, WandererApp.PromEx, config :wanderer_app, WandererApp.PromEx, manual_metrics_start_delay: :no_delay
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, config :wanderer_app,
grafana_datasource_id: "wanderer" grafana_datasource_id: "wanderer"

View File

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

View File

@@ -53,7 +53,6 @@ map_subscriptions_enabled =
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false") |> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
|> String.to_existing_atom() |> String.to_existing_atom()
map_subscription_characters_limit = map_subscription_characters_limit =
config_dir config_dir
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_CHARACTERS_LIMIT", 100) |> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_CHARACTERS_LIMIT", 100)
@@ -78,7 +77,9 @@ config :wanderer_app,
web_app_url: web_app_url, web_app_url: web_app_url,
git_sha: System.get_env("GIT_SHA", "111"), git_sha: System.get_env("GIT_SHA", "111"),
custom_route_base_url: System.get_env("CUSTOM_ROUTE_BASE_URL"), custom_route_base_url: System.get_env("CUSTOM_ROUTE_BASE_URL"),
invites: System.get_env("WANDERER_INVITES", "false") == "true", invites: System.get_env("WANDERER_INVITES", "false") |> String.to_existing_atom(),
admin_username: System.get_env("WANDERER_ADMIN_USERNAME", "admin"),
admin_password: System.get_env("WANDERER_ADMIN_PASSWORD"),
admins: admins, admins: admins,
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(), corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""), corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
@@ -86,7 +87,13 @@ config :wanderer_app,
wallet_tracking_enabled: wallet_tracking_enabled, wallet_tracking_enabled: wallet_tracking_enabled,
subscription_settings: %{ subscription_settings: %{
plans: [ plans: [
%{id: "alpha", characters_limit: map_subscription_characters_limit, hubs_limit: map_subscription_hubs_limit, 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", id: "omega",
characters_limit: 300, characters_limit: 300,
@@ -185,9 +192,20 @@ if config_env() == :prod do
|> get_var_from_path_or_env("DATABASE_SSL_ENABLED", "false") |> get_var_from_path_or_env("DATABASE_SSL_ENABLED", "false")
|> String.to_existing_atom() |> 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, config :wanderer_app, WandererApp.Repo,
url: database_url, url: database_url,
ssl: db_ssl_enabled, ssl: db_ssl_enabled,
ssl_opts: client_opts,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
socket_options: maybe_ipv6 socket_options: maybe_ipv6

61
flake.lock generated Normal file
View 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
}

View File

@@ -30,4 +30,19 @@ defmodule WandererApp do
defmacro __using__(which) when is_atom(which) do defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, []) apply(__MODULE__, which, [])
end end
def log_exception(kind, reason, stacktrace) do
reason = Exception.normalize(kind, reason, stacktrace)
crash_reason =
case kind do
:throw -> {{:nocatch, reason}, stacktrace}
_ -> {reason, stacktrace}
end
Logger.error(
Exception.format(kind, reason, stacktrace),
crash_reason: crash_reason
)
end
end end

View File

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

View File

@@ -43,7 +43,6 @@ defmodule WandererApp.Api.AccessList do
primary?(true) primary?(true)
argument :owner_id, :uuid, allow_nil?: false 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) change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
end end
@@ -51,8 +50,6 @@ defmodule WandererApp.Api.AccessList do
update :update do update :update do
accept [:name, :description, :owner_id] accept [:name, :description, :owner_id]
primary?(true) primary?(true)
argument :owner_id_text_input, :string, allow_nil?: true
end end
update :assign_owner do update :assign_owner do

View File

@@ -104,13 +104,21 @@ defmodule WandererApp.Api.AccessListMember do
end end
end end
postgres do
references do
reference :access_list, on_delete: :delete
end
end
identities do identities do
identity :uniq_acl_character_id, [:access_list_id, :eve_character_id] do identity :uniq_acl_character_id, [:access_list_id, :eve_character_id] do
pre_check?(true) pre_check?(true)
end 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) pre_check?(true)
end end
identity :uniq_acl_alliance_id, [:access_list_id, :eve_alliance_id] do identity :uniq_acl_alliance_id, [:access_list_id, :eve_alliance_id] do
pre_check?(true) pre_check?(true)
end end

View File

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

View File

@@ -18,6 +18,7 @@ defmodule WandererApp.Api.Map do
define(:update, action: :update) define(:update, action: :update)
define(:update_acls, action: :update_acls) define(:update_acls, action: :update_acls)
define(:update_hubs, action: :update_hubs) define(:update_hubs, action: :update_hubs)
define(:update_options, action: :update_options)
define(:assign_owner, action: :assign_owner) define(:assign_owner, action: :assign_owner)
define(:mark_as_deleted, action: :mark_as_deleted) define(:mark_as_deleted, action: :mark_as_deleted)
@@ -63,7 +64,6 @@ defmodule WandererApp.Api.Map do
primary?(true) primary?(true)
argument :owner_id, :uuid, allow_nil?: false 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 :create_default_acl, :boolean, allow_nil?: true
argument :acls, {:array, :uuid}, allow_nil?: true argument :acls, {:array, :uuid}, allow_nil?: true
argument :acls_text_input, :string, allow_nil?: true argument :acls_text_input, :string, allow_nil?: true
@@ -113,6 +113,10 @@ defmodule WandererApp.Api.Map do
accept [:hubs] accept [:hubs]
end end
update :update_options do
accept [:options]
end
update :mark_as_deleted do update :mark_as_deleted do
accept([]) accept([])
@@ -168,6 +172,10 @@ defmodule WandererApp.Api.Map do
allow_nil?(true) allow_nil?(true)
end end
attribute :options, :string do
allow_nil? true
end
create_timestamp(:inserted_at) create_timestamp(:inserted_at)
update_timestamp(:updated_at) update_timestamp(:updated_at)
end end

View File

@@ -44,6 +44,13 @@ defmodule WandererApp.Api.MapAccessList do
belongs_to :access_list, WandererApp.Api.AccessList, primary_key?: true, allow_nil?: false belongs_to :access_list, WandererApp.Api.AccessList, primary_key?: true, allow_nil?: false
end end
postgres do
references do
reference :map, on_delete: :delete
reference :access_list, on_delete: :delete
end
end
identities do identities do
identity :unique_map_acl, [:map_id, :access_list_id] do identity :unique_map_acl, [:map_id, :access_list_id] do
pre_check?(false) pre_check?(false)

View File

@@ -18,10 +18,7 @@ defmodule WandererApp.Api.MapConnection do
action: :read action: :read
) )
define(:by_locations, define(:by_locations, action: :read_by_locations)
get_by: [:map_id, :solar_system_source, :solar_system_target],
action: :read
)
define(:read_by_map, action: :read_by_map) define(:read_by_map, action: :read_by_map)
define(:get_link_pairs_advanced, action: :get_link_pairs_advanced) 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))) filter(expr(map_id == ^arg(:map_id)))
end 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 read :get_link_pairs_advanced do
argument(:map_id, :string, allow_nil?: false) argument(:map_id, :string, allow_nil?: false)
argument(:include_mass_crit, :boolean, allow_nil?: false) argument(:include_mass_crit, :boolean, allow_nil?: false)

View File

@@ -14,6 +14,7 @@ defmodule WandererApp.Api.MapSystemSignature do
define(:all_active, action: :all_active) define(:all_active, action: :all_active)
define(:create, action: :create) define(:create, action: :create)
define(:update, action: :update) define(:update, action: :update)
define(:update_linked_system, action: :update_linked_system)
define(:by_id, define(:by_id,
get_by: [:id], get_by: [:id],
@@ -66,13 +67,18 @@ defmodule WandererApp.Api.MapSystemSignature do
:name, :name,
:description, :description,
:kind, :kind,
:group :group,
:linked_system_id
] ]
primary? true primary? true
require_atomic? false require_atomic? false
end end
update :update_linked_system do
accept [:linked_system_id]
end
read :by_system_id do read :by_system_id do
argument(:system_id, :string, allow_nil?: false) argument(:system_id, :string, allow_nil?: false)
@@ -99,6 +105,10 @@ defmodule WandererApp.Api.MapSystemSignature do
allow_nil? true allow_nil? true
end end
attribute :linked_system_id, :integer do
allow_nil? true
end
attribute :kind, :string attribute :kind, :string
attribute :group, :string attribute :group, :string

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,13 +68,14 @@ defmodule WandererApp.Character.TrackerManager.Impl do
state state
false -> 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) 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) WandererApp.Cache.insert("tracked_characters", tracked_characters)
%{state | characters: tracked_characters} %{state | characters: tracked_characters}
@@ -177,9 +178,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
characters characters
|> Enum.map(fn character_id -> |> Enum.map(fn character_id ->
Task.start_link(fn -> WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [
WandererApp.Character.Tracker.update_online(character_id) character_id
end) ])
end) end)
state state
@@ -204,10 +205,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
Process.send_after(self(), :check_online_errors, @check_online_errors_interval) Process.send_after(self(), :check_online_errors, @check_online_errors_interval)
characters characters
|> Enum.map(fn character_id -> |> Task.async_stream(
Task.start_link(fn -> fn character_id ->
WandererApp.Character.Tracker.check_online_errors(character_id) WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :check_online_errors, [
end) character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in check_online_errors: #{inspect(reason)}")
end) end)
state state
@@ -225,9 +235,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
characters characters
|> Enum.map(fn character_id -> |> Enum.map(fn character_id ->
Task.start_link(fn -> WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [
WandererApp.Character.Tracker.update_location(character_id) character_id
end) ])
end) end)
state state
@@ -254,9 +264,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
characters characters
|> Enum.map(fn character_id -> |> Enum.map(fn character_id ->
Task.start_link(fn -> WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [
WandererApp.Character.Tracker.update_ship(character_id) character_id
end) ])
end) end)
state state
@@ -282,10 +292,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
Process.send_after(self(), :update_info, @update_info_interval) Process.send_after(self(), :update_info, @update_info_interval)
characters characters
|> Enum.map(fn character_id -> |> Task.async_stream(
Task.start_link(fn -> fn character_id ->
WandererApp.Character.Tracker.update_info(character_id) WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [
end) character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in update_info: #{inspect(reason)}")
end) end)
state state
@@ -311,10 +330,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
Process.send_after(self(), :update_wallet, @update_wallet_interval) Process.send_after(self(), :update_wallet, @update_wallet_interval)
characters characters
|> Enum.map(fn character_id -> |> Task.async_stream(
Task.start_link(fn -> fn character_id ->
WandererApp.Character.Tracker.update_wallet(character_id) WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [
end) character_id
])
end,
timeout: :timer.seconds(15),
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task
)
|> Enum.each(fn
{:ok, _result} -> :ok
{:error, reason} -> @logger.error("Error in update_wallet: #{inspect(reason)}")
end) end)
state state
@@ -355,7 +383,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end end
end end
end, end,
max_concurrency: 20, max_concurrency: System.schedulers_online(),
on_timeout: :kill_task, on_timeout: :kill_task,
timeout: :timer.seconds(15) timeout: :timer.seconds(15)
) )
@@ -391,7 +419,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
WandererApp.Character.update_character_state(character_id, character_state) WandererApp.Character.update_character_state(character_id, character_state)
end, end,
max_concurrency: 20, max_concurrency: System.schedulers_online(),
on_timeout: :kill_task, on_timeout: :kill_task,
timeout: :timer.seconds(30) timeout: :timer.seconds(30)
) )
@@ -401,7 +429,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end end
def handle_info({:stop_track, character_id}, state) do def handle_info({:stop_track, character_id}, state) do
Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end) @logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
stop_tracking(state, character_id) stop_tracking(state, character_id)
end end

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More