diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index acc93a1..b9cd36f 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -52,6 +52,9 @@ jobs:
     - name: Run linters
       run: tox -e linters
 
+    - name: Run tests
+      run: tox -e unit-tests
+
 
   typecheck:
     runs-on: ubuntu-latest
diff --git a/.gitignore b/.gitignore
index 016a067..744363f 100755
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,5 @@ build/*
 venv
 *~
 cache
-.buildozer
 bin
 .idea/*
diff --git a/Makefile b/Makefile
index ed44567..1989bbd 100644
--- a/Makefile
+++ b/Makefile
@@ -4,9 +4,5 @@ install:
 run:
 	gweatherrouting
 
-run-kivy:
-	python3 setup.kivy.py install
-	gweatherrouting-kivy
-
 build-standalone:
 	nuitka3 gweatherrouting/main.py --follow-imports --follow-stdlib 
diff --git a/README.md b/README.md
index 3bc8fd2..0c3529b 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,7 @@
 <!-- [![PyPI version](https://badge.fury.io/py/gweatherrouting.svg)](https://badge.fury.io/py/gweatherrouting) -->
 
 GWeatherRouting is an opensource sailing route calculator written in python and:
-- Gtk3 for the desktop version (Linux/Mac/Windows)
-- Kivy for the mobile version (iOS/Android) [Read here](README.mobile.md)
+- Gtk3 for the desktop version (Linux/Mac/*BSD/Windows)
 
 ![Routing in progress](https://github.com/dakk/gweatherrouting/raw/master/media/s3.png)
 
@@ -40,7 +39,6 @@ GWeatherRouting is an opensource sailing route calculator written in python and:
 - Ortodromic track render
 - AIS rendering
 - Boat realtime dashboard
-- Mobile version
 
 
 ## Installation
diff --git a/README.mobile.md b/README.mobile.md
deleted file mode 100644
index c74f6d0..0000000
--- a/README.mobile.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Gweatherrouting Mobile
-
-Mobile version is far from behind useful. Currently we are experimenting with kivy framework, and we're able to create a working APK with a map. 
-
-![Mobile](https://github.com/dakk/gweatherrouting/raw/master/media/mobile.png)
-
-## Building for android
-
-Install buildozer:
-
-```
-git clone https://github.com/kivy/buildozer.git
-cd buildozer
-sudo python setup.py install
-```
-
-
-Create an apk:
-
-```
-rm -rf .buildozer/android/platform/build-armeabi-v7a/build/python-installs/gweatherrouting/gweatherrouting* && buildozer android debug deploy run && buildozer android logcat | grep python
-```
\ No newline at end of file
diff --git a/android_recipes/eccodes/__init__.py b/android_recipes/eccodes/__init__.py
deleted file mode 100644
index 9aa1cc3..0000000
--- a/android_recipes/eccodes/__init__.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# isort:skip_file
-from pythonforandroid.recipe import Recipe
-from pythonforandroid.logger import shprint
-from pythonforandroid.util import (
-    current_directory,
-    ensure_dir,
-    BuildInterruptingException,
-)
-from multiprocessing import cpu_count
-from os.path import join
-import sh
-
-
-class EcccodesRecipe(Recipe):
-    version = "2.23.0"
-    built_libraries = {"libeccodes.so": "build/lib"}
-    url = "https://confluence.ecmwf.int/download/attachments/45757960/eccodes-{version}-Source.tar.gz"  # ?api=v2'
-    need_stl_shared = True
-    patches = ["patches/log2.patch"]
-
-    def prebuild_arch(self, arch):
-        self.apply_patches(arch)
-
-    def get_recipe_env(self, arch):
-        env = super().get_recipe_env(arch)
-        return env
-
-    def build_arch(self, arch):
-        source_dir = self.get_build_dir(arch.arch)
-        build_target = join(source_dir, "build")
-        install_target = join(build_target, "install")
-
-        ensure_dir(build_target)
-        with current_directory(build_target):
-            env = self.get_recipe_env(arch)
-            shprint(sh.rm, "-rf", "CMakeFiles/", "CMakeCache.txt", _env=env)
-            shprint(
-                sh.cmake,
-                source_dir,
-                "-DIEEE_LE=1",
-                "-DIEEE_BE=1",
-                "-DENABLE_TESTS=OFF",
-                "-DENABLE_EXTRA_TESTS=OFF",
-                "-DENABLE_BUILD_TOOLS=OFF",
-                "-DENABLE_FORTRAN=OFF",
-                "-DENABLE_NETCDF=OFF",
-                "-DDISABLE_OS_CHECK=ON",
-                "-DCMAKE_SYSTEM_NAME=Android",
-                "-DCMAKE_POSITION_INDEPENDENT_CODE=1",
-                "-DCMAKE_ANDROID_ARCH_ABI={arch}".format(arch=arch.arch),
-                "-DCMAKE_ANDROID_NDK=" + self.ctx.ndk_dir,
-                "-DCMAKE_BUILD_TYPE=Release",
-                "-DCMAKE_INSTALL_PREFIX={}".format(install_target),
-                "-DANDROID_ABI={arch}".format(arch=arch.arch),
-                "-DBUILD_SHARED_LIBS=ON",
-                _env=env,
-            )
-            shprint(sh.make, "-j" + str(cpu_count()), _env=env)
-            shprint(sh.make, "install", _env=env)
-
-
-recipe = EcccodesRecipe()
diff --git a/android_recipes/eccodes/patches/log2.patch b/android_recipes/eccodes/patches/log2.patch
deleted file mode 100644
index 3356868..0000000
--- a/android_recipes/eccodes/patches/log2.patch
+++ /dev/null
@@ -1,13 +0,0 @@
-diff -ur "eccodes-2.23.0-Source (copy 1)/src/grib_optimize_decimal_factor.c" eccodes-2.23.0-Source/src/grib_optimize_decimal_factor.c
---- "eccodes-2.23.0-Source (copy 1)/src/grib_optimize_decimal_factor.c"	2021-08-25 11:35:41.000000000 +0200
-+++ eccodes-2.23.0-Source/src/grib_optimize_decimal_factor.c	2021-11-12 11:20:43.331305877 +0100
-@@ -28,9 +28,7 @@
-     return (int)(log(DBL_MAX) / log(10)) - 1;
- }
- 
--#ifdef ECCODES_ON_WINDOWS
- #define log2(a) (log(a) / 1.44269504088896340736)
--#endif
- 
- static void factec(int* krep, const double pa, const int knbit, const long kdec, const int range, long* ke, int* knutil)
- {
diff --git a/buildozer.spec b/buildozer.spec
deleted file mode 100644
index 08239a8..0000000
--- a/buildozer.spec
+++ /dev/null
@@ -1,416 +0,0 @@
-[app]
-
-# (str) Title of your application
-title = GWeatherRouting
-
-# (str) Package name
-package.name = gweatherrouting
-
-# (str) Package domain (needed for android/ios packaging)
-package.domain = org.gweatherrouting
-
-# (str) Source code where the main.py live
-source.dir = ./gweatherroutingkivy
-
-# (list) Source files to include (let empty to include all the files)
-source.include_exts = py,png,jpg,kv,atlas,geojson,pol
-
-# (list) List of inclusions using pattern matching
-#source.include_patterns = assets/*,images/*.png
-
-# (list) Source files to exclude (let empty to not exclude anything)
-#source.exclude_exts = spec
-
-# (list) List of directory to exclude (let empty to not exclude anything)
-#source.exclude_dirs = tests, bin, venv
-
-# (list) List of exclusions using pattern matching
-#source.exclude_patterns = license,images/*/*.jpg
-
-# (str) Application versioning (method 1)
-version = 0.1
-
-# (str) Application versioning (method 2)
-# version.regex = __version__ = ['"](.*)['"]
-# version.filename = %(source.dir)s/main.py
-
-# (list) Application requirements
-# comma separated e.g. requirements = sqlite3,kivy
-requirements = python3,kivy,numpy,attrs,cffi,findlibs,eccodes,weatherrouting==0.1.6,colorlog,mapview,latlon3,pyproj,geojson_utils,PIL,requests,urllib3,chardet,idna,geojson,gpxpy,kivymd,bs4,pyserial,pynmea2,git+git://github.com/dakk/gweatherrouting@master#egg=gweatherrouting,git+git://github.com/ecmwf/eccodes-python@master#egg=eccodes
-# (str) Custom source folders for requirements
-# Sets custom source for any requirements with recipes
-# requirements.source.kivy = ../../kivy
-# requirements.source.gweatherrouting=../gweatherrouting
-
-# (str) Presplash of the application
-#presplash.filename = %(source.dir)s/data/presplash.png
-
-# (str) Icon of the application
-#icon.filename = %(source.dir)s/data/icon.png
-
-# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
-orientation = portrait
-
-# (list) List of service to declare
-#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY
-
-#
-# OSX Specific
-#
-
-#
-# author = © Copyright Info
-
-# change the major version of python used by the app
-osx.python_version = 3
-
-# Kivy version to use
-osx.kivy_version = 1.9.1
-
-#
-# Android specific
-#
-
-# (bool) Indicate if the application should be fullscreen or not
-fullscreen = 0
-
-# (string) Presplash background color (for android toolchain)
-# Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
-# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
-# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
-# olive, purple, silver, teal.
-#android.presplash_color = #FFFFFF
-
-# (string) Presplash animation using Lottie format.
-# see https://lottiefiles.com/ for examples and https://airbnb.design/lottie/
-# for general documentation.
-# Lottie files can be created using various tools, like Adobe After Effect or Synfig.
-#android.presplash_lottie = "path/to/lottie/file.json"
-
-# (str) Adaptive icon of the application (used if Android API level is 26+ at runtime)
-#icon.adaptive_foreground.filename = %(source.dir)s/data/icon_fg.png
-#icon.adaptive_background.filename = %(source.dir)s/data/icon_bg.png
-
-# (list) Permissions
-android.permissions = INTERNET
-
-# (list) features (adds uses-feature -tags to manifest)
-#android.features = android.hardware.usb.host
-
-# (int) Target Android API, should be as high as possible.
-#android.api = 27
-
-# (int) Minimum API your APK will support.
-#android.minapi = 21
-
-# (int) Android SDK version to use
-#android.sdk = 20
-
-# (str) Android NDK version to use
-#android.ndk = 19b
-
-# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi.
-#android.ndk_api = 21
-
-# (bool) Use --private data storage (True) or --dir public storage (False)
-#android.private_storage = True
-
-# (str) Android NDK directory (if empty, it will be automatically downloaded.)
-#android.ndk_path = 
-
-# (str) Android SDK directory (if empty, it will be automatically downloaded.)
-#android.sdk_path =
-
-# (str) ANT directory (if empty, it will be automatically downloaded.)
-#android.ant_path =
-
-# (bool) If True, then skip trying to update the Android sdk
-# This can be useful to avoid excess Internet downloads or save time
-# when an update is due and you just want to test/build your package
-# android.skip_update = False
-
-# (bool) If True, then automatically accept SDK license
-# agreements. This is intended for automation only. If set to False,
-# the default, you will be shown the license when first running
-# buildozer.
-# android.accept_sdk_license = False
-
-# (str) Android entry point, default is ok for Kivy-based app
-#android.entrypoint = org.kivy.android.PythonActivity
-
-# (str) Full name including package path of the Java class that implements Android Activity
-# use that parameter together with android.entrypoint to set custom Java class instead of PythonActivity
-#android.activity_class_name = org.kivy.android.PythonActivity
-
-# (str) Extra xml to write directly inside the <manifest> element of AndroidManifest.xml
-# use that parameter to provide a filename from where to load your custom XML code
-#android.extra_manifest_xml = ./src/android/extra_manifest.xml
-
-# (str) Extra xml to write directly inside the <manifest><application> tag of AndroidManifest.xml
-# use that parameter to provide a filename from where to load your custom XML arguments:
-#android.extra_manifest_application_arguments = ./src/android/extra_manifest_application_arguments.xml
-
-# (str) Full name including package path of the Java class that implements Python Service
-# use that parameter to set custom Java class instead of PythonService
-#android.service_class_name = org.kivy.android.PythonService
-
-# (str) Android app theme, default is ok for Kivy-based app
-# android.apptheme = "@android:style/Theme.NoTitleBar"
-
-# (list) Pattern to whitelist for the whole project
-#android.whitelist =
-
-# (str) Path to a custom whitelist file
-#android.whitelist_src =
-
-# (str) Path to a custom blacklist file
-#android.blacklist_src =
-
-# (list) List of Java .jar files to add to the libs so that pyjnius can access
-# their classes. Don't add jars that you do not need, since extra jars can slow
-# down the build process. Allows wildcards matching, for example:
-# OUYA-ODK/libs/*.jar
-#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar
-
-# (list) List of Java files to add to the android project (can be java or a
-# directory containing the files)
-#android.add_src =
-
-# (list) Android AAR archives to add
-#android.add_aars =
-
-# (list) Gradle dependencies to add
-#android.gradle_dependencies =
-
-# (bool) Enable AndroidX support. Enable when 'android.gradle_dependencies'
-# contains an 'androidx' package, or any package from Kotlin source.
-# android.enable_androidx requires android.api >= 28
-#android.enable_androidx = False
-
-# (list) add java compile options
-# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option
-# see https://developer.android.com/studio/write/java8-support for further information
-# android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8"
-
-# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies}
-# please enclose in double quotes 
-# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }"
-#android.add_gradle_repositories =
-
-# (list) packaging options to add 
-# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
-# can be necessary to solve conflicts in gradle_dependencies
-# please enclose in double quotes 
-# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'"
-#android.add_packaging_options =
-
-# (list) Java classes to add as activities to the manifest.
-#android.add_activities = com.example.ExampleActivity
-
-# (str) OUYA Console category. Should be one of GAME or APP
-# If you leave this blank, OUYA support will not be enabled
-#android.ouya.category = GAME
-
-# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
-#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png
-
-# (str) XML file to include as an intent filters in <activity> tag
-#android.manifest.intent_filters =
-
-# (str) launchMode to set for the main activity
-#android.manifest.launch_mode = standard
-
-# (list) Android additional libraries to copy into libs/armeabi
-#android.add_libs_armeabi = libs/android/*.so
-#android.add_libs_armeabi_v7a = libs/android-v7/*.so
-#android.add_libs_arm64_v8a = libs/android-v8/*.so
-#android.add_libs_x86 = libs/android-x86/*.so
-#android.add_libs_mips = libs/android-mips/*.so
-
-# (bool) Indicate whether the screen should stay on
-# Don't forget to add the WAKE_LOCK permission if you set this to True
-#android.wakelock = False
-
-# (list) Android application meta-data to set (key=value format)
-#android.meta_data =
-
-# (list) Android library project to add (will be added in the
-# project.properties automatically.)
-#android.library_references =
-
-# (list) Android shared libraries which will be added to AndroidManifest.xml using <uses-library> tag
-#android.uses_library =
-
-# (str) Android logcat filters to use
-#android.logcat_filters = *:S python:D
-
-# (bool) Android logcat only display log for activity's pid
-#android.logcat_pid_only = False
-
-# (str) Android additional adb arguments
-#android.adb_args = -H host.docker.internal
-
-# (bool) Copy library instead of making a libpymodules.so
-#android.copy_libs = 1
-
-# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
-android.arch = armeabi-v7a
-
-# (int) overrides automatic versionCode computation (used in build.gradle)
-# this is not the same as app version and should only be edited if you know what you're doing
-# android.numeric_version = 1
-
-# (bool) enables Android auto backup feature (Android API >=23)
-android.allow_backup = True
-
-# (str) XML file for custom backup rules (see official auto backup documentation)
-# android.backup_rules =
-
-# (str) If you need to insert variables into your AndroidManifest.xml file,
-# you can do so with the manifestPlaceholders property.
-# This property takes a map of key-value pairs. (via a string)
-# Usage example : android.manifest_placeholders = [myCustomUrl:\"org.kivy.customurl\"]
-# android.manifest_placeholders = [:]
-
-# (bool) disables the compilation of py to pyc/pyo files when packaging
-# android.no-compile-pyo = True
-
-#
-# Python for android (p4a) specific
-#
-
-# (str) python-for-android URL to use for checkout
-#p4a.url =
-
-# (str) python-for-android fork to use in case if p4a.url is not specified, defaults to upstream (kivy)
-#p4a.fork = kivy
-
-# (str) python-for-android branch to use, defaults to master
-#p4a.branch = master
-
-# (str) python-for-android specific commit to use, defaults to HEAD, must be within p4a.branch
-#p4a.commit = HEAD
-
-# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
-#p4a.source_dir =
-
-# (str) The directory in which python-for-android should look for your own build recipes (if any)
-p4a.local_recipes = android_recipes
-
-# (str) Filename to the hook for p4a
-#p4a.hook =
-
-# (str) Bootstrap to use for android builds
-#p4a.bootstrap = sdl2
-
-# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
-#p4a.port =
-
-# Control passing the --use-setup-py vs --ignore-setup-py to p4a
-# "in the future" --use-setup-py is going to be the default behaviour in p4a, right now it is not
-# Setting this to false will pass --ignore-setup-py, true will pass --use-setup-py
-# NOTE: this is general setuptools integration, having pyproject.toml is enough, no need to generate
-# setup.py if you're using Poetry, but you need to add "toml" to source.include_exts.
-#p4a.setup_py = false
-
-# (str) extra command line arguments to pass when invoking pythonforandroid.toolchain
-#p4a.extra_args =
-
-
-#
-# iOS specific
-#
-
-# (str) Path to a custom kivy-ios folder
-#ios.kivy_ios_dir = ../kivy-ios
-# Alternately, specify the URL and branch of a git checkout:
-ios.kivy_ios_url = https://github.com/kivy/kivy-ios
-ios.kivy_ios_branch = master
-
-# Another platform dependency: ios-deploy
-# Uncomment to use a custom checkout
-#ios.ios_deploy_dir = ../ios_deploy
-# Or specify URL and branch
-ios.ios_deploy_url = https://github.com/phonegap/ios-deploy
-ios.ios_deploy_branch = 1.10.0
-
-# (bool) Whether or not to sign the code
-ios.codesign.allowed = false
-
-# (str) Name of the certificate to use for signing the debug version
-# Get a list of available identities: buildozer ios list_identities
-#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"
-
-# (str) The development team to use for signing the debug version
-#ios.codesign.development_team.debug = <hexstring>
-
-# (str) Name of the certificate to use for signing the release version
-#ios.codesign.release = %(ios.codesign.debug)s
-
-# (str) The development team to use for signing the release version
-#ios.codesign.development_team.release = <hexstring>
-
-# (str) URL pointing to .ipa file to be installed
-# This option should be defined along with `display_image_url` and `full_size_image_url` options.
-#ios.manifest.app_url =
-
-# (str) URL pointing to an icon (57x57px) to be displayed during download
-# This option should be defined along with `app_url` and `full_size_image_url` options.
-#ios.manifest.display_image_url =
-
-# (str) URL pointing to a large icon (512x512px) to be used by iTunes
-# This option should be defined along with `app_url` and `display_image_url` options.
-#ios.manifest.full_size_image_url =
-
-
-[buildozer]
-
-# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
-log_level = 2
-
-# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
-warn_on_root = 1
-
-# (str) Path to build artifact storage, absolute or relative to spec file
-# build_dir = ./.buildozer
-
-# (str) Path to build output (i.e. .apk, .ipa) storage
-# bin_dir = ./bin
-
-#    -----------------------------------------------------------------------------
-#    List as sections
-#
-#    You can define all the "list" as [section:key].
-#    Each line will be considered as a option to the list.
-#    Let's take [app] / source.exclude_patterns.
-#    Instead of doing:
-#
-#[app]
-#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
-#
-#    This can be translated into:
-#
-#[app:source.exclude_patterns]
-#license
-#data/audio/*.wav
-#data/images/original/*
-#
-
-
-#    -----------------------------------------------------------------------------
-#    Profiles
-#
-#    You can extend section / key with a profile
-#    For example, you want to deploy a demo version of your application without
-#    HD content. You could first change the title to add "(demo)" in the name
-#    and extend the excluded directories to remove the HD content.
-#
-#[app@demo]
-#title = My Application (demo)
-#
-#[app:source.exclude_patterns@demo]
-#images/hd/*
-#
-#    Then, invoke the command line with the "demo" profile:
-#
-#buildozer --profile demo android debug
diff --git a/gweatherrouting/core/gribmanager.py b/gweatherrouting/core/gribmanager.py
index 17c84d8..9d4555a 100644
--- a/gweatherrouting/core/gribmanager.py
+++ b/gweatherrouting/core/gribmanager.py
@@ -8,9 +8,6 @@
 from gweatherrouting.core.grib import Grib
 from gweatherrouting.core.storage import GRIB_DIR, TEMP_DIR, Storage
 
