Beispiel #1
0
class Progress:
    def __init__(self, parent):
        self.parent = parent
        self.dialog = None
        self.application = None

    def started(self, title):
        self.dialog = QProgressDialog(title, "Stop", 0, 0, self.parent)
        self.dialog.setCancelButton(None)
        self.dialog.show()
        QCoreApplication.processEvents(QEventLoop.AllEvents)

    def progress(self, msg, percent):
        self.dialog.setLabelText(msg)
        # otherwise KProgressDialog automatically closes itself, sigh
        if percent < 100:
            self.dialog.setValue(percent)
        QCoreApplication.processEvents(QEventLoop.AllEvents)

    def finished(self):
        if self.dialog:
            self.dialog.done(0)
        self.dialog = None
Beispiel #2
0
class Progress:
    def __init__(self, parent):
        self.parent = parent
        self.dialog = None
        self.application = None

    def started(self, title):
        self.dialog = QProgressDialog(title, "Stop", 0, 0, self.parent)
        self.dialog.setCancelButton(None)
        self.dialog.show()
        QCoreApplication.processEvents(QEventLoop.AllEvents)

    def progress(self, msg, percent):
        self.dialog.setLabelText(msg)
        # otherwise KProgressDialog automatically closes itself, sigh
        if percent < 100:
            self.dialog.setValue(percent)
        QCoreApplication.processEvents(QEventLoop.AllEvents)

    def finished(self):
        if self.dialog:
            self.dialog.done(0)
        self.dialog = None
