Skip to content
Snippets Groups Projects
mapfile.py 38.8 KiB
Newer Older
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#                                                                       #
#   MapServer REST API is a python wrapper around MapServer which       #
#   allows to manipulate a mapfile in a RESTFul way. It has been        #
#   developped to match as close as possible the way the GeoServer      #
#   REST API acts.                                                      #
#                                                                       #
#   Copyright (C) 2011-2013 Neogeo Technologies.                        #
#                                                                       #
#   This file is part of MapServer Rest API.                            #
#                                                                       #
#   MapServer Rest API is free software: you can redistribute it        #
#   and/or modify it under the terms of the GNU General Public License  #
#   as published by the Free Software Foundation, either version 3 of   #
#   the License, or (at your option) any later version.                 #
#                                                                       #
#   MapServer Rest API is distributed in the hope that it will be       #
#   useful, but WITHOUT ANY WARRANTY; without even the implied warranty #
#   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the     #
#   GNU General Public License for more details.                        #
#                                                                       #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

import os
import re
import mapscript
import urlparse
import stores
import metadata
from extensions import plugins
class MetadataMixin(object):

    def __getattr__(self, attr):
        if hasattr(self, "ms") and hasattr(metadata, attr):
            return functools.partial(getattr(metadata, attr), self.ms)
        raise AttributeError("'%s' object has no attribute '%s'" %
                             (type(self).__name__, attr))


def get_store_connection_string(info):
    cparam = info["connectionParameters"]
    if cparam.get("dbtype", "") == "postgis":
        # First mandatory
        url = "PG:dbname=%s port=%s host=%s " % (cparam["database"], cparam["port"], cparam["host"])
        # Then optionals:
        url += " ".join("%s=%s" % (p, cparam[p]) for p in ["user", "password"] if p in cparam)
        return url
    elif "url" in cparam:
        url = urlparse.urlparse(cparam["url"])
        if url.scheme != "file" or url.netloc:
            raise ValueError("Only local files are suported.")
    else:
        raise ValueError("Unhandled type '%s'" % cparam.get("dbtype", "<unknown>"))


class Class(object):
    """
    """

    def __init__(self, backend):
        self.ms = backend


class Layer(MetadataMixin):
    """
    """

    def __init__(self, backend):
        self.ms = backend

    def enable(self, enabled=True):
        requests = ["GetCapabilities", "GetMap", "GetFeatureInfo", "GetLegendGraphic"]
        self.ms.status = mapscript.MS_ON if enabled else mapscript.MS_OFF
        self.set_metadata("wms_enable_request", " ".join(('%s' if enabled else "!%s") % c for c in requests))

    def get_type_name(self):
        return {
            0: "POINT",
            1: "LINESTRING",
            2: "POLYGON",
            3: "RASTER",
            4: "ANNOTATION",
            }[self.ms.type]

    def get_proj4(self):
        return self.ms.getProjection()

    def get_extent(self):
        extent = self.ms.getExtent()
        return stores.Extent(extent.minx, extent.miny, extent.maxx, extent.maxy)
    def get_latlon_extent(self):
        rect = mapscript.rectObj(*self.get_extent())
        res = rect.project(mapscript.projectionObj(self.get_proj4()),
                           mapscript.projectionObj('+init=epsg:4326'))
        return stores.Extent(rect.minx, rect.miny, rect.maxx, rect.maxy)

        fields = self.get_metadata("gml_include_items", "")

        if fields == "all":
            # TODO: Get fields from feature type
            raise NotImplemented()
        return fields

    def iter_fields(self):
        return iter(self.get_fields())

    def iter_classes(self):
        for i in xrange(self.ms.numclasses):
            yield Class(self.ms.getClass(i))

    def get_styles(self):
        return set(clazz.ms.group for clazz in self.iter_classes())

    def iter_styles(self):
        return iter(self.get_styles())

    def get_SLD(self):
        return self.ms.generateSLD().decode("LATIN1").encode("UTF8")

    def add_style_sld(self, mf, s_name, new_sld):

        # Because we do not want to apply the sld to real layers by mistake
        # we need to rename it to something we are sure is not used.
        sld_layer_name = "__mra_tmp_template"

        # Most xml parsers will have trouble with the kind of mess we get as sld.
        # Mostly because we haven't got the proper declarations, we fallback to
        # an html parser, which luckily is much more forgiving.
        from xml.dom.minidom import parseString
        xmlsld = parseString(new_sld)