-# except:
-#     from gweatherrouting.core.utils.dummy_storage import Storage
-
 # from bs4 import BeautifulSoup
 
 logger = logging.getLogger("gweatherrouting")
diff --git a/gweatherrouting/core/utils/__init__.py b/gweatherrouting/core/utils/__init__.py
index f84f54d..87daf37 100644
--- a/gweatherrouting/core/utils/__init__.py
+++ b/gweatherrouting/core/utils/__init__.py
@@ -22,12 +22,8 @@
 from geojson_utils import point_in_polygon
 from typing import Dict, Callable
 
-# try:
 from gweatherrouting.core.storage import *  # noqa: F401, F403
 
-# except:
-#     from .dummy_storage import *  # noqa: F401, F403
-
 this_dir, this_fn = os.path.split(__file__)
 COUNTRIES = json.load(open(this_dir + "/../../data/countries.geojson", "r"))
 COUNTRY_SHAPES = []
diff --git a/gweatherrouting/core/utils/dummy_storage.py b/gweatherrouting/core/utils/dummy_storage.py
deleted file mode 100644
index cc53c00..0000000
--- a/gweatherrouting/core/utils/dummy_storage.py
+++ /dev/null
@@ -1,127 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-APP_NAME = "gweatherrouting"
-DATA_DIR = "/storage"
-GRIB_DIR = DATA_DIR
-TEMP_DIR = DATA_DIR
-
-
-class Storage(dict):
-    __init = False
-
-    def __init__(self, filename=None, parent=None, *args, **kwargs):
-        super(Storage, self).__init__(*args, **kwargs)
-
-        self.__parent = parent
-        self.__filename = filename
-        self.__handlers = {}
-
-        for arg in args:
-            if isinstance(arg, dict):
-                for k, v in arg.iteritems():
-                    self[k] = v
-
-        if kwargs:
-            for k, v in kwargs.iteritems():
-                self[k] = v
-
-        if parent is not None:
-            self.__init = True
-
-    def __getattr__(self, attr):
-        return self.get(attr)
-
-    def __setattr__(self, key, value):
-        self.__setitem__(key, value)
-        self.save()
-        self.notify_change(key, value)
-
-    def __setitem__(self, key, value):
-        super(Storage, self).__setitem__(key, value)
-        self.__dict__.update({key: value})
-        self.save()
-        self.notify_change(key, value)
-
-    def __delattr__(self, item):
-        self.__delitem__(item)
-        self.save()
-
-    def __delitem__(self, key):
-        super(Storage, self).__delitem__(key)
-        del self.__dict__[key]
-        self.save()
-
-    def loadData(self, data):
-        for x in data:
-            if isinstance(data[x], dict):
-                self[x].loadData(data[x])
-            else:
-                self[x] = data[x]
-
-    def save(self):
-        if self.__parent:
-            return self.__parent.save()
-
-        if not self.__filename or not self.__init:
-            return
-
-    def load(self):
-        if self.__parent:
-            return
-
-        if not self.__filename:
-            return
-
-        return
-
-    def loadOrSaveDefault(self):
-        try:
-            self.load()
-            self.__init = True
-        except:
-            self.__init = True
-            self.save()
-
-    def to_dict(self):
-        d = {}
-        for x in self:
-            if x.find("Storage") != -1:
-                continue
-
-            if isinstance(self[x], dict):
-                d[x] = self[x].to_dict()
-            else:
-                d[x] = self[x]
-
-        return d
-
-    def register_on_change(self, k, handler):
-        if k not in self.__handlers:
-            self.__handlers[k] = []
-
-        self.__handlers[k].append(handler)
-        handler(self[k])
-
-    def notify_change(self, k, v):
-        print("notify change", k, v)
-        if not self.__init:
-            return
-
-        if k not in self.__handlers:
-            return
-
-        for x in self.__handlers[k]:
-            x(v)
diff --git a/gweatherrouting/kivy/__init__.py b/gweatherrouting/kivy/__init__.py
deleted file mode 100644
index 39ece76..0000000
--- a/gweatherrouting/kivy/__init__.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-# isort:skip_file
-from .gribscreen import GribScreen  # noqa: F401
-from .trackscreen import TrackScreen  # noqa: F401
-from .settingsscreen import SettingsScreen  # noqa: F401
-from .regattascreen import RegattaScreen  # noqa: F401
diff --git a/gweatherrouting/kivy/app.kv b/gweatherrouting/kivy/app.kv
deleted file mode 100644
index 28b5aea..0000000
--- a/gweatherrouting/kivy/app.kv
+++ /dev/null
@@ -1,201 +0,0 @@
-#:kivy 1.0
-
-#:import name gweatherrouting.kivy.gribscreen
-##:include gweatherrouting/kivy/gribscreen.kv
-
-#:import name gweatherrouting.kivy.trackscreen
-##:include gweatherrouting/kivy/trackscreen.kv
-
-#:import name gweatherrouting.kivy.settingsscreen
-##:include gweatherrouting/kivy/settingsscreen.kv
-
-#:import name gweatherrouting.kivy.regattascreen
-##:include gweatherrouting/kivy/regattascreen.kv
-
-#:import name gweatherrouting.kivy.timepickerdialog
-##:include gweatherrouting/kivy/timepickerdialog.kv
-
-
-MDScreen:
-    id: mainScreen
-
-    MDToolbar:
-        id: toolbar
-        title: "GWR"
-        pos_hint: {"top": 1}
-        elevation: 10
-        md_bg_color: app.theme_cls.accent_color
-        right_action_items: [["dots-vertical", lambda x: app.callback()]]
-        left_action_items: [["menu", lambda x: navDrawer.set_state("open")]]
-
-    # MDBottomAppBar:
-    #     MDToolbar:
-    #         title: "Title"
-    #         icon: "git"
-    #         type: "bottom"
-    #         left_action_items: [["menu", lambda x: x]]
-    #         on_action_button: app.callback(self.icon)
-
-    MDNavigationLayout:
-        y: toolbar.height
-        size_hint_y: None
-        height: root.height - toolbar.height
-
-        ScreenManager:
-            id: screenManager
-
-            MDScreen:
-                name: "Map Screen"
-                id: mapScreen
-
-                MDBoxLayout:
-                    orientation: "vertical"
-
-                    MDBoxLayout:
-                        size_hint_y: None
-                        height: "50dp"
-                        orientation: "horizontal"
-
-                        MDIconButton:
-                            icon: "step-backward-2"
-                            user_font_size: "28sp"
-                            on_press: app.onBackwardClick(hours=12)
-
-                        MDIconButton:
-                            icon: "step-backward"
-                            user_font_size: "28sp"
-                            on_press: app.onBackwardClick(hours=1)
-
-                        MDIconButton:
-                            icon: "step-forward"
-                            user_font_size: "28sp"
-                            on_press: app.onForwardClick(hours=1)
-
-                        # MDIconButton:
-                        #     icon: "play"
-                        #     user_font_size: "28sp"
-                        #     on_press: app.onPlayClick()
-
-                        MDIconButton:
-                            icon: "step-forward-2"
-                            user_font_size: "28sp"
-                            on_press: app.onForwardClick(hours=12)
-
-                        MDIconButton:
-                            icon: "clock"
-                            user_font_size: "28sp"
-                            on_press: app.openTimePickerDialog()
-
-                        MDLabel:
-                            pos_hint: {"right": 1}
-                            id: timeLabel
-                            text: str(app.timeControl.time)
-
-
-                    MapView
-                        id: mapView
-                        zoom:   8
-                        lat:    39
-                        lon:    9
-
-                        on_touch_up: app.onMapTouchDown(args)
-
-            GribScreen:
-                name: "Grib Screen"
-                id: gribScreen
-
-
-            TrackScreen:
-                name: "Track Screen"
-                id: trackScreen
-
-            MDScreen:
-                name: "Routings Screen"
-                id: routingScreen
-
-                MDLabel:
-                    text: "Screen 2"
-                    halign: "center"
-
-            RegattaScreen:
-                name: "Settings Screen"
-                id: regattaScreen
-
-            SettingsScreen:
-                name: "Settings Screen"
-                id: settingsScreen
-
-
-        MDNavigationDrawer:
-            id: navDrawer
-
-            MDBoxLayout:
-                orientation: "vertical"
-                padding: "8dp"
-                spacing: "8dp"
-
-                ScrollView:
-                    MDList:
-                        id: drawerList
-
-                        OneLineListItem:
-                            text: "Map"
-
-                            on_press:
-                                root.ids.navDrawer.set_state("close")
-                                root.ids.screenManager.current = mapScreen.name
-
-                            IconLeftWidget:
-                                icon: "map"
-                                theme_text_color: "Custom"
-                                text_color: app.theme_cls.accent_color
-
-                        OneLineListItem:
-                            text: "Regatta"
-
-                            on_press:
-                                root.ids.navDrawer.set_state("close")
-                                root.ids.screenManager.current = regattaScreen.name
-
-                            IconLeftWidget:
-                                icon: "plus"
-
-                        OneLineListItem:
-                            text: "Gribs"
-
-                            on_press:
-                                root.ids.navDrawer.set_state("close")
-                                root.ids.screenManager.current = gribScreen.name
-
-                            IconLeftWidget:
-                                icon: "language-python"
-
-                        OneLineListItem:
-                            text: "Tracks & POIs"
-
-                            on_press:
-                                root.ids.navDrawer.set_state("close")
-                                root.ids.screenManager.current = trackScreen.name
-                            
-                            IconLeftWidget:
-                                icon: "minus"
-
-                        OneLineListItem:
-                            text: "Routings"
-
-                            on_press:
-                                root.ids.navDrawer.set_state("close")
-                                root.ids.screenManager.current = routingScreen.name
-
-                            IconLeftWidget:
-                                icon: "plus"
-
-                        OneLineListItem:
-                            text: "Settings"
-
-                            on_press:
-                                root.ids.navDrawer.set_state("close")
-                                root.ids.screenManager.current = settingsScreen.name
-
-                            IconLeftWidget:
-                                icon: "map"
\ No newline at end of file
diff --git a/gweatherrouting/kivy/app.py b/gweatherrouting/kivy/app.py
deleted file mode 100644
index 7ca7986..0000000
--- a/gweatherrouting/kivy/app.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-
-import os
-
-from kivy.app import Builder
-
-# from kivy_garden.mapview import MapView
-from kivymd.app import MDApp
-
-from ..core import TimeControl
-from .maplayers import GribMapLayer, POIMapLayer, TrackMapLayer
-from .timepickerdialog import TimePickerDialog
-
-
-class GWeatherRoutingApp(MDApp):
-    def __init__(self, core):
-        super(GWeatherRoutingApp, self).__init__()
-        self.timeControl = TimeControl()
-        self.core = core
-
-    def build(self):
-        strs = [
-            "gribscreen.kv",
-            "trackscreen.kv",
-            "regattascreen.kv",
-            "settingsscreen.kv",
-            "timepickerdialog.kv",
-            "app.kv",
-        ]
-
-        ss = ""
-        for x in strs:
-            f = open(os.path.abspath(os.path.dirname(__file__)) + "/" + x, "r")
-            ss += f.read() + "\n"
-            f.close()
-
-        # self.root = Builder.load_file(os.path.abspath(os.path.dirname(__file__)) + "/app.kv")
-        self.root = Builder.load_string(ss)
-        return self.root
-
-    def on_start(self):
-        # Setup map
-        self.root.ids.mapView.add_layer(
-            GribMapLayer(self.core.gribManager, self.timeControl)
-        )
-        self.root.ids.mapView.add_layer(
-            POIMapLayer(self.core.poiManager, self.timeControl)
-        )
-        self.root.ids.mapView.add_layer(
-            TrackMapLayer(self.core.trackManager, self.timeControl)
-        )
-
-        # Setup grib
-        self.root.ids.gribScreen.gribManager = self.core.gribManager
-        # self.root.ids.gribScreen.updateLocalGribs()
-
-        self.root.ids.trackScreen.trackManager = self.core.trackManager
-        self.root.ids.trackScreen.poiManager = self.core.poiManager
-
-    def onMapTouchDown(self, k):
-        # print (k)
-        # TODO: open an menu with add poi / point to track
-        pass
-
-    def onForwardClick(self, hours):
-        self.timeControl.increase(hours=hours)
-        self.root.ids.timeLabel.text = str(self.timeControl.time)
-
-    def onBackwardClick(self, hours):
-        self.timeControl.decrease(hours=hours)
-        self.root.ids.timeLabel.text = str(self.timeControl.time)
-
-    def openTimePickerDialog(self):
-        dialog = TimePickerDialog(self.timeControl.time)
-        dialog.open()
diff --git a/gweatherrouting/kivy/gribscreen.kv b/gweatherrouting/kivy/gribscreen.kv
deleted file mode 100644
index 045ff91..0000000
--- a/gweatherrouting/kivy/gribscreen.kv
+++ /dev/null
@@ -1,50 +0,0 @@
-#:kivy 1.0
-<LocalGribListItem>:
-    IconLeftWidget:
-        icon: root.icon
-
-    RightBody:
-        orientation: 'horizontal'
-
-        MDIconButton:
-            icon: 'delete'
-            margin_right: dp(10)
-            on_release: root.remove()
-
-        MDCheckbox:
-            active: root.active
-            on_active:
-                root.active = self.active
-                root.setEnabled(self.active)
-
-<RemoteGribListItem>:
-    on_press: root.callback(self.url)
-
-    IconLeftWidget:
-        icon: "tailwind"
-
-
-<GribScreen>:
-    MDProgressBar:
-        id: downloadProgress
-        value: 0
-        pos_hint: { 'bottom': 1 }
-
-    ScrollView:            
-        MDList:
-            id: gribList
-
-    MDFloatingActionButton:
-        icon: "cloud-download"
-        elevation_normal: 12
-        pos_hint: {'right': 1, 'bottom': 0.86}
-        md_bg_color: app.theme_cls.primary_color
-        on_release: root.openGribDownloadDialog()
-
-
-    # MDFloatingActionButton:
-    #     icon: "folder-open"
-    #     elevation_normal: 12
-    #     pos_hint: {'center_x': .6, 'bottom': 0.86 }
-    #     md_bg_color: app.theme_cls.primary_color
-    #     on_release: root.openGribDownloadDialog()
\ No newline at end of file
diff --git a/gweatherrouting/kivy/gribscreen.py b/gweatherrouting/kivy/gribscreen.py
deleted file mode 100644
index 09324e2..0000000
--- a/gweatherrouting/kivy/gribscreen.py
+++ /dev/null
@@ -1,115 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-from threading import Thread
-
-from kivy.properties import BooleanProperty, ObjectProperty, StringProperty
-from kivymd.uix.boxlayout import MDBoxLayout
-from kivymd.uix.dialog import MDDialog
-from kivymd.uix.list import IRightBodyTouch, ThreeLineAvatarIconListItem
-from kivymd.uix.screen import MDScreen
-
-
-class RemoteGribListItem(ThreeLineAvatarIconListItem):
-    callback = ObjectProperty(None)
-    url = StringProperty("")
-
-
-class LocalGribListItem(ThreeLineAvatarIconListItem):
-    icon = StringProperty("tailwind")
-    active = BooleanProperty(False)
-    gribManager = ObjectProperty()
-    updateCallback = ObjectProperty()
-
-    def setEnabled(self, enabled):
-        self.gribManager.changeState(self.text, enabled)
-
-    def remove(self):
-        self.gribManager.remove(self.text)
-        self.updateCallback()
-
-
-class RightBody(IRightBodyTouch, MDBoxLayout):
-    pass
-
-
-class GribScreen(MDScreen):
-    gribManager = None
-    gribDownloadDialog = None
-
-    def updateLocalGribs(self):
-        gribList = self.ids.gribList
-        gribList.clear_widgets()
-        self.gribManager.refreshLocalGribs()
-
-        for x in self.gribManager.localGribs:
-            gribList.add_widget(
-                LocalGribListItem(
-                    text=x.name,
-                    updateCallback=self.updateLocalGribs,
-                    gribManager=self.gribManager,
-                    active=self.gribManager.isEnabled(x.name),
-                    secondary_text="Start at %s and is valid for %s hours"
-                    % (str(x.startTime), str(x.lastForecast)),
-                    tertiary_text="Centre: %s" % x.centre,
-                )
-            )
-
-    def on_pre_enter(self, *args):
-        self.updateLocalGribs()
-
-    def onSelectRemoteGrib(self, uri):
-        self.gribDownloadDialog.dismiss()
-        self.ids.downloadProgress.value = 0
-
-        def onGribDownloadPercentage(percentage):
-            self.ids.downloadProgress.value = percentage
-
-        def onGribDownloadCompleted(g):
-            self.updateLocalGribs()
-
-        t = Thread(
-            target=self.gribManager.download,
-            args=(
-                uri,
-                onGribDownloadPercentage,
-                onGribDownloadCompleted,
-            ),
-        )
-        t.start()
-
-    def openGribDownloadDialog(self):
-        if not self.gribDownloadDialog:
-            items = []
-
-            for x in self.gribManager.getDownloadList():
-                items.append(
-                    RemoteGribListItem(
-                        text=x[0],
-                        secondary_text="Start at %s, %s" % (str(x[3]), str(x[2])),
-                        tertiary_text="Centre: %s" % x[1],
-                        url=x[4],
-                        callback=self.onSelectRemoteGrib,
-                    )
-                )
-
-            self.gribDownloadDialog = MDDialog(
-                title="Download a GRIB", type="simple", items=items
-            )
-
-        self.gribDownloadDialog.open()
-
-    def openFileChooserDialog(self):
-        pass
diff --git a/gweatherrouting/kivy/maplayers/__init__.py b/gweatherrouting/kivy/maplayers/__init__.py
deleted file mode 100644
index 7638375..0000000
--- a/gweatherrouting/kivy/maplayers/__init__.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-# isort:skip_file
-from .gribmaplayer import GribMapLayer  # noqa: F401
-from .poimaplayer import POIMapLayer  # noqa: F401
-from .trackmaplayer import TrackMapLayer  # noqa: F401
diff --git a/gweatherrouting/kivy/maplayers/gribmaplayer.py b/gweatherrouting/kivy/maplayers/gribmaplayer.py
deleted file mode 100644
index e908022..0000000
--- a/gweatherrouting/kivy/maplayers/gribmaplayer.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-import math
-
-from kivy.graphics import Color, Line
-
-# from kivy.graphics.tesselator import TYPE_POLYGONS, WINDING_ODD, Tesselator
-# from kivy.metrics import dp
-# from kivy.properties import ObjectProperty, StringProperty
-# from kivy.utils import get_color_from_hex
-# from kivy_garden.mapview.constants import CACHE_DIR
-# from kivy_garden.mapview.downloader import Downloader
-from kivy_garden.mapview.view import MapLayer
-
-from ...common import windColor
-
-
-class GribMapLayer(MapLayer):
-    def __init__(self, gribManager, timeControl, **kwargs):
-        super().__init__(**kwargs)
-        self.gribManager = gribManager
-        self.timeControl = timeControl
-        self.timeControl.connect("time-change", self.onTimeChange)
-        self.arrowOpacity = 0.3
-
-    def onTimeChange(self, t):
-        self.draw()
-
-    def reposition(self):
-        """Function called when :class:`MapView` is moved. You must recalculate
-        the position of your children."""
-        self.draw()
-
-    def unload(self):
-        """Called when the view want to completly unload the layer."""
-        pass
-
-    def drawWindArrow(self, x, y, wdir, wspeed):
-        wdir = -math.radians(wdir)
-
-        a, b, c = windColor(wspeed)
-
-        length = 25
-        RD = 30
-        self.canvas.add(Color(a, b, c, self.arrowOpacity))
-
-        xy = [x, y, x + (length * math.sin(wdir)), y + (length * math.cos(wdir))]
-        self.canvas.add(Line(points=xy, width=1.5))
-
-        xy = [
-            x + (length * math.sin(wdir)),
-            y + (length * math.cos(wdir)),
-            x + (4 * math.sin(wdir - math.radians(RD))),
-            y + (4 * math.cos(wdir - math.radians(RD))),
-        ]
-        self.canvas.add(Line(points=xy, width=1.5))
-
-        xy = [
-            x + (length * math.sin(wdir)),
-            y + (length * math.cos(wdir)),
-            x + (4 * math.sin(wdir + math.radians(RD))),
-            y + (4 * math.cos(wdir + math.radians(RD))),
-        ]
-        self.canvas.add(Line(points=xy, width=1.5))
-
-    def draw(self):  # noqa: C901
-        view = self.parent
-        zoom = view.zoom
-        bbox = view.get_bbox()
-
-        p1lat, p1lon, p2lat, p2lon = bbox
-
-        bounds = (
-            (min(p1lat, p2lat), min(p1lon, p2lon)),
-            (max(p1lat, p2lat), max(p1lon, p2lon)),
-        )
-        data = self.gribManager.getWind2D(self.timeControl.time, bounds)
-
-        self.canvas.clear()
-
-        if not data or len(data) == 0:
-            return
-
-        # scale = int(math.fabs(zoom - 8))
-        if zoom > 8:
-            scale = 1
-        elif zoom > 7:
-            scale = 2
-        elif zoom > 6:
-            scale = 3
-        elif zoom > 5:
-            scale = 4
-        elif zoom > 4:
-            scale = 5
-        elif zoom > 3:
-            scale = 6
-        elif zoom > 2:
-            scale = 7
-        elif zoom > 1:
-            scale = 8
-        elif zoom > 0:
-            scale = 9
-
-        # Draw arrows
-        for x in data[::scale]:
-            for y in x[::scale]:
-                xx, yy = view.get_window_xy_from(y[2][0], y[2][1], zoom)
-                self.drawWindArrow(xx, yy, y[0], y[1])
diff --git a/gweatherrouting/kivy/maplayers/poimaplayer.py b/gweatherrouting/kivy/maplayers/poimaplayer.py
deleted file mode 100644
index ca99298..0000000
--- a/gweatherrouting/kivy/maplayers/poimaplayer.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-from kivy.core.text import Label as CoreLabel
-from kivy.graphics import Color, Line, Rectangle
-from kivy_garden.mapview.view import MapLayer
-
-
-class POIMapLayer(MapLayer):
-    def __init__(self, poiManager, timeControl, **kwargs):
-        super().__init__(**kwargs)
-        self.poiManager = poiManager
-        self.timeControl = timeControl
-        self.timeControl.connect("time-change", self.onTimeChange)
-
-    def onTimeChange(self, t):
-        self.draw()
-
-    def reposition(self):
-        """Function called when :class:`MapView` is moved. You must recalculate
-        the position of your children."""
-        self.draw()
-
-    def unload(self):
-        """Called when the view want to completly unload the layer."""
-        pass
-
-    def draw(self):
-        view = self.parent
-        zoom = view.zoom
-        # bbox = view.get_bbox()
-        self.canvas.clear()
-
-        for tr in self.poiManager:
-            if not tr.visible:
-                continue
-
-            x, y = view.get_window_xy_from(tr.position[0], tr.position[1], zoom)
-
-            # self.canvas.add(Color(1-self.colour, self.colour, 0, 1))
-            # self.rect = Rectangle(size=self.size, pos=self.pos)
-            # self.canvas.add(self.rect)
-            self.canvas.add(Color(1, 1, 1))
-            label = CoreLabel(text=tr.name, font_size=16)
-            label.refresh()
-            text = label.texture
-            self.canvas.add(
-                Rectangle(size=text.size, pos=[x - 20, y - 20], texture=text)
-            )
-            # self.canvas.ask_update()
-
-            # self.canvas.add(Triangle([x - 5, y - 5, x, y + 5, x + 5, y - 5]))
-
-            # Style.Poi.Font.apply(cr)
-            # cr.move_to(x - 10, y - 10)
-            # cr.show_text(tr.name)
-            # cr.stroke()
-
-            # Style.Poi.Triangle.apply(cr)
-
-            self.canvas.add(Line(points=[x - 5, y - 5, x, y + 5]))
-            self.canvas.add(Line(points=[x + 5, y - 5, x, y + 5]))
-            self.canvas.add(Line(points=[x - 5, y - 5, x + 5, y - 5]))
diff --git a/gweatherrouting/kivy/maplayers/trackmaplayer.py b/gweatherrouting/kivy/maplayers/trackmaplayer.py
deleted file mode 100644
index b076661..0000000
--- a/gweatherrouting/kivy/maplayers/trackmaplayer.py
+++ /dev/null
@@ -1,156 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-# flake8: noqa: F841
-from kivy_garden.mapview.view import MapLayer
-
-
-class TrackMapLayer(MapLayer):
-    def __init__(self, trackManager, timeControl, **kwargs):
-        super().__init__(**kwargs)
-        self.trackManager = trackManager
-        self.timeControl = timeControl
-        self.timeControl.connect("time-change", self.onTimeChange)
-
-    def onTimeChange(self, t):
-        self.draw()
-
-    def reposition(self):
-        """Function called when :class:`MapView` is moved. You must recalculate
-        the position of your children."""
-        self.draw()
-
-    def unload(self):
-        """Called when the view want to completly unload the layer."""
-        pass
-
-    def draw(self):
-        view = self.parent
-        zoom = view.zoom
-        bbox = view.get_bbox()
-
-        p1lat, p1lon, p2lat, p2lon = bbox
-
-        for tr in self.trackManager.routings:
-            if not tr.visible:
-                continue
-
-            prevx = None
-            prevy = None
-            prevp = None
-            i = 0
-
-            for p in tr:
-                i += 1
-                x, y = view.get_window_xy_from(p[0], p[1], zoom)
-
-                # if prevp is None:
-                # 	Style.Track.RoutingTrackFont.apply(cr)
-                # 	cr.move_to(x+10, y)
-                # 	cr.show_text(tr.name)
-                # 	cr.stroke()
-
-                # # Draw boat
-                # if prevp is not None:
-                # 	tprev = dateutil.parser.parse(prevp[2])
-                # 	tcurr = dateutil.parser.parse(p[2])
-
-                # 	if tcurr >= self.timeControl.time and tprev < self.timeControl.time:
-                # 		dt = (tcurr-tprev).total_seconds()
-                # 		dl = utils.pointDistance(prevp[0], prevp[1], p[0], p[1]) / dt *
-                # (self.timeControl.time - tprev).total_seconds()
-
-                # 		rp = utils.routagePointDistance (prevp[0], prevp[1], dl, math.radians(p[6]))
-
-                # 		Style.Track.RoutingBoat.apply(cr)
-                # 		xx, yy = gpsmap.convert_geographic_to_screen (OsmGpsMap.MapPoint.new_degrees
-                # (rp[0], rp[1]))
-                # 		cr.arc(xx, yy, 7, 0, 2 * math.pi)
-                # 		cr.fill()
-
-                # if prevx is not None and prevy is not None:
-                # 	Style.Track.RoutingTrack.apply(cr)
-                # 	cr.move_to (prevx, prevy)
-                # 	cr.line_to (x, y)
-                # 	cr.stroke()
-
-                # Style.Track.RoutingTrackCircle.apply(cr)
-                # cr.arc(x, y, 5, 0, 2 * math.pi)
-                # cr.stroke()
-
-                prevx = x
-                prevy = y
-                # prevp = p
-
-        for tr in self.trackManager:
-            if not tr.visible:
-                continue
-
-            active = False
-            if (
-                self.trackManager.hasActive()
-                and self.trackManager.getActive().name == tr.name
-            ):
-                active = True
-
-            prevx = None
-            prevy = None
-            i = 0
-
-            for p in tr:
-                i += 1
-                x, y = view.get_window_xy_from(p[0], p[1], zoom)
-
-                # if prevx is None:
-                # 	if active:
-                # 		Style.Track.TrackActiveFont.apply(cr)
-                # 	else:
-                # 		Style.Track.TrackInactiveFont.apply(cr)
-
-                # 	cr.move_to(x-10, y-10)
-                # 	cr.show_text(tr.name)
-                # 	cr.stroke()
-
-                # if active:
-                # 	Style.Track.TrackActivePoiFont.apply(cr)
-                # else:
-                # 	Style.Track.TrackInactivePoiFont.apply(cr)
-
-                # cr.move_to(x-4, y+18)
-                # cr.show_text(str(i))
-                # cr.stroke()
-
-                # if prevx is not None and prevy is not None:
-                # 	if active:
-                # 		Style.Track.TrackActive.apply(cr)
-                # 	else:
-                # 		Style.Track.TrackInactive.apply(cr)
-
-                # 	cr.move_to (prevx, prevy)
-                # 	cr.line_to (x, y)
-                # 	cr.stroke()
-
-                # if active:
-                # 	Style.Track.TrackActive.apply(cr)
-                # else:
-                # 	Style.Track.TrackInactive.apply(cr)
-
-                # Style.resetDash(cr)
-
-                # cr.arc(x, y, 5, 0, 2 * math.pi)
-                # cr.stroke()
-
-                prevx = x
-                prevy = y
diff --git a/gweatherrouting/kivy/regattascreen.kv b/gweatherrouting/kivy/regattascreen.kv
deleted file mode 100644
index e2b5238..0000000
--- a/gweatherrouting/kivy/regattascreen.kv
+++ /dev/null
@@ -1,21 +0,0 @@
-#:kivy 1.0
-
-<RegattaScreen>:
-    MDTabs:
-        Tab:
-            title: "Map"
-            content_text: f"This is an example text for {self.title}"
-
-        Tab:
-            title: "Gauges"
-            content_text: f"This is an example text for {self.title}"
-
-        Tab:
-            title: "Logs"
-            content_text: f"This is an example text for {self.title}"
-
-        Tab:
-            title: "Parameters"
-            content_text: f"This is an example text for {self.title}"
-
-        
\ No newline at end of file
diff --git a/gweatherrouting/kivy/settingsscreen.kv b/gweatherrouting/kivy/settingsscreen.kv
deleted file mode 100644
index a86ed27..0000000
--- a/gweatherrouting/kivy/settingsscreen.kv
+++ /dev/null
@@ -1,4 +0,0 @@
-#:kivy 1.0
-
-<SettingsScreen>:
-    ScrollView:
diff --git a/gweatherrouting/kivy/timepickerdialog.kv b/gweatherrouting/kivy/timepickerdialog.kv
deleted file mode 100644
index 6474a39..0000000
--- a/gweatherrouting/kivy/timepickerdialog.kv
+++ /dev/null
@@ -1,29 +0,0 @@
-#:kivy 1.0
-
-<TimePickerDialogContent>:
-    orientation: "vertical"
-    spacing: "12dp"
-    size_hint_y: None
-    height: "120dp"
-
-    MDBoxLayout:
-        orientation: "horizontal"
-
-        MDLabel:
-            id: dateLabel
-            text: root.datetime.strftime("%m/%d/%Y")
-
-        MDRaisedButton:
-            text: "Set date"
-            on_press: root.openDatePicker()
-
-    MDBoxLayout:
-        orientation: "horizontal"
-
-        MDLabel:
-            id: timeLabel
-            text: root.datetime.strftime("%H:%M:%S")
-
-        MDRaisedButton:
-            text: "Set time"
-            on_press: root.openTimePicker()
\ No newline at end of file
diff --git a/gweatherrouting/kivy/timepickerdialog.py b/gweatherrouting/kivy/timepickerdialog.py
deleted file mode 100644
index e4be2ac..0000000
--- a/gweatherrouting/kivy/timepickerdialog.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-from kivymd.uix.boxlayout import MDBoxLayout
-from kivymd.uix.button import MDFlatButton, MDRaisedButton
-from kivymd.uix.dialog import MDDialog
-from kivymd.uix.picker import MDDatePicker, MDTimePicker
-
-
-class TimePickerDialogContent(MDBoxLayout):
-    def __init__(self, datetime):
-        self.datetime = datetime
-        super().__init__()
-
-    def onTimeSave(self, instance, time):
-        self.datetime = self.datetime.replace(hour=time.hour, minute=time.minute)
-        self.ids.timeLabel.text = self.datetime.strftime("%H:%M")
-
-    def onDateSave(self, instance, date, date_range):
-        self.datetime = self.datetime.replace(
-            year=date.year, month=date.month, day=date.day
-        )
-        self.ids.dateLabel.text = self.datetime.strftime("%d/%m/%Y")
-
-    def openTimePicker(self):
-        time_dialog = MDTimePicker()
-        time_dialog.set_time(self.datetime)
-        time_dialog.bind(time=self.onTimeSave)
-        time_dialog.open()
-
-    def openDatePicker(self):
-        date_dialog = MDDatePicker(
-            year=self.datetime.year, month=self.datetime.month, day=self.datetime.day
-        )
-        date_dialog.bind(on_save=self.onDateSave)
-        date_dialog.open()
-
-    def getResult(self):
-        return self.datetime
-
-
-class TimePickerDialog(MDDialog):
-    def __init__(self, datetime):
-        self.pickerContent = TimePickerDialogContent(datetime)
-
-        super().__init__(
-            title="Set Date and Time",
-            type="custom",
-            content_cls=self.pickerContent,
-            buttons=[
-                MDFlatButton(
-                    text="CANCEL",
-                    # theme_text_color="Custom",
-                    # text_color=self.theme_cls.primary_color,
-                ),
-                MDRaisedButton(
-                    text="SET",
-                    # theme_text_color="Custom",
-                    # text_color=self.theme_cls.primary_color,
-                ),
-            ],
-        )
-
-    def getResult(self):
-        return self.pickerContent.getResult()
diff --git a/gweatherrouting/kivy/trackscreen.kv b/gweatherrouting/kivy/trackscreen.kv
deleted file mode 100644
index 3ed32d6..0000000
--- a/gweatherrouting/kivy/trackscreen.kv
+++ /dev/null
@@ -1,25 +0,0 @@
-#:kivy 1.0
-<TrackListItem>:
-    IconLeftWidget:
-        icon: 'map-marker-path'
-
-    RightCheckbox:
-        active: root.visible
-        # on_active:
-        #     root.active = self.active
-        #     root.setEnabled(self.active)
-
-<POIListItem>:
-    IconLeftWidget:
-        icon: 'map-marker'
-
-    RightCheckbox:
-        active: root.visible
-        # on_active:
-        #     root.active = self.active
-        #     root.setEnabled(self.active)
-
-<TrackScreen>:
-    ScrollView:
-        MDList:
-            id: trackList
diff --git a/gweatherrouting/kivy/trackscreen.py b/gweatherrouting/kivy/trackscreen.py
deleted file mode 100644
index 324f504..0000000
--- a/gweatherrouting/kivy/trackscreen.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-from kivy.properties import BooleanProperty, ObjectProperty
-from kivymd.uix.list import IRightBodyTouch, TwoLineIconListItem
-from kivymd.uix.screen import MDScreen
-from kivymd.uix.selectioncontrol import MDCheckbox
-
-
-class TrackListItem(TwoLineIconListItem):
-    visible = BooleanProperty(False)
-    trackManager = ObjectProperty()
-
-    # def setVisibility(self, enabled):
-    # 	self.gribManager.changeState(self.text, enabled)
-
-
-class POIListItem(TwoLineIconListItem):
-    visible = BooleanProperty(False)
-    poiManager = ObjectProperty()
-
-
-class RightCheckbox(IRightBodyTouch, MDCheckbox):
-    pass
-
-
-class TrackScreen(MDScreen):
-    trackManager = None
-    poiManager = None
-
-    def updateTracksAndPois(self):
-        trackList = self.ids.trackList
-
-        for x in self.trackManager:
-            trackList.add_widget(
-                TrackListItem(
-                    text=x.name,
-                    trackManager=self.trackManager,
-                    visible=x.visible,
-                    secondary_text="Distance of %f nm, with %d track points"
-                    % (x.length(), len(x.waypoints)),
-                )
-            )
-
-        for x in self.poiManager:
-            trackList.add_widget(
-                POIListItem(
-                    text=x.name,
-                    poiManager=self.poiManager,
-                    visible=x.visible,
-                    secondary_text="Latitude: %f, Longitude: %f"
-                    % (x.position[0], x.position[1]),
-                )
-            )
-
-    def on_pre_enter(self, *args):
-        self.updateTracksAndPois()
diff --git a/gweatherrouting/main.py b/gweatherrouting/main.py
index 448e55d..a80f69d 100644
--- a/gweatherrouting/main.py
+++ b/gweatherrouting/main.py
@@ -21,12 +21,6 @@
 logger = logging.getLogger("gweatherrouting")
 
 
