File indexing completed on 2024-05-05 03:55:01

0001 # SPDX-FileCopyrightText: 2021 Volker Krause <vkrause@kde.org>
0002 # SPDX-License-Identifier: LGPL-2.0-or-later
0003 
0004 import os
0005 import requests
0006 from qgis import *
0007 from qgis.core import *
0008 import time
0009 import zipfile
0010 from config import *
0011 
0012 # Download and unpack Shapefiles
0013 class LayerDownloadTask(QgsTask):
0014     def __init__(self, url, dest):
0015         super().__init__('Download Shapefile', QgsTask.CanCancel)
0016         self.url = url
0017         self.dest = dest
0018 
0019     def run(self):
0020         try:
0021             QgsMessageLog.logMessage(f"Downloading and unpacking {self.dest}...", LOG_CATEGORY, Qgis.Info)
0022             if not os.path.exists(self.dest):
0023                 r = requests.get(self.url)
0024                 if r.status_code < 400:
0025                     with open(self.dest, 'wb') as f:
0026                         f.write(r.content)
0027             with zipfile.ZipFile(self.dest, 'r') as z:
0028                 z.extractall('.')
0029             QgsMessageLog.logMessage(f"Downloaded and unpacked {self.dest}.", LOG_CATEGORY, Qgis.Info)
0030         except Exception as e:
0031             QgsMessageLog.logMessage(f"Exception in task: {e}", LOG_CATEGORY, Qgis.Critical)
0032         return True
0033 
0034 
0035 # Load and simplify Shapefile layers
0036 # Simplification is done to massively speed up geometry intersection computation
0037 # (for reference: the original KItinerary tz spatial index took 8h to compute without simplification,
0038 # and about 15 minutes with a Douglas Peucker simplification with a 0.001 threshold, with no practical
0039 # loss of precision
0040 class LoadLayerTask(QgsTask):
0041     def __init__(self, url, fileName, context, layerName):
0042         super().__init__(f"Loading layer {fileName}", QgsTask.CanCancel)
0043         self.layer = None
0044         self.url = url
0045         self.fileName = fileName
0046         self.context = context
0047         self.layerName = layerName
0048         self.downloadTask = LayerDownloadTask(url, fileName)
0049         self.addSubTask(self.downloadTask, [], QgsTask.ParentDependsOnSubTask)
0050 
0051     def run(self):
0052         QgsMessageLog.logMessage(f"Simplifying layer {self.fileName}", LOG_CATEGORY, Qgis.Info)
0053         fullLayer = QgsVectorLayer(self.fileName, f"{self.fileName}-full-resolution", 'ogr')
0054         if not fullLayer.isValid():
0055             QgsMessageLog.logMessage(f"Failed to load layer {self.fileName}!", LOG_CATEGORY, Qgis.Critical)
0056         result = processing.run('qgis:simplifygeometries', {'INPUT': fullLayer, 'METHOD': 0, 'TOLERANCE': 0.001, 'OUTPUT': 'TEMPORARY_OUTPUT' })
0057         self.layer = result['OUTPUT']
0058         self.layer.setName(f"{self.fileName}-simplified")
0059         self.context[self.layerName] = self.layer
0060         QgsMessageLog.logMessage(f"Simplified layer {self.fileName}", LOG_CATEGORY, Qgis.Info)
0061         return True
0062 
0063     def finished(self, result):
0064         QgsProject.instance().addMapLayer(self.layer)
0065 
0066 
0067 # Filter out too small elements in the ISO 3166-2 layer
0068 class Iso3166_2FilterTask(QgsTask):
0069     def __init__(self, context):
0070         super().__init__('Filtering ISO 3166-2 layer', QgsTask.CanCancel)
0071         self.context = context
0072 
0073     def run(self):
0074         QgsMessageLog.logMessage('Filtering ISO 3166-2 layer', LOG_CATEGORY, Qgis.Info)
0075         subdivLayer = self.context['subdivLayer']
0076         toBeRemoved = []
0077         for feature in subdivLayer.getFeatures():
0078             # sic: the key is really "admin_leve" in the input file, due to length restrictions in the Shapefile...
0079             level = feature['admin_leve']
0080             country = feature['ISO3166-2'][:2]
0081             if not isinstance(level, str) or not isinstance(country, str):
0082                 continue
0083             for filter in ISO3166_2_FILTER:
0084                 if int(level) == filter['admin_level'] and country == filter['country']:
0085                     toBeRemoved.append(feature.id())
0086                     break
0087         subdivLayer.dataProvider().deleteFeatures(toBeRemoved)
0088         return True
0089 
0090 
0091 # Setup all data layers we need
0092 class LoadLayersTask(QgsTask):
0093     def __init__(self, context):
0094         super().__init__('Loading layers...', QgsTask.CanCancel)
0095         self.context = context
0096         self.tasks = [
0097             LoadLayerTask(TZDATA_URL, f"timezones.shapefile-{TZDATA_VERSION}.zip", context, 'tzLayer'),
0098             LoadLayerTask(ISO3166_1_URL, f"iso3166-1-boundaries.shp-{ISO3166_1_VERSION}.zip", context, 'countryLayer'),
0099             LoadLayerTask(ISO3166_2_URL, f"iso3166-2-boundaries.shp-{ISO3166_2_VERSION}.zip", context, 'subdivLayer')
0100         ]
0101         for task in self.tasks:
0102             self.addSubTask(task, [], QgsTask.ParentDependsOnSubTask)
0103 
0104         self.filterTask = Iso3166_2FilterTask(context)
0105         self.addSubTask(self.filterTask, [self.tasks[2]], QgsTask.ParentDependsOnSubTask)
0106 
0107     def run(self):
0108         return True