Maël Méliani's avatar
Maël Méliani committed
        try:
            xmlsld.firstChild.getElementsByTagName("NamedLayer")[0]\
                .getElementsByTagName("Name")[0].firstChild.data = sld_layer_name
Maël Méliani's avatar
Maël Méliani committed
        except:
            raise ValueError("Bad sld (No NamedLayer/Name)")

        # Remove encoding ?
        # @wapiflapi Mapscript ne gère pas les espaces...
        new_sld = xmlsld.toxml()
        ms_template_layer = self.ms.clone()
        ms_template_layer.name = sld_layer_name
        mf.ms.insertLayer(ms_template_layer)
        try:
            ms_template_layer.applySLD(new_sld, sld_layer_name)
        except:
            raise ValueError("Unable to access storage.")

        for i in xrange(ms_template_layer.numclasses):
            ms_class = ms_template_layer.getClass(i)
            ms_class.group = s_name
            self.ms.insertClass(ms_class)

        mf.ms.removeLayer(ms_template_layer.index)
    def set_default_style(self, mf):

        if self.ms.type == mapscript.MS_LAYER_POINT:
            self.ms.tolerance = 8
            self.ms.toleranceunits = 6
            s_name = 'default_point'
        elif self.ms.type == mapscript.MS_LAYER_LINE:
            self.ms.tolerance = 8
            self.ms.toleranceunits = 6
            s_name = 'default_line'
        elif self.ms.type == mapscript.MS_LAYER_POLYGON:
            self.ms.tolerance = 0
            self.ms.toleranceunits = 6
            s_name = 'default_polygon'
        else:
            return

        try:
            style = open(os.path.join(os.path.dirname(__file__), "%s.sld" % s_name)).read()
        except IOError, OSError:
            return

        self.add_style_sld(mf, s_name, style)
    def remove_style(self, s_name):
        for c_index in reversed(xrange(self.ms.numclasses)):
            c = self.ms.getClass(c_index)
            if c.group == s_name:
                self.ms.removeClass(c_index)
                break
        else:
            raise KeyError(s_name)

    def update(self, metadata):
        self.update_metadatas(metadata)


class LayerGroup(object):
    """
    """

    # TODO: We need to handle the order of the layers in a group.

    def __init__(self, name, mapfile):
        """
        """

        self.name = name
        self.mapfile = mapfile

    def iter_layers(self):
        return self.mapfile.iter_layers(meta={"wms_group_name":self.name})

    def get_layers(self):
        return list(self.iter_layers())

    def add_layer(self, layer):
        layer.ms.group = self.name
        layer.set_metadata("wms_group_name", self.name)
        for k, v in self.mapfile.get_mra_metadata("layergroups")[self.name]:
            layer.set_metadata("wms_group_%s" % k, v)
        self.mapfile.move_layer_down(layer.ms.name)

    def add(self, *args):
        for layer in args:
            if isinstance(layer, basestring):
            self.add_layer(layer)

    def remove_layer(self, layer):
        for mkey in layer.get_metadata_keys():
            # (We really do not want to use iter_metadata_keys())
            if mkey.startswith("wms_group_"):
                layer.del_metadata(mkey)

    def remove(self, *args):
        for layer in args:
            if isinstance(layer, basestring):
                layer = mapfile.get_layer(layer)
            self.remove_layer(layer)

    def clear(self):
        # Remove all the layers from this group.
        for layer in self.mapfile.iter_layers(attr={"group": self.name}):
    def get_latlon_extent(self):
        layers = self.get_layers()
        if not layers:
            return stores.Extent(0, 0, 0, 0)

        extent = layers[0].get_latlon_extent()
            e = layer.get_latlon_extent()
            extent.addX(e.minX(), e.maxX())
            extent.addY(e.minY(), e.maxY())

        return extent

class LayerModel(MetadataMixin):
    """
    """

    def __init__(self, backend):
        self.ms = backend
        self.name = self.get_mra_metadata("name", None)