-def startUIKivy():
-    from .kivy.app import GWeatherRoutingApp
-
-    GWeatherRoutingApp(Core()).run()
-
-
 def startUIGtk():
     import gi
 
@@ -39,8 +33,5 @@ def startUIGtk():
     Gtk.main()
 
 
-# if __name__ == "__main__":
-#     startUIKivy()
-
-# if __name__ == "__main__":
-#     startUIGtk()
+if __name__ == "__main__":
+    startUIGtk()
diff --git a/gweatherroutingkivy/main.py b/gweatherroutingkivy/main.py
deleted file mode 100644
index 573316c..0000000
--- a/gweatherroutingkivy/main.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-
-import logging
-
-from gweatherrouting.core import Core
-
-logger = logging.getLogger("gweatherrouting")
-
-
-def startUIKivy():
-    from gweatherrouting.kivy.app import GWeatherRoutingApp
-
-    GWeatherRoutingApp(Core()).run()
-
-
-if __name__ == "__main__":
-    startUIKivy()
diff --git a/media/mobile.png b/media/mobile.png
deleted file mode 100644
index 702ac5c..0000000
Binary files a/media/mobile.png and /dev/null differ
diff --git a/requirements.kivy.txt b/requirements.kivy.txt
deleted file mode 100644
index af59795..0000000
--- a/requirements.kivy.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-requests
-colorlog
-eccodes==1.3.3
-geojson_utils
-bs4
-vext
-vext.gi
-pyserial
-pynmea2
-gpxpy
-numpy
-kivy[base]
-mapview
-briefcase
-kivymd
-nmeatoolkit
-matplotlib
\ No newline at end of file
diff --git a/setup.kivy.py b/setup.kivy.py
deleted file mode 100644
index 65f3f7b..0000000
--- a/setup.kivy.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2017-2024 Davide Gessa
-"""
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-For detail about GNU see <http://www.gnu.org/licenses/>.
-"""
-from setuptools import setup
-
-setup(
-    name="gweatherrouting",
-    version=0.1,
-    description="",
-    author="Davide Gessa",
-    setup_requires="setuptools",
-    author_email="gessadavide@gmail.com",
-    packages=[
-        "gweatherrouting",
-        "gweatherrouting.core",
-        "gweatherrouting.kivy",
-        "gweatherrouting.kivy.maplayers",
-        "gweatherrouting.common",
-    ],
-    package_data={
-        "gweatherrouting": [
-            "data/*",
-            "data/boats/*",
-            "data/polars/*",
-            "data/symbols/*",
-            "data/s57/*",
-            "kivy/*.kv",
-        ]
-    },
-    entry_points={
-        "console_scripts": ["gweatherrouting-kivy=gweatherrouting.main:startUIKivy"],
-    },
-    options={},
-    executables={},
-    install_requires=open("requirements.kivy.txt", "r").read().split("\n"),
-)
diff --git a/setup.py b/setup.py
index bf6d562..e3e8695 100644
--- a/setup.py
+++ b/setup.py
@@ -39,8 +39,6 @@
         "gweatherrouting.core.utils",
         "gweatherrouting.core.geo",
         "gweatherrouting.common",
