Compare commits

...

3 Commits

Author SHA1 Message Date
Dmitry Popov
d622e486f4 feat(Core): Support map default layout option 2024-10-07 22:57:46 +04:00
Dmitry Popov
81926633b0 Merge branch 'main' into 19-add-map-custom-options 2024-10-07 20:56:08 +04:00
Dmitry Popov
e4fe8fdc53 feat(Core): Support map default layout option 2024-10-07 13:40:37 +04:00
9 changed files with 347 additions and 52 deletions

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

@@ -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)
@@ -112,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([])
@@ -167,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

@@ -52,6 +52,15 @@ defmodule WandererApp.Map do
end end
end end
def get_map_options!(map) do
map
|> Map.get(:options)
|> case do
nil -> %{"layout" => "left_to_right"}
options -> Jason.decode!(options)
end
end
def update_map(map_id, map_update) do def update_map(map_id, map_update) do
Cachex.get_and_update(:map_cache, map_id, fn map -> Cachex.get_and_update(:map_cache, map_id, fn map ->
case map do case map do

View File

@@ -19,46 +19,47 @@ defmodule WandererApp.Map.PositionCalculator do
def get_system_bounding_rect(_system), do: [{0, 0}, {0, 0}] def get_system_bounding_rect(_system), do: [{0, 0}, {0, 0}]
def get_new_system_position(nil, rtree_name) do def get_new_system_position(nil, rtree_name, opts) do
{:ok, {x, y}} = rtree_name |> _check_system_available_positions(@start_x, @start_y, 1) {:ok, {x, y}} = rtree_name |> check_system_available_positions(@start_x, @start_y, 1, opts)
%{x: x, y: y} %{x: x, y: y}
end end
def get_new_system_position( def get_new_system_position(
%{position_x: start_x, position_y: start_y} = _old_system, %{position_x: start_x, position_y: start_y} = _old_system,
rtree_name rtree_name,
opts
) do ) do
{:ok, {x, y}} = rtree_name |> _check_system_available_positions(start_x, start_y, 1) {:ok, {x, y}} = rtree_name |> check_system_available_positions(start_x, start_y, 1, opts)
%{x: x, y: y} %{x: x, y: y}
end end
defp _check_system_available_positions(_rtree_name, _start_x, _start_y, 100) do defp check_system_available_positions(_rtree_name, _start_x, _start_y, 100, _opts),
{:ok, {@start_x, @start_y}} do: {:ok, {@start_x, @start_y}}
end
defp _check_system_available_positions(rtree_name, start_x, start_y, level) do defp check_system_available_positions(rtree_name, start_x, start_y, level, opts) do
possible_positions = _get_available_positions(level, start_x, start_y) possible_positions = get_available_positions(level, start_x, start_y, opts)
case _get_available_position(possible_positions, rtree_name) do case get_available_position(possible_positions, rtree_name) do
{:ok, nil} -> {:ok, nil} ->
rtree_name |> _check_system_available_positions(start_x, start_y, level + 1) rtree_name |> check_system_available_positions(start_x, start_y, level + 1, opts)
{:ok, position} -> {:ok, position} ->
{:ok, position} {:ok, position}
end end
end end
defp _get_available_position([], _rtree_name), do: {:ok, nil} defp get_available_position([], _rtree_name), do: {:ok, nil}
defp _get_available_position([position | rest], rtree_name) do defp get_available_position([position | rest], rtree_name) do
if _is_available_position(position, rtree_name) do if is_available_position(position, rtree_name) do
{:ok, position} {:ok, position}
else else
_get_available_position(rest, rtree_name) get_available_position(rest, rtree_name)
end end
end end
defp _is_available_position({x, y} = _position, rtree_name) do defp is_available_position({x, y} = _position, rtree_name) do
case DDRT.query(get_system_bounding_rect(%{position_x: x, position_y: y}), rtree_name) do case DDRT.query(get_system_bounding_rect(%{position_x: x, position_y: y}), rtree_name) do
{:ok, []} -> {:ok, []} ->
true true
@@ -71,9 +72,10 @@ defmodule WandererApp.Map.PositionCalculator do
end end
end end
def _get_available_positions(level, x, y), do: _adjusted_coordinates(1 + level * 2, x, y) def get_available_positions(level, x, y, opts),
do: adjusted_coordinates(1 + level * 2, x, y, opts)
defp _edge_coordinates(n) when n > 1 do defp edge_coordinates(n, opts) when n > 1 do
min = -div(n, 2) min = -div(n, 2)
max = div(n, 2) max = div(n, 2)
# Top edge # Top edge
@@ -90,16 +92,20 @@ defmodule WandererApp.Map.PositionCalculator do
|> Enum.uniq() |> Enum.uniq()
end end
defp _sorted_edge_coordinates(n) when n > 1 do defp sorted_edge_coordinates(n, opts) when n > 1 do
coordinates = _edge_coordinates(n) coordinates = edge_coordinates(n, opts)
middle_right_index = div(n, 2) start_index = get_start_index(n, opts[:layout])
Enum.slice(coordinates, middle_right_index, length(coordinates) - middle_right_index) ++ Enum.slice(coordinates, start_index, length(coordinates) - start_index) ++
Enum.slice(coordinates, 0, middle_right_index) Enum.slice(coordinates, 0, start_index)
end end
defp _adjusted_coordinates(n, start_x, start_y) when n > 1 do defp get_start_index(n, "left_to_right"), do: div(n, 2)
sorted_coords = _sorted_edge_coordinates(n)
defp get_start_index(n, "top_to_bottom"), do: div(n, 2) + n - 1
defp adjusted_coordinates(n, start_x, start_y, opts) when n > 1 do
sorted_coords = sorted_edge_coordinates(n, opts)
Enum.map(sorted_coords, fn {x, y} -> Enum.map(sorted_coords, fn {x, y} ->
{ {

View File

@@ -11,7 +11,8 @@ defmodule WandererApp.Map.Server.Impl do
defstruct [ defstruct [
:map_id, :map_id,
:rtree_name, :rtree_name,
map: nil map: nil,
map_opts: []
] ]
# @ccp1 -1 # @ccp1 -1
@@ -795,6 +796,9 @@ defmodule WandererApp.Map.Server.Impl do
} }
end end
def handle_event({:options_updated, options}, %{map: map, map_id: map_id} = state),
do: %{state | map_opts: [layout: options.layout]}
def handle_event({ref, _result}, %{map_id: _map_id} = state) do def handle_event({ref, _result}, %{map_id: _map_id} = state) do
Process.demonitor(ref, [:flush]) Process.demonitor(ref, [:flush])
@@ -834,12 +838,12 @@ defmodule WandererApp.Map.Server.Impl do
character_id, character_id,
location, location,
old_location, old_location,
%{map: map, map_id: map_id, rtree_name: rtree_name} = _state %{map: map, map_id: map_id, rtree_name: rtree_name, map_opts: map_opts} = _state
) do ) do
case is_nil(old_location.solar_system_id) and case is_nil(old_location.solar_system_id) and
_can_add_location(map.scope, location.solar_system_id) do _can_add_location(map.scope, location.solar_system_id) do
true -> true ->
:ok = maybe_add_system(map_id, location, nil, rtree_name) :ok = maybe_add_system(map_id, location, nil, rtree_name, map_opts)
_ -> _ ->
case _is_connection_valid( case _is_connection_valid(
@@ -849,8 +853,8 @@ defmodule WandererApp.Map.Server.Impl do
) do ) do
true -> true ->
{:ok, character} = WandererApp.Character.get_character(character_id) {:ok, character} = WandererApp.Character.get_character(character_id)
:ok = maybe_add_system(map_id, location, old_location, rtree_name) :ok = maybe_add_system(map_id, location, old_location, rtree_name, map_opts)
:ok = maybe_add_system(map_id, old_location, location, rtree_name) :ok = maybe_add_system(map_id, old_location, location, rtree_name, map_opts)
:ok = maybe_add_connection(map_id, location, old_location, character) :ok = maybe_add_connection(map_id, location, old_location, character)
_ -> _ ->
@@ -1097,7 +1101,7 @@ defmodule WandererApp.Map.Server.Impl do
end)} end)}
defp _add_system( defp _add_system(
%{map_id: map_id, rtree_name: rtree_name} = state, %{map_id: map_id, map_opts: map_opts, rtree_name: rtree_name} = state,
%{ %{
solar_system_id: solar_system_id, solar_system_id: solar_system_id,
coordinates: coordinates coordinates: coordinates
@@ -1113,7 +1117,7 @@ defmodule WandererApp.Map.Server.Impl do
_ -> _ ->
%{x: x, y: y} = %{x: x, y: y} =
WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name) WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts)
%{"x" => x, "y" => y} %{"x" => x, "y" => y}
end end
@@ -1255,20 +1259,22 @@ defmodule WandererApp.Map.Server.Impl do
defp _init_map( defp _init_map(
state, state,
%{characters: characters} = map, %{characters: characters} = initial_map,
subscription_settings, subscription_settings,
systems, systems,
connections connections
) do ) do
map = map =
map initial_map
|> WandererApp.Map.new() |> WandererApp.Map.new()
|> WandererApp.Map.update_subscription_settings!(subscription_settings) |> WandererApp.Map.update_subscription_settings!(subscription_settings)
|> WandererApp.Map.add_systems!(systems) |> WandererApp.Map.add_systems!(systems)
|> WandererApp.Map.add_connections!(connections) |> WandererApp.Map.add_connections!(connections)
|> WandererApp.Map.add_characters!(characters) |> WandererApp.Map.add_characters!(characters)
%{state | map: map} map_options = WandererApp.Map.get_map_options!(initial_map)
%{state | map: map, map_opts: [layout: map_options |> Map.get("layout")]}
end end
defp _init_map_systems(state, [] = _systems), do: state defp _init_map_systems(state, [] = _systems), do: state
@@ -1614,11 +1620,11 @@ defmodule WandererApp.Map.Server.Impl do
defp maybe_add_connection(_map_id, _location, _old_location, _character), do: :ok defp maybe_add_connection(_map_id, _location, _old_location, _character), do: :ok
defp maybe_add_system(map_id, location, old_location, rtree_name) defp maybe_add_system(map_id, location, old_location, rtree_name, opts)
when not is_nil(location) do when not is_nil(location) do
case WandererApp.Map.check_location(map_id, location) do case WandererApp.Map.check_location(map_id, location) do
{:ok, location} -> {:ok, location} ->
{:ok, position} = calc_new_system_position(map_id, old_location, rtree_name) {:ok, position} = calc_new_system_position(map_id, old_location, rtree_name, opts)
case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id( case WandererApp.MapSystemRepo.get_by_map_and_solar_system_id(
map_id, map_id,
@@ -1688,14 +1694,14 @@ defmodule WandererApp.Map.Server.Impl do
end end
end end
defp maybe_add_system(_map_id, _location, _old_location, _rtree_name), do: :ok defp maybe_add_system(_map_id, _location, _old_location, _rtree_name, _opts), do: :ok
defp calc_new_system_position(map_id, old_location, rtree_name) do defp calc_new_system_position(map_id, old_location, rtree_name, opts),
do:
{:ok, {:ok,
map_id map_id
|> WandererApp.Map.find_system_by_location(old_location) |> WandererApp.Map.find_system_by_location(old_location)
|> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name)} |> WandererApp.Map.PositionCalculator.get_new_system_position(rtree_name, opts)}
end
defp _broadcast_acl_updates( defp _broadcast_acl_updates(
{:ok, {:ok,

View File

@@ -5,6 +5,8 @@ defmodule WandererAppWeb.MapsLive do
alias BetterNumber, as: Number alias BetterNumber, as: Number
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
@impl true @impl true
def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do
{:ok, active_characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id}) {:ok, active_characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id})
@@ -112,6 +114,13 @@ defmodule WandererAppWeb.MapsLive do
"auto_renew?" => true "auto_renew?" => true
} }
options_form =
map.options
|> case do
nil -> %{"layout" => "left_to_right"}
options -> Jason.decode!(options)
end
{:ok, estimated_price, discount} = {:ok, estimated_price, discount} =
WandererApp.Map.SubscriptionManager.estimate_price(subscription_form, false) WandererApp.Map.SubscriptionManager.estimate_price(subscription_form, false)
@@ -130,6 +139,7 @@ defmodule WandererAppWeb.MapsLive do
active_settings_tab: "general", active_settings_tab: "general",
is_adding_subscription?: false, is_adding_subscription?: false,
selected_subscription: nil, selected_subscription: nil,
options_form: options_form |> to_form(),
map_subscriptions: map_subscriptions, map_subscriptions: map_subscriptions,
subscription_form: subscription_form |> to_form(), subscription_form: subscription_form |> to_form(),
estimated_price: estimated_price, estimated_price: estimated_price,
@@ -142,6 +152,10 @@ defmodule WandererAppWeb.MapsLive do
{"3 Months", "3"}, {"3 Months", "3"},
{"6 Months", "6"}, {"6 Months", "6"},
{"1 Year", "12"} {"1 Year", "12"}
],
layout_options: [
{"Left To Right", "left_to_right"},
{"Top To Bottom", "top_to_bottom"}
] ]
) )
|> allow_upload(:settings, |> allow_upload(:settings,
@@ -653,6 +667,28 @@ defmodule WandererAppWeb.MapsLive do
|> push_patch(to: ~p"/maps")} |> push_patch(to: ~p"/maps")}
end end
def handle_event(
"update_options",
%{
"layout" => layout
} = options_form,
%{assigns: %{map_id: map_id, map: map, current_user: current_user}} = socket
) do
options = %{layout: layout}
updated_map =
map
|> WandererApp.Api.Map.update_options!(%{options: Jason.encode!(options)})
@pubsub_client.broadcast(
WandererApp.PubSub,
"maps:#{map_id}",
{:options_updated, options}
)
{:noreply, socket |> assign(map: updated_map, options_form: options_form)}
end
@impl true @impl true
def handle_event("noop", _, socket) do def handle_event("noop", _, socket) do
{:noreply, socket} {:noreply, socket}

View File

@@ -26,7 +26,6 @@
> >
<figure class="absolute z-10 h-200 avatar w-full h-full"> <figure class="absolute z-10 h-200 avatar w-full h-full">
<img :if={map.scope === :all} class="absolute h-200" src="/images/all_back.webp" /> <img :if={map.scope === :all} class="absolute h-200" src="/images/all_back.webp" />
<img <img
:if={map.scope === :wormholes} :if={map.scope === :wormholes}
class="absolute h-200" class="absolute h-200"
@@ -190,7 +189,6 @@
> >
<div role="tablist" class="tabs tabs-bordered"> <div role="tablist" class="tabs tabs-bordered">
<a <a
:if={@map_subscriptions_enabled?}
role="tab" role="tab"
phx-click="change_settings_tab" phx-click="change_settings_tab"
phx-value-tab="general" phx-value-tab="general"
@@ -201,6 +199,17 @@
> >
<.icon name="hero-wrench-screwdriver-solid" class="w-4 h-4" />&nbsp;General <.icon name="hero-wrench-screwdriver-solid" class="w-4 h-4" />&nbsp;General
</a> </a>
<a
role="tab"
phx-click="change_settings_tab"
phx-value-tab="import"
class={[
"tab",
classes("tab-active": @active_settings_tab == "import")
]}
>
<.icon name="hero-wrench-screwdriver-solid" class="w-4 h-4" />&nbsp;Import/Export
</a>
<a <a
:if={@map_subscriptions_enabled?} :if={@map_subscriptions_enabled?}
role="tab" role="tab"
@@ -227,6 +236,27 @@
</a> </a>
</div> </div>
<.header :if={@active_settings_tab == "general"} class="bordered border-1 border-zinc-800"> <.header :if={@active_settings_tab == "general"} class="bordered border-1 border-zinc-800">
<:actions>
<.form
:let={f}
:if={assigns |> Map.get(:options_form, false)}
for={@options_form}
phx-change="update_options"
>
<div class="stat-title">Map systems layout</div>
<div class="stat-value text-white">
<.input
type="select"
field={f[:layout]}
class="p-dropdown p-component p-inputwrapper"
placeholder="Map default layout"
options={@layout_options}
/>
</div>
</.form>
</:actions>
</.header>
<.header :if={@active_settings_tab == "import"} class="bordered border-1 border-zinc-800">
Import/Export Map Settings Import/Export Map Settings
<:actions> <:actions>
<.form :if={assigns |> Map.get(:import_form, false)} for={@import_form} phx-change="import"> <.form :if={assigns |> Map.get(:import_form, false)} for={@import_form} phx-change="import">

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.AddMapOptions do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:maps_v1) do
add :options, :text
end
end
def down do
alter table(:maps_v1) do
remove :options
end
end
end

View File

@@ -0,0 +1,186 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "slug",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "description",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "personal_note",
"type": "text"
},
{
"allow_nil?": true,
"default": "[]",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "hubs",
"type": [
"array",
"text"
]
},
{
"allow_nil?": false,
"default": "\"wormholes\"",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "scope",
"type": "text"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "deleted",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "only_tracked_characters",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "options",
"type": "text"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "maps_v1_owner_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "character_v1"
},
"size": null,
"source": "owner_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "E5FC6B5F1B9AD5E23163494C7C93A8002F9C812AFC7A26A8C33A344877086A03",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "maps_v1_unique_slug_index",
"keys": [
{
"type": "atom",
"value": "slug"
}
],
"name": "unique_slug",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "maps_v1"
}