Maël Méliani's avatar
Maël Méliani committed
    def get_extent(self):
        extent = self.ms.getExtent()
        return stores.Extent(extent.minx, extent.miny, extent.maxx, extent.maxy)

    def get_latlon_extent(self):
        rect = mapscript.rectObj(*self.get_extent())
        res = rect.project(mapscript.projectionObj(self.ms.getProjection()),
                           mapscript.projectionObj('+init=epsg:4326'))
        return stores.Extent(rect.minx, rect.miny, rect.maxx, rect.maxy)

    def get_proj4(self):
        return self.ms.getProjection()

    def get_wkt(self):
        return tools.proj4_to_wkt(self.ms.getProjection())

    def get_authority(self):
        return tools.wkt_to_authority(self.get_wkt())

    def get_authority_name(self):
        return self.get_authority()[0]

    def get_authority_code(self):
        return self.get_authority()[1]

    """
    """

    def update(self, ws, ft_name, ds_name, metadata):

        ds = ws.get_datastore(ds_name)
        ft = ds[ft_name]
        self.name = ft_name

        # Set basic attributes.
        self.ms.name = "ft:%s:%s:%s" % (ws.name, ds_name, ft_name)
        self.ms.status = mapscript.MS_OFF
        self.ms.type = ft.get_geomtype_mapscript()
        self.ms.setProjection(ft.get_proj4())
        self.ms.setExtent(*ft.get_extent())

        # Configure the connection to the store.
        # This is a little hacky as we have to translate stuff...
        info = ws.get_datastore_info(ds_name)
        cparam = info["connectionParameters"]
        if cparam.get("dbtype", None) in ["postgis", "postgres", "postgresql"]:
            self.ms.connectiontype = mapscript.MS_POSTGIS
            connection = "dbname=%s port=%s host=%s " % (cparam["database"], cparam["port"], cparam["host"])
            connection += " ".join("%s=%s" % (p, cparam[p]) for p in ["user", "password"] if p in cparam)
            self.ms.connection = connection
            self.ms.data = "%s FROM %s" % (ds[ft_name].get_geometry_column(), ft_name)
            self.set_metadata("ows_extent", "%s %s %s %s" %
                (ft.get_extent().minX(), ft.get_extent().minY(),
Maël Méliani's avatar
Maël Méliani committed
                ft.get_extent().maxX(), ft.get_extent().maxY()))
        #elif cpram["dbtype"] in ["shp", "shapefile"]:
        else:
            self.ms.connectiontype = mapscript.MS_SHAPEFILE
            url = urlparse.urlparse(cparam["url"])
            self.ms.data = tools.get_resource_path(url.path)

            # TODO: strip extention.
        #else:
        #    raise ValueError("Unhandled type '%s'" % info["dbtype"])

        # Deactivate wms and wfs requests, because we are a virtual layer.
Maël Méliani's avatar
Maël Méliani committed
        self.set_metadatas({
            "wms_enable_request": "!GetCapabilities !GetMap !GetFeatureInfo !GetLegendGraphic",
            "wfs_enable_request": "!GetCapabilities !DescribeFeatureType !GetFeature",
            })

        # Update mra metadatas, and make sure the mandatory ones are left untouched.
        self.update_mra_metadatas({"name": ft_name, "type": "featuretype", "storage": ds_name,
                                   "workspace": ws.name, "is_model": True})

    def configure_layer(self, ws, layer, enabled=True):
        plugins.extend("pre_configure_vector_layer", self, ws, layer)
        # We must also update all our personal attributes (type, ...)
        layer.ms.type = self.ms.type
        layer.ms.setProjection(self.ms.getProjection())
        layer.ms.setExtent(self.ms.extent.minx, self.ms.extent.miny,
                           self.ms.extent.maxx, self.ms.extent.maxy)
        layer.ms.data = self.ms.data
        layer.ms.connectiontype = self.ms.connectiontype
        layer.ms.connection = self.ms.connection

        layer.update_mra_metadatas({
                "name": self.get_mra_metadata("name"),
                "type": self.get_mra_metadata("type"),
                "storage": self.get_mra_metadata("storage"),
                "workspace": self.get_mra_metadata("workspace"),
                "is_model": False,
                })

        layer.update_metadatas({
                "wfs_name": layer.get_metadata("wms_name"),
                "wfs_title": layer.get_metadata("wms_title"),
                "wfs_abstract": layer.get_metadata("wms_abstract"),
                })

        if enabled:
            layer.set_metadata("wfs_enable_request",
                               "GetCapabilities GetFeature DescribeFeatureType")

        # Configure the layer based on information from the store.
        ds = ws.get_datastore(self.get_mra_metadata("storage"))
        ft = ds[self.get_mra_metadata("name")]

        # Configure the different fields.
        field_names = []
        for field in ft.iterfields():