class OsmDownloaderDialog(QDialog, FORM_CLASS):
    """Downloader for OSM data."""

    def __init__(self, parent=None, iface=None):
        """Constructor for import dialog.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE OpenStreetMap Downloader'))

        self.iface = iface
        self.buildings_url = 'http://osm.linfiniti.com/buildings-shp'
        self.building_points_url = \
            'http://osm.linfiniti.com/building-points-shp'
        self.roads_url = 'http://osm.linfiniti.com/roads-shp'

        self.help_context = 'openstreetmap_downloader'
        # creating progress dialog for download
        self.progress_dialog = QProgressDialog(self)
        self.progress_dialog.setAutoClose(False)
        title = self.tr('InaSAFE OpenStreetMap Downloader')
        self.progress_dialog.setWindowTitle(title)
        # Set up context help
        help_button = self.button_box.button(QtGui.QDialogButtonBox.Help)
        help_button.clicked.connect(self.show_help)

        self.show_info()

        # set up the validator for the file name prefix
        expression = QRegExp('^[A-Za-z0-9-_]*$')
        validator = QRegExpValidator(expression, self.filename_prefix)
        self.filename_prefix.setValidator(validator)

        # Set Proxy in webpage
        proxy = get_proxy()
        self.network_manager = QNetworkAccessManager(self)
        if proxy is not None:
            self.network_manager.setProxy(proxy)
        self.restore_state()

        # Setup the rectangle map tool
        self.canvas = iface.mapCanvas()
        self.rectangle_map_tool = \
            RectangleMapTool(self.canvas)
        self.rectangle_map_tool.rectangle_created.connect(
            self.update_extent_from_rectangle)
        self.button_extent_rectangle.clicked.connect(
            self.drag_rectangle_on_map_canvas)

        # Setup pan tool
        self.pan_tool = QgsMapToolPan(self.canvas)
        self.canvas.setMapTool(self.pan_tool)
        self.update_extent_from_map_canvas()

    def show_info(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        header = html_header()
        footer = html_footer()

        string = header

        heading = m.Heading(self.tr('OSM Downloader'), **INFO_STYLE)
        body = self.tr(
            'This tool will fetch building (\'structure\') or road ('
            '\'highway\') data from the OpenStreetMap project for you. '
            'The downloaded data will have InaSAFE keywords defined and a '
            'default QGIS style applied. To use this tool effectively:'
        )
        tips = m.BulletedList()
        tips.add(self.tr(
            'Your current extent, when opening this window, will be used to '
            'determine the area for which you want data to be retrieved.'
            'You can interactively select the area by using the '
            '\'select on map\' button - which will temporarily hide this '
            'window and allow you to drag a rectangle on the map. After you '
            'have finished dragging the rectangle, this window will '
            'reappear.'))
        tips.add(self.tr(
            'Check the output directory is correct. Note that the saved '
            'dataset will be called either roads.shp or buildings.shp (and '
            'associated files).'
        ))
        tips.add(self.tr(
            'By default simple file names will be used (e.g. roads.shp, '
            'buildings.shp). If you wish you can specify a prefix to '
            'add in front of this default name. For example using a prefix '
            'of \'padang-\' will cause the downloaded files to be saved as '
            '\'padang-roads.shp\' and \'padang-buildings.shp\'. Note that '
            'the only allowed prefix characters are A-Z, a-z, 0-9 and the '
            'characters \'-\' and \'_\'. You can leave this blank if you '
            'prefer.'
        ))
        tips.add(self.tr(
            'If a dataset already exists in the output directory it will be '
            'overwritten.'
        ))
        tips.add(self.tr(
            'This tool requires a working internet connection and fetching '
            'buildings or roads will consume your bandwidth.'))
        tips.add(m.Link(
            'http://www.openstreetmap.org/copyright',
            text=self.tr(
                'Downloaded data is copyright OpenStreetMap contributors'
                ' (click for more info).')
        ))
        message = m.Message()
        message.add(heading)
        message.add(body)
        message.add(tips)
        string += message.to_html()
        string += footer

        self.web_view.setHtml(string)

    def restore_state(self):
        """ Read last state of GUI from configuration file."""
        settings = QSettings()
        try:
            last_path = settings.value('directory', type=str)
        except TypeError:
            last_path = ''
        self.output_directory.setText(last_path)

    def save_state(self):
        """ Store current state of GUI to configuration file """
        settings = QSettings()
        settings.setValue('directory', self.output_directory.text())

    def show_help(self):
        """Load the help text for the dialog."""
        show_context_help(self.help_context)

    def update_extent(self, extent):
        """Update extent value in GUI based from an extent.

        :param extent: A list in the form [xmin, ymin, xmax, ymax] where all
            coordinates provided are in Geographic / EPSG:4326.
        :type extent: list
        """
        self.min_longitude.setText(str(extent[0]))
        self.min_latitude.setText(str(extent[1]))
        self.max_longitude.setText(str(extent[2]))
        self.max_latitude.setText(str(extent[3]))

    def update_extent_from_map_canvas(self):
        """Update extent value in GUI based from value in map.

        .. note:: Delegates to update_extent()
        """

        self.groupBox.setTitle(self.tr('Bounding box from the map canvas'))
        # Get the extent as [xmin, ymin, xmax, ymax]
        extent = viewport_geo_array(self.iface.mapCanvas())
        self.update_extent(extent)

    def update_extent_from_rectangle(self):
        """Update extent value in GUI based from the QgsMapTool rectangle.

        .. note:: Delegates to update_extent()
        """

        self.show()
        self.canvas.unsetMapTool(self.rectangle_map_tool)
        self.canvas.setMapTool(self.pan_tool)

        rectangle = self.rectangle_map_tool.rectangle()
        if rectangle:
            self.groupBox.setTitle(self.tr('Bounding box from rectangle'))
            extent = rectangle_geo_array(rectangle, self.iface.mapCanvas())
            self.update_extent(extent)

    def validate_extent(self):
        """Validate the bounding box before user click OK to download.

        :return: True if the bounding box is valid, otherwise False
        :rtype: bool
        """
        min_latitude = float(str(self.min_latitude.text()))
        max_latitude = float(str(self.max_latitude.text()))
        min_longitude = float(str(self.min_longitude.text()))
        max_longitude = float(str(self.max_longitude.text()))

        # min_latitude < max_latitude
        if min_latitude >= max_latitude:
            return False

        # min_longitude < max_longitude
        if min_longitude >= max_longitude:
            return False

        # -90 <= latitude <= 90
        if min_latitude < -90 or min_latitude > 90:
            return False
        if max_latitude < -90 or max_latitude > 90:
            return False

        # -180 <= longitude <= 180
        if min_longitude < -180 or min_longitude > 180:
            return False
        if max_longitude < -180 or max_longitude > 180:
            return False

        return True

    @pyqtSignature('')  # prevents actions being handled twice
    def on_directory_button_clicked(self):
        """Show a dialog to choose directory."""
        # noinspection PyCallByClass,PyTypeChecker
        self.output_directory.setText(QFileDialog.getExistingDirectory(
            self, self.tr('Select download directory')))

    def drag_rectangle_on_map_canvas(self):
        """Hide the dialog and allow the user to draw a rectangle."""

        self.hide()
        self.rectangle_map_tool.reset()
        self.canvas.unsetMapTool(self.pan_tool)
        self.canvas.setMapTool(self.rectangle_map_tool)

    def accept(self):
        """Do osm download and display it in QGIS."""
        error_dialog_title = self.tr('InaSAFE OpenStreetMap Downloader Error')

        # Lock the groupbox
        self.groupBox.setDisabled(True)

        # Validate extent
        valid_flag = self.validate_extent()
        if not valid_flag:
            message = self.tr(
                'The bounding box is not valid. Please make sure it is '
                'valid or check your projection!')
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            display_warning_message_box(self, error_dialog_title, message)
            # Unlock the groupbox
            self.groupBox.setEnabled(True)
            return

        # Get all the feature types
        index = self.feature_type.currentIndex()
        if index == 0:
            feature_types = ['buildings', 'roads', 'building-points']
        elif index == 1:
            feature_types = ['buildings']
        elif index == 2:
            feature_types = ['building-points']
        else:
            feature_types = ['roads']

        try:
            self.save_state()
            self.require_directory()
            for feature_type in feature_types:

                output_directory = self.output_directory.text()
                output_prefix = self.filename_prefix.text()
                overwrite = self.overwrite_checkBox.isChecked()
                output_base_file_path = self.get_output_base_path(
                    output_directory, output_prefix, feature_type, overwrite)

                self.download(feature_type, output_base_file_path)
                try:
                    self.load_shapefile(feature_type, output_base_file_path)
                except FileMissingError as exception:
                    display_warning_message_box(
                        self,
                        error_dialog_title,
                        exception.message)
            self.done(QDialog.Accepted)
            self.rectangle_map_tool.reset()

        except CanceledImportDialogError:
            # don't show anything because this exception raised
            # when user canceling the import process directly
            pass
        except Exception as exception:  # pylint: disable=broad-except
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            display_warning_message_box(
                self, error_dialog_title, exception.message)

            self.progress_dialog.cancel()

        finally:
            # Unlock the groupbox
            self.groupBox.setEnabled(True)

    def get_output_base_path(
            self,
            output_directory,
            output_prefix,
            feature_type,
            overwrite):
        """Get a full base name path to save the shapefile.

        :param output_directory: The directory where to put results.
        :type output_directory: str

        :param output_prefix: The prefix to add for the shapefile.
        :type output_prefix: str

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings', 'building-points' or 'roads' are supported.
        :type feature_type: str

        :param overwrite: Boolean to know if we can overwrite existing files.
        :type overwrite: bool

        :return: The base path.
        :rtype: str
        """
        path = os.path.join(
            output_directory, '%s%s' % (output_prefix, feature_type))

        if overwrite:

            # If a shapefile exists, we must remove it (only the .shp)
            shp = '%s.shp' % path
            if os.path.isfile(shp):
                os.remove(shp)

        else:
            separator = '-'
            suffix = self.get_unique_file_path_suffix(
                '%s.shp' % path, separator)

            if suffix:
                path = os.path.join(output_directory, '%s%s%s%s' % (
                    output_prefix, feature_type, separator, suffix))

        return path

    @staticmethod
    def get_unique_file_path_suffix(file_path, separator='-', i=0):
        """Return the minimum number to suffix the file to not overwrite one.
        Example : /tmp/a.txt exists.
            - With file_path='/tmp/b.txt' will return 0.
            - With file_path='/tmp/a.txt' will return 1 (/tmp/a-1.txt)

        :param file_path: The file to check.
        :type file_path: str

        :param separator: The separator to add before the prefix.
        :type separator: str

        :param i: The minimum prefix to check.
        :type i: int

        :return: The minimum prefix you should add to not overwrite a file.
        :rtype: int
        """

        basename = os.path.splitext(file_path)
        if i != 0:
            file_path_test = os.path.join(
                '%s%s%s%s' % (basename[0], separator, i, basename[1]))
        else:
            file_path_test = file_path

        if os.path.isfile(file_path_test):
            return OsmDownloaderDialog.get_unique_file_path_suffix(
                file_path, separator, i + 1)
        else:
            return i

    def require_directory(self):
        """Ensure directory path entered in dialog exist.

        When the path does not exist, this function will ask the user if he
        want to create it or not.

        :raises: CanceledImportDialogError - when user choose 'No' in
            the question dialog for creating directory.
        """
        path = self.output_directory.text()

        if os.path.exists(path):
            return

        title = self.tr('Directory %s not exist') % path
        question = self.tr(
            'Directory %s not exist. Do you want to create it?') % path
        # noinspection PyCallByClass,PyTypeChecker
        answer = QMessageBox.question(
            self, title, question, QMessageBox.Yes | QMessageBox.No)

        if answer == QMessageBox.Yes:
            if len(path) != 0:
                os.makedirs(path)
            else:
                # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
                display_warning_message_box(
                    self,
                    self.tr('InaSAFE error'),
                    self.tr('Output directory can not be empty.'))
                raise CanceledImportDialogError()
        else:
            raise CanceledImportDialogError()

    def download(self, feature_type, output_base_path):
        """Download shapefiles from Kartoza server.

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings', 'building-points' or 'roads' are supported.
        :type feature_type: str

        :param output_base_path: The base path of the shape file.
        :type output_base_path: str

        :raises: ImportDialogError, CanceledImportDialogError
        """

        # preparing necessary data
        min_longitude = str(self.min_longitude.text())
        min_latitude = str(self.min_latitude.text())
        max_longitude = str(self.max_longitude.text())
        max_latitude = str(self.max_latitude.text())

        box = (
            '{min_longitude},{min_latitude},{max_longitude},'
            '{max_latitude}').format(
                min_longitude=min_longitude,
                min_latitude=min_latitude,
                max_longitude=max_longitude,
                max_latitude=max_latitude
            )
        if feature_type == 'buildings':
            url = '{url}?bbox={box}&qgis_version=2'.format(
                url=self.buildings_url, box=box)
        elif feature_type == 'building-points':
            url = '{url}?bbox={box}&qgis_version=2'.format(
                url=self.building_points_url, box=box)
        else:
            url = '{url}?bbox={box}&qgis_version=2'.format(
                url=self.roads_url, box=box)

        if 'LANG' in os.environ:
            env_lang = os.environ['LANG']
            url += '&lang=%s' % env_lang

        path = tempfile.mktemp('.shp.zip')

        # download and extract it
        self.fetch_zip(url, path, feature_type)
        self.extract_zip(path, output_base_path)

        self.progress_dialog.done(QDialog.Accepted)

    def fetch_zip(self, url, output_path, feature_type):
        """Download zip containing shp file and write to output_path.

        :param url: URL of the zip bundle.
        :type url: str

        :param output_path: Path of output file,
        :type output_path: str

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings', 'building-points' or 'roads' are supported.
        :type feature_type: str

        :raises: ImportDialogError - when network error occurred
        """
        LOGGER.debug('Downloading file from URL: %s' % url)
        LOGGER.debug('Downloading to: %s' % output_path)

        self.progress_dialog.show()

        # Infinite progress bar when the server is fetching data.
        # The progress bar will be updated with the file size later.
        self.progress_dialog.setMaximum(0)
        self.progress_dialog.setMinimum(0)
        self.progress_dialog.setValue(0)

        # Get a pretty label from feature_type, but not translatable
        label_feature_type = feature_type.replace('-', ' ')

        label_text = self.tr('Fetching %s' % label_feature_type)
        self.progress_dialog.setLabelText(label_text)

        # Download Process
        downloader = FileDownloader(
            self.network_manager, url, output_path, self.progress_dialog)
        try:
            result = downloader.download()
        except IOError as ex:
            raise IOError(ex)

        if result[0] is not True:
            _, error_message = result

            if result[0] == QNetworkReply.OperationCanceledError:
                raise CanceledImportDialogError(error_message)
            else:
                raise DownloadError(error_message)

    @staticmethod
    def extract_zip(zip_path, destination_base_path):
        """Extract different extensions to the destination base path.

        Example : test.zip contains a.shp, a.dbf, a.prj
        and destination_base_path = '/tmp/CT-buildings
        Expected result :
            - /tmp/CT-buildings.shp
            - /tmp/CT-buildings.dbf
            - /tmp/CT-buildings.prj

        If two files in the zip with the same extension, only one will be
        copied.

        :param zip_path: The path of the .zip file
        :type zip_path: str

        :param destination_base_path: The destination base path where the shp
            will be written to.
        :type destination_base_path: str

        :raises: IOError - when not able to open path or output_dir does not
            exist.
        """

        import zipfile

        handle = open(zip_path, 'rb')
        zip_file = zipfile.ZipFile(handle)
        for name in zip_file.namelist():
            extension = os.path.splitext(name)[1]
            output_final_path = u'%s%s' % (destination_base_path, extension)
            output_file = open(output_final_path, 'wb')
            output_file.write(zip_file.read(name))
            output_file.close()

        handle.close()

    def load_shapefile(self, feature_type, base_path):
        """Load downloaded shape file to QGIS Main Window.

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings', 'building-points' or 'roads' are supported.
        :type feature_type: str

        :param base_path: The base path of the shape file (without extension).
        :type base_path: str

        :raises: FileMissingError - when buildings.shp not exist
        """

        path = '%s.shp' % base_path

        if not os.path.exists(path):
            message = self.tr(
                '%s does not exist. The server does not have any data for '
                'this extent.' % path)
            raise FileMissingError(message)

        self.iface.addVectorLayer(path, feature_type, 'ogr')

        canvas_srid = self.canvas.mapRenderer().destinationCrs().srsid()
        on_the_fly_projection = self.canvas.hasCrsTransformEnabled()
        if canvas_srid != 4326 and not on_the_fly_projection:
            if QGis.QGIS_VERSION_INT >= 20400:
                self.canvas.setCrsTransformEnabled(True)
            else:
                display_warning_message_bar(
                    self.tr('Enable \'on the fly\''),
                    self.tr(
                        'Your current projection is different than EPSG:4326. '
                        'You should enable \'on the fly\' to display '
                        'correctly your layers')
                    )

    def reject(self):
        """Redefinition of the reject() method
        to remove the rectangle selection tool.
        It will call the super method.
        """

        self.canvas.unsetMapTool(self.rectangle_map_tool)
        self.rectangle_map_tool.reset()

        super(OsmDownloaderDialog, self).reject()
Beispiel #4
0
class OsmDownloader(QDialog, Ui_OsmDownloaderBase):
    """Downloader for OSM data."""

    def __init__(self, parent=None, iface=None):
        """Constructor for import dialog.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE OpenStreetMap Downloader'))

        self.iface = iface
        self.url = "http://osm.linfiniti.com/buildings-shp"

        # creating progress dialog for download
        self.progressDialog = QProgressDialog(self)
        self.progressDialog.setAutoClose(False)
        myTitle = self.tr("InaSAFE OpenStreetMap Downloader")
        self.progressDialog.setWindowTitle(myTitle)
        # Set up context help
        helpButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(helpButton, QtCore.SIGNAL('clicked()'),
                               self.show_help)

        self.show_info()

        self.network_manager = QNetworkAccessManager(self)
        self.restore_state()
        self.update_extent()

    def show_info(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        header = html_header()
        footer = html_footer()

        string = header

        heading = m.Heading(self.tr('OSM Downloader'), **INFO_STYLE)
        body = self.tr(
            'This tool will fetch building (\'structure\') data from the '
            'OpenStreetMap project for you. The downloaded data will have '
            'InaSAFE keywords defined and a default QGIS style applied. To '
            'use this tool effectively:'
        )
        tips = m.BulletedList()
        tips.add(self.tr(
            'Use QGIS to zoom in to the area for which you want building data '
            'to be retrieved.'))
        tips.add(self.tr(
            'Check the output directory is correct. Note that the saved '
            'dataset will be called buildings.shp (and its associated files).'
        ))
        tips.add(self.tr(
            'If a dataset already exists in the output directory it will be '
            'overwritten.'
        ))
        tips.add(self.tr(
            'This tool requires a working internet connection and fetching '
            'buildings will consume your bandwidth.'))
        tips.add(m.Link(
            'http://www.openstreetmap.org/copyright',
            text=self.tr(
                'Downloaded data is copyright OpenStreetMap contributors'
                ' (click for more info).')
        ))
        message = m.Message()
        message.add(heading)
        message.add(body)
        message.add(tips)
        string += message.to_html()
        string += footer

        self.webView.setHtml(string)

    def restore_state(self):
        """ Read last state of GUI from configuration file."""
        mySetting = QSettings()
        self.outDir.setText(mySetting.value('directory'))

    def save_state(self):
        """ Store current state of GUI to configuration file """
        mySetting = QSettings()
        mySetting.setValue('directory', self.outDir.text())

    def show_help(self):
        """Load the help text for the dialog."""
        show_context_help('openstreetmap_downloader')

    def update_extent(self):
        """ Update extent value in GUI based from value in map."""
        myExtent = self.iface.mapCanvas().extent()
        self.minLongitude.setText(str(myExtent.xMinimum()))
        self.minLatitude.setText(str(myExtent.yMinimum()))
        self.maxLongitude.setText(str(myExtent.xMaximum()))
        self.maxLatitude.setText(str(myExtent.yMaximum()))

    @pyqtSignature('')  # prevents actions being handled twice
    def on_pBtnDir_clicked(self):
        """ Show a dialog to choose directory """
        # noinspection PyCallByClass,PyTypeChecker
        self.outDir.setText(QFileDialog.getExistingDirectory(
            self, self.tr("Select download directory")))

    def accept(self):
        """Do osm download and display it in QGIS."""

        try:
            self.save_state()

            self.require_directory()
            self.download()
            self.load_shapefile()
            self.done(QDialog.Accepted)
        except CanceledImportDialogError:
            # don't show anything because this exception raised
            # when user canceling the import process directly
            pass
        except Exception as myEx:
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QMessageBox.warning(
                self,
                self.tr("InaSAFE OpenStreetMap downloader error"),
                str(myEx))

            self.progressDialog.cancel()

    def require_directory(self):
        """Ensure directory path entered in dialog exist.

        When the path does not exist, this function will ask the user if he
        want to create it or not.

        :raises: CanceledImportDialogError - when user choose 'No' in
            the question dialog for creating directory.
        """

        myDir = str(self.outDir.text())

        if os.path.exists(myDir):
            return

        myTitle = self.tr("Directory %s not exist") % myDir
        myQuestion = self.tr(
            "Directory %s not exist. Do you want to create it?"
        ) % myDir
        # noinspection PyCallByClass,PyTypeChecker
        myAnswer = QMessageBox.question(
            self, myTitle,
            myQuestion, QMessageBox.Yes | QMessageBox.No)

        if myAnswer == QMessageBox.Yes:
            os.makedirs(myDir)
        else:
            raise CanceledImportDialogError()

    def download(self):
        """Download shapefiles from Linfinti server.

        :raises: ImportDialogError, CanceledImportDialogError
        """

        ## preparing necessary data
        myMinLng = str(self.minLongitude.text())
        myMinLat = str(self.minLatitude.text())
        myMaxLng = str(self.maxLongitude.text())
        myMaxLat = str(self.maxLatitude.text())

        myCoordinate = "{myMinLng},{myMinLat},{myMaxLng},{myMaxLat}".format(
            myMinLng=myMinLng,
            myMinLat=myMinLat,
            myMaxLng=myMaxLng,
            myMaxLat=myMaxLat
        )

        myShapeUrl = "{url}?bbox={myCoordinate}".format(
            url=self.url,
            myCoordinate=myCoordinate
        )

        myFilePath = tempfile.mktemp('.shp.zip')

        # download and extract it
        self.fetch_zip(myShapeUrl, myFilePath)
        print myFilePath
        print str(self.outDir.text())
        self.extract_zip(myFilePath, str(self.outDir.text()))

        self.progressDialog.done(QDialog.Accepted)

    def fetch_zip(self, url, output_path):
        """Download zip containing shp file and write to output_path.

        :param url: URL of the zip bundle.
        :type url: str

        :param output_path: Path of output file,
        :type output_path: str

        :raises: ImportDialogError - when network error occurred
        """

        self.progressDialog.show()
        self.progressDialog.setMaximum(100)
        self.progressDialog.setValue(0)

        # myLabelText = "Begin downloading shapefile from " \
        #               + "%s ..."
        # self.progressDialog.setLabelText(self.tr(myLabelText) % (url))
        myLabelText = self.tr("Downloading shapefile")
        self.progressDialog.setLabelText(myLabelText)

        myResult = download_url(
            self.network_manager, url, output_path,
            self.progressDialog)

        if myResult is not True:
            _, myErrorMessage = myResult
            raise ImportDialogError(myErrorMessage)

    def extract_zip(self, path, output_dir):
        """Extract all content of a .zip file from path to output_dir.

        :param path: The path of the .zip file
        :type path: str

        :param output_dir: Output directory where the shp will be written to.
        :type output_dir: str

        :raises: IOError - when not able to open path or output_dir does not
            exist.
        """

        import zipfile

        # extract all files...
        myHandle = open(path, 'rb')
        myZip = zipfile.ZipFile(myHandle)
        for myName in myZip.namelist():
            myOutPath = os.path.join(output_dir, myName)
            myOutFile = open(myOutPath, 'wb')
            myOutFile.write(myZip.read(myName))
            myOutFile.close()

        myHandle.close()

    def load_shapefile(self):
        """
        Load downloaded shape file to QGIS Main Window.

        :raises: ImportDialogError - when buildings.shp not exist
        """

        myDir = str(self.outDir.text())
        myPath = os.path.join(myDir, 'buildings.shp')

        if not os.path.exists(myPath):
            myMessage = self.tr(
                "%s don't exist. The server don't have buildings data."
            )
            raise ImportDialogError(myMessage)

        self.iface.addVectorLayer(myPath, 'buildings', 'ogr')
Beispiel #5
0
class OsmDownloader(QDialog, Ui_OsmDownloaderBase):
    """Downloader for OSM data."""

    def __init__(self, parent=None, iface=None):
        """Constructor for import dialog.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE OpenStreetMap Downloader'))

        self.iface = iface
        self.buildings_url = "http://osm.linfiniti.com/buildings-shp"
        self.roads_url = "http://osm.linfiniti.com/roads-shp"

        self.help_context = 'openstreetmap_downloader'
        # creating progress dialog for download
        self.progress_dialog = QProgressDialog(self)
        self.progress_dialog.setAutoClose(False)
        title = self.tr("InaSAFE OpenStreetMap Downloader")
        self.progress_dialog.setWindowTitle(title)
        # Set up context help
        help_button = self.button_box.button(QtGui.QDialogButtonBox.Help)
        help_button.clicked.connect(self.show_help)

        self.show_info()

        # set up the validator for the file name prefix
        expression = QRegExp('^[A-Za-z0-9-_]*$')
        validator = QRegExpValidator(expression)
        self.filename_prefix.setValidator(validator)

        # Set Proxy in webpage
        proxy = get_proxy()
        self.network_manager = QNetworkAccessManager(self)
        if not proxy is None:
            self.network_manager.setProxy(proxy)
        self.restore_state()
        self.update_extent()

    def show_info(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        header = html_header()
        footer = html_footer()

        string = header

        heading = m.Heading(self.tr('OSM Downloader'), **INFO_STYLE)
        body = self.tr(
            'This tool will fetch building (\'structure\') or road ('
            '\'highway\') data from the OpenStreetMap project for you. '
            'The downloaded data will have InaSAFE keywords defined and a '
            'default QGIS style applied. To use this tool effectively:'
        )
        tips = m.BulletedList()
        tips.add(self.tr(
            'Your current extent will be used to determine the area for which '
            'you want data to be retrieved. You can adjust it manually using '
            'the bounding box options below.'))
        tips.add(self.tr(
            'Check the output directory is correct. Note that the saved '
            'dataset will be called either roads.shp or buildings.shp (and '
            'associated files).'
        ))
        tips.add(self.tr(
            'By default simple file names will be used (e.g. roads.shp, '
            'buildings.shp). If you wish you can specify a prefix to '
            'add in front of this default name. For example using a prefix '
            'of \'padang-\' will cause the downloaded files to be saved as '
            '\'padang-roads.shp\' and \'padang-buildings.shp\'. Note that '
            'the only allowed prefix characters are A-Z, a-z, 0-9 and the '
            'characters \'-\' and \'_\'. You can leave this blank if you '
            'prefer.'
        ))
        tips.add(self.tr(
            'If a dataset already exists in the output directory it will be '
            'overwritten.'
        ))
        tips.add(self.tr(
            'This tool requires a working internet connection and fetching '
            'buildings or roads will consume your bandwidth.'))
        tips.add(m.Link(
            'http://www.openstreetmap.org/copyright',
            text=self.tr(
                'Downloaded data is copyright OpenStreetMap contributors'
                ' (click for more info).')
        ))
        message = m.Message()
        message.add(heading)
        message.add(body)
        message.add(tips)
        string += message.to_html()
        string += footer

        self.web_view.setHtml(string)

    def restore_state(self):
        """ Read last state of GUI from configuration file."""
        settings = QSettings()
        try:
            last_path = settings.value('directory', type=str)
        except TypeError:
            last_path = ''
        self.output_directory.setText(last_path)

    def save_state(self):
        """ Store current state of GUI to configuration file """
        settings = QSettings()
        settings.setValue('directory', self.output_directory.text())

    def show_help(self):
        """Load the help text for the dialog."""
        show_context_help(self.help_context)

    def update_extent(self):
        """ Update extent value in GUI based from value in map."""
        # Get the extent as [xmin, ymin, xmax, ymax]
        extent = viewport_geo_array(self.iface.mapCanvas())
        self.min_longitude.setText(str(extent[0]))
        self.min_latitude.setText(str(extent[1]))
        self.max_longitude.setText(str(extent[2]))
        self.max_latitude.setText(str(extent[3]))

    @pyqtSignature('')  # prevents actions being handled twice
    def on_directory_button_clicked(self):
        """ Show a dialog to choose directory """
        # noinspection PyCallByClass,PyTypeChecker
        self.output_directory.setText(QFileDialog.getExistingDirectory(
            self, self.tr("Select download directory")))

    def accept(self):
        """Do osm download and display it in QGIS."""

        index = self.feature_type.currentIndex()
        if index == 0:
            feature_types = ['buildings', 'roads']
        elif index == 1:
            feature_types = ['buildings']
        else:
            feature_types = ['roads']

        try:
            self.save_state()
            self.require_directory()
            for feature_type in feature_types:
                self.download(feature_type)
                self.load_shapefile(feature_type)
            self.done(QDialog.Accepted)
        except CanceledImportDialogError:
            # don't show anything because this exception raised
            # when user canceling the import process directly
            pass
        except Exception as myEx:
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QMessageBox.warning(
                self,
                self.tr("InaSAFE OpenStreetMap downloader error"),
                str(myEx))

            self.progress_dialog.cancel()

    def require_directory(self):
        """Ensure directory path entered in dialog exist.

        When the path does not exist, this function will ask the user if he
        want to create it or not.

        :raises: CanceledImportDialogError - when user choose 'No' in
            the question dialog for creating directory.
        """
        path = str(self.output_directory.text())

        if os.path.exists(path):
            return

        title = self.tr("Directory %s not exist") % path
        question = self.tr(
            "Directory %s not exist. Do you want to create it?"
        ) % path
        # noinspection PyCallByClass,PyTypeChecker
        answer = QMessageBox.question(
            self, title,
            question, QMessageBox.Yes | QMessageBox.No)

        if answer == QMessageBox.Yes:
            if len(path) != 0:
                os.makedirs(path)
            else:
            # noinspection PyCallByClass,PyTypeChecker, PyArgumentList
                QMessageBox.warning(
                    self,
                    self.tr('InaSAFE error'),
                    self.tr('Output directory can not be empty.'))
                raise CanceledImportDialogError()
        else:
            raise CanceledImportDialogError()

    def download(self, feature_type):
        """Download shapefiles from Linfinti server.

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings' or 'roads' are supported.
        :type feature_type: str

        :raises: ImportDialogError, CanceledImportDialogError
        """

        ## preparing necessary data
        min_longitude = str(self.min_longitude.text())
        min_latitude = str(self.min_latitude.text())
        max_longitude = str(self.max_longitude.text())
        max_latitude = str(self.max_latitude.text())

        box = (
            '{min_longitude},{min_latitude},{max_longitude},'
            '{max_latitude}').format(
                min_longitude=min_longitude,
                min_latitude=min_latitude,
                max_longitude=max_longitude,
                max_latitude=max_latitude
            )
        output_prefix = self.filename_prefix.text()
        if feature_type == 'buildings':
            url = "{url}?bbox={box}&qgis_version=2".format(
                url=self.buildings_url, box=box)
        else:
            url = "{url}?bbox={box}&qgis_version=2".format(
                url=self.roads_url, box=box)

        if output_prefix is not None:
            url += '&output_prefix=%s' % output_prefix

        path = tempfile.mktemp('.shp.zip')

        # download and extract it
        self.fetch_zip(url, path)
        #print path
        #print str(self.output_directory.text())

        self.extract_zip(path, str(self.output_directory.text()))

        self.progress_dialog.done(QDialog.Accepted)

    def fetch_zip(self, url, output_path):
        """Download zip containing shp file and write to output_path.

        :param url: URL of the zip bundle.
        :type url: str

        :param output_path: Path of output file,
        :type output_path: str

        :raises: ImportDialogError - when network error occurred
        """
        LOGGER.debug('Downloading file from URL: %s' % url)
        LOGGER.debug('Downloading to: %s' % output_path)

        self.progress_dialog.show()
        self.progress_dialog.setMaximum(100)
        self.progress_dialog.setValue(0)

        # label_text = "Begin downloading shapefile from " \
        #               + "%s ..."
        # self.progress_dialog.setLabelText(self.tr(label_text) % (url))
        label_text = self.tr("Downloading shapefile")
        self.progress_dialog.setLabelText(label_text)

        result = download_url(
            self.network_manager, url, output_path,
            self.progress_dialog)

        if result[0] is not True:
            _, error_message = result
            raise ImportDialogError(error_message)

    @staticmethod
    def extract_zip(path, output_dir):
        """Extract all content of a .zip file from path to output_dir.

        :param path: The path of the .zip file
        :type path: str

        :param output_dir: Output directory where the shp will be written to.
        :type output_dir: str

        :raises: IOError - when not able to open path or output_dir does not
            exist.
        """

        import zipfile

        # extract all files...
        handle = open(path, 'rb')
        zip_file = zipfile.ZipFile(handle)
        for name in zip_file.namelist():
            output_path = os.path.join(output_dir, name)
            output_file = open(output_path, 'wb')
            output_file.write(zip_file.read(name))
            output_file.close()

        handle.close()

    def load_shapefile(self, feature_type):
        """
        Load downloaded shape file to QGIS Main Window.

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings' or 'roads' are supported.
        :type feature_type: str

        :raises: ImportDialogError - when buildings.shp not exist
        """
        output_prefix = self.filename_prefix.text()
        path = str(self.output_directory.text())
        path = os.path.join(path, '%s%s.shp' % (output_prefix, feature_type))

        if not os.path.exists(path):
            message = self.tr(
                "%s don't exist. The server doesn't have any data."
            )
            raise ImportDialogError(message)

        self.iface.addVectorLayer(path, feature_type, 'ogr')
Beispiel #6
0
class ImportDialog(QDialog, Ui_ImportDialogBase):

    def __init__(self, theParent=None, theIface=None):
        """Constructor for import dialog.

        Args:
           * theParent - Optional widget to use as parent
           * theIface - an instance of QGisInterface
        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """
        QDialog.__init__(self, theParent)
        self.parent = theParent
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE OpenStreetMap Downloader'))

        self.iface = theIface
        self.url = "http://osm.linfiniti.com/buildings-shp"

        ## region coordinate: (latitude, longtitude, zoom_level)
        self.regionExtent = {
            '0': [18.87685, -71.493, 6],     # haiti
            '1': [-2.5436300, 116.8887, 3],  # indonesia
            '2': [1.22449, 15.40999, 2],     # africa
            '3': [34.05, 56.55, 3],          # middle east
            '4': [12.98855, 121.7166, 4],    # philipine
        }

        # creating progress dialog for download
        self.progressDialog = QProgressDialog(self)
        self.progressDialog.setAutoClose(False)
        myTitle = self.tr("InaSAFE OpenStreetMap Downloader")
        self.progressDialog.setWindowTitle(myTitle)

        ## set map parameter based on placeholder self.map widget
        theMap = InasafeLightMaps(self.gbxMap)
        theMap.setGeometry(self.map.geometry())
        theMap.setSizePolicy(self.map.sizePolicy())
        self.map = theMap

        self.nam = QNetworkAccessManager(self)

        self.setupOptions()

        self.restoreState()

        self.cbxRegion.currentIndexChanged.connect(self.regionChanged)
        self.map.m_normalMap.updated.connect(self.updateExtent)

    def regionChanged(self, theIndex):
        """ Slot that called when region combo box changed.
        Params:
            theIndex - index of combo box
        """
        myRegionIndex = str(self.cbxRegion.itemData(theIndex).toString())
        myCenter = self.regionExtent[myRegionIndex]
        self.map.setCenter(myCenter[0], myCenter[1], myCenter[2])

    # pylint: disable=W0613
    def resizeEvent(self, theEvent):
        """ Function that called when resize event occurred.
        Params:
            theEvent - QEvent instance. Not used.
        """
        self.map.resize(self.gbxMap.width() - 30, self.gbxMap.height() - 30)
    # pylint: disable=W0613

    def setupOptions(self):
        """ Set the content of combo box for region and preset """
        self.cbxRegion.insertItem(0, 'Indonesia', 1)
        self.cbxRegion.insertItem(1, 'Africa', 2)
        self.cbxRegion.insertItem(2, 'Philippines', 4)
        self.cbxRegion.insertItem(3, 'Central Asia/Middle East', 3)
        self.cbxRegion.insertItem(4, 'Haiti', 0)

        self.cbxPreset.insertItem(0, self.tr('Buildings'), 'building')
        self.cbxPreset.insertItem(0, self.tr('Highway'), 'highway')

    def restoreState(self):
        """ Read last state of GUI from configuration file """
        mySetting = QSettings()

        myRegion = mySetting.value('region').toInt()
        if myRegion[1] is True:
            self.cbxRegion.setCurrentIndex(myRegion[0])

        myPreset = mySetting.value('preset').toInt()
        if myPreset[1] is True:
            self.cbxPreset.setCurrentIndex(myPreset[0])

        self.outDir.setText(mySetting.value('directory').toString())

        myZoomLevel = mySetting.value('zoom_level').toInt()
        myLatitude = mySetting.value('latitude').toDouble()
        myLongitude = mySetting.value('longitude').toDouble()

        if myZoomLevel[1] is True:
            self.map.setCenter(myLatitude[0], myLongitude[0], myZoomLevel[0])
        else:
            # just set to indonesia extent
            myCenter = self.regionExtent['1']
            self.map.setCenter(myCenter[0], myCenter[1], myCenter[2])

    def saveState(self):
        """ Store current state of GUI to configuration file """
        mySetting = QSettings()
        mySetting.setValue('region', self.cbxRegion.currentIndex())
        mySetting.setValue('preset', self.cbxPreset.currentIndex())
        mySetting.setValue('directory', self.outDir.text())

        mySetting.setValue('zoom_level', self.map.getZoomLevel())
        myCenter = self.map.getCenter()
        mySetting.setValue('latitude', myCenter[0])
        mySetting.setValue('longitude', myCenter[1])

    def updateExtent(self):
        """ Update extent value in GUI based from value in map widget"""
        myExtent = self.map.getExtent()
        self.minLongitude.setText(str(myExtent[1]))
        self.minLatitude.setText(str(myExtent[0]))
        self.maxLongitude.setText(str(myExtent[3]))
        self.maxLatitude.setText(str(myExtent[2]))

    @pyqtSignature('')  # prevents actions being handled twice
    def on_pBtnDir_clicked(self):
        """ Show a dialog to choose directory """
        self.outDir.setText(QFileDialog.getExistingDirectory(
            self, self.tr("Select Directory")))

    def accept(self):
        """ Do import process """

        try:
            self.saveState()

            self.ensureDirExist()
            self.doImport()
            self.loadShapeFile()
            self.done(QDialog.Accepted)
        except CanceledImportDialogError:
            # don't show anything because this exception raised
            # when user canceling the import process directly
            pass
        except Exception as myEx:
            QMessageBox.warning(
                self,
                self.tr("InaSAFE OpenStreetMap Downloader Error"),
                str(myEx))

            self.progressDialog.cancel()

    def ensureDirExist(self):
        """
        Ensure directory path entered in dialog exist.
        When the path is not exist, this function will
        ask the user if he want to create it or not.

        Raises:
            CanceledImportDialogError - when user choose 'No'
                        in question dialog for creating directory.
        """

        myDir = str(self.outDir.text())

        if os.path.exists(myDir):
            return

        myTitle = self.tr("Directory %1 not exist").arg(myDir)
        myQuestion = self.tr(
            "Directory %1 not exist. Are you want to create it?"
        ).arg(myDir)
        myAnswer = QMessageBox.question(
            self, myTitle,
            myQuestion, QMessageBox.Yes | QMessageBox.No)

        if myAnswer == QMessageBox.Yes:
            os.makedirs(myDir)
        else:
            raise CanceledImportDialogError()

    def doImport(self):
        """
        Import shape files from Linfinti.
        Raises:
            * ImportDialogError - when network error occurred
            * CanceledImportDialogError - when user press cancel button
        """

        ## preparing necessary data
        myMinLng = str(self.minLongitude.text())
        myMinLat = str(self.minLatitude.text())
        myMaxLng = str(self.maxLongitude.text())
        myMaxLat = str(self.maxLatitude.text())

        myCurrentIndex = self.cbxPreset.currentIndex()
        myType = str(self.cbxPreset.itemData(myCurrentIndex).toString())

        myCoordinate = "{myMinLng},{myMinLat},{myMaxLng},{myMaxLat}".format(
            myMinLng=myMinLng,
            myMinLat=myMinLat,
            myMaxLng=myMaxLng,
            myMaxLat=myMaxLat
        )

        myShapeUrl = "{url}?bbox={myCoordinate}&obj={type}".format(
            url=self.url,
            myCoordinate=myCoordinate,
            type=myType
        )

        myFilePath = tempfile.mktemp('.shp.zip')

        # download and extract it
        self.downloadShapeFile(myShapeUrl, myFilePath)
        self.extractZip(myFilePath, str(self.outDir.text()))

        self.progressDialog.done(QDialog.Accepted)

    def downloadShapeFile(self, theUrl, theOutput):
        """
        Download shape file from theUrl and write to theOutput.
        Params:
            * theUrl - URL of shape file
            * theOutput - path of output file
        Raises:
            * ImportDialogError - when network error occurred
        """

        self.progressDialog.show()
        self.progressDialog.setMaximum(100)
        self.progressDialog.setValue(0)

        # myLabelText = "Begin downloading shapefile from " \
        #               + "%1 ..."
        # self.progressDialog.setLabelText(self.tr(myLabelText).arg(theUrl))
        myLabelText = self.tr("Begin downloading shapefile")
        self.progressDialog.setLabelText(myLabelText)

        myResult = httpDownload(self.nam, theUrl, theOutput,
                                self.progressDialog)

        if myResult is not True:
            _, myErrorMessage = myResult
            raise ImportDialogError(myErrorMessage)

    def extractZip(self, thePath, theOutDir):
        """
        Extract all content of zip file from thePath to theOutDir.
        Args:
           * thePath - the path of zip file
           * theOutDir - output directory
        Raises:
            IOError - when cannot open thePath or theOutDir is not exist.
        """

        import zipfile

        ## extract all files...
        myHandle = open(thePath, 'rb')
        myZip = zipfile.ZipFile(myHandle)
        for myName in myZip.namelist():
            myOutPath = os.path.join(theOutDir, myName)
            myOutFile = open(myOutPath, 'wb')
            myOutFile.write(myZip.read(myName))
            myOutFile.close()

        myHandle.close()

    def loadShapeFile(self):
        """
        Load downloaded shape file to QGIS Main Window.

        Raises:
            ImportDialogError - when buildings.shp not exist
        """

        myDir = str(self.outDir.text())
        myPath = os.path.join(myDir, 'buildings.shp')

        if not os.path.exists(myPath):
            myMessage = self.tr(
                "%s don't exist. The server don't have buildings data."
            )
            raise ImportDialogError(myMessage)

        self.iface.addVectorLayer(myPath, 'buildings', 'ogr')
class OsmDownloaderDialog(QDialog, FORM_CLASS):
    """Downloader for OSM data."""

    def __init__(self, parent=None, iface=None):
        """Constructor for import dialog.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE OpenStreetMap Downloader'))

        self.iface = iface
        self.buildings_url = "http://osm.linfiniti.com/buildings-shp"
        self.roads_url = "http://osm.linfiniti.com/roads-shp"

        self.help_context = 'openstreetmap_downloader'
        # creating progress dialog for download
        self.progress_dialog = QProgressDialog(self)
        self.progress_dialog.setAutoClose(False)
        title = self.tr("InaSAFE OpenStreetMap Downloader")
        self.progress_dialog.setWindowTitle(title)
        # Set up context help
        help_button = self.button_box.button(QtGui.QDialogButtonBox.Help)
        help_button.clicked.connect(self.show_help)

        self.show_info()

        # set up the validator for the file name prefix
        expression = QRegExp('^[A-Za-z0-9-_]*$')
        validator = QRegExpValidator(expression, self.filename_prefix)
        self.filename_prefix.setValidator(validator)

        # Set Proxy in webpage
        proxy = get_proxy()
        self.network_manager = QNetworkAccessManager(self)
        if proxy is not None:
            self.network_manager.setProxy(proxy)
        self.restore_state()
        self.update_extent()

    def show_info(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        header = html_header()
        footer = html_footer()

        string = header

        heading = m.Heading(self.tr('OSM Downloader'), **INFO_STYLE)
        body = self.tr(
            'This tool will fetch building (\'structure\') or road ('
            '\'highway\') data from the OpenStreetMap project for you. '
            'The downloaded data will have InaSAFE keywords defined and a '
            'default QGIS style applied. To use this tool effectively:'
        )
        tips = m.BulletedList()
        tips.add(self.tr(
            'Your current extent will be used to determine the area for which '
            'you want data to be retrieved. You can adjust it manually using '
            'the bounding box options below.'))
        tips.add(self.tr(
            'Check the output directory is correct. Note that the saved '
            'dataset will be called either roads.shp or buildings.shp (and '
            'associated files).'
        ))
        tips.add(self.tr(
            'By default simple file names will be used (e.g. roads.shp, '
            'buildings.shp). If you wish you can specify a prefix to '
            'add in front of this default name. For example using a prefix '
            'of \'padang-\' will cause the downloaded files to be saved as '
            '\'padang-roads.shp\' and \'padang-buildings.shp\'. Note that '
            'the only allowed prefix characters are A-Z, a-z, 0-9 and the '
            'characters \'-\' and \'_\'. You can leave this blank if you '
            'prefer.'
        ))
        tips.add(self.tr(
            'If a dataset already exists in the output directory it will be '
            'overwritten.'
        ))
        tips.add(self.tr(
            'This tool requires a working internet connection and fetching '
            'buildings or roads will consume your bandwidth.'))
        tips.add(m.Link(
            'http://www.openstreetmap.org/copyright',
            text=self.tr(
                'Downloaded data is copyright OpenStreetMap contributors'
                ' (click for more info).')
        ))
        message = m.Message()
        message.add(heading)
        message.add(body)
        message.add(tips)
        string += message.to_html()
        string += footer

        self.web_view.setHtml(string)

    def restore_state(self):
        """ Read last state of GUI from configuration file."""
        settings = QSettings()
        try:
            last_path = settings.value('directory', type=str)
        except TypeError:
            last_path = ''
        self.output_directory.setText(last_path)

    def save_state(self):
        """ Store current state of GUI to configuration file """
        settings = QSettings()
        settings.setValue('directory', self.output_directory.text())

    def show_help(self):
        """Load the help text for the dialog."""
        show_context_help(self.help_context)

    def update_extent(self):
        """ Update extent value in GUI based from value in map."""
        # Get the extent as [xmin, ymin, xmax, ymax]
        extent = viewport_geo_array(self.iface.mapCanvas())
        self.min_longitude.setText(str(extent[0]))
        self.min_latitude.setText(str(extent[1]))
        self.max_longitude.setText(str(extent[2]))
        self.max_latitude.setText(str(extent[3]))

    def validate_extent(self):
        """Validate the bounding box before user click OK to download.

        :return: True if the bounding box is valid, otherwise False
        :rtype: bool
        """
        min_latitude = float(str(self.min_latitude.text()))
        max_latitude = float(str(self.max_latitude.text()))
        min_longitude = float(str(self.min_longitude.text()))
        max_longitude = float(str(self.max_longitude.text()))

        # min_latitude < max_latitude
        if min_latitude >= max_latitude:
            return False

        # min_longitude < max_longitude
        if min_longitude >= max_longitude:
            return False

        # -90 <= latitude <= 90
        if min_latitude < -90 or min_latitude > 90:
            return False
        if max_latitude < -90 or max_latitude > 90:
            return False

        # -180 <= longitude <= 180
        if min_longitude < -180 or min_longitude > 180:
            return False
        if max_longitude < -180 or max_longitude > 180:
            return False

        return True

    @pyqtSignature('')  # prevents actions being handled twice
    def on_directory_button_clicked(self):
        """ Show a dialog to choose directory """
        # noinspection PyCallByClass,PyTypeChecker
        self.output_directory.setText(QFileDialog.getExistingDirectory(
            self, self.tr("Select download directory")))

    def accept(self):
        """Do osm download and display it in QGIS."""
        error_dialog_title = self.tr('InaSAFE OpenStreetMap Downloader Error')

        # Validate extent
        valid_flag = self.validate_extent()
        if not valid_flag:
            message = self.tr(
                'The bounding box is not valid. Please make sure it is '
                'valid or check your projection!')
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QMessageBox.warning(self, error_dialog_title, message)
            return

        # Get all the feature types
        index = self.feature_type.currentIndex()
        if index == 0:
            feature_types = ['buildings', 'roads']
        elif index == 1:
            feature_types = ['buildings']
        else:
            feature_types = ['roads']

        try:
            self.save_state()
            self.require_directory()
            for feature_type in feature_types:
                self.download(feature_type)
                self.load_shapefile(feature_type)
            self.done(QDialog.Accepted)
        except CanceledImportDialogError:
            # don't show anything because this exception raised
            # when user canceling the import process directly
            pass
        except Exception as exception:  # pylint: disable=broad-except
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QMessageBox.warning(self, error_dialog_title, str(exception))

            self.progress_dialog.cancel()

    def require_directory(self):
        """Ensure directory path entered in dialog exist.

        When the path does not exist, this function will ask the user if he
        want to create it or not.

        :raises: CanceledImportDialogError - when user choose 'No' in
            the question dialog for creating directory.
        """
        path = str(self.output_directory.text())

        if os.path.exists(path):
            return

        title = self.tr("Directory %s not exist") % path
        question = self.tr(
            "Directory %s not exist. Do you want to create it?") % path
        # noinspection PyCallByClass,PyTypeChecker
        answer = QMessageBox.question(
            self, title, question, QMessageBox.Yes | QMessageBox.No)

        if answer == QMessageBox.Yes:
            if len(path) != 0:
                os.makedirs(path)
            else:
                # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
                QMessageBox.warning(
                    self,
                    self.tr('InaSAFE error'),
                    self.tr('Output directory can not be empty.'))
                raise CanceledImportDialogError()
        else:
            raise CanceledImportDialogError()

    def download(self, feature_type):
        """Download shapefiles from Linfiniti server.

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings' or 'roads' are supported.
        :type feature_type: str

        :raises: ImportDialogError, CanceledImportDialogError
        """

        # preparing necessary data
        min_longitude = str(self.min_longitude.text())
        min_latitude = str(self.min_latitude.text())
        max_longitude = str(self.max_longitude.text())
        max_latitude = str(self.max_latitude.text())

        box = (
            '{min_longitude},{min_latitude},{max_longitude},'
            '{max_latitude}').format(
                min_longitude=min_longitude,
                min_latitude=min_latitude,
                max_longitude=max_longitude,
                max_latitude=max_latitude
            )
        output_prefix = self.filename_prefix.text()
        if feature_type == 'buildings':
            url = "{url}?bbox={box}&qgis_version=2".format(
                url=self.buildings_url, box=box)
        else:
            url = "{url}?bbox={box}&qgis_version=2".format(
                url=self.roads_url, box=box)

        if output_prefix is not None:
            url += '&output_prefix=%s' % output_prefix

        path = tempfile.mktemp('.shp.zip')

        # download and extract it
        self.fetch_zip(url, path)
        self.extract_zip(path, str(self.output_directory.text()))

        self.progress_dialog.done(QDialog.Accepted)

    def fetch_zip(self, url, output_path):
        """Download zip containing shp file and write to output_path.

        :param url: URL of the zip bundle.
        :type url: str

        :param output_path: Path of output file,
        :type output_path: str

        :raises: ImportDialogError - when network error occurred
        """
        LOGGER.debug('Downloading file from URL: %s' % url)
        LOGGER.debug('Downloading to: %s' % output_path)

        self.progress_dialog.show()
        self.progress_dialog.setMaximum(100)
        self.progress_dialog.setValue(0)

        label_text = self.tr("Downloading shapefile")
        self.progress_dialog.setLabelText(label_text)

        # Download Process
        downloader = FileDownloader(
            self.network_manager, url, output_path, self.progress_dialog)
        try:
            result = downloader.download()
        except IOError as ex:
            raise IOError(ex)

        if result[0] is not True:
            _, error_message = result
            raise DownloadError(error_message)

    @staticmethod
    def extract_zip(path, output_dir):
        """Extract all content of a .zip file from path to output_dir.

        :param path: The path of the .zip file
        :type path: str

        :param output_dir: Output directory where the shp will be written to.
        :type output_dir: str

        :raises: IOError - when not able to open path or output_dir does not
            exist.
        """

        import zipfile

        # extract all files...
        handle = open(path, 'rb')
        zip_file = zipfile.ZipFile(handle)
        for name in zip_file.namelist():
            output_path = os.path.join(output_dir, name)
            output_file = open(output_path, 'wb')
            output_file.write(zip_file.read(name))
            output_file.close()

        handle.close()

    def load_shapefile(self, feature_type):
        """Load downloaded shape file to QGIS Main Window.

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings' or 'roads' are supported.
        :type feature_type: str

        :raises: ImportDialogError - when buildings.shp not exist
        """
        output_prefix = self.filename_prefix.text()
        path = str(self.output_directory.text())
        path = os.path.join(path, '%s%s.shp' % (output_prefix, feature_type))

        if not os.path.exists(path):
            message = self.tr(
                "%s don't exist. The server doesn't have any data.")
            raise ImportDialogError(message)

        self.iface.addVectorLayer(path, feature_type, 'ogr')
class OsmDownloaderDialog(QDialog, FORM_CLASS):
    """Downloader for OSM data."""
    def __init__(self, parent=None, iface=None):
        """Constructor for import dialog.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE OpenStreetMap Downloader'))

        self.iface = iface
        self.buildings_url = 'http://osm.linfiniti.com/buildings-shp'
        self.building_points_url = \
            'http://osm.linfiniti.com/building-points-shp'
        self.roads_url = 'http://osm.linfiniti.com/roads-shp'

        self.help_context = 'openstreetmap_downloader'
        # creating progress dialog for download
        self.progress_dialog = QProgressDialog(self)
        self.progress_dialog.setAutoClose(False)
        title = self.tr('InaSAFE OpenStreetMap Downloader')
        self.progress_dialog.setWindowTitle(title)
        # Set up context help
        help_button = self.button_box.button(QtGui.QDialogButtonBox.Help)
        help_button.clicked.connect(self.show_help)

        self.show_info()

        # set up the validator for the file name prefix
        expression = QRegExp('^[A-Za-z0-9-_]*$')
        validator = QRegExpValidator(expression, self.filename_prefix)
        self.filename_prefix.setValidator(validator)

        # Set Proxy in webpage
        proxy = get_proxy()
        self.network_manager = QNetworkAccessManager(self)
        if proxy is not None:
            self.network_manager.setProxy(proxy)
        self.restore_state()

        # Setup the rectangle map tool
        self.canvas = iface.mapCanvas()
        self.rectangle_map_tool = \
            RectangleMapTool(self.canvas)
        self.rectangle_map_tool.rectangle_created.connect(
            self.update_extent_from_rectangle)
        self.button_extent_rectangle.clicked.connect(
            self.drag_rectangle_on_map_canvas)

        # Setup pan tool
        self.pan_tool = QgsMapToolPan(self.canvas)
        self.canvas.setMapTool(self.pan_tool)
        self.update_extent_from_map_canvas()

    def show_info(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        header = html_header()
        footer = html_footer()

        string = header

        heading = m.Heading(self.tr('OSM Downloader'), **INFO_STYLE)
        body = self.tr(
            'This tool will fetch building (\'structure\') or road ('
            '\'highway\') data from the OpenStreetMap project for you. '
            'The downloaded data will have InaSAFE keywords defined and a '
            'default QGIS style applied. To use this tool effectively:')
        tips = m.BulletedList()
        tips.add(
            self.
            tr('Your current extent, when opening this window, will be used to '
               'determine the area for which you want data to be retrieved.'
               'You can interactively select the area by using the '
               '\'select on map\' button - which will temporarily hide this '
               'window and allow you to drag a rectangle on the map. After you '
               'have finished dragging the rectangle, this window will '
               'reappear.'))
        tips.add(
            self.
            tr('Check the output directory is correct. Note that the saved '
               'dataset will be called either roads.shp or buildings.shp (and '
               'associated files).'))
        tips.add(
            self.
            tr('By default simple file names will be used (e.g. roads.shp, '
               'buildings.shp). If you wish you can specify a prefix to '
               'add in front of this default name. For example using a prefix '
               'of \'padang-\' will cause the downloaded files to be saved as '
               '\'padang-roads.shp\' and \'padang-buildings.shp\'. Note that '
               'the only allowed prefix characters are A-Z, a-z, 0-9 and the '
               'characters \'-\' and \'_\'. You can leave this blank if you '
               'prefer.'))
        tips.add(
            self.
            tr('If a dataset already exists in the output directory it will be '
               'overwritten.'))
        tips.add(
            self.
            tr('This tool requires a working internet connection and fetching '
               'buildings or roads will consume your bandwidth.'))
        tips.add(
            m.Link(
                'http://www.openstreetmap.org/copyright',
                text=self.tr(
                    'Downloaded data is copyright OpenStreetMap contributors'
                    ' (click for more info).')))
        message = m.Message()
        message.add(heading)
        message.add(body)
        message.add(tips)
        string += message.to_html()
        string += footer

        self.web_view.setHtml(string)

    def restore_state(self):
        """ Read last state of GUI from configuration file."""
        settings = QSettings()
        try:
            last_path = settings.value('directory', type=str)
        except TypeError:
            last_path = ''
        self.output_directory.setText(last_path)

    def save_state(self):
        """ Store current state of GUI to configuration file """
        settings = QSettings()
        settings.setValue('directory', self.output_directory.text())

    def show_help(self):
        """Load the help text for the dialog."""
        show_context_help(self.help_context)

    def update_extent(self, extent):
        """Update extent value in GUI based from an extent.

        :param extent: A list in the form [xmin, ymin, xmax, ymax] where all
            coordinates provided are in Geographic / EPSG:4326.
        :type extent: list
        """
        self.min_longitude.setText(str(extent[0]))
        self.min_latitude.setText(str(extent[1]))
        self.max_longitude.setText(str(extent[2]))
        self.max_latitude.setText(str(extent[3]))

    def update_extent_from_map_canvas(self):
        """Update extent value in GUI based from value in map.

        .. note:: Delegates to update_extent()
        """

        self.groupBox.setTitle(self.tr('Bounding box from the map canvas'))
        # Get the extent as [xmin, ymin, xmax, ymax]
        extent = viewport_geo_array(self.iface.mapCanvas())
        self.update_extent(extent)

    def update_extent_from_rectangle(self):
        """Update extent value in GUI based from the QgsMapTool rectangle.

        .. note:: Delegates to update_extent()
        """

        self.show()
        self.canvas.unsetMapTool(self.rectangle_map_tool)
        self.canvas.setMapTool(self.pan_tool)

        rectangle = self.rectangle_map_tool.rectangle()
        if rectangle:
            self.groupBox.setTitle(self.tr('Bounding box from rectangle'))
            extent = rectangle_geo_array(rectangle, self.iface.mapCanvas())
            self.update_extent(extent)

    def validate_extent(self):
        """Validate the bounding box before user click OK to download.

        :return: True if the bounding box is valid, otherwise False
        :rtype: bool
        """
        min_latitude = float(str(self.min_latitude.text()))
        max_latitude = float(str(self.max_latitude.text()))
        min_longitude = float(str(self.min_longitude.text()))
        max_longitude = float(str(self.max_longitude.text()))

        # min_latitude < max_latitude
        if min_latitude >= max_latitude:
            return False

        # min_longitude < max_longitude
        if min_longitude >= max_longitude:
            return False

        # -90 <= latitude <= 90
        if min_latitude < -90 or min_latitude > 90:
            return False
        if max_latitude < -90 or max_latitude > 90:
            return False

        # -180 <= longitude <= 180
        if min_longitude < -180 or min_longitude > 180:
            return False
        if max_longitude < -180 or max_longitude > 180:
            return False

        return True

    @pyqtSignature('')  # prevents actions being handled twice
    def on_directory_button_clicked(self):
        """Show a dialog to choose directory."""
        # noinspection PyCallByClass,PyTypeChecker
        self.output_directory.setText(
            QFileDialog.getExistingDirectory(
                self, self.tr('Select download directory')))

    def drag_rectangle_on_map_canvas(self):
        """Hide the dialog and allow the user to draw a rectangle."""

        self.hide()
        self.rectangle_map_tool.reset()
        self.canvas.unsetMapTool(self.pan_tool)
        self.canvas.setMapTool(self.rectangle_map_tool)

    def accept(self):
        """Do osm download and display it in QGIS."""
        error_dialog_title = self.tr('InaSAFE OpenStreetMap Downloader Error')

        # Lock the groupbox
        self.groupBox.setDisabled(True)

        # Validate extent
        valid_flag = self.validate_extent()
        if not valid_flag:
            message = self.tr(
                'The bounding box is not valid. Please make sure it is '
                'valid or check your projection!')
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            display_warning_message_box(self, error_dialog_title, message)
            # Unlock the groupbox
            self.groupBox.setEnabled(True)
            return

        # Get all the feature types
        index = self.feature_type.currentIndex()
        if index == 0:
            feature_types = ['buildings', 'roads', 'building-points']
        elif index == 1:
            feature_types = ['buildings']
        elif index == 2:
            feature_types = ['building-points']
        else:
            feature_types = ['roads']

        try:
            self.save_state()
            self.require_directory()
            for feature_type in feature_types:

                output_directory = self.output_directory.text()
                output_prefix = self.filename_prefix.text()
                overwrite = self.overwrite_checkBox.isChecked()
                output_base_file_path = self.get_output_base_path(
                    output_directory, output_prefix, feature_type, overwrite)

                self.download(feature_type, output_base_file_path)
                try:
                    self.load_shapefile(feature_type, output_base_file_path)
                except FileMissingError as exception:
                    display_warning_message_box(self, error_dialog_title,
                                                exception.message)
            self.done(QDialog.Accepted)
            self.rectangle_map_tool.reset()

        except CanceledImportDialogError:
            # don't show anything because this exception raised
            # when user canceling the import process directly
            pass
        except Exception as exception:  # pylint: disable=broad-except
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            display_warning_message_box(self, error_dialog_title,
                                        exception.message)

            self.progress_dialog.cancel()

        finally:
            # Unlock the groupbox
            self.groupBox.setEnabled(True)

    def get_output_base_path(self, output_directory, output_prefix,
                             feature_type, overwrite):
        """Get a full base name path to save the shapefile.

        :param output_directory: The directory where to put results.
        :type output_directory: str

        :param output_prefix: The prefix to add for the shapefile.
        :type output_prefix: str

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings', 'building-points' or 'roads' are supported.
        :type feature_type: str

        :param overwrite: Boolean to know if we can overwrite existing files.
        :type overwrite: bool

        :return: The base path.
        :rtype: str
        """
        path = os.path.join(output_directory,
                            '%s%s' % (output_prefix, feature_type))

        if overwrite:

            # If a shapefile exists, we must remove it (only the .shp)
            shp = '%s.shp' % path
            if os.path.isfile(shp):
                os.remove(shp)

        else:
            separator = '-'
            suffix = self.get_unique_file_path_suffix('%s.shp' % path,
                                                      separator)

            if suffix:
                path = os.path.join(
                    output_directory, '%s%s%s%s' %
                    (output_prefix, feature_type, separator, suffix))

        return path

    @staticmethod
    def get_unique_file_path_suffix(file_path, separator='-', i=0):
        """Return the minimum number to suffix the file to not overwrite one.
        Example : /tmp/a.txt exists.
            - With file_path='/tmp/b.txt' will return 0.
            - With file_path='/tmp/a.txt' will return 1 (/tmp/a-1.txt)

        :param file_path: The file to check.
        :type file_path: str

        :param separator: The separator to add before the prefix.
        :type separator: str

        :param i: The minimum prefix to check.
        :type i: int

        :return: The minimum prefix you should add to not overwrite a file.
        :rtype: int
        """

        basename = os.path.splitext(file_path)
        if i != 0:
            file_path_test = os.path.join(
                '%s%s%s%s' % (basename[0], separator, i, basename[1]))
        else:
            file_path_test = file_path

        if os.path.isfile(file_path_test):
            return OsmDownloaderDialog.get_unique_file_path_suffix(
                file_path, separator, i + 1)
        else:
            return i

    def require_directory(self):
        """Ensure directory path entered in dialog exist.

        When the path does not exist, this function will ask the user if he
        want to create it or not.

        :raises: CanceledImportDialogError - when user choose 'No' in
            the question dialog for creating directory.
        """
        path = self.output_directory.text()

        if os.path.exists(path):
            return

        title = self.tr('Directory %s not exist') % path
        question = self.tr(
            'Directory %s not exist. Do you want to create it?') % path
        # noinspection PyCallByClass,PyTypeChecker
        answer = QMessageBox.question(self, title, question,
                                      QMessageBox.Yes | QMessageBox.No)

        if answer == QMessageBox.Yes:
            if len(path) != 0:
                os.makedirs(path)
            else:
                # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
                display_warning_message_box(
                    self, self.tr('InaSAFE error'),
                    self.tr('Output directory can not be empty.'))
                raise CanceledImportDialogError()
        else:
            raise CanceledImportDialogError()

    def download(self, feature_type, output_base_path):
        """Download shapefiles from Kartoza server.

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings', 'building-points' or 'roads' are supported.
        :type feature_type: str

        :param output_base_path: The base path of the shape file.
        :type output_base_path: str

        :raises: ImportDialogError, CanceledImportDialogError
        """

        # preparing necessary data
        min_longitude = str(self.min_longitude.text())
        min_latitude = str(self.min_latitude.text())
        max_longitude = str(self.max_longitude.text())
        max_latitude = str(self.max_latitude.text())

        box = ('{min_longitude},{min_latitude},{max_longitude},'
               '{max_latitude}').format(min_longitude=min_longitude,
                                        min_latitude=min_latitude,
                                        max_longitude=max_longitude,
                                        max_latitude=max_latitude)
        if feature_type == 'buildings':
            url = '{url}?bbox={box}&qgis_version=2'.format(
                url=self.buildings_url, box=box)
        elif feature_type == 'building-points':
            url = '{url}?bbox={box}&qgis_version=2'.format(
                url=self.building_points_url, box=box)
        else:
            url = '{url}?bbox={box}&qgis_version=2'.format(url=self.roads_url,
                                                           box=box)

        if 'LANG' in os.environ:
            env_lang = os.environ['LANG']
            url += '&lang=%s' % env_lang

        path = tempfile.mktemp('.shp.zip')

        # download and extract it
        self.fetch_zip(url, path, feature_type)
        self.extract_zip(path, output_base_path)

        self.progress_dialog.done(QDialog.Accepted)

    def fetch_zip(self, url, output_path, feature_type):
        """Download zip containing shp file and write to output_path.

        :param url: URL of the zip bundle.
        :type url: str

        :param output_path: Path of output file,
        :type output_path: str

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings', 'building-points' or 'roads' are supported.
        :type feature_type: str

        :raises: ImportDialogError - when network error occurred
        """
        LOGGER.debug('Downloading file from URL: %s' % url)
        LOGGER.debug('Downloading to: %s' % output_path)

        self.progress_dialog.show()

        # Infinite progress bar when the server is fetching data.
        # The progress bar will be updated with the file size later.
        self.progress_dialog.setMaximum(0)
        self.progress_dialog.setMinimum(0)
        self.progress_dialog.setValue(0)

        # Get a pretty label from feature_type, but not translatable
        label_feature_type = feature_type.replace('-', ' ')

        label_text = self.tr('Fetching %s' % label_feature_type)
        self.progress_dialog.setLabelText(label_text)

        # Download Process
        downloader = FileDownloader(self.network_manager, url, output_path,
                                    self.progress_dialog)
        try:
            result = downloader.download()
        except IOError as ex:
            raise IOError(ex)

        if result[0] is not True:
            _, error_message = result

            if result[0] == QNetworkReply.OperationCanceledError:
                raise CanceledImportDialogError(error_message)
            else:
                raise DownloadError(error_message)

    @staticmethod
    def extract_zip(zip_path, destination_base_path):
        """Extract different extensions to the destination base path.

        Example : test.zip contains a.shp, a.dbf, a.prj
        and destination_base_path = '/tmp/CT-buildings
        Expected result :
            - /tmp/CT-buildings.shp
            - /tmp/CT-buildings.dbf
            - /tmp/CT-buildings.prj

        If two files in the zip with the same extension, only one will be
        copied.

        :param zip_path: The path of the .zip file
        :type zip_path: str

        :param destination_base_path: The destination base path where the shp
            will be written to.
        :type destination_base_path: str

        :raises: IOError - when not able to open path or output_dir does not
            exist.
        """

        import zipfile

        handle = open(zip_path, 'rb')
        zip_file = zipfile.ZipFile(handle)
        for name in zip_file.namelist():
            extension = os.path.splitext(name)[1]
            output_final_path = u'%s%s' % (destination_base_path, extension)
            output_file = open(output_final_path, 'wb')
            output_file.write(zip_file.read(name))
            output_file.close()

        handle.close()

    def load_shapefile(self, feature_type, base_path):
        """Load downloaded shape file to QGIS Main Window.

        :param feature_type: What kind of features should be downloaded.
            Currently 'buildings', 'building-points' or 'roads' are supported.
        :type feature_type: str

        :param base_path: The base path of the shape file (without extension).
        :type base_path: str

        :raises: FileMissingError - when buildings.shp not exist
        """

        path = '%s.shp' % base_path

        if not os.path.exists(path):
            message = self.tr(
                '%s does not exist. The server does not have any data for '
                'this extent.' % path)
            raise FileMissingError(message)

        self.iface.addVectorLayer(path, feature_type, 'ogr')

        canvas_srid = self.canvas.mapRenderer().destinationCrs().srsid()
        on_the_fly_projection = self.canvas.hasCrsTransformEnabled()
        if canvas_srid != 4326 and not on_the_fly_projection:
            if QGis.QGIS_VERSION_INT >= 20400:
                self.canvas.setCrsTransformEnabled(True)
            else:
                display_warning_message_bar(
                    self.tr('Enable \'on the fly\''),
                    self.tr(
                        'Your current projection is different than EPSG:4326. '
                        'You should enable \'on the fly\' to display '
                        'correctly your layers'))

    def reject(self):
        """Redefinition of the reject() method
        to remove the rectangle selection tool.
        It will call the super method.
        """

        self.canvas.unsetMapTool(self.rectangle_map_tool)
        self.rectangle_map_tool.reset()

        super(OsmDownloaderDialog, self).reject()
Beispiel #9
0
class OsmDownloader(QDialog, Ui_OsmDownloaderBase):
    """Downloader for OSM data."""
    def __init__(self, parent=None, iface=None):
        """Constructor for import dialog.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE OpenStreetMap Downloader'))

        self.iface = iface
        self.url = "http://osm.linfiniti.com/buildings-shp"

        # creating progress dialog for download
        self.progressDialog = QProgressDialog(self)
        self.progressDialog.setAutoClose(False)
        myTitle = self.tr("InaSAFE OpenStreetMap Downloader")
        self.progressDialog.setWindowTitle(myTitle)

        self.show_info()

        self.network_manager = QNetworkAccessManager(self)
        self.restore_state()
        self.update_extent()

    def show_info(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        header = html_header()
        footer = html_footer()

        string = header

        heading = m.Heading(self.tr('OSM Downloader'), **INFO_STYLE)
        body = self.tr(
            'This tool will fetch building (\'structure\') data from the '
            'OpenStreetMap project for you. The downloaded data will have '
            'InaSAFE keywords defined and a default QGIS style applied. To '
            'use this tool effectively:')
        tips = m.BulletedList()
        tips.add(
            self.
            tr('Use QGIS to zoom in to the area for which you want building data '
               'to be retrieved.'))
        tips.add(
            self.
            tr('Check the output directory is correct. Note that the saved '
               'dataset will be called buildings.shp (and its associated files).'
               ))
        tips.add(
            self.
            tr('If a dataset already exists in the output directory it will be '
               'overwritten.'))
        tips.add(
            self.
            tr('This tool requires a working internet connection and fetching '
               'buildings will consume your bandwidth.'))
        tips.add(
            m.Link(
                'http://www.openstreetmap.org/copyright',
                text=self.tr(
                    'Downloaded data is copyright OpenStreetMap contributors'
                    ' (click for more info).')))
        message = m.Message()
        message.add(heading)
        message.add(body)
        message.add(tips)
        string += message.to_html()
        string += footer

        self.webView.setHtml(string)

    def restore_state(self):
        """ Read last state of GUI from configuration file."""
        mySetting = QSettings()
        self.outDir.setText(mySetting.value('directory').toString())

    def save_state(self):
        """ Store current state of GUI to configuration file """
        mySetting = QSettings()
        mySetting.setValue('directory', self.outDir.text())

    def update_extent(self):
        """ Update extent value in GUI based from value in map."""
        myExtent = self.iface.mapCanvas().extent()
        self.minLongitude.setText(str(myExtent.xMinimum()))
        self.minLatitude.setText(str(myExtent.yMinimum()))
        self.maxLongitude.setText(str(myExtent.xMaximum()))
        self.maxLatitude.setText(str(myExtent.yMaximum()))

    @pyqtSignature('')  # prevents actions being handled twice
    def on_pBtnDir_clicked(self):
        """ Show a dialog to choose directory """
        # noinspection PyCallByClass,PyTypeChecker
        self.outDir.setText(
            QFileDialog.getExistingDirectory(
                self, self.tr("Select download directory")))

    def accept(self):
        """Do osm download and display it in QGIS."""

        try:
            self.save_state()

            self.require_directory()
            self.download()
            self.load_shapefile()
            self.done(QDialog.Accepted)
        except CanceledImportDialogError:
            # don't show anything because this exception raised
            # when user canceling the import process directly
            pass
        except Exception as myEx:
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QMessageBox.warning(
                self, self.tr("InaSAFE OpenStreetMap downloader error"),
                str(myEx))

            self.progressDialog.cancel()

    def require_directory(self):
        """Ensure directory path entered in dialog exist.

        When the path does not exist, this function will ask the user if he
        want to create it or not.

        :raises: CanceledImportDialogError - when user choose 'No' in
            the question dialog for creating directory.
        """

        myDir = str(self.outDir.text())

        if os.path.exists(myDir):
            return

        myTitle = self.tr("Directory %1 not exist").arg(myDir)
        myQuestion = self.tr(
            "Directory %1 not exist. Are you want to create it?").arg(myDir)
        # noinspection PyCallByClass,PyTypeChecker
        myAnswer = QMessageBox.question(self, myTitle, myQuestion,
                                        QMessageBox.Yes | QMessageBox.No)

        if myAnswer == QMessageBox.Yes:
            os.makedirs(myDir)
        else:
            raise CanceledImportDialogError()

    def download(self):
        """Download shapefiles from Linfinti server.

        :raises: ImportDialogError, CanceledImportDialogError
        """

        ## preparing necessary data
        myMinLng = str(self.minLongitude.text())
        myMinLat = str(self.minLatitude.text())
        myMaxLng = str(self.maxLongitude.text())
        myMaxLat = str(self.maxLatitude.text())

        myCoordinate = "{myMinLng},{myMinLat},{myMaxLng},{myMaxLat}".format(
            myMinLng=myMinLng,
            myMinLat=myMinLat,
            myMaxLng=myMaxLng,
            myMaxLat=myMaxLat)

        myShapeUrl = "{url}?bbox={myCoordinate}".format(
            url=self.url, myCoordinate=myCoordinate)

        myFilePath = tempfile.mktemp('.shp.zip')

        # download and extract it
        self.fetch_zip(myShapeUrl, myFilePath)
        print myFilePath
        print str(self.outDir.text())
        self.extract_zip(myFilePath, str(self.outDir.text()))

        self.progressDialog.done(QDialog.Accepted)

    def fetch_zip(self, url, output_path):
        """Download zip containing shp file and write to output_path.

        :param url: URL of the zip bundle.
        :type url: str

        :param output_path: Path of output file,
        :type output_path: str

        :raises: ImportDialogError - when network error occurred
        """

        self.progressDialog.show()
        self.progressDialog.setMaximum(100)
        self.progressDialog.setValue(0)

        # myLabelText = "Begin downloading shapefile from " \
        #               + "%1 ..."
        # self.progressDialog.setLabelText(self.tr(myLabelText).arg(url))
        myLabelText = self.tr("Downloading shapefile")
        self.progressDialog.setLabelText(myLabelText)

        myResult = download_url(self.network_manager, url, output_path,
                                self.progressDialog)

        if myResult is not True:
            _, myErrorMessage = myResult
            raise ImportDialogError(myErrorMessage)

    def extract_zip(self, path, output_dir):
        """Extract all content of a .zip file from path to output_dir.

        :param path: The path of the .zip file
        :type path: str

        :param output_dir: Output directory where the shp will be written to.
        :type output_dir: str

        :raises: IOError - when not able to open path or output_dir does not
            exist.
        """

        import zipfile

        ## extract all files...
        myHandle = open(path, 'rb')
        myZip = zipfile.ZipFile(myHandle)
        for myName in myZip.namelist():
            myOutPath = os.path.join(output_dir, myName)
            myOutFile = open(myOutPath, 'wb')
            myOutFile.write(myZip.read(myName))
            myOutFile.close()

        myHandle.close()

    def load_shapefile(self):
        """
        Load downloaded shape file to QGIS Main Window.

        :raises: ImportDialogError - when buildings.shp not exist
        """

        myDir = str(self.outDir.text())
        myPath = os.path.join(myDir, 'buildings.shp')

        if not os.path.exists(myPath):
            myMessage = self.tr(
                "%s don't exist. The server don't have buildings data.")
            raise ImportDialogError(myMessage)

        self.iface.addVectorLayer(myPath, 'buildings', 'ogr')
Beispiel #10
0
class ImportDialog(QDialog, Ui_ImportDialogBase):
    def __init__(self, theParent=None, theIface=None):
        """Constructor for import dialog.

        Args:
           * theParent - Optional widget to use as parent
           * theIface - an instance of QGisInterface
        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """
        QDialog.__init__(self, theParent)
        self.parent = theParent
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE OpenStreetMap Downloader'))

        self.iface = theIface
        self.url = "http://osm.linfiniti.com/buildings-shp"

        ## region coordinate: (latitude, longtitude, zoom_level)
        self.regionExtent = {
            '0': [18.87685, -71.493, 6],  # haiti
            '1': [-2.5436300, 116.8887, 3],  # indonesia
            '2': [1.22449, 15.40999, 2],  # africa
            '3': [34.05, 56.55, 3],  # middle east
            '4': [12.98855, 121.7166, 4],  # philipine
        }

        # creating progress dialog for download
        self.progressDialog = QProgressDialog(self)
        self.progressDialog.setAutoClose(False)
        myTitle = self.tr("InaSAFE OpenStreetMap Downloader")
        self.progressDialog.setWindowTitle(myTitle)

        ## set map parameter based on placeholder self.map widget
        theMap = InasafeLightMaps(self.gbxMap)
        theMap.setGeometry(self.map.geometry())
        theMap.setSizePolicy(self.map.sizePolicy())
        self.map = theMap

        self.nam = QNetworkAccessManager(self)

        self.setupOptions()

        self.restoreState()

        self.cbxRegion.currentIndexChanged.connect(self.regionChanged)
        self.map.m_normalMap.updated.connect(self.updateExtent)

    def regionChanged(self, theIndex):
        """ Slot that called when region combo box changed.
        Params:
            theIndex - index of combo box
        """
        myRegionIndex = str(self.cbxRegion.itemData(theIndex).toString())
        myCenter = self.regionExtent[myRegionIndex]
        self.map.setCenter(myCenter[0], myCenter[1], myCenter[2])

    # pylint: disable=W0613
    def resizeEvent(self, theEvent):
        """ Function that called when resize event occurred.
        Params:
            theEvent - QEvent instance. Not used.
        """
        self.map.resize(self.gbxMap.width() - 30, self.gbxMap.height() - 30)

    # pylint: disable=W0613

    def setupOptions(self):
        """ Set the content of combo box for region and preset """
        self.cbxRegion.insertItem(0, 'Indonesia', 1)
        self.cbxRegion.insertItem(1, 'Africa', 2)
        self.cbxRegion.insertItem(2, 'Philippines', 4)
        self.cbxRegion.insertItem(3, 'Central Asia/Middle East', 3)
        self.cbxRegion.insertItem(4, 'Haiti', 0)

        self.cbxPreset.insertItem(0, self.tr('Buildings'), 'building')
        self.cbxPreset.insertItem(0, self.tr('Highway'), 'highway')

    def restoreState(self):
        """ Read last state of GUI from configuration file """
        mySetting = QSettings()

        myRegion = mySetting.value('region').toInt()
        if myRegion[1] is True:
            self.cbxRegion.setCurrentIndex(myRegion[0])

        myPreset = mySetting.value('preset').toInt()
        if myPreset[1] is True:
            self.cbxPreset.setCurrentIndex(myPreset[0])

        self.outDir.setText(mySetting.value('directory').toString())

        myZoomLevel = mySetting.value('zoom_level').toInt()
        myLatitude = mySetting.value('latitude').toDouble()
        myLongitude = mySetting.value('longitude').toDouble()

        if myZoomLevel[1] is True:
            self.map.setCenter(myLatitude[0], myLongitude[0], myZoomLevel[0])
        else:
            # just set to indonesia extent
            myCenter = self.regionExtent['1']
            self.map.setCenter(myCenter[0], myCenter[1], myCenter[2])

    def saveState(self):
        """ Store current state of GUI to configuration file """
        mySetting = QSettings()
        mySetting.setValue('region', self.cbxRegion.currentIndex())
        mySetting.setValue('preset', self.cbxPreset.currentIndex())
        mySetting.setValue('directory', self.outDir.text())

        mySetting.setValue('zoom_level', self.map.getZoomLevel())
        myCenter = self.map.getCenter()
        mySetting.setValue('latitude', myCenter[0])
        mySetting.setValue('longitude', myCenter[1])

    def updateExtent(self):
        """ Update extent value in GUI based from value in map widget"""
        myExtent = self.map.getExtent()
        self.minLongitude.setText(str(myExtent[1]))
        self.minLatitude.setText(str(myExtent[0]))
        self.maxLongitude.setText(str(myExtent[3]))
        self.maxLatitude.setText(str(myExtent[2]))

    @pyqtSignature('')  # prevents actions being handled twice
    def on_pBtnDir_clicked(self):
        """ Show a dialog to choose directory """
        self.outDir.setText(
            QFileDialog.getExistingDirectory(self,
                                             self.tr("Select Directory")))

    def accept(self):
        """ Do import process """

        try:
            self.saveState()

            self.ensureDirExist()
            self.doImport()
            self.loadShapeFile()
            self.done(QDialog.Accepted)
        except CanceledImportDialogError:
            # don't show anything because this exception raised
            # when user canceling the import process directly
            pass
        except Exception as myEx:
            QMessageBox.warning(
                self, self.tr("InaSAFE OpenStreetMap Downloader Error"),
                str(myEx))

            self.progressDialog.cancel()

    def ensureDirExist(self):
        """
        Ensure directory path entered in dialog exist.
        When the path is not exist, this function will
        ask the user if he want to create it or not.

        Raises:
            CanceledImportDialogError - when user choose 'No'
                        in question dialog for creating directory.
        """

        myDir = str(self.outDir.text())

        if os.path.exists(myDir):
            return

        myTitle = self.tr("Directory %1 not exist").arg(myDir)
        myQuestion = self.tr(
            "Directory %1 not exist. Are you want to create it?").arg(myDir)
        myAnswer = QMessageBox.question(self, myTitle, myQuestion,
                                        QMessageBox.Yes | QMessageBox.No)

        if myAnswer == QMessageBox.Yes:
            os.makedirs(myDir)
        else:
            raise CanceledImportDialogError()

    def doImport(self):
        """
        Import shape files from Linfinti.
        Raises:
            * ImportDialogError - when network error occurred
            * CanceledImportDialogError - when user press cancel button
        """

        ## preparing necessary data
        myMinLng = str(self.minLongitude.text())
        myMinLat = str(self.minLatitude.text())
        myMaxLng = str(self.maxLongitude.text())
        myMaxLat = str(self.maxLatitude.text())

        myCurrentIndex = self.cbxPreset.currentIndex()
        myType = str(self.cbxPreset.itemData(myCurrentIndex).toString())

        myCoordinate = "{myMinLng},{myMinLat},{myMaxLng},{myMaxLat}".format(
            myMinLng=myMinLng,
            myMinLat=myMinLat,
            myMaxLng=myMaxLng,
            myMaxLat=myMaxLat)

        myShapeUrl = "{url}?bbox={myCoordinate}&obj={type}".format(
            url=self.url, myCoordinate=myCoordinate, type=myType)

        myFilePath = tempfile.mktemp('.shp.zip')

        # download and extract it
        self.downloadShapeFile(myShapeUrl, myFilePath)
        self.extractZip(myFilePath, str(self.outDir.text()))

        self.progressDialog.done(QDialog.Accepted)

    def downloadShapeFile(self, theUrl, theOutput):
        """
        Download shape file from theUrl and write to theOutput.
        Params:
            * theUrl - URL of shape file
            * theOutput - path of output file
        Raises:
            * ImportDialogError - when network error occurred
        """

        self.progressDialog.show()
        self.progressDialog.setMaximum(100)
        self.progressDialog.setValue(0)

        # myLabelText = "Begin downloading shapefile from " \
        #               + "%1 ..."
        # self.progressDialog.setLabelText(self.tr(myLabelText).arg(theUrl))
        myLabelText = self.tr("Begin downloading shapefile")
        self.progressDialog.setLabelText(myLabelText)

        myResult = httpDownload(self.nam, theUrl, theOutput,
                                self.progressDialog)

        if myResult is not True:
            _, myErrorMessage = myResult
            raise ImportDialogError(myErrorMessage)

    def extractZip(self, thePath, theOutDir):
        """
        Extract all content of zip file from thePath to theOutDir.
        Args:
           * thePath - the path of zip file
           * theOutDir - output directory
        Raises:
            IOError - when cannot open thePath or theOutDir is not exist.
        """

        import zipfile

        ## extract all files...
        myHandle = open(thePath, 'rb')
        myZip = zipfile.ZipFile(myHandle)
        for myName in myZip.namelist():
            myOutPath = os.path.join(theOutDir, myName)
            myOutFile = open(myOutPath, 'wb')
            myOutFile.write(myZip.read(myName))
            myOutFile.close()

        myHandle.close()

    def loadShapeFile(self):
        """
        Load downloaded shape file to QGIS Main Window.

        Raises:
            ImportDialogError - when buildings.shp not exist
        """

        myDir = str(self.outDir.text())
        myPath = os.path.join(myDir, 'buildings.shp')

        if not os.path.exists(myPath):
            myMessage = self.tr(
                "%s don't exist. The server don't have buildings data.")
            raise ImportDialogError(myMessage)

        self.iface.addVectorLayer(myPath, 'buildings', 'ogr')