bluemap
This module provides tools for rendering influence maps for games like Eve Online/Echoes. The rendering is implemented in C++ and Cython to achieve high performance, while still maintaining a simple and flexible Python API. Please note that the API is still in development and might change until the first stable release.
Bluemap is an influence map generator for games like Eve Online/Echoes. It is based on the algorithm from Paladin Vent (which was continued by Verite Rendition), but was written from scratch in C++ and Cython. It is designed to be faster and easier to use.
While the algorithm for the influence layer itself stayed the same, it was not a stable implementation as the
final output depended on the processing order. Which, when using structures like maps or sets, is not guaranteed to be
the same every time. It is especially different between the legacy Java implementation and the new C++ one. Also, this
was an issue because the actual ids of owners and systems change how the final output looks like. So the original
recursive DFS algorithm was replaced with an iterative BFS algorithm to ensure the same output every time. The legacy
algorithm can be found at the legacy-algorithm
tag.
The python API documentation can be found here.
This project is still work in progress. The API might change until a stable version is released. If you decide to already use it, please make sure to pin the version in your requirements.txt file. Until version 1.0.0 is released, minor versions might contain breaking changes. I will try to keep the changes as minimal as possible, but I cannot guarantee that there will be no breaking changes.
If you find a bug or have a feature request, please open an issue on GitHub.
Overview
As stated before, this project is implemented in C++ and Cython. The C++ part is responsible for the rendering of the influence layer, and the calculation of the owner label positions. All other parts are implemented in Cython and Python.
The C++ library does work in general standalone, but except for a testing tool that requires a specific file format as input, there is no real way to use it directly. So you would have to write your own wrapper around it, which loads the data from some source.
Installation
PyPi has precompiled wheels for Windows (64bit), Linux and macOS (min version 14.0, untested). 32bit Windows is supported but not automated. PyPi may or may not have a precompiled wheel for 32bit Windows.
We support Python 3.12 and higher (atm 3.12 and 3.13 on PyPi)
The precompiled package can be installed via pip. There are multiple variations that can be installed via pip:
Name | Map | Tables | MySQL DB |
---|---|---|---|
bluemap[minimal] |
✅ | ❌ | ❌ |
bluemap[table] |
✅ | ✅ | ❌ |
bluemap[CLI] |
✅ | ✅ | ✅ |
e.g. to install the full version, you can use the following command:
pip install bluemap[CLI]
- Map: The module for rendering the influence map
- Tables: The module for rendering tables (depends on Pillow)
- MySQL DB: The module for loading data from a MySQL database (depends on pymysql)
Also note all functionality is available in the bluemap
package. The extras are only for the convenience of the
installation. You can also install the base version and add the dependencies manually.
Usage (CLI)
The CLI supports rendering of maps with data from a mysql database. The program will create all required tables on the first run. However, you do have to populate the tables yourself. You can find the static data for Eve Online on the UniWIKI. For the sovereignty data, you need to use the ESI API.
Arg | Description |
---|---|
--help,-h |
Show the help message |
--host HOST |
The host of the db |
--user USER |
The user for the db |
--password PASSWORD |
The password for the db (empty for no password) |
--database DATABASE |
The database to use |
--text [header] TEXT |
Extra text to render (see below) |
--output,-o OUTPUT |
The output file for the image |
--map_out,-mo PATH |
The output file for the map data |
--map_in,-mi PATH |
The input file for the old map data |
The database args are all required, the other ones are optional. map_in
and map_out
are used for the rendering of
changed influence areas. If the old map is provided, in areas where the influence changed, the old influence will be
rendered as diagonal lines. These files in principle simply store the id of the owner for every pixel. Please refer
to the implementation for the exact format.
The text
argument is used to render additional text in the top left corner. This argument may be repeated multiple
times for multiple lines of text. There are three ways to use this
--text "Some text"
: This will render the text in the default font--text header "Some text"
: This will render the text in the header font (bold)--text
: This will render an empty line (but an empty string would also work)
(all three ways can be chained for multiple lines)
Example usage:
python -m bluemap.main \
--host localhost \
--user root \
--password "" \
--database evemap \
-o influence.png \
-mi sovchange_2025-02-16.dat \
-mo sovchange_2025-02-23.dat \
--text header "Influence Map" \
--text \
--text "Generated by Blaumeise03"
Usage (Library)
The library is very simple to use. You can find an example inside the main.py file. The main class
is the SovMap
class. This does all the heavy lifting. The load_data
method is used to load the data into the map.
Please note that the API is subject to change until a stable version is released. I recommend pinning the version in your requirements.txt file and manually update it.
🚨 IMPORTANT 🚨: Before you use any of the
SovMap.set_XYZ_function
methods, read theCustomization
section below. Otherwise, you may run into memory leaks.
from bluemap import SovMap
sov_map = SovMap()
sov_map.load_data(
owners=[{
'id': 10001,
'color': (0, 255, 0),
'name': 'OwnerA',
'npc': False,
}],
systems=[
{
'id': 20001, 'name': 'Jita',
'constellation_id': 30001, 'region_id': 40001,
'x': -129064e12, 'y': 60755e12, 'z': -117469e12,
'has_station': True,
'sov_power': 6.0,
'owner': 10001,
}, {'id': 20002, 'name': ...}
],
connections=[
(20001, 20002),
],
regions=[{'id': 40001, 'name': 'The Forge',
'x': -96420e12, 'y': 64027e12, 'z': -112539e12},
],
filter_outside=True, # Will skip all systems outside the map
)
For the rendering, please refer to the render
method inside the main.py. You can see the usage
with documentation there.
Rendering
Some more special methods. First of all, the rendering is implemented in C++ and does not interact with Python. Therefore, it can be used with Python's multithreading. In general, all methods are thread safe. But any modifications to the map are blocked as long as any thread is rendering. The rendering will split the map into columns, every thread will render one column. There is a default implementation inside _map.pyx:
from bluemap import SovMap, ColumnWorker
sov_map = SovMap()
if not sov_map.calculated:
sov_map.calculate_influence()
from concurrent.futures.thread import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=16) as pool:
workers = sov_map.create_workers(16)
pool.map(ColumnWorker.render, workers)
But please make sure, two ColumnWorkers who overlap are not thread safe. The create_workers
method will generate
disjoint workers. But if you call the method multiple times, you have to make sure the workers are disjointed. See the
source code of the SovMap.render
functions for more information.
Tables
The module bluemap.table
contains classed for rendering of tables. This requires the Pillow
package. Please refer
to the example inside the main.py file on how to use it.
Algorithm Details
The algorithm is based on the algorithm from Paladin Vent. It has two steps: The spreading of influence over neighbored systems, and the actual calculation of influence per pixel. Every system starts with an optional owner and a power value.
Influence Spreading
In the first step, this initial power is spread over the neighbors via the connections. The initial power is first
converted into its actual power used for the algorithm. That was done because in the original implementation, the power
input was simply the ADM level of the system which needed to be converted to yield nicer results. This power_function
can be modified by the user. The default implementation is:
def power_function(
sov_power: float,
has_station: bool,
owner_id: int) -> float:
return 10.0 * (6 if sov_power >= 6.0 else sov_power / 2.0)
Any function that matches this signature can be used. In the C++ implementation, this can be done by providing a
matching std::function
. When compiled against CPython (which happens in the PyPi builds), a python callable may be
provided via SovMap.set_sov_power_function
.
The spreading of the influence is done via a BFS algorithm. The influence is spread over the connections to the neighbored systems. For every jump, the influence is being reduced. By default, the influence is reduced to 30% of the previous value. This can be modified as well:
def power_falloff_function(
value: float,
base_value: float,
distance: int) -> float:
return value * 0.3
This function is evaluated every time the distance is increased. The value
is the current influence, the base_value
is the original influence and the distance
is the current distance from the source system. If the falloff returns
0 or less, the algorithm will stop spreading the influence. This can be used to limit the influence to a certain range.
This is WIP, at the moment there is still a hard limit of two jumps (three for systems with an ADM of 6 or above).
For every system, a map of owners with their accumulated influence is created.
Influence Aggregation
The second step is done on the image itself for every pixel, the topology of the map is no longer relevant. For every
pixel, the influences of all systems in a 400 pixel radius are accumulated. The influence is weighted by the following
formula: power / (500 + dist_sq)
. This is done for every owner for every system. The owner with the highest influence
is considered the owner of the pixel and will be rendered in the final image.
The influence is rendered as the color of the owner, with the alpha channel representing the influence according to the following function:
import math
def influence_to_alpha(influence: float) -> float:
return float(min(
190,
int(math.log(math.log(influence + 1.0) + 1.0) * 700)))
Additionally, the borders of the influence areas are rendered with a stronger color (higher alpha value).
Old Owner Overlay
The algorithm does generate two images. The first one is the RGBA image itself. But additionally, a second image containing the owner id for every pixel is generated. This image can be provided the next time to the algorithm to highlight areas where the influence changed. The old owner will be rendered as diagonal lines in the final image.
Customization
As stated before, a lot of functions for the rendering can be customized. The default functions are implemented in C++ are really fast. Replacing them with Python functions adds considerable overhead. For the influence spreading, this is not a big issue, as this is pretty fast anyway and does not scale with the size of the image. But for the influence aggregation, this can be a problem. Simply replacing the C++ functions with Python functions will double the rendering time.
It is planned to provide a more efficient way of specifying simple mathematical expressions a strings, which get compiled into a callable that does not hook into python. This is not implemented yet.
All objects that are a function, a bound method, or a callable (that implements
__call__
) can be used as a function. For bound methods, theself
argument is allowed in the signature.
🚨 IMPORTANT 🚨: Do not provide methods of the SovMap class (or ony inherited class) as a function. This will cause a circular reference and will prevent the garbage collector from collecting the object. The underlying C++ object will hold a reference to the callable and the SovMap class holds a reference to the C++ class. If a bound method is provided (which holds a reference to the bound object), we have a cycle. I do not think the garbage collector is able to infer that this is a cycle and thus will never collect the object.
Alternatively, you can implement a wrapper function that holds a weak reference to the callable. As long as you ensure that the callable is not deallocated while the SovMap is in use, this should work.
The return types of the functions are strict. The functions must exactly return the type they are supposed to return.
One last thing that can be customized is the automatic color generation. If the algorithm tries to render an owner that
does not yet have a color assigned, it will generate a new color. By default, the SovMap class has this function
implemented (SovMap.next_color
). But another function may be passed, it must have this signature:
def next_color(owner_id: int) -> tuple[int, int, int]:
pass
But please note, the set_generate_owner_color_function
does only affect the rendering of the influence layer. The
function SovMap.draw_systems
does also generate colors for owners, but it will always use the next_color
function
from the SovMap class.
Building
Python
On windows, this project requires the MSVC compiler. The library requires at least C++17, if you use a different
compiler on windows, you will have to modify the extra_compile_args
inside the setup.py file.
Also, this project requires Python 3.12 or higher. I have not tested it with lower versions, but the C++ code gets compiled against CPython and uses features from the C-API that require Python 3.12. That being said, this is technically speaking not required. You could disable the C-API usage, but at the moment, you would have to remove the functions from the Cython code that depend on the C-API.
Compiling can either happen via
python -m build
or, if you want to build it in-place
python setup.py build_ext --inplace
this will also generate .html
files for an analysis of the Cython code.
Standalone
This project has a small CMakelists.txt file that can be used to compile the C++ code as a standalone executable. It
does download std_image_write from GitHub to write the png image. However, as I have mentioned, the C++ code has no
nice way to load the data. Refer to Map::load_data
inside the Map.cpp file for the required format.
Credits
The original algorithm was created by Paladin Vent and continued by Verite Rendition. Verite's version can be found at https://www.verite.space/. I do not know if Paladin Vent has a website (feel free to contact me to add it here). The original algorithm was written in Java and can be found on Verite's website.
Core classes
1""" 2This module provides tools for rendering influence maps for games like Eve Online/Echoes. The rendering is implemented 3in C++ and Cython to achieve high performance, while still maintaining a simple and flexible Python API. Please note 4that the API is still in development and might change until the first stable release. 5 6.. include:: ../README.md 7 :start-line: 2 8 9# Core classes 10""" 11 12__all__ = ['SovMap', 'ColumnWorker', 'SolarSystem', 'Region', 'Owner', 'MapOwnerLabel', 'OwnerImage', 'stream', 'table'] 13 14from ._map import *
Initialize the map with the given size and offset. The offset is used to shift the map around.
Parameters
- width:
- height:
- offset_x:
- offset_y:
This is a blocking operation on the underlying map object.
Parameters
- filename:
Returns
Set the function that calculates the sov power for a system. The function must take three arguments: the sov power of the system according to the data source, a boolean indicating if the system has a station and the owner ID of the system. The function must return the sov power that should be used for the map. The id is 0 if the system has no owner.
If this function is not set, the default function
>>> lambda sov_power, _, __ = 10.0 * (6 if sov_power >= 6.0 else sov_power / 2.0)
will be used. This is implemented in the C++ code, however setting a python function has no measurable performance impact. At least if a simple function is used, more complex ones will of course bump up the time. But this penalty only scales with the number of systems and is independent of the resolution of the map. Only the call to calculate_influence will be slower.
IMPORTANT: THIS FUNCTION MAY NOT CALL ANY FUNCTIONS THAT WILL MODIFY/READ FROM THE MAP. THIS WILL RESULT IN A DEADLOCK. This affects all functions marked with "This is a blocking operation on the underlying map object."
Parameters
- func: the function (double, bool, int) -> double
Returns
Set the function that calculates the power falloff for the spreading of the influence. The function must take three arguments: the power of the previous system, the power of the source system and the number of jumps to the source system. The function must return the new power of the current system.
If this function is not set, the default function
>>> lambda value, _, __: value * 0.3
will be used. This is implemented in the C++ code, however setting a python function has no measurable performance impact.
IMPORTANT: THIS FUNCTION MAY NOT CALL ANY FUNCTIONS THAT WILL MODIFY/READ FROM THE MAP. THIS WILL RESULT IN A DEADLOCK. This affects all functions marked with "This is a blocking operation on the underlying map object."
Parameters
- func: the function (double, double, int) -> double
Returns
Sets the function that converts the influence value to the alpha value of the pixel. The function must take one argument: the influence value and return the alpha value (0-255) of the pixel.
If this function is not set, the default function
>>> import math
>>> lambda influence: float(min(190, int(math.log(math.log(influence + 1.0) + 1.0) * 700)))
will be used. It must return a float. This is implemented in the C++ code, exchaning it with a python function will affect the performance of the rendering. This function is called for every pixel of the map. Using this default python implementation will double the time it takes to render the map.
IMPORTANT: THIS FUNCTION MAY NOT CALL ANY FUNCTIONS THAT WILL MODIFY/READ FROM THE MAP. THIS WILL RESULT IN A DEADLOCK. This affects all functions marked with "This is a blocking operation on the underlying map object."
Parameters
- func: the function (double) -> double
Returns
Set the function that generates the color for an owner. The function must take one argument: the owner ID and return a tuple of three integers (0-255) representing the color of the owner.
It will be called for every owner that should get rendered, but doesn't have a color set.
Parameters
- func:
Returns
Load data into the map. Only systems inside the map will be saved, other systems will be ignored.
This is a blocking operation on the underlying map object.
Parameters
- owners: a list of owner data, each entry is a dict with the keys 'id' (int), 'color' (3-tuple) and 'npc' (bool). Optionally 'name' (str)
- systems: a list of system data, each entry is a dict with the keys 'id', 'x', 'z', 'constellation_id', 'region_id', 'has_station', 'sov_power' and 'owner'
- connections: a list of jump data, each entry is a tuple of two system IDs
- regions: a list of region data (or None), each entry is a dict with the keys 'id', 'x', 'z' and optionally 'name' (str)
- filter_outside: if True, systems outside the map will be skipped
Returns
Render the map. This method will calculate the influence of each owner and render the map. The rendering is done in parallel using the given number of threads.
Warning: Calling this method while a rendering is already in progress is not safe and is considered undefined behavior.
Parameters
- thread_count:
Returns
This is a blocking operation on the underlying map object.
Returns
Get the image as a buffer. This method will remove the image from the map, further calls to this method will return None. The buffer wrapper provides already two methods to convert the image to a Pillow image or a numpy array. But it can be used by any function that supports the buffer protocol.
See https://docs.python.org/3/c-api/buffer.html
>>> import PIL.Image
>>> sov_map = SovMap()
>>> #...
>>> image = sov_map.get_image().as_pil_image()
Or to create a numpy array:
>>> image = sov_map.get_image().as_ndarray()
This is a blocking operation on the underlying map object.
Returns
the image buffer if available, None otherwise
Save the image to a file. Requires Pillow or OpenCV to be installed. Use the get_image method if you want to get better control over the image.
This method will remove the image from the map, further calls to get_image will return None and further calls to save will raise a ValueError.
This is a blocking operation on the underlying map object.
Parameters
- path:
- strategy: the strategy to use for saving the image, either "PIL" or "cv2". If None, the first available strategy will be used.
Raises
- ImportError: if no strategy is available (i.e., neither PIL, nor opencv-python are installed)
- RuntimeError: if no image is available
- ValueError: if an invalid strategy is provided
Returns
Returns the owner buffer as a BufferWrapper. The buffer contains the owner IDs for each pixel (0 = None). Calling this function will deplet the buffer from the map, further calls to this function, or to get_owner_image will return None.
The buffer can be used to get a numpy array or a OwnerImage:
>>> sov_map = SovMap()
>>> #...
>>> buffer = sov_map.get_owner_buffer()
>>> arr = buffer.as_ndarray()
>>> owner_image = OwnerImage(buffer)
>>> # OR
>>> owner_image = sov_map.get_owner_image()
This is a blocking operation on the underlying map object.
Returns
Returns the owner image as an OwnerImage. The owner image is a special image that contains the owner IDs for each pixel. The owner IDs are 0 for None. The owner image can be saved and loaded to/from disk.
See also get_owner_buffer.
This is a blocking operation on the underlying map object.
Returns
Save the owner data to a file. This data is required for the rendering of the next map to highlight changed owners. It is however optional, if the old data is not provided, only the current sov data will be used for rendering.
This is a blocking operation on the underlying map object.
Parameters
- path: the path to save the owner data to
- compress: whether to compress the data or not
Raises
- ValueError: if no owner image is available (not possible irc)
Returns
Load the old owner data from a file. This data is required for the rendering of the next map to highlight changed owners. It is however optional, if the old data is not provided, only the current sov data will be used for rendering.
This is a blocking operation on the underlying map object.
Parameters
- path: the path to load the owner data from
Raises
- FileNotFoundError: if the file does not exist
- RuntimeError: if the resolution of the owner data does not match the resolution of the map
Returns
Load the old owner data from an OwnerImage.
WARNING: This method will take over the ownership of the buffer from the OwnerImage. The OwnerImage will be unusable after this method is called. If you want to keep the OwnerImage, make a copy of it before calling this method.
This is a blocking operation on the underlying map object.
Parameters
- owner_image: the owner image to load the data from
Returns
Update the size of the map. This will recalculate the scale automatically.
This is a blocking operation on the underlying map object.
Parameters
- width: the new width (or None to keep the current width)
- height: the new height (or None to keep the current height)
- sample_rate: the new sample rate (or None to keep the current sample rate)
Returns
Draw the solar systems on the map. This method will draw the solar systems on the given ImageDraw object. The ImageDraw object must have the same resolution as the map.
Parameters
- draw: the ImageDraw object to draw the systems on
Returns
Get the next color for the given owner ID. This method will generate a new color for the owner. It will be a unique color not used by any other owner and will be added to the color_table and can be later retrieved by get_new_colors() to persist the colors.
Parameters
- owner_id:
Returns
Get a dictionary of all solar systems in the map. The key is the system ID, the value is the SolarSystem object.
Modifications to the dict or the SolarSystem objects will not be reflected in the map. Modifications might cause unexpected behavior.
Returns
Get a dictionary of all owners in the map. The key is the owner ID, the value is the Owner object.
Modifications to the dict or the Owner objects will not be reflected in the map. Modifications might cause unexpected behavior.
Returns
Get a list of all connections in the map. Each connection is a tuple of two system IDs.
Modifications to the list will not be reflected in the map. Modifications might cause unexpected behavior.
Returns
The sample rate used to calculate the position of owner labels. Default is 8.
Returns
the sample rate
The scale used to calculate the position of solar systems. Default is 4.8445284569785E17 / ((width - 20) / 2.0). If other values are set, the scale will be recalculated. So if you want to set a custom scale, set scale after setting width and height.
Returns
Renders this column (blocking). Rendering happens without the GIL, so this method can be called on different objects from multiple threads simultaneously to speed up rendering. Calling this method on the same object from multiple threads is not possible and will result in the two calls being serialized.
See also SovMap.render() for an example multithreaded rendering implementation. It is recommended to use that method instead of calling this method directly.