# doc-export: LeafletExample
"""
This example demonstrates the use of Leaflet to display a slippy map.
"""


import os
from urllib.request import urlopen, Request
import re
import base64
import mimetypes

from flexx import flx


_leaflet_url = 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/'
_leaflet_version = '1.0.3'
_leaflet_icons = [
    'marker-icon.png',
    'marker-icon-2x.png',
    'marker-shadow.png',
]


if 'LEAFLET_DIR' in os.environ:
    _base_url = 'file://%s' % os.environ['LEAFLET_DIR']
else:
    _base_url = '%s/%s' % (_leaflet_url, _leaflet_version)
mimetypes.init()


def _get_code(item):
    """ Get a text item from _base_url
    """
    url = '%s/%s' % (_base_url, item)
    req = Request(url, headers={'User-Agent': 'flexx/%s' % flx.__version__})
    return urlopen(req).read().decode()


def _get_data(item_or_url):
    """ Get a binary item from url or _base_url
    """
    if '://' in item_or_url:
        url = item_or_url
    else:
        url = '%s/%s' % (_base_url, item_or_url)
    req = Request(url, headers={'User-Agent': 'flexx/%s' % flx.__version__})
    return urlopen(req).read()


def _embed_css_resources(css, types=('.png',)):
    """ Replace urls in css with data urls
    """
    type_str = '|'.join('\%s' % t for t in types)
    rx = re.compile('(url\s*\(\s*(.*(%s))\s*\))' % type_str)
    found = rx.findall(css)
    for match, item, ext in found:
        data = base64.b64encode(_get_data(item)).decode()
        mime = mimetypes.types_map[ext]
        repl = 'url(data:%s;base64,%s)' % (mime, data)
        css = css.replace(match, repl)
    return css


leaflet_src = 'leaflet-src.js' if 'LEAFLET_DEBUG' in os.environ else 'leaflet.js'
flx.assets.associate_asset(
    __name__,
    leaflet_src,
    lambda: _get_code(leaflet_src),
)
flx.assets.associate_asset(
    __name__,
    'leaflet.css',
    lambda: _embed_css_resources(_get_code('leaflet.css')),
)
for icon in _leaflet_icons:
    flx.assets.add_shared_data(icon, _get_data('images/%s' % icon))