-        "gweatherrouting.kivy",
-        "gweatherrouting.kivy.maplayers",
         "gweatherrouting.gtk",
         "gweatherrouting.gtk.maplayers",
         "gweatherrouting.gtk.settings",
@@ -59,7 +57,6 @@
             "gtk/*.glade",
             "gtk/settings/*.glade",
             "gtk/widgets/*.glade",
-            "kivy/*.kv",
         ]
     },
     entry_points={
diff --git a/gweatherrouting/kivy/maplayers/isochronesmaplayer.py b/test/__init__.py
similarity index 100%
rename from gweatherrouting/kivy/maplayers/isochronesmaplayer.py
rename to test/__init__.py
diff --git a/gweatherrouting/kivy/regattascreen.py b/test/main.py
similarity index 83%
rename from gweatherrouting/kivy/regattascreen.py
rename to test/main.py
index 4c8f5f4..8cb053c 100644
--- a/gweatherrouting/kivy/regattascreen.py
+++ b/test/main.py
@@ -13,9 +13,8 @@
 
 For detail about GNU see <http://www.gnu.org/licenses/>.
 """
-from kivymd.uix.screen import MDScreen
 
+import unittest
 
-class RegattaScreen(MDScreen):
-    def on_pre_enter(self, *args):
-        pass
+if __name__ == "__main__":
+    unittest.main()
diff --git a/gweatherrouting/kivy/settingsscreen.py b/test/test_placeholder.py
similarity index 82%
rename from gweatherrouting/kivy/settingsscreen.py
rename to test/test_placeholder.py
index ef83346..eeaa019 100644
--- a/gweatherrouting/kivy/settingsscreen.py
+++ b/test/test_placeholder.py
@@ -13,9 +13,10 @@
 
 For detail about GNU see <http://www.gnu.org/licenses/>.
 """
-from kivymd.uix.screen import MDScreen
 
+import unittest
 
-class SettingsScreen(MDScreen):
-    def on_pre_enter(self, *args):
-        pass
+
+class TestPlaceholder(unittest.TestCase):
+    def test_place_holder(self):
+        self.assertEqual(True, True)
diff --git a/tox.ini b/tox.ini
index 796d49c..45eae88 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = linters,typecheck
+envlist = linters,typecheck,unit-tests
 requires =
     tox>=4
 skipsdist=True
@@ -11,13 +11,21 @@ commands =
     python -I -m build --wheel -C=--build-option=-- -C=--build-option=-- -C=--build-option=-j4
 
 
+[testenv:unit-tests]
+deps =
+    {[testenv]deps}
+    pytest
+    parameterized
+commands =
+    pytest #-rP
+
+
 [testenv:flake8]
 deps =
     ; {[testenv]deps}
     flake8
 commands =
     flake8 ./gweatherrouting
-    flake8 ./gweatherroutingkivy
 
 [testenv:isort]
 deps =
@@ -43,7 +51,6 @@ deps =
     types-python-dateutil
 commands =
     mypy ./gweatherrouting
-    mypy ./gweatherroutingkivy
 
 [testenv:linters]
 deps =