fix merge conflict, add a logging error to temporarily solve issue removing a wireless link when multiple wireless links exist
This commit is contained in:
commit
08d4bf98c7
19 changed files with 407 additions and 102 deletions
56
CHANGELOG.md
56
CHANGELOG.md
|
@ -1,3 +1,35 @@
|
||||||
|
## 2020-02-20 CORE 6.1.0
|
||||||
|
* New
|
||||||
|
* config services - these services leverage a proper template engine and have configurable parameters, given enough time may replace existing services
|
||||||
|
* core-imn-to-xml - IMN to XML utility script
|
||||||
|
* replaced internal code for determining ip/mac address with netaddr library
|
||||||
|
* Enhancements
|
||||||
|
* added distributed package for built packages
|
||||||
|
* made use of python type hinting for functions and their return values
|
||||||
|
* updated Quagga zebra service to remove deprecated warning
|
||||||
|
* Removed
|
||||||
|
* removed stale ns3 code
|
||||||
|
* CORETK GUI
|
||||||
|
* added logging
|
||||||
|
* improved error dialog
|
||||||
|
* properly use global ipv6 addresses for nodes
|
||||||
|
* disable proxy usage by default, flag available to enable
|
||||||
|
* gRPC API
|
||||||
|
* add_link - now returns created interface information
|
||||||
|
* set_node_service - can now set files and directories to properly replicate previous usage
|
||||||
|
* get_emane_event_channel - return information related to the currently used emane event channel
|
||||||
|
* Bugfixes
|
||||||
|
* fixed session SDT functionality back to working order, due to python3 changes
|
||||||
|
* avoid shutting down services for nodes that are not up
|
||||||
|
* EMANE bypass model options will now display properly in GUIs
|
||||||
|
* XML scenarios will now properly read in custom node icons
|
||||||
|
* \#372 - fixed mobility waypoint comparisons
|
||||||
|
* \#370 - fixed radvd service
|
||||||
|
* \#368 - updated frr services to properly start staticd when needed
|
||||||
|
* \#358 - fixed systemd service install path
|
||||||
|
* \#350 - fixed frr babel wireless configuration
|
||||||
|
* \#354 - updated frr to reset interfaces to properly take configurations
|
||||||
|
|
||||||
## 2020-01-01 CORE 6.0.0
|
## 2020-01-01 CORE 6.0.0
|
||||||
* New
|
* New
|
||||||
* beta release of the python based tk GUI, use **coretk-gui** to try it out, plan will be to eventually sunset the old GUI once this is good enough
|
* beta release of the python based tk GUI, use **coretk-gui** to try it out, plan will be to eventually sunset the old GUI once this is good enough
|
||||||
|
@ -114,11 +146,11 @@
|
||||||
* Added EMANE prefix configuration when looking for emane model manifest files
|
* Added EMANE prefix configuration when looking for emane model manifest files
|
||||||
* requires configuring **emane_prefix** in /etc/core/core.conf
|
* requires configuring **emane_prefix** in /etc/core/core.conf
|
||||||
* Cleanup
|
* Cleanup
|
||||||
* Refactoring of the core python package structure, trying to help provide better organization and
|
* Refactoring of the core python package structure, trying to help provide better organization and
|
||||||
logical groupings
|
logical groupings
|
||||||
* Issues
|
* Issues
|
||||||
* \#246 - Fixed network to network link handling when reading xml files
|
* \#246 - Fixed network to network link handling when reading xml files
|
||||||
* \#236 - Fixed storing/reading of link configuration values within xml files
|
* \#236 - Fixed storing/reading of link configuration values within xml files
|
||||||
* \#170 - FRR Service
|
* \#170 - FRR Service
|
||||||
* \#155 - EMANE path configuration
|
* \#155 - EMANE path configuration
|
||||||
* \#233 - Python 3 support
|
* \#233 - Python 3 support
|
||||||
|
@ -152,16 +184,16 @@
|
||||||
|
|
||||||
## 2018-05-22 CORE 5.1
|
## 2018-05-22 CORE 5.1
|
||||||
* DAEMON:
|
* DAEMON:
|
||||||
* removed and cleared out code that is either legacy or no longer supported (Xen, BSD, Kernel patching, RPM/DEB
|
* removed and cleared out code that is either legacy or no longer supported (Xen, BSD, Kernel patching, RPM/DEB
|
||||||
specific files)
|
specific files)
|
||||||
* default nodes are now set in the node map
|
* default nodes are now set in the node map
|
||||||
* moved ns3 and netns directories to the top of the repo
|
* moved ns3 and netns directories to the top of the repo
|
||||||
* changes to make use of fpm as the tool for building packages
|
* changes to make use of fpm as the tool for building packages
|
||||||
* removed usage of logzero to avoid dependency issues for built packages
|
* removed usage of logzero to avoid dependency issues for built packages
|
||||||
* removed daemon addons directory
|
* removed daemon addons directory
|
||||||
* added CoreEmu to core.emulator.coreemu to help begin serving as the basis for a more formal API for scripting
|
* added CoreEmu to core.emulator.coreemu to help begin serving as the basis for a more formal API for scripting
|
||||||
and creating new external APIs out of
|
and creating new external APIs out of
|
||||||
* cleaned up logging, moved more logging to DEBUG from INFO, tried to mold INFO message to be more simple and
|
* cleaned up logging, moved more logging to DEBUG from INFO, tried to mold INFO message to be more simple and
|
||||||
informative
|
informative
|
||||||
* EMANE 1.0.1-1.21 supported
|
* EMANE 1.0.1-1.21 supported
|
||||||
* updates to leverage EMANE python bindings for dynamically parsing phy/mac manifest files
|
* updates to leverage EMANE python bindings for dynamically parsing phy/mac manifest files
|
||||||
|
@ -175,7 +207,7 @@
|
||||||
* updated broken help links in GUI Help->About
|
* updated broken help links in GUI Help->About
|
||||||
* Packaging:
|
* Packaging:
|
||||||
* fixed PYTHON_PATH to PYTHONPATH in sysv script
|
* fixed PYTHON_PATH to PYTHONPATH in sysv script
|
||||||
* added make command to leverage FPM as the tool for creating deb/rpm packages going forward, there is documentation
|
* added make command to leverage FPM as the tool for creating deb/rpm packages going forward, there is documentation
|
||||||
within README.md to try it out
|
within README.md to try it out
|
||||||
* TEST:
|
* TEST:
|
||||||
* fixed some broken tests
|
* fixed some broken tests
|
||||||
|
@ -184,7 +216,7 @@
|
||||||
* \#142 - duplication of custom services
|
* \#142 - duplication of custom services
|
||||||
* \#136 - sphinx-apidoc command not found
|
* \#136 - sphinx-apidoc command not found
|
||||||
* \#137 - make command fails when using distclean
|
* \#137 - make command fails when using distclean
|
||||||
|
|
||||||
## 2017-09-01 CORE 5.0
|
## 2017-09-01 CORE 5.0
|
||||||
* DEVELOPMENT:
|
* DEVELOPMENT:
|
||||||
* support for editorconfig to help standardize development across IDEs, from the defined configuration file
|
* support for editorconfig to help standardize development across IDEs, from the defined configuration file
|
||||||
|
@ -339,7 +371,7 @@
|
||||||
* added "--addons" startup mode to pass control to code included from addons dir
|
* added "--addons" startup mode to pass control to code included from addons dir
|
||||||
* added "Locked" entry to View menu to prevent moving items
|
* added "Locked" entry to View menu to prevent moving items
|
||||||
* use currently selected node type when invoking a topology generator
|
* use currently selected node type when invoking a topology generator
|
||||||
* updated throughput plots with resizing, color picker, plot labels, locked scales, and save/load plot
|
* updated throughput plots with resizing, color picker, plot labels, locked scales, and save/load plot
|
||||||
configuration with imn file
|
configuration with imn file
|
||||||
* improved session dialog
|
* improved session dialog
|
||||||
* EMANE:
|
* EMANE:
|
||||||
|
@ -356,11 +388,11 @@
|
||||||
* XML import and export
|
* XML import and export
|
||||||
* renamed "cored.py" to "cored", "coresendmsg.py" to "coresendmsg"
|
* renamed "cored.py" to "cored", "coresendmsg.py" to "coresendmsg"
|
||||||
* code reorganization and clean-up
|
* code reorganization and clean-up
|
||||||
* updated XML export to write NetworkPlan, MotionPlan, and ServicePlan within a Scenario tag, added new
|
* updated XML export to write NetworkPlan, MotionPlan, and ServicePlan within a Scenario tag, added new
|
||||||
"Save As XML..." File menu entry
|
"Save As XML..." File menu entry
|
||||||
* added script_start/pause/stop options to Ns2ScriptedMobility
|
* added script_start/pause/stop options to Ns2ScriptedMobility
|
||||||
* "python" source sub-directory renamed to "daemon"
|
* "python" source sub-directory renamed to "daemon"
|
||||||
* added "cored -e" option to execute a Python script, adding its session to the active sessions list, allowing for
|
* added "cored -e" option to execute a Python script, adding its session to the active sessions list, allowing for
|
||||||
GUI connection
|
GUI connection
|
||||||
* support comma-separated list for custom_services_dir in core.conf file
|
* support comma-separated list for custom_services_dir in core.conf file
|
||||||
* updated kernel patches for Linux kernel 3.5
|
* updated kernel patches for Linux kernel 3.5
|
||||||
|
@ -369,7 +401,7 @@
|
||||||
* integrate ns-3 node location between CORE and ns-3 simulation
|
* integrate ns-3 node location between CORE and ns-3 simulation
|
||||||
* added ns-3 random walk mobility example
|
* added ns-3 random walk mobility example
|
||||||
* updated ns-3 Wifi example to allow GUI connection and moving of nodes
|
* updated ns-3 Wifi example to allow GUI connection and moving of nodes
|
||||||
* fixed the following bugs: 54, 103, 111, 136, 145, 153, 157, 160, 161, 162, 164, 165, 168, 170, 171, 173, 174, 176,
|
* fixed the following bugs: 54, 103, 111, 136, 145, 153, 157, 160, 161, 162, 164, 165, 168, 170, 171, 173, 174, 176,
|
||||||
184, 190, 193
|
184, 190, 193
|
||||||
|
|
||||||
## 2012-09-25 CORE 4.4
|
## 2012-09-25 CORE 4.4
|
||||||
|
@ -410,7 +442,7 @@
|
||||||
* support /etc/core/environment and ~/.core/environment files
|
* support /etc/core/environment and ~/.core/environment files
|
||||||
* added Ns2ScriptedMobility model to Python, removed from the GUI
|
* added Ns2ScriptedMobility model to Python, removed from the GUI
|
||||||
* namespace nodes mount a private /sys
|
* namespace nodes mount a private /sys
|
||||||
* fixed the following bugs: 80, 81, 84, 99, 104, 109, 110, 122, 124, 131, 133, 134, 135, 137, 140, 143, 144, 146,
|
* fixed the following bugs: 80, 81, 84, 99, 104, 109, 110, 122, 124, 131, 133, 134, 135, 137, 140, 143, 144, 146,
|
||||||
147, 151, 154, 155
|
147, 151, 154, 155
|
||||||
|
|
||||||
## 2012-03-07 CORE 4.3
|
## 2012-03-07 CORE 4.3
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# Process this file with autoconf to produce a configure script.
|
# Process this file with autoconf to produce a configure script.
|
||||||
|
|
||||||
# this defines the CORE version number, must be static for AC_INIT
|
# this defines the CORE version number, must be static for AC_INIT
|
||||||
AC_INIT(core, 6.0.0)
|
AC_INIT(core, 6.1.0)
|
||||||
|
|
||||||
# autoconf and automake initialization
|
# autoconf and automake initialization
|
||||||
AC_CONFIG_SRCDIR([netns/version.h.in])
|
AC_CONFIG_SRCDIR([netns/version.h.in])
|
||||||
|
|
|
@ -74,6 +74,9 @@ bootfrr()
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bootdaemon "zebra"
|
bootdaemon "zebra"
|
||||||
|
if grep -q "^ip route " $FRR_CONF; then
|
||||||
|
bootdaemon "staticd"
|
||||||
|
fi
|
||||||
for r in rip ripng ospf6 ospf bgp babel; do
|
for r in rip ripng ospf6 ospf bgp babel; do
|
||||||
if grep -q "^router \\<$${}{r}\\>" $FRR_CONF; then
|
if grep -q "^router \\<$${}{r}\\>" $FRR_CONF; then
|
||||||
bootdaemon "$${}{r}d"
|
bootdaemon "$${}{r}d"
|
||||||
|
@ -93,3 +96,10 @@ if [ "$1" != "zebra" ]; then
|
||||||
fi
|
fi
|
||||||
confcheck
|
confcheck
|
||||||
bootfrr
|
bootfrr
|
||||||
|
|
||||||
|
# reset interfaces
|
||||||
|
% for ifc, _, _ , _ in interfaces:
|
||||||
|
ip link set dev ${ifc.name} down
|
||||||
|
sleep 1
|
||||||
|
ip link set dev ${ifc.name} up
|
||||||
|
% endfor
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import font, ttk
|
||||||
|
|
||||||
from core.gui import appconfig, themes
|
from core.gui import appconfig, themes
|
||||||
from core.gui.coreclient import CoreClient
|
from core.gui.coreclient import CoreClient
|
||||||
|
@ -29,8 +29,15 @@ class Application(tk.Frame):
|
||||||
self.statusbar = None
|
self.statusbar = None
|
||||||
self.validation = None
|
self.validation = None
|
||||||
|
|
||||||
|
# fonts
|
||||||
|
self.fonts_size = None
|
||||||
|
self.icon_text_font = None
|
||||||
|
self.edge_font = None
|
||||||
|
|
||||||
# setup
|
# setup
|
||||||
self.guiconfig = appconfig.read()
|
self.guiconfig = appconfig.read()
|
||||||
|
self.app_scale = self.guiconfig["scale"]
|
||||||
|
self.setup_scaling()
|
||||||
self.style = ttk.Style()
|
self.style = ttk.Style()
|
||||||
self.setup_theme()
|
self.setup_theme()
|
||||||
self.core = CoreClient(self, proxy)
|
self.core = CoreClient(self, proxy)
|
||||||
|
@ -38,6 +45,14 @@ class Application(tk.Frame):
|
||||||
self.draw()
|
self.draw()
|
||||||
self.core.set_up()
|
self.core.set_up()
|
||||||
|
|
||||||
|
def setup_scaling(self):
|
||||||
|
self.fonts_size = {name: font.nametofont(name)["size"] for name in font.names()}
|
||||||
|
themes.scale_fonts(self.fonts_size, self.app_scale)
|
||||||
|
self.icon_text_font = font.Font(
|
||||||
|
family="TkIconFont", size=int(12 * self.app_scale)
|
||||||
|
)
|
||||||
|
self.edge_font = font.Font(family="TkDefaultFont", size=int(8 * self.app_scale))
|
||||||
|
|
||||||
def setup_theme(self):
|
def setup_theme(self):
|
||||||
themes.load(self.style)
|
themes.load(self.style)
|
||||||
self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
|
self.master.bind_class("Menu", "<<ThemeChanged>>", themes.theme_change_menu)
|
||||||
|
@ -56,9 +71,11 @@ class Application(tk.Frame):
|
||||||
def center(self):
|
def center(self):
|
||||||
screen_width = self.master.winfo_screenwidth()
|
screen_width = self.master.winfo_screenwidth()
|
||||||
screen_height = self.master.winfo_screenheight()
|
screen_height = self.master.winfo_screenheight()
|
||||||
x = int((screen_width / 2) - (WIDTH / 2))
|
x = int((screen_width / 2) - (WIDTH * self.app_scale / 2))
|
||||||
y = int((screen_height / 2) - (HEIGHT / 2))
|
y = int((screen_height / 2) - (HEIGHT * self.app_scale / 2))
|
||||||
self.master.geometry(f"{WIDTH}x{HEIGHT}+{x}+{y}")
|
self.master.geometry(
|
||||||
|
f"{int(WIDTH * self.app_scale)}x{int(HEIGHT * self.app_scale)}+{x}+{y}"
|
||||||
|
)
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
self.master.option_add("*tearOff", tk.FALSE)
|
self.master.option_add("*tearOff", tk.FALSE)
|
||||||
|
|
|
@ -96,6 +96,7 @@ def check_directory():
|
||||||
"nodes": [],
|
"nodes": [],
|
||||||
"recentfiles": [],
|
"recentfiles": [],
|
||||||
"observers": [{"name": "hello", "cmd": "echo hello"}],
|
"observers": [{"name": "hello", "cmd": "echo hello"}],
|
||||||
|
"scale": 1.0,
|
||||||
}
|
}
|
||||||
save(config)
|
save(config)
|
||||||
|
|
||||||
|
|
|
@ -794,7 +794,7 @@ class CoreClient:
|
||||||
image=image,
|
image=image,
|
||||||
emane=emane,
|
emane=emane,
|
||||||
)
|
)
|
||||||
if NodeUtils.is_custom(model):
|
if NodeUtils.is_custom(node_type, model):
|
||||||
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
|
services = NodeUtils.get_custom_node_services(self.app.guiconfig, model)
|
||||||
node.services[:] = services
|
node.services[:] = services
|
||||||
logging.info(
|
logging.info(
|
||||||
|
|
|
@ -100,17 +100,17 @@ class MobilityPlayerDialog(Dialog):
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
frame.columnconfigure(i, weight=1)
|
frame.columnconfigure(i, weight=1)
|
||||||
|
|
||||||
image = Images.get(ImageEnum.START, width=ICON_SIZE)
|
image = Images.get(ImageEnum.START, width=int(ICON_SIZE * self.app.app_scale))
|
||||||
self.play_button = ttk.Button(frame, image=image, command=self.click_play)
|
self.play_button = ttk.Button(frame, image=image, command=self.click_play)
|
||||||
self.play_button.image = image
|
self.play_button.image = image
|
||||||
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
self.play_button.grid(row=0, column=0, sticky="ew", padx=PADX)
|
||||||
|
|
||||||
image = Images.get(ImageEnum.PAUSE, width=ICON_SIZE)
|
image = Images.get(ImageEnum.PAUSE, width=int(ICON_SIZE * self.app.app_scale))
|
||||||
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
|
self.pause_button = ttk.Button(frame, image=image, command=self.click_pause)
|
||||||
self.pause_button.image = image
|
self.pause_button.image = image
|
||||||
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
self.pause_button.grid(row=0, column=1, sticky="ew", padx=PADX)
|
||||||
|
|
||||||
image = Images.get(ImageEnum.STOP, width=ICON_SIZE)
|
image = Images.get(ImageEnum.STOP, width=int(ICON_SIZE * self.app.app_scale))
|
||||||
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
|
self.stop_button = ttk.Button(frame, image=image, command=self.click_stop)
|
||||||
self.stop_button.image = image
|
self.stop_button.image = image
|
||||||
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
self.stop_button.grid(row=0, column=2, sticky="ew", padx=PADX)
|
||||||
|
|
|
@ -38,7 +38,7 @@ class NodeServiceDialog(Dialog):
|
||||||
if len(services) == 0:
|
if len(services) == 0:
|
||||||
# not custom node type and node's services haven't been modified before
|
# not custom node type and node's services haven't been modified before
|
||||||
if not NodeUtils.is_custom(
|
if not NodeUtils.is_custom(
|
||||||
canvas_node.core_node.model
|
canvas_node.core_node.type, canvas_node.core_node.model
|
||||||
) and not self.app.core.service_been_modified(self.node_id):
|
) and not self.app.core.service_been_modified(self.node_id):
|
||||||
services = set(self.app.core.default_services[model])
|
services = set(self.app.core.default_services[model])
|
||||||
# services of default type nodes were modified to be empty
|
# services of default type nodes were modified to be empty
|
||||||
|
|
|
@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from core.gui import appconfig
|
from core.gui import appconfig
|
||||||
from core.gui.dialogs.dialog import Dialog
|
from core.gui.dialogs.dialog import Dialog
|
||||||
from core.gui.themes import FRAME_PAD, PADX, PADY
|
from core.gui.themes import FRAME_PAD, PADX, PADY, scale_fonts
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
||||||
class PreferencesDialog(Dialog):
|
class PreferencesDialog(Dialog):
|
||||||
def __init__(self, master: "Application", app: "Application"):
|
def __init__(self, master: "Application", app: "Application"):
|
||||||
super().__init__(master, app, "Preferences", modal=True)
|
super().__init__(master, app, "Preferences", modal=True)
|
||||||
|
self.gui_scale = tk.DoubleVar(value=self.app.app_scale)
|
||||||
preferences = self.app.guiconfig["preferences"]
|
preferences = self.app.guiconfig["preferences"]
|
||||||
self.editor = tk.StringVar(value=preferences["editor"])
|
self.editor = tk.StringVar(value=preferences["editor"])
|
||||||
self.theme = tk.StringVar(value=preferences["theme"])
|
self.theme = tk.StringVar(value=preferences["theme"])
|
||||||
|
@ -64,6 +65,26 @@ class PreferencesDialog(Dialog):
|
||||||
entry = ttk.Entry(frame, textvariable=self.gui3d)
|
entry = ttk.Entry(frame, textvariable=self.gui3d)
|
||||||
entry.grid(row=3, column=1, sticky="ew")
|
entry.grid(row=3, column=1, sticky="ew")
|
||||||
|
|
||||||
|
label = ttk.Label(frame, text="Scaling")
|
||||||
|
label.grid(row=4, column=0, pady=PADY, padx=PADX, sticky="w")
|
||||||
|
|
||||||
|
scale_frame = ttk.Frame(frame)
|
||||||
|
scale_frame.grid(row=4, column=1, sticky="ew")
|
||||||
|
scale_frame.columnconfigure(0, weight=1)
|
||||||
|
scale = ttk.Scale(
|
||||||
|
scale_frame,
|
||||||
|
from_=0.5,
|
||||||
|
to=5,
|
||||||
|
value=1,
|
||||||
|
orient=tk.HORIZONTAL,
|
||||||
|
variable=self.gui_scale,
|
||||||
|
)
|
||||||
|
scale.grid(row=0, column=0, sticky="ew")
|
||||||
|
entry = ttk.Entry(
|
||||||
|
scale_frame, textvariable=self.gui_scale, width=4, state="disabled"
|
||||||
|
)
|
||||||
|
entry.grid(row=0, column=1)
|
||||||
|
|
||||||
def draw_buttons(self):
|
def draw_buttons(self):
|
||||||
frame = ttk.Frame(self.top)
|
frame = ttk.Frame(self.top)
|
||||||
frame.grid(sticky="ew")
|
frame.grid(sticky="ew")
|
||||||
|
@ -87,5 +108,27 @@ class PreferencesDialog(Dialog):
|
||||||
preferences["editor"] = self.editor.get()
|
preferences["editor"] = self.editor.get()
|
||||||
preferences["gui3d"] = self.gui3d.get()
|
preferences["gui3d"] = self.gui3d.get()
|
||||||
preferences["theme"] = self.theme.get()
|
preferences["theme"] = self.theme.get()
|
||||||
|
self.gui_scale.set(round(self.gui_scale.get(), 2))
|
||||||
|
app_scale = self.gui_scale.get()
|
||||||
|
self.app.guiconfig["scale"] = app_scale
|
||||||
|
|
||||||
self.app.save_config()
|
self.app.save_config()
|
||||||
|
self.scale_adjust()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
|
def scale_adjust(self):
|
||||||
|
app_scale = self.gui_scale.get()
|
||||||
|
self.app.app_scale = app_scale
|
||||||
|
self.app.master.tk.call("tk", "scaling", app_scale)
|
||||||
|
|
||||||
|
# scale fonts
|
||||||
|
scale_fonts(self.app.fonts_size, app_scale)
|
||||||
|
self.app.icon_text_font.config(size=int(12 * app_scale))
|
||||||
|
self.app.edge_font.config(size=int(8 * app_scale))
|
||||||
|
|
||||||
|
# scale application window
|
||||||
|
self.app.center()
|
||||||
|
|
||||||
|
# scale toolbar and canvas items
|
||||||
|
self.app.toolbar.scale()
|
||||||
|
self.app.canvas.scale_graph()
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter.font import Font
|
|
||||||
from typing import TYPE_CHECKING, Any, Tuple
|
from typing import TYPE_CHECKING, Any, Tuple
|
||||||
|
|
||||||
from core.gui import themes
|
from core.gui import themes
|
||||||
|
@ -14,6 +13,7 @@ if TYPE_CHECKING:
|
||||||
TEXT_DISTANCE = 0.30
|
TEXT_DISTANCE = 0.30
|
||||||
EDGE_WIDTH = 3
|
EDGE_WIDTH = 3
|
||||||
EDGE_COLOR = "#ff0000"
|
EDGE_COLOR = "#ff0000"
|
||||||
|
WIRELESS_WIDTH = 1.5
|
||||||
WIRELESS_COLOR = "#009933"
|
WIRELESS_COLOR = "#009933"
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,7 +32,10 @@ class CanvasWirelessEdge:
|
||||||
self.dst = dst
|
self.dst = dst
|
||||||
self.canvas = canvas
|
self.canvas = canvas
|
||||||
self.id = self.canvas.create_line(
|
self.id = self.canvas.create_line(
|
||||||
*position, tags=tags.WIRELESS_EDGE, width=EDGE_WIDTH, fill="#009933"
|
*position,
|
||||||
|
tags=tags.WIRELESS_EDGE,
|
||||||
|
width=WIRELESS_WIDTH * self.canvas.app.app_scale,
|
||||||
|
fill=WIRELESS_COLOR,
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
|
@ -62,13 +65,18 @@ class CanvasEdge:
|
||||||
self.dst_interface = None
|
self.dst_interface = None
|
||||||
self.canvas = canvas
|
self.canvas = canvas
|
||||||
self.id = self.canvas.create_line(
|
self.id = self.canvas.create_line(
|
||||||
x1, y1, x2, y2, tags=tags.EDGE, width=EDGE_WIDTH, fill=EDGE_COLOR
|
x1,
|
||||||
|
y1,
|
||||||
|
x2,
|
||||||
|
y2,
|
||||||
|
tags=tags.EDGE,
|
||||||
|
width=EDGE_WIDTH * self.canvas.app.app_scale,
|
||||||
|
fill=EDGE_COLOR,
|
||||||
)
|
)
|
||||||
self.text_src = None
|
self.text_src = None
|
||||||
self.text_dst = None
|
self.text_dst = None
|
||||||
self.text_middle = None
|
self.text_middle = None
|
||||||
self.token = None
|
self.token = None
|
||||||
self.font = Font(size=8)
|
|
||||||
self.link = None
|
self.link = None
|
||||||
self.asymmetric_link = None
|
self.asymmetric_link = None
|
||||||
self.throughput = None
|
self.throughput = None
|
||||||
|
@ -118,7 +126,7 @@ class CanvasEdge:
|
||||||
y1,
|
y1,
|
||||||
text=label_one,
|
text=label_one,
|
||||||
justify=tk.CENTER,
|
justify=tk.CENTER,
|
||||||
font=self.font,
|
font=self.canvas.app.edge_font,
|
||||||
tags=tags.LINK_INFO,
|
tags=tags.LINK_INFO,
|
||||||
)
|
)
|
||||||
self.text_dst = self.canvas.create_text(
|
self.text_dst = self.canvas.create_text(
|
||||||
|
@ -126,7 +134,7 @@ class CanvasEdge:
|
||||||
y2,
|
y2,
|
||||||
text=label_two,
|
text=label_two,
|
||||||
justify=tk.CENTER,
|
justify=tk.CENTER,
|
||||||
font=self.font,
|
font=self.canvas.app.edge_font,
|
||||||
tags=tags.LINK_INFO,
|
tags=tags.LINK_INFO,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -147,7 +155,7 @@ class CanvasEdge:
|
||||||
if self.text_middle is None:
|
if self.text_middle is None:
|
||||||
x, y = self.get_midpoint()
|
x, y = self.get_midpoint()
|
||||||
self.text_middle = self.canvas.create_text(
|
self.text_middle = self.canvas.create_text(
|
||||||
x, y, tags=tags.THROUGHPUT, font=self.font, text=value
|
x, y, tags=tags.THROUGHPUT, font=self.canvas.app.edge_font, text=value
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.canvas.itemconfig(self.text_middle, text=value)
|
self.canvas.itemconfig(self.text_middle, text=value)
|
||||||
|
|
|
@ -7,12 +7,12 @@ from PIL import Image, ImageTk
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
from core.gui.dialogs.shapemod import ShapeDialog
|
from core.gui.dialogs.shapemod import ShapeDialog
|
||||||
from core.gui.graph import tags
|
from core.gui.graph import tags
|
||||||
from core.gui.graph.edges import CanvasEdge, CanvasWirelessEdge
|
from core.gui.graph.edges import EDGE_WIDTH, CanvasEdge, CanvasWirelessEdge
|
||||||
from core.gui.graph.enums import GraphMode, ScaleOption
|
from core.gui.graph.enums import GraphMode, ScaleOption
|
||||||
from core.gui.graph.node import CanvasNode
|
from core.gui.graph.node import CanvasNode
|
||||||
from core.gui.graph.shape import Shape
|
from core.gui.graph.shape import Shape
|
||||||
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
|
from core.gui.graph.shapeutils import ShapeType, is_draw_shape, is_marker
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images, TypeToImage
|
||||||
from core.gui.nodeutils import EdgeUtils, NodeUtils
|
from core.gui.nodeutils import EdgeUtils, NodeUtils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -224,10 +224,14 @@ class CanvasGraph(tk.Canvas):
|
||||||
# peer to peer node is not drawn on the GUI
|
# peer to peer node is not drawn on the GUI
|
||||||
if NodeUtils.is_ignore_node(core_node.type):
|
if NodeUtils.is_ignore_node(core_node.type):
|
||||||
continue
|
continue
|
||||||
image = NodeUtils.node_image(core_node, self.app.guiconfig)
|
image = NodeUtils.node_image(
|
||||||
|
core_node, self.app.guiconfig, self.app.app_scale
|
||||||
|
)
|
||||||
# if the gui can't find node's image, default to the "edit-node" image
|
# if the gui can't find node's image, default to the "edit-node" image
|
||||||
if not image:
|
if not image:
|
||||||
image = Images.get(ImageEnum.EDITNODE, ICON_SIZE)
|
image = Images.get(
|
||||||
|
ImageEnum.EDITNODE, int(ICON_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
x = core_node.position.x
|
x = core_node.position.x
|
||||||
y = core_node.position.y
|
y = core_node.position.y
|
||||||
node = CanvasNode(self.master, x, y, core_node, image)
|
node = CanvasNode(self.master, x, y, core_node, image)
|
||||||
|
@ -667,6 +671,14 @@ class CanvasGraph(tk.Canvas):
|
||||||
core_node = self.core.create_node(
|
core_node = self.core.create_node(
|
||||||
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
|
actual_x, actual_y, self.node_draw.node_type, self.node_draw.model
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
self.node_draw.image = Images.get(
|
||||||
|
self.node_draw.image_enum, int(ICON_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
self.node_draw.image = Images.get_custom(
|
||||||
|
self.node_draw.image_file, int(ICON_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
node = CanvasNode(self.master, x, y, core_node, self.node_draw.image)
|
node = CanvasNode(self.master, x, y, core_node, self.node_draw.image)
|
||||||
self.core.canvas_nodes[core_node.id] = node
|
self.core.canvas_nodes[core_node.id] = node
|
||||||
self.nodes[node.id] = node
|
self.nodes[node.id] = node
|
||||||
|
@ -915,3 +927,28 @@ class CanvasGraph(tk.Canvas):
|
||||||
width=self.itemcget(edge.id, "width"),
|
width=self.itemcget(edge.id, "width"),
|
||||||
fill=self.itemcget(edge.id, "fill"),
|
fill=self.itemcget(edge.id, "fill"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def scale_graph(self):
|
||||||
|
for nid, canvas_node in self.nodes.items():
|
||||||
|
img = None
|
||||||
|
if NodeUtils.is_custom(
|
||||||
|
canvas_node.core_node.type, canvas_node.core_node.model
|
||||||
|
):
|
||||||
|
for custom_node in self.app.guiconfig["nodes"]:
|
||||||
|
if custom_node["name"] == canvas_node.core_node.model:
|
||||||
|
img = Images.get_custom(
|
||||||
|
custom_node["image"], int(ICON_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
image_enum = TypeToImage.get(
|
||||||
|
canvas_node.core_node.type, canvas_node.core_node.model
|
||||||
|
)
|
||||||
|
img = Images.get(image_enum, int(ICON_SIZE * self.app.app_scale))
|
||||||
|
|
||||||
|
self.itemconfig(nid, image=img)
|
||||||
|
canvas_node.image = img
|
||||||
|
canvas_node.scale_text()
|
||||||
|
canvas_node.scale_antennas()
|
||||||
|
|
||||||
|
for edge_id in self.find_withtag(tags.EDGE):
|
||||||
|
self.itemconfig(edge_id, width=int(EDGE_WIDTH * self.app.app_scale))
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import font
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
@ -17,7 +16,8 @@ from core.gui.dialogs.wlanconfig import WlanConfigDialog
|
||||||
from core.gui.errors import show_grpc_error
|
from core.gui.errors import show_grpc_error
|
||||||
from core.gui.graph import tags
|
from core.gui.graph import tags
|
||||||
from core.gui.graph.tooltip import CanvasTooltip
|
from core.gui.graph.tooltip import CanvasTooltip
|
||||||
from core.gui.nodeutils import EdgeUtils, NodeUtils
|
from core.gui.images import ImageEnum, Images
|
||||||
|
from core.gui.nodeutils import ANTENNA_SIZE, EdgeUtils, NodeUtils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.gui.app import Application
|
from core.gui.app import Application
|
||||||
|
@ -42,21 +42,21 @@ class CanvasNode:
|
||||||
self.id = self.canvas.create_image(
|
self.id = self.canvas.create_image(
|
||||||
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
|
x, y, anchor=tk.CENTER, image=self.image, tags=tags.NODE
|
||||||
)
|
)
|
||||||
text_font = font.Font(family="TkIconFont", size=12)
|
|
||||||
label_y = self._get_label_y()
|
label_y = self._get_label_y()
|
||||||
self.text_id = self.canvas.create_text(
|
self.text_id = self.canvas.create_text(
|
||||||
x,
|
x,
|
||||||
label_y,
|
label_y,
|
||||||
text=self.core_node.name,
|
text=self.core_node.name,
|
||||||
tags=tags.NODE_NAME,
|
tags=tags.NODE_NAME,
|
||||||
font=text_font,
|
font=self.app.icon_text_font,
|
||||||
fill="#0000CD",
|
fill="#0000CD",
|
||||||
)
|
)
|
||||||
self.tooltip = CanvasTooltip(self.canvas)
|
self.tooltip = CanvasTooltip(self.canvas)
|
||||||
self.edges = set()
|
self.edges = set()
|
||||||
self.interfaces = []
|
self.interfaces = []
|
||||||
self.wireless_edges = set()
|
self.wireless_edges = set()
|
||||||
self.antennae = []
|
self.antennas = []
|
||||||
|
self.antenna_images = {}
|
||||||
self.setup_bindings()
|
self.setup_bindings()
|
||||||
|
|
||||||
def setup_bindings(self):
|
def setup_bindings(self):
|
||||||
|
@ -95,8 +95,13 @@ class CanvasNode:
|
||||||
if other == self.id:
|
if other == self.id:
|
||||||
other = token[1]
|
other = token[1]
|
||||||
self.canvas.nodes[other].wireless_edges.discard(wireless_edge)
|
self.canvas.nodes[other].wireless_edges.discard(wireless_edge)
|
||||||
wlan_edge = self.canvas.wireless_edges.pop(token, None)
|
try:
|
||||||
self.canvas.delete(wlan_edge.id)
|
wlan_edge = self.canvas.wireless_edges.pop(token)
|
||||||
|
self.canvas.delete(wlan_edge.id)
|
||||||
|
except KeyError:
|
||||||
|
logging.error(
|
||||||
|
"wireless link not found, potentially multiple wireless link issue"
|
||||||
|
)
|
||||||
self.delete_antennas()
|
self.delete_antennas()
|
||||||
|
|
||||||
self.wireless_edges.clear()
|
self.wireless_edges.clear()
|
||||||
|
@ -106,33 +111,37 @@ class CanvasNode:
|
||||||
|
|
||||||
def add_antenna(self):
|
def add_antenna(self):
|
||||||
x, y = self.canvas.coords(self.id)
|
x, y = self.canvas.coords(self.id)
|
||||||
offset = len(self.antennae) * 8
|
offset = len(self.antennas) * 8 * self.app.app_scale
|
||||||
|
img = Images.get(ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale))
|
||||||
antenna_id = self.canvas.create_image(
|
antenna_id = self.canvas.create_image(
|
||||||
x - 16 + offset,
|
x - 16 + offset,
|
||||||
y - 23,
|
y - int(23 * self.app.app_scale),
|
||||||
anchor=tk.CENTER,
|
anchor=tk.CENTER,
|
||||||
image=NodeUtils.ANTENNA_ICON,
|
image=img,
|
||||||
tags=tags.ANTENNA,
|
tags=tags.ANTENNA,
|
||||||
)
|
)
|
||||||
self.antennae.append(antenna_id)
|
self.antennas.append(antenna_id)
|
||||||
|
self.antenna_images[antenna_id] = img
|
||||||
|
|
||||||
def delete_antenna(self):
|
def delete_antenna(self):
|
||||||
"""
|
"""
|
||||||
delete one antenna
|
delete one antenna
|
||||||
"""
|
"""
|
||||||
logging.debug("Delete an antenna on %s", self.core_node.name)
|
logging.debug("Delete an antenna on %s", self.core_node.name)
|
||||||
if self.antennae:
|
if self.antennas:
|
||||||
antenna_id = self.antennae.pop()
|
antenna_id = self.antennas.pop()
|
||||||
self.canvas.delete(antenna_id)
|
self.canvas.delete(antenna_id)
|
||||||
|
self.antenna_images.pop(antenna_id, None)
|
||||||
|
|
||||||
def delete_antennas(self):
|
def delete_antennas(self):
|
||||||
"""
|
"""
|
||||||
delete all antennas
|
delete all antennas
|
||||||
"""
|
"""
|
||||||
logging.debug("Remove all antennas for %s", self.core_node.name)
|
logging.debug("Remove all antennas for %s", self.core_node.name)
|
||||||
for antenna_id in self.antennae:
|
for antenna_id in self.antennas:
|
||||||
self.canvas.delete(antenna_id)
|
self.canvas.delete(antenna_id)
|
||||||
self.antennae.clear()
|
self.antennas.clear()
|
||||||
|
self.antenna_images.clear()
|
||||||
|
|
||||||
def redraw(self):
|
def redraw(self):
|
||||||
self.canvas.itemconfig(self.id, image=self.image)
|
self.canvas.itemconfig(self.id, image=self.image)
|
||||||
|
@ -142,6 +151,12 @@ class CanvasNode:
|
||||||
image_box = self.canvas.bbox(self.id)
|
image_box = self.canvas.bbox(self.id)
|
||||||
return image_box[3] + NODE_TEXT_OFFSET
|
return image_box[3] + NODE_TEXT_OFFSET
|
||||||
|
|
||||||
|
def scale_text(self):
|
||||||
|
text_bound = self.canvas.bbox(self.text_id)
|
||||||
|
prev_y = (text_bound[3] + text_bound[1]) / 2
|
||||||
|
new_y = self._get_label_y()
|
||||||
|
self.canvas.move(self.text_id, 0, new_y - prev_y)
|
||||||
|
|
||||||
def move(self, x: int, y: int):
|
def move(self, x: int, y: int):
|
||||||
x, y = self.canvas.get_scaled_coords(x, y)
|
x, y = self.canvas.get_scaled_coords(x, y)
|
||||||
current_x, current_y = self.canvas.coords(self.id)
|
current_x, current_y = self.canvas.coords(self.id)
|
||||||
|
@ -165,7 +180,7 @@ class CanvasNode:
|
||||||
self.canvas.move_selection(self.id, x_offset, y_offset)
|
self.canvas.move_selection(self.id, x_offset, y_offset)
|
||||||
|
|
||||||
# move antennae
|
# move antennae
|
||||||
for antenna_id in self.antennae:
|
for antenna_id in self.antennas:
|
||||||
self.canvas.move(antenna_id, x_offset, y_offset)
|
self.canvas.move(antenna_id, x_offset, y_offset)
|
||||||
|
|
||||||
# move edges
|
# move edges
|
||||||
|
@ -355,3 +370,17 @@ class CanvasNode:
|
||||||
self.canvas.delete(wireless_edge.id)
|
self.canvas.delete(wireless_edge.id)
|
||||||
else:
|
else:
|
||||||
logging.debug("%s is not a wireless edge", token)
|
logging.debug("%s is not a wireless edge", token)
|
||||||
|
|
||||||
|
def scale_antennas(self):
|
||||||
|
for i in range(len(self.antennas)):
|
||||||
|
antenna_id = self.antennas[i]
|
||||||
|
image = Images.get(
|
||||||
|
ImageEnum.ANTENNA, int(ANTENNA_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
|
self.canvas.itemconfig(antenna_id, image=image)
|
||||||
|
self.antenna_images[antenna_id] = image
|
||||||
|
node_x, node_y = self.canvas.coords(self.id)
|
||||||
|
x, y = self.canvas.coords(antenna_id)
|
||||||
|
dx = node_x - 16 + (i * 8 * self.app.app_scale) - x
|
||||||
|
dy = node_y - int(23 * self.app.app_scale) - y
|
||||||
|
self.canvas.move(antenna_id, dx, dy)
|
||||||
|
|
|
@ -3,6 +3,7 @@ from tkinter import messagebox
|
||||||
|
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
from core.api.grpc import core_pb2
|
||||||
from core.gui.appconfig import LOCAL_ICONS_PATH
|
from core.gui.appconfig import LOCAL_ICONS_PATH
|
||||||
|
|
||||||
|
|
||||||
|
@ -90,3 +91,23 @@ class ImageEnum(Enum):
|
||||||
SHUTDOWN = "shutdown"
|
SHUTDOWN = "shutdown"
|
||||||
CANCEL = "cancel"
|
CANCEL = "cancel"
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
|
|
||||||
|
|
||||||
|
class TypeToImage:
|
||||||
|
type_to_image = {
|
||||||
|
(core_pb2.NodeType.DEFAULT, "router"): ImageEnum.ROUTER,
|
||||||
|
(core_pb2.NodeType.DEFAULT, "PC"): ImageEnum.PC,
|
||||||
|
(core_pb2.NodeType.DEFAULT, "host"): ImageEnum.HOST,
|
||||||
|
(core_pb2.NodeType.DEFAULT, "mdr"): ImageEnum.MDR,
|
||||||
|
(core_pb2.NodeType.DEFAULT, "prouter"): ImageEnum.PROUTER,
|
||||||
|
(core_pb2.NodeType.HUB, ""): ImageEnum.HUB,
|
||||||
|
(core_pb2.NodeType.SWITCH, ""): ImageEnum.SWITCH,
|
||||||
|
(core_pb2.NodeType.WIRELESS_LAN, ""): ImageEnum.WLAN,
|
||||||
|
(core_pb2.NodeType.EMANE, ""): ImageEnum.EMANE,
|
||||||
|
(core_pb2.NodeType.RJ45, ""): ImageEnum.RJ45,
|
||||||
|
(core_pb2.NodeType.TUNNEL, ""): ImageEnum.TUNNEL,
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, node_type, model):
|
||||||
|
return cls.type_to_image.get((node_type, model), None)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import logging
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
|
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
from core.api.grpc.core_pb2 import NodeType
|
from core.api.grpc.core_pb2 import NodeType
|
||||||
from core.gui.images import ImageEnum, Images
|
from core.gui.images import ImageEnum, Images, TypeToImage
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
|
@ -100,32 +100,35 @@ class NodeUtils:
|
||||||
node_type: NodeType,
|
node_type: NodeType,
|
||||||
model: str,
|
model: str,
|
||||||
gui_config: Dict[str, List[Dict[str, str]]],
|
gui_config: Dict[str, List[Dict[str, str]]],
|
||||||
|
scale=1.0,
|
||||||
) -> "ImageTk.PhotoImage":
|
) -> "ImageTk.PhotoImage":
|
||||||
if model == "":
|
|
||||||
model = None
|
image_enum = TypeToImage.get(node_type, model)
|
||||||
try:
|
if image_enum:
|
||||||
image = cls.NODE_ICONS[(node_type, model)]
|
return Images.get(image_enum, int(ICON_SIZE * scale))
|
||||||
return image
|
else:
|
||||||
except KeyError:
|
|
||||||
image_stem = cls.get_image_file(gui_config, model)
|
image_stem = cls.get_image_file(gui_config, model)
|
||||||
if image_stem:
|
if image_stem:
|
||||||
return Images.get_with_image_file(image_stem, ICON_SIZE)
|
return Images.get_with_image_file(image_stem, int(ICON_SIZE * scale))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def node_image(
|
def node_image(
|
||||||
cls, core_node: "core_pb2.Node", gui_config: Dict[str, List[Dict[str, str]]]
|
cls,
|
||||||
|
core_node: "core_pb2.Node",
|
||||||
|
gui_config: Dict[str, List[Dict[str, str]]],
|
||||||
|
scale=1.0,
|
||||||
) -> "ImageTk.PhotoImage":
|
) -> "ImageTk.PhotoImage":
|
||||||
image = cls.node_icon(core_node.type, core_node.model, gui_config)
|
image = cls.node_icon(core_node.type, core_node.model, gui_config, scale)
|
||||||
if core_node.icon:
|
if core_node.icon:
|
||||||
try:
|
try:
|
||||||
image = Images.create(core_node.icon, ICON_SIZE)
|
image = Images.create(core_node.icon, int(ICON_SIZE * scale))
|
||||||
except OSError:
|
except OSError:
|
||||||
logging.error("invalid icon: %s", core_node.icon)
|
logging.error("invalid icon: %s", core_node.icon)
|
||||||
return image
|
return image
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_custom(cls, model: str) -> bool:
|
def is_custom(cls, node_type: NodeType, model: str) -> bool:
|
||||||
return model not in cls.NODE_MODELS
|
return node_type == NodeType.DEFAULT and model not in cls.NODE_MODELS
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_custom_node_services(
|
def get_custom_node_services(
|
||||||
|
|
|
@ -29,7 +29,7 @@ class StatusBar(ttk.Frame):
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
self.columnconfigure(1, weight=7)
|
self.columnconfigure(1, weight=5)
|
||||||
self.columnconfigure(2, weight=1)
|
self.columnconfigure(2, weight=1)
|
||||||
self.columnconfigure(3, weight=1)
|
self.columnconfigure(3, weight=1)
|
||||||
self.columnconfigure(4, weight=1)
|
self.columnconfigure(4, weight=1)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import font, ttk
|
||||||
|
|
||||||
THEME_DARK = "black"
|
THEME_DARK = "black"
|
||||||
PADX = (0, 5)
|
PADX = (0, 5)
|
||||||
|
@ -176,25 +176,35 @@ def style_listbox(widget: tk.Widget):
|
||||||
|
|
||||||
def theme_change(event: tk.Event):
|
def theme_change(event: tk.Event):
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
style.configure(Styles.picker_button, font=("TkDefaultFont", 8, "normal"))
|
style.configure(Styles.picker_button, font="TkSmallCaptionFont")
|
||||||
style.configure(
|
style.configure(
|
||||||
Styles.green_alert,
|
Styles.green_alert,
|
||||||
background="green",
|
background="green",
|
||||||
padding=0,
|
padding=0,
|
||||||
relief=tk.NONE,
|
relief=tk.NONE,
|
||||||
font=("TkDefaultFont", 8, "normal"),
|
font="TkSmallCaptionFont",
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
Styles.yellow_alert,
|
Styles.yellow_alert,
|
||||||
background="yellow",
|
background="yellow",
|
||||||
padding=0,
|
padding=0,
|
||||||
relief=tk.NONE,
|
relief=tk.NONE,
|
||||||
font=("TkDefaultFont", 8, "normal"),
|
font="TkSmallCaptionFont",
|
||||||
)
|
)
|
||||||
style.configure(
|
style.configure(
|
||||||
Styles.red_alert,
|
Styles.red_alert,
|
||||||
background="red",
|
background="red",
|
||||||
padding=0,
|
padding=0,
|
||||||
relief=tk.NONE,
|
relief=tk.NONE,
|
||||||
font=("TkDefaultFont", 8, "normal"),
|
font="TkSmallCaptionFont",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def scale_fonts(fonts_size, scale):
|
||||||
|
for name in font.names():
|
||||||
|
f = font.nametofont(name)
|
||||||
|
if name in fonts_size:
|
||||||
|
if name == "TkSmallCaptionFont":
|
||||||
|
f.config(size=int(fonts_size[name] * scale * 8 / 9))
|
||||||
|
else:
|
||||||
|
f.config(size=int(fonts_size[name] * scale))
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
from enum import Enum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from tkinter.font import Font
|
|
||||||
from typing import TYPE_CHECKING, Callable
|
from typing import TYPE_CHECKING, Callable
|
||||||
|
|
||||||
from core.api.grpc import core_pb2
|
from core.api.grpc import core_pb2
|
||||||
|
@ -25,6 +25,12 @@ TOOLBAR_SIZE = 32
|
||||||
PICKER_SIZE = 24
|
PICKER_SIZE = 24
|
||||||
|
|
||||||
|
|
||||||
|
class NodeTypeEnum(Enum):
|
||||||
|
NODE = 0
|
||||||
|
NETWORK = 1
|
||||||
|
OTHER = 2
|
||||||
|
|
||||||
|
|
||||||
def icon(image_enum, width=TOOLBAR_SIZE):
|
def icon(image_enum, width=TOOLBAR_SIZE):
|
||||||
return Images.get(image_enum, width)
|
return Images.get(image_enum, width)
|
||||||
|
|
||||||
|
@ -43,10 +49,8 @@ class Toolbar(ttk.Frame):
|
||||||
self.master = app.master
|
self.master = app.master
|
||||||
self.time = None
|
self.time = None
|
||||||
|
|
||||||
# picker data
|
|
||||||
self.picker_font = Font(size=8)
|
|
||||||
|
|
||||||
# design buttons
|
# design buttons
|
||||||
|
self.play_button = None
|
||||||
self.select_button = None
|
self.select_button = None
|
||||||
self.link_button = None
|
self.link_button = None
|
||||||
self.node_button = None
|
self.node_button = None
|
||||||
|
@ -71,9 +75,18 @@ class Toolbar(ttk.Frame):
|
||||||
# dialog
|
# dialog
|
||||||
self.marker_tool = None
|
self.marker_tool = None
|
||||||
|
|
||||||
|
# these variables help keep track of what images being drawn so that scaling is possible
|
||||||
|
# since ImageTk.PhotoImage does not have resize method
|
||||||
|
self.node_enum = None
|
||||||
|
self.network_enum = None
|
||||||
|
self.annotation_enum = None
|
||||||
|
|
||||||
# draw components
|
# draw components
|
||||||
self.draw()
|
self.draw()
|
||||||
|
|
||||||
|
def get_icon(self, image_enum, width=TOOLBAR_SIZE):
|
||||||
|
return Images.get(image_enum, int(width * self.app.app_scale))
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
self.columnconfigure(0, weight=1)
|
self.columnconfigure(0, weight=1)
|
||||||
self.rowconfigure(0, weight=1)
|
self.rowconfigure(0, weight=1)
|
||||||
|
@ -85,20 +98,23 @@ class Toolbar(ttk.Frame):
|
||||||
self.design_frame = ttk.Frame(self)
|
self.design_frame = ttk.Frame(self)
|
||||||
self.design_frame.grid(row=0, column=0, sticky="nsew")
|
self.design_frame.grid(row=0, column=0, sticky="nsew")
|
||||||
self.design_frame.columnconfigure(0, weight=1)
|
self.design_frame.columnconfigure(0, weight=1)
|
||||||
self.create_button(
|
self.play_button = self.create_button(
|
||||||
self.design_frame,
|
self.design_frame,
|
||||||
icon(ImageEnum.START),
|
self.get_icon(ImageEnum.START),
|
||||||
self.click_start,
|
self.click_start,
|
||||||
"start the session",
|
"start the session",
|
||||||
)
|
)
|
||||||
self.select_button = self.create_button(
|
self.select_button = self.create_button(
|
||||||
self.design_frame,
|
self.design_frame,
|
||||||
icon(ImageEnum.SELECT),
|
self.get_icon(ImageEnum.SELECT),
|
||||||
self.click_selection,
|
self.click_selection,
|
||||||
"selection tool",
|
"selection tool",
|
||||||
)
|
)
|
||||||
self.link_button = self.create_button(
|
self.link_button = self.create_button(
|
||||||
self.design_frame, icon(ImageEnum.LINK), self.click_link, "link tool"
|
self.design_frame,
|
||||||
|
self.get_icon(ImageEnum.LINK),
|
||||||
|
self.click_link,
|
||||||
|
"link tool",
|
||||||
)
|
)
|
||||||
self.create_node_button()
|
self.create_node_button()
|
||||||
self.create_network_button()
|
self.create_network_button()
|
||||||
|
@ -130,18 +146,21 @@ class Toolbar(ttk.Frame):
|
||||||
|
|
||||||
self.stop_button = self.create_button(
|
self.stop_button = self.create_button(
|
||||||
self.runtime_frame,
|
self.runtime_frame,
|
||||||
icon(ImageEnum.STOP),
|
self.get_icon(ImageEnum.STOP),
|
||||||
self.click_stop,
|
self.click_stop,
|
||||||
"stop the session",
|
"stop the session",
|
||||||
)
|
)
|
||||||
self.runtime_select_button = self.create_button(
|
self.runtime_select_button = self.create_button(
|
||||||
self.runtime_frame,
|
self.runtime_frame,
|
||||||
icon(ImageEnum.SELECT),
|
self.get_icon(ImageEnum.SELECT),
|
||||||
self.click_runtime_selection,
|
self.click_runtime_selection,
|
||||||
"selection tool",
|
"selection tool",
|
||||||
)
|
)
|
||||||
self.plot_button = self.create_button(
|
self.plot_button = self.create_button(
|
||||||
self.runtime_frame, icon(ImageEnum.PLOT), self.click_plot_button, "plot"
|
self.runtime_frame,
|
||||||
|
self.get_icon(ImageEnum.PLOT),
|
||||||
|
self.click_plot_button,
|
||||||
|
"plot",
|
||||||
)
|
)
|
||||||
self.runtime_marker_button = self.create_button(
|
self.runtime_marker_button = self.create_button(
|
||||||
self.runtime_frame,
|
self.runtime_frame,
|
||||||
|
@ -164,23 +183,38 @@ class Toolbar(ttk.Frame):
|
||||||
self.node_picker = ttk.Frame(self.master)
|
self.node_picker = ttk.Frame(self.master)
|
||||||
# draw default nodes
|
# draw default nodes
|
||||||
for node_draw in NodeUtils.NODES:
|
for node_draw in NodeUtils.NODES:
|
||||||
toolbar_image = icon(node_draw.image_enum)
|
toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
|
||||||
image = icon(node_draw.image_enum, PICKER_SIZE)
|
image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
|
||||||
func = partial(
|
func = partial(
|
||||||
self.update_button, self.node_button, toolbar_image, node_draw
|
self.update_button,
|
||||||
|
self.node_button,
|
||||||
|
toolbar_image,
|
||||||
|
node_draw,
|
||||||
|
NodeTypeEnum.NODE,
|
||||||
|
node_draw.image_enum,
|
||||||
)
|
)
|
||||||
self.create_picker_button(image, func, self.node_picker, node_draw.label)
|
self.create_picker_button(image, func, self.node_picker, node_draw.label)
|
||||||
# draw custom nodes
|
# draw custom nodes
|
||||||
for name in sorted(self.app.core.custom_nodes):
|
for name in sorted(self.app.core.custom_nodes):
|
||||||
node_draw = self.app.core.custom_nodes[name]
|
node_draw = self.app.core.custom_nodes[name]
|
||||||
toolbar_image = Images.get_custom(node_draw.image_file, TOOLBAR_SIZE)
|
toolbar_image = Images.get_custom(
|
||||||
image = Images.get_custom(node_draw.image_file, PICKER_SIZE)
|
node_draw.image_file, int(TOOLBAR_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
|
image = Images.get_custom(
|
||||||
|
node_draw.image_file, int(PICKER_SIZE * self.app.app_scale)
|
||||||
|
)
|
||||||
func = partial(
|
func = partial(
|
||||||
self.update_button, self.node_button, toolbar_image, node_draw
|
self.update_button,
|
||||||
|
self.node_button,
|
||||||
|
toolbar_image,
|
||||||
|
node_draw,
|
||||||
|
NodeTypeEnum,
|
||||||
|
node_draw.image_file,
|
||||||
)
|
)
|
||||||
self.create_picker_button(image, func, self.node_picker, name)
|
self.create_picker_button(image, func, self.node_picker, name)
|
||||||
# draw edit node
|
# draw edit node
|
||||||
image = icon(ImageEnum.EDITNODE, PICKER_SIZE)
|
# image = icon(ImageEnum.EDITNODE, PICKER_SIZE)
|
||||||
|
image = self.get_icon(ImageEnum.EDITNODE, PICKER_SIZE)
|
||||||
self.create_picker_button(
|
self.create_picker_button(
|
||||||
image, self.click_edit_node, self.node_picker, "Custom"
|
image, self.click_edit_node, self.node_picker, "Custom"
|
||||||
)
|
)
|
||||||
|
@ -281,13 +315,24 @@ class Toolbar(ttk.Frame):
|
||||||
dialog = CustomNodesDialog(self.app, self.app)
|
dialog = CustomNodesDialog(self.app, self.app)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
|
|
||||||
def update_button(self, button: ttk.Button, image: "ImageTk", node_draw: NodeDraw):
|
def update_button(
|
||||||
|
self,
|
||||||
|
button: ttk.Button,
|
||||||
|
image: "ImageTk",
|
||||||
|
node_draw: NodeDraw,
|
||||||
|
type_enum,
|
||||||
|
image_enum,
|
||||||
|
):
|
||||||
logging.debug("update button(%s): %s", button, node_draw)
|
logging.debug("update button(%s): %s", button, node_draw)
|
||||||
self.hide_pickers()
|
self.hide_pickers()
|
||||||
button.configure(image=image)
|
button.configure(image=image)
|
||||||
button.image = image
|
button.image = image
|
||||||
self.app.canvas.mode = GraphMode.NODE
|
self.app.canvas.mode = GraphMode.NODE
|
||||||
self.app.canvas.node_draw = node_draw
|
self.app.canvas.node_draw = node_draw
|
||||||
|
if type_enum == NodeTypeEnum.NODE:
|
||||||
|
self.node_enum = image_enum
|
||||||
|
elif type_enum == NodeTypeEnum.NETWORK:
|
||||||
|
self.network_enum = image_enum
|
||||||
|
|
||||||
def hide_pickers(self):
|
def hide_pickers(self):
|
||||||
logging.debug("hiding pickers")
|
logging.debug("hiding pickers")
|
||||||
|
@ -305,13 +350,14 @@ class Toolbar(ttk.Frame):
|
||||||
"""
|
"""
|
||||||
Create network layer button
|
Create network layer button
|
||||||
"""
|
"""
|
||||||
image = icon(ImageEnum.ROUTER)
|
image = self.get_icon(ImageEnum.ROUTER, TOOLBAR_SIZE)
|
||||||
self.node_button = ttk.Button(
|
self.node_button = ttk.Button(
|
||||||
self.design_frame, image=image, command=self.draw_node_picker
|
self.design_frame, image=image, command=self.draw_node_picker
|
||||||
)
|
)
|
||||||
self.node_button.image = image
|
self.node_button.image = image
|
||||||
self.node_button.grid(sticky="ew")
|
self.node_button.grid(sticky="ew")
|
||||||
Tooltip(self.node_button, "Network-layer virtual nodes")
|
Tooltip(self.node_button, "Network-layer virtual nodes")
|
||||||
|
self.node_enum = ImageEnum.ROUTER
|
||||||
|
|
||||||
def draw_network_picker(self):
|
def draw_network_picker(self):
|
||||||
"""
|
"""
|
||||||
|
@ -320,12 +366,17 @@ class Toolbar(ttk.Frame):
|
||||||
self.hide_pickers()
|
self.hide_pickers()
|
||||||
self.network_picker = ttk.Frame(self.master)
|
self.network_picker = ttk.Frame(self.master)
|
||||||
for node_draw in NodeUtils.NETWORK_NODES:
|
for node_draw in NodeUtils.NETWORK_NODES:
|
||||||
toolbar_image = icon(node_draw.image_enum)
|
toolbar_image = self.get_icon(node_draw.image_enum, TOOLBAR_SIZE)
|
||||||
image = icon(node_draw.image_enum, PICKER_SIZE)
|
image = self.get_icon(node_draw.image_enum, PICKER_SIZE)
|
||||||
self.create_picker_button(
|
self.create_picker_button(
|
||||||
image,
|
image,
|
||||||
partial(
|
partial(
|
||||||
self.update_button, self.network_button, toolbar_image, node_draw
|
self.update_button,
|
||||||
|
self.network_button,
|
||||||
|
toolbar_image,
|
||||||
|
node_draw,
|
||||||
|
NodeTypeEnum.NETWORK,
|
||||||
|
node_draw.image_enum,
|
||||||
),
|
),
|
||||||
self.network_picker,
|
self.network_picker,
|
||||||
node_draw.label,
|
node_draw.label,
|
||||||
|
@ -340,13 +391,14 @@ class Toolbar(ttk.Frame):
|
||||||
Create link-layer node button and the options that represent different
|
Create link-layer node button and the options that represent different
|
||||||
link-layer node types.
|
link-layer node types.
|
||||||
"""
|
"""
|
||||||
image = icon(ImageEnum.HUB)
|
image = self.get_icon(ImageEnum.HUB, TOOLBAR_SIZE)
|
||||||
self.network_button = ttk.Button(
|
self.network_button = ttk.Button(
|
||||||
self.design_frame, image=image, command=self.draw_network_picker
|
self.design_frame, image=image, command=self.draw_network_picker
|
||||||
)
|
)
|
||||||
self.network_button.image = image
|
self.network_button.image = image
|
||||||
self.network_button.grid(sticky="ew")
|
self.network_button.grid(sticky="ew")
|
||||||
Tooltip(self.network_button, "link-layer nodes")
|
Tooltip(self.network_button, "link-layer nodes")
|
||||||
|
self.network_enum = ImageEnum.HUB
|
||||||
|
|
||||||
def draw_annotation_picker(self):
|
def draw_annotation_picker(self):
|
||||||
"""
|
"""
|
||||||
|
@ -361,11 +413,11 @@ class Toolbar(ttk.Frame):
|
||||||
(ImageEnum.TEXT, ShapeType.TEXT),
|
(ImageEnum.TEXT, ShapeType.TEXT),
|
||||||
]
|
]
|
||||||
for image_enum, shape_type in nodes:
|
for image_enum, shape_type in nodes:
|
||||||
toolbar_image = icon(image_enum)
|
toolbar_image = self.get_icon(image_enum, TOOLBAR_SIZE)
|
||||||
image = icon(image_enum, PICKER_SIZE)
|
image = self.get_icon(image_enum, PICKER_SIZE)
|
||||||
self.create_picker_button(
|
self.create_picker_button(
|
||||||
image,
|
image,
|
||||||
partial(self.update_annotation, toolbar_image, shape_type),
|
partial(self.update_annotation, toolbar_image, shape_type, image_enum),
|
||||||
self.annotation_picker,
|
self.annotation_picker,
|
||||||
shape_type.value,
|
shape_type.value,
|
||||||
)
|
)
|
||||||
|
@ -378,13 +430,14 @@ class Toolbar(ttk.Frame):
|
||||||
"""
|
"""
|
||||||
Create marker button and options that represent different marker types
|
Create marker button and options that represent different marker types
|
||||||
"""
|
"""
|
||||||
image = icon(ImageEnum.MARKER)
|
image = self.get_icon(ImageEnum.MARKER, TOOLBAR_SIZE)
|
||||||
self.annotation_button = ttk.Button(
|
self.annotation_button = ttk.Button(
|
||||||
self.design_frame, image=image, command=self.draw_annotation_picker
|
self.design_frame, image=image, command=self.draw_annotation_picker
|
||||||
)
|
)
|
||||||
self.annotation_button.image = image
|
self.annotation_button.image = image
|
||||||
self.annotation_button.grid(sticky="ew")
|
self.annotation_button.grid(sticky="ew")
|
||||||
Tooltip(self.annotation_button, "background annotation tools")
|
Tooltip(self.annotation_button, "background annotation tools")
|
||||||
|
self.annotation_enum = ImageEnum.MARKER
|
||||||
|
|
||||||
def create_observe_button(self):
|
def create_observe_button(self):
|
||||||
menu_button = ttk.Menubutton(
|
menu_button = ttk.Menubutton(
|
||||||
|
@ -429,13 +482,16 @@ class Toolbar(ttk.Frame):
|
||||||
self.app.statusbar.set_status(message)
|
self.app.statusbar.set_status(message)
|
||||||
self.app.canvas.stopped_session()
|
self.app.canvas.stopped_session()
|
||||||
|
|
||||||
def update_annotation(self, image: "ImageTk.PhotoImage", shape_type: ShapeType):
|
def update_annotation(
|
||||||
|
self, image: "ImageTk.PhotoImage", shape_type: ShapeType, image_enum
|
||||||
|
):
|
||||||
logging.debug("clicked annotation: ")
|
logging.debug("clicked annotation: ")
|
||||||
self.hide_pickers()
|
self.hide_pickers()
|
||||||
self.annotation_button.configure(image=image)
|
self.annotation_button.configure(image=image)
|
||||||
self.annotation_button.image = image
|
self.annotation_button.image = image
|
||||||
self.app.canvas.mode = GraphMode.ANNOTATION
|
self.app.canvas.mode = GraphMode.ANNOTATION
|
||||||
self.app.canvas.annotation_type = shape_type
|
self.app.canvas.annotation_type = shape_type
|
||||||
|
self.annotation_enum = image_enum
|
||||||
if is_marker(shape_type):
|
if is_marker(shape_type):
|
||||||
if self.marker_tool:
|
if self.marker_tool:
|
||||||
self.marker_tool.destroy()
|
self.marker_tool.destroy()
|
||||||
|
@ -460,3 +516,24 @@ class Toolbar(ttk.Frame):
|
||||||
|
|
||||||
def click_two_node_button(self):
|
def click_two_node_button(self):
|
||||||
logging.debug("Click TWONODE button")
|
logging.debug("Click TWONODE button")
|
||||||
|
|
||||||
|
# def scale_button(cls, button, image_enum, scale):
|
||||||
|
def scale_button(self, button, image_enum):
|
||||||
|
image = icon(image_enum, int(TOOLBAR_SIZE * self.app.app_scale))
|
||||||
|
button.config(image=image)
|
||||||
|
button.image = image
|
||||||
|
|
||||||
|
def scale(self):
|
||||||
|
self.scale_button(self.play_button, ImageEnum.START)
|
||||||
|
self.scale_button(self.select_button, ImageEnum.SELECT)
|
||||||
|
self.scale_button(self.link_button, ImageEnum.LINK)
|
||||||
|
self.scale_button(self.node_button, self.node_enum)
|
||||||
|
self.scale_button(self.network_button, self.network_enum)
|
||||||
|
self.scale_button(self.annotation_button, self.annotation_enum)
|
||||||
|
|
||||||
|
self.scale_button(self.runtime_select_button, ImageEnum.SELECT)
|
||||||
|
self.scale_button(self.stop_button, ImageEnum.STOP)
|
||||||
|
self.scale_button(self.plot_button, ImageEnum.PLOT)
|
||||||
|
self.scale_button(self.runtime_marker_button, ImageEnum.MARKER)
|
||||||
|
self.scale_button(self.node_command_button, ImageEnum.TWONODE)
|
||||||
|
self.scale_button(self.run_command_button, ImageEnum.RUN)
|
||||||
|
|
|
@ -570,10 +570,10 @@ class WayPoint:
|
||||||
return not self == other
|
return not self == other
|
||||||
|
|
||||||
def __lt__(self, other: "WayPoint") -> bool:
|
def __lt__(self, other: "WayPoint") -> bool:
|
||||||
result = self.time < other.time
|
if self.time == other.time:
|
||||||
if result:
|
return self.nodenum < other.nodenum
|
||||||
result = self.nodenum < other.nodenum
|
else:
|
||||||
return result
|
return self.time < other.time
|
||||||
|
|
||||||
|
|
||||||
class WayPointMobility(WirelessModel):
|
class WayPointMobility(WirelessModel):
|
||||||
|
|
17
daemon/tests/test_mobility.py
Normal file
17
daemon/tests/test_mobility.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.location.mobility import WayPoint
|
||||||
|
|
||||||
|
|
||||||
|
class TestMobility:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"wp1, wp2, expected",
|
||||||
|
[
|
||||||
|
(WayPoint(10.0, 1, [0, 0], 1.0), WayPoint(1.0, 2, [0, 0], 1.0), False),
|
||||||
|
(WayPoint(1.0, 1, [0, 0], 1.0), WayPoint(10.0, 2, [0, 0], 1.0), True),
|
||||||
|
(WayPoint(1.0, 1, [0, 0], 1.0), WayPoint(1.0, 2, [0, 0], 1.0), True),
|
||||||
|
(WayPoint(1.0, 2, [0, 0], 1.0), WayPoint(1.0, 1, [0, 0], 1.0), False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_waypoint_lessthan(self, wp1, wp2, expected):
|
||||||
|
assert (wp1 < wp2) == expected
|
Loading…
Add table
Add a link
Reference in a new issue