Maël Méliani's avatar
Maël Méliani committed
            layer.set_metadatas({
                "gml_%s_alias" % field.get_name(): field.get_name(),
                "gml_%s_type" % field.get_name(): field.get_type_gml(),
                # TODO: Add gml_<field name>_precision, gml_<field name>_width
                })
        geometry_column = ft.get_geometry_column()
        if geometry_column == None:
            geometry_column = "geometry"
Maël Méliani's avatar
Maël Méliani committed
        layer.set_metadatas({
            "ows_include_items": ",".join(field_names),
            "gml_include_items": ",".join(field_names),
            "gml_geometries": geometry_column,
            "gml_%s_type" % geometry_column: ft.get_geomtype_gml(),
Maël Méliani's avatar
Maël Méliani committed
            # TODO: Add gml_<geometry name>_occurances,
            "wfs_srs": "EPSG:4326",
            "wfs_getfeature_formatlist": "OGRGML,SHAPEZIP",
            })
        if ft.get_fid_column() != None:
            layer.set_metadatas({
                "wfs_featureid": ft.get_fid_column(),
                "gml_featureid": ft.get_fid_column(),
                })
        plugins.extend("post_configure_vector_layer", self, ws, ds, ft, layer)
    """
    """

    def update(self, ws, c_name, cs_name, metadata):

        cs = ws.get_coveragestore(cs_name)
        self.name = c_name

        # Set basic attributes.
        self.ms.name = "c:%s:%s" % (cs_name, c_name)
        self.ms.status = mapscript.MS_OFF
        self.ms.type = mapscript.MS_LAYER_RASTER
        self.ms.setProjection(cs.get_proj4())
        self.ms.setExtent(*cs.get_extent())
        self.ms.setProcessingKey("RESAMPLE","AVERAGE")

        # Configure the connection to the store.
        # This is a little hacky as we have to translate stuff...
        info = ws.get_coveragestore_info(cs_name)
        cparam = info["connectionParameters"]

        #if cparam["dbtype"] in ["tif", "tiff"]:
        self.ms.connectiontype = mapscript.MS_RASTER
        url = urlparse.urlparse(cparam["url"])
        self.ms.data = tools.get_resource_path(url.path)
            # TODO: strip extention.
        #else:
        #    raise ValueError("Unhandled type '%s'" % cparam["dbtype"])

        # Deactivate wms and wcs requests, because we are a virtual layer.
