MAP Display Part 1 - wb8tyw/D-Rats GitHub Wiki

I am having issues converting the Map Display related modules from PyGtk to GTK 3.

It looks like the entire internals need to be changed for GTK 3

I have got it to the point where it briefly displays the map and then blanks it out. But I only get the map, no scale, or anything else.

I was able to fix an existing bug where the refresh did not repaint the map that was flagged by pylint. The code is still suffering from unknown resource leaks and what appears to be unresponsive busy loops.

And that is on Python2. The code does not work at all on Python3 and is emitting deprecation warnings. And there seems to be multiple levels of things deprecated, and it looks like GTK4 will deprecate even more. Finding clear examples of how to move from deprecated code to new code is hard.

One of the issues is that graphics drawing now requires using a cairo context that is only available in the handler for doing a draw operation.

So I have decided to start a redesign / re-write from the ground up as I learn how to do GTK+ programming. This work will be done in the python3_tyw branch of my fork.

Skeleton module

Initial commit of mapdisplay3.py

For this, I am going to use the python logging module instead of printlog.

So for the first step is to make a shell mapwindow module that just has a "hello world" type functionality.

Then make sure that it runs on python2/python3 on Linux and msys2.org mingw packages on Windows 7.

In order to use have the PATH environment setup with msys2, you need to launch the "MSYS2 MinGW 64 bit" bash shell.

Add command parsing, inprove logging, and fix internationalization

Start using the ArgumentParser class for parsing arguments, set it up so that you can specify the log level that is desired during debugging.

Here I setup a custom Parser action for the setting the log level.

A loglevel is a number from 0 to 50 with the higher number being more important to display. There are 6 levels that have predefined names, and if we want to, we can add more names.

I wanted this parser to accept either a name or a number to be fully compatible. It will not need to be changed if we add custom labels in the future.

Using the logger module should eventually simplify quite a bit of code. It can be used on Microsoft Platforms to log to the Windows Event Log, and we can also eventually use it to manage what is displayed in the D-Rats Event log window.

For internationalization, For now, I am putting all the displayed strings in upper case, and will eventually put the exact case messages in the English dictionary.

The pylance program complains if the underscore function is not defined, so there is a hack to define it. But we do not want to have that hack override what the program sets for the locale. So we have to check to see if "_" is in locals(). At least that is how I understand things now. At some point I will need to add some tests for that to verify.

At this point the program looks like this:

#!/usr/bin/python
'''Map Display Unit Test for GTK3'''

from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals

import logging
if not '_' in locals():
    import gettext
    _ = gettext.gettext


def main():
    '''Main function for unit testing.'''

    import argparse

    from .dplatform import get_platform
    gettext.install("D-RATS")
    lang = gettext.translation("D-RATS",
                               localedir="locale",
                               fallback=True)
    lang.install()
    # pylint: disable=global-statement
    global _
    _ = lang.gettext

    # pylint: disable=too-few-public-methods
    class LoglevelAction(argparse.Action):
        '''Custom Log Level action.'''

        def __init__(self, option_strings, dest, nargs=None, **kwargs):
            if nargs is not None:
                raise ValueError("nargs is not allowed")
            argparse.Action.__init__(self, option_strings, dest, **kwargs)

        def __call__(self, parser, namespace, values, option_strings=None):
            level = values.upper()
            level_name = logging.getLevelName(level)
            # Contrary to documentation, the above returns for me
            # an int if given a name or number of a known named level and
            # str if given a number for a level with out a name.
            if isinstance(level_name, int):
                level_name = level
            elif level_name.startswith('Level '):
                level_name = int(level)
            setattr(namespace, self.dest, level_name)

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description=_('MAP DISPLAY TEST'))
    parser.add_argument('-c', '--config',
                        default=get_platform().config_dir(),
                        help=_("USE ALTERNATE CONFIGURATION DIRECTORY"))
    # While this actually returns an int, it needs to be set to the
    # default type of str for the action routine to handle both named and
    # numbered levels.
    parser.add_argument('--loglevel',
                        action=LoglevelAction,
                        default='INFO',
                        help=_('LOGLEVEL TO TEST WITH'))

    args = parser.parse_args()

    logging.basicConfig(
        format="%(asctime)s:%(levelname)s:%(name)s:%(message)s",
        datefmt="%m/%d/%Y %H:%M:%S",
        level=args.loglevel)

    logger = logging.getLogger("mapdisplay3")

    logger.info('Executing Unit test function.')

if __name__ == "__main__":
    main()

And a simple run:

$ python3 -m d_rats.mapdisplay3 -h
usage: mapdisplay3.py [-h] [-c CONFIG] [--loglevel LOGLEVEL]

MAP DISPLAY TEST