class LeafletWidget(flx.Widget):
    """ A widget that shows a slippy/tile-map using Leaflet.
    """

    layers = flx.ListProp([], doc="""
        List of tilemap layer tuples: (url, 'Layer').
        """)

    zoom = flx.IntProp(8, settable=True, doc="""
        Zoom level for the map.
        """)

    min_zoom = flx.IntProp(0, settable=True, doc="""
        self zoom level for the map.
        """)

    max_zoom = flx.IntProp(18, settable=True, doc="""
        Maximum zoom level for the map.
        """)

    center = flx.FloatPairProp((5.2, 5.5), settable=True, doc="""
        The center of the map.
        """)

    show_layers = flx.BoolProp(False, settable=True, doc="""
        Whether to show layers-icon on the top-right of the map.
        """)

    show_scale = flx.BoolProp(False, settable=True, doc="""
        Whether to show scale at bottom-left of map.
        """)
    
    @flx.action
    def add_layer(self, url, name=None, attribution=None):
        """ Add a layer to the map.
        """
        # Avoid duplicates
        self.remove_layer(url)
        if name:
            self.remove_layer(name)
        # Add layer
        layers = self.layers + [(url, name or 'Layer', attribution or '')]
        self._mutate_layers(layers)

    @flx.action
    def remove_layer(self, url_or_name):
        """ Remove a layer from the map by url or name.
        """
        layers = list(self.layers)
        for i in reversed(range(len(layers))):
            if url_or_name in layers[i]:
                layers.pop(i)
        self._mutate_layers(layers)

    def _create_dom(self):
        global L, document
        node = document.createElement('div')
        self.mapnode = document.createElement('div')
        node.appendChild(self.mapnode)
        self.mapnode.id = 'maproot'
        self.mapnode.style.position = 'absolute'
        self.mapnode.style.top = '0px'
        self.mapnode.style.left = '0px'
        self.map = L.map(self.mapnode)
        self.map.on('zoomend', self.map_handle_zoom)
        self.map.on('moveend', self.map_handle_move)
        self.map.on('click', self.map_handle_mouse)
        self.map.on('dblclick', self.map_handle_mouse)
        # Container to keep track of leaflet layer objects
        self.layer_container = []
        self.layer_control = L.control.layers()
        self.scale = L.control.scale({'imperial': False, 'maxWidth': 200})
        # Set the path for icon images
        L.Icon.Default.prototype.options.imagePath = '_data/shared/'
        return node

    def map_handle_zoom(self, e):
        global isNaN
        zoom = self.map.getZoom()
        if isNaN(zoom):
            return
        if zoom != self.zoom:
            self.set_zoom(zoom)

    def map_handle_move(self, e):
        center_coord = self.map.getCenter()
        center = center_coord.lat, center_coord.lng
        if center != self.center:
            self.set_center(center)

    def map_handle_mouse(self, e):
        latlng = [e.latlng.lat, e.latlng.lng]
        xy = [e.layerPoint.x, e.layerPoint.y]
        self.pointer_event(e.type, latlng, xy)

    @flx.emitter
    def pointer_event(self, event, latlng, xy):
        return {'event': event, 'latlng': latlng, 'xy': xy}

    @flx.reaction
    def __handle_zoom(self):
        self.map.setZoom(self.zoom)

    @flx.reaction
    def __handle_min_zoom(self):
        self.map.setMinZoom(self.min_zoom)

    @flx.reaction
    def __handle_max_zoom(self):
        self.map.setMaxZoom(self.max_zoom)

    @flx.reaction
    def __handle_center(self):
        self.map.panTo(self.center)

    @flx.reaction
    def __handle_show_layers(self):
        if self.show_layers:
            self.map.addControl(self.layer_control)
        else:
            self.map.removeControl(self.layer_control)

    @flx.reaction
    def __handle_show_scale(self):
        if self.show_scale:
            self.map.addControl(self.scale)
        else:
            self.map.removeControl(self.scale)

    @flx.reaction
    def __size_changed(self):
        size = self.size
        if size[0] or size[1]:
            self.mapnode.style.width = size[0] + 'px'
            self.mapnode.style.height = size[1] + 'px'
            # Notify the map that it's container's size changed
            self.map.invalidateSize()

    @flx.reaction
    def __layers_changed(self):
        global L
        for layer in self.layer_container:
            self.layer_control.removeLayer(layer)
            if self.map.hasLayer(layer):
                self.map.removeLayer(layer)
        for layer_url, layer_name, layer_attribution in self.layers:
            if not layer_url.endswith('.png'):
                if not layer_url.endswith('/'):
                    layer_url += '/'
                layer_url += '{z}/{x}/{y}.png'
            new_layer = L.tileLayer(layer_url, {'attribution': layer_attribution})
            self.layer_container.append(new_layer)
            self.map.addLayer(new_layer)
            self.layer_control.addOverlay(new_layer, layer_name)


class LeafletExample(flx.Widget):

    def init(self):
        with flx.HBox():
            self.leaflet = LeafletWidget(
                flex=1,
                center=(52, 4.1),
                zoom=12,
                show_scale=lambda: self.cbs.checked,
                show_layers=lambda: self.cbl.checked,
            )
            with flx.VBox():
                self.btna = flx.Button(text='Add SeaMap')
                self.btnr = flx.Button(text='Remove SeaMap')
                self.cbs = flx.CheckBox(text='Show scale')
                self.cbl = flx.CheckBox(text='Show layers')
                self.list = flx.VBox()
                flx.Widget(flex=1)

        self.leaflet.add_layer('http://a.tile.openstreetmap.org/', 'OpenStreetMap')

    @flx.reaction('btna.pointer_click')
    def handle_seamap_add(self, *events):
        self.leaflet.add_layer('http://t1.openseamap.org/seamark/', 'OpenSeaMap')

    @flx.reaction('btnr.pointer_click')
    def handle_seamap_remove(self, *events):
        self.leaflet.remove_layer('http://t1.openseamap.org/seamark/', 'OpenSeaMap')

    # @flx.reaction('cbs.checked', 'cbl.checked')
    # def handle_checkboxes(self, *events):
    #     self.leaflet.set_show_scale(self.cbs.checked
    #     self.leaflet.show_layers = self.cbl.checked

    @flx.reaction('leaflet.pointer_event')
    def handle_leaflet_mouse(self, *events):
        global L
        ev = events[-1]
        latlng = tuple(ev['latlng'])
        flx.Label(text='%f, %f' % (int(100*latlng[0])/100, int(100*latlng[1])/100),
                       parent=self.list)
        latlng = tuple(ev['latlng'])
        if ev['event'] == 'click':
            m = L.marker(ev['latlng'])
            m.bindTooltip('%f, %f' % (latlng[0], latlng[1]))
            m.addTo(self.leaflet.map)


if __name__ == '__main__':
    flx.launch(LeafletExample, 'firefox')
    flx.run()