Maël Méliani's avatar
Maël Méliani committed
        self.set_metadatas({
            "wms_enable_request": "!GetCapabilities !GetMap !GetFeatureInfo !GetLegendGraphic",
            "wcs_enable_request": "!GetCapabilities !DescribeCoverage !GetCoverage",

        # Update mra metadatas, and make sure the mandatory ones are left untouched.
        self.update_mra_metadatas(metadata)
        self.update_mra_metadatas({"name": c_name, "type": "coverage", "storage": cs_name,
                                   "workspace": ws.name, "is_model": True})

    def configure_layer(self, ws, layer, enabled=True):
        plugins.extend("pre_configure_raster_layer", self, ws, layer)
        # We must also update all our personal attributes (type, ...)
        layer.ms.type = self.ms.type
        layer.ms.setProjection(self.ms.getProjection())
        layer.ms.setExtent(self.ms.extent.minx, self.ms.extent.miny,
                           self.ms.extent.maxx, self.ms.extent.maxy)
        layer.ms.setProcessingKey("RESAMPLE","AVERAGE")
        layer.ms.data = self.ms.data
        layer.ms.connectiontype = self.ms.connectiontype
        layer.ms.connection = self.ms.connection

        layer.update_mra_metadatas({
                "name": self.get_mra_metadata("name"),
                "type": self.get_mra_metadata("type"),
                "storage": self.get_mra_metadata("storage"),
                "workspace": self.get_mra_metadata("workspace"),
                "is_model": False,
                })

        layer.set_metadatas({
                "wfs_name": layer.get_metadata("wms_name"),
                "wfs_title": layer.get_metadata("wms_title"),
                "wfs_abstract": layer.get_metadata("wms_abstract"),
Maël Méliani's avatar
Maël Méliani committed
                # TODO: wfs_keywordlist, wcs_srs, wcs_getfeature_formatlist...
Maël Méliani's avatar
Maël Méliani committed
            layer.set_metadata("wcs_enable_request", "GetCapabilities GetCoverage DescribeCoverage")
        plugins.extend("post_configure_raster_layer", self, ws, layer)


Maël Méliani's avatar
Maël Méliani committed
    # TODO
    pass

class MapfileWorkspace(Workspace):
    """ A workspace representing a whole mapfile.
    This is currently the only existing type of workspace,
    but there should be others that can handle subsets of
    the mapfile.
    """

    def __init__(self, mapfile):
        # We are obvliously the default workspace.
        self.name = mapfile.get_default_workspace_name()
        self.mapfile = mapfile

    def save(self):
        """Saves the workspace to disk, same as calling save on the
        associated mapfile.
        """
        self.mapfile.save()

    # Stores:
    def get_store(self, st_type, name):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        cparam = get_store_connection_string(self.get_store_info(st_type, name))
        if st_type == "datastores":
            return stores.Datastore(cparam)
        elif st_type == "coveragestores":
            return stores.Coveragestore(cparam)

    def get_store_info(self, st_type, name):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        info = self.mapfile.get_mra_metadata(st_type, {})[name].copy()
        info["name"] = name
        return info

    def iter_store_names(self, st_type):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        return self.mapfile.get_mra_metadata(st_type, {}).iterkeys()

    def iter_stores(self, st_type):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        return self.mapfile.get_mra_metadata(st_type, {}).iteritems()

    def create_store(self, st_type, name, configuration):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        with self.mapfile.mra_metadata(st_type, {}) as stores:
            if name in stores:
                raise KeyExists(name)
            stores[name] = configuration

    def update_store(self, st_type, name, configuration):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        with self.mapfile.mra_metadata(st_type, {}) as stores:
            stores[name].update(configuration)

    def delete_store(self, st_type, name):
        st_type = st_type if st_type.endswith("s") else st_type + "s"
        with self.mapfile.mra_metadata(st_type, {}) as stores:
            del stores[name]

    # Datastores:

    def get_datastore(self, name):
        """Returns a store.Datastore object from the workspace."""
        return self.get_store("datastores", name)

    def get_datastore_info(self, name):
        """Returns info for a datastore from the workspace."""
        return self.get_store_info("datastores", name)

    def iter_datastore_names(self):
        """Return an iterator over the datastore names."""
        return self.iter_store_names("datastores")

    def iter_datastores(self):
        """Return an iterator over the datastore (names, configuration)."""
        return self.iter_stores("datastores")

    def create_datastore(self, name, configuration):
        """Creates a new datastore."""
        return self.create_store("datastores", name, configuration)

    def update_datastore(self, name, configuration):
        """Update a datastore."""
        return self.update_store("datastores", name, configuration)

    def delete_datastore(self, name):
        """Delete a datastore."""
        return self.delete_store("datastores", name)

    # Coveragestores (this is c/p from datastores):

    def get_coveragestore(self, name):
        """Returns a store.Coveragestore object from the workspace."""
        return self.get_store("coveragestores", name)

    def get_coveragestore_info(self, name):
        """Returns info for a coveragestore from the workspace."""
        return self.get_store_info("coveragestores", name)

    def iter_coveragestore_names(self):
        """Return an iterator over the coveragestore names."""
        return self.iter_store_names("coveragestores")

    def iter_coveragestores(self):
        """Return an iterator over the coveragestore (names, configuration)."""
        return self.iter_stores("coveragestores")

    def create_coveragestore(self, name, configuration):
        """Creates a new coveragestore."""
        return self.create_store("coveragestores", name, configuration)

    def update_coveragestore(self, name, configuration):
        """Update a coveragestore."""
        return self.update_store("coveragestores", name, configuration)

    def delete_coveragestore(self, name):
        """Delete a coveragestore."""
        return self.delete_store("coveragestores", name)

    # Feature types

    def iter_featuretypemodels(self, ds_name=None, **kwargs):
        kwargs.setdefault("mra", {}).update({"type":"featuretype", "is_model":True})
        if ds_name != None:
            kwargs["mra"].update({"storage":ds_name, "workspace":self.name})
        for ms_layer in self.mapfile.iter_ms_layers(**kwargs):
    def get_featuretypemodel(self, ft_name, ds_name):
        # Improvement: Use get by name combined with a coverage-specific naming.
        try:
            return next(self.iter_featuretypemodels(ds_name, mra={"name":ft_name}))
        except StopIteration:
            raise KeyError((ds_name, ft_name))

    def has_featuretypemodel(self, ft_name, ds_name):
        # Improvement: See get_featuretypemodel
    def create_featuretypemodel(self, ft_name, ds_name, metadata={}):
        if self.has_featuretypemodel(ft_name, ds_name):
        ft = FeatureTypeModel(mapscript.layerObj(self.mapfile.ms))
        ft.update(self, ft_name, ds_name, metadata)
        return ft

    def update_featuretypemodel(self, ft_name, ds_name, metadata={}):
        ft = self.get_featuretypemodel(ft_name, ds_name)
        ft.update(self, ft_name, ds_name, metadata)

    def delete_featuretypemodel(self, ft_name, ds_name):
        try:
            next(self.mapfile.iter_layers(mra={"workspace":self.name, "type":"featuretype",
                                               "storage":ds_name, "name":ft_name}))
        except StopIteration:
            pass # No layers use our featuretyp, all OK.
        else:
            raise ValueError("The featuretype '%s' can't be delete because it is used." % ft_name)

        ft = self.get_featuretypemodel(ft_name, ds_name)
        self.mapfile.ms.removeLayer(ft.ms.index)

    # Coverages

    def iter_coveragemodels(self, cs_name=None, **kwargs):
        kwargs.setdefault("mra", {}).update({"type":"coverage", "is_model":True})
        if cs_name != None:
            kwargs["mra"].update({"storage":cs_name, "workspace":self.name})
        for ms_layer in self.mapfile.iter_ms_layers(**kwargs):
    def get_coveragemodel(self, c_name, cs_name):
        # Improvement: Use get by name combined with a coverage-specific naming.
        try:
            return next(self.iter_coveragemodels(cs_name, mra={"name":c_name}))
        except StopIteration:
            raise KeyError((cs_name, c_name))

    def has_coveragemodel(self, c_name, cs_name):
        # Improvement: See get_coveragemodel
    def create_coveragemodel(self, c_name, cs_name, metadata={}):
        if self.has_coveragemodel(c_name, cs_name):
        c = CoverageModel(mapscript.layerObj(self.mapfile.ms))
        c.update(self, c_name, cs_name, metadata)
        return c

    def update_coveragemodel(self, c_name, cs_name, metadata={}):
        c = self.get_coveragemodel(c_name, cs_name)
        c.update(self, c_name, cs_name, metadata)

    def delete_coveragemodel(self, c_name, cs_name):
        try:
            next(self.mapfile.iter_layers(mra={"workspace":self.name, "type":"coverage",
                                               "storage":cs_name, "name":c_name}))
        except StopIteration:
            pass # No layers use our featuretyp, all OK.
        else:
            raise ValueError("The coverage '%s' can't be delete because it is used." % c_name)

        self.mapfile.ms.removeLayer(c.ms.index)

    # All the above :)

    def get_model(self, m_name, s_type, s_name):

        if s_type == "coverage":
            return self.get_coveragemodel(m_name, s_name)
            return self.get_featuretypemodel(m_name, s_name)
        else:
            raise ValueError("Bad storage type '%s'." % s_type)


def create_mapfile(path, map_name, data):
    if os.path.exists("%s.map" % path):
        raise KeyExists(map_name)

    mf = mapscript.mapObj()
    mf.name = map_name
    # The following could be defined in <mapfile.py>:

    mf.web.metadata.set("ows_name", map_name)
    mf.web.metadata.set("ows_title", data.get("title", map_name))
    mf.web.metadata.set("ows_abstract", data.get("abstract", ""))

    # Set default values:
    # It should be configurable to the future.

    # mf.web.metadata.set("ows_keywordlist", "")
    # mf.web.metadata.set("ows_keywordlist_vocabulary", "")
    # + ows_keywordlist_[vocabulary’s name]_items
    # mf.web.metadata.set("wms_onlineresource", "")
    # mf.web.metadata.set("wfs_onlineresource", "")
    # mf.web.metadata.set("wms_service_onlineresource", "")
    # mf.web.metadata.set("wfs_service_onlineresource", "")
    mf.web.metadata.set("wms_srs", "EPSG:4326")
    mf.web.metadata.set("wfs_srs", "EPSG:4326")
    mf.web.metadata.set("wms_bbox_extended", "true")
    # mf.web.metadata.set("wms_resx", "")
    # mf.web.metadata.set("wms_resy", "")

    mf.web.metadata.set("ows_schemas_location",
                        "http://schemas.opengeospatial.net")
    mf.web.metadata.set("ows_updatesequence", "foo")
    mf.web.metadata.set("ows_addresstype", "foo")
    mf.web.metadata.set("ows_address", "foo")
    mf.web.metadata.set("ows_city", "foo")
    mf.web.metadata.set("ows_stateorprovince", "foo")
    mf.web.metadata.set("ows_postcode", "foo")
    mf.web.metadata.set("ows_contactperson", "foo")
    mf.web.metadata.set("ows_contactposition", "foo")
    mf.web.metadata.set("ows_contactorganization", "foo")
    mf.web.metadata.set("ows_contactelectronicmailaddress", "foo")
    mf.web.metadata.set("ows_contactfacsimiletelephone", "foo")
    mf.web.metadata.set("ows_contactvoicetelephone", "foo")
    mf.web.metadata.set("wms_fees", "none")
    mf.web.metadata.set("wfs_fees", "none")
    mf.web.metadata.set("wms_accessconstraints", "none")
    mf.web.metadata.set("wfs_accessconstraints", "none")
    # mf.web.metadata.set("ows_attribution_logourl_format", "")
    # mf.web.metadata.set("ows_attribution_logourl_height", "")
    # mf.web.metadata.set("ows_attribution_logourl_href", "")
    # mf.web.metadata.set("ows_attribution_logourl_width", "")
    # mf.web.metadata.set("ows_attribution_onlineresource", "")
    # mf.web.metadata.set("ows_attribution_title", "")

    mf.web.metadata.set("wms_enable_request",
                        "GetCapabilities GetMap GetFeatureInfo GetLegendGraphic")
    mf.web.metadata.set("wfs_enable_request",
                        "GetCapabilities DescribeFeatureType GetFeature")
    mf.web.metadata.set("ows_sld_enabled", "true")
    mf.web.metadata.set("wms_getcapabilities_version", "1.3.0")
    mf.web.metadata.set("wfs_getcapabilities_version", "1.0.0")
    # mf.web.metadata.set("wms_getmap_formatlist", "")
    # mf.web.metadata.set("wms_getlegendgraphic_formatlist", "")
    mf.web.metadata.set("wms_feature_info_mime_type",
                        "application/vnd.ogc.gml,text/plain")
                        # TODO: text/html
    mf.web.metadata.set("wms_encoding", "UTF-8")
    mf.web.metadata.set("wfs_encoding", "UTF-8")

    # mf.web.metadata.set("wms_timeformat", "")
    # mf.web.metadata.set("wms_languages", "")
    # mf.web.metadata.set("wms_layerlimit", "")
    # mf.web.metadata.set("wms_rootlayer_abstract", "")
    # mf.web.metadata.set("wms_rootlayer_keywordlist", "")
    # mf.web.metadata.set("wms_rootlayer_title", "")
    # mf.web.metadata.set("wfs_maxfeatures", "")
    # mf.web.metadata.set("wfs_feature_collection", "")
    # mf.web.metadata.set("wfs_namespace_uri", "")
    # mf.web.metadata.set("wfs_namespace_prefix", "")

    mf.status = mapscript.MS_ON
    mf.setSize(256,256)
    mf.maxsize = 4096
    mf.resolution = 96
    mf.imagetype = 'png'
    mf.imagecolor.setRGB(255,255,255)
    mf.setProjection("init=epsg:4326")
    mf.setExtent(-180,-90,180,90)
    mf.units = mapscript.MS_DD

    mf.save("%s.map" % path)


class Mapfile(MetadataMixin):
    """
    """

    def __init__(self, path, root=None):

        if root != None:
            full_path = os.path.realpath(os.path.join(root, "%s.map" % path))
            if not full_path.startswith(root):
                raise IOError("mapfile '%s' outside root directory." % (path))
            path = full_path
        if isinstance(path, mapscript.mapObj):
            self.path = None
            self.ms = path
        else:
            self.path = path
            self.ms = mapscript.mapObj(self.path)

        self.filename = os.path.basename(self.path)

        # We have one workspace that represents the file.
        self.__default_workspace = MapfileWorkspace(self)

    def save(self, path=None):
        if path is None:
            path = self.path
        self.ms.save(path)

    def rawtext(self):
        open(self.path, "r").read()

    def update(self, configuration):
        raise NotImplemented()

    # Layers:

    def iter_ms_layers(self, attr={}, meta={}, mra={}):
        def check(f, v):
            return f(v) if callable(f) else f == v

        for l in xrange(self.ms.numlayers):
            ms_layer = self.ms.getLayer(l)
            if not all(check(checker, getattr(ms_layer, k, None)) for k, checker in attr.iteritems()):
                continue
            if not all(check(checker, metadata.get_metadata(ms_layer, k, None)) for k, checker in meta.iteritems()):
                continue
            if not all(check(checker, metadata.get_mra_metadata(ms_layer, k, None)) for k, checker in mra.iteritems()):
                continue
            yield ms_layer

    def iter_layers(self, **kwargs):
        kwargs.setdefault("mra", {}).update({"is_model": lambda x: x != True})
        for ms_layer in self.iter_ms_layers(**kwargs):
            yield Layer(ms_layer)

    def get_layer(self, l_name):
        try:
            return next(self.iter_layers(attr={"name": l_name}))
        except StopIteration:
            raise KeyError(l_name)

    def move_layer_down(self, l_name):
        layer = self.get_layer(l_name)
        self.ms.moveLayerDown(layer.ms.index)

    def has_layer(self, l_name):
        try:
            self.get_layer(l_name)
        except KeyError:
            return False
        else:
            return True

    def get_layer_wsm(self, l_name):
        layer = self.get_layer(l_name)
        ws = self.get_workspace(layer.get_mra_metadata("workspace"))
        model = ws.get_model(m_name=layer.get_mra_metadata("name"),
                             s_type=layer.get_mra_metadata("type"),
                             s_name=layer.get_mra_metadata("storage"))
        return layer, ws, model

    def create_layer(self, ws, model, l_name, l_enabled, l_metadata={}):
        # First create the layer, then configure it.

        if self.has_layer(l_name):
            raise KeyExists(l_name)

        # We still clone, because we have to start from something,
        # but everything should be configured() anyway.
        layer = Layer(model.ms.clone())
        self.ms.insertLayer(layer.ms)

        # is queryable (by default):
        layer.ms.template = "foo.html" # TODO: Support html format response
        l_metadata.setdefault("wms_title", l_name)
        l_metadata.setdefault("wms_abstract", l_name)
        l_metadata.setdefault("wms_bbox_extended", "true")
        # TODO: Add other default values as above:
        # wms_keywordlist, wms_keywordlist_vocabulary
        # wms_keywordlist_<vocabulary>_items
        # wms_srs
        # wms_dataurl_(format|href)
        # ows_attribution_(title|onlineresource)
        # ows_attribution_logourl_(href|format|height|width)
        # ows_identifier_(authority|value)
        # ows_authorityurl_(name|href)
        # ows_metadataurl_(type|href|format)

        # TODO: pass correct data (title, abstract, ...) to layer.update()
        layer.update(l_metadata)
        layer.set_default_style(self)
    def delete_layer(self, l_name):
        layer = self.get_layer(l_name)
        self.ms.removeLayer(layer.ms.index)

    # Layergroups

    def create_layergroup(self, lg_name, mra_metadata={}):
        with self.mra_metadata("layergroups", {}) as layergroups:
            if lg_name in layergroups:
                raise KeyExists(lg_name)
            layergroups[lg_name] = mra_metadata
        return LayerGroup(lg_name, self)

    def iter_layergroups(self):
        return (LayerGroup(name, self) for name in self.get_mra_metadata("layergroups", {}).iterkeys())

    def get_layergroup(self, lg_name):
        if lg_name in self.get_mra_metadata("layergroups", {}):
            return LayerGroup(lg_name, self)
        else:
            raise KeyError(lg_name)

    def add_to_layergroup(self, lg_name, *args):
        lg = self.get_layergroup(lg_name)
        lg.add(*args)

    def remove_from_layergroup(self, lg_name, *args):
        lg = self.get_layergroup(lg_name)
        lg.remove(*args)

    def delete_layergroup(self, lg_name):
        layer_group = self.get_layergroup(lg_name)
        # Remove all the layers from this group.
        for layer in self.iter_layers(attr={"group": layer_group.name}):
            layer_group.remove(layer)
        # Remove the group from mra metadata.
        with self.mra_metadata("layergroups", {}) as layergroups:
    # Styles:

    def iter_styles(self):
        return iter(self.get_styles())