optional arguments:
  -h, --help            show this help message and exit
  -c CONFIG, --config CONFIG
                        USE ALTERNATE CONFIGURATION DIRECTORY (default:
                        /home/malmberg/.d-rats-ev)
  --loglevel LOGLEVEL   LOGLEVEL TO TEST WITH (default: INFO)


$ python3 -m d_rats.mapdisplay3
11/10/2021 07:58:52:INFO:mapdisplay3:Executing Unit test function.

Now there are a lot of map related classes and files. I am going to move them under their own map directory. My intention is to make the map display be more independent of D-rats.

So create a map directory and move the mapdisplay3.py program into it as mapdisplay.py. We create a simple __init__.py file for this directory.

'''Package to handle Maps for D-Rats.'''

from .mapwindow import MapWindow as Window

And we create a new MapWindow class in its own file mapwindow.py

'''Map Window Module.'''

import logging

import gi
gi.require_version("Gtk", "3.0")

from gi.repository import Gtk

class MapWindow(Gtk.Window):
    '''
    Map Window.

    This Creates the main map window display.

    :param config: Config object
    :param args: Optional arguments
    '''
    def __init__(self, config):
        Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL)

        self.logger = logging.getLogger("MapWindow")

        self.connect("destroy", Gtk.main_quit)
        self.config = config
        self.marker_list = None
        self.map_tiles = []
        self.logger.info("Testing MapWindow")

    @staticmethod
    def test():
        '''Test method.''' 
        Gtk.main()

The mapdisplay.py class has turned into a test class, and may become simply a test program in the future as there is no reason for d-rats to call it.

By putting the test function as a "staticmethod" in the Mapwindow class, there is no need for the test program to import Gtk related objects.

The __init__.py file controls what classes are visible from the directory. An empty __init__.py file makes everything visible, and we do not want that. It also allows us to give the classes different names where they are used.

Map.Window is a lot more readable than Map.MapWindow.

So now our basic test module is setup as.

'''Map Display Unit Test for GTK3'''
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals

import logging

# This makes pylance happy with out overriding settings
# from the invoker of the class
if not '_' in locals():
    import gettext
    _ = gettext.gettext


def main():
    '''Main function for unit testing.'''

    import argparse
    from d_rats.dplatform import get_platform
    from d_rats import config

    import d_rats.map as Map

    gettext.install("D-RATS")
    lang = gettext.translation("D-RATS",
                               localedir="locale",
                               fallback=True)
    lang.install()
    # pylint: disable=global-statement
    global _
    _ = lang.gettext

    # pylint: disable=too-few-public-methods
    class LoglevelAction(argparse.Action):
        '''
        Custom Log Level action.

        This allows entering a log level command line argument
        as either a known log level name or a number.
        '''

        def __init__(self, option_strings, dest, nargs=None, **kwargs):
            if nargs is not None:
                raise ValueError("nargs is not allowed")
            argparse.Action.__init__(self, option_strings, dest, **kwargs)

        def __call__(self, parser, namespace, values, option_strings=None):
            level = values.upper()
            level_name = logging.getLevelName(level)
            # Contrary to documentation, the above returns for me
            # an int if given a name or number of a known named level and
            # str if given a number for a level with out a name.
            if isinstance(level_name, int):
                level_name = level
            elif level_name.startswith('Level '):
                level_name = int(level)
            setattr(namespace, self.dest, level_name)

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description=_('MAP DISPLAY TEST'))
    parser.add_argument('-c', '--config',
                        default=get_platform().config_dir(),
                        help=_("USE ALTERNATE CONFIGURATION DIRECTORY"))

    # While loglevel actually returns an int, it needs to be set to the
    # default type of str for the action routine to handle both named and
    # numbered levels.
    parser.add_argument('--loglevel',
                        action=LoglevelAction,
                        default='INFO',
                        help=_('LOGLEVEL TO TEST WITH'))

    # Default latitude and longitude
    parser.add_argument('--latitude',
                        type=float,
                        default=45.525012,
                        help=_('INITIAL LATITUDE'))
    parser.add_argument('--longitude',
                        type=float,
                        default=-122.916434,
                        help=_('INITIAL LONGITUDE'))

    args = parser.parse_args()

    logging.basicConfig(
        format="%(asctime)s:%(levelname)s:%(name)s:%(message)s",
        datefmt="%m/%d/%Y %H:%M:%S",
        level=args.loglevel)

    # Each class should have their own logger.
    logger = logging.getLogger("mapdisplay")

    # WB8TYW: DratsConfig takes an unused argument.
    conf = config.DratsConfig(None)
    if args.config:
        logger.info("main: re-config option found -- Reconfigure D-rats")
        get_platform(args.config)

    logger.info('Executing Unit test function.')

    map_window = Map.Window(conf)
    map_window.show()

    Map.Window.test()

if __name__ == "__main__":
    main()