"""Eve universe models Part 2/2, containing location related models."""
# pylint: disable = too-few-public-methods
import math
import re
from collections import namedtuple
from typing import Iterable, List, Optional, Set
from bitfield import BitField
from django.db import models
from django.utils.functional import cached_property
from eveuniverse.constants import EveGroupId, EveRegionId
from eveuniverse.core import dotlan, evesdeapi
from eveuniverse.managers import (
EveAsteroidBeltManager,
EveMoonManager,
EvePlanetManager,
EveStargateManager,
)
from eveuniverse.providers import esi
from .base import EveUniverseEntityModel, _SectionBase, determine_effective_sections
from .entities import EveEntity
from .universe_1 import EveType
[docs]
class EveAsteroidBelt(EveUniverseEntityModel):
"""An asteroid belt in Eve Online"""
eve_planet = models.ForeignKey(
"EvePlanet", on_delete=models.CASCADE, related_name="eve_asteroid_belts"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
objects = EveAsteroidBeltManager()
class _EveUniverseMeta:
esi_pk = "asteroid_belt_id"
esi_path_object = "Universe.get_universe_asteroid_belts_asteroid_belt_id"
field_mappings = {
"eve_planet": "planet_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
load_order = 200
[docs]
class EveConstellation(EveUniverseEntityModel):
"""A star constellation in Eve Online"""
eve_region = models.ForeignKey(
"EveRegion", on_delete=models.CASCADE, related_name="eve_constellations"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
class _EveUniverseMeta:
esi_pk = "constellation_id"
esi_path_list = "Universe.get_universe_constellations"
esi_path_object = "Universe.get_universe_constellations_constellation_id"
field_mappings = {
"eve_region": "region_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
children = {"systems": "EveSolarSystem"}
load_order = 192
[docs]
@classmethod
def eve_entity_category(cls) -> str:
return EveEntity.CATEGORY_CONSTELLATION
[docs]
class EveMoon(EveUniverseEntityModel):
"""A moon in Eve Online"""
eve_planet = models.ForeignKey(
"EvePlanet", on_delete=models.CASCADE, related_name="eve_moons"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
objects = EveMoonManager()
class _EveUniverseMeta:
esi_pk = "moon_id"
esi_path_object = "Universe.get_universe_moons_moon_id"
field_mappings = {
"eve_planet": "planet_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
load_order = 220
[docs]
class EvePlanet(EveUniverseEntityModel):
"""A planet in Eve Online"""
[docs]
class Section(_SectionBase):
"""Sections that can be optionally loaded with each instance"""
ASTEROID_BELTS = "asteroid_belts" #:
MOONS = "moons" #:
eve_solar_system = models.ForeignKey(
"EveSolarSystem", on_delete=models.CASCADE, related_name="eve_planets"
)
eve_type = models.ForeignKey(
"EveType", on_delete=models.CASCADE, related_name="eve_planets"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
enabled_sections = BitField(
flags=tuple(Section.values()),
help_text=(
"Flags for loadable sections. True if instance was loaded with section."
), # no index, because MySQL does not support it for bitwise operations
) # type: ignore
objects = EvePlanetManager()
class _EveUniverseMeta:
esi_pk = "planet_id"
esi_path_object = "Universe.get_universe_planets_planet_id"
field_mappings = {
"eve_solar_system": "system_id",
"eve_type": "type_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
children = {"moons": "EveMoon", "asteroid_belts": "EveAsteroidBelt"}
load_order = 205
[docs]
def type_name(self) -> str:
"""Return shortened name of planet type.
Note: Accesses the eve_type object.
"""
matches = re.findall(r"Planet \((\S*)\)", self.eve_type.name)
return matches[0] if matches else ""
@classmethod
def _children(cls, enabled_sections: Optional[Set[str]] = None) -> dict:
enabled_sections = determine_effective_sections(enabled_sections)
children = {}
if cls.Section.ASTEROID_BELTS in enabled_sections:
children["asteroid_belts"] = "EveAsteroidBelt"
if cls.Section.MOONS in enabled_sections:
children["moons"] = "EveMoon"
return children
[docs]
class EveRegion(EveUniverseEntityModel):
"""A star region in Eve Online"""
description = models.TextField(default="")
class _EveUniverseMeta:
esi_pk = "region_id"
esi_path_list = "Universe.get_universe_regions"
esi_path_object = "Universe.get_universe_regions_region_id"
children = {"constellations": "EveConstellation"}
load_order = 190
@property
def profile_url(self) -> str:
"""URL to default third party website with profile info about this entity."""
return dotlan.region_url(self.name)
[docs]
@classmethod
def eve_entity_category(cls) -> str:
return EveEntity.CATEGORY_REGION
[docs]
class EveSolarSystem(EveUniverseEntityModel):
"""A solar system in Eve Online"""
[docs]
class Section(_SectionBase):
"""Sections that can be optionally loaded with each instance"""
PLANETS = "planets" #:
STARGATES = "stargates" #:
STARS = "stars" #:
STATIONS = "stations" #:
eve_constellation = models.ForeignKey(
"EveConstellation", on_delete=models.CASCADE, related_name="eve_solarsystems"
)
eve_star = models.OneToOneField(
"EveStar",
on_delete=models.SET_DEFAULT,
default=None,
null=True,
related_name="eve_solarsystem",
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
security_status = models.FloatField()
enabled_sections = BitField(
flags=tuple(Section.values()),
help_text=(
"Flags for loadable sections. True if instance was loaded with section."
), # no index, because MySQL does not support it for bitwise operations
) # type: ignore
class _EveUniverseMeta:
esi_pk = "system_id"
esi_path_list = "Universe.get_universe_systems"
esi_path_object = "Universe.get_universe_systems_system_id"
field_mappings = {
"eve_constellation": "constellation_id",
"eve_star": "star_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
children = {}
load_order = 194
NearestCelestial = namedtuple(
"NearestCelestial", ["eve_type", "eve_object", "distance"]
)
NearestCelestial.__doc__ = "Container for a nearest celestial"
@property
def profile_url(self) -> str:
"""Return URL to default third party website
with profile info about this solar system.
"""
return dotlan.solar_system_url(self.name)
@property
def is_high_sec(self) -> bool:
"""Return True when this solar system is in high sec, else False."""
return round(self.security_status, 1) >= 0.5
@property
def is_low_sec(self) -> bool:
"""Return True when this solar system is in low sec, else False."""
return 0 < round(self.security_status, 1) < 0.5
@property
def is_null_sec(self) -> bool:
"""Return True when this solar system is in null sec, else False."""
return (
not self.is_w_space
and not self.is_trig_space
and not self.is_abyssal_deadspace
and round(self.security_status, 1) <= 0
and not self.is_w_space
)
@property
def is_w_space(self) -> bool:
"""Return True when this solar system is in wormhole space, else False."""
return 31_000_000 <= self.id < 32_000_000
@cached_property
def is_trig_space(self) -> bool:
"""Return True when this solar system is in Triglavian space, else False."""
return self.eve_constellation.eve_region_id == EveRegionId.POCHVEN
@property
def is_abyssal_deadspace(self) -> bool:
"""Return True when this solar system is in abyssal deadspace, else False."""
return 32_000_000 <= self.id < 33_000_000
[docs]
@classmethod
def eve_entity_category(cls) -> str:
"""Return related EveEntity category."""
return EveEntity.CATEGORY_SOLAR_SYSTEM
[docs]
def distance_to(self, destination: "EveSolarSystem") -> Optional[float]:
"""Calculates the distance in meters between the current and the given solar system
Args:
destination: Other solar system to use in calculation
Returns:
Distance in meters or None if one of the systems is in WH space
"""
if not self.position_x or not self.position_y or not self.position_z:
return None
if (
not destination
or not destination.position_x
or not destination.position_y
or not destination.position_z
):
return None
if (
self.is_w_space
or destination.is_w_space
or self.is_trig_space
or destination.is_trig_space
):
return None
return math.sqrt(
(destination.position_x - self.position_x) ** 2
+ (destination.position_y - self.position_y) ** 2
+ (destination.position_z - self.position_z) ** 2
)
[docs]
def route_to(
self, destination: "EveSolarSystem"
) -> Optional[List["EveSolarSystem"]]:
"""Calculates the shortest route between the current and the given solar system
Args:
destination: Other solar system to use in calculation
Returns:
List of solar system objects incl. origin and destination
or None if no route can be found (e.g. if one system is in WH space)
"""
if (
self.is_w_space
or destination.is_w_space
or self.is_trig_space
or destination.is_trig_space
):
return None
path_ids = self._calc_route_esi(self.id, destination.id)
if path_ids is None:
return None
return [
EveSolarSystem.objects.get_or_create_esi(id=solar_system_id) # type: ignore
for solar_system_id in path_ids
]
[docs]
def jumps_to(self, destination: "EveSolarSystem") -> Optional[int]:
"""Calculates the shortest route between the current and the given solar system
Args:
destination: Other solar system to use in calculation
Returns:
Number of total jumps
or None if no route can be found (e.g. if one system is in WH space)
"""
if (
self.is_w_space
or destination.is_w_space
or self.is_trig_space
or destination.is_trig_space
):
return None
path_ids = self._calc_route_esi(self.id, destination.id)
return len(path_ids) - 1 if path_ids is not None else None
@staticmethod
def _calc_route_esi(origin_id: int, destination_id: int) -> Optional[List[int]]:
"""returns the shortest route between two given solar systems.
Route is calculated by ESI
Args:
destination_id: ID of the other solar system to use in calculation
Returns:
List of solar system IDs incl. origin and destination
or None if no route can be found (e.g. if one system is in WH space)
"""
try:
return esi.client.Routes.get_route_origin_destination(
origin=origin_id, destination=destination_id
).results()
except OSError: # FIXME: ESI is supposed to return 404,
# but django-esi is actually returning an OSError
return None
[docs]
def nearest_celestial(
self, x: int, y: int, z: int, group_id: Optional[int] = None
) -> Optional[NearestCelestial]:
"""Determine nearest celestial to given coordinates as eveuniverse object.
Args:
x, y, z: Start point in space to look from
group_id: Eve ID of group to filter results by
Raises:
HTTPError: If an HTTP error is encountered
Returns:
Eve item or None if none is found
"""
item = evesdeapi.nearest_celestial(
solar_system_id=self.id, x=x, y=y, z=z, group_id=group_id
)
if not item:
return None
eve_type, _ = EveType.objects.get_or_create_esi(id=item.type_id) # type: ignore
class_mapping = {
EveGroupId.ASTEROID_BELT: EveAsteroidBelt,
EveGroupId.MOON: EveMoon,
EveGroupId.PLANET: EvePlanet,
EveGroupId.STAR: EveStar,
EveGroupId.STARGATE: EveStargate,
EveGroupId.STATION: EveStation,
}
try:
my_class = class_mapping[eve_type.eve_group_id]
except KeyError:
return None
obj, _ = my_class.objects.get_or_create_esi(id=item.id)
return self.NearestCelestial(
eve_type=eve_type, eve_object=obj, distance=item.distance
)
@classmethod
def _children(cls, enabled_sections: Optional[Set[str]] = None) -> dict:
enabled_sections = determine_effective_sections(enabled_sections)
children = {}
if cls.Section.PLANETS in enabled_sections:
children["planets"] = "EvePlanet"
if cls.Section.STARGATES in enabled_sections:
children["stargates"] = "EveStargate"
if cls.Section.STATIONS in enabled_sections:
children["stations"] = "EveStation"
return children
@classmethod
def _disabled_fields(cls, enabled_sections: Optional[Set[str]] = None) -> set:
enabled_sections = determine_effective_sections(enabled_sections)
if cls.Section.STARS not in enabled_sections:
return {"eve_star"}
return set()
@classmethod
def _inline_objects(cls, enabled_sections: Optional[Set[str]] = None) -> dict:
if not enabled_sections or cls.Section.PLANETS not in enabled_sections:
return {}
return super()._inline_objects()
[docs]
class EveStar(EveUniverseEntityModel):
"""A star in Eve Online"""
age = models.BigIntegerField()
eve_type = models.ForeignKey(
"EveType", on_delete=models.CASCADE, related_name="eve_stars"
)
luminosity = models.FloatField()
radius = models.PositiveIntegerField()
spectral_class = models.CharField(max_length=16)
temperature = models.PositiveIntegerField()
class _EveUniverseMeta:
esi_pk = "star_id"
esi_path_object = "Universe.get_universe_stars_star_id"
field_mappings = {"eve_type": "type_id"}
load_order = 222
[docs]
class EveStargate(EveUniverseEntityModel):
"""A stargate in Eve Online"""
destination_eve_stargate = models.OneToOneField(
"EveStargate", on_delete=models.SET_DEFAULT, null=True, default=None, blank=True
)
destination_eve_solar_system = models.ForeignKey(
"EveSolarSystem",
on_delete=models.SET_DEFAULT,
null=True,
default=None,
blank=True,
related_name="destination_eve_stargates",
)
eve_solar_system = models.ForeignKey(
"EveSolarSystem", on_delete=models.CASCADE, related_name="eve_stargates"
)
eve_type = models.ForeignKey(
"EveType", on_delete=models.CASCADE, related_name="eve_stargates"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
objects = EveStargateManager()
class _EveUniverseMeta:
esi_pk = "stargate_id"
esi_path_object = "Universe.get_universe_stargates_stargate_id"
field_mappings = {
"destination_eve_stargate": ("destination", "stargate_id"),
"destination_eve_solar_system": ("destination", "system_id"),
"eve_solar_system": "system_id",
"eve_type": "type_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
dont_create_related = {
"destination_eve_stargate",
"destination_eve_solar_system",
}
load_order = 224
[docs]
class EveStation(EveUniverseEntityModel):
"""A space station in Eve Online"""
eve_race = models.ForeignKey(
"EveRace",
on_delete=models.SET_DEFAULT,
default=None,
null=True,
related_name="eve_stations",
)
eve_solar_system = models.ForeignKey(
"EveSolarSystem",
on_delete=models.CASCADE,
related_name="eve_stations",
)
eve_type = models.ForeignKey(
"EveType",
on_delete=models.CASCADE,
related_name="eve_stations",
)
max_dockable_ship_volume = models.FloatField()
office_rental_cost = models.FloatField()
owner_id = models.PositiveIntegerField(default=None, null=True, db_index=True)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
reprocessing_efficiency = models.FloatField()
reprocessing_stations_take = models.FloatField()
services = models.ManyToManyField("EveStationService")
class _EveUniverseMeta:
esi_pk = "station_id"
esi_path_object = "Universe.get_universe_stations_station_id"
field_mappings = {
"eve_race": "race_id",
"eve_solar_system": "system_id",
"eve_type": "type_id",
"owner_id": "owner",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
inline_objects = {"services": "EveStationService"}
load_order = 207
[docs]
@classmethod
def eve_entity_category(cls) -> str:
return EveEntity.CATEGORY_STATION
@classmethod
def _update_or_create_inline_objects(
cls,
*,
parent_eve_data_obj: dict,
parent_obj,
wait_for_children: bool,
enabled_sections: Iterable[str],
task_priority: Optional[int] = None,
) -> None:
"""updates_or_creates station service objects for EveStations"""
if "services" in parent_eve_data_obj:
services = []
for service_name in parent_eve_data_obj["services"]:
service, _ = EveStationService.objects.get_or_create(name=service_name)
services.append(service)
if services:
parent_obj.services.add(*services)
[docs]
class EveStationService(models.Model):
"""A service in a space station"""
name = models.CharField(max_length=50, unique=True)
def __str__(self) -> str:
return self.name