diff --git a/controllers/budget.py b/controllers/budget.py index 8d07c9e84b..112e8da370 100755 --- a/controllers/budget.py +++ b/controllers/budget.py @@ -283,7 +283,6 @@ def kit_export_pdf(): """ Export a list of Kits in Adobe PDF format Uses Geraldo SubReport - @ToDo: Use S3PDF Method """ try: from reportlab.lib.units import cm @@ -452,7 +451,6 @@ def item_export_pdf(): """ Export a list of Items in Adobe PDF format Uses Geraldo Grouping Report - @ToDo: Use S3PDF Method """ try: from reportlab.lib.units import cm diff --git a/controllers/cms.py b/controllers/cms.py index 69a80ba162..d087827d78 100644 --- a/controllers/cms.py +++ b/controllers/cms.py @@ -242,7 +242,7 @@ def prep(r): modules = {} _modules = current.deployment_settings.modules for module in _modules: - if module in ("appadmin", "errors", "ocr"): + if module in ("appadmin", "errors"): continue modules[module] = _modules[module].get("name_nice") s3db.cms_post_module.field.requires = \ diff --git a/languages/de.py b/languages/de.py index d3677c52b5..b218fe4e05 100644 --- a/languages/de.py +++ b/languages/de.py @@ -1264,7 +1264,7 @@ 'Create Item Pack': 'Packung erstellen', 'Create Item Type': 'Gegenstandsart anlegen', 'Create Item': 'Neuen Artikel anlegen', -'Create Job Title': 'Tätigkeitsbezeichnung erstellen', +'Create Job Title': 'Tätigkeit anlegen', 'Create Kit': 'Ausstattung (Kit) anlegen', 'Create Kitting': 'Kit zusammenstellen', 'Create Layer': 'Kartenebene anlegen', @@ -1592,6 +1592,7 @@ 'Delete Item from Request': 'Artikel aus Anfrage entfernen', 'Delete Item': 'Eintrag löschen', 'Delete Job Role': 'Tätigkeit löschen', +'Delete Job Title': 'Tätigkeit löschen', 'Delete Key': 'Schlüssel löschen', 'Delete Kit': 'Ausstattung (Kit) löschen', 'Delete Layer': 'Ebene löschen', @@ -1943,6 +1944,7 @@ 'Edit Item in Request': 'Angefragten Artikel bearbeiten', 'Edit Item': 'Artikel bearbeiten', 'Edit Job Role': 'Tätigkeit bearbeiten', +'Edit Job Title': 'Tätigkeit bearbeiten', 'Edit Key': 'Schlüssel bearbeiten', 'Edit Kit': 'Ausstattung (Kit) bearbeiten', 'Edit Layer': 'Kartenebene bearbeiten', @@ -2916,9 +2918,13 @@ 'Job Role': 'Tätigkeit', 'Job Roles': 'Tätigkeiten', 'Job Seeking': 'Arbeitssuche', -'Job Title Catalog': 'Katalog der Tätigkeitsbezeichnungen', -'Job Title': 'Tätigkeitsbezeichnung', -'Job Titles': 'Tätigkeitsbezeichnungen', +'Job Title Catalog': 'Tätigkeitskatalog', +'Job Title Details': 'Details zur Tätigkeit', +'Job Title added': 'Tätigkeit hinzugefügt', +'Job Title deleted': 'Tätigkeit gelöscht', +'Job Title updated': 'Tätigkeit aktualisiert', +'Job Title': 'Tätigkeit', +'Job Titles': 'Tätigkeiten', 'Jordan': 'Jordanien', 'Journal Entry Details': 'Details zum Journaleintrag', 'Journal entry added': 'Journaleintrag hinzugefügt', @@ -3130,6 +3136,7 @@ 'List Items in Stock': 'Liste Bestandsartikel', 'List Items': 'Liste der Artikel', 'List Job Roles': 'Liste der Tätigkeiten', +'List Job Titles': 'Liste Tätigkeiten', 'List Keys': 'Schlüssel auflisten', 'List Kits': 'Liste Ausstattungen (Kits)', 'List Layers': 'Liste Layer', @@ -3211,6 +3218,7 @@ 'List Skill Types': 'Liste der Typen von Fähigkeiten', 'List Skills': 'Liste Fähigkeiten', 'List Solutions': 'Liste Lösungen', +'List Staff Records': 'Liste Mitarbeiterakten', 'List Staff Types': 'Mitarbeitertypen auflisten', 'List Status': 'Status auflisten', 'List Stock in Warehouse': 'Liste Bestände im Warenlager', diff --git a/models/00_tables.py b/models/00_tables.py index 4824a7a98f..e961923e74 100644 --- a/models/00_tables.py +++ b/models/00_tables.py @@ -38,7 +38,6 @@ import s3db.irs import s3db.member import s3db.msg -import s3db.ocr import s3db.org import s3db.patient import s3db.po @@ -91,10 +90,7 @@ ) # ============================================================================= -# Make available for S3Models -# - legacy for backwards compatibility w docs & custom modules -from s3.s3fields import S3ReusableField, s3_comments, s3_meta_fields -s3.comments = s3_comments -s3.meta_fields = s3_meta_fields +# Make available for controllers +from s3 import S3ReusableField, s3_comments, s3_meta_fields # END ========================================================================= diff --git a/models/00_utils.py b/models/00_utils.py index 1670a7437c..877d3752fe 100644 --- a/models/00_utils.py +++ b/models/00_utils.py @@ -229,19 +229,14 @@ def s3_rest_controller(prefix=None, resourcename=None, **attr): set_handler("timeplot", s3base.S3TimePlot) set_handler("xform", s3base.S3XForms) - # Don't load S3PDF unless needed (very slow import with Reportlab) - method = r.method - if method == "import" and r.representation == "pdf": - from s3.s3pdf import S3PDF - set_handler("import", S3PDF(), - http = ("GET", "POST"), - representation = "pdf" - ) - # Plugin OrgRoleManager when appropriate s3base.S3OrgRoleManager.set_method(r) - # List of methods which can have custom action buttons + # List of methods rendering datatables with default action buttons + dt_methods = (None, "datatable", "datatable_f", "summary") + + # List of methods rendering datatables with custom action buttons, + # => for these, s3.actions must not be touched, see below # (defining here allows postp to add a custom method to the list) s3.action_methods = ("import", "review", @@ -253,14 +248,8 @@ def s3_rest_controller(prefix=None, resourcename=None, **attr): # Execute the request output = r(**attr) - if isinstance(output, dict) and \ - method in (None, - "report", - "search", - "datatable", - "datatable_f", - "summary", - ): + method = r.method + if isinstance(output, dict) and method in dt_methods: if s3.actions is None: diff --git a/models/zz_last.py b/models/zz_last.py index 8c7e313d80..006f2d84f6 100644 --- a/models/zz_last.py +++ b/models/zz_last.py @@ -47,5 +47,5 @@ menu.breadcrumbs = S3OptionsMenu.breadcrumbs # Set up plugins -from plugins import PluginLoader -PluginLoader.setup_all() +#from plugins import PluginLoader +#PluginLoader.setup_all() diff --git a/modules/s3/__init__.py b/modules/s3/__init__.py index 50b743c7f8..fc0c05505c 100755 --- a/modules/s3/__init__.py +++ b/modules/s3/__init__.py @@ -116,9 +116,6 @@ # De-duplication from .s3merge import S3Merge -# Don't load S3PDF unless needed (very slow import with reportlab) -#from .s3pdf import S3PDF - # Advanced Framework ========================================================== # # Tracking System diff --git a/modules/s3/codecs/pdf.py b/modules/s3/codecs/pdf.py index 78d1bb6dfb..66201bc362 100644 --- a/modules/s3/codecs/pdf.py +++ b/modules/s3/codecs/pdf.py @@ -965,7 +965,7 @@ class S3PDFTable(object): Class to build a table that can then be placed in a pdf document The table will be formatted so that is fits on the page. This class - doesn't need to be called directly. Rather see S3PDF.addTable() + doesn't need to be called directly. Rather see S3RL_PDF.get_resource_flowable """ MIN_COL_WIDTH = 200 diff --git a/modules/s3/s3crud.py b/modules/s3/s3crud.py index d78c78926c..96c0b43121 100644 --- a/modules/s3/s3crud.py +++ b/modules/s3/s3crud.py @@ -481,11 +481,6 @@ def create(self, r, **attr): else: session.confirmation = current.T("Data uploaded") - elif representation == "pdf": - from .s3pdf import S3PDF - exporter = S3PDF() - return exporter(r, **attr) - elif representation == "url": results = self.import_url(r) return results diff --git a/modules/s3/s3gis.py b/modules/s3/s3gis.py index 623b7ab00f..12ff253ec9 100644 --- a/modules/s3/s3gis.py +++ b/modules/s3/s3gis.py @@ -76,6 +76,7 @@ from .s3rtb import S3ResourceTree from .s3track import S3Trackable from .s3utils import s3_include_ext, s3_include_underscore, s3_str +from .s3validators import JSONERRORS # Map WKT types to db types GEOM_TYPES = {"point": 1, @@ -225,6 +226,8 @@ "Zoo" ) +DEFAULT = lambda: None + # ----------------------------------------------------------------------------- class GIS(object): """ @@ -456,7 +459,7 @@ def fetch_kml(self, url, filepath, session_id_name, session_id): url = element.text if url: # Follow NetworkLink (synchronously) - warning2 = self.fetch_kml(url, filepath) + warning2 = self.fetch_kml(url, filepath, session_id_name, session_id) warning += warning2 except (etree.XMLSyntaxError,): e = sys.exc_info()[1] @@ -547,7 +550,7 @@ def geocode(address, postcode=None, Lx_ids=None, geocoder=None): else: raise NotImplementedError - geocode_ = lambda names, g=g, **kwargs: g.geocode(names, **kwargs) + geocode_ = lambda names, inst=g, **kwargs: inst.geocode(names, **kwargs) location = address if postcode: @@ -620,7 +623,7 @@ def geocode(address, postcode=None, Lx_ids=None, geocoder=None): if not results: output = "Can't check that these results are specific enough" for result in results: - place2, (lat2, lon2) = result + place2 = result[0] if place == place2: output = "We can only geocode to the Lx" break @@ -1076,18 +1079,19 @@ def get_parent_bounds(parent): orderby=table.level) row_list = rows.as_list() row_list.reverse() - ok = False + ok, bounds = False, None for row in row_list: if row["lon_min"] is not None and row["lon_max"] is not None and \ row["lat_min"] is not None and row["lat_max"] is not None and \ row["lon"] != row["lon_min"] != row["lon_max"] and \ row["lat"] != row["lat_min"] != row["lat_max"]: + bounds = row["lat_min"], row["lon_min"], row["lat_max"], row["lon_max"], row["name"] ok = True break if ok: # This level is suitable - return row["lat_min"], row["lon_min"], row["lat_max"], row["lon_max"], row["name"] + return bounds else: # This level is suitable @@ -1390,6 +1394,8 @@ def set_config(config_id=None, force_update_cache=False): @ToDo: Merge configs for Event """ + cache = Storage() + _gis = current.response.s3.gis # If an id has been supplied, try it first. If it matches what's in @@ -1397,7 +1403,7 @@ def set_config(config_id=None, force_update_cache=False): if config_id and not force_update_cache and \ _gis.config and \ _gis.config.id == config_id: - return + return cache db = current.db s3db = current.s3db @@ -1436,7 +1442,6 @@ def set_config(config_id=None, force_update_cache=False): mtable.on(mtable.id == stable.marker_id), ) - cache = Storage() row = None rows = None if config_id: @@ -4621,28 +4626,42 @@ def import_geonames(self, country, level=None): # Parse File current_row = 0 + + def in_bbox(row, bbox): + return (row.lon_min < bbox[0]) & \ + (row.lon_max > bbox[1]) & \ + (row.lat_min < bbox[2]) & \ + (row.lat_max > bbox[3]) for line in f: current_row += 1 # Format of file: http://download.geonames.org/export/dump/readme.txt - geonameid, \ - name, \ - asciiname, \ - alternatenames, \ - lat, \ - lon, \ - feature_class, \ - feature_code, \ - country_code, \ - cc2, \ - admin1_code, \ - admin2_code, \ - admin3_code, \ - admin4_code, \ - population, \ - elevation, \ - gtopo30, \ - timezone, \ - modification_date = line.split("\t") + # - tab-limited values, columns: + # geonameid : integer id of record in geonames database + # name : name of geographical point (utf8) + # asciiname : name of geographical point in plain ascii characters + # alternatenames : alternatenames, comma separated + # latitude : latitude in decimal degrees (wgs84) + # longitude : longitude in decimal degrees (wgs84) + # feature class : see http://www.geonames.org/export/codes.html + # feature code : see http://www.geonames.org/export/codes.html + # country code : ISO-3166 2-letter country code + # cc2 : alternate country codes, comma separated, ISO-3166 2-letter country code + # admin1 code : fipscode (subject to change to iso code) + # admin2 code : code for the second administrative division + # admin3 code : code for third level administrative division + # admin4 code : code for fourth level administrative division + # population : bigint + # elevation : in meters + # dem : digital elevation model, srtm3 or gtopo30 + # timezone : the iana timezone id + # modification date : date of last modification in yyyy-MM-dd format + # + parsed = line.split("\t") + geonameid = parsed[0] + name = parsed[1] + lat = parsed[4] + lon = parsed[5] + feature_code = parsed[7] if feature_code == fc: # Add WKT @@ -4659,12 +4678,7 @@ def import_geonames(self, country, level=None): # Locate Parent parent = "" # 1st check for Parents whose bounds include this location (faster) - def in_bbox(row): - return (row.lon_min < lon_min) & \ - (row.lon_max > lon_max) & \ - (row.lat_min < lat_min) & \ - (row.lat_max > lat_max) - for row in all_parents.find(lambda row: in_bbox(row)): + for row in all_parents.find(lambda row: in_bbox(row, [lon_min, lon_max, lat_min, lat_max])): # Search within this subset with a full geometry check # Uses Shapely. # @ToDo provide option to use PostGIS/Spatialite @@ -4887,7 +4901,7 @@ def propagate(parent): feature["inherited"] = False update_location_tree(feature) # all_locations is False here # All Done! - return + return None # Single Feature @@ -6294,11 +6308,11 @@ def shrink_polygon(shape): return output # ------------------------------------------------------------------------- - def show_map(self, - id = "default_map", + @staticmethod + def show_map(id = "default_map", height = None, width = None, - bbox = {}, + bbox = DEFAULT, lat = None, lon = None, zoom = None, @@ -6314,7 +6328,7 @@ def show_map(self, features = None, feature_queries = None, feature_resources = None, - wms_browser = {}, + wms_browser = DEFAULT, catalogue_layers = False, legend = False, toolbar = False, @@ -6332,7 +6346,7 @@ def show_map(self, scaleline = None, zoomcontrol = None, zoomWheelEnabled = True, - mgrs = {}, + mgrs = DEFAULT, window = False, window_hide = False, closable = True, @@ -6436,6 +6450,13 @@ def show_map(self, """ + if bbox is DEFAULT: + bbox = {} + if wms_browser is DEFAULT: + wms_browser = {} + if mgrs is DEFAULT: + mgrs = {} + return MAP(id = id, height = height, width = width, @@ -6498,17 +6519,10 @@ def __init__(self, **opts): # We haven't yet run _setup() self.setup = False - self.callback = None - self.error_message = None - self.components = [] # Options for server-side processing self.opts = opts opts_get = opts.get - self.id = map_id = opts_get("id", "default_map") - - # Options for client-side processing - self.options = {} # Adapt CSS to size of Map _class = "map_wrapper" @@ -6516,9 +6530,17 @@ def __init__(self, **opts): _class = "%s fullscreen" % _class if opts_get("print_mode"): _class = "%s print" % _class - self.attributes = {"_class": _class, - "_id": map_id, - } + + # Set Map ID + self.id = map_id = opts_get("id", "default_map") + + super(MAP, self).__init__(_class=_class, _id=map_id) + + # Options for client-side processing + self.options = {} + + self.callback = None + self.error_message = None self.parent = None # Show Color Picker? @@ -6532,6 +6554,11 @@ def __init__(self, **opts): if style not in s3.stylesheets: s3.stylesheets.append(style) + self.globals = None + self.i18n = None + self.scripts = None + self.plugin_callbacks = None + # ------------------------------------------------------------------------- def _setup(self): """ @@ -7368,18 +7395,17 @@ def __init__(self, **opts): """ self.opts = opts + opts_get = opts.get # Pass options to DIV() - opts_get = opts.get map_id = opts_get("id", "default_map") - height = opts_get("height", current.deployment_settings.get_gis_map_height()) - self.attributes = {"_id": map_id, - "_style": "height:%ipx;width:100%%" % height, - } - # @ToDo: Add HTML Controls (Toolbar, LayerTree, etc) - self.components = [DIV(_class = "s3-gis-tooltip", - ), - ] + height = opts_get("height") + if height is None: + height = current.deployment_settings.get_gis_map_height() + super(MAP2, self).__init__(DIV(_class = "s3-gis-tooltip"), + _id = map_id, + _style = "height:%ipx;width:100%%" % height, + ) # Load CSS now as too late in xml() stylesheets = current.response.s3.stylesheets @@ -7686,15 +7712,15 @@ def _options(self): layer_types = set(layer_types) scripts = [] scripts_append = scripts.append - for LayerType in layer_types: + for layer_type_class in layer_types: try: # Instantiate the Class - layer = LayerType(layers) + layer = layer_type_class(layers) layer.as_dict(options) for script in layer.scripts: scripts_append(script) except Exception as exception: - error = "%s not shown: %s" % (LayerType.__name__, exception) + error = "%s not shown: %s" % (layer_type_class.__name__, exception) current.log.error(error) response = current.response if response.s3.debug: @@ -7825,10 +7851,10 @@ def addFeatureQueries(feature_queries): # Lat/Lon via Join or direct? try: - layer["query"][0].gis_location.lat - join = True - except: - join = False + join = hasattr(layer["query"][0].gis_location, "lat") + except (AttributeError, KeyError): + # Invalid layer + continue # Push the Features into a temporary table in order to have them accessible via GeoJSON # @ToDo: Maintenance Script to clean out old entries (> 24 hours?) @@ -8025,7 +8051,7 @@ def addFeatureResources(feature_resources): try: # JSON Object? style = json.loads(style) - except: + except JSONERRORS: current.log.error("Invalid Style: %s" % style) style = None else: @@ -8119,7 +8145,7 @@ def addFeatureResources(feature_resources): try: # JSON Object? style = json.loads(style) - except: + except JSONERRORS: current.log.error("Invalid Style: %s" % style) style = None if not style: @@ -8172,6 +8198,10 @@ class Layer(object): Abstract base class for Layers from Catalogue """ + tablename = None + dictname = "layer_generic" + style = False + def __init__(self, all_layers, openlayers=6): self.openlayers = openlayers @@ -8193,7 +8223,6 @@ def __init__(self, all_layers, openlayers=6): query = (table.layer_id.belongs(set(layer_ids))) rows = current.db(query).select(*fields) - SubLayer = self.SubLayer # Flag to show whether we've set the default baselayer # (otherwise a config higher in the hierarchy can overrule one lower down) base = True @@ -8210,12 +8239,16 @@ def __init__(self, all_layers, openlayers=6): layer_id = record.layer_id # Find the 1st row in all_layers which matches this - for row in all_layers: - if row["gis_layer_config.layer_id"] == layer_id: - layer_config = row["gis_layer_config"] + row = None + for candidate in all_layers: + if candidate["gis_layer_config.layer_id"] == layer_id: + row = candidate break + if not row: + continue # Check if layer is enabled + layer_config = row["gis_layer_config"] if layer_config.enabled is False: continue @@ -8247,7 +8280,7 @@ def __init__(self, all_layers, openlayers=6): # databases: try: style_dict = json.loads(style_dict) - except ValueError: + except JSONERRORS: pass if style_dict: record["style"] = style_dict @@ -8285,7 +8318,7 @@ def __init__(self, all_layers, openlayers=6): # SubLayers handled differently append(record) else: - append(SubLayer(record, openlayers)) + append(self.SubLayer(record, openlayers)) # Alphasort layers # - client will only sort within their type: s3.gis.layers.js @@ -8307,13 +8340,11 @@ def as_dict(self, options=None): # Add this layer to the list of layers for this layer type append(sublayer_dict) - if sublayer_dicts: - if options: - # Used by Map._setup() - options[self.dictname] = sublayer_dicts - else: - # Used by as_json() and hence as_javascript() - return sublayer_dicts + if sublayer_dicts and options: + # Used by Map._setup() + options[self.dictname] = sublayer_dicts + + return sublayer_dicts # ------------------------------------------------------------------------- def as_json(self): @@ -8323,8 +8354,9 @@ def as_json(self): result = self.as_dict() if result: - #return json.dumps(result, indent=4, separators=(",", ": "), sort_keys=True) return json.dumps(result, separators=SEPARATORS) + else: + return "" # ------------------------------------------------------------------------- def as_javascript(self): @@ -8336,6 +8368,8 @@ def as_javascript(self): result = self.as_json() if result: return '''S3.gis.%s=%s\n''' % (self.dictname, result) + else: + return "" # ------------------------------------------------------------------------- class SubLayer(object): @@ -8353,6 +8387,9 @@ def __init__(self, record, openlayers): if hasattr(self, "projection_id"): self.projection = Projection(self.projection_id) + def __getattr__(self, key): + return self.__dict__.__getattribute__(key) + def setup_clustering(self, output): if hasattr(self, "cluster_attribute"): cluster_attribute = self.cluster_attribute @@ -8440,6 +8477,7 @@ class LayerBing(Layer): # ------------------------------------------------------------------------- def as_dict(self, options=None): + sublayers = self.sublayers if sublayers: if Projection().epsg != 900913: @@ -8468,9 +8506,11 @@ def as_dict(self, options=None): if options: # Used by Map._setup() options[self.dictname] = ldict - else: - # Used by as_json() and hence as_javascript() - return ldict + else: + ldict = None + + # Used by as_json() and hence as_javascript() + return ldict # ----------------------------------------------------------------------------- class LayerCoordinate(Layer): @@ -8496,9 +8536,11 @@ def as_dict(self, options=None): if options: # Used by Map._setup() options[self.dictname] = ldict - else: - # Used by as_json() and hence as_javascript() - return ldict + else: + ldict = None + + # Used by as_json() and hence as_javascript() + return ldict # ----------------------------------------------------------------------------- class LayerEmpty(Layer): @@ -8513,6 +8555,7 @@ class LayerEmpty(Layer): # ------------------------------------------------------------------------- def as_dict(self, options=None): + sublayers = self.sublayers if sublayers: sublayer = sublayers[0] @@ -8526,9 +8569,11 @@ def as_dict(self, options=None): if options: # Used by Map._setup() options[self.dictname] = ldict - else: - # Used by as_json() and hence as_javascript() - return ldict + else: + ldict = None + + # Used by as_json() and hence as_javascript() + return ldict # ----------------------------------------------------------------------------- class LayerFeature(Layer): @@ -8563,7 +8608,7 @@ def __init__(self, record, openlayers): def as_dict(self): if self.skip: # Skip layer - return + return None # @ToDo: Option to force all filters via POST? if self.aggregate: # id is used for url_format @@ -8795,7 +8840,9 @@ class LayerGoogle(Layer): # ------------------------------------------------------------------------- def as_dict(self, options=None): + sublayers = self.sublayers + if sublayers: T = current.T spherical_mercator = (Projection().epsg == 900913) @@ -8877,9 +8924,11 @@ def as_dict(self, options=None): if options: # Used by Map._setup() options[self.dictname] = ldict - else: - # Used by as_json() and hence as_javascript() - return ldict + else: + ldict = None + + # Used by as_json() and hence as_javascript() + return ldict # ----------------------------------------------------------------------------- class LayerGPX(Layer): @@ -8930,18 +8979,22 @@ class LayerJS(Layer): # ------------------------------------------------------------------------- def as_dict(self, options=None): + + sublayer_dicts = [] + sublayers = self.sublayers if sublayers: - sublayer_dicts = [] append = sublayer_dicts.append for sublayer in sublayers: append(sublayer.code) if options: # Used by Map._setup() options[self.dictname] = sublayer_dicts - else: - # Used by as_json() and hence as_javascript() - return sublayer_dicts + else: + sublayer_dicts = [] + + # Used by as_json() and hence as_javascript() + return sublayer_dicts # ----------------------------------------------------------------------------- class LayerKML(Layer): @@ -9138,6 +9191,7 @@ class LayerOpenWeatherMap(Layer): # ------------------------------------------------------------------------- def as_dict(self, options=None): + sublayers = self.sublayers if sublayers: apikey = current.deployment_settings.get_gis_api_openweathermap() @@ -9158,9 +9212,11 @@ def as_dict(self, options=None): if options: # Used by Map._setup() options[self.dictname] = ldict - else: - # Used by as_json() and hence as_javascript() - return ldict + else: + ldict = None + + # Used by as_json() and hence as_javascript() + return ldict # ----------------------------------------------------------------------------- class LayerShapefile(Layer): @@ -9702,7 +9758,7 @@ def as_dict(self): # Native JSON try: style.style = json.loads(style.style) - except: + except JSONERRORS: current.log.error("Unable to decode Style: %s" % style.style) style.style = None output.style = style.style @@ -9813,7 +9869,6 @@ def widget(self, method = "map", widget_id = None, visible = True, - callback = None, **attr): """ Render a Map widget suitable for use in an S3Filter-based page @@ -9822,11 +9877,24 @@ def widget(self, @param r: the S3Request @param method: the widget method @param widget_id: the widget ID - @param callback: None by default in case DIV is hidden @param visible: whether the widget is initially visible @param attr: controller attributes + + @keyword callback: callback to show the map: + - "DEFAULT".............call show_map as soon as all + components are loaded and ready + (= use default show_map callback) + - custom JavaScript.....invoked as soon as all components + are loaded an ready + - None..................only load the components, map + will be shown by a later explicit + call to show_map (this is the default + here since the map DIV would typically + be hidden initially, e.g. summary tab) """ + callback = attr.get("callback") + if not widget_id: widget_id = "default_map" @@ -9959,8 +10027,6 @@ def export(self, r, **attr): if not current_lx: # or not current_lx.level: # Must have a location r.error(400, current.ERROR.BAD_REQUEST) - else: - self.lx = current_lx.id tables = [] # Parse the ?resources= parameter @@ -9971,7 +10037,9 @@ def export(self, r, **attr): resources = current.deployment_settings.get_gis_poi_export_resources() if not isinstance(resources, list): resources = [resources] - [tables.extend(t.split(",")) for t in resources] + + for t in resources: + tables.extend(t.split(",")) # Parse the ?update_feed= parameter update_feed = True @@ -9991,8 +10059,10 @@ def export(self, r, **attr): # Export a combined tree tree = self.export_combined_tree(tables, - msince=msince, - update_feed=update_feed) + msince = msince, + update_feed = update_feed, + lx = current_lx.id, + ) xml = current.xml @@ -10026,7 +10096,7 @@ def export(self, r, **attr): return output # ------------------------------------------------------------------------- - def export_combined_tree(self, tables, msince=None, update_feed=True): + def export_combined_tree(self, tables, msince=None, update_feed=True, lx=None): """ Export a combined tree of all records in tables, which are in Lx, and have been updated since msince. @@ -10035,14 +10105,13 @@ def export_combined_tree(self, tables, msince=None, update_feed=True): @param msince: minimum modified_on datetime, "auto" for automatic from feed data, None to turn it off @param update_feed: update the last_update datetime in the feed + @param lx: the id of the current Lx """ db = current.db s3db = current.s3db ftable = s3db.gis_poi_feed - lx = self.lx - elements = [] for tablename in tables: @@ -10184,7 +10253,7 @@ def apply_method(r, **attr): fields.insert(3, field) from .s3utils import s3_mark_required - labels, required = s3_mark_required(fields, ["file", "location_id"]) + labels = s3_mark_required(fields, ["file", "location_id"])[0] s3.has_required = True form = SQLFORM.factory(*fields, @@ -10203,7 +10272,7 @@ def apply_method(r, **attr): if form.accepts(request.vars, current.session): form_vars = form.vars if form_vars.file != "": - File = open(uploadpath + form_vars.file, "r") + osm_file = open(uploadpath + form_vars.file, "r") else: # Create .poly file if r.record: @@ -10259,8 +10328,8 @@ def apply_method(r, **attr): current.session.error = T("OSM file generation failed!") redirect(URL(args=r.id)) try: - File = open(filename, "r") - except: + osm_file = open(filename, "r") + except IOError: current.session.error = T("Cannot open created OSM file!") redirect(URL(args=r.id)) @@ -10268,7 +10337,7 @@ def apply_method(r, **attr): "osm", "import.xsl") ignore_errors = form_vars.get("ignore_errors", None) xml = current.xml - tree = xml.parse(File) + tree = xml.parse(osm_file) define_resource = s3db.resource response.error = "" import_count = 0 diff --git a/modules/s3/s3import.py b/modules/s3/s3import.py index a86465b24e..440410625f 100644 --- a/modules/s3/s3import.py +++ b/modules/s3/s3import.py @@ -216,9 +216,6 @@ def apply_method(self, r, **attr): except (KeyError, AttributeError): self.uploadTitle = T("Import") - # @todo: correct to switch this off for the whole session? - current.session.s3.ocr_enabled = False - # Reset all errors/warnings self.error = None self.warning = None diff --git a/modules/s3/s3pdf.py b/modules/s3/s3pdf.py deleted file mode 100644 index 21f0deac66..0000000000 --- a/modules/s3/s3pdf.py +++ /dev/null @@ -1,4583 +0,0 @@ -# -*- coding: utf-8 -*- - -""" Resource PDF Tools - - @see: U{B{I{S3XRC}} } - - @requires: U{B{I{ReportLab}} } - - ###################################################################### - DEPRECATION WARNING - - This class is being replaced by the S3RL_PDF codec - - Initially the reporting features will be replaced, with the OCR - process being removed at a later stage. - ###################################################################### - - @copyright: 2011-2021 (c) Sahana Software Foundation - @license: MIT - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -""" - -__all__ = ("S3PDF",) - -import datetime -import json -import math -import os -import re -import sys -import subprocess -import unicodedata - -from io import StringIO -from html.entities import name2codepoint - -try: - from lxml import etree -except ImportError: - sys.stderr.write("ERROR: lxml module needed for XML handling\n") - raise - -from gluon import * -from gluon.storage import Storage -from gluon.contenttype import contenttype - -from .s3datetime import S3DateTime -from .s3rest import S3Method -from .s3utils import s3_represent_value, s3_str, s3_validate -from .s3codec import S3Codec - -try: - from PIL import Image - from PIL import ImageOps - from PIL import ImageStat - PILImported = True -except(ImportError): - try: - import Image - import ImageOps - import ImageStat - PILImported = True - except(ImportError): - sys.stderr.write("S3 Debug: S3PDF: Python Image Library not installed\n") - PILImported = False -try: - from reportlab.pdfgen import canvas - from reportlab.lib.fonts import tt2ps - from reportlab.rl_config import canvas_basefontname as _baseFontName - from reportlab.platypus import BaseDocTemplate, PageTemplate - from reportlab.platypus.frames import Frame - from reportlab.platypus import Spacer, PageBreak, Paragraph - from reportlab.platypus import Table - from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle - from reportlab.lib.units import inch - from reportlab.lib import colors - from reportlab.lib.pagesizes import A4, LETTER, landscape, portrait - from reportlab.platypus.flowables import Flowable - reportLabImported = True -except ImportError: - sys.stderr.write("S3 Debug: S3PDF: Reportlab not installed\n") - reportLabImported = False - -# Maximum number of options a field can have -MAX_FORM_OPTIONS_LIMIT = 12 - -# Will be loaded with values during S3PDF apply_method -ERROR = Storage() - -DEBUG = False - -# ============================================================================= -def checkDependencies(r): - T = current.T - ERROR = Storage( - PIL_ERROR=T("PIL (Python Image Library) not installed"), - REPORTLAB_ERROR=T("ReportLab not installed"), - ) - # Check that the necessary reportLab classes were imported - if not reportLabImported: - r.error(501, ERROR.REPORTLAB_ERROR) - if not PILImported: - r.error(501, ERROR.PIL_ERROR) - # redirect() is not available in this scope - #current.session.error = self.ERROR.REPORTLAB_ERROR - #redirect(URL(extension="")) - - -# ============================================================================= -if reportLabImported: - - # ========================================================================= - class ChangePageTitle(Flowable): - def __init__(self, doc, newTitle): - Flowable.__init__(self) - self.doc = doc - self.title = newTitle - - def draw(self): - self.doc.title = self.title - - # ========================================================================= - class Overlay(Flowable): - def __init__(self, callback, data): - Flowable.__init__(self) - self.function = callback - self.data = data - - def draw(self): - self.function(self.canv, self.data) - - # ========================================================================= - class EdenDocTemplate(BaseDocTemplate): - """ - The standard document template for eden reports - It allows for the following page templates: - 1) First Page - 2) Even Page - 3) Odd Page - 4) Landscape Page - """ - - # --------------------------------------------------------------------- - def setPageTemplates(self, - first, - firstEnd, - even = None, - odd = None, - landscape = None, - ): - """ - Determine which page template to use - """ - - self.onfirst = first - self.onfirstEnd = firstEnd - if even: - self.oneven = even - else: - self.oneven = first - if odd: - self.onodd = odd - else: - self.onodd = first - if landscape: - self.onlandscape = landscape - else: - self.onlandscape = first - self.needLandscape = False - - # --------------------------------------------------------------------- - def handle_pageBegin(self): - """ - Determine which page template to use - """ - - self._handle_pageBegin() - if self.needLandscape: - self._handle_nextPageTemplate("landscape") - elif self.page %2 == 1: - self._handle_nextPageTemplate("odd") - else: - self._handle_nextPageTemplate("even") - - # --------------------------------------------------------------------- - def build(self, flowables, canvasmaker=canvas.Canvas): - """ - Build the document using the flowables. - - Set up the page templates that the document can use - - """ - - self._calc() # in case we changed margins sizes etc - showBoundary = 0 # for debugging set to 1 - frameT = Frame(self.leftMargin, - self.bottomMargin, - self.width, - self.height, - id="body", - showBoundary = showBoundary) - self.addPageTemplates([PageTemplate(id="first", - frames=frameT, - onPage=self.onfirst, - onPageEnd=self.onfirstEnd, - pagesize=self.pagesize), - PageTemplate(id="even", - frames=frameT, - onPage=self.oneven, - onPageEnd=self.onfirstEnd, - pagesize=self.pagesize), - PageTemplate(id="odd", - frames=frameT, - onPage=self.onodd, - onPageEnd=self.onfirstEnd, - pagesize=self.pagesize), - PageTemplate(id="landscape", - frames=frameT, - onPage=self.onlandscape, - pagesize=self.pagesize), - ]) - BaseDocTemplate.build(self, flowables, canvasmaker=canvasmaker) - -# ============================================================================= -class S3PDF(S3Method): - """ - Class to help generate PDF documents. - - A typical implementation would be as follows: - - exporter = s3base.S3PDF() - return exporter(xrequest, **attr) - - Currently this class supports two types of reports: - A List: Typically called from the icon shown in a search - For example inv/warehouse - A Header plus List: Typically called from a button on a form - For example ??? - - Add additional generic forms to the apply_method() function - For specialist forms a S3PDF() object will need to be created. - See the apply_method() for ideas on how to create a form, - but as a minimum the following structure is required: - - pdf = S3PDF() - pdf.newDocument(pdf.defaultTitle(resource)) - - # Add specific pages here - - return pdf.buildDoc() - """ - - # ------------------------------------------------------------------------- - def apply_method(self, r, **attr): - """ - Apply CRUD methods - - @param r: the S3Request - @param attr: dictionary of parameters for the method handler - The attributes that it knows about are: - * componentname - * formname - * list_fields - * report_groupby - * report_hide_comments - - @return: output object to send to the view - """ - - # --------------------------------------------------------------------- - def getParam(key): - """ - nested function to get the parameters passed into apply_method - - @todo find out if this has been done better elsewhere! :( - - This will first try and get the argument from the attr parameter, - if it's not here then try self._config() - """ - value = attr.get(key) - if value != None: - return value - return self._config(key) - - T = current.T - self.ERROR = ERROR = Storage( - NO_RECORDS=T("No records in this resource. Add one more records manually and then retry."), - TESSERACT_ERROR=T("%(app)s not installed. Ask the Server Administrator to install on Server.") % dict(app="Tesseract 3.01"), - EMPTY_OCR_FORM=T("Selected OCR Form has no pages. Use another revision of create a new revision by downloading a new Form."), - INVALID_IMAGE_TYPE=T("Uploaded file(s) are not Image(s). Supported image formats are '.png', '.jpg', '.bmp', '.gif'."), - OCR_DISABLED=T("OCR module is disabled. Ask the Server Administrator to enable it."), - IMAGE_MAGICK_ERROR=T("%(app)s not installed. Ask the Server Administrator to install on Server.") % dict(app="ImageMagick"), - NOT_PDF_FILE=T("Uploaded file is not a PDF file. Provide a Form in valid PDF Format."), - INVALID_PDF=T("Uploaded PDF file has more/less number of page(s) than required. Check if you have provided appropriate revision for your Form as well as check the Form contains appropriate number of pages."), - NO_UTC_OFFSET=T("No UTC offset found. Please set UTC offset in your 'User Profile' details. Example: UTC+0530"), - INVALID_JOBID=T("The provided 'jobuuid' is invalid. The session of Form upload is invalid. You should retry uploading."), - INVALID_FORMID=T("The provided 'formuuid' is invalid. You have selected a Form revision which does not exist on this server."), - UNRECOVERABLE_ERROR=T("The uploaded Form is unreadable, please do manual data entry."), - JOB_COMPLETE=T("This job has already been finished successfully."), - ) - - self.r = r - checkDependencies(r) - settings = current.deployment_settings - request = current.request - response = current.response - session = current.session - db = current.db - - if DEBUG: - content_disposition = "inline" - else: - content_disposition = "attachment" - - if settings.get_pdf_size() == "Letter": - self.paper_size = LETTER - else: - self.paper_size = A4 - - try: - self.logo = os.path.join(request.folder, - settings.get_pdf_logo()) - except: - self.logo = None - self.headerBanner = None - - method = self.method - - callback = getParam("callback") - if callback != None: - title = getParam("formname") - if title == None: - title = self.defaultTitle(self.resource) - header = getParam("header") - if header == None: - header = self.pageHeader - footer = getParam("footer") - if footer == None: - footer = self.pageFooter - filename = getParam("filename") - if filename == None: - filename = title - self.newDocument(title, - header=header, - footer=footer, - filename = filename) - try: - id = r.component_id - if id == None: - id = r.id - except: - try: - id = r.id - except: - id = None - - callback(self, id=id) - # Build the document - doc = self.buildDoc() - # Set content type and disposition headers - if response: - response.headers["Content-Type"] = contenttype(".pdf") - response.headers["Content-disposition"] = \ - "%s; filename=\"%s\"" % (content_disposition, - self.filename) - - # Return the stream - return doc - - elif r.http == "GET": - if self.method in ("read", "list"): - # Normal PDF output - # Get the configuration parameters - componentname = getParam("componentname") - title = getParam("formname") - list_fields = getParam("list_fields") - report_groupby = getParam("report_groupby") - report_hide_comments = getParam("report_hide_comments") - filename = getParam("filename") - if filename == None: - filename = title - - # Create the document shell - if title == None: - title = self.defaultTitle(self.resource) - self.newDocument(title, - header=self.pageHeader, - footer=self.pageFooter, - filename = filename) - - if "report_landscape" in attr: - self.setLandscape() - # get the header details, if appropriate - if "rheader" in attr and attr["rheader"]: - self.extractrHeader(attr["rheader"]) - self.addSpacer(3) - elif componentname: - self.addrHeader(self.resource, - list_fields, - report_hide_comments=report_hide_comments) - self.addSpacer(3) - # Add details to the document - if componentname == None: - # Document that only has a resource list - self.addTable(self.resource, - list_fields=list_fields, - report_groupby=report_groupby, - report_hide_comments=report_hide_comments) - else: - # Document that has a resource header and component list - # Get the raw data for the component - ptable = self.resource.table - ctable = db[componentname] - raw_data = [] - linkfield = None - for link in ptable._referenced_by: - if link[0] == componentname: - linkfield = link[1] - break - if linkfield != None: - query = ctable[linkfield] == self.record_id - records = db(query).select() - find_fields = [] - for component in self.resource.components.values(): - find_fields += component.readable_fields() - fields = [] - if list_fields: - for lf in list_fields: - for field in find_fields: - if field.name == lf: - fields.append(field) - break - else: - for field in find_fields: - if field.type == "id": - continue - if report_hide_comments and field.name == "comments": - continue - fields.append(field) - if not fields: - fields = [ptable.id] - label_fields = [f.label for f in fields] - - for record in records: - data = [] - for field in fields: - value = record[field.name] - text = s3_represent_value(field, - value=value, - strip_markup=True, - non_xml_output=True, - extended_comments=True - ) - data.append(text) - raw_data.append(data) - self.addTable(raw_data = raw_data, - list_fields=label_fields) - - if "report_footer" in attr: - self.addSpacer(3) - self.extractrHeader(attr["report_footer"]) - # Build the document - doc = self.buildDoc() - - # Set content type and disposition headers - if response: - response.headers["Content-Type"] = contenttype(".pdf") - response.headers["Content-disposition"] = \ - "%s; filename=\"%s\"" % (content_disposition, - self.filename) - - # Return the stream - return doc - - elif method == "create": - if current.deployment_settings.has_module("ocr"): - # Create an OCR PDF form - import uuid - formUUID = uuid.uuid1() - self.newOCRForm(formUUID) - - # Put values - self.OCRPDFManager() - - # Build the document - doc = self.buildDoc() - numPages = self.doc.numPages - layoutXML = self.__getOCRLayout() - self.__update_dbmeta(formUUID, layoutXML, numPages) - - # Set content type and disposition headers - if response: - response.headers["Content-Type"] = contenttype(".pdf") - response.headers["Content-disposition"] = \ - "%s; filename=\"%s\"" % (content_disposition, - self.filename) - - # Return the stream - return doc - - else: - # @ToDo: Produce a simple form - r.error(501, self.ERROR.OCR_DISABLED) - - elif method == "import": - # Render a review UI - if not current.deployment_settings.has_module("ocr"): - r.error(501, self.ERROR.OCR_DISABLED) - - authorised = self._permitted(method="create") - if not authorised: - r.unauthorised() - - try: - if r.component: - trigger = r.args[3] - else: - trigger = r.args[1] - except(IndexError): - trigger = None - - if trigger == "review": - try: - jobuuid = r.vars["jobuuid"] - except(KeyError): - r.error(501, current.ERROR.BAD_REQUEST) - - # Check if operation is valid on the given job_uuid - current.s3db.table("ocr_meta") - statustable = db.ocr_form_status - query = (statustable.job_uuid == jobuuid) - row = db(query).select(statustable.review_status, - statustable.job_has_errors, - statustable.image_set_uuid, - statustable.form_uuid, - limitby=(0, 1)).first() - if not row: - # No such job - r.error(501, self.ERROR.INVALID_JOBID) - - if row.review_status == 1: - # Job has already been reviewed - r.error(501, self.ERROR.JOB_COMPLETE) - - # Retrieve meta data - if row.job_has_errors == 1: - job_has_errors = True - else: - job_has_errors = False - - self.setuuid = row.image_set_uuid - - # Retrieve s3ocrxml - formuuid = row.form_uuid - metatable = db.ocr_meta - row = db(metatable.form_uuid == formuuid).select(metatable.s3ocrxml_file, - limitby=(0, 1)).first() - if not row: - r.error(501, self.ERROR.INVALID_FORMID) - - s3ocrxml_filename = row.s3ocrxml_file - f = open(os.path.join(r.folder, - "uploads", - "ocr_meta", - s3ocrxml_filename), - "rb") - s3ocrxml = f.read() - f.close() - - s3ocrdict = self.__s3ocrxml2dict(s3ocrxml) - - # Retrieve the job - import_job = self.resource.import_tree(None, None, - job_id=jobuuid, - commit_job=False, - ignore_errors=True) - - s3import_enabled = True - if s3import_enabled: - s3ocrdata = self.__importjob2data(import_job) - - else: - # Retrive s3ocr data xml - table = db.ocr_data_xml - query = (table.image_set_uuid == self.setuuid) - row = db(query).select(table.data_file, - limitby=(0, 1)).first() - - if not row: - r.error(501, current.ERROR.BAD_RECORD) - - s3ocrdataxml_filename = row.data_file - f = open(os.path.join(r.folder, - "uploads", - "ocr_payload", - s3ocrdataxml_filename), - "rb") - s3ocrdataxml = f.read() - f.close() - - s3ocrdata = self.__temp_ocrdataxml_parser(s3ocrdataxml) - - reviewform = self.__create_review_form(s3ocrdict, s3ocrdata) - - return response.render("_ocr_review.html", - dict(reviewform=reviewform) - ) - - elif trigger == "image": - # Do import job - try: - setuuid = r.vars["setuuid"] - resource_table = r.vars["resource_table"] - field_name = r.vars["field_name"] - except(KeyError): - r.error(501, current.ERROR.BAD_REQUEST) - - try: - value = r.vars["value"] - except(KeyError): - value = None - try: - sequence = r.vars["sequence"] - except(KeyError): - r.error(501, current.ERROR.BAD_REQUEST) - - # Load ocr tables - current.s3db.table("ocr_meta") - table = db.ocr_field_crops - if value: - query = (table.image_set_uuid == setuuid) & \ - (table.resource_table == resource_table) & \ - (table.field_name == field_name) & \ - (table.value == value) - row = db(query).select(table.image_file, - limitby=(0, 1)).first() - else: - query = (table.image_set_uuid == setuuid) & \ - (table.resource_table == resource_table) & \ - (table.field_name == field_name) & \ - (table.sequence == sequence) - row = db(query).select(table.image_file, - limitby=(0, 1)).first() - if not row: - r.error(501, current.ERROR.BAD_RECORD) - - format = row.image_file[-4:] - image_file = open(os.path.join(r.folder, - "uploads", - "ocr_payload", - row.image_file)) - image_file_content = image_file.read() - image_file.close() - # Set content type and disposition headers - if response: - response.headers["Content-Type"] = contenttype(format) - response.headers["Content-disposition"] = \ - "%s; filename=\"%s\"" % ("inline", - "tempimage%s" % format) - - # Return the stream - return image_file_content - - elif trigger == "import": - # Do import job - try: - setuuid = r.vars["setuuid"] - except(KeyError): - r.error(501, current.ERROR.BAD_REQUEST) - - # Check if operation is valid on the given set_uuid - statustable = current.s3db.ocr_form_status - query = (statustable.image_set_uuid == setuuid) - row = db(query).select(statustable.job_uuid, - limitby=(0, 1)).first() - if row: - # This set of images has already been imported - jobuuid = row.job_uuid - - if r.component: - # If component - request_args = request.get("args", ["", ""]) - record_id = request_args[0] - component_name = request_args[1] - urlprefix = "%s/%s/%s" % (request.function, - record_id, - component_name) - else: - # Not a component - urlprefix = request.function - - redirect(URL(request.controller, - "%s/upload.pdf" % urlprefix, - args="review", - vars={"jobuuid":jobuuid})) - - table = db.ocr_data_xml - row = db(table.image_set_uuid == setuuid).select(table.data_file, - table.form_uuid, - limitby=(0, 1) - ).first() - if not row: - r.error(501, current.ERROR.BAD_RECORD) - - data_file = open(os.path.join(r.folder, - "uploads", - "ocr_payload", - row.data_file)) - formuuid = row.form_uuid - - datafile_content = data_file.read() - data_file.close() - - metatable = db.ocr_meta - row = db(metatable.form_uuid == formuuid).select(metatable.s3ocrxml_file, - limitby=(0, 1) - ).first() - if not row: - r.error(501, self.ERROR.INVALID_FORMID) - - s3ocrxml_filename = row.s3ocrxml_file - f = open(os.path.join(r.folder, - "uploads", - "ocr_meta", - s3ocrxml_filename), - "rb") - s3ocrxml = f.read() - f.close() - - s3ocrdict = self.__s3ocrxml2dict(s3ocrxml) - crosslimit_options = {} - for resourcename in s3ocrdict["$resource_seq"]: - resource = s3ocrdict[resourcename] - for fieldname in resource["$field_seq"]: - field = resource[fieldname] - if field.has_options: - if field.options and \ - field.options.count > MAX_FORM_OPTIONS_LIMIT: - if resourcename not in crosslimit_options: - crosslimit_options[resourcename] = [fieldname] - else: - crosslimit_options[resourcename].append(fieldname) - - if len(crosslimit_options) != 0: - s3xml_root = etree.fromstring(datafile_content) - resource_element = s3xml_root.getchildren()[0] - resourcename = resource_element.attrib.get("name") - for field in resource_element: - if field.tag == "data": - if resourcename in crosslimit_options: - fieldname = field.attrib.get("field") - if fieldname in crosslimit_options[resourcename]: - match_status = {} - value = s3_str(field.text).lower() - for option in s3ocrdict[resourcename][fieldname].options.list: - try: - fieldtext = option.label.lower() - except: - fieldtext = "" - match_status[option.value] =\ - self.dameraulevenshtein(cast2ascii(fieldtext), - cast2ascii(value)) - - closematch_value = 1000000000 - closematch = [] - - for match in match_status.keys(): - if match_status[match] < closematch_value: - closematch = [match] - closematch_value = match_status[match] - elif match_status[match] == closematch_value: - closematch.append(match) - - if len(closematch) > 0: - value = closematch[0] - else: - value = "" - - field.text = value - field.attrib["value"] = value - - - elif field.tag == "resource": - resourcename = field.attrib.get("name") - for subfield in field: - if subfield.tag == "data": - fieldname = subfield.attrib.get("field") - if resourcename in crosslimit_options and\ - fieldname in crosslimit_options[resourcename]: - match_status = {} - value = s3_str(subfield.text).lower() - for option in s3ocrdict[resourcename][fieldname].options.list: - try: - fieldtext = option.label.lower() - except: - fieldtext = "" - match_status[option.value] =\ - self.dameraulevenshtein(cast2ascii(fieldtext), - cast2ascii(value)) - - closematch_value = 1000000000 - closematch = [] - - for match in match_status.keys(): - if match_status[match] < closematch_value: - closematch = [match] - closematch_value = match_status[match] - elif match_status[match] == closematch_value: - closematch.append(match) - - if len(closematch) > 0: - value = closematch[0] - else: - value = "" - - subfield.text = value - subfield.attrib["value"] = value - - datafile_content = etree.tostring(s3xml_root) - - # import_xml routine - outputjson = self.resource.import_xml(StringIO(datafile_content), - commit_job=False, - ignore_errors=True) - - # Get metadata for review - jobuuid = self.resource.job.job_id - json2dict = json.loads(outputjson, strict=False) - - if "message" in json2dict: - jobhaserrors = 1 - else: - jobhaserrors = 0 - - # Check status code - if json2dict.get("statuscode") != "200": - r.error(501, self.ERROR.UNRECOVERABLE_ERROR) - - # Store metadata for review - db.ocr_form_status.insert(image_set_uuid=setuuid, - form_uuid=formuuid, - job_uuid=jobuuid, - job_has_errors=jobhaserrors) - - if r.component: - request_args = request.get("args", ["", ""]) - record_id = request_args[0] - component_name = request_args[1] - urlprefix = "%s/%s/%s" % (request.function, - record_id, - component_name) - - else: - # Not a component - urlprefix = request.function - - redirect(URL(request.controller, - "%s/upload.pdf" % urlprefix, - args="review", - vars={"jobuuid":jobuuid})) - - else: - # Render upload UI - - # Check if user has UTC offset in his profile - auth = current.auth - if auth.user: - utc_offset = auth.user.utc_offset - else: - r.error(501, self.ERROR.NO_UTC_OFFSET) - - # Load OCR tables - current.s3db.ocr_meta - - # Create an html image upload form for user - formuuid = r.vars.get("formuuid", None) - uploadformat = r.vars.get("uploadformat", None) - requesturl = request.env.path_info - createurl = "%s/create.pdf" %\ - requesturl[0:requesturl.rfind("/")] - if not (formuuid and uploadformat): - availForms = self.__getResourceForms() - return response.render("_ocr_upload.html", - dict(availForms=availForms, - createurl=createurl)) - else: - try: - numpages = self.__getNumPages(formuuid) - except: - r.error(501, current.ERROR.BAD_RECORD) - - if not numpages: - r.error(501, self.ERROR.EMPTY_OCR_FORM) - - return response.render("_ocr_page_upload.html", - dict(numpages=numpages, - posturl=createurl, - formuuid=formuuid, - uploadformat=uploadformat)) - - numpages = self.__getNumPages(formuuid) - if not numpages: - r.error(501, self.ERROR.EMPTY_OCR_FORM) - - return response.render("_ocr_page_upload.html", - dict(numpages=numpages, - posturl=createurl, - formuuid=formuuid, - uploadformat=uploadformat)) - - else: - r.error(405, current.ERROR.BAD_METHOD) - - elif r.http == "POST": - if method == "create": - # Upload scanned OCR images - if not current.deployment_settings.has_module("ocr"): - r.error(501, self.ERROR.OCR_DISABLED) - - # Form meta vars - formuuid = r.vars.formuuid - numpages = int(r.vars.numpages) - uploadformat = r.vars.uploadformat - - # Set id for given form - import uuid - setuuid = uuid.uuid1() - - # Load model - current.s3db.ocr_meta - - # Check for upload format - if uploadformat == "image": - # store each page into db/disk - payloadtable = db.ocr_payload - for eachpage in range(1, numpages + 1): - varname = "page%s" % eachpage - fileholder = r.vars[varname] - pagenumber = eachpage - - # server side file validation - imgfilename = fileholder.filename - extension = lambda m: m[m.rfind(".") + 1:] - imageformats = ["jpg", "png", "gif", "bmp"] - - if extension(imgfilename) not in imageformats: - r.error(501, self.ERROR.INVALID_IMAGE_TYPE) - - # store page - payloadtable.insert( - image_set_uuid=setuuid, - image_file=payloadtable["image_file"].store(\ - fileholder.file, - fileholder.filename), - page_number=pagenumber) - - elif uploadformat == "pdf": - fileholder = r.vars["pdffile"] - # server side file validation - filename = fileholder.filename - extension = lambda m: m[m.rfind(".") + 1:] - - if extension(filename) != "pdf": - r.error(501, self.ERROR.NOT_PDF_FILE) - - # create temp dir to extract the images - uniqueuuid = setuuid # to make it thread safe - inputfilename = "%s_%s" % (uniqueuuid, fileholder.filename) - outputfilename = "%s_%s.png" % (uniqueuuid, - fileholder.filename[:-4]) - - ocr_temp_dir = os.path.join(self.r.folder, - "uploads", "ocr_temp") - try: - os.mkdir(ocr_temp_dir) - except(OSError): - pass - - f = open(os.path.join(ocr_temp_dir, inputfilename), "w") - f.write(fileholder.file.read()) - f.close() - - success = subprocess.call(["convert", - os.path.join(ocr_temp_dir, - inputfilename), - os.path.join(ocr_temp_dir, - outputfilename)]) - if success != 0: - self.r.error(501, self.ERROR.IMAGE_MAGICK_ERROR) - - # Store each page into db/disk - payloadtable = db.ocr_payload - - if numpages == 1: - imagefilename = outputfilename - imgfilepath = os.path.join(ocr_temp_dir, imagefilename) - try: - imgfile = open(imgfilepath) - except(IOError): - self.r.error(501, self.ERROR.INVALID_PDF) - pagenumber = 1 - - # Store page - payloadtable.insert( - image_set_uuid=setuuid, - image_file=payloadtable["image_file"].store(\ - imgfile, - imagefilename), - page_number=pagenumber) - imgfile.close() - os.remove(imgfilepath) - - else: - for eachpage in range(0, numpages): - imagefilename = "%s-%s.png" % (outputfilename[:-4], - eachpage) - imgfilepath = os.path.join(ocr_temp_dir, - imagefilename) - try: - imgfile = open(imgfilepath, "r") - except(IOError): - self.r.error(501, self.ERROR.INVALID_PDF) - - pagenumber = eachpage + 1 - - # Store page - payloadtable.insert( - image_set_uuid=setuuid, - image_file=payloadtable["image_file"].store(\ - imgfile, - imagefilename), - page_number=pagenumber) - imgfile.close() - os.remove(imgfilepath) - - os.remove(os.path.join(ocr_temp_dir, inputfilename)) - try: - os.rmdir(ocr_temp_dir) - except(OSError): - import shutil - shutil.rmtree(ocr_temp_dir) - - else: - r.error(501, self.ERROR.INVALID_IMAGE_TYPE) - - # OCR it - s3ocrimageparser = S3OCRImageParser(self, r) - output = s3ocrimageparser.parse(formuuid, setuuid) - - table = db.ocr_data_xml - table.insert(image_set_uuid=setuuid, - data_file=table["data_file"].store( - StringIO(output), - "%s-data.xml" % setuuid), - form_uuid=formuuid, - ) - - if r.component: - request_args = current.request.get("args", ["", ""]) - record_id = request_args[0] - component_name = request_args[1] - urlprefix = "%s/%s/%s" % (request.function, - record_id, - component_name) - - else: - # Not a component - urlprefix = request.function - - redirect(URL(request.controller, - "%s/import.pdf" % urlprefix, - args="import", - vars={"setuuid":setuuid})) - - elif method == "import": - if not current.deployment_settings.has_module("ocr"): - r.error(501, self.ERROR.OCR_DISABLED) - - authorised = self._permitted(method="create") - if not authorised: - r.unauthorised() - - try: - if r.component: - trigger = r.args[3] - else: - trigger = r.args[1] - except(IndexError): - trigger = None - - if trigger == "review": - # Review UI post - jobuuid = r.vars.pop("jobuuid") - - # Check if operation is valid on the given job_uuid - statustable = current.s3db.ocr_form_status - query = (statustable.job_uuid == jobuuid) - row = db(query).select(statustable.review_status, - limitby=(0, 1)).first() - if not row: - r.error(501, self.ERROR.INVALID_JOBID) - - if row.review_status == 1: - # Job has already been reviewed - r.error(501, self.ERROR.JOB_COMPLETE) - - try: - r.vars.pop("_utc_offset") - except: - pass - - try: - ignore_fields = r.vars.pop("ignore-fields-list") - except: - ignore_fields = None - - if not ignore_fields: - ignore_fields = [] - else: - try: - ignore_fields = ignore_fields.split("|") - except: - ignore_fields = [ignore_fields] - - datadict = Storage() - for field in r.vars.keys(): - resourcetable, fieldname = field.split("-") - if resourcetable not in datadict: - datadict[resourcetable] = Storage() - - datadict[resourcetable][fieldname] = r.vars[field] - - for field in ignore_fields: - resourcetable, fieldname = field.split("-") - datadict[resourcetable].pop(fieldname) - if len(datadict[resourcetable]) == 0: - datadict.pop(resourcetable) - - s3xml_etree_dict = Storage() - for resource in datadict.keys(): - s3xml_root = etree.Element("s3xml") - resource_element = etree.SubElement(s3xml_root, "resource") - resource_element.attrib["name"] = resource - - for field in datadict[resource].keys(): - fieldvalue = datadict[resource][field] - fieldvalue = str(fieldvalue) if fieldvalue else "" - fieldtype = db[resource][field].type - if fieldtype.startswith("reference "): - reference_resource_name = fieldtype[len("reference "):] - # reference element - reference_element =\ - etree.SubElement(resource_element, "reference") - reference_element.attrib["field"] = field - reference_element.attrib["resource"] = reference_resource_name - # resource element - ref_res_element =\ - etree.SubElement(reference_element, "resource") - ref_res_element.attrib["name"] = reference_resource_name - # data element - ref_res_data_element =\ - etree.SubElement(ref_res_element, "data") - ref_res_data_element.attrib["field"] = "name" - try: - ref_res_data_element.text = cast2ascii(fieldvalue) - except(ValueError): - ref_res_data_element.text = "" - else: - field_element = etree.SubElement(resource_element, "data") - field_element.attrib["field"] = field - try: - field_element.attrib["value"] = cast2ascii(fieldvalue) - except(ValueError): - field_element.attrib["value"] = "" - try: - field_element.text = cast2ascii(fieldvalue) - except(ValueError): - field_element.text = "" - - s3xml_etree_dict[resource] = s3xml_root - - errordict = {} - - _record = current.xml.record - s3record_dict = Storage() - for tablename in s3xml_etree_dict.keys(): - record = _record(db[tablename], - s3xml_etree_dict[tablename].getchildren()[0]) - s3record_dict[tablename] = record - - import_job = r.resource.import_tree(None, None, job_id=jobuuid, - ignore_errors=False, - commit_job=False) - - response.headers["Content-Type"] = contenttype(".json") - - for tablename in s3record_dict.keys(): - record = s3record_dict[tablename] - possible_items = [] - our_item = None - for eachitem in import_job.items.keys(): - item = import_job.items[eachitem] - if item.table == tablename: - if item.data and (len(item.data) > 0): - our_item = item - else: - if item.data and (len(item.data) == 0): - possible_items.append(item) - - if our_item: - our_item.update(record) - elif len(possible_items) > 0: - possible_items[0].update(record) - else: - import_job.add_item(s3xml_etree_dict[tablename].getchildren()[0]) - - for resourcename in datadict.keys(): - table = db[resourcename] - for field in datadict[resourcename].keys(): - if not table[field].type.startswith("reference "): - value, error = s3_validate(table, - field, - datadict[resourcename][field]) - if error: - errordict["%s-%s" % (resourcename, field)] = str(error) - - if not import_job.error_tree: - store_success = import_job.store() - if store_success: - if import_job.error_tree: - errordict = self.__parse_job_error_tree(import_job.error_tree) - success = False - else: - # Revalidate data - for resourcename in datadict.keys(): - table = db[resourcename] - for field in datadict[resourcename].keys(): - if not table[field].type.startswith("reference "): - value, error =\ - s3_validate(table, - field, - datadict[resourcename][field]) - if error: - errordict["%s-%s" % (resourcename, field)] = str(error) - - if len(errordict) > 0: - success = False - else: - success = True - import_job.commit() - - else: - errordict = self.__parse_job_error_tree(import_job.error_tree) - success = False - else: - errordict = self.__parse_job_error_tree(import_job.error_tree) - success = False - - if success: - session.confirmation =\ - T("OCR review data has been stored into the database successfully.") - - # Perform cleanup - statustable = db["ocr_form_status"] - query = (statustable.job_uuid == jobuuid) - row = db(query).select(statustable.image_set_uuid).first() - image_set_uuid = row.image_set_uuid - - # Set review status = true - db(query).update(review_status=1) - - # Remove cropped images from the database - cropstable = db.ocr_field_crops - query = (cropstable.image_set_uuid == image_set_uuid) - - # Delete uploaded files - rows = db(query).select(cropstable.image_file) - for row in rows: - filename = row.image_file - filepath = os.path.join(self.r.folder, - "uploads", - "ocr_payload", - filename) - os.remove(filepath) - - # Delete records - db(query).delete() - - return json.dumps({"success": success, - "error": errordict}) - - else: - r.error(405, current.ERROR.BAD_METHOD) - - else: - r.error(501, current.ERROR.BAD_REQUEST) - - # ------------------------------------------------------------------------- - def __parse_job_error_tree(self, tree): - """ - create a dictionary of fields with errors - - @param tree: S3ImportJob.error_tree - @return: errordict - """ - - errordict = {} - - for resource in tree: - resourcename = resource.attrib.get("name") - for field in resource: - fieldname = field.attrib.get("field") - error = field.attrib.get("error") - if error: - errordict["%s-%s" % (resourcename, fieldname)] = error - - return errordict - - # ------------------------------------------------------------------------- - def dameraulevenshtein(self, seq1, seq2): - """ - Calculate the Damerau-Levenshtein distance between sequences. - - This distance is the number of additions, deletions, substitutions, - and transpositions needed to transform the first sequence into the - second. Although generally used with strings, any sequences of - comparable objects will work. - - Transpositions are exchanges of *consecutive* characters; all other - operations are self-explanatory. - - This implementation is O(N*M) time and O(M) space, for N and M the - lengths of the two sequences. - - >>> dameraulevenshtein('ba', 'abc') - 2 - >>> dameraulevenshtein('fee', 'deed') - 2 - - It works with arbitrary sequences too: - >>> dameraulevenshtein('abcd', ['b', 'a', 'c', 'd', 'e']) - 2 - """ - - # codesnippet:D0DE4716-B6E6-4161-9219-2903BF8F547F - # Conceptually, this is based on a len(seq1) + 1 * len(seq2) + 1 matrix. - # However, only the current and two previous rows are needed at once, - # so we only store those. - oneago = None - thisrow = list(range(1, len(seq2) + 1) + [0]) - for x in range(len(seq1)): - # Python lists wrap around for negative indices, so put the - # leftmost column at the *end* of the list. This matches with - # the zero-indexed strings and saves extra calculation. - twoago, oneago, thisrow = oneago, thisrow, [0] * len(seq2) + [x + 1] - for y in range(len(seq2)): - delcost = oneago[y] + 1 - addcost = thisrow[y - 1] + 1 - subcost = oneago[y - 1] + (seq1[x] != seq2[y]) - thisrow[y] = min(delcost, addcost, subcost) - # This block deals with transpositions - if (x > 0 and y > 0 and seq1[x] == seq2[y - 1] - and seq1[x - 1] == seq2[y] and seq1[x] != seq2[y]): - thisrow[y] = min(thisrow[y], twoago[y - 2] + 1) - return thisrow[len(seq2) - 1] - - # ------------------------------------------------------------------------- - def __temp_ocrdataxml_parser(self, s3ocrdataxml): - """ - convert data generated from ocr parser to a dictionary - - @param s3dataxml: output of S3OCRImageParser - - @return: python dictionary equalant to the input xml - """ - - s3ocrdataxml_etree = etree.fromstring(s3ocrdataxml) - s3ocrdatadict = Storage() - - s3xml_root = s3ocrdataxml_etree - resource_element = s3xml_root.getchildren()[0] - s3ocr_root = etree.Element("s3ocr") - - if self.r.component: # if it is a component - s3ocr_root.append(resource_element) - - else: # if it is main resource - componentetrees = [] - # mres is main resource etree - mres = etree.Element("resource") - for attr in resource_element.attrib.keys(): - mres.set(attr, resource_element.attrib.get(attr)) - for field_element in resource_element: - if field_element.tag in ["data", "reference"]: # main resource fields - mres.append(field_element) - elif field_element.tag == "resource": # component resource - componentetrees.append(field_element) - - serialised_component_etrees = componentetrees - - # create s3ocr tree - s3ocr_root.append(mres) - for res in serialised_component_etrees: - s3ocr_root.append(res) - - for resource in s3ocr_root: - resourcename = resource.attrib.get("name") - s3ocrdatadict[resourcename] = Storage() - for field in resource: - if field.tag == "reference": - fieldname = field.attrib.get("field") - ref_res_field = field.getchildren()[0] - datafield = ref_res_field.getchildren()[0] - value = datafield.text - - else: - fieldname = field.attrib.get("field") - value = field.attrib.get("value") - text = field.text - if not value: - value = text - - s3ocrdatadict[resourcename][fieldname] = value - return s3ocrdatadict - - # ------------------------------------------------------------------------- - def __importjob2data(self, importjob): - """ - convert data from import job into a dictionary - - @param importjob: S3ImportJob instance - - @return: data of S3ImportJob into a dictionary - """ - - s3ocrdata = Storage() - - import_item_dict = importjob.items - for eachitem in import_item_dict.keys(): - import_item = import_item_dict[eachitem] - if import_item.data and len(import_item.data) > 0: - s3ocrdata[str(import_item.table)] = import_item.data - - return s3ocrdata - - # ------------------------------------------------------------------------- - def __create_review_form(self, s3ocrdict, s3ocrdata): - """ - create a html review form using the available data - - @param s3ocrdict: output of self.__s3ocrxml2dict() - @param s3ocrdata: output of self.__importjob2data() - - @return: html review form - """ - - ptablecontent = [] - fieldnum = 1 - request = current.request - T = current.T - r = self.r - setuuid = self.setuuid - if r.component: - request_args = request.get("args",["",""]) - record_id = request_args[0] - component_name = request_args[1] - urlprefix = "%s/%s/%s" % (request.function, - record_id, - component_name) - else: - # Not a component - urlprefix = request.function - - for resourcename in s3ocrdict["$resource_seq"]: - # Resource title - resource = s3ocrdict[resourcename] - ptablecontent.append(TR(TD(DIV(resourcename, _class="resource_name"), - _colspan="4"), - _class="titletr") - ) - - ctablecontent = [] - for fieldname in resource["$field_seq"]: - field = resource[fieldname] - comment = field.comment if field.comment else "" - - try: - ocrdata = s3ocrdata[resourcename][fieldname] - if ocrdata: - condition = (isinstance(ocrdata, str) or \ - isinstance(ocrdata, int)) - if condition: - value = str(ocrdata) - elif isinstance(ocrdata, datetime.date): - value = datetime.date.strftime(ocrdata, "%Y-%m-%d") - elif isinstance(ocrdata, datetime.datetime): - value = datetime.datetime.strftime(ocrdata, "%Y-%m-%d %H:%M:%S") - else: - value = unicodedata.normalize("NFKD", - ocrdata).encode("ascii", - "ignore") - else: - value = "" - except(KeyError): - value="" - - name = "%s-%s" % (resourcename, fieldname) - - if field.has_options: - if field.type == "multiselect": - if field.options.count <= MAX_FORM_OPTIONS_LIMIT: - options = [] - optct = 1 - try: - value = value.split("|")[1:-1] - except: - value = [str(value)] - chk = lambda m, n: "on" if str(m) in n else None - for option in field.options.list: - options.append(TD(IMG(_src=URL(request.application, - r.prefix, - "%s/upload.pdf" % urlprefix, - args="image", - vars={"setuuid": setuuid, - "resource_table": resourcename, - "field_name": fieldname, - "value": option.value - } - ), - _style="border: solid #333 1px;"), - _style="text-align:center;"), - TD(INPUT(_id="%s-%s" % (name, optct), - _value=option.value, - _type="checkbox", - _class="field-%s" % fieldnum, - _name=name, - value=chk(option.value, - value))), - TD(LABEL(option.label, - _for="%s-%s" % (name, optct)))) - optct += 1 - input_area = TABLE(TR(options), - _class="field-%s" % fieldnum) - - else: - for line in range(1, 3): - ctablecontent.append(TR(TD(IMG(_src=URL(request.application, - r.prefix, - "%s/upload.pdf" % urlprefix, - args="image", - vars={"setuuid": setuuid, - "resource_table": resourcename, - "field_name": fieldname, - "sequence": line - } - ), - _style="border: solid #333 1px;"), - _style="text-align:center; padding:5px;", - _colspan="4"))) - - options = [] - optct = 1 - - chk = lambda m, n: "on" if str(m) in n else None - for option in field.options.list: - options.append(TR(TD(INPUT(_id="%s-%s" % (name, optct), - _value=option.value, - _type="checkbox", - _class="field-%s" % fieldnum, - _name=name, - value=chk(option.value, - value) - )), - TD(LABEL(option.label, - _for="%s-%s" % (name, optct))))) - optct += 1 - input_area = TABLE(options, - _class="field-%s" % fieldnum) - - elif field.type == "boolean": - options = [] - optct = 1 - chk = lambda m, n: m if str(m) == str(n) else None - for option in [Storage({"value": "yes", - "label": T("Yes")}), - Storage({"value": "no", - "label": T("No")})]: - options.append(TD(IMG(_src=URL(request.application, - r.prefix, - "%s/upload.pdf" % urlprefix, - args="image", - vars={"setuuid": setuuid, - "resource_table": resourcename, - "field_name": fieldname, - "value": option.value - } - ), - _style="border: solid #333 1px;"), - _style="text-align:center;"), - TD(INPUT(_id="%s-%s" % (name, optct), - _value=option.value, - _type="radio", - _class="field-%s" % fieldnum, - _name=name, - value=chk(option.value, - value))), - TD(LABEL(option.label, - _for="%s-%s" % (name, optct)))) - optct += 1 - input_area = TABLE(TR(options), - _class="field-%s" % fieldnum) - - else: - if field.options.count <= MAX_FORM_OPTIONS_LIMIT: - options = [] - optct = 1 - chk = lambda m, n: m if str(m) == str(n) else None - for option in field.options.list: - options.append(TD(IMG(_src=URL(request.application, - r.prefix, - "%s/upload.pdf" % urlprefix, - args="image", - vars={"setuuid": setuuid, - "resource_table": resourcename, - "field_name": fieldname, - "value": option.value - } - ), - _style="border: solid #333 1px;"), - _style="text-align:center;"), - TD(INPUT(_id="%s-%s" % (name, optct), - _value=option.value, - _type="radio", - _class="field-%s" % fieldnum, - _name=name, - value=chk(option.value, - value))), - TD(LABEL(option.label, - _for="%s-%s" % (name, optct)))) - optct += 1 - input_area = TABLE(TR(options), - _class="field-%s" % fieldnum) - - else: - for line in range(1, 3): - ctablecontent.append(TR(TD(IMG(_src=URL(request.application, - r.prefix, - "%s/upload.pdf" % urlprefix, - args="image", - vars={"setuuid": setuuid, - "resource_table": resourcename, - "field_name": fieldname, - "sequence": line - } - ), - _style="border: solid #333 1px;"), - _style="text-align:center; padding:5px;", - _colspan="4"))) - - options = [] - optct = 1 - chk = lambda m, n: m if str(m) == str(n) else None - for option in field.options.list: - options.append(TR(TD(INPUT(_id="%s-%s" % (name, optct), - _value=option.value, - _type="radio", - _class="field-%s" % fieldnum, - _name=name, - value=chk(option.value, - value) - )), - TD(LABEL(option.label, - _for="%s-%s" % (name, optct))))) - optct += 1 - input_area = TABLE(options, - _class="field-%s" % fieldnum) - - else: - if field.type in ["string", "integer", "double"]: - for line in range(1, field.lines + 1): - ctablecontent.append(TR(TD(IMG(_src=URL(request.application, - r.prefix, - "%s/upload.pdf" % urlprefix, - args="image", - vars={"setuuid": setuuid, - "resource_table": resourcename, - "field_name": fieldname, - "sequence": line - } - ), - _style="border: solid #333 1px;"), - _style="text-align:center; padding:5px;", - _colspan="4"))) - input_area = INPUT(_id="%s-id" % name.replace("-", "_"), - _class="field-%s" % fieldnum, - _value=value, _name=name) - - elif field.type == "date": - subsec = {"DD":1, - "MO":2, - "YYYY":3} - imglist = [] - for sec in ["YYYY", "MO", "DD"]: - imglist.append(IMG(_src=URL(request.application, - r.prefix, - "%s/upload.pdf" % urlprefix, - args="image", - vars={"setuuid": setuuid, - "resource_table": resourcename, - "field_name": field, - "sequence": subsec[sec]} - ), - _style="border: solid #333 1px;")) - ctablecontent.append(TR(TD(imglist, - _style="text-align:center; padding:5px;", - _colspan="4"))) - - try: - value = value.strftime("%Y-%m-%d") - except(AttributeError): - try: - value = datetime.datetime.strptime(value, "%Y-%m-%d") - value = value.strftime("%Y-%m-%d") - except(ValueError): - value = "" - input_area = INPUT(_id="%s-id" % name.replace("-", "_"), - _class="field-%s date" % fieldnum, - _value=value, _name=name) - - elif field.type == "datetime": - subsec = {"HH":1, - "MM":2, - "DD":3, - "MO":4, - "YYYY":5} - imglist = [] - for eachsec in ["YYYY", "MO", "DD", "HH", "MM"]: - imglist.append(IMG(_src=URL(request.application, - r.prefix, - "%s/upload.pdf" % urlprefix, - args="image", - vars={"setuuid": setuuid, - "resource_table": resourcename, - "field_name": fieldname, - "sequence": subsec[eachsec], - } - ), - _style="border: solid #333 1px;")) - ctablecontent.append(TR(TD(imglist, - _style="text-align:center; padding:5px;", - _colspan="4"))) - - try: - value = value.strftime("%Y-%m-%d %H:%M:%S") - except(AttributeError): - try: - value = datetime.datetime.strptime(value,"%Y-%m-%d %H:%M:%S") - value = value.strftime("%Y-%m-%d %H:%M:%S") - except(ValueError): - value = "" - - input_area = INPUT(_id="%s-id" % name.replace("-", "_"), - _class="field-%s datetime" % fieldnum, - _value=value, _name=name) - - elif field.type == "textbox": - for line in range(1, field.lines + 1): - ctablecontent.append(TR(TD(IMG(_src=URL(request.application, - r.prefix, - "%s/upload.pdf" % urlprefix, - args="image", - vars={"setuuid": setuuid, - "resource_table": resourcename, - "field_name": fieldname, - "sequence": line - } - ), - _style="border: solid #333 1px;"), - _style="text-align:center; padding:5px;", - _colspan="4"))) - input_area = TEXTAREA(value, - _class="field-%s" % fieldnum, - _name=name) - - else: - input_area = SPAN() - - ctablecontent.append(TR(TD(TABLE(TR(TD(field.label)), - TR(TD(SPAN(_id="%s-error" % name, - _style="font-size: 12px; font-weight:bold; color: red;", - _class="error-span")))), - _class="label", _style="vertical-align: top;"), - TD(input_area, _class="infield"), - TD(comment, _class="comment", _style="vertical-align: top;"), - TD(TAG["BUTTON"](T("clear"), - _name="button-%s" % fieldnum, - _class="clrbutton" - ), - TAG["BUTTON"](T("ignore"), - _name="ignore-%s" % name, - _class="ignore-button"), - _class="clear", _style="vertical-align: top;"), - _class="fieldtr")) - - ctablecontent.append(TR(TD(_colspan="4", - _style="border: solid #999 3px;"))) - fieldnum += 1 - - ptablecontent.extend(ctablecontent) - - # Submit button - ptablecontent.append(TR(TD(TAG["button"](T("Submit"), - _class="submit-button", - _style="width: 70px; height: 20px;"), - _colspan="4", - _style="text-align:center; padding: 5px;"))) - - output = FORM(TABLE(ptablecontent, _class="ptable"), - _id="ocr-review-form") - - return output - - # ------------------------------------------------------------------------- - def __s3ocrxml2dict(self, s3ocrxml): - """ - convert s3ocrxml to dictionary so that it can be used in templates - - @param s3ocrxml: content of a s3ocrxml file, in text - - @return: equivalent dictionary for s3ocrxml file - """ - - db = current.db - s3ocr_etree = etree.fromstring(s3ocrxml) - s3ocrdict = Storage() - resource_seq = [] - - for resource in s3ocr_etree: - resourcename = resource.attrib.get("name") - table = db[resourcename] - s3ocrdict[resourcename] = Storage() - resource_seq.append(resourcename) - field_seq = [] - for field in resource: - get = field.attrib.get - fieldname = get("name") - - if get("readable") == "True" and \ - get("writable") == "True": - - field_seq.append(fieldname) - - fieldlabel = get("label") - fieldtype = get("type") - numlines = get("lines", "1") - - if get("reference") == "1": - fieldreference = True - else: - fieldreference = False - fieldresource = get("resource") - if get("has_options") == "True": - fieldhasoptions = True - else: - fieldhasoptions = False - - # Get html comment - fieldcomment = table[fieldname].comment - - if fieldhasoptions: - try: - s3ocrselect = field.getchildren()[0] - options_found = True - except(IndexError): - fieldoptions = None - options_found = False - - if options_found: - - numoptions = len(s3ocrselect.getchildren()) - optionlist = [] - - for option in s3ocrselect: - optionlabel = option.text - optionvalue = option.attrib.get("value") - optionlist.append(Storage({"label": optionlabel, - "value": optionvalue})) - - fieldoptions = Storage({"count": numoptions, - "list": optionlist}) - - else: - fieldoptions = None - else: - fieldoptions = None - - s3ocrdict[resourcename][fieldname] = Storage({"label": fieldlabel, - "type": fieldtype, - "comment": fieldcomment, - "reference": fieldreference, - "resource": fieldresource, - "has_options": fieldhasoptions, - "options": fieldoptions, - "lines": int(numlines) - }) - s3ocrdict[resourcename]["$field_seq"] = field_seq - - s3ocrdict["$resource_seq"] = resource_seq - - return s3ocrdict - - # ------------------------------------------------------------------------- - def newDocument(self, - title, - header, - footer, - filename = None, - heading=None, - ): - """ - This will create a new empty PDF document. - Data then needs to be added to this document. - - @param title: The title that will appear at the top of the document - and in the filename - - @return: An empty pdf document - """ - - # Get the document variables - now = self.request.now.isoformat()[:19].replace("T", " ") - docTitle = "%s %s" % (title, now) - if filename == None: - self.filename = "%s_%s.pdf" % (title, now) - else: - self.filename = "%s_%s.pdf" % (filename, now) - self.output = StringIO() - self.doc = EdenDocTemplate(self.output, title=docTitle) - self.doc.setPageTemplates(header,footer) - self.content = [] - if heading == None: - heading = title - self.title = heading - self.prevtitle = heading - self.setPortrait() - self.leftMargin = 0.4 * inch - self.rightMargin = 0.4 * inch - self.topMargin = 0.4 * inch - self.bottomMargin = 0.4 * inch - self.MINIMUM_MARGIN_SIZE = 0.3 * inch - self.setMargins() - - # ------------------------------------------------------------------------- - def newOCRForm(self, - formUUID, - pdfname="ocrform.pdf", - top=65, - left=50, - bottom=None, - right=None, - **args): - - self.content = [] - self.output = StringIO() - self.layoutEtree = etree.Element("s3ocrlayout") - try: - pdfTitle = current.response.s3.crud_strings[self.tablename].label_create.decode("utf-8") - except: - pdfTitle = self.resource.tablename - - formResourceName = self.resource.tablename - formRevision = self.__book_revision(formUUID, formResourceName) - self.filename = "%s_rev%s.pdf" % (formResourceName, formRevision) - self.doc = self.S3PDFOCRForm(self.output, - formUUID=formUUID, - pdfTitle = pdfTitle, - formRevision=formRevision, - formResourceName=formResourceName) - - # ------------------------------------------------------------------------- - def __getResourceForms(self): - """ - Get all form UUIDs/Revs available for a given resource - - @return: a list of all available forms for the given - resource, the list will contain tuples such - that the first value is form-uuid and the - second value is form-revision - """ - - db = current.db - table = db.ocr_meta - query = (table.resource_name == self.resource.tablename) - rows = db(query).select(table.form_uuid, - table.revision, - orderby=~table.revision) - availForms = [] - append = availForms.append - for row in rows: - append({"uuid" : row.form_uuid, - "revision": row.revision, - }) - return availForms - - # ------------------------------------------------------------------------- - def __getNumPages(self, formuuid): - """ - Gets Number of pages for given form UUID - - @param formuuid: uuid of the form, for which - number of pages is required - - @return: number of pages in a form identified - by uuid - """ - - db = current.db - table = db.ocr_meta - row = db(table.form_uuid == formuuid).select(table.pages, - limitby=(0, 1) - ).first() - return int(row.pages) - - # ------------------------------------------------------------------------- - def __s3OCREtree(self): - """ - Optimise & Modifiy s3xml etree to and produce s3ocr etree - - @return: s3ocr etree - """ - - r = self.r - - s3xml_etree = self.resource.export_struct(options=True, - references=True, - stylesheet=None, - as_json=False, - as_tree=True) - - # Additional XML tags - ITEXT = "label" - HINT = "comment" - TYPE = "type" - HASOPTIONS = "has_options" - LINES = "lines" - BOXES = "boxes" - REFERENCE = "reference" - RESOURCE = "resource" - - # Components Localised Text added to the etree - # Convering s3xml to s3ocr_xml (nicer to traverse) - s3xml_root = s3xml_etree.getroot() - resource_element = s3xml_root.getchildren()[0] - s3ocr_root = etree.Element("s3ocr") - - # Store components which have to be excluded - settings = current.deployment_settings - self.exclude_component_list =\ - settings.get_pdf_excluded_fields("%s_%s" % \ - (r.prefix, - r.resource.name)) - - if r.component: # if it is a component - s3ocr_root.append(resource_element) - - else: # if it is main resource - componentetrees = [] - # mres is main resource etree - mres = etree.Element("resource") - for attr in resource_element.attrib.keys(): - mres.set(attr, resource_element.attrib.get(attr)) - for field_element in resource_element: - if field_element.tag == "field": # main resource fields - mres.append(field_element) - elif field_element.tag == "resource": # component resource - componentetrees.append(field_element) - - serialised_component_etrees = componentetrees - - # create s3ocr tree - s3ocr_root.append(mres) - for res in serialised_component_etrees: - s3ocr_root.append(res) - - # Database fieldtype to ocr fieldtype mapping - self.generic_ocr_field_type = { - "string": "string", - "text": "textbox", - "boolean" : "boolean", - "double": "double", - "date": "date", - "datetime": "datetime", - "integer": "integer", - "list:integer": "multiselect", - "list:string": "multiselect", - "list:double": "multiselect", - "list:text": "multiselect", - } - - # Remove fields which are not required - # Load user-defined configurations - FIELD_TYPE_LINES = { # mapping types with number of lines - "string": 1, - "textbox": 2, - "integer": 1, - "double": 1, - "date": 1, - "datetime": 1, - } - FIELD_TYPE_BOXES = { # mapping type with numboxes - "integer": 8, - "double": 16, - } - for resource in s3ocr_root.iterchildren(): - rget = resource.attrib.get - resourcetablename = rget("name") - - # Exclude components - if not r.component: - if rget("name") in self.exclude_component_list: - s3ocr_root.remove(resource) - continue - - if "alias" in resource.attrib: - alias = resource.attrib["alias"] - elif "_" in resourcetablename: - alias = resourcetablename.split("_", 1)[1] - else: - alias = resourcetablename - - if alias == self.resource.alias and \ - resourcetablename == self.resource.tablename: - fieldresource = self.resource - elif alias in self.resource.components: - fieldresource = self.resource.components[alias] - else: - continue - - for field in resource.iterchildren(): - get = field.attrib.get - set = field.set - fieldname = get("name") - # Fields which have to be displayed - fieldtype = get(TYPE) - - if fieldtype.startswith("reference "): - set(RESOURCE, fieldtype.split("reference ")[1]) - set(REFERENCE, "1") - else: - set(REFERENCE, "0") - - # Load OCR-specific fieldtypes - ocrfieldtype = self.generic_ocr_field_type.get(fieldtype, None) - if ocrfieldtype != None: - set(TYPE, ocrfieldtype) - # Refresh fieldtypes after update - fieldtype = get(TYPE) - - # Set num boxes and lines - fieldhasoptions = get(HASOPTIONS) - if fieldhasoptions == "False": - set(LINES, str(FIELD_TYPE_LINES.get(fieldtype, 1))) - if fieldtype in FIELD_TYPE_BOXES.keys(): - set(BOXES, str(FIELD_TYPE_BOXES.get(fieldtype))) - - # If field is readable but not writable set default value - if get("readable", "False") == "True" and \ - get("writable", "False") == "False": - - fieldname = get("name") - try: - fielddefault = fieldresource.table[fieldname].default - except(KeyError): - fielddefault = "None" - set("default", str(fielddefault)) - - # For unknown field types - if fieldtype not in list(self.generic_ocr_field_type.values()): - set(TYPE, "string") - set(HASOPTIONS, "False") - set(LINES, "2") - # Refresh fieldtypes after update - fieldtype = get(TYPE) - - # In OCR, boolean fields should be shown as options - if fieldtype == "boolean": - set(HASOPTIONS, "True") - - # Fields removed which need not be displayed - if get("readable", "False") == "False" and \ - get("writable", "False") == "False": - resource.remove(field) - continue - - if get(HASOPTIONS, "False") == "True" and \ - get(TYPE) != "boolean": - s3ocrselect = field.getchildren()[0] - for option in s3ocrselect.iterchildren(): - if option.text == "" or option.text == None: - s3ocrselect.remove(option) - continue - - return s3ocr_root - - # ------------------------------------------------------------------------- - def OCRPDFManager(self): - """ - Produces OCR Compatible PDF forms - """ - - T = current.T - s3ocr_root = self.__s3OCREtree() # get element s3xml - self.s3ocrxml = etree.tostring(s3ocr_root, pretty_print=DEBUG) - self.content = [] - s3ocr_layout_etree = self.layoutEtree - - # @ToDo: Define font sizes centrally rather than in flowables - #titlefontsize = 16 - #sectionfontsize = 14 - #regularfontsize = 12 - #hintfontsize = 10 - - ITEXT = "label" - HINT = "comment" - TYPE = "type" - HASOPTIONS = "has_options" - LINES = "lines" - BOXES = "boxes" - REFERENCE = "reference" - RESOURCE = "resource" - - dtformat = current.deployment_settings.get_L10n_datetime_format() - if str(dtformat)[:2] == "%m": - # US-style - date_hint = T("fill in order: month(2) day(2) year(4)") - datetime_hint = T("fill in order: hour(2) min(2) month(2) day(2) year(4)") - else: - # ISO-style - date_hint = T("fill in order: day(2) month(2) year(4)") - datetime_hint = T("fill in order: hour(2) min(2) day(2) month(2) year(4)") - l10n = { - "datetime_hint": { - "date": date_hint, - "datetime": datetime_hint, - }, - "boolean": { - "yes": T("Yes"), - "no": T("No"), - }, - "select": { - "multiselect": T("Select one or more option(s) that apply"), - "singleselect": T("Select the option that applies"), - }, - } - - # Print the etree - append = self.content.append - SubElement = etree.SubElement - for resource in s3ocr_root: - name = resource.attrib.get("name") - # Create resource element of ocr layout xml - s3ocr_layout_resource_etree = SubElement(s3ocr_layout_etree, - "resource", - name=name) - - styleSheet = getStyleSheet() - # @ToDo: Check if this is needed by OCR (removed for now as ugly) - #append(DrawHrLine(0.5)) - #append(Paragraph(html_unescape_and_strip(resource.attrib.get(ITEXT, - # name)), - # styleSheet["Section"])) - #append(DrawHrLine(0.5)) - - for field in resource.iterchildren(): - get = field.attrib.get - # Create field element of ocr layout xml - s3ocr_layout_field_etree = SubElement(s3ocr_layout_resource_etree, - "field", - name=get("name"), - type=get("type")) - - if get(REFERENCE) == "1": - s3ocr_layout_field_etree.set(REFERENCE, "1") - s3ocr_layout_field_etree.set(RESOURCE, get(RESOURCE)) - - fieldlabel = get(ITEXT) - spacing = " " * 5 - fieldhint = self.__trim(get(HINT)) - - if fieldhint: - append(Paragraph(html_unescape_and_strip("%s%s( %s )" % \ - (fieldlabel, - spacing, - fieldhint)), - styleSheet["Question"])) - - else: - append(Paragraph(html_unescape_and_strip(fieldlabel), - styleSheet["Question"])) - - if get("readable", "False") == "True" and \ - get("writable", "False") == "False": - append(Paragraph(html_unescape_and_strip(get("default", - "No default Value")), - styleSheet["DefaultAnswer"])) - - # Remove the layout component of empty fields - s3ocr_layout_resource_etree.remove(s3ocr_layout_field_etree) - - elif get(HASOPTIONS) == "True": - fieldtype = get(TYPE) - # The field has to be shown with options - if fieldtype == "boolean": - bool_text = l10n.get("boolean") - append(DrawOptionBoxes(s3ocr_layout_field_etree, - [bool_text.get("yes").decode("utf-8"), - bool_text.get("no").decode("utf-8")], - ["yes", "no"])) - - else: - if fieldtype == "multiselect": - option_hint = l10n.get("select").get("multiselect") - else: - #option_hint = l10n.get("select").get("singleselect") - option_hint = None - - s3ocrselect = field.getchildren()[0] - numoptions = len(s3ocrselect.getchildren()) - - if numoptions <= MAX_FORM_OPTIONS_LIMIT: - s3ocr_layout_field_etree.attrib["limitcrossed"] = "1" - if option_hint: - append(DrawHintBox(option_hint.decode("utf-8"))) - - options = s3ocrselect.iterchildren() - # Only show 4 options per row - opts = [] - oppend = opts.append - for row in range(int(math.ceil(numoptions / 4.0))): - labels = [] - lappend = labels.append - values = [] - vappend = values.append - i = 1 - for option in options: - label = option.text - if label in opts: - continue - oppend(label) - lappend(label) - vappend(option.attrib.get("value")) - if i == 4: - break - i += 1 - append(DrawOptionBoxes(s3ocr_layout_field_etree, - labels, - values)) - else: - append(DrawHintBox(T("Enter a value carefully without spelling mistakes, this field needs to match existing data.").decode("utf-8"))) - for line in range(2): - append(StringInputBoxes(numBoxes=None, - etreeElem=s3ocr_layout_field_etree)) - else: - # It is a text field - fieldtype = get(TYPE) - BOXES_TYPES = ["string", "textbox", "integer", - "double", "date", "datetime",] - if fieldtype in BOXES_TYPES: - if fieldtype in ["string", "textbox"]: - #form.linespace(3) - num_lines = int(get("lines", 1)) - for line in range(num_lines): - append(StringInputBoxes(numBoxes=None, - etreeElem=s3ocr_layout_field_etree)) - - elif fieldtype in ["integer", "double"]: - num_boxes = int(get("boxes", 9)) - append(StringInputBoxes(numBoxes=num_boxes, - etreeElem=s3ocr_layout_field_etree)) - - elif fieldtype in ["date", "datetime"]: - # Print hint - #hinttext = \ - # l10n.get("datetime_hint").get(fieldtype).decode("utf-8") - #append(DrawHintBox(hinttext)) - - if fieldtype == "datetime": - append(DateTimeBoxes(s3ocr_layout_field_etree)) - elif fieldtype == "date": - append(DateBoxes(s3ocr_layout_field_etree)) - - else: - self.r.error(501, current.ERROR.PARSE_ERROR) - return - - # ------------------------------------------------------------------------- - def __getOCRLayout(self): - """ - return layout file - - @return: layout xml for the generated OCR form - """ - - prettyprint = True if DEBUG else False - return etree.tostring(self.layoutEtree, pretty_print=prettyprint) - - # ------------------------------------------------------------------------- - @staticmethod - def __trim(text): - """ - Helper to trim off any enclosing paranthesis - - @param text: text which need to be trimmed - - @return: text with front and rear paranthesis stripped - """ - - if isinstance(text, str) and \ - text[0] == "(" and \ - text[-1] == ")": - text = text[1:-1] - return text - - # ------------------------------------------------------------------------- - def __update_dbmeta(self, formUUID, layoutXML, numPages): - """ - Store the PDF layout information into the database/disk. - - @param formUUID: uuid of the generated form - @param layoutXML: layout xml of the generated form - @param numPages: number of pages in the generated form - """ - - layout_file_stream = StringIO(layoutXML) - layout_file_name = "%s_xml" % formUUID - - s3ocrxml_file_stream = StringIO(self.s3ocrxml) - s3ocrxml_file_name = "%s_ocrxml" % formUUID - - db = current.db - table = db.ocr_meta - rows = db(table.form_uuid == formUUID).select() - row = rows[0] - row.update_record(layout_file=table.layout_file.store(\ - layout_file_stream, - layout_file_name), - s3ocrxml_file=table.s3ocrxml_file.store(\ - s3ocrxml_file_stream, - s3ocrxml_file_name), - pages=numPages) - - # ------------------------------------------------------------------------- - @staticmethod - def __book_revision(formUUID, formResourceName): - """ - Books a revision number for current operation in ocr_meta - - @param formUUID: uuid of the generated form - @param formResourceName: name of the eden resource - """ - - db = current.db - table = current.s3db.ocr_meta - - # Determine revision - #selector = table["revision"].max() - #rows = db(table.resource_name == formResourceName).select(selector) - #row = rows.first() - #revision = 0 if (row[selector] == None) else (row[selector] + 1) - - # Make the table migratable - # Take the timestamp in hex - import uuid - revision = uuid.uuid5(formUUID, formResourceName).hex.upper()[:6] - - table.insert(form_uuid=formUUID, - resource_name=formResourceName, - revision=revision) - - return revision - - # ------------------------------------------------------------------------- - @staticmethod - def defaultTitle(resource): - """ - Method to extract a generic title from the resource using the - crud strings - - @param: resource: a S3Resource object - - @return: the title as a String - """ - - try: - return current.response.s3.crud_strings.get(resource.table._tablename).get("title_list") - except: - # No CRUD Strings for this resource - return current.T(resource.name.replace("_", " ")).decode("utf-8") - - # ------------------------------------------------------------------------- - def setMargins(self, left=None, right=None, top=None, bottom=None): - """ - Method to set the margins of the document - - @param left: the size of the left margin, default None - @param right: the size of the right margin, default None - @param top: the size of the top margin, default None - @param bottom: the size of the bottom margin, default None - - The margin is only changed if a value is provided, otherwise the - last value that was set will be used. The original values are set - up to be an inch - in newDocument() - - @todo: make this for a page rather than the document - """ - - if left != None: - self.doc.leftMargin = left - self.leftMargin = left - else: - self.doc.leftMargin = self.leftMargin - if right != None: - self.doc.rightMargin = right - self.rightMargin = right - else: - self.doc.rightMargin = self.rightMargin - if top != None: - self.doc.topMargin = top - self.topMargin = top - else: - self.doc.topMargin = self.topMargin - if bottom != None: - self.doc.bottomMargin = bottom - self.bottomMargin = bottom - else: - self.doc.bottomMargin = self.bottomMargin - - # ------------------------------------------------------------------------- - def setPortrait(self): - """ - Method to set the orientation of the document to be portrait - - @todo: make this for a page rather than the document - """ - - self.doc.pagesize = portrait(self.paper_size) - - # ------------------------------------------------------------------------- - def setLandscape(self): - """ - Method to set the orientation of the document to be landscape - - @todo: make this for a page rather than the document - """ - - self.doc.pagesize = landscape(self.paper_size) - - # ------------------------------------------------------------------------- - def addTable(self, - resource = None, - raw_data = None, - list_fields=None, - report_groupby=None, - report_hide_comments=False - ): - """ - Method to create a table that will be inserted into the document - - @param resource: A S3Resource object - @param list_Fields: A list of field names - @param report_groupby: A field name that is to be used as a sub-group - All the records that share the same report_groupby value will - be clustered together - @param report_hide_comments: Any comment field will be hidden - - This uses the class S3PDFTable to build and properly format the table. - The table is then built and stored in the document flow ready for - generating the pdf. - - If the table is too wide for the page then it will automatically - adjust the margin, font or page orientation. If it is still too - wide then the table will be split across multiple pages. - """ - - table = S3PDFTable(document=self, - resource=resource, - raw_data=raw_data, - list_fields=list_fields, - groupby=report_groupby, - hide_comments=report_hide_comments - ) - result = table.build() - if result != None: - self.content += result - - # ------------------------------------------------------------------------- - def extractrHeader(self, - rHeader - ): - """ - Method to convert the HTML generated for a rHeader into PDF - """ - - # let's assume that it's a callable rHeader - try: - # switch the representation to html so the rHeader doesn't barf - repr = self.r.representation - self.r.representation = "html" - html = rHeader(self.r) - self.r.representation = repr - except: - # okay so maybe it wasn't ... it could be an HTML object - html = rHeader - parser = S3html2pdf(pageWidth = self.doc.width, - exclude_class_list=["tabs"]) - result = parser.parse(html) - if result != None: - self.content += result - - # ------------------------------------------------------------------------- - def addrHeader(self, - resource = None, - raw_data = None, - list_fields=None, - report_hide_comments=False - ): - """ - Method to create a rHeader table that is inserted into the document - - @param resource: A S3Resource object - @param list_Fields: A list of field names - @param report_hide_comments: Any comment field will be hidden - - This uses the class S3PDFTable to build and properly format the table. - The table is then built and stored in the document flow ready for - generating the pdf. - """ - - rHeader = S3PDFRHeader(self, - resource, - raw_data, - list_fields, - report_hide_comments - ) - result = rHeader.build() - if result != None: - self.content += result - - # ------------------------------------------------------------------------- - def addPlainTable(self, text, style=None, append=True): - """ - """ - - table = Table(text, style=style) - if append: - self.content.append(table) - return table - - # ------------------------------------------------------------------------- - def addParagraph(self, text, style=None, append=True): - """ - Method to create a paragraph that may be inserted into the document - - @param text: The text for the paragraph - @param append: If True then the paragraph will be stored in the - document flow ready for generating the pdf. - - @return The paragraph - - This method can return the paragraph rather than inserting into the - document. This is useful if the paragraph needs to be first - inserted in another flowable, before being added to the document. - An example of when this is useful is when large amounts of text - (such as a comment) are added to a cell of a table. - """ - - if text != "": - if style == None: - styleSheet = getSampleStyleSheet() - style = styleSheet["Normal"] - para = Paragraph(text, style) - if append: - self.content.append(para) - return para - return "" - - # ------------------------------------------------------------------------- - def addSpacer(self, height, append=True): - """ - Add a spacer to the story - """ - - spacer = Spacer(1, height) - if append: - self.content.append(spacer) - return spacer - - # ------------------------------------------------------------------------- - def addOverlay(self, callback, data): - """ - Add an overlay to the page - """ - - self.content.append(Overlay(callback, data)) - - # ------------------------------------------------------------------------- - def addBoxes(self, cnt, append=True): - """ - Add square text boxes for text entry to the story - """ - - boxes = StringInputBoxes(cnt, etree.Element("dummy")) - if append: - self.content.append(boxes) - return boxes - - # ------------------------------------------------------------------------- - def throwPageBreak(self): - """ - Method to force a page break in the report - """ - - self.content.append(PageBreak()) - - # ------------------------------------------------------------------------- - def changePageTitle(self, newTitle): - """ - Method to force a page break in the report - """ - - self.content.append(ChangePageTitle(self, newTitle)) - - # ------------------------------------------------------------------------- - def getStyledTable(self, table, colWidths=None, rowHeights = None, style=[]): - """ - Method to create a simple table - """ - - (list, style) = self.addCellStyling(table, style) - return Table(list, - colWidths=colWidths, - rowHeights=rowHeights, - style=style, - ) - - # ------------------------------------------------------------------------- - def getTableMeasurements(self, tempTable): - """ - Method to calculate the dimensions of the table - """ - - tempDoc = EdenDocTemplate(StringIO()) - tempDoc.setPageTemplates(lambda x, y: None, lambda x, y: None) - tempDoc.pagesize = portrait(self.paper_size) - tempDoc.build([tempTable], canvasmaker=canvas.Canvas) - return (tempTable._colWidths, tempTable._rowHeights) - - # ------------------------------------------------------------------------- - def cellStyle(self, style, cell): - """ - Add special styles to the text in a cell - """ - - if style == "*GREY": - return [("TEXTCOLOR", cell, cell, colors.lightgrey)] - elif style == "*RED": - return [("TEXTCOLOR", cell, cell, colors.red)] - return [] - - # ------------------------------------------------------------------------- - def addCellStyling(self, table, style): - """ - Add special styles to the text in a table - """ - - row = 0 - for line in table: - col = 0 - for cell in line: - try: - if cell.startswith("*"): - (instruction,sep,text) = cell.partition(" ") - style += self.cellStyle(instruction, (col, row)) - table[row][col] = text - except: - pass - col += 1 - row += 1 - return (table, style) - - # ------------------------------------------------------------------------- - def setHeaderBanner (self, image): - """ - Method to add a banner to a page - used by pageHeader - """ - - self.headerBanner = os.path.join(current.request.folder,image) - - # ------------------------------------------------------------------------- - def pageHeader(self, canvas, doc): - """ - Method to generate the basic look of a page. - It is a callback method and will not be called directly - """ - - canvas.saveState() - if self.logo and os.path.exists(self.logo): - im = Image.open(self.logo) - (iwidth, iheight) = im.size - height = 1.0 * inch - width = iwidth * (height/iheight) - canvas.drawImage(self.logo, - inch, - doc.pagesize[1] - 1.2 * inch, - width = width, - height = height) - if self.headerBanner and os.path.exists(self.headerBanner): - im = Image.open(self.headerBanner) - (iwidth, iheight) = im.size - height = 0.75 * inch - width = iwidth * (height / iheight) - canvas.drawImage(self.headerBanner, - 3 * inch, - doc.pagesize[1] - 0.95 * inch, - width = width, - height = height) - canvas.setFont("Helvetica-Bold", 14) - canvas.drawCentredString(doc.pagesize[0] / 2.0, - doc.pagesize[1] - 1.3*inch, self.title - ) - canvas.setFont("Helvetica-Bold", 8) - now = S3DateTime.datetime_represent(datetime.datetime.utcnow(), utc=True) - canvas.drawCentredString(doc.pagesize[0] - 1.5 * inch, - doc.pagesize[1] - 1.3 * inch, now - ) - canvas.restoreState() - - # ------------------------------------------------------------------------- - def pageFooter(self, canvas, doc): - """ - Method to generate the basic look of a page. - It is a callback method and will not be called directly - """ - - canvas.saveState() - canvas.setFont("Helvetica", 7) - canvas.drawString(inch, 0.75 * inch, - "Page %d %s" % (doc.page, - self.prevtitle - ) - ) - self.prevtitle = self.title - canvas.restoreState() - - # ------------------------------------------------------------------------- - def buildDoc(self): - """ - Method to build the PDF document. - The response headers are set up for a pdf document and the document - is then sent - - @return the document as a stream of characters - - @todo add a proper template class so that the doc.build is more generic - """ - - styleSheet = getSampleStyleSheet() - self.doc.build(self.content, - canvasmaker=canvas.Canvas) - self.output.seek(0) - return self.output.read() - - # Nested classes that extend external libraries - # If the external library failed to be imported then we get a stacktrace - if reportLabImported: - - # ===================================================================== - class S3PDFOCRForm(BaseDocTemplate): - """ - Extended class of the BaseDocTemplate to be used with OCR Forms. - The form has a standard page template that draws handles on the - page in the four corners, the middle of the side and bottom edges - """ - - _invalidInitArgs = ("pageTemplates",) - - # ----------------------------------------------------------------- - def __init__(self, filename, **attr): - - BaseDocTemplate.__init__(self, filename, **attr) - self.formUUID = attr.get("formUUID", "") - self.formResourceName = attr.get("formResourceName", "") - self.formRevision = attr.get("formRevision", "") - self.pdfTitle = attr.get("pdfTitle", "OCR Form") - self.content = [] - self.leftMargin = 20 - self.rightMargin = 20 - self.topMargin = 20 - self.bottomMargin = 20 - settings = current.deployment_settings - if settings.get_pdf_size() == "Letter": - self.paper_size = LETTER - else: - self.paper_size = A4 - - # ----------------------------------------------------------------- - def handle_pageBegin(self): - """ - Override base method to add a change of page template after - the firstpage. - """ - - self._handle_pageBegin() - self._handle_nextPageTemplate("Later") - - # ----------------------------------------------------------------- - def build(self, content=[], canvasmaker=canvas.Canvas, **attr): - """ - Build the document using the flowables. - """ - - T = current.T - self._calc() # in case we changed margins sizes etc - frameT = Frame(self.leftMargin, - self.bottomMargin, - self.width, - self.height, - id="normal") - self.addPageTemplates([PageTemplate(id="First", - frames=frameT, - onPage=self.firstPageTemplate, - pagesize=self.pagesize), - PageTemplate(id="Later", - frames=frameT, - onPage=self.laterPageTemplate, - pagesize=self.pagesize)]) - - # Generate PDF header - ocrInstructions = [ - T("1. Fill the necessary fields in BLOCK CAPITAL letters.").decode("utf-8"), - T("2. Always use one box per letter and leave one box space to separate words.").decode("utf-8"), - T("3. Fill in the circles completely.").decode("utf-8"), - ] - # Put pdf title - styleSheet = getStyleSheet() - self.content = [Paragraph(html_unescape_and_strip(self.pdfTitle), styleSheet["Title"])] - # Print input instructions - append = self.content.append - for eachInstruction in ocrInstructions: - append(Paragraph(html_unescape_and_strip(eachInstruction), - styleSheet["Instructions"])) - - # Add content - self.content.extend(content) - # Build OCRable PDF form - BaseDocTemplate.build(self, self.content, - canvasmaker=canvasmaker) - self.numPages = self.canv.getPageNumber() - 1 - - # ----------------------------------------------------------------- - def firstPageTemplate(self, canvas, doc): - """ - Template for first page - """ - - self.laterPageTemplate(canvas, doc) - - # ----------------------------------------------------------------- - def laterPageTemplate(self, canvas, doc): - """ - Template for all pages but first - """ - - self.pageDecorate(canvas, doc) - self.pageMeta(canvas, doc) - - # ----------------------------------------------------------------- - def pageDecorate(self, canvas, doc): - """ - Decorate Page with blocks for OCR-ability - """ - - canvas.saveState() - pagewidth, pageheight = self.paper_size - canvas.rect(10, 10, 10, 10, fill=1) #btlf - canvas.rect(pagewidth - 20, 10, 10, 10, fill=1) #btrt - canvas.rect(10, pageheight - 20, 10, 10, fill=1) #tplf - canvas.rect(pagewidth / 2 - 5, 10, 10, 10, fill=1) #btmd - canvas.rect(10, pageheight / 2 - 5, 10, 10, fill=1) #mdlf - canvas.rect(pagewidth - 20, - pageheight - 20, 10, 10, fill=1) #tprt - canvas.rect(pagewidth - 20, - pageheight / 2 - 5, 10, 10, fill=1) #mdrt - canvas.restoreState() - - # ----------------------------------------------------------------- - def pageMeta(self, canvas, doc): - """ - Put pagenumber and other meta info on each page - """ - - canvas.saveState() - canvas.setFont("Helvetica", 7) - pageNumberText = "Page %s" % self.canv.getPageNumber() - pagewidth, pageheight = self.paper_size - metaHeight = 14 - pageNumberWidth = pagewidth - (((len(pageNumberText) + 2) * 5) + 40) - pageNumberHeight = metaHeight - canvas.drawString(pageNumberWidth, pageNumberHeight, pageNumberText) - - uuidText = "UUID %s" % self.formUUID - uuidWidth = 40 - uuidHeight = metaHeight - canvas.drawString(uuidWidth, uuidHeight, uuidText) - resourceNameText = self.formResourceName - revisionText = self.formRevision - otherMetaText = "Resource %s Revision %s" % (resourceNameText, - revisionText) - otherMetaWidth = (pagewidth / 2) + 20 - otherMetaHeight = metaHeight - canvas.drawString(otherMetaWidth, otherMetaHeight, otherMetaText) - canvas.restoreState() - -# ============================================================================= -class S3PDFDataSource: - """ - Class to get the labels and the data from the database - """ - - def __init__(self, obj): - """ - Method to create the S3PDFDataSource object - """ - - self.resource = obj.resource - self.list_fields = obj.list_fields - self.report_groupby = obj.report_groupby - self.hideComments = obj.hideComments - self.fields = None - self.labels = None - self.records = False - - # ------------------------------------------------------------------------- - def select(self): - """ - Internally used method to get the data from the database - - If the list of fields is provided then only these will be returned - otherwise all fields on the table will be returned - - Automatically the id field will be hidden, and if - hideComments is true then the comments field will also be hidden. - - If a groupby field is provided then this will be used as the sort - criteria, otherwise it will sort by the first field - - The returned records are stored in the records property. - """ - - resource = self.resource - list_fields = self.list_fields - if not list_fields: - fields = resource.readable_fields() - for field in fields: - if field.type == "id": - fields.remove(field) - if self.hideComments and field.name == "comments": - fields.remove(field) - if not fields: - fields = [table.id] - list_fields = [f.name for f in fields] - else: - indices = S3Codec.indices - list_fields = [f for f in list_fields if f not in indices] - - # Filter and orderby - filter = current.response.s3.filter - if filter is not None: - resource.add_filter(filter) - orderby = self.report_groupby - - # Retrieve the resource contents - table = resource.table - rfields = resource.resolve_selectors(list_fields)[0] - fields = [f for f in rfields if f.show] - headers = [f.label for f in rfields if f.show] - if orderby != None: - orderby = fields[0].field - self.records = resource.select(list_fields, - limit=None, - orderby=orderby, - as_rows=True) - - # Pass to getLabels - self.labels = headers - # Pass to getData - self.fields = fields - # Better to return a PDF, even if it has no records - #if not self.records: - # current.session.warning = current.ERROR.NO_RECORDS - # redirect(URL(extension="")) - - # ------------------------------------------------------------------------- - def getLabels(self): - """ - Internally used method to get the field labels - - Used to remove the report_groupby label (if present) - """ - - # Collect the labels from the select() call - labels = self.labels - if self.report_groupby != None: - for label in labels: - if label == self.report_groupby.label: - labels.remove(label) - return labels - - # ------------------------------------------------------------------------- - def getData(self): - """ - Internally used method to format the data from the database - - This will extract the data from the returned records list. - - If there is a groupby then the records will be grouped by this field. - For each new value the groupby field will be placed in a list of - its own. This will then be followed by lists of the records that - share this value - - If there is no groupby then the result is a simple matrix of - rows by fields - """ - - # Build the data list - data = [] - currentGroup = None - subheadingList = [] - rowNumber = 1 - for item in self.records: - row = [] - if self.report_groupby != None: - # @ToDo: non-XML output should use Field.represent - # - this saves the extra parameter - groupData = s3_represent_value(self.report_groupby, - record=item, - strip_markup=True, - non_xml_output=True - ) - if groupData != currentGroup: - currentGroup = groupData - data.append([groupData]) - subheadingList.append(rowNumber) - rowNumber += 1 - - for field in self.fields: - if self.report_groupby != None: - if field.label == self.report_groupby.label: - continue - if field.field: - text = s3_represent_value(field.field, - record=item, - strip_markup=True, - non_xml_output=True, - extended_comments=True - ) - if text == "" or not field.field: - # some represents replace the data with an image which will - # then be lost by the strip_markup, so get back what we can - tname = field.tname - fname = field.fname - if fname in item: - text = item[fname] - elif tname in item and fname in item[tname]: - text = item[tname][fname] - else: - text = "" - row.append(text) - data.append(row) - rowNumber += 1 - return (subheadingList, data) - -# ============================================================================= -class S3PDFRHeader(): - """ - Class to build a simple table that holds the details of one record, - which can then be placed in a pdf document - - This class doesn't need to be called directly. - Rather see S3PDF.addrHeader() - """ - - def __init__(self, - document, - resource=None, - raw_data=None, - list_fields=None, - hide_comments=False - ): - """ - Method to create an rHeader object - - @param document: An S3PDF object - @param resource: An S3Resource object - @param list_fields: A list of field names - @param hide_comments: Any comment field will be hidden - """ - - self.pdf = document - self.resource = resource - self.raw_data = raw_data - self.list_fields = list_fields - self.hideComments = hide_comments - self.report_groupby = None - self.data = [] - self.subheadingList = [] - self.labels = [] - self.fontsize = 10 - - # ------------------------------------------------------------------------- - def build(self): - """ - Method to build the table. - - @return: A list of Table objects. Normally this will be a list with - just one table object, but if the table needs to be split - across columns then one object per page will be created. - """ - - if self.resource != None: - ds = S3PDFDataSource(self) - # Get records - ds.select() - self.labels = ds.getLabels() - self.data.append(self.labels) - (self.subheadingList, data) = ds.getData() - self.data + data - - if self.raw_data != None: - self.data = self.raw_data - - self.rheader = [] - if len(self.data) == 0: - return None - else: - NONE = current.messages["NONE"] - for index in range(len(self.labels)): - try: - value = data[0][index] - except: - value = NONE - self.rheader.append([self.labels[index], - value]) - content = [] - style = [("FONTSIZE", (0, 0), (-1, -1), self.fontsize), - ("VALIGN", (0, 0), (-1, -1), "TOP"), - ("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"), - ("FONTNAME", (1, 0), (1, -1), "Helvetica"), - ] - (self.rheader,style) = self.pdf.addCellStyling(self.rheader, style) - table = Table(self.rheader, - repeatRows=1, - style=style, - hAlign="LEFT", - ) - content.append(table) - return content - -# ============================================================================= -# Custom Flowables (used by OCR) -if reportLabImported: - - # ========================================================================= - class DrawHrLine(Flowable): - """ - Draw a horizontal line - """ - - def __init__(self, lineThickness): - Flowable.__init__(self) - self.lineThickness = 1 - if current.deployment_settings.get_pdf_size() == "Letter": - self.paper_size = LETTER - else: - self.paper_size = A4 - - # --------------------------------------------------------------------- - def draw(self): - canv = self.canv - pagewidth, pageheight = self.paper_size - self.canv.line(0, -5, pagewidth - 100, -5) - - # --------------------------------------------------------------------- - def wrap(self, availWidth, availHeight): - self._width = availWidth - self._height = self.lineThickness - return self._width, self._height - - # ========================================================================= - class StringInputBoxes(Flowable): - """ - Draw input boxes in a complete line - """ - - def __init__(self, numBoxes=None, etreeElem=None): - Flowable.__init__(self) - self.spaceAfter = 2 - self.sideLength = 15 - self.numBoxes = numBoxes - self.fontsize = 10 - self.etreeElem = etreeElem - if current.deployment_settings.get_pdf_size() == "Letter": - self.paper_size = LETTER - else: - self.paper_size = A4 - - # --------------------------------------------------------------------- - def draw(self): - canv = self.canv - pagewidth, pageheight = self.paper_size - numBoxes = int((pagewidth - (100 + self.fontsize)) / self.sideLength) - if self.numBoxes != None and\ - isinstance(self.numBoxes, int): - numBoxes = self.numBoxes - canv.setLineWidth(0.90) - canv.setStrokeGray(0.9) - widthPointer = self.fontsize - # values are set manually - xpadding = 6 # default - ypadding = 4 - margin = 50 # as set - # Reportlab's coordinate system uses bottom left - # as origin, so we have to take top left marker as - # origin to provide input for Python Imaging. - markerOrigin = (29, 29) # top left marker location - xCoord = pagewidth - \ - (self.layoutCoords[0] + xpadding + margin) - \ - markerOrigin[0] + \ - self.fontsize - yCoord = pageheight - \ - (self.layoutCoords[1] + ypadding + margin) - \ - markerOrigin[1] - for box in range(numBoxes): - self.canv.rect(widthPointer, - 0, - self.sideLength, - self.sideLength) - widthPointer += self.sideLength - StringInputBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % xCoord, - y="%s" % yCoord, - side="%s" % self.sideLength, - boxes="%s" % numBoxes, - page="%s" % self.canv.getPageNumber()) - StringInputBoxEtree.text = " " - - # --------------------------------------------------------------------- - def wrap(self, availWidth, availHeight): - self.layoutCoords = availWidth, availHeight - self._width = availWidth - self._height = self.sideLength + self.spaceAfter - return self._width, self._height - - # ========================================================================= - class DateBoxes(Flowable): - """ - Draw date boxes - """ - - def __init__(self, etreeElem): - Flowable.__init__(self) - self.spaceAfter = 2 - self.sideLength = 15 - self.fontsize = 10 - self.etreeElem = etreeElem - if current.deployment_settings.get_pdf_size() == "Letter": - self.paper_size = LETTER - else: - self.paper_size = A4 - - # --------------------------------------------------------------------- - def draw(self): - canv = self.canv - pagewidth, pageheight = self.paper_size - canv.setLineWidth(0.90) - canv.setStrokeGray(0.9) - widthPointer = self.fontsize - # Values are set manually - xpadding = 6 # default - ypadding = 4 - margin = 50 # as set - # Reportlab's coordinate system uses bottom left - # as origin, so we have to take top left marker as - # origin to provide input for Python Imaging. - markerOrigin = (29, 29) # top left marker location - xCoord = pagewidth - \ - (self.layoutCoords[0] + xpadding + margin) - \ - markerOrigin[0] + \ - self.fontsize - yCoord = pageheight - \ - (self.layoutCoords[1] + ypadding + margin) - \ - markerOrigin[1] - - sideLength = self.sideLength - rect = self.canv.rect - for box in range(1, 11): - if box not in (3, 6): - rect(widthPointer, - 0, - sideLength, - sideLength) - else: - self.canv.drawString(widthPointer + 5, - self.height, - "/") - widthPointer += 15 - getPageNumber = self.canv.getPageNumber - dtformat = current.deployment_settings.get_L10n_datetime_format() - if str(dtformat)[:2] == "%m": - # US-style - DateBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % xCoord, - y="%s" % yCoord, - side="%s" % sideLength, - boxes="2", - page="%s" % getPageNumber()) - DateBoxEtree.text = "MO" - DateBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % (xCoord + (sideLength * 3)), - y="%s" % yCoord, - side="%s" % sideLength, - boxes="2", - page="%s" % getPageNumber()) - DateBoxEtree.text = "DD" - else: - # ISO-style - DateBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % xCoord, - y="%s" % yCoord, - side="%s" % sideLength, - boxes="2", - page="%s" % getPageNumber()) - DateBoxEtree.text = "DD" - DateBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % (xCoord + (sideLength * 3)), - y="%s" % yCoord, - side="%s" % sideLength, - boxes="2", - page="%s" % getPageNumber()) - DateBoxEtree.text = "MO" - DateBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % (xCoord + (sideLength * 6)), - y="%s" % yCoord, - side="%s" % sideLength, - boxes="4", - page="%s" % getPageNumber()) - DateBoxEtree.text = "YYYY" - - # --------------------------------------------------------------------- - def wrap(self, availWidth, availHeight): - self.layoutCoords = availWidth, availHeight - self._width = availWidth - self._height = self.sideLength + self.spaceAfter - return self._width, self._height - - # ========================================================================= - class DateTimeBoxes(Flowable): - """ - Draw datetime boxes - """ - - def __init__(self, etreeElem): - Flowable.__init__(self) - self.spaceAfter = 2 - self.sideLength = 15 - self.fontsize = 10 - self.etreeElem = etreeElem - if current.deployment_settings.get_pdf_size() == "Letter": - self.paper_size = LETTER - else: - self.paper_size = A4 - - # --------------------------------------------------------------------- - def draw(self): - canv = self.canv - pagewidth, pageheight = self.paper_size - canv.setLineWidth(0.90) - canv.setStrokeGray(0.9) - widthPointer = self.fontsize - # Values are set manually - xpadding = 6 # default - ypadding = 4 - margin = 50 # as set - # Reportlab's coordinate system uses bottom-left - # as origin, so we have to take top-left marker as - # origin to provide input for Python Imaging. - markerOrigin = (29, 29) # top-left marker location - xCoord = pagewidth - \ - (self.layoutCoords[0] + xpadding + margin) - \ - markerOrigin[0]+\ - self.fontsize - yCoord = pageheight - \ - (self.layoutCoords[1] + ypadding + margin) - \ - markerOrigin[1] - - for box in range(1, 18): - if box not in (3, 6, 7, 10, 13): - self.canv.rect(widthPointer, - 0, - self.sideLength, - self.sideLength) - widthPointer += 15 - DateTimeBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % xCoord, - y="%s" % yCoord, - side="%s" % self.sideLength, - boxes="2", - page="%s" % self.canv.getPageNumber()) - DateTimeBoxEtree.text = "HH" - DateTimeBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % (xCoord + (self.sideLength * 3)), - y="%s" % yCoord, - side="%s" % self.sideLength, - boxes="2", - page="%s" % self.canv.getPageNumber()) - DateTimeBoxEtree.text = "MM" - dtformat = current.deployment_settings.get_L10n_datetime_format() - if str(dtformat)[:2] == "%m": - # US-style - DateTimeBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % (xCoord + (self.sideLength * 7)), - y="%s" % yCoord, - side="%s" % self.sideLength, - boxes="2", - page="%s" % self.canv.getPageNumber()) - DateTimeBoxEtree.text = "MO" - DateTimeBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % (xCoord + (self.sideLength * 10)), - y="%s" % yCoord, - side="%s" % self.sideLength, - boxes="2", - page="%s" % self.canv.getPageNumber()) - DateTimeBoxEtree.text = "DD" - else: - # ISO-style - DateTimeBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % (xCoord + (self.sideLength * 7)), - y="%s" % yCoord, - side="%s" % self.sideLength, - boxes="2", - page="%s" % self.canv.getPageNumber()) - DateTimeBoxEtree.text = "DD" - DateTimeBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % (xCoord + (self.sideLength * 10)), - y="%s" % yCoord, - side="%s" % self.sideLength, - boxes="2", - page="%s" % self.canv.getPageNumber()) - DateTimeBoxEtree.text = "MO" - DateTimeBoxEtree = etree.SubElement(self.etreeElem, - "textbox", - x="%s" % (xCoord + (self.sideLength * 13)), - y="%s" % yCoord, - side="%s" % self.sideLength, - boxes="4", - page="%s" % self.canv.getPageNumber()) - DateTimeBoxEtree.text = "YYYY" - - # --------------------------------------------------------------------- - def wrap(self, availWidth, availHeight): - self.layoutCoords = availWidth, availHeight - self._width = availWidth - self._height = self.sideLength + self.spaceAfter - return self._width, self._height - - # ========================================================================= - class DrawOptionBoxes(Flowable): - """ - Draw a set of Option Boxes (for Boolean or Multi-Select) - - along with Labels - """ - - def __init__(self, etreeElem, labels, values): - Flowable.__init__(self) - self.etreeElem = etreeElem - self.fontsize = 8 - self.spaceAfter = 2 - self.labels = labels - self.text = labels[0] - self.values = values - if current.deployment_settings.get_pdf_size() == "Letter": - self.paper_size = LETTER - else: - self.paper_size = A4 - - # --------------------------------------------------------------------- - def draw(self): - canv = self.canv - pagewidth, pageheight = self.paper_size - canv.setLineWidth(0.90) - canv.setStrokeGray(0.9) - fontsize = self.fontsize - radius = (fontsize / 2) - 1 - # Values are set manually - xpadding = 6 # default - ypadding = 8 - margin = 50 # as set - # Reportlab's coordinate system uses bottom left - # as origin, so we have to take top-left marker as - # origin to provide input for Python Imaging. - markerOrigin = (29, 29) # top-left marker location - layoutCoords = self.layoutCoords - pwidth = pagewidth - (layoutCoords[0] + xpadding + margin) - markerOrigin[0] - pheight = pageheight - (layoutCoords[1] + ypadding + margin) - markerOrigin[1] - labels = self.labels - index = 0 - values = self.values - circle = self.canv.circle - drawString = self.canv.drawString - getPageNumber = self.canv.getPageNumber - etreeElem = self.etreeElem - height = self.height - cheight = height + (fontsize / 4) + 1 - width = self.width - # Width of the circle - cwidth = width + fontsize - # Initial X for the elements - _cwidth = width + fontsize - _swidth = width + (fontsize * 2) - for label in labels: - # Draw circle to fill-in - circleCenter = (_cwidth, cheight) - circle(circleCenter[0], - circleCenter[1], - radius, - fill=0) - # Add label - drawString(_swidth, height, - html_unescape_and_strip(label)) - xCoord = pwidth + circleCenter[0] - yCoord = pheight + circleCenter[0] - optionBoxEtree = etree.SubElement(etreeElem, - "optionbox", - x="%s" % xCoord, - y="%s" % yCoord, - radius="%s" % radius, - boxes="1", - page="%s" % getPageNumber()) - optionBoxEtree.set("value", values[index]) - optionBoxEtree.text = label - xwidth = cwidth + (fontsize * (len(label) + 2)) / 1.4 - _cwidth += xwidth - _swidth += xwidth - index += 1 - - # --------------------------------------------------------------------- - def wrap(self, availWidth, availHeight): - self.layoutCoords = availWidth, availHeight - width = 0 - for label in self.labels: - width += (len(label) + 8) - fontsize = self.fontsize - self._width = (fontsize * width) / 2 - self._height = fontsize + self.spaceAfter - return self._width, self._height - - # ========================================================================= - class DrawHintBox(Flowable): - """ - Draw Help Text to explain how to fill out a question - """ - - def __init__(self, text=""): - Flowable.__init__(self) - self.text = text - self.fontsize = 6 - self.spaceAfter = 2 - if current.deployment_settings.get_pdf_size() == "Letter": - self.paper_size = LETTER - else: - self.paper_size = A4 - - # --------------------------------------------------------------------- - def draw(self): - canv = self.canv - canv.setFillGray(0.4) - self.canv.drawString(self.width + (self.fontsize / 2), - self.height, - html_unescape_and_strip(self.text)) - - # --------------------------------------------------------------------- - def wrap(self, availWidth, availHeight): - fontsize = self.fontsize - self._width = (fontsize * (len(self.text) + 4)) / 2 - self._height = fontsize + self.spaceAfter - return self._width, self._height - - # ------------------------------------------------------------------------- - # Custom styleSheets - _baseFontNameB = tt2ps(_baseFontName, 1, 0) - _baseFontNameI = tt2ps(_baseFontName, 0, 1) - _baseFontNameBI = tt2ps(_baseFontName, 1, 1) - - def getStyleSheet(): - """ - """ - - styleSheet = getSampleStyleSheet() - styleSheet.add(ParagraphStyle(name="Instructions", - parent=styleSheet["Bullet"], - fontName=_baseFontName, - fontSize=12, - firstLineIndent=0, - spaceBefore=3), - alias="Inst") - styleSheet.add(ParagraphStyle(name="Section", - parent=styleSheet["Normal"], - fontName=_baseFontName, - fontSize=13, - spaceBefore=5, - spaceAfter=5, - firstLineIndent=0), - alias="Sec") - styleSheet.add(ParagraphStyle(name="Question", - parent=styleSheet["Normal"], - fontName=_baseFontName, - fontSize=11, - firstLineIndent=0, - spaceAfter=5, - spaceBefore=10), - alias="Quest") - styleSheet.add(ParagraphStyle(name="DefaultAnswer", - parent=styleSheet["Normal"], - fontName=_baseFontName, - fontSize=10, - firstLineIndent=0, - spaceBefore=3), - alias="DefAns") - return styleSheet - -# Helper functions (used by OCR) -html_unescape_and_strip = lambda m: html_strip(html_unescape(m)) - -# ============================================================================= -def html_unescape(text): - """ - Helper function, unscape any html special characters - """ - - return re.sub("&(%s);" % "|".join(name2codepoint), - lambda m: chr(name2codepoint[m.group(1)]), - text) - -# ============================================================================= -def html_strip(text): - """ - Strips html markup from text - """ - - mark = 0 - markstart = 0 - markend = 0 - index = 0 - occur = 0 - for i in text: - if i == "<": - try: - if text[index+1] != " ": - mark = 1 - markstart = index - except(IndexError): - pass - elif i == ">": - if mark == 1: - mark = 0 - markend = index - text = "%s%s" % (text[:markstart], text[markend+1:]) - occur = 1 - break - - index += 1 - - if occur == 1: - text = html_strip(text) - - return text - -# ============================================================================= -# Convert unicode to ascii compatible strings -cast2ascii = lambda m: \ - m if isinstance(m, str) else unicodedata.normalize("NFKD", - m).encode("ascii", - "ignore") - -# ============================================================================= -class S3OCRImageParser(object): - """ - Image Parsing and OCR Utility - """ - - def __init__(self, s3method, r): - """ - Intialise class instance with environment variables and functions - """ - - self.r = r - self.request = current.request - checkDependencies(r) - - # ------------------------------------------------------------------------- - def parse(self, form_uuid, set_uuid, **kwargs): - """ - Performs OCR on a given set of pages - """ - - raw_images = {} - images = {} - - self.set_uuid = set_uuid - db = current.db - T = current.T - request = self.request - - # Get metadata of the form - metatable = "ocr_meta" - query = (db[metatable]["form_uuid"] == form_uuid) - row = db(query).select(limitby=(0, 1)).first() - revision = row["revision"] - resourcename = row["resource_name"] - layoutfilename = row["layout_file"] - pages = int(row["pages"]) - is_component = True if len(self.r.resource.components) == 1 else False - - # Open each page - for eachpage in range(1, pages+1): - payloadtable = "ocr_payload" - row =\ - db((db[payloadtable]["image_set_uuid"]==set_uuid) &\ - (db[payloadtable]["page_number"]==eachpage) - ).select().first() - - pageimagefile = row["image_file"] - raw_images[eachpage] =\ - Image.open(os.path.join(self.r.folder, - "uploads", - "ocr_payload", - pageimagefile)) - - # Transform each image - for each_img_index in raw_images.keys(): - images[each_img_index] = {} - images[each_img_index]["image"] =\ - self.__convertImage2binary(raw_images[each_img_index]) - images[each_img_index]["markers"] =\ - self.__getMarkers(images[each_img_index]["image"]) - images[each_img_index]["orientation"] =\ - self.__getOrientation(images[each_img_index]["markers"]) - if images[each_img_index]["orientation"] != 0.0: - images[each_img_index]["image"] =\ - images[each_img_index]["image"].rotate(images[each_img_index]["orientation"]) - images[each_img_index]["markers"] =\ - self.__getMarkers(images[each_img_index]["image"]) - images[each_img_index]["orientation"] =\ - self.__getOrientation(images[each_img_index]["markers"]) - - images[each_img_index]["scalefactor"] =\ - self.__scaleFactor(images[each_img_index]["markers"]) - - # Get layout file, convert it to etree - layout_file = open(os.path.join(self.r.folder, - "uploads", - "ocr_meta", - layoutfilename), - "rb") - layout_xml = layout_file.read() - layout_file.close() - layout_etree = etree.fromstring(layout_xml) - - # Data etree - s3xml_root_etree = etree.Element("s3xml") - parent_resource_exist = False - - SubElement = etree.SubElement - for resource in layout_etree: - # Create data etree - if not is_component: - if parent_resource_exist == False: - s3xml_parent_resource_etree = SubElement(s3xml_root_etree, - "resource") - s3xml_resource_etree = s3xml_parent_resource_etree - parent_resource_exist = True - else: - s3xml_resource_etree = SubElement(s3xml_parent_resource_etree, - "resource") - else: - s3xml_resource_etree = SubElement(s3xml_root_etree, - "resource") - - s3xml_resource_etree.set("name", - resource.attrib.get("name", None)) - - for field in resource: - field_name = field.attrib.get("name", None) - field_type = field.attrib.get("type", None) - field_reference = field.attrib.get("reference") - - if field_reference == "1": - field_is_reference = True - field_resource = field.attrib.get("resource") - else: - field_is_reference = False - - # Create data/reference etree - if field_is_reference: - s3xml_reference_etree = SubElement(s3xml_resource_etree, - "reference") - s3xml_reference_etree.set("field", field_name) - s3xml_reference_etree.set("resource", field_resource) - - s3xml_sub_reference_etree = SubElement(s3xml_reference_etree, - "resource") - s3xml_sub_reference_etree.set("name", field_resource) - - s3xml_field_etree = SubElement(s3xml_sub_reference_etree, - "data") - s3xml_field_etree.set("field", "name") - - else: - s3xml_field_etree = SubElement(s3xml_resource_etree, - "data") - s3xml_field_etree.set("field", field_name) - #s3xml_field_etree.set("type", field_type) - - components = field.getchildren() - numcomponents = len(components) - null_field = False - if numcomponents == 0: - continue - else: - component_type = components[0].tag - if component_type in ("optionbox", "textbox"): - if component_type == "optionbox": - linenum = 0 - OCRText = [] - OCRValue = [] - for component in components: - get = component.attrib.get - comp_x = float(get("x")) - comp_y = float(get("y")) - comp_boxes = int(get("boxes")) - comp_radius = float(get("radius")) - comp_page = int(get("page")) - comp_value = str(get("value")) - comp_text = str(component.text) - try: - page_origin = images[comp_page]["markers"] - except(KeyError): - self.r.error(501, - T("insufficient number of pages provided")) - crop_box = ( - int(page_origin[0][0]+\ - (comp_x*\ - images[comp_page]["scalefactor"]["x"])-\ - comp_radius*images[comp_page]["scalefactor"]["x"]), - int(page_origin[0][1]+\ - (comp_y*\ - images[comp_page]["scalefactor"]["y"])-\ - comp_radius*images[comp_page]["scalefactor"]["y"]), - int(page_origin[0][0]+\ - (comp_x*\ - images[comp_page]["scalefactor"]["x"])+\ - comp_radius*images[comp_page]["scalefactor"]["x"]), - int(page_origin[0][1]+\ - (comp_y*\ - images[comp_page]["scalefactor"]["y"])+\ - comp_radius*images[comp_page]["scalefactor"]["y"]), - ) - temp_image = images[comp_page]["image"].crop(crop_box) - cropped_image = images[comp_page]["image"].crop(crop_box) - result = self.__ocrIt(cropped_image, - form_uuid, - resourcename, - linenum, - content_type="optionbox", - resource_table=resource.attrib.get("name"), - field_name=field.attrib.get("name"), - field_value=comp_value) - if result: - OCRText.append(s3_str(comp_text).strip()) - OCRValue.append(s3_str(comp_value).strip()) - - linenum += 1 - - # Store values into xml - if len(OCRValue) in [0, 1]: - uOCRValue = "|".join(OCRValue) - uOCRText = "|".join(OCRText) - else: - uOCRValue = "|%s|" % "|".join(OCRValue) - uOCRText = "|%s|" % "|".join(OCRText) - - s3xml_field_etree.set("value", uOCRValue) - s3xml_field_etree.text = uOCRText - - if len(OCRValue) == 0: - null_field = True - else: - null_field = False - - elif component_type == "textbox": - linenum = 1 - if field_type in ["date", "datetime"]: - # Date(Time) Text Box - OCRedValues = {} - comp_count = 1 - for component in components: - get = component.attrib.get - comp_x = float(get("x")) - comp_y = float(get("y")) - comp_boxes = int(get("boxes")) - comp_side = float(get("side")) - comp_page = int(get("page")) - comp_meta = str(component.text) - try: - page_origin = images[comp_page]["markers"] - except(KeyError): - self.r.error(501, - T("insufficient number of pages provided")) - crop_box = ( - int(page_origin[0][0]+\ - (comp_x*\ - images[comp_page]["scalefactor"]["x"])), - int(page_origin[0][1]+\ - (comp_y*\ - images[comp_page]["scalefactor"]["y"])), - int(page_origin[0][0]+\ - (comp_x*\ - images[comp_page]["scalefactor"]["x"])+\ - comp_side*comp_boxes*images[comp_page]["scalefactor"]["x"]), - int(page_origin[0][1]+\ - (comp_y*\ - images[comp_page]["scalefactor"]["y"])+\ - comp_side*images[comp_page]["scalefactor"]["y"]), - ) - cropped_image = images[comp_page]["image"].crop(crop_box) - output = self.__ocrIt(cropped_image, - form_uuid, - resourcename, - linenum, - resource_table=resource.attrib.get("name"), - field_name=field.attrib.get("name"), - field_seq=comp_count) - linenum += 1 - comp_count += 1 - - OCRedValues[comp_meta] = s3_str(output).strip() - - # YYYY - yyyy = datetime.datetime.now().year - try: - if int(OCRedValues["YYYY"]) in range(1800, 2300): - yyyy = int(OCRedValues["YYYY"]) - except: - pass - - if yyyy % 4 == 0: - leapyear = True - else: - leapyear = False - - # MO - try: - if int(OCRedValues["MO"]) in range(1, 13): - mo = int(OCRedValues["MO"]) - except: - mo = 1 - - # DD - try: - if int(OCRedValues["DD"]) in range(1, 32): - dd = int(OCRedValues["DD"]) - except: - dd = 1 - - if mo in [4, 6, 9, 11]: - if dd == 31: - dd = 1 - elif mo == 2: - if leapyear: - if dd > 29: - dd = 1 - else: - if dd > 28: - dd = 1 - - if field_type == "datetime": - # MM - try: - if int(OCRedValues["MM"]) in range(0, 60): - mm = int(OCRedValues["MM"]) - except: - mm = 0 - - # MM - try: - if int(OCRedValues["HH"]) in range(0, 24): - hh = int(OCRedValues["HH"]) - except: - hh = 0 - - if field_type == "date": - s3xml_field_etree.set("value", - "%s-%s-%s" % (yyyy, mo, dd)) - s3xml_field_etree.text =\ - "%s-%s-%s" % (yyyy, mo, dd) - - elif field_type == "datetime": - utctime = self.__convert_utc(yyyy, mo, dd, hh, mm) - utcftime = utctime.strftime("%Y-%m-%dT%H:%M:%SZ") - s3xml_field_etree.set("value", utcftime) - s3xml_field_etree.text = utcftime - - else: - # Normal Text Box - ocrText = "" - comp_count = 1 - for component in components: - comp_x = float(component.attrib.get("x")) - comp_y = float(component.attrib.get("y")) - comp_boxes = int(component.attrib.get("boxes")) - comp_side = float(component.attrib.get("side")) - comp_page = int(component.attrib.get("page")) - comp_meta = str(component.text) - try: - page_origin = images[comp_page]["markers"] - except(KeyError): - self.r.error(501, - T("insufficient number of pages provided")) - crop_box = ( - int(page_origin[0][0]+\ - (comp_x*\ - images[comp_page]["scalefactor"]["x"])), - int(page_origin[0][1]+\ - (comp_y*\ - images[comp_page]["scalefactor"]["y"])), - int(page_origin[0][0]+\ - (comp_x*\ - images[comp_page]["scalefactor"]["x"])+\ - comp_side*comp_boxes*images[comp_page]["scalefactor"]["x"]), - int(page_origin[0][1]+\ - (comp_y*\ - images[comp_page]["scalefactor"]["y"])+\ - comp_side*images[comp_page]["scalefactor"]["y"]), - ) - cropped_image = images[comp_page]["image"].crop(crop_box) - output = self.__ocrIt(cropped_image, - form_uuid, - resourcename, - linenum, - resource_table=resource.attrib.get("name"), - field_name=field.attrib.get("name"), - field_seq=comp_count) - ocrText += output - linenum += 1 - comp_count += 1 - - output = s3_str(ocrText).strip() - # Store OCRText - if field_type in ["double", "integer"]: - try: - output = int(self.__strip_spaces(output)) - except: - output = 0 - s3xml_field_etree.set("value", - "%s" % output) - s3xml_field_etree.text =\ - "%s" % output - else: - s3xml_field_etree.text = output - - if len("%s" % output) == 0: - null_field = True - else: - null_field = False - - else: - continue - - if null_field: - if field_is_reference: - s3xml_resource_etree.remove(s3xml_reference_etree) - - else: - s3xml_resource_etree.remove(s3xml_field_etree) - - output = etree.tostring(s3xml_root_etree, pretty_print=True) - return output - - # ------------------------------------------------------------------------- - def __strip_spaces(self, text): - """ - Remove all spaces from a string - """ - - try: - text = "".join(text.split()) - except: - pass - - return text - - # ------------------------------------------------------------------------- - def __convert_utc(self, - yyyy, - mo, - dd, - hh, - mm): - """ - Convert local time to UTC - """ - - timetuple = datetime.datetime.strptime("%s-%s-%s %s:%s:00" % (yyyy, - mo, - dd, - hh, - mm), - "%Y-%m-%d %H:%M:%S") - auth = current.auth - if auth.user: - utc_offset = auth.user.utc_offset - else: - utc_offset = None - try: - t = utc_offset.split()[1] - if len(t) == 5: - sign = t[0] - hours = t[1:3] - minutes = t[3:5] - tdelta = datetime.timedelta(hours=int(hours), minutes=int(minutes)) - if sign == "+": - utctime = timetuple - tdelta - elif sign == "-": - utctime = timetuple + tdelta - except: - utctime = timetuple - - return utctime - - # ------------------------------------------------------------------------- - def __ocrIt(self, - image, - form_uuid, - resourcename, - linenum, - content_type="textbox", - **kwargs): - """ - Put Tesseract to work, actual OCRing will be done here - """ - - db = current.db - ocr_field_crops = "ocr_field_crops" - import uuid - uniqueuuid = uuid.uuid1() # to make it thread safe - - resource_table = kwargs.get("resource_table") - field_name = kwargs.get("field_name") - - inputfilename = "%s_%s_%s_%s.tif" % (uniqueuuid, - form_uuid, - resourcename, - linenum) - outputfilename = "%s_%s_%s_%s_text" % (uniqueuuid, - form_uuid, - resourcename, - linenum) - - ocr_temp_dir = os.path.join(self.r.folder, "uploads", "ocr_temp") - - try: - os.mkdir(ocr_temp_dir) - except(OSError): - pass - - if content_type == "optionbox": - field_value = kwargs.get("field_value") - imgfilename = "%s.png" % inputfilename[:-3] - imgpath = os.path.join(ocr_temp_dir, imgfilename) - image.save(imgpath) - imgfile = open(imgpath, "r") - db[ocr_field_crops].insert(image_set_uuid=self.set_uuid, - resource_table=resource_table, - field_name=field_name, - image_file=db[ocr_field_crops]["image_file"].store(imgfile, - imgfilename), - value=field_value) - imgfile.close() - os.remove(imgpath) - - stat = ImageStat.Stat(image) - if stat.mean[0] < 96 : - return True - else: - return None - - elif content_type == "textbox": - field_seq = kwargs.get("field_seq") - - inputpath = os.path.join(ocr_temp_dir, inputfilename) - image.save(inputpath) - - success =\ - subprocess.call(["tesseract", inputpath, - os.path.join(ocr_temp_dir, outputfilename)]) - if success != 0: - self.r.error(501, ERROR.TESSERACT_ERROR) - outputpath = os.path.join(ocr_temp_dir, "%s.txt" % outputfilename) - outputfile = open(outputpath) - outputtext = outputfile.read() - outputfile.close() - output = outputtext.replace("\n", " ") - os.remove(outputpath) - imgfilename = "%s.png" % inputfilename[:-3] - imgpath = os.path.join(ocr_temp_dir, imgfilename) - image.save(imgpath) - imgfile = open(imgpath, "r") - db[ocr_field_crops].insert(image_set_uuid=self.set_uuid, - resource_table=resource_table, - field_name=field_name, - image_file=db[ocr_field_crops]["image_file"].store(imgfile, - imgfilename), - sequence=field_seq) - imgfile.close() - os.remove(imgpath) - os.remove(inputpath) - - try: - os.rmdir(ocr_temp_dir) - except(OSError): - import shutil - shutil.rmtree(ocr_temp_dir) - return output - - # ------------------------------------------------------------------------- - def __convertImage2binary(self, image, threshold = 180): - """ - Converts the image into binary based on a threshold. here it is 180 - """ - - image = ImageOps.grayscale(image) - image.convert("L") - - width, height = image.size - - for x in range(width): - for y in range(height): - if image.getpixel((x,y)) < 180 : - image.putpixel((x,y), 0) - else: - image.putpixel((x,y), 255) - return image - - # ------------------------------------------------------------------------- - def __findRegions(self, im): - """ - Return the list of regions which are found by the following algorithm. - - ----------------------------------------------------------- - Raster Scanning Algorithm for Connected Component Analysis: - ----------------------------------------------------------- - - On the first pass: - ================= - 1. Iterate through each element of the data by column, then by row (Raster Scanning) - 2. If the element is not the background - 1. Get the neighboring elements of the current element - 2. If there are no neighbors, uniquely label the current element and continue - 3. Otherwise, find the neighbor with the smallest label and assign it to the current element - 4. Store the equivalence between neighboring labels - - On the second pass: - =================== - 1. Iterate through each element of the data by column, then by row - 2. If the element is not the background - 1. Relabel the element with the lowest equivalent label - ( source: http://en.wikipedia.org/wiki/Connected_Component_Labeling ) - """ - - width, height = im.size - ImageOps.grayscale(im) - im = im.convert("L") - - regions = {} - pixel_region = [[0 for y in range(height)] for x in range(width)] - equivalences = {} - n_regions = 0 - - # First pass: find regions. - for x in range(width): - for y in range(height): - # Look for a black pixel - if im.getpixel((x, y)) == 0 : # BLACK - # get the region number from north or west or create new region - region_n = pixel_region[x-1][y] if x > 0 else 0 - region_w = pixel_region[x][y-1] if y > 0 else 0 - #region_nw = pixel_region[x-1][y-1] if x > 0 and y > 0 else 0 - #region_ne = pixel_region[x-1][y+1] if x > 0 else 0 - - max_region = max(region_n, region_w) - - if max_region > 0: - #a neighbour already has a region, new region is the smallest > 0 - new_region = min([i for i in (region_n, region_w) if i > 0]) - #update equivalences - if max_region > new_region: - if max_region in equivalences: - equivalences[max_region].add(new_region) - else: - equivalences[max_region] = {new_region} - else: - n_regions += 1 - new_region = n_regions - - pixel_region[x][y] = new_region - - # Scan image again, assigning all equivalent regions the same region value. - for x in range(width): - for y in range(height): - r = pixel_region[x][y] - if r > 0: - while r in equivalences: - r = min(equivalences[r]) - - if r in regions: - regions[r].add(x, y) - else: - regions[r] = self.__Region(x, y) - - return list(regions.values()) - - # ------------------------------------------------------------------------- - def __getOrientation(self, markers): - """ - Returns orientation of the sheet in radians - """ - - x1, y1 = markers[0] - x2, y2 = markers[2] - try: - slope = ((x2 - x1) * 1.0) / ((y2 - y1) * 1.0) - except(ZeroDivisionError): - slope = 999999999999999999999999999 - return math.atan(slope) * (180.0 / math.pi) * (-1) - - # ------------------------------------------------------------------------- - def __scaleFactor(self, markers): - """ - Returns the scale factors lengthwise and breadthwise - """ - - stdWidth = sum((596, -60)) - stdHeight = sum((842, -60)) - li = [markers[0], markers[2]] - sf_y = self.__distance(li)/stdHeight - li = [markers[6], markers[2]] - sf_x = self.__distance(li)/stdWidth - return {"x": sf_x, - "y": sf_y - } - - # ------------------------------------------------------------------------- - def __distance(self, li): - """ - Returns the euclidean distance if the input is of the form [(x1, y1), (x2, y2)] - """ - - return math.sqrt(math.fsum((math.pow(math.fsum((int(li[1][0]), -int(li[0][0]))), 2), math.pow(math.fsum((int(li[1][1]), -int(li[0][1]))), 2)))) - - # ------------------------------------------------------------------------- - def __getMarkers(self, image): - """ - Gets the markers on the OCR image - """ - - centers = {} - present = 0 - - regions = self.__findRegions(image) - - for r in regions: - if r.area > 320 and r.aspectratio() < 1.5 and r.aspectratio() > 0.67: - present += 1 - centers[present] = r.centroid() - - # This is the list of all the markers on the form. - markers = sorted(centers.values()) - l1 = sorted(markers[0:3], key=lambda y: y[1]) - l2 = markers[3:4] - l3 = sorted(markers[4:7], key=lambda y: y[1]) - markers = [] - markers.extend(l1) - markers.extend(l2) - markers.extend(l3) - #markers.sort(key=lambda x: (x[0], x[1])) - return markers - - # ========================================================================= - class __Region(): - """ - """ - - def __init__(self, x, y): - """ Initialize the region """ - self._pixels = [(x, y)] - self._min_x = x - self._max_x = x - self._min_y = y - self._max_y = y - self.area = 1 - - # --------------------------------------------------------------------- - def add(self, x, y): - """ Add a pixel to the region """ - self._pixels.append((x, y)) - self.area += 1 - self._min_x = min(self._min_x, x) - self._max_x = max(self._max_x, x) - self._min_y = min(self._min_y, y) - self._max_y = max(self._max_y, y) - - # --------------------------------------------------------------------- - def centroid(self): - """ Returns the centroid of the bounding box """ - return ((self._min_x + self._max_x) / 2, - (self._min_y + self._max_y) / 2) - - # --------------------------------------------------------------------- - def box(self): - """ Returns the bounding box of the region """ - return [ (self._min_x, self._min_y) , (self._max_x, self._max_y)] - - # --------------------------------------------------------------------- - def aspectratio(self): - """ Calculating the aspect ratio of the region """ - width = self._max_x - self._min_x - length = self._max_y - self._min_y - return float(width)/float(length) - -# END ========================================================================= diff --git a/modules/s3/s3validators.py b/modules/s3/s3validators.py index 0dee6295a6..67aaa8782f 100644 --- a/modules/s3/s3validators.py +++ b/modules/s3/s3validators.py @@ -1471,7 +1471,7 @@ def validate(self, value, record_id=None): # the update form. # NOTE: A FieldStorage with data evaluates as False (odd!) uploaded_image = post_vars.get(self.field_name) - if uploaded_image not in (b"", None): # Py 3.x it's b"", which is equivalent to "" in Py 2.x + if uploaded_image not in (b"", None): return uploaded_image cropped_image = post_vars.get("imagecrop-data") @@ -1485,9 +1485,9 @@ def validate(self, value, record_id=None): if cropped_image: import base64 - metadata, cropped_image = cropped_image.split(",") + metadata, cropped_image = cropped_image.rsplit(",", 1) #filename, datatype, enctype = metadata.split(";") - filename = metadata.split(";", 1)[0] + filename = metadata.rsplit(";", 2)[0] f = Storage() f.filename = uuid4().hex + filename diff --git a/modules/s3/s3widgets.py b/modules/s3/s3widgets.py index f8c8a3bd68..50e30d0a95 100644 --- a/modules/s3/s3widgets.py +++ b/modules/s3/s3widgets.py @@ -1406,8 +1406,13 @@ def create_person(self, data): # Onvalidation? (doesn't currently exist) + set_record_owner = current.auth.s3_set_record_owner + update_super = s3db.update_super + # Create new person record person_id = ptable.insert(**person) + person_record = {"id": person_id} + update_super(ptable, person_record) if not person_id: return (None, T("Could not add person record")) @@ -1421,18 +1426,16 @@ def create_person(self, data): human_resource_id = htable.insert(person_id = person_id, organisation_id = data_get("organisation_id"), ) - - # Update the super-entities - record = {"id": person_id} - s3db.update_super(ptable, record) + update_super(ptable, {"id": human_resource_id}) + set_record_owner(htable, human_resource_id) # Update ownership & realm - current.auth.s3_set_record_owner(ptable, person_id) + set_record_owner(ptable, person_id) # Onaccept? (not relevant for this case) # Read the created pe_id - pe_id = record.get("pe_id") + pe_id = person_record.get("pe_id") if not pe_id: return (None, T("Could not add person details")) @@ -9039,7 +9042,7 @@ def __call__(self, field, value, **attributes): def s3_comments_widget(field, value, **attr): """ A smaller-than-normal textarea - to be used by the s3.comments() & gis.desc_field Reusable fields + to be used by the s3_comments & gis.desc_field Reusable fields """ _id = attr.get("_id", "%s_%s" % (field._tablename, field.name)) diff --git a/modules/s3/s3xml.py b/modules/s3/s3xml.py index 08195a9107..d2f11c9581 100644 --- a/modules/s3/s3xml.py +++ b/modules/s3/s3xml.py @@ -1374,7 +1374,7 @@ def record(cls, continue if not filename: filename = download_url.split("?")[0] or "upload.bin" - if download_url.find("://"): + if not download_url.find("://"): # Not a full URL, try prepending protocol download_url = "http://%s" % download_url try: diff --git a/modules/s3cfg.py b/modules/s3cfg.py index 761f91dcbe..51f360fe79 100644 --- a/modules/s3cfg.py +++ b/modules/s3cfg.py @@ -2076,21 +2076,6 @@ def get_pdf_export_font(self): language = current.session.s3.language return self.__lazy("L10n", "pdf_export_font", self.fonts.get(language)) - def get_pdf_excluded_fields(self, resourcename): - """ - Optical Character Recognition (OCR) - """ - - excluded_fields = self.pdf.get("excluded_fields") - if excluded_fields is None: - excluded_fields = {"hms_hospital": ["hrm_human_resource", - ], - "pr_group": ["pr_group_membership", - ], - } - - return excluded_fields.get(resourcename, []) - def get_pdf_max_rows(self): """ Maximum number of records in a single PDF table/list export diff --git a/modules/s3db/hrm.py b/modules/s3db/hrm.py index c086d3ec1a..0c8babd63c 100644 --- a/modules/s3db/hrm.py +++ b/modules/s3db/hrm.py @@ -6505,7 +6505,11 @@ def hrm_human_resource_onaccept(form): if entity: auth.set_realm_entity(ptable, person, entity = entity, - force_update = True) + force_update = True, + ) + else: + # Always update the person record's realm + auth.set_realm_entity(ptable, person, force_update=True) tracker = S3Tracker() if person_id: diff --git a/modules/s3db/ocr.py b/modules/s3db/ocr.py deleted file mode 100644 index e699d1e7a8..0000000000 --- a/modules/s3db/ocr.py +++ /dev/null @@ -1,211 +0,0 @@ -# -*- coding: utf-8 -*- - -""" OCR Utility Functions - - @copyright: 2009-2021 (c) Sahana Software Foundation - @license: MIT - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. -""" - -__all__ = ("OCRDataModel", - "ocr_buttons", - ) - -import os - -from gluon import * -from gluon.storage import Storage -from ..s3 import * - -# ============================================================================= -class OCRDataModel(S3Model): - """ - """ - - names = ("ocr_meta", - "ocr_payload", - "ocr_form_status", - "ocr_field_crops", - "ocr_data_xml", - ) - - def model(self): - - #T = current.T - - #messages = current.messages - #UNKNOWN_OPT = messages.UNKNOWN_OPT - #NONE = messages["NONE"] - - define_table = self.define_table - - # Upload folders - folder = current.request.folder - metadata_folder = os.path.join(folder, "uploads", "ocr_meta") - payload_folder = os.path.join(folder, "uploads", "ocr_payload") - - # ===================================================================== - # OCR Meta Data - # - tablename = "ocr_meta" - define_table(tablename, - Field("form_uuid", - notnull=True, - length=128, - unique=True), - Field("resource_name", - notnull=True), - Field("s3ocrxml_file", "upload", - length = current.MAX_FILENAME_LENGTH, - uploadfolder = metadata_folder, - ), - Field("layout_file", "upload", - length = current.MAX_FILENAME_LENGTH, - uploadfolder = metadata_folder, - ), - Field("revision", - notnull=True, - length=128, - unique=True), - Field("pages", "integer"), - *s3_meta_fields()) - - #====================================================================== - # OCR Payload - # - tablename = "ocr_payload" - define_table(tablename, - # a set of images = one complete form - Field("image_set_uuid", - notnull=True), - Field("image_file", "upload", - length = current.MAX_FILENAME_LENGTH, - notnull = True, - uploadfolder = payload_folder, - ), - Field("page_number", "integer", - notnull=True), - *s3_meta_fields()) - - #====================================================================== - # OCR Form Status - # - tablename = "ocr_form_status" - define_table(tablename, - Field("image_set_uuid", - notnull=True, - length=128, - unique=True), - Field("form_uuid", - notnull=True), - Field("review_status", "integer", - notnull=True, - default=0), - Field("job_uuid", - length=128, - unique=True), - Field("job_has_errors", "integer"), - *s3_meta_fields()) - - #====================================================================== - # OCR Field Crops - # - tablename = "ocr_field_crops" - define_table(tablename, - Field("image_set_uuid", - notnull=True), - Field("resource_table", - notnull=True), - Field("field_name", - notnull=True), - Field("image_file", "upload", - length = current.MAX_FILENAME_LENGTH, - notnull = True, - uploadfolder = payload_folder, - ), - Field("value"), - Field("sequence", "integer"), - *s3_meta_fields()) - - #====================================================================== - # OCR XML Data - # - tablename = "ocr_data_xml" - define_table(tablename, - Field("image_set_uuid", - length=128, - unique=True, - notnull=True), - Field("data_file", "upload", - length = current.MAX_FILENAME_LENGTH, - notnull = True, - uploadfolder = payload_folder, - ), - Field("form_uuid", - notnull=True, - default=""), - *s3_meta_fields()) - -# ============================================================================= -def ocr_buttons(r): - """ Generate 'Print PDF' button in the view """ - - if not current.deployment_settings.has_module("ocr"): - return "" - - if r.component: - urlargs = [r.id, r.component_name] - - else: - urlargs = [] - - f = r.function - c = r.controller - a = r.application - - T = current.T - UPLOAD = T("Upload Scanned OCR Form") - DOWNLOAD = T("Download OCR-able PDF Form") - - _style = "height:10px;float:right;padding:3px;" - - output = DIV( - - A(IMG(_src="/%s/static/img/upload-ocr.png" % a, _alt=UPLOAD), - _id = "upload-pdf-btn", - _href = URL(c=c, f=f, args=urlargs + ["import.pdf"]), - _title = UPLOAD, - _style = _style, - ), - - A(IMG(_src="/%s/static/img/download-ocr.png" % a, _alt=DOWNLOAD), - _id = "download-pdf-btn", - _href = URL(c=c, f=f, args=urlargs + ["create.pdf"]), - _title = DOWNLOAD, - _style = _style, - ), - - ) - return output - -# END ========================================================================= diff --git a/modules/s3log.py b/modules/s3log.py index 9f2868e177..0643db311b 100644 --- a/modules/s3log.py +++ b/modules/s3log.py @@ -103,10 +103,7 @@ def setup(cls): Set up current.log """ - if hasattr(current, "log"): - return current.log = cls() - return # ------------------------------------------------------------------------- def configure_logger(self): @@ -114,9 +111,6 @@ def configure_logger(self): Configure output handlers """ - if hasattr(current, "log"): - return - settings = current.deployment_settings console = settings.get_log_console() logfile = settings.get_log_logfile() @@ -198,7 +192,6 @@ def _log(severity, message, value=None): extra = {"caller": "(%s %s %s)" % caller} logger.log(severity, msg, extra=extra) - return # ------------------------------------------------------------------------- @classmethod @@ -310,7 +303,6 @@ def listen(self): self.handler = handler self.strbuf = strbuf - return # ------------------------------------------------------------------------- def read(self): diff --git a/modules/templates/CCC/views/cms_post_list_filter.html b/modules/templates/CCC/views/cms_post_list_filter.html index 4d2684711d..f21b539918 100644 --- a/modules/templates/CCC/views/cms_post_list_filter.html +++ b/modules/templates/CCC/views/cms_post_list_filter.html @@ -2,7 +2,6 @@
{{try:}}{{if title:}}{{=H2(title)}}{{pass}}{{except:}}{{pass}} - {{#try:=s3db.ocr_buttons(r)except:pass}}
{{try:}}{{rheader=rheader}} diff --git a/modules/templates/RLP/views/delegation.html b/modules/templates/RLP/views/delegation.html index 25c9990fc7..c83c6ddcc8 100644 --- a/modules/templates/RLP/views/delegation.html +++ b/modules/templates/RLP/views/delegation.html @@ -2,7 +2,6 @@
{{try:}}{{if title:}}{{=H2(title)}}{{pass}}{{except:}}{{pass}} - {{#try:=s3db.ocr_buttons(r)except:pass}}
{{try:}}{{rheader=rheader}} diff --git a/modules/templates/RLPPTM/auth_roles.csv b/modules/templates/RLPPTM/auth_roles.csv index 06e5286dd3..9c71319f08 100644 --- a/modules/templates/RLPPTM/auth_roles.csv +++ b/modules/templates/RLPPTM/auth_roles.csv @@ -45,11 +45,13 @@ ORG_ADMIN,Organisation Admin,,,inv,recv,,READ|UPDATE,, ORG_ADMIN,Organisation Admin,,,inv,recv_process,,READ|UPDATE,, ORG_ADMIN,Organisation Admin,,,inv,send,,NONE,, ORG_ADMIN,Organisation Admin,,,org,organisation,,CREATE|READ|UPDATE|DELETE,, +ORG_ADMIN,Organisation Admin,,,org,,,READ,, ORG_ADMIN,Organisation Admin,,,req,req,,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,req,,,READ,, ORG_ADMIN,Organisation Admin,,,supply,item,,NONE,, ORG_ADMIN,Organisation Admin,,,supply,item_pack,,READ,, ORG_ADMIN,Organisation Admin,,,supply,,,NONE,, +ORG_ADMIN,Organisation Admin,,,,,gis_location,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,hrm_human_resource,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,inv_recv,READ|UPDATE,, ORG_ADMIN,Organisation Admin,,,,,inv_send,READ,, @@ -58,6 +60,7 @@ ORG_ADMIN,Organisation Admin,,,,,org_facility,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,org_office,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,org_organisation,READ|UPDATE,, ORG_ADMIN,Organisation Admin,,,,,org_service_site,CREATE|READ|UPDATE|DELETE,, +ORG_ADMIN,Organisation Admin,,,,,pr_address,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,pr_contact,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,pr_contact_emergency,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,pr_person,CREATE|READ|UPDATE|DELETE,, @@ -66,6 +69,8 @@ ORG_ADMIN,Organisation Admin,,,,,req_req,CREATE|READ|UPDATE|DELETE,, ORG_GROUP_ADMIN,Organisation Group Admin,,false,fin,,,READ,, ORG_GROUP_ADMIN,Organisation Group Admin,,,hrm,person,,CREATE|READ|UPDATE|DELETE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,hrm,staff,,CREATE|READ|UPDATE|DELETE,, +ORG_GROUP_ADMIN,Organisation Group Admin,,,hrm,compose,,CREATE|READ|UPDATE|DELETE,, +ORG_GROUP_ADMIN,Organisation Group Admin,,,msg,compose,,CREATE|READ|UPDATE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,org,facility,,READ|UPDATE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,org,organisation,,CREATE|READ|UPDATE|DELETE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,org,,,CREATE|READ|UPDATE|DELETE,, diff --git a/modules/templates/RLPPTM/config.py b/modules/templates/RLPPTM/config.py index 10fbcdd741..00bc960664 100644 --- a/modules/templates/RLPPTM/config.py +++ b/modules/templates/RLPPTM/config.py @@ -26,8 +26,6 @@ ISSUER_ORG_TYPE = "pe_id$pe_id:org_organisation.org_organisation_organisation_type.organisation_type_id" -ALLOWED_FORMATS = ("html", "iframe", "popup", "aadata", "json") - # ============================================================================= def config(settings): @@ -159,10 +157,11 @@ def config(settings): settings.pr.availability_json_rules = True # ------------------------------------------------------------------------- - settings.hrm.record_tab = False + settings.hrm.record_tab = True settings.hrm.staff_experience = False + settings.hrm.staff_departments = False settings.hrm.teams = False - settings.hrm.use_address = False + settings.hrm.use_address = True settings.hrm.use_id = False settings.hrm.use_skills = False settings.hrm.use_certificates = False @@ -244,14 +243,25 @@ def rlpptm_realm_entity(table, row): # # a OU hierarchy (default ok) # realm_entity = 0 # - #elif tablename == "pr_person": - # - # # Persons are owned by the org employing them (default ok) - # realm_entity = 0 - # - if tablename in ("disease_case_diagnostics", - "disease_testing_report", - ): + if tablename == "pr_person": + + # Human resources belong to their org's realm + htable = s3db.hrm_human_resource + otable = s3db.org_organisation + + left = otable.on(otable.id == htable.organisation_id) + query = (htable.person_id == row.id) & \ + (htable.deleted == False) + org = db(query).select(otable.pe_id, + left = left, + limitby = (0, 1), + ).first() + if org: + realm_entity = org.pe_id + + elif tablename in ("disease_case_diagnostics", + "disease_testing_report", + ): # Test results / daily reports inherit realm-entity # from the testing site table = s3db.table(tablename) @@ -356,6 +366,36 @@ def rlpptm_realm_entity(table, row): if program: realm_entity = program.realm_entity + elif tablename in ("pr_person_details", + ): + + # Inherit from person via person_id + table = s3db.table(tablename) + ptable = s3db.pr_person + query = (table._id == row.id) & \ + (ptable.id == table.person_id) + person = db(query).select(ptable.realm_entity, + limitby = (0, 1), + ).first() + if person: + realm_entity = person.realm_entity + + elif tablename in ("pr_address", + "pr_contact", + "pr_contact_emergency", + ): + + # Inherit from person via PE + table = s3db.table(tablename) + ptable = s3db.pr_person + query = (table._id == row.id) & \ + (ptable.pe_id == table.pe_id) + person = db(query).select(ptable.realm_entity, + limitby = (0, 1), + ).first() + if person: + realm_entity = person.realm_entity + elif tablename in ("inv_send", "inv_recv"): # Shipments inherit realm-entity from the sending/receiving site table = s3db.table(tablename) @@ -1136,6 +1176,7 @@ def prep(r): # Restrict data formats settings.ui.export_formats = None representation = r.representation + ALLOWED_FORMATS = ("html", "iframe", "popup", "aadata", "json") if representation not in ALLOWED_FORMATS and \ not(r.record and representation == "card"): r.error(403, current.ERROR.NOT_PERMITTED) @@ -2164,6 +2205,117 @@ def custom_postp(r, output): settings.customise_fin_voucher_invoice_controller = customise_fin_voucher_invoice_controller + # ------------------------------------------------------------------------- + def human_resource_onvalidation(form): + + db = current.db + s3db = current.s3db + + form_vars = form.vars + + person_id = form_vars.get("person_id") + if person_id: + table = s3db.hrm_human_resource + query = (table.person_id == person_id) & \ + (table.deleted == False) + duplicate = db(query).select(table.id, + limitby = (0, 1), + ).first() + if duplicate: + form.errors.person_id = current.T("Person already has a staff record") + + # ------------------------------------------------------------------------- + def customise_hrm_human_resource_resource(r, tablename): + + s3db = current.s3db + + s3db.add_custom_callback("hrm_human_resource", + "onvalidation", + human_resource_onvalidation, + ) + + settings.customise_hrm_human_resource_resource = customise_hrm_human_resource_resource + + # ------------------------------------------------------------------------- + def customise_hrm_human_resource_controller(**attr): + + s3db = current.s3db + + s3 = current.response.s3 + + # Custom prep + standard_prep = s3.prep + def prep(r): + # Restrict data formats + from .helpers import restrict_data_formats + restrict_data_formats(r) + + # Call standard prep + result = standard_prep(r) if callable(standard_prep) else True + + resource = r.resource + + is_org_group_admin = current.auth.s3_has_role("ORG_GROUP_ADMIN") + + # Configure components to inherit realm_entity from person + s3db.configure("pr_person", + realm_components = ("person_details", + "contact", + "address", + ), + ) + + phone_label = settings.get_ui_label_mobile_phone() + list_fields = ["organisation_id", + "person_id", + "job_title_id", + "site_id", + (T("Email"), "person_id$email.value"), + (phone_label, "person_id$phone.value"), + "status", + ] + + from s3 import S3OptionsFilter, S3TextFilter, s3_get_filter_opts + filter_widgets = [ + S3TextFilter(["person_id$first_name", + "person_id$last_name", + "organisation_id$name", + "person_id$email.value", + "person_id$phone.value", + ], + label = T("Search"), + ), + S3OptionsFilter("job_title_id", + options = lambda: s3_get_filter_opts("hrm_job_title"), + hidden = True, + ), + ] + if is_org_group_admin: + filter_widgets[1:1] = [ + S3OptionsFilter( + "organisation_id$group__link.group_id", + label = T("Organization Group"), + options = lambda: s3_get_filter_opts("org_group"), + ), + S3OptionsFilter( + "organisation_id$organisation_type__link.organisation_type_id", + label = T("Organization Type"), + options = lambda: s3_get_filter_opts("org_organisation_type"), + hidden = True, + ), + ] + + resource.configure(filter_widgets = filter_widgets, + list_fields = list_fields, + ) + + return result + s3.prep = prep + + return attr + + settings.customise_hrm_human_resource_controller = customise_hrm_human_resource_controller + # ------------------------------------------------------------------------- def add_org_tags(): """ @@ -2423,6 +2575,21 @@ def prep(r): field = ctable.obsolete field.readable = field.writable = True + elif r.component_name == "human_resource": + + phone_label = settings.get_ui_label_mobile_phone() + list_fields = ["organisation_id", + "person_id", + "job_title_id", + "site_id", + (T("Email"), "person_id$email.value"), + (phone_label, "person_id$phone.value"), + "status", + ] + + r.component.configure(list_fields = list_fields, + ) + return result s3.prep = prep @@ -3015,12 +3182,8 @@ def customise_org_facility_controller(**attr): def prep(r): # Restrict data formats - allowed = ("html", "iframe", "popup", "aadata", "plain", "geojson", "pdf", "xls") - if r.method == "info": - allowed += ("json", ) - settings.ui.export_formats = ("pdf", "xls") - if r.representation not in allowed: - r.error(403, current.ERROR.NOT_PERMITTED) + from .helpers import restrict_data_formats + restrict_data_formats(r) # Call standard prep result = standard_prep(r) if callable(standard_prep) else True @@ -3256,6 +3419,21 @@ def prep(r): settings.customise_project_project_controller = customise_project_project_controller + # ------------------------------------------------------------------------- + def customise_pr_person_resource(r, tablename): + + s3db = current.s3db + + # Configure components to inherit realm_entity from person + s3db.configure("pr_person", + realm_components = ("person_details", + "contact", + "address", + ), + ) + + settings.customise_pr_person_resource = customise_pr_person_resource + # ------------------------------------------------------------------------- def customise_pr_person_controller(**attr): @@ -3264,6 +3442,10 @@ def customise_pr_person_controller(**attr): # Custom prep standard_prep = s3.prep def prep(r): + # Restrict data formats + from .helpers import restrict_data_formats + restrict_data_formats(r) + # Call standard prep result = standard_prep(r) if callable(standard_prep) else True @@ -3275,26 +3457,58 @@ def prep(r): keys = StringTemplateParser.keys(settings.get_pr_name_format()) name_fields = [fn for fn in keys if fn in NAMES] - if r.controller == "default": - # Personal profile (default/person) - if not r.component: + if r.controller in ("default", "hrm") and not r.component: + # Personal profile (default/person) or staff - # Last name is required - table = r.resource.table - table.last_name.requires = IS_NOT_EMPTY() + # Last name is required + table = r.resource.table + table.last_name.requires = IS_NOT_EMPTY() + + # Custom Form + crud_fields = name_fields + ["date_of_birth", + "gender", + ] + r.resource.configure(crud_form = S3SQLCustomForm(*crud_fields), + deletable = False, + ) + + if r.component_name == "address": + ctable = r.component.table + + # Configure location selector and geocoder + from s3 import S3LocationSelector + field = ctable.location_id + field.widget = S3LocationSelector(levels = ("L1", "L2", "L3", "L4"), + required_levels = ("L1", "L2", "L3"), + show_address = True, + show_postcode = True, + show_map = True, + ) + s3.scripts.append("/%s/static/themes/RLP/js/geocoderPlugin.js" % r.application) + + elif r.component_name == "human_resource": + + phone_label = settings.get_ui_label_mobile_phone() + r.component.configure(list_fields= ["job_title_id", + "site_id", + (T("Email"), "person_id$email.value"), + (phone_label, "person_id$phone.value"), + "status", + ], + deletable = False, + ) + s3.crud_strings["hrm_human_resource"]["label_list_button"] = T("List Staff Records") - # Custom Form - crud_fields = name_fields - r.resource.configure(crud_form = S3SQLCustomForm(*crud_fields), - deletable = False, - ) return result s3.prep = prep # Custom rheader - if current.request.controller == "default": - from .rheaders import rlpptm_profile_rheader + from .rheaders import rlpptm_profile_rheader, rlpptm_hr_rheader + controller = current.request.controller + if controller == "default": attr["rheader"] = rlpptm_profile_rheader + elif controller == "hrm": + attr["rheader"] = rlpptm_hr_rheader return attr diff --git a/modules/templates/RLPPTM/helpers.py b/modules/templates/RLPPTM/helpers.py index ede794dc2a..345187607d 100644 --- a/modules/templates/RLPPTM/helpers.py +++ b/modules/templates/RLPPTM/helpers.py @@ -267,6 +267,26 @@ def get_role_hrs(role_uid, pe_id=None, organisation_id=None): return hr_ids if hr_ids else None +# ----------------------------------------------------------------------------- +def restrict_data_formats(r): + """ + Restrict data exports (prevent S3XML/S3JSON of records) + + @param r: the S3Request + """ + + settings = current.deployment_settings + allowed = ("html", "iframe", "popup", "aadata", "plain", "geojson", "pdf", "xls") + if r.record: + allowed += ("card",) + if r.method in ("report", "timeplot", "filter", "lookup"): + allowed += ("json",) + elif r.method == "options": + allowed += ("s3json",) + settings.ui.export_formats = ("pdf", "xls") + if r.representation not in allowed: + r.error(403, current.ERROR.NOT_PERMITTED) + # ----------------------------------------------------------------------------- def assign_pending_invoices(billing_id, organisation_id=None, invoice_id=None): """ diff --git a/modules/templates/RLPPTM/hrm_job_title.csv b/modules/templates/RLPPTM/hrm_job_title.csv new file mode 100644 index 0000000000..7277bbd9fd --- /dev/null +++ b/modules/templates/RLPPTM/hrm_job_title.csv @@ -0,0 +1,4 @@ +Name,Comments +Leitung, +Mitarbeiter (hauptamtlich), +Mitarbeiter (ehrenamtlich), diff --git a/modules/templates/RLPPTM/menus.py b/modules/templates/RLPPTM/menus.py index f8972901ea..307e2dac9d 100644 --- a/modules/templates/RLPPTM/menus.py +++ b/modules/templates/RLPPTM/menus.py @@ -355,29 +355,31 @@ def org(): M("Create Organization", m="create", restrict="ORG_GROUP_ADMIN"), ) - return M(c="org")( + return M(c=("org", "hrm"))( org_menu, - M("Facilities", f="facility", link=False)( + M("Facilities", f="facility", link=False, restrict="ORG_GROUP_ADMIN")( M("Test Stations to review", vars = {"$$review": "1"}, - restrict = "ORG_GROUP_ADMIN", ), M("Unapproved Test Stations", vars = {"$$pending": "1"}, - restrict = "ORG_GROUP_ADMIN", ), M("Public Registry", m="summary"), ), - M("Statistics", link=False)( + M("Statistics", link=False, restrict="ORG_GROUP_ADMIN")( M("Organizations", f="organisation", m="report"), M("Facilities", f="facility", m="report"), ), + M("Staff", c="hrm", f=("staff", "person"), + restrict=("ORG_ADMIN", "ORG_GROUP_ADMIN"), + ), M("Administration", restrict=("ADMIN"))( M("Facility Types", f="facility_type"), M("Organization Types", f="organisation_type"), M("Services", f="service"), M("Service Modes", f="service_mode"), M("Booking Modes", f="booking_mode"), + M("Job Titles", c="hrm", f="job_title"), ), ) diff --git a/modules/templates/RLPPTM/rheaders.py b/modules/templates/RLPPTM/rheaders.py index 3087ef7662..2a36345165 100644 --- a/modules/templates/RLPPTM/rheaders.py +++ b/modules/templates/RLPPTM/rheaders.py @@ -8,7 +8,7 @@ from gluon import current, A, URL -from s3 import S3ResourceHeader, s3_rheader_resource +from s3 import S3ResourceHeader, s3_fullname, s3_rheader_resource # ============================================================================= def rlpptm_fin_rheader(r, tabs=None): @@ -507,4 +507,40 @@ def rlpptm_profile_rheader(r, tabs=None): ) return rheader +# ============================================================================= +def rlpptm_hr_rheader(r, tabs=None): + """ Custom rheader for hrm/person """ + + if r.representation != "html": + # Resource headers only used in interactive views + return None + + tablename, record = s3_rheader_resource(r) + if tablename != r.tablename: + resource = current.s3db.resource(tablename, id=record.id) + else: + resource = r.resource + + rheader = None + rheader_fields = [] + + if record: + + T = current.T + + if tablename == "pr_person": + + tabs = [(T("Person Details"), None), + (T("Contact Information"), "contacts"), + (T("Address"), "address"), + (T("Staff Record"), "human_resource"), + ] + rheader_fields = [] + rheader_title = s3_fullname + + rheader = S3ResourceHeader(rheader_fields, tabs, title=rheader_title) + rheader = rheader(r, table=resource.table, record=record) + + return rheader + # END ========================================================================= diff --git a/modules/templates/RLPPTM/tasks.cfg b/modules/templates/RLPPTM/tasks.cfg index cb8c722ad1..1185be13f9 100644 --- a/modules/templates/RLPPTM/tasks.cfg +++ b/modules/templates/RLPPTM/tasks.cfg @@ -57,6 +57,9 @@ org,facility_type,org_facility_type.csv,facility_type.xsl org,service_mode,org_service_mode.csv,service_mode.xsl org,booking_mode,org_booking_mode.csv,booking_mode.xsl # ----------------------------------------------------------------------------- +# HRM +hrm,job_title,hrm_job_title.csv,job_title.xsl +# ----------------------------------------------------------------------------- # Warehouse Types and Default Warehouse inv,warehouse_type,inv_warehouse_type.csv,warehouse_type.xsl inv,warehouse,inv_warehouse.csv,warehouse.xsl diff --git a/modules/templates/default/Demo/config.py b/modules/templates/default/Demo/config.py index d883b268d6..5519b6021d 100644 --- a/modules/templates/default/Demo/config.py +++ b/modules/templates/default/Demo/config.py @@ -407,12 +407,6 @@ def config(settings): # restricted = True, # module_type = None, #)), - #("ocr", Storage( - # name_nice = T("Optical Character Recognition"), - # #description = "Optical Character Recognition for reading the scanned handwritten paper forms.", - # restricted = False, - # module_type = None, - #)), #("work", Storage( # name_nice = T("Jobs"), # #description = "Simple Volunteer Jobs Management", diff --git a/modules/templates/default/config.py b/modules/templates/default/config.py index e63dae5baa..dca5bc6dc6 100644 --- a/modules/templates/default/config.py +++ b/modules/templates/default/config.py @@ -1582,12 +1582,6 @@ def config(settings): # #description = "Used by Assess", # module_type = None, #)), - #("ocr", Storage( - # name_nice = T("Optical Character Recognition"), - # #description = "Optical Character Recognition for reading the scanned handwritten paper forms.", - # restricted = False, - # module_type = None, - #)), ]) # END ========================================================================= diff --git a/static/scripts/S3/S3.js b/static/scripts/S3/S3.js index 7b4b723635..7c0495c3c0 100644 --- a/static/scripts/S3/S3.js +++ b/static/scripts/S3/S3.js @@ -1,2385 +1,2385 @@ -/** - * Custom Javascript functions added as part of the S3 Framework - * Strings are localised in views/l10n.js - */ - - /** - * The startsWith string function is introduced in JS 1.8.6 -- it's not even - * accepted in ECMAScript yet, so don't expect all browsers to have it. - * Thx to http://www.moreofless.co.uk/javascript-string-startswith-endswith/ - * for showing how to add it to string if not present. - */ -if (typeof String.prototype.startsWith != 'function') { - String.prototype.startsWith = function(str) { - return this.substring(0, str.length) === str; - }; -} - -// Global variable to store all of our variables inside -var S3 = {}; -S3.gis = {}; -S3.gis.options = {}; -S3.timeline = {}; -S3.JSONRequest = {}; // Used to store and abort JSON requests -//S3.TimeoutVar = {}; // Used to store and abort JSON requests - -S3.queryString = { - // From https://github.com/sindresorhus/query-string - parse: function(str) { - if (typeof str !== 'string') { - return {}; - } - - str = str.trim().replace(/^(\?|#)/, ''); - - if (!str) { - return {}; - } - - return str.trim().split('&').reduce(function (ret, param) { - var parts = param.replace(/\+/g, ' ').split('='); - var key = parts[0]; - var val = parts[1]; - - key = decodeURIComponent(key); - // missing `=` should be `null`: - // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters - val = val === undefined ? null : decodeURIComponent(val); - - if (!ret.hasOwnProperty(key)) { - ret[key] = val; - } else if (Array.isArray(ret[key])) { - ret[key].push(val); - } else { - ret[key] = [ret[key], val]; - } - - return ret; - }, {}); - }, - stringify: function(obj) { - return obj ? Object.keys(obj).map(function (key) { - var val = obj[key]; - - if (Array.isArray(val)) { - return val.map(function (val2) { - return encodeURIComponent(key) + '=' + encodeURIComponent(val2); - }).join('&'); - } - - return encodeURIComponent(key) + '=' + encodeURIComponent(val); - }).join('&') : ''; - } -}; - -S3.uid = function() { - // Generate a random uid - // Used for jQueryUI modals and some Map popups - // http://jsperf.com/random-uuid/2 - return (((+(new Date())) / 1000 * 0x10000 + Math.random() * 0xffff) >> 0).toString(16); -}; - -S3.Utf8 = { - // Used by dataTables - // http://www.webtoolkit.info - encode: function(string) { - string = string.replace(/\r\n/g, '\n'); - var utftext = ''; - for (var n = 0; n < string.length; n++) { - var c = string.charCodeAt(n); - if (c < 128) { - utftext += String.fromCharCode(c); - } else if ((c > 127) && (c < 2048)) { - utftext += String.fromCharCode((c >> 6) | 192); - utftext += String.fromCharCode((c & 63) | 128); - } else { - utftext += String.fromCharCode((c >> 12) | 224); - utftext += String.fromCharCode(((c >> 6) & 63) | 128); - utftext += String.fromCharCode((c & 63) | 128); - } - } - return utftext; - }, - decode: function(utftext) { - var string = '', - i = 0, - c = 0, - c1 = 0, - c2 = 0; - while ( i < utftext.length ) { - c = utftext.charCodeAt(i); - if (c < 128) { - string += String.fromCharCode(c); - i++; - } else if ((c > 191) && (c < 224)) { - c1 = utftext.charCodeAt(i+1); - string += String.fromCharCode(((c & 31) << 6) | (c1 & 63)); - i += 2; - } else { - c1 = utftext.charCodeAt(i+1); - c2 = utftext.charCodeAt(i+2); - string += String.fromCharCode(((c & 15) << 12) | ((c1 & 63) << 6) | (c2 & 63)); - i += 3; - } - } - return string; - } -}; - -S3.addTooltips = function() { - // Help Tooltips - $.cluetip.defaults.cluezIndex = 9999; // Need to be able to show on top of Ext Windows - $('.tooltip').cluetip({activation: 'hover', sticky: false, splitTitle: '|'}); - $('label[title][title!=""]').cluetip({splitTitle: '|', showTitle:false}); - $('.tooltipbody').cluetip({activation: 'hover', sticky: false, splitTitle: '|', showTitle: false}); - var tipCloseText = 'close'; - $('.stickytip').cluetip({ - activation: 'hover', - sticky: true, - closePosition: 'title', - closeText: tipCloseText, - splitTitle: '|' - }); - $('.errortip').cluetip({ - activation: 'click', - sticky: true, - closePosition: 'title', - closeText: tipCloseText, - splitTitle: '|' - }); - $('.ajaxtip').cluetip({ - activation: 'click', - sticky: true, - closePosition: 'title', - closeText: tipCloseText, - width: 380 - }); - $('.htmltip').cluetip({ - activation: 'hover', - sticky: false, - local: true, - attribute: 'show', - width: 380 - }); -}; - -// jQueryUI Modal Popups -S3.addModals = function() { - $('a.s3_add_resource_link').attr('href', function(index, attr) { - var url_out = attr; - // Avoid Duplicate callers - if (attr.indexOf('caller=') == -1) { - // Add the caller to the URL vars so that the popup knows which field to refresh/set - // Default formstyle - var caller = $(this).parents('tr').attr('id'); - if (!caller) { - // DIV-based formstyle - caller = $(this).parent().parent().attr('id'); - } - if (!caller) { - caller = $(this).parents('.form-row').attr('id'); - } - if (!caller) { - // Bootstrap formstyle - caller = $(this).parents('.control-group').attr('id'); - } - if (caller) { - caller = caller.replace(/__row_comment/, '') // DRRPP formstyle - .replace(/__row/, ''); - url_out = attr + '&caller=' + caller; - } - } - return url_out; - }); - $('.s3_add_resource_link, .s3_modal').off('.S3Modal') - .on('click.S3Modal', function() { - var title = this.title; - var url = this.href; - var i = url.indexOf('caller='); - if (i != -1) { - var caller = url.slice(i + 7); - i = caller.indexOf('&'); - if (i != -1) { - caller = caller.slice(0, i); - } - var select = $('#' + caller); - if (select.hasClass('multiselect-widget')) { - // Close the menu (otherwise this shows above the popup) - select.multiselect('close'); - // Lower the z-Index - //select.css('z-index', 1049); - } - } - - // Create an iframe - var id = S3.uid(), - dialog = $(''; - var closelink = $('' + i18n.close_map + ''); - - // @ToDo: Also make the represent link act as a close - closelink.bind('click', function(evt) { - $('#map').html(oldhtml); - evt.preventDefault(); - }); - - $('#map').html(iframe); - $('#map').append($("
").append(closelink)); -}; -S3.popupWin = null; -S3.openPopup = function(url, center) { - if ( !S3.popupWin || S3.popupWin.closed ) { - var params = 'width=640, height=480'; - if (center === true) { - params += ',left=' + (screen.width - 640)/2 + - ',top=' + (screen.height - 480)/2; - } - S3.popupWin = window.open(url, 'popupWin', params); - } else { - S3.popupWin.focus(); - } -}; - -// ============================================================================ -/** - * Filter options of a drop-down field (=target) by the selection made - * in another field (=trigger), e.g.: - * - Task Form: Activity options filtered by Project selection - * - * @todo: fix updateAddResourceLink - * @todo: move into separate file and load only when needed? - */ - -(function() { - - /** - * Get a CSS selector for trigger/target fields - * - * @param {string|object} setting - the setting for trigger/target, value of the - * name-attribute in regular form fields, or an - * object describing a field in an inline component - * (see below for the latter) - * @param {string} setting.prefix - the inline form prefix (default: 'default') - * @param {string} setting.alias - the component alias for the inline form (e.g. task_project) - * @param {string} setting.name - the field name - * @param {string} setting.inlineType - the inline form type, 'link' (for S3SQLInlineLink), - * or 'sub' (for other S3SQLInlineComponent types) - * @param {string} setting.inlineRows - the inline form has multiple rows, default: true - */ - var getSelector = function(setting) { - - var selector; - if (typeof setting == 'string') { - // Simple field name - selector = '[name="' + setting + '"]'; - } else { - // Inline form - var prefix = setting.prefix ? setting.prefix : 'default'; - if (setting.alias) { - prefix += setting.alias; - } - var type = setting.inlineType || 'sub', - rows = setting.inlineRows; - if (rows === undefined) { - rows = true; - } - if (type == 'sub') { - var name = setting.name; - if (rows) { - selector = '[name^="' + prefix + '"][name*="_i_' + name + '_edit_"]'; - } else { - selector = '[name="' + prefix + '-' + name + '"]'; - } - } else if (type == 'link') { - selector = '[name="link_' + prefix + '"]'; - } - } - return selector; - }; - - - /** - * Add a throbber while loading the widget options - * - * @param {object} widget - the target widget - * @param {string} resourceName - the target resource name - */ - var addThrobber = function(widget, resourceName) { - - var throbber = widget.siblings('.' + resourceName + '-throbber'); - if (!throbber.length) { - widget.after('
'); - } - }; - - /** - * Remove a previously inserted throbber - * - * @param {string} resourceName - the target resource name - */ - var removeThrobber = function(widget, resourceName) { - - var throbber = widget.siblings('.' + resourceName + '-throbber'); - if (throbber.length) { - throbber.remove(); - } - }; - - /** - * Update the AddResourceLink for the target with lookup key and - * value, so that the popup can pre-populate them; or hide the - * AddResourceLink if no trigger value has been selected - * - * @param {string} resourceName - the target resource name - * @param {string} key - the lookup key - * @param {string} value - the selected trigger value - */ - var updateAddResourceLink = function(resourceName, key, value) { - - $('a#' + resourceName + '_add').each(function() { - var search = this.search, - queries = [], - selectable = false; - if (search) { - var items = search.substring(1).split('&'); - items.forEach(function(item) { - if (decodeURIComponent(item.split('=')[0]) != key) { - queries.push(item); - } - }); - } - if (value !== undefined && value !== null) { - var query = encodeURIComponent(key) + '=' + encodeURIComponent(value); - queries.push(query); - selectable = true; - } - if (queries.length) { - search = '?' + queries.join('&'); - } else { - search = ''; - } - var href = this.protocol + '//' + this.host + this.pathname + search + this.hash, - $this = $(this).attr('href', href); - if (selectable) { - $this.parent().show(); - } else { - $this.parent().hide(); - } - }); - }; - - /** - * Render the options from the JSON data - * - * @param {array} data - the JSON data - * @param {object} settings - the settings - */ - var renderOptions = function(data, settings) { - - var options = [], - defaultValue = 0; - - if (data.length === 0) { - // No options available - var showEmptyField = settings.showEmptyField; - if (showEmptyField || showEmptyField === undefined) { - var msgNoRecords = settings.msgNoRecords || '-'; - options.push(''); - } - } else { - - // Pre-process the data - var fncPrep = settings.fncPrep, - prepResult = null; - if (fncPrep) { - try { - prepResult = fncPrep(data); - } catch (e) {} - } - - // Render the options - var lookupField = settings.lookupField || 'id', - fncRepresent = settings.fncRepresent, - record, - value, - name, - title; - - for (var i = 0; i < data.length; i++) { - record = data[i]; - value = record[lookupField]; - if (i === 0) { - defaultValue = value; - } - name = fncRepresent ? fncRepresent(record, prepResult) : record.name; - // Does the option have an onhover-tooltip? - if (record._tooltip) { - title = ' title="' + record._tooltip + '"'; - } else { - title = ''; - } - options.push('' + name + ''); - } - if (settings.optional) { - // Add (and default to) empty option - defaultValue = 0; - options.unshift(''); - } - } - return {options: options.join(''), defaultValue: defaultValue}; - }; - - /** - * Update the options of the target field from JSON data - * - * @param {jQuery} widget - the widget - * @param {object} data - the data as rendered by renderOptions - * @param {bool} empty - no options available (don't bother retaining - * the current value) - */ - var updateOptions = function(widget, data, empty) { - - // Catch unsupported widget type - if (widget.hasClass('checkboxes-widget-s3')) { - s3_debug('filterOptionsS3 error: checkboxes-widget-s3 not supported, updateOptions aborted'); - return; - } - - var options = data.options, - newValue = data.defaultValue, - selectedValues = []; - - // Get the current value of the target field - if (!empty) { - var currentValue = ''; - if (widget.hasClass('checkboxes-widget-s3')) { - // Checkboxes-widget-s3 target, not currently supported - //currentValue = new Array(); - //widget.find('input:checked').each(function() { - // currentValue.push($(this).val()); - //}); - return; - } else { - // SELECT-based target (Select, MultiSelect, GroupedOpts) - currentValue = widget.val(); - if (!currentValue) { - // Options list not populated yet? - currentValue = widget.prop('value'); - } - if (!$.isArray(currentValue)) { - currentValue = [currentValue]; - } - for (var i = 0, len = currentValue.length, val; i < len; i++) { - val = currentValue[i]; - if (val && $(options).filter('option[value=' + val + ']').length) { - selectedValues.push(val); - } - } - } - if (selectedValues.length) { - // Retain selected value - newValue = selectedValues; - } - } - - // Convert IS_ONE_OF_EMPTY into a ').addClass(widget.attr('class')) - .attr('id', widget.attr('id')) - .attr('name', widget.attr('name')) - .data('visible', widget.data('visible')) - .hide(); - widget.replaceWith(select); - widget = select; - } - - // Update the target field options - var disable = options === ''; - widget.html(options) - .val(newValue) - .change() - .prop('disabled', disable); - - // Refresh groupedopts or multiselect - if (widget.hasClass('groupedopts-widget')) { - widget.groupedopts('refresh'); - } else if (widget.hasClass('multiselect-widget')) { - widget.multiselect('refresh'); - // Disabled-attribute not reflected by refresh (?) - if (!disable) { - widget.multiselect('enable'); - } else { - widget.multiselect('disable'); - } - } - return widget; - }; - - /** - * Replace the widget HTML with the data returned by Ajax request - * - * @param {jQuery} widget: the widget - * @param {string} data: the HTML data - */ - var replaceWidgetHTML = function(widget, data) { - - if (data !== '') { - - // Do we have a groupedopts or multiselect widget? - var is_groupedopts = false, - is_multiselect = false; - if (widget.hasClass('groupedopts-widget')) { - is_groupedopts = true; - } else if (widget.hasClass('multiselect-widget')) { - is_multiselect = true; - } - - // Store selected value before replacing the widget HTML - var widgetValue; - if (is_groupedopts || is_multiselect) { - if (widget.prop('tagName').toLowerCase() == 'select') { - widgetValue = widget.val(); - } - } - - // Replace the widget with the HTML returned - widget.html(data) - .change() - .prop('disabled', false); - - // Restore selected values if the options are still available - if (is_groupedopts || is_multiselect) { - if (widgetValue) { - var new_value = []; - for (var i=0, len=widgetValue.length, val; i 1) { - if (target.first().attr('type') == 'checkbox') { - var checkboxesWidget = target.first().closest('.checkboxes-widget-s3'); - if (checkboxesWidget) { - // Not currently supported => skip - s3_debug('filterOptionsS3 error: checkboxes-widget-s3 not supported, skipping'); - return; - //target = checkboxesWidget; - } - } else { - // Multiple rows inside an inline form - multiple = true; - } - } - - if (multiple && settings.getWidgetHTML) { - s3_debug('filterOptionsS3 warning: getWidgetHTML=true not suitable for multiple target widgets (e.g. inline rows)'); - target = target.first(); - multiple = false; - } - var requestTarget = multiple ? target.first() : target; - - // Abort previous request (if any) - var previousRequest = requestTarget.data('update-request'); - if (previousRequest) { - try { - previousRequest.abort(); - } catch(err) {} - } - - // Disable the target field if no value selected in trigger field - var lookupResource = settings.lookupResource; - if (value === '' || value === null || value === undefined) { - target.val('').prop('disabled', true); - if (target.multiselect('instance')) { - target.multiselect('refresh') - .multiselect('disable'); - } - // Trigger change-event on target for filter cascades - target.change(); - updateAddResourceLink(lookupResource, lookupKey); - return; - } - - // Construct the URL for the Ajax request - var url; - if (settings.lookupURL) { - url = settings.lookupURL; - if (value) { - url = url.concat(value); - } - } else { - var lookupPrefix = settings.lookupPrefix; - url = S3.Ap.concat('/', lookupPrefix, '/', lookupResource, '.json'); - // Append lookup key to the URL - var q; - if (value) { - q = lookupResource + '.' + lookupKey + '=' + value; - if (url.indexOf('?') != -1) { - url = url.concat('&' + q); - } else { - url = url.concat('?' + q); - } - } - } - var tooltip = settings.tooltip; - if (tooltip) { - tooltip = 'tooltip=' + tooltip; - if (url.indexOf('?') != -1) { - url = url.concat('&' + tooltip); - } else { - url = url.concat('?' + tooltip); - } - } - - // Represent options unless settings.represent is falsy - var represent = settings.represent; - if (represent || typeof represent == 'undefined') { - if (url.indexOf('?') != -1) { - url = url.concat('&represent=1'); - } else { - url = url.concat('?represent=1'); - } - } - - var request = null; - if (!settings.getWidgetHTML) { - - // Hide all visible targets and show throbber (remember visibility) - target.each(function() { - var widget = $(this), - visible = true; - if (widget.hasClass('groupedopts-widget')) { - visible = widget.groupedopts('visible'); - } else { - visible = widget.data('visible') || widget.is(':visible'); - } - if (visible) { - widget.data('visible', true); - if (widget.hasClass('groupedopts-widget')) { - widget.groupedopts('hide'); - } else { - widget.hide(); - } - addThrobber(widget, lookupResource); - } else { - widget.data('visible', false); - } - }); - - // Send update request - request = $.ajaxS3({ - url: url, - dataType: 'json', - success: function(data) { - - // Render the options - var options = renderOptions(data, settings), - empty = data.length === 0 ? true : false; - - // Apply to all targets - target.each(function() { - - var widget = $(this); - - // Update the widget - widget = updateOptions(widget, options, empty); - - // Show the widget if it was visible before - if (widget.data('visible')) { - if (widget.hasClass('groupedopts-widget')) { - if (!empty) { - widget.groupedopts('show'); - } - } else { - widget.show(); - } - } - - // Remove throbber - removeThrobber(widget, lookupResource); - }); - - // Modify URL for Add-link and show the Add-link - updateAddResourceLink(lookupResource, lookupKey, value); - - // Clear navigate-away-confirm if not a user change - if (!userChange) { - S3ClearNavigateAwayConfirm(); - } - } - }); - - } else { - - // Find the target widget - var targetName = settings.targetWidget || target.attr('name'); - var widget = $('[name = "' + targetName + '"]'), - visible = true, - show_widget = false; - - // Hide the widget if it is visible, add throbber - if (widget.hasClass('groupedopts-widget')) { - visible = widget.groupedopts('visible'); - } else { - visible = widget.data('visible') || widget.is(':visible'); - } - if (visible) { - show_widget = true; - widget.data('visible', true); - if (widget.hasClass('groupedopts-widget')) { - widget.groupedopts('hide'); - } else { - widget.hide(); - } - addThrobber(widget, lookupResource); - } - - // Send update request - request = $.ajaxS3({ - url: url, - dataType: 'html', - success: function(data) { - - // Replace the widget HTML - widget = replaceWidgetHTML(widget, data, settings); - - // Show the widget if it was visible before, remove throbber - if (show_widget) { - if (widget.hasClass('groupedopts-widget')) { - if (widget.find('option').length) { - widget.groupedopts('show'); - } - } else { - widget.show(); - } - } - removeThrobber(widget, lookupResource); - - // Modify URL for Add-link and show the Add-link - updateAddResourceLink(lookupResource, lookupKey, value); - - // Clear navigate-away-confirm if not a user change - if (!userChange) { - S3ClearNavigateAwayConfirm(); - } - } - }); - } - requestTarget.data('update-request', request); - }; - - /** - * Helper method to extract the trigger information, returns an - * array with the actual trigger widget - * - * @param {jQuery} trigger - the trigger field(s) - * @returns {array} [triggerField, triggerValue] - */ - var getTriggerData = function(trigger) { - - var triggerField = trigger, - triggerValue = ''; - if (triggerField.attr('type') == 'checkbox') { - var checkboxesWidget = triggerField.closest('.checkboxes-widget-s3'); - if (checkboxesWidget) { - triggerField = checkboxesWidget; - } - } - if (triggerField.hasClass('checkboxes-widget-s3')) { - triggerValue = []; - triggerField.find('input:checked').each(function() { - triggerValue.push($(this).val()); - }); - } else if (triggerField.hasClass('s3-hierarchy-input')) { - triggerValue = ''; - var value = triggerField.val(); - if (value) { - value = JSON.parse(value); - if (value.constructor === Array) { - if (value.length) { - triggerValue = value[0]; - } - } else - if (!!value) { - triggerValue = value; - } - } - } else if (triggerField.length == 1) { - triggerValue = triggerField.val(); - } - return [triggerField, triggerValue]; - }; - - /** - * Main entry point, configures the event handlers - * - * @param {object} settings - the settings - * @param {string|object} settings.trigger - the trigger (see getSelector) - * @param {string|object} settings.target - the target (see getSelector) - * @param {string} settings.scope - the event scope ('row' for current inline row, - * 'form' for the master form) - * @param {string} settings.event - the trigger event name - * (default: triggerUpdate.[trigger field name]) - * @param {string} settings.lookupKey - the field name to look up (default: trigger - * field name) - * @param {string} settings.lookupField - the name of the field referenced by lookupKey, - * default: 'id' - * @param {string} settings.lookupPrefix - the prefix (controller name) for the lookup - * URL, required - * @param {string} settings.lookupResource - the resource name (function name) for the - * lookup URL, required - * @param {string} settings.lookupURL - override lookup URL - * @param {function} settings.fncPrep - preprocessing function for the JSON data (optional) - * @param {function} settings.fncRepresent - representation function for the JSON data, - * optional, using record.name by default - * @param {bool} settings.getWidgetHTML - lookup returns HTML (to replace the widget) - * rather than JSON data (to update it options) - * @param {string} settings.targetWidget - alternative name-attribute for the target widget, - * overrides the selector generated from target-setting, - * not recommended - * @param {bool} settings.showEmptyField - show an option for None if no options are - * available - * @param {string} settings.msgNoRecords - show this text for the None-option - * @param {bool} settings.optional - add a None-option (without text) even when options - * are available (so the user can select None) - * @param {string} settings.tooltip - additional tooltip field to request from back-end, - * either a field selector or an expression "f(k,v)" - * where f is a function name that can be looked up - * from s3db, and k,v are field selectors for the row, - * f will be called with a list of tuples (k,v) for each - * row and is expected to return a dict {k:tooltip} - */ - $.filterOptionsS3 = function(settings) { - - var trigger = settings.trigger, triggerName; - - if (settings.event) { - triggerName = settings.event; - } else if (typeof trigger == 'string') { - triggerName = trigger; - } else { - triggerName = trigger.name; - } - - var lookupKey = settings.lookupKey || triggerName, - triggerSelector = getSelector(settings.trigger), - targetSelector = getSelector(settings.target), - triggerField, - targetField, - targetForm; - - if (!targetSelector) { - return; - } else { - targetField = $(targetSelector); - if (!targetField.length) { - return; - } - targetForm = targetField.closest('form'); - } - - if (!triggerSelector) { - return; - } else { - // Trigger must be in the same form as target - triggerField = targetForm.find(triggerSelector); - if (!triggerField.length) { - return; - } - } - - // Initial event-less update of the target(s) - $(triggerSelector).each(function() { - var trigger = $(this), - $scope; - // Hidden inline rows must not trigger an initial update - // @note: check visibility of the row not of the trigger, e.g. - // AutoComplete triggers are always hidden! - // @note: must check for CSS explicitly, not just visibility because - // the entire form could be hidden (e.g. list-add) - var inlineRow = trigger.closest('.inline-form'); - if (inlineRow.length && (inlineRow.hasClass('empty-row') || inlineRow.css('display') == 'none')) { - return; - } - if (settings.scope == 'row') { - $scope = trigger.closest('.edit-row.inline-form,.add-row.inline-form'); - } else { - $scope = targetForm; - } - var triggerData = getTriggerData(trigger), - target = $scope.find(targetSelector); - updateTarget(target, lookupKey, triggerData[1], settings, false); - }); - - // Change-event for the trigger fires trigger-event for the target - // form, delegated to targetForm so it happens also for dynamically - // inserted triggers (e.g. inline forms) - var changeEventName = 'change.s3options', - triggerEventName = 'triggerUpdate.' + triggerName; - targetForm.undelegate(triggerSelector, changeEventName) - .delegate(triggerSelector, changeEventName, function() { - var triggerData = getTriggerData($(this)); - targetForm.trigger(triggerEventName, triggerData); - }); - - // Trigger-event for the target form updates all targets within scope - targetForm.on(triggerEventName, function(e, triggerField, triggerValue) { - // Determine the scope - var $scope; - if (settings.scope == 'row') { - $scope = triggerField.closest('.edit-row.inline-form,.add-row.inline-form'); - } else { - $scope = targetForm; - } - // Update all targets within scope - var target = $scope.find(targetSelector); - updateTarget(target, lookupKey, triggerValue, settings, true); - }); - }; -})(jQuery); - -// ============================================================================ -/** - * Link any action buttons/link with the s3-cancel class to the referrer - * (if on the same server and application), or to a default URL (if given), - * or hide them if neither referrer nor default URL are available. - */ -(function() { - - /** - * Strip query and hash from a URL - * - * @param {string} url - the URL - */ - var stripQuery = function(url) { - var newurl = url.split('?')[0].split('#')[0]; - return newurl; - }; - - /** - * Main entry point - * - * @param {string} defaultURL - the default URL - */ - $.cancelButtonS3 = function(defaultURL) { - var cancelButtons = $('.s3-cancel'); - if (!cancelButtons.length) { - return; - } - var referrer = document.referrer; - if (referrer && stripQuery(referrer) != stripQuery(document.URL)) { - var anchor = document.createElement('a'); - anchor.href = referrer; - if (anchor.host == window.location.host && - anchor.pathname.lastIndexOf(S3.Ap, 0) === 0) { - cancelButtons.attr('href', referrer); - } else if (defaultURL) { - cancelButtons.attr('href', defaultURL); - } else { - cancelButtons.hide(); - } - } else if (defaultURL) { - cancelButtons.attr('href', defaultURL); - } else { - cancelButtons.hide(); - } - }; -})(jQuery); - -// ============================================================================ -/** - * Add a Slider to a field - used by S3SliderWidget - */ -S3.slider = function(fieldname, min, max, step, value) { - var real_input = $('#' + fieldname); - var selector = '#' + fieldname + '_slider'; - $(selector).slider({ - min: min, - max: max, - step: step, - value: value, - slide: function (event, ui) { - // Set the value of the real input - real_input.val(ui.value); - }, - change: function( /* event, ui */ ) { - if (value == null) { - // Set a default value - // - halfway between min & max - value = (min + max) / 2; - // - rounded to nearest step - var modulo = value % step; - if (modulo != 0) { - if (modulo < (step / 2)) { - // round down - value = value - modulo; - } else { - // round up - value = value + modulo; - } - } - $(selector).slider('option', 'value', value); - // Show the control - $(selector + ' .ui-slider-handle').show(); - // Show the value - // Hide the help text - real_input.show().next().remove(); - } - } - }); - if (value == null) { - // Don't show a value until Slider is touched - $(selector + ' .ui-slider-handle').hide(); - // Show help text - real_input.hide() - .after('

' + i18n.slider_help + '

'); - } - // Enable the field before form is submitted - real_input.closest('form').submit(function() { - real_input.prop('disabled', false); - // Normal Submit - return true; - }); -}; - -/** - * Add a Range Slider to a field - used by S3SliderFilter - */ -S3.range_slider = function(selector, min_id, max_id, min_value, max_value, step, values) { - var slider_div = $('#' + selector), - min_input = $('#' + min_id), - max_input = $('#' + max_id); - slider_div.slider({ - range: true, - min: min_value, - max: max_value, - step: step, - values: values, - slide: function(event, ui) { - // Set the value of the real inputs & trigger change event - min_input.val(ui.values[0]); - max_input.val(ui.values[1]).closest('form').trigger('optionChanged'); - } - }); - // Update Slider if INPUTs change - min_input.on('change.slider', function() { - var value = min_input.val(); - if (value == '') { - value = min_value; - } - slider_div.slider('values', 0, value); - }); - max_input.on('change.slider', function() { - var value = max_input.val(); - if (value == '') { - value = max_value; - } - slider_div.slider('values', 1, value); - }); -}; - -// ============================================================================ -/** - * Add a querystring variable to an existing URL and redirect into it. - * It accounts for existing variables and will override an existing one. - * Sample usage: _onchange="S3.reloadWithQueryStringVars({'_language': $(this).val()});") - * used by IFRC layouts.py - */ - -S3.reloadWithQueryStringVars = function(queryStringVars) { - var existingQueryVars = location.search ? location.search.substring(1).split('&') : [], - currentUrl = location.search ? location.href.replace(location.search, '') : location.href, - newQueryVars = {}, - newUrl = currentUrl + '?'; - if (existingQueryVars.length > 0) { - for (var i = 0; i < existingQueryVars.length; i++) { - var pair = existingQueryVars[i].split('='); - newQueryVars[pair[0]] = pair[1]; - } - } - if (queryStringVars) { - for (var queryStringVar in queryStringVars) { - newQueryVars[queryStringVar] = queryStringVars[queryStringVar]; - } - } - if (newQueryVars) { - for (var newQueryVar in newQueryVars) { - newUrl += newQueryVar + '=' + newQueryVars[newQueryVar] + '&'; - } - newUrl = newUrl.substring(0, newUrl.length - 1); - window.location.href = newUrl; - } else { - window.location.href = location.href; - } -}; - -// ============================================================================ -// Module pattern to hide internal vars -(function () { - var deduplication = function() { - // Deduplication event handlers - $('.mark-deduplicate').click(function() { - var url = $('#markDuplicateURL').attr('href'); - if (url) { - $.ajaxS3({ - type: 'POST', - url: url, - data: {}, - dataType: 'JSON', - // gets moved to .done() inside .ajaxS3 - success: function( /* data */ ) { - $('.mark-deduplicate, .unmark-deduplicate, .deduplicate').toggleClass('hide'); - } - }); - } - }); - $('.unmark-deduplicate').click(function() { - var url = $('#markDuplicateURL').attr('href'); - if (url) { - $.ajaxS3({ - type: 'POST', - url: url + '?remove=1', - data: {}, - dataType: 'JSON', - // gets moved to .done() inside .ajaxS3 - success: function( /* data */ ) { - $('.mark-deduplicate, .unmark-deduplicate, .deduplicate').toggleClass('hide'); - } - }); - } - }); - $('.swap-button').click(function() { - // Swap widgets between original and duplicate side - var id = this.id; - var name = id.slice(5); - - var original = $('#original_' + name); - var original_id = original.attr('id'); - var original_name = original.attr('name'); - var original_parent = original.parent().closest('td.mwidget'); - var duplicate = $('#duplicate_' + name); - var duplicate_id = duplicate.attr('id'); - var duplicate_name = duplicate.attr('name'); - var duplicate_parent = duplicate.parent().closest('td.mwidget'); - - // Rename with placeholder names - original.attr('id', 'swap_original_id'); - original.attr('name', 'swap_original_name'); - $('#dummy' + original_id).attr('id', 'dummy_swap_original_id'); - duplicate.attr('id', 'swap_duplicate_id'); - duplicate.attr('name', 'swap_duplicate_name'); - $('#dummy' + duplicate_id).attr('id', 'dummy_swap_duplicate_id'); - - // Swap elements - original_parent.before(''); - var o = original_parent.detach(); - duplicate_parent.before(''); - var d = duplicate_parent.detach(); - $('#swap_original_placeholder').after(d); - $('#swap_original_placeholder').remove(); - $('#swap_duplicate_placeholder').after(o); - $('#swap_duplicate_placeholder').remove(); - - // Rename to original names - original.attr('id', duplicate_id); - original.attr('name', duplicate_name); - $('#dummy_swap_original_id').attr('id', 'dummy' + duplicate_id); - duplicate.attr('id', original_id); - duplicate.attr('name', original_name); - $('#dummy_swap_duplicate').attr('id', 'dummy' + original_id); - }); - }; - - /** - * Used by Themes with a Side-menu - * - for long pages with small side menus, we want the side-menu to always be visible - * BUT - * - for short pages with large side-menus, we don't want the side-menu to scroll - */ - var onResize = function() { - // Default Theme - var side_menu_holder = $('.aside'); - /* Doesn't work on IFRC - if (!side_menu_holder.length) { - // IFRC? - side_menu_holder = $('#left-col'); - } */ - if (side_menu_holder.length) { - // Default Theme - var header = $('#menu_modules'); - if (!header.length) { - // Bootstrap? - header = $('#navbar-inner'); - if (!header.length) { - // IFRC? - header = $('#header'); - } - } - // Default Theme - var side_menu = $('#menu_options'); - /* Doesn't work on IFRC - if (!side_menu.length) { - // IFRC? - side_menu = $('#main-sub-menu'); - } */ - //var footer = $('#footer'); - //if ((header.height() + footer.height() + side_menu.height()) < $(window).height()) { - if ((header.height() + side_menu.height() + 10) < $(window).height()) { - side_menu_holder.css('position', 'fixed'); - $('#content').css('min-height', side_menu.height()); - } else { - side_menu_holder.css('position', 'static'); - } - } - }; - - // ======================================================================== - $(document).ready(function() { - // Web2Py Layer - $('.alert-error').hide().slideDown('slow'); - $('.alert-error').click(function() { - $(this).fadeOut('slow'); - return false; - }); - $('.alert-warning').hide().slideDown('slow'); - $('.alert-warning').click(function() { - $(this).fadeOut('slow'); - return false; - }); - $('.alert-info').hide().slideDown('slow'); - $('.alert-info').click(function() { - $(this).fadeOut('slow'); - return false; - }); - $('.alert-success').hide().slideDown('slow'); - $('.alert-success').click(function() { - $(this).fadeOut('slow'); - return false; - }); - $("input[type='checkbox'].delete").click(function() { - if ((this.checked) && (!confirm(i18n.delete_confirmation))) { - this.checked = false; - } - }); - - // T2 Layer - //try { $('.zoom').fancyZoom( { - // scaleImg: true, - // closeOnClick: true, - // directory: S3.Ap.concat('/static/media') - //}); } catch(e) {} - - // S3 Layer - - // If a form is submitted with errors, this will scroll - // the window to the first form error message - var inputErrorId = $('form .error[id]').eq(0).attr('id'); - if (inputErrorId != undefined) { - var inputName = inputErrorId.replace('__error', ''), - inputId = $('[name=' + inputName + ']').attr('id'), - inputLabel = $('[for=' + inputId + ']'); - try { - window.scrollTo(0, inputLabel.offset().top); - // Prevent first-field focus from scrolling back to - // the top of the form again: - S3.FocusOnFirstField = false; - } catch(e) {} - } - - // dataTables' delete button - // (can't use S3.confirmClick as the buttons haven't yet rendered) - if (S3.interactive) { - $(document).on('click', 'a.delete-btn', function(event) { - if (confirm(i18n.delete_confirmation)) { - return true; - } else { - event.preventDefault(); - return false; - } - }); - - if (S3.FocusOnFirstField != false) { - // Focus On First Field - $('input:text:visible:first').focus(); - } - } - - // Accept comma as thousands separator - $('input.int_amount').keyup(function() { - this.value = this.value.reverse() - .replace(/[^0-9\-,]|\-(?=.)/g, '') - .reverse(); - }); - $('input.float_amount').keyup(function() { - this.value = this.value.reverse() - .replace(/[^0-9\-\.,]|[\-](?=.)|[\.](?=[0-9]*[\.])/g, '') - .reverse(); - }); - - // Auto-capitalize first names - $('input[name="first_name"]').focusout(function() { - this.value = this.value.charAt(0).toLocaleUpperCase() + this.value.substring(1); - }); - - // Ensure that phone fields appear with + at beginning not end in RTL - if (S3.rtl) { - $('.phone-widget').each(function() { - // When form initially renders, ensure the LTR mark is placed at the beginning so that it looks correct - // http://www.fileformat.info/info/unicode/char/200e/index.htm - var value = this.value; - if (value && (value.charAt(0) != '\u200E')) { - this.value = '\u200E' + value; - } - }); - $('.phone-widget').focusout(function() { - var value = this.value; - if (value) { - // When new data is entered then: - // 1. Ensure the LTR mark is placed at the beginning so that it looks correct - // 2. Ensure that if there is a trailing + then it is moved to the beginning - if (value.charAt(0) != '\u200E') { - if (value.charAt(value.length - 1) == '+') { - this.value = '\u200E' + '+' + value.substr(0, value.length - 2); - } else { - this.value = '\u200E' + value; - } - } else { - if (value.charAt(value.length - 1) == '+') { - this.value = '\u200E' + '+' + value.substr(1, value.length - 2); - } - } - } - }); - } - - // ListCreate Views - $('#show-add-btn').click(function() { - // Hide the Button - $('#show-add-btn').hide(10, function() { - // Show the Form - $('#list-add').slideDown('medium'); - // Resize any jQueryUI SelectMenu buttons - $('.select-widget').selectmenu('refresh'); - }); - }); - - // Resizable textareas - $('textarea.resizable:not(.textarea-processed)').each(function() { - var that = $(this); - // Avoid non-processed teasers. - if (that.is(('textarea.teaser:not(.teaser-processed)'))) { - return false; - } - var textarea = that.addClass('textarea-processed'); - var staticOffset = null; - // When wrapping the text area, work around an IE margin bug. See: - // http://jaspan.com/ie-inherited-margin-bug-form-elements-and-haslayout - that.wrap('
') - .parent().append($('
').mousedown(startDrag)); - var grippie = $('div.grippie', that.parent())[0]; - grippie.style.marginRight = (grippie.offsetWidth - that[0].offsetWidth) + 'px'; - function startDrag(e) { - staticOffset = textarea.height() - e.pageY; - textarea.css('opacity', 0.25); - $(document).mousemove(performDrag).mouseup(endDrag); - return false; - } - function performDrag(e) { - textarea.height(Math.max(32, staticOffset + e.pageY) + 'px'); - return false; - } - function endDrag( /* e */ ) { - $(document).unbind('mousemove', performDrag).unbind('mouseup', endDrag); - textarea.css('opacity', 1); - } - return true; - }); - - // IE6 non anchor hover hack - $('#modulenav .hoverable').hover( - function() { - $(this).addClass('hovered'); - }, - function() { - $(this).removeClass('hovered'); - } - ); - - // Menu popups (works in IE6) - $('#modulenav li').hover( - function() { - var header_width = $(this).width(); - var popup_width = $('ul', this).width(); - if (popup_width !== null){ - if (popup_width < header_width){ - $('ul', this).css({ - 'width': header_width.toString() + 'px' - }); - } - } - $('ul', this).css('display', 'block'); - }, - function() { - $('ul', this).css('display', 'none'); - } - ); - - // Event Handlers for the page - S3.redraw(); - - // Popovers (Bootstrap themes only) - if (typeof($.fn.popover) != 'undefined') { - // Applies to elements created after $(document).ready - $('body').popover({ - selector: '.s3-popover', - trigger: 'hover', - placement: 'left' - }); - } - - // Handle Page Resizes - onResize(); - $(window).bind('resize', onResize); - - // De-duplication Event Handlers - deduplication(); - - // Timezone and UTC Offset - var anyform = $('form'); - if (anyform.length) { - var now = new Date(), - tz; - anyform.append(""); - try { - tz = Intl.DateTimeFormat().resolvedOptions().timeZone; - } catch(e) { - // not supported - } - if (tz) { - anyform.append(""); - } - } - - // Social Media 'share' buttons - if ($('#socialmedia_share').length > 0) { - // DIV exists (deployment_setting on) - var currenturl = document.location.href; - // Linked-In - $('#socialmedia_share').append(""); - // Twitter - $('#socialmedia_share').append(""); - // Facebook - $('#socialmedia_share').append(""); - } - - // Form toggle (e.g. S3Profile update-form) - $('.form-toggle').click(function() { - var self = $(this), - hidden = $(this).data('hidden'); - hidden = hidden && hidden != 'False'; - self.data('hidden', hidden ? false : true) - .siblings().slideToggle('medium', function() { - self.children('span').each(function() { - $(this).text(hidden ? $(this).data('off') : $(this).data('on')) - .siblings().toggle(); - }); - }); - }); - - // Options Menu Toggle on mobile - $('#menu-options-toggle,#list-filter-toggle').on('click', function(e) { - e.stopPropagation(); - var $this = $(this); - var status = $this.data('status'), - menu; - if (this.id == 'menu-options-toggle') { - menu = $('#menu-options'); - } else { - menu = $('#list-filter'); - } - if (status == 'off') { - menu.hide().removeClass('hide-for-small').slideDown(400, function() { - $this.data('status', 'on').text($this.data('on')); - }); - } else { - menu.slideUp(400, function() { - menu.addClass('hide-for-small').show(); - $this.data('status', 'off').text($this.data('off')); - }); - } - }); - - /** - * Click-handler for s3-download-buttons: - * - * - any action item with a class 's3-download-button' and - * a 'url' data property (data-url) - * - * - for download of server-generated attachments, e.g. XLS or PDF - * - * - downloads the target document in a hidden iframe, which, if - * the file is sent with content-disposition "attachment", will - * only open the file dialog and nothing else - * - * - if this fails, the response will be opened in a modal dialog - * (JSON messages will be handled with a simple alert-box, though) - */ - $('.s3-download-button').on('click', function(e) { - - // Do nothing else - e.preventDefault(); - e.stopPropagation(); - - var url = $(this).data('url'); - if (!url) { - return; - } - - // Re-use it if it already exists - var iframe = document.getElementById("s3-download"); - if (iframe == null) { - iframe = document.createElement('iframe'); - iframe.id = "s3-download"; - iframe.style.visibility = 'hidden'; - document.body.appendChild(iframe); - } - - $('#s3-download').off('load').on('load', function() { - // This event is only fired when contents was loaded into the - // hidden iframe rather than downloaded as attachment, which - // should only happen if there was some kind of error - var message, - self = $(this); - try { - // Try to parse the JSON message - message = JSON.parse(this.contentDocument.body.textContent).message; - } catch(e) { - // No JSON message => show iframe contents as-is in a modal - self.dialog({ - title: 'Download failed', - width: 500, - height: 300, - close: function() { - self.attr('src', '').remove(); - } - }).css({ - visibility: 'visible', - width: '100%' - }); - return; - } - alert(message); - }); - - iframe.src = url; - return false; - }); - }); - -}()); -// ============================================================================ +/** + * Custom Javascript functions added as part of the S3 Framework + * Strings are localised in views/l10n.js + */ + + /** + * The startsWith string function is introduced in JS 1.8.6 -- it's not even + * accepted in ECMAScript yet, so don't expect all browsers to have it. + * Thx to http://www.moreofless.co.uk/javascript-string-startswith-endswith/ + * for showing how to add it to string if not present. + */ +if (typeof String.prototype.startsWith != 'function') { + String.prototype.startsWith = function(str) { + return this.substring(0, str.length) === str; + }; +} + +// Global variable to store all of our variables inside +var S3 = {}; +S3.gis = {}; +S3.gis.options = {}; +S3.timeline = {}; +S3.JSONRequest = {}; // Used to store and abort JSON requests +//S3.TimeoutVar = {}; // Used to store and abort JSON requests + +S3.queryString = { + // From https://github.com/sindresorhus/query-string + parse: function(str) { + if (typeof str !== 'string') { + return {}; + } + + str = str.trim().replace(/^(\?|#)/, ''); + + if (!str) { + return {}; + } + + return str.trim().split('&').reduce(function (ret, param) { + var parts = param.replace(/\+/g, ' ').split('='); + var key = parts[0]; + var val = parts[1]; + + key = decodeURIComponent(key); + // missing `=` should be `null`: + // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters + val = val === undefined ? null : decodeURIComponent(val); + + if (!ret.hasOwnProperty(key)) { + ret[key] = val; + } else if (Array.isArray(ret[key])) { + ret[key].push(val); + } else { + ret[key] = [ret[key], val]; + } + + return ret; + }, {}); + }, + stringify: function(obj) { + return obj ? Object.keys(obj).map(function (key) { + var val = obj[key]; + + if (Array.isArray(val)) { + return val.map(function (val2) { + return encodeURIComponent(key) + '=' + encodeURIComponent(val2); + }).join('&'); + } + + return encodeURIComponent(key) + '=' + encodeURIComponent(val); + }).join('&') : ''; + } +}; + +S3.uid = function() { + // Generate a random uid + // Used for jQueryUI modals and some Map popups + // http://jsperf.com/random-uuid/2 + return (((+(new Date())) / 1000 * 0x10000 + Math.random() * 0xffff) >> 0).toString(16); +}; + +S3.Utf8 = { + // Used by dataTables + // http://www.webtoolkit.info + encode: function(string) { + string = string.replace(/\r\n/g, '\n'); + var utftext = ''; + for (var n = 0; n < string.length; n++) { + var c = string.charCodeAt(n); + if (c < 128) { + utftext += String.fromCharCode(c); + } else if ((c > 127) && (c < 2048)) { + utftext += String.fromCharCode((c >> 6) | 192); + utftext += String.fromCharCode((c & 63) | 128); + } else { + utftext += String.fromCharCode((c >> 12) | 224); + utftext += String.fromCharCode(((c >> 6) & 63) | 128); + utftext += String.fromCharCode((c & 63) | 128); + } + } + return utftext; + }, + decode: function(utftext) { + var string = '', + i = 0, + c = 0, + c1 = 0, + c2 = 0; + while ( i < utftext.length ) { + c = utftext.charCodeAt(i); + if (c < 128) { + string += String.fromCharCode(c); + i++; + } else if ((c > 191) && (c < 224)) { + c1 = utftext.charCodeAt(i+1); + string += String.fromCharCode(((c & 31) << 6) | (c1 & 63)); + i += 2; + } else { + c1 = utftext.charCodeAt(i+1); + c2 = utftext.charCodeAt(i+2); + string += String.fromCharCode(((c & 15) << 12) | ((c1 & 63) << 6) | (c2 & 63)); + i += 3; + } + } + return string; + } +}; + +S3.addTooltips = function() { + // Help Tooltips + $.cluetip.defaults.cluezIndex = 9999; // Need to be able to show on top of Ext Windows + $('.tooltip').cluetip({activation: 'hover', sticky: false, splitTitle: '|'}); + $('label[title][title!=""]').cluetip({splitTitle: '|', showTitle:false}); + $('.tooltipbody').cluetip({activation: 'hover', sticky: false, splitTitle: '|', showTitle: false}); + var tipCloseText = 'close'; + $('.stickytip').cluetip({ + activation: 'hover', + sticky: true, + closePosition: 'title', + closeText: tipCloseText, + splitTitle: '|' + }); + $('.errortip').cluetip({ + activation: 'click', + sticky: true, + closePosition: 'title', + closeText: tipCloseText, + splitTitle: '|' + }); + $('.ajaxtip').cluetip({ + activation: 'click', + sticky: true, + closePosition: 'title', + closeText: tipCloseText, + width: 380 + }); + $('.htmltip').cluetip({ + activation: 'hover', + sticky: false, + local: true, + attribute: 'show', + width: 380 + }); +}; + +// jQueryUI Modal Popups +S3.addModals = function() { + $('a.s3_add_resource_link').attr('href', function(index, attr) { + var url_out = attr; + // Avoid Duplicate callers + if (attr.indexOf('caller=') == -1) { + // Add the caller to the URL vars so that the popup knows which field to refresh/set + // Default formstyle + var caller = $(this).parents('tr').attr('id'); + if (!caller) { + // DIV-based formstyle + caller = $(this).parent().parent().attr('id'); + } + if (!caller) { + caller = $(this).parents('.form-row').attr('id'); + } + if (!caller) { + // Bootstrap formstyle + caller = $(this).parents('.control-group').attr('id'); + } + if (caller) { + caller = caller.replace(/__row_comment/, '') // DRRPP formstyle + .replace(/__row/, ''); + url_out = attr + '&caller=' + caller; + } + } + return url_out; + }); + $('.s3_add_resource_link, .s3_modal').off('.S3Modal') + .on('click.S3Modal', function() { + var title = this.title; + var url = this.href; + var i = url.indexOf('caller='); + if (i != -1) { + var caller = url.slice(i + 7); + i = caller.indexOf('&'); + if (i != -1) { + caller = caller.slice(0, i); + } + var select = $('#' + caller); + if (select.hasClass('multiselect-widget')) { + // Close the menu (otherwise this shows above the popup) + select.multiselect('close'); + // Lower the z-Index + //select.css('z-index', 1049); + } + } + + // Create an iframe + var id = S3.uid(), + dialog = $(''; + var closelink = $('' + i18n.close_map + ''); + + // @ToDo: Also make the represent link act as a close + closelink.bind('click', function(evt) { + $('#map').html(oldhtml); + evt.preventDefault(); + }); + + $('#map').html(iframe); + $('#map').append($("
").append(closelink)); +}; +S3.popupWin = null; +S3.openPopup = function(url, center) { + if ( !S3.popupWin || S3.popupWin.closed ) { + var params = 'width=640, height=480'; + if (center === true) { + params += ',left=' + (screen.width - 640)/2 + + ',top=' + (screen.height - 480)/2; + } + S3.popupWin = window.open(url, 'popupWin', params); + } else { + S3.popupWin.focus(); + } +}; + +// ============================================================================ +/** + * Filter options of a drop-down field (=target) by the selection made + * in another field (=trigger), e.g.: + * - Task Form: Activity options filtered by Project selection + * + * @todo: fix updateAddResourceLink + * @todo: move into separate file and load only when needed? + */ + +(function() { + + /** + * Get a CSS selector for trigger/target fields + * + * @param {string|object} setting - the setting for trigger/target, value of the + * name-attribute in regular form fields, or an + * object describing a field in an inline component + * (see below for the latter) + * @param {string} setting.prefix - the inline form prefix (default: 'default') + * @param {string} setting.alias - the component alias for the inline form (e.g. task_project) + * @param {string} setting.name - the field name + * @param {string} setting.inlineType - the inline form type, 'link' (for S3SQLInlineLink), + * or 'sub' (for other S3SQLInlineComponent types) + * @param {string} setting.inlineRows - the inline form has multiple rows, default: true + */ + var getSelector = function(setting) { + + var selector; + if (typeof setting == 'string') { + // Simple field name + selector = '[name="' + setting + '"]'; + } else { + // Inline form + var prefix = setting.prefix ? setting.prefix : 'default'; + if (setting.alias) { + prefix += setting.alias; + } + var type = setting.inlineType || 'sub', + rows = setting.inlineRows; + if (rows === undefined) { + rows = true; + } + if (type == 'sub') { + var name = setting.name; + if (rows) { + selector = '[name^="' + prefix + '"][name*="_i_' + name + '_edit_"]'; + } else { + selector = '[name="' + prefix + '-' + name + '"]'; + } + } else if (type == 'link') { + selector = '[name="link_' + prefix + '"]'; + } + } + return selector; + }; + + + /** + * Add a throbber while loading the widget options + * + * @param {object} widget - the target widget + * @param {string} resourceName - the target resource name + */ + var addThrobber = function(widget, resourceName) { + + var throbber = widget.siblings('.' + resourceName + '-throbber'); + if (!throbber.length) { + widget.after('
'); + } + }; + + /** + * Remove a previously inserted throbber + * + * @param {string} resourceName - the target resource name + */ + var removeThrobber = function(widget, resourceName) { + + var throbber = widget.siblings('.' + resourceName + '-throbber'); + if (throbber.length) { + throbber.remove(); + } + }; + + /** + * Update the AddResourceLink for the target with lookup key and + * value, so that the popup can pre-populate them; or hide the + * AddResourceLink if no trigger value has been selected + * + * @param {string} resourceName - the target resource name + * @param {string} key - the lookup key + * @param {string} value - the selected trigger value + */ + var updateAddResourceLink = function(resourceName, key, value) { + + $('a#' + resourceName + '_add').each(function() { + var search = this.search, + queries = [], + selectable = false; + if (search) { + var items = search.substring(1).split('&'); + items.forEach(function(item) { + if (decodeURIComponent(item.split('=')[0]) != key) { + queries.push(item); + } + }); + } + if (value !== undefined && value !== null) { + var query = encodeURIComponent(key) + '=' + encodeURIComponent(value); + queries.push(query); + selectable = true; + } + if (queries.length) { + search = '?' + queries.join('&'); + } else { + search = ''; + } + var href = this.protocol + '//' + this.host + this.pathname + search + this.hash, + $this = $(this).attr('href', href); + if (selectable) { + $this.parent().show(); + } else { + $this.parent().hide(); + } + }); + }; + + /** + * Render the options from the JSON data + * + * @param {array} data - the JSON data + * @param {object} settings - the settings + */ + var renderOptions = function(data, settings) { + + var options = [], + defaultValue = 0; + + if (data.length === 0) { + // No options available + var showEmptyField = settings.showEmptyField; + if (showEmptyField || showEmptyField === undefined) { + var msgNoRecords = settings.msgNoRecords || '-'; + options.push(''); + } + } else { + + // Pre-process the data + var fncPrep = settings.fncPrep, + prepResult = null; + if (fncPrep) { + try { + prepResult = fncPrep(data); + } catch (e) {} + } + + // Render the options + var lookupField = settings.lookupField || 'id', + fncRepresent = settings.fncRepresent, + record, + value, + name, + title; + + for (var i = 0; i < data.length; i++) { + record = data[i]; + value = record[lookupField]; + if (i === 0) { + defaultValue = value; + } + name = fncRepresent ? fncRepresent(record, prepResult) : record.name; + // Does the option have an onhover-tooltip? + if (record._tooltip) { + title = ' title="' + record._tooltip + '"'; + } else { + title = ''; + } + options.push('' + name + ''); + } + if (settings.optional) { + // Add (and default to) empty option + defaultValue = 0; + options.unshift(''); + } + } + return {options: options.join(''), defaultValue: defaultValue}; + }; + + /** + * Update the options of the target field from JSON data + * + * @param {jQuery} widget - the widget + * @param {object} data - the data as rendered by renderOptions + * @param {bool} empty - no options available (don't bother retaining + * the current value) + */ + var updateOptions = function(widget, data, empty) { + + // Catch unsupported widget type + if (widget.hasClass('checkboxes-widget-s3')) { + s3_debug('filterOptionsS3 error: checkboxes-widget-s3 not supported, updateOptions aborted'); + return; + } + + var options = data.options, + newValue = data.defaultValue, + selectedValues = []; + + // Get the current value of the target field + if (!empty) { + var currentValue = ''; + if (widget.hasClass('checkboxes-widget-s3')) { + // Checkboxes-widget-s3 target, not currently supported + //currentValue = new Array(); + //widget.find('input:checked').each(function() { + // currentValue.push($(this).val()); + //}); + return; + } else { + // SELECT-based target (Select, MultiSelect, GroupedOpts) + currentValue = widget.val(); + if (!currentValue) { + // Options list not populated yet? + currentValue = widget.prop('value'); + } + if (!$.isArray(currentValue)) { + currentValue = [currentValue]; + } + for (var i = 0, len = currentValue.length, val; i < len; i++) { + val = currentValue[i]; + if (val && $(options).filter('option[value=' + val + ']').length) { + selectedValues.push(val); + } + } + } + if (selectedValues.length) { + // Retain selected value + newValue = selectedValues; + } + } + + // Convert IS_ONE_OF_EMPTY into a ').addClass(widget.attr('class')) + .attr('id', widget.attr('id')) + .attr('name', widget.attr('name')) + .data('visible', widget.data('visible')) + .hide(); + widget.replaceWith(select); + widget = select; + } + + // Update the target field options + var disable = options === ''; + widget.html(options) + .val(newValue) + .change() + .prop('disabled', disable); + + // Refresh groupedopts or multiselect + if (widget.hasClass('groupedopts-widget')) { + widget.groupedopts('refresh'); + } else if (widget.hasClass('multiselect-widget')) { + widget.multiselect('refresh'); + // Disabled-attribute not reflected by refresh (?) + if (!disable) { + widget.multiselect('enable'); + } else { + widget.multiselect('disable'); + } + } + return widget; + }; + + /** + * Replace the widget HTML with the data returned by Ajax request + * + * @param {jQuery} widget: the widget + * @param {string} data: the HTML data + */ + var replaceWidgetHTML = function(widget, data) { + + if (data !== '') { + + // Do we have a groupedopts or multiselect widget? + var is_groupedopts = false, + is_multiselect = false; + if (widget.hasClass('groupedopts-widget')) { + is_groupedopts = true; + } else if (widget.hasClass('multiselect-widget')) { + is_multiselect = true; + } + + // Store selected value before replacing the widget HTML + var widgetValue; + if (is_groupedopts || is_multiselect) { + if (widget.prop('tagName').toLowerCase() == 'select') { + widgetValue = widget.val(); + } + } + + // Replace the widget with the HTML returned + widget.html(data) + .change() + .prop('disabled', false); + + // Restore selected values if the options are still available + if (is_groupedopts || is_multiselect) { + if (widgetValue) { + var new_value = []; + for (var i=0, len=widgetValue.length, val; i 1) { + if (target.first().attr('type') == 'checkbox') { + var checkboxesWidget = target.first().closest('.checkboxes-widget-s3'); + if (checkboxesWidget) { + // Not currently supported => skip + s3_debug('filterOptionsS3 error: checkboxes-widget-s3 not supported, skipping'); + return; + //target = checkboxesWidget; + } + } else { + // Multiple rows inside an inline form + multiple = true; + } + } + + if (multiple && settings.getWidgetHTML) { + s3_debug('filterOptionsS3 warning: getWidgetHTML=true not suitable for multiple target widgets (e.g. inline rows)'); + target = target.first(); + multiple = false; + } + var requestTarget = multiple ? target.first() : target; + + // Abort previous request (if any) + var previousRequest = requestTarget.data('update-request'); + if (previousRequest) { + try { + previousRequest.abort(); + } catch(err) {} + } + + // Disable the target field if no value selected in trigger field + var lookupResource = settings.lookupResource; + if (value === '' || value === null || value === undefined) { + target.val('').prop('disabled', true); + if (target.multiselect('instance')) { + target.multiselect('refresh') + .multiselect('disable'); + } + // Trigger change-event on target for filter cascades + target.change(); + updateAddResourceLink(lookupResource, lookupKey); + return; + } + + // Construct the URL for the Ajax request + var url; + if (settings.lookupURL) { + url = settings.lookupURL; + if (value) { + url = url.concat(value); + } + } else { + var lookupPrefix = settings.lookupPrefix; + url = S3.Ap.concat('/', lookupPrefix, '/', lookupResource, '.json'); + // Append lookup key to the URL + var q; + if (value) { + q = lookupResource + '.' + lookupKey + '=' + value; + if (url.indexOf('?') != -1) { + url = url.concat('&' + q); + } else { + url = url.concat('?' + q); + } + } + } + var tooltip = settings.tooltip; + if (tooltip) { + tooltip = 'tooltip=' + tooltip; + if (url.indexOf('?') != -1) { + url = url.concat('&' + tooltip); + } else { + url = url.concat('?' + tooltip); + } + } + + // Represent options unless settings.represent is falsy + var represent = settings.represent; + if (represent || typeof represent == 'undefined') { + if (url.indexOf('?') != -1) { + url = url.concat('&represent=1'); + } else { + url = url.concat('?represent=1'); + } + } + + var request = null; + if (!settings.getWidgetHTML) { + + // Hide all visible targets and show throbber (remember visibility) + target.each(function() { + var widget = $(this), + visible = true; + if (widget.hasClass('groupedopts-widget')) { + visible = widget.groupedopts('visible'); + } else { + visible = widget.data('visible') || widget.is(':visible'); + } + if (visible) { + widget.data('visible', true); + if (widget.hasClass('groupedopts-widget')) { + widget.groupedopts('hide'); + } else { + widget.hide(); + } + addThrobber(widget, lookupResource); + } else { + widget.data('visible', false); + } + }); + + // Send update request + request = $.ajaxS3({ + url: url, + dataType: 'json', + success: function(data) { + + // Render the options + var options = renderOptions(data, settings), + empty = data.length === 0 ? true : false; + + // Apply to all targets + target.each(function() { + + var widget = $(this); + + // Update the widget + widget = updateOptions(widget, options, empty); + + // Show the widget if it was visible before + if (widget.data('visible')) { + if (widget.hasClass('groupedopts-widget')) { + if (!empty) { + widget.groupedopts('show'); + } + } else { + widget.show(); + } + } + + // Remove throbber + removeThrobber(widget, lookupResource); + }); + + // Modify URL for Add-link and show the Add-link + updateAddResourceLink(lookupResource, lookupKey, value); + + // Clear navigate-away-confirm if not a user change + if (!userChange) { + S3ClearNavigateAwayConfirm(); + } + } + }); + + } else { + + // Find the target widget + var targetName = settings.targetWidget || target.attr('name'); + var widget = $('[name = "' + targetName + '"]'), + visible = true, + show_widget = false; + + // Hide the widget if it is visible, add throbber + if (widget.hasClass('groupedopts-widget')) { + visible = widget.groupedopts('visible'); + } else { + visible = widget.data('visible') || widget.is(':visible'); + } + if (visible) { + show_widget = true; + widget.data('visible', true); + if (widget.hasClass('groupedopts-widget')) { + widget.groupedopts('hide'); + } else { + widget.hide(); + } + addThrobber(widget, lookupResource); + } + + // Send update request + request = $.ajaxS3({ + url: url, + dataType: 'html', + success: function(data) { + + // Replace the widget HTML + widget = replaceWidgetHTML(widget, data, settings); + + // Show the widget if it was visible before, remove throbber + if (show_widget) { + if (widget.hasClass('groupedopts-widget')) { + if (widget.find('option').length) { + widget.groupedopts('show'); + } + } else { + widget.show(); + } + } + removeThrobber(widget, lookupResource); + + // Modify URL for Add-link and show the Add-link + updateAddResourceLink(lookupResource, lookupKey, value); + + // Clear navigate-away-confirm if not a user change + if (!userChange) { + S3ClearNavigateAwayConfirm(); + } + } + }); + } + requestTarget.data('update-request', request); + }; + + /** + * Helper method to extract the trigger information, returns an + * array with the actual trigger widget + * + * @param {jQuery} trigger - the trigger field(s) + * @returns {array} [triggerField, triggerValue] + */ + var getTriggerData = function(trigger) { + + var triggerField = trigger, + triggerValue = ''; + if (triggerField.attr('type') == 'checkbox') { + var checkboxesWidget = triggerField.closest('.checkboxes-widget-s3'); + if (checkboxesWidget) { + triggerField = checkboxesWidget; + } + } + if (triggerField.hasClass('checkboxes-widget-s3')) { + triggerValue = []; + triggerField.find('input:checked').each(function() { + triggerValue.push($(this).val()); + }); + } else if (triggerField.hasClass('s3-hierarchy-input')) { + triggerValue = ''; + var value = triggerField.val(); + if (value) { + value = JSON.parse(value); + if (value.constructor === Array) { + if (value.length) { + triggerValue = value[0]; + } + } else + if (!!value) { + triggerValue = value; + } + } + } else if (triggerField.length == 1) { + triggerValue = triggerField.val(); + } + return [triggerField, triggerValue]; + }; + + /** + * Main entry point, configures the event handlers + * + * @param {object} settings - the settings + * @param {string|object} settings.trigger - the trigger (see getSelector) + * @param {string|object} settings.target - the target (see getSelector) + * @param {string} settings.scope - the event scope ('row' for current inline row, + * 'form' for the master form) + * @param {string} settings.event - the trigger event name + * (default: triggerUpdate.[trigger field name]) + * @param {string} settings.lookupKey - the field name to look up (default: trigger + * field name) + * @param {string} settings.lookupField - the name of the field referenced by lookupKey, + * default: 'id' + * @param {string} settings.lookupPrefix - the prefix (controller name) for the lookup + * URL, required + * @param {string} settings.lookupResource - the resource name (function name) for the + * lookup URL, required + * @param {string} settings.lookupURL - override lookup URL + * @param {function} settings.fncPrep - preprocessing function for the JSON data (optional) + * @param {function} settings.fncRepresent - representation function for the JSON data, + * optional, using record.name by default + * @param {bool} settings.getWidgetHTML - lookup returns HTML (to replace the widget) + * rather than JSON data (to update it options) + * @param {string} settings.targetWidget - alternative name-attribute for the target widget, + * overrides the selector generated from target-setting, + * not recommended + * @param {bool} settings.showEmptyField - show an option for None if no options are + * available + * @param {string} settings.msgNoRecords - show this text for the None-option + * @param {bool} settings.optional - add a None-option (without text) even when options + * are available (so the user can select None) + * @param {string} settings.tooltip - additional tooltip field to request from back-end, + * either a field selector or an expression "f(k,v)" + * where f is a function name that can be looked up + * from s3db, and k,v are field selectors for the row, + * f will be called with a list of tuples (k,v) for each + * row and is expected to return a dict {k:tooltip} + */ + $.filterOptionsS3 = function(settings) { + + var trigger = settings.trigger, triggerName; + + if (settings.event) { + triggerName = settings.event; + } else if (typeof trigger == 'string') { + triggerName = trigger; + } else { + triggerName = trigger.name; + } + + var lookupKey = settings.lookupKey || triggerName, + triggerSelector = getSelector(settings.trigger), + targetSelector = getSelector(settings.target), + triggerField, + targetField, + targetForm; + + if (!targetSelector) { + return; + } else { + targetField = $(targetSelector); + if (!targetField.length) { + return; + } + targetForm = targetField.closest('form'); + } + + if (!triggerSelector) { + return; + } else { + // Trigger must be in the same form as target + triggerField = targetForm.find(triggerSelector); + if (!triggerField.length) { + return; + } + } + + // Initial event-less update of the target(s) + $(triggerSelector).each(function() { + var trigger = $(this), + $scope; + // Hidden inline rows must not trigger an initial update + // @note: check visibility of the row not of the trigger, e.g. + // AutoComplete triggers are always hidden! + // @note: must check for CSS explicitly, not just visibility because + // the entire form could be hidden (e.g. list-add) + var inlineRow = trigger.closest('.inline-form'); + if (inlineRow.length && (inlineRow.hasClass('empty-row') || inlineRow.css('display') == 'none')) { + return; + } + if (settings.scope == 'row') { + $scope = trigger.closest('.edit-row.inline-form,.add-row.inline-form'); + } else { + $scope = targetForm; + } + var triggerData = getTriggerData(trigger), + target = $scope.find(targetSelector); + updateTarget(target, lookupKey, triggerData[1], settings, false); + }); + + // Change-event for the trigger fires trigger-event for the target + // form, delegated to targetForm so it happens also for dynamically + // inserted triggers (e.g. inline forms) + var changeEventName = 'change.s3options', + triggerEventName = 'triggerUpdate.' + triggerName; + targetForm.undelegate(triggerSelector, changeEventName) + .delegate(triggerSelector, changeEventName, function() { + var triggerData = getTriggerData($(this)); + targetForm.trigger(triggerEventName, triggerData); + }); + + // Trigger-event for the target form updates all targets within scope + targetForm.on(triggerEventName, function(e, triggerField, triggerValue) { + // Determine the scope + var $scope; + if (settings.scope == 'row') { + $scope = triggerField.closest('.edit-row.inline-form,.add-row.inline-form'); + } else { + $scope = targetForm; + } + // Update all targets within scope + var target = $scope.find(targetSelector); + updateTarget(target, lookupKey, triggerValue, settings, true); + }); + }; +})(jQuery); + +// ============================================================================ +/** + * Link any action buttons/link with the s3-cancel class to the referrer + * (if on the same server and application), or to a default URL (if given), + * or hide them if neither referrer nor default URL are available. + */ +(function() { + + /** + * Strip query and hash from a URL + * + * @param {string} url - the URL + */ + var stripQuery = function(url) { + var newurl = url.split('?')[0].split('#')[0]; + return newurl; + }; + + /** + * Main entry point + * + * @param {string} defaultURL - the default URL + */ + $.cancelButtonS3 = function(defaultURL) { + var cancelButtons = $('.s3-cancel'); + if (!cancelButtons.length) { + return; + } + var referrer = document.referrer; + if (referrer && stripQuery(referrer) != stripQuery(document.URL)) { + var anchor = document.createElement('a'); + anchor.href = referrer; + if (anchor.host == window.location.host && + anchor.pathname.lastIndexOf(S3.Ap, 0) === 0) { + cancelButtons.attr('href', referrer); + } else if (defaultURL) { + cancelButtons.attr('href', defaultURL); + } else { + cancelButtons.hide(); + } + } else if (defaultURL) { + cancelButtons.attr('href', defaultURL); + } else { + cancelButtons.hide(); + } + }; +})(jQuery); + +// ============================================================================ +/** + * Add a Slider to a field - used by S3SliderWidget + */ +S3.slider = function(fieldname, min, max, step, value) { + var real_input = $('#' + fieldname); + var selector = '#' + fieldname + '_slider'; + $(selector).slider({ + min: min, + max: max, + step: step, + value: value, + slide: function (event, ui) { + // Set the value of the real input + real_input.val(ui.value); + }, + change: function( /* event, ui */ ) { + if (value == null) { + // Set a default value + // - halfway between min & max + value = (min + max) / 2; + // - rounded to nearest step + var modulo = value % step; + if (modulo != 0) { + if (modulo < (step / 2)) { + // round down + value = value - modulo; + } else { + // round up + value = value + modulo; + } + } + $(selector).slider('option', 'value', value); + // Show the control + $(selector + ' .ui-slider-handle').show(); + // Show the value + // Hide the help text + real_input.show().next().remove(); + } + } + }); + if (value == null) { + // Don't show a value until Slider is touched + $(selector + ' .ui-slider-handle').hide(); + // Show help text + real_input.hide() + .after('

' + i18n.slider_help + '

'); + } + // Enable the field before form is submitted + real_input.closest('form').submit(function() { + real_input.prop('disabled', false); + // Normal Submit + return true; + }); +}; + +/** + * Add a Range Slider to a field - used by S3SliderFilter + */ +S3.range_slider = function(selector, min_id, max_id, min_value, max_value, step, values) { + var slider_div = $('#' + selector), + min_input = $('#' + min_id), + max_input = $('#' + max_id); + slider_div.slider({ + range: true, + min: min_value, + max: max_value, + step: step, + values: values, + slide: function(event, ui) { + // Set the value of the real inputs & trigger change event + min_input.val(ui.values[0]); + max_input.val(ui.values[1]).closest('form').trigger('optionChanged'); + } + }); + // Update Slider if INPUTs change + min_input.on('change.slider', function() { + var value = min_input.val(); + if (value == '') { + value = min_value; + } + slider_div.slider('values', 0, value); + }); + max_input.on('change.slider', function() { + var value = max_input.val(); + if (value == '') { + value = max_value; + } + slider_div.slider('values', 1, value); + }); +}; + +// ============================================================================ +/** + * Add a querystring variable to an existing URL and redirect into it. + * It accounts for existing variables and will override an existing one. + * Sample usage: _onchange="S3.reloadWithQueryStringVars({'_language': $(this).val()});") + * used by IFRC layouts.py + */ + +S3.reloadWithQueryStringVars = function(queryStringVars) { + var existingQueryVars = location.search ? location.search.substring(1).split('&') : [], + currentUrl = location.search ? location.href.replace(location.search, '') : location.href, + newQueryVars = {}, + newUrl = currentUrl + '?'; + if (existingQueryVars.length > 0) { + for (var i = 0; i < existingQueryVars.length; i++) { + var pair = existingQueryVars[i].split('='); + newQueryVars[pair[0]] = pair[1]; + } + } + if (queryStringVars) { + for (var queryStringVar in queryStringVars) { + newQueryVars[queryStringVar] = queryStringVars[queryStringVar]; + } + } + if (newQueryVars) { + for (var newQueryVar in newQueryVars) { + newUrl += newQueryVar + '=' + newQueryVars[newQueryVar] + '&'; + } + newUrl = newUrl.substring(0, newUrl.length - 1); + window.location.href = newUrl; + } else { + window.location.href = location.href; + } +}; + +// ============================================================================ +// Module pattern to hide internal vars +(function () { + var deduplication = function() { + // Deduplication event handlers + $('.mark-deduplicate').click(function() { + var url = $('#markDuplicateURL').attr('href'); + if (url) { + $.ajaxS3({ + type: 'POST', + url: url, + data: {}, + dataType: 'JSON', + // gets moved to .done() inside .ajaxS3 + success: function( /* data */ ) { + $('.mark-deduplicate, .unmark-deduplicate, .deduplicate').toggleClass('hide'); + } + }); + } + }); + $('.unmark-deduplicate').click(function() { + var url = $('#markDuplicateURL').attr('href'); + if (url) { + $.ajaxS3({ + type: 'POST', + url: url + '?remove=1', + data: {}, + dataType: 'JSON', + // gets moved to .done() inside .ajaxS3 + success: function( /* data */ ) { + $('.mark-deduplicate, .unmark-deduplicate, .deduplicate').toggleClass('hide'); + } + }); + } + }); + $('.swap-button').click(function() { + // Swap widgets between original and duplicate side + var id = this.id; + var name = id.slice(5); + + var original = $('#original_' + name); + var original_id = original.attr('id'); + var original_name = original.attr('name'); + var original_parent = original.parent().closest('td.mwidget'); + var duplicate = $('#duplicate_' + name); + var duplicate_id = duplicate.attr('id'); + var duplicate_name = duplicate.attr('name'); + var duplicate_parent = duplicate.parent().closest('td.mwidget'); + + // Rename with placeholder names + original.attr('id', 'swap_original_id'); + original.attr('name', 'swap_original_name'); + $('#dummy' + original_id).attr('id', 'dummy_swap_original_id'); + duplicate.attr('id', 'swap_duplicate_id'); + duplicate.attr('name', 'swap_duplicate_name'); + $('#dummy' + duplicate_id).attr('id', 'dummy_swap_duplicate_id'); + + // Swap elements + original_parent.before(''); + var o = original_parent.detach(); + duplicate_parent.before(''); + var d = duplicate_parent.detach(); + $('#swap_original_placeholder').after(d); + $('#swap_original_placeholder').remove(); + $('#swap_duplicate_placeholder').after(o); + $('#swap_duplicate_placeholder').remove(); + + // Rename to original names + original.attr('id', duplicate_id); + original.attr('name', duplicate_name); + $('#dummy_swap_original_id').attr('id', 'dummy' + duplicate_id); + duplicate.attr('id', original_id); + duplicate.attr('name', original_name); + $('#dummy_swap_duplicate').attr('id', 'dummy' + original_id); + }); + }; + + /** + * Used by Themes with a Side-menu + * - for long pages with small side menus, we want the side-menu to always be visible + * BUT + * - for short pages with large side-menus, we don't want the side-menu to scroll + */ + var onResize = function() { + // Default Theme + var side_menu_holder = $('.aside'); + /* Doesn't work on IFRC + if (!side_menu_holder.length) { + // IFRC? + side_menu_holder = $('#left-col'); + } */ + if (side_menu_holder.length) { + // Default Theme + var header = $('#menu_modules'); + if (!header.length) { + // Bootstrap? + header = $('#navbar-inner'); + if (!header.length) { + // IFRC? + header = $('#header'); + } + } + // Default Theme + var side_menu = $('#menu_options'); + /* Doesn't work on IFRC + if (!side_menu.length) { + // IFRC? + side_menu = $('#main-sub-menu'); + } */ + //var footer = $('#footer'); + //if ((header.height() + footer.height() + side_menu.height()) < $(window).height()) { + if ((header.height() + side_menu.height() + 10) < $(window).height()) { + side_menu_holder.css('position', 'fixed'); + $('#content').css('min-height', side_menu.height()); + } else { + side_menu_holder.css('position', 'static'); + } + } + }; + + // ======================================================================== + $(document).ready(function() { + // Web2Py Layer + $('.alert-error').hide().slideDown('slow'); + $('.alert-error').click(function() { + $(this).fadeOut('slow'); + return false; + }); + $('.alert-warning').hide().slideDown('slow'); + $('.alert-warning').click(function() { + $(this).fadeOut('slow'); + return false; + }); + $('.alert-info').hide().slideDown('slow'); + $('.alert-info').click(function() { + $(this).fadeOut('slow'); + return false; + }); + $('.alert-success').hide().slideDown('slow'); + $('.alert-success').click(function() { + $(this).fadeOut('slow'); + return false; + }); + $("input[type='checkbox'].delete").click(function() { + if ((this.checked) && (!confirm(i18n.delete_confirmation))) { + this.checked = false; + } + }); + + // T2 Layer + //try { $('.zoom').fancyZoom( { + // scaleImg: true, + // closeOnClick: true, + // directory: S3.Ap.concat('/static/media') + //}); } catch(e) {} + + // S3 Layer + + // If a form is submitted with errors, this will scroll + // the window to the first form error message + var inputErrorId = $('form .error[id]').eq(0).attr('id'); + if (inputErrorId != undefined) { + var inputName = inputErrorId.replace('__error', ''), + inputId = $('[name=' + inputName + ']').attr('id'), + inputLabel = $('[for=' + inputId + ']'); + try { + window.scrollTo(0, inputLabel.offset().top); + // Prevent first-field focus from scrolling back to + // the top of the form again: + S3.FocusOnFirstField = false; + } catch(e) {} + } + + // dataTables' delete button + // (can't use S3.confirmClick as the buttons haven't yet rendered) + if (S3.interactive) { + $(document).on('click', 'a.delete-btn', function(event) { + if (confirm(i18n.delete_confirmation)) { + return true; + } else { + event.preventDefault(); + return false; + } + }); + + if (S3.FocusOnFirstField != false) { + // Focus On First Field + $('input:text:visible:first').focus(); + } + } + + // Accept comma as thousands separator + $('input.int_amount').keyup(function() { + this.value = this.value.reverse() + .replace(/[^0-9\-,]|\-(?=.)/g, '') + .reverse(); + }); + $('input.float_amount').keyup(function() { + this.value = this.value.reverse() + .replace(/[^0-9\-\.,]|[\-](?=.)|[\.](?=[0-9]*[\.])/g, '') + .reverse(); + }); + + // Auto-capitalize first names + $('input[name="first_name"]').focusout(function() { + this.value = this.value.charAt(0).toLocaleUpperCase() + this.value.substring(1); + }); + + // Ensure that phone fields appear with + at beginning not end in RTL + if (S3.rtl) { + $('.phone-widget').each(function() { + // When form initially renders, ensure the LTR mark is placed at the beginning so that it looks correct + // http://www.fileformat.info/info/unicode/char/200e/index.htm + var value = this.value; + if (value && (value.charAt(0) != '\u200E')) { + this.value = '\u200E' + value; + } + }); + $('.phone-widget').focusout(function() { + var value = this.value; + if (value) { + // When new data is entered then: + // 1. Ensure the LTR mark is placed at the beginning so that it looks correct + // 2. Ensure that if there is a trailing + then it is moved to the beginning + if (value.charAt(0) != '\u200E') { + if (value.charAt(value.length - 1) == '+') { + this.value = '\u200E' + '+' + value.substr(0, value.length - 2); + } else { + this.value = '\u200E' + value; + } + } else { + if (value.charAt(value.length - 1) == '+') { + this.value = '\u200E' + '+' + value.substr(1, value.length - 2); + } + } + } + }); + } + + // ListCreate Views + $('#show-add-btn').click(function() { + // Hide the Button + $('#show-add-btn').hide(10, function() { + // Show the Form + $('#list-add').slideDown('medium'); + // Resize any jQueryUI SelectMenu buttons + $('.select-widget').selectmenu('refresh'); + }); + }); + + // Resizable textareas + $('textarea.resizable:not(.textarea-processed)').each(function() { + var that = $(this); + // Avoid non-processed teasers. + if (that.is(('textarea.teaser:not(.teaser-processed)'))) { + return false; + } + var textarea = that.addClass('textarea-processed'); + var staticOffset = null; + // When wrapping the text area, work around an IE margin bug. See: + // http://jaspan.com/ie-inherited-margin-bug-form-elements-and-haslayout + that.wrap('
') + .parent().append($('
').mousedown(startDrag)); + var grippie = $('div.grippie', that.parent())[0]; + grippie.style.marginRight = (grippie.offsetWidth - that[0].offsetWidth) + 'px'; + function startDrag(e) { + staticOffset = textarea.height() - e.pageY; + textarea.css('opacity', 0.25); + $(document).mousemove(performDrag).mouseup(endDrag); + return false; + } + function performDrag(e) { + textarea.height(Math.max(32, staticOffset + e.pageY) + 'px'); + return false; + } + function endDrag( /* e */ ) { + $(document).unbind('mousemove', performDrag).unbind('mouseup', endDrag); + textarea.css('opacity', 1); + } + return true; + }); + + // IE6 non anchor hover hack + $('#modulenav .hoverable').hover( + function() { + $(this).addClass('hovered'); + }, + function() { + $(this).removeClass('hovered'); + } + ); + + // Menu popups (works in IE6) + $('#modulenav li').hover( + function() { + var header_width = $(this).width(); + var popup_width = $('ul', this).width(); + if (popup_width !== null){ + if (popup_width < header_width){ + $('ul', this).css({ + 'width': header_width.toString() + 'px' + }); + } + } + $('ul', this).css('display', 'block'); + }, + function() { + $('ul', this).css('display', 'none'); + } + ); + + // Event Handlers for the page + S3.redraw(); + + // Popovers (Bootstrap themes only) + if (typeof($.fn.popover) != 'undefined') { + // Applies to elements created after $(document).ready + $('body').popover({ + selector: '.s3-popover', + trigger: 'hover', + placement: 'left' + }); + } + + // Handle Page Resizes + onResize(); + $(window).bind('resize', onResize); + + // De-duplication Event Handlers + deduplication(); + + // Timezone and UTC Offset + var anyform = $('form'); + if (anyform.length) { + var now = new Date(), + tz; + anyform.append(""); + try { + tz = Intl.DateTimeFormat().resolvedOptions().timeZone; + } catch(e) { + // not supported + } + if (tz) { + anyform.append(""); + } + } + + // Social Media 'share' buttons + if ($('#socialmedia_share').length > 0) { + // DIV exists (deployment_setting on) + var currenturl = document.location.href; + // Linked-In + $('#socialmedia_share').append(""); + // Twitter + $('#socialmedia_share').append(""); + // Facebook + $('#socialmedia_share').append(""); + } + + // Form toggle (e.g. S3Profile update-form) + $('.form-toggle').click(function() { + var self = $(this), + hidden = $(this).data('hidden'); + hidden = hidden && hidden != 'False'; + self.data('hidden', hidden ? false : true) + .siblings().slideToggle('medium', function() { + self.children('span').each(function() { + $(this).text(hidden ? $(this).data('off') : $(this).data('on')) + .siblings().toggle(); + }); + }); + }); + + // Options Menu Toggle on mobile + $('#menu-options-toggle,#list-filter-toggle').on('click', function(e) { + e.stopPropagation(); + var $this = $(this); + var status = $this.data('status'), + menu; + if (this.id == 'menu-options-toggle') { + menu = $('#menu-options'); + } else { + menu = $('#list-filter'); + } + if (status == 'off') { + menu.hide().removeClass('hide-for-small').slideDown(400, function() { + $this.data('status', 'on').text($this.data('on')); + }); + } else { + menu.slideUp(400, function() { + menu.addClass('hide-for-small').show(); + $this.data('status', 'off').text($this.data('off')); + }); + } + }); + + /** + * Click-handler for s3-download-buttons: + * + * - any action item with a class 's3-download-button' and + * a 'url' data property (data-url) + * + * - for download of server-generated attachments, e.g. XLS or PDF + * + * - downloads the target document in a hidden iframe, which, if + * the file is sent with content-disposition "attachment", will + * only open the file dialog and nothing else + * + * - if this fails, the response will be opened in a modal dialog + * (JSON messages will be handled with a simple alert-box, though) + */ + $('.s3-download-button').on('click', function(e) { + + // Do nothing else + e.preventDefault(); + e.stopPropagation(); + + var url = $(this).data('url'); + if (!url) { + return; + } + + // Re-use it if it already exists + var iframe = document.getElementById("s3-download"); + if (iframe == null) { + iframe = document.createElement('iframe'); + iframe.id = "s3-download"; + iframe.style.visibility = 'hidden'; + document.body.appendChild(iframe); + } + + $('#s3-download').off('load').on('load', function() { + // This event is only fired when contents was loaded into the + // hidden iframe rather than downloaded as attachment, which + // should only happen if there was some kind of error + var message, + self = $(this); + try { + // Try to parse the JSON message + message = JSON.parse(this.contentDocument.body.textContent).message; + } catch(e) { + // No JSON message => show iframe contents as-is in a modal + self.dialog({ + title: 'Download failed', + width: 500, + height: 300, + close: function() { + self.attr('src', '').remove(); + } + }).css({ + visibility: 'visible', + width: '100%' + }); + return; + } + alert(message); + }); + + iframe.src = url; + return false; + }); + }); + +}()); +// ============================================================================ diff --git a/static/scripts/S3/s3.gis.js b/static/scripts/S3/s3.gis.js index 41f51c1779..b9f57e2879 100644 --- a/static/scripts/S3/s3.gis.js +++ b/static/scripts/S3/s3.gis.js @@ -31,1734 +31,182 @@ OpenLayers.ProxyHost = S3.Ap.concat('/gis/proxy?url='); * https://git.osgeo.org/gitea/GDI-RP/MapSkinner/src/branch/pre_master/service/helper/epsg_api.py */ S3.gis.yx = [ - 4326, - 4258, - 31466, - 31467, - 31468, - 31469, - 2166, - 2167, - 2168, - 2036, - 2044, - 2045, - 2065, - 2081, - 2082, - 2083, - 2085, - 2086, - 2091, - 2092, - 2093, - 2096, - 2097, - 2098, - 2105, - 2106, - 2107, - 2108, - 2109, - 2110, - 2111, - 2112, - 2113, - 2114, - 2115, - 2116, - 2117, - 2118, - 2119, - 2120, - 2121, - 2122, - 2123, - 2124, - 2125, - 2126, - 2127, - 2128, - 2129, - 2130, - 2131, - 2132, - 2169, - 2170, - 2171, - 2172, - 2173, - 2174, - 2175, - 2176, - 2177, - 2178, - 2179, - 2180, - 2193, - 2199, - 2200, - 2206, - 2207, - 2208, - 2209, - 2210, - 2211, - 2212, - 2319, - 2320, - 2321, - 2322, - 2323, - 2324, - 2325, - 2326, - 2327, - 2328, - 2329, - 2330, - 2331, - 2332, - 2333, - 2334, - 2335, - 2336, - 2337, - 2338, - 2339, - 2340, - 2341, - 2342, - 2343, - 2344, - 2345, - 2346, - 2347, - 2348, - 2349, - 2350, - 2351, - 2352, - 2353, - 2354, - 2355, - 2356, - 2357, - 2358, - 2359, - 2360, - 2361, - 2362, - 2363, - 2364, - 2365, - 2366, - 2367, - 2368, - 2369, - 2370, - 2371, - 2372, - 2373, - 2374, - 2375, - 2376, - 2377, - 2378, - 2379, - 2380, - 2381, - 2382, - 2383, - 2384, - 2385, - 2386, - 2387, - 2388, - 2389, - 2390, - 2391, - 2392, - 2393, - 2394, - 2395, - 2396, - 2397, - 2398, - 2399, - 2400, - 2401, - 2402, - 2403, - 2404, - 2405, - 2406, - 2407, - 2408, - 2409, - 2410, - 2411, - 2412, - 2413, - 2414, - 2415, - 2416, - 2417, - 2418, - 2419, - 2420, - 2421, - 2422, - 2423, - 2424, - 2425, - 2426, - 2427, - 2428, - 2429, - 2430, - 2431, - 2432, - 2433, - 2434, - 2435, - 2436, - 2437, - 2438, - 2439, - 2440, - 2441, - 2442, - 2443, - 2444, - 2445, - 2446, - 2447, - 2448, - 2449, - 2450, - 2451, - 2452, - 2453, - 2454, - 2455, - 2456, - 2457, - 2458, - 2459, - 2460, - 2461, - 2462, - 2463, - 2464, - 2465, - 2466, - 2467, - 2468, - 2469, - 2470, - 2471, - 2472, - 2473, - 2474, - 2475, - 2476, - 2477, - 2478, - 2479, - 2480, - 2481, - 2482, - 2483, - 2484, - 2485, - 2486, - 2487, - 2488, - 2489, - 2490, - 2491, - 2492, - 2493, - 2494, - 2495, - 2496, - 2497, - 2498, - 2499, - 2500, - 2501, - 2502, - 2503, - 2504, - 2505, - 2506, - 2507, - 2508, - 2509, - 2510, - 2511, - 2512, - 2513, - 2514, - 2515, - 2516, - 2517, - 2518, - 2519, - 2520, - 2521, - 2522, - 2523, - 2524, - 2525, - 2526, - 2527, - 2528, - 2529, - 2530, - 2531, - 2532, - 2533, - 2534, - 2535, - 2536, - 2537, - 2538, - 2539, - 2540, - 2541, - 2542, - 2543, - 2544, - 2545, - 2546, - 2547, - 2548, - 2549, - 2551, - 2552, - 2553, - 2554, - 2555, - 2556, - 2557, - 2558, - 2559, - 2560, - 2561, - 2562, - 2563, - 2564, - 2565, - 2566, - 2567, - 2568, - 2569, - 2570, - 2571, - 2572, - 2573, - 2574, - 2575, - 2576, - 2577, - 2578, - 2579, - 2580, - 2581, - 2582, - 2583, - 2584, - 2585, - 2586, - 2587, - 2588, - 2589, - 2590, - 2591, - 2592, - 2593, - 2594, - 2595, - 2596, - 2597, - 2598, - 2599, - 2600, - 2601, - 2602, - 2603, - 2604, - 2605, - 2606, - 2607, - 2608, - 2609, - 2610, - 2611, - 2612, - 2613, - 2614, - 2615, - 2616, - 2617, - 2618, - 2619, - 2620, - 2621, - 2622, - 2623, - 2624, - 2625, - 2626, - 2627, - 2628, - 2629, - 2630, - 2631, - 2632, - 2633, - 2634, - 2635, - 2636, - 2637, - 2638, - 2639, - 2640, - 2641, - 2642, - 2643, - 2644, - 2645, - 2646, - 2647, - 2648, - 2649, - 2650, - 2651, - 2652, - 2653, - 2654, - 2655, - 2656, - 2657, - 2658, - 2659, - 2660, - 2661, - 2662, - 2663, - 2664, - 2665, - 2666, - 2667, - 2668, - 2669, - 2670, - 2671, - 2672, - 2673, - 2674, - 2675, - 2676, - 2677, - 2678, - 2679, - 2680, - 2681, - 2682, - 2683, - 2684, - 2685, - 2686, - 2687, - 2688, - 2689, - 2690, - 2691, - 2692, - 2693, - 2694, - 2695, - 2696, - 2697, - 2698, - 2699, - 2700, - 2701, - 2702, - 2703, - 2704, - 2705, - 2706, - 2707, - 2708, - 2709, - 2710, - 2711, - 2712, - 2713, - 2714, - 2715, - 2716, - 2717, - 2718, - 2719, - 2720, - 2721, - 2722, - 2723, - 2724, - 2725, - 2726, - 2727, - 2728, - 2729, - 2730, - 2731, - 2732, - 2733, - 2734, - 2735, - 2738, - 2739, - 2740, - 2741, - 2742, - 2743, - 2744, - 2745, - 2746, - 2747, - 2748, - 2749, - 2750, - 2751, - 2752, - 2753, - 2754, - 2755, - 2756, - 2757, - 2758, - 2935, - 2936, - 2937, - 2938, - 2939, - 2940, - 2941, - 2953, - 2963, - 3006, - 3007, - 3008, - 3009, - 3010, - 3011, - 3012, - 3013, - 3014, - 3015, - 3016, - 3017, - 3018, - 3019, - 3020, - 3021, - 3022, - 3023, - 3024, - 3025, - 3026, - 3027, - 3028, - 3029, - 3030, - 3034, - 3035, - 3038, - 3039, - 3040, - 3041, - 3042, - 3043, - 3044, - 3045, - 3046, - 3047, - 3048, - 3049, - 3050, - 3051, - 3058, - 3059, - 3068, - 3114, - 3115, - 3116, - 3117, - 3118, - 3120, - 3126, - 3127, - 3128, - 3129, - 3130, - 3131, - 3132, - 3133, - 3134, - 3135, - 3136, - 3137, - 3138, - 3139, - 3140, - 3146, - 3147, - 3150, - 3151, - 3152, - 3300, - 3301, - 3328, - 3329, - 3330, - 3331, - 3332, - 3333, - 3334, - 3335, - 3346, - 3350, - 3351, - 3352, - 3366, - 3386, - 3387, - 3388, - 3389, - 3390, - 3396, - 3397, - 3398, - 3399, - 3407, - 3414, - 3416, - 3764, - 3788, - 3789, - 3790, - 3791, - 3793, - 3795, - 3796, - 3819, - 3821, - 3823, - 3824, - 3833, - 3834, - 3835, - 3836, - 3837, - 3838, - 3839, - 3840, - 3841, - 3842, - 3843, - 3844, - 3845, - 3846, - 3847, - 3848, - 3849, - 3850, - 3851, - 3852, - 3854, - 3873, - 3874, - 3875, - 3876, - 3877, - 3878, - 3879, - 3880, - 3881, - 3882, - 3883, - 3884, - 3885, - 3888, - 3889, - 3906, - 3907, - 3908, - 3909, - 3910, - 3911, - 4001, - 4002, - 4003, - 4004, - 4005, - 4006, - 4007, - 4008, - 4009, - 4010, - 4011, - 4012, - 4013, - 4014, - 4015, - 4016, - 4017, - 4018, - 4019, - 4020, - 4021, - 4022, - 4023, - 4024, - 4025, - 4026, - 4027, - 4028, - 4029, - 4030, - 4031, - 4032, - 4033, - 4034, - 4035, - 4036, - 4037, - 4038, - 4040, - 4041, - 4042, - 4043, - 4044, - 4045, - 4046, - 4047, - 4052, - 4053, - 4054, - 4055, - 4074, - 4075, - 4080, - 4081, - 4120, - 4121, - 4122, - 4123, - 4124, - 4125, - 4126, - 4127, - 4128, - 4129, - 4130, - 4131, - 4132, - 4133, - 4134, - 4135, - 4136, - 4137, - 4138, - 4139, - 4140, - 4141, - 4142, - 4143, - 4144, - 4145, - 4146, - 4147, - 4148, - 4149, - 4150, - 4151, - 4152, - 4153, - 4154, - 4155, - 4156, - 4157, - 4158, - 4159, - 4160, - 4161, - 4162, - 4163, - 4164, - 4165, - 4166, - 4167, - 4168, - 4169, - 4170, - 4171, - 4172, - 4173, - 4174, - 4175, - 4176, - 4178, - 4179, - 4180, - 4181, - 4182, - 4183, - 4184, - 4185, - 4188, - 4189, - 4190, - 4191, - 4192, - 4193, - 4194, - 4195, - 4196, - 4197, - 4198, - 4199, - 4200, - 4201, - 4202, - 4203, - 4204, - 4205, - 4206, - 4207, - 4208, - 4209, - 4210, - 4211, - 4212, - 4213, - 4214, - 4215, - 4216, - 4218, - 4219, - 4220, - 4221, - 4222, - 4223, - 4224, - 4225, - 4226, - 4227, - 4228, - 4229, - 4230, - 4231, - 4232, - 4233, - 4234, - 4235, - 4236, - 4237, - 4238, - 4239, - 4240, - 4241, - 4242, - 4243, - 4244, - 4245, - 4246, - 4247, - 4248, - 4249, - 4250, - 4251, - 4252, - 4253, - 4254, - 4255, - 4256, - 4257, - 4259, - 4260, - 4261, - 4262, - 4263, - 4264, - 4265, - 4266, - 4267, - 4268, - 4269, - 4270, - 4271, - 4272, - 4273, - 4274, - 4275, - 4276, - 4277, - 4278, - 4279, - 4280, - 4281, - 4282, - 4283, - 4284, - 4285, - 4286, - 4287, - 4288, - 4289, - 4291, - 4292, - 4293, - 4294, - 4295, - 4296, - 4297, - 4298, - 4299, - 4300, - 4301, - 4302, - 4303, - 4304, - 4306, - 4307, - 4308, - 4309, - 4310, - 4311, - 4312, - 4313, - 4314, - 4315, - 4316, - 4317, - 4318, - 4319, - 4322, - 4324, - 4327, - 4329, - 4339, - 4341, - 4343, - 4345, - 4347, - 4349, - 4351, - 4353, - 4355, - 4357, - 4359, - 4361, - 4363, - 4365, - 4367, - 4369, - 4371, - 4373, - 4375, - 4377, - 4379, - 4381, - 4383, - 4386, - 4388, - 4417, - 4434, - 4463, - 4466, - 4469, - 4470, - 4472, - 4475, - 4480, - 4482, - 4483, - 4490, - 4491, - 4492, - 4493, - 4494, - 4495, - 4496, - 4497, - 4498, - 4499, - 4500, - 4501, - 4502, - 4503, - 4504, - 4505, - 4506, - 4507, - 4508, - 4509, - 4510, - 4511, - 4512, - 4513, - 4514, - 4515, - 4516, - 4517, - 4518, - 4519, - 4520, - 4521, - 4522, - 4523, - 4524, - 4525, - 4526, - 4527, - 4528, - 4529, - 4530, - 4531, - 4532, - 4533, - 4534, - 4535, - 4536, - 4537, - 4538, - 4539, - 4540, - 4541, - 4542, - 4543, - 4544, - 4545, - 4546, - 4547, - 4548, - 4549, - 4550, - 4551, - 4552, - 4553, - 4554, - 4555, - 4557, - 4558, - 4568, - 4569, - 4570, - 4571, - 4572, - 4573, - 4574, - 4575, - 4576, - 4577, - 4578, - 4579, - 4580, - 4581, - 4582, - 4583, - 4584, - 4585, - 4586, - 4587, - 4588, - 4589, - 4600, - 4601, - 4602, - 4603, - 4604, - 4605, - 4606, - 4607, - 4608, - 4609, - 4610, - 4611, - 4612, - 4613, - 4614, - 4615, - 4616, - 4617, - 4618, - 4619, - 4620, - 4621, - 4622, - 4623, - 4624, - 4625, - 4626, - 4627, - 4628, - 4629, - 4630, - 4631, - 4632, - 4633, - 4634, - 4635, - 4636, - 4637, - 4638, - 4639, - 4640, - 4641, - 4642, - 4643, - 4644, - 4645, - 4646, - 4652, - 4653, - 4654, - 4655, - 4656, - 4657, - 4658, - 4659, - 4660, - 4661, - 4662, - 4663, - 4664, - 4665, - 4666, - 4667, - 4668, - 4669, - 4670, - 4671, - 4672, - 4673, - 4674, - 4675, - 4676, - 4677, - 4678, - 4679, - 4680, - 4681, - 4682, - 4683, - 4684, - 4685, - 4686, - 4687, - 4688, - 4689, - 4690, - 4691, - 4692, - 4693, - 4694, - 4695, - 4696, - 4697, - 4698, - 4699, - 4700, - 4701, - 4702, - 4703, - 4704, - 4705, - 4706, - 4707, - 4708, - 4709, - 4710, - 4711, - 4712, - 4713, - 4714, - 4715, - 4716, - 4717, - 4718, - 4719, - 4720, - 4721, - 4722, - 4723, - 4724, - 4725, - 4726, - 4727, - 4728, - 4729, - 4730, - 4731, - 4732, - 4733, - 4734, - 4735, - 4736, - 4737, - 4738, - 4739, - 4740, - 4741, - 4742, - 4743, - 4744, - 4745, - 4746, - 4747, - 4748, - 4749, - 4750, - 4751, - 4752, - 4753, - 4754, - 4755, - 4756, - 4757, - 4758, - 4759, - 4760, - 4761, - 4762, - 4763, - 4764, - 4765, - 4766, - 4767, - 4768, - 4769, - 4770, - 4771, - 4772, - 4773, - 4774, - 4775, - 4776, - 4777, - 4778, - 4779, - 4780, - 4781, - 4782, - 4783, - 4784, - 4785, - 4786, - 4787, - 4788, - 4789, - 4790, - 4791, - 4792, - 4793, - 4794, - 4795, - 4796, - 4797, - 4798, - 4799, - 4800, - 4801, - 4802, - 4803, - 4804, - 4805, - 4806, - 4807, - 4808, - 4809, - 4810, - 4811, - 4812, - 4813, - 4814, - 4815, - 4816, - 4817, - 4818, - 4819, - 4820, - 4821, - 4822, - 4823, - 4824, - 4839, - 4855, - 4856, - 4857, - 4858, - 4859, - 4860, - 4861, - 4862, - 4863, - 4864, - 4865, - 4866, - 4867, - 4868, - 4869, - 4870, - 4871, - 4872, - 4873, - 4874, - 4875, - 4876, - 4877, - 4878, - 4879, - 4880, - 4883, - 4885, - 4887, - 4889, - 4891, - 4893, - 4895, - 4898, - 4900, - 4901, - 4902, - 4903, - 4904, - 4907, - 4909, - 4921, - 4923, - 4925, - 4927, - 4929, - 4931, - 4933, - 4935, - 4937, - 4939, - 4941, - 4943, - 4945, - 4947, - 4949, - 4951, - 4953, - 4955, - 4957, - 4959, - 4961, - 4963, - 4965, - 4967, - 4969, - 4971, - 4973, - 4975, - 4977, - 4979, - 4981, - 4983, - 4985, - 4987, - 4989, - 4991, - 4993, - 4995, - 4997, - 4999, - 5012, - 5013, - 5017, - 5048, - 5105, - 5106, - 5107, - 5108, - 5109, - 5110, - 5111, - 5112, - 5113, - 5114, - 5115, - 5116, - 5117, - 5118, - 5119, - 5120, - 5121, - 5122, - 5123, - 5124, - 5125, - 5126, - 5127, - 5128, - 5129, - 5130, - 5132, - 5167, - 5168, - 5169, - 5170, - 5171, - 5172, - 5173, - 5174, - 5175, - 5176, - 5177, - 5178, - 5179, - 5180, - 5181, - 5182, - 5183, - 5184, - 5185, - 5186, - 5187, - 5188, - 5224, - 5228, - 5229, - 5233, - 5245, - 5246, - 5251, - 5252, - 5253, - 5254, - 5255, - 5256, - 5257, - 5258, - 5259, - 5263, - 5264, - 5269, - 5270, - 5271, - 5272, - 5273, - 5274, - 5275, - 5801, - 5802, - 5803, - 5804, - 5808, - 5809, - 5810, - 5811, - 5812, - 5813, - 5814, - 5815, - 5816, - 20004, - 20005, - 20006, - 20007, - 20008, - 20009, - 20010, - 20011, - 20012, - 20013, - 20014, - 20015, - 20016, - 20017, - 20018, - 20019, - 20020, - 20021, - 20022, - 20023, - 20024, - 20025, - 20026, - 20027, - 20028, - 20029, - 20030, - 20031, - 20032, - 20064, - 20065, - 20066, - 20067, - 20068, - 20069, - 20070, - 20071, - 20072, - 20073, - 20074, - 20075, - 20076, - 20077, - 20078, - 20079, - 20080, - 20081, - 20082, - 20083, - 20084, - 20085, - 20086, - 20087, - 20088, - 20089, - 20090, - 20091, - 20092, - 21413, - 21414, - 21415, - 21416, - 21417, - 21418, - 21419, - 21420, - 21421, - 21422, - 21423, - 21453, - 21454, - 21455, - 21456, - 21457, - 21458, - 21459, - 21460, - 21461, - 21462, - 21463, - 21473, - 21474, - 21475, - 21476, - 21477, - 21478, - 21479, - 21480, - 21481, - 21482, - 21483, - 21896, - 21897, - 21898, - 21899, - 22171, - 22172, - 22173, - 22174, - 22175, - 22176, - 22177, - 22181, - 22182, - 22183, - 22184, - 22185, - 22186, - 22187, - 22191, - 22192, - 22193, - 22194, - 22195, - 22196, - 22197, - 25884, - 27205, - 27206, - 27207, - 27208, - 27209, - 27210, - 27211, - 27212, - 27213, - 27214, - 27215, - 27216, - 27217, - 27218, - 27219, - 27220, - 27221, - 27222, - 27223, - 27224, - 27225, - 27226, - 27227, - 27228, - 27229, - 27230, - 27231, - 27232, - 27391, - 27392, - 27393, - 27394, - 27395, - 27396, - 27397, - 27398, - 27492, - 28402, - 28403, - 28404, - 28405, - 28406, - 28407, - 28408, - 28409, - 28410, - 28411, - 28412, - 28413, - 28414, - 28415, - 28416, - 28417, - 28418, - 28419, - 28420, - 28421, - 28422, - 28423, - 28424, - 28425, - 28426, - 28427, - 28428, - 28429, - 28430, - 28431, - 28432, - 28462, - 28463, - 28464, - 28465, - 28466, - 28467, - 28468, - 28469, - 28470, - 28471, - 28472, - 28473, - 28474, - 28475, - 28476, - 28477, - 28478, - 28479, - 28480, - 28481, - 28482, - 28483, - 28484, - 28485, - 28486, - 28487, - 28488, - 28489, - 28490, - 28491, - 28492, - 29701, - 29702, - 30161, - 30162, - 30163, - 30164, - 30165, - 30166, - 30167, - 30168, - 30169, - 30170, - 30171, - 30172, - 30173, - 30174, - 30175, - 30176, - 30177, - 30178, - 30179, - 30800, - 31251, - 31252, - 31253, - 31254, - 31255, - 31256, - 31257, - 31258, - 31259, - 31275, - 31276, - 31277, - 31278, - 31279, - 31281, - 31282, - 31283, - 31284, - 31285, - 31286, - 31287, - 31288, - 31289, - 31290, - 31700 + 2036, 2044, 2045, 2065, 2081, 2082, 2083, 2085, 2086, 2091, 2092, 2093, + 2096, 2097, 2098, 2105, 2106, 2107, 2108, 2109, 2110, 2111, 2112, 2113, + 2114, 2115, 2116, 2117, 2118, 2119, 2120, 2121, 2122, 2123, 2124, 2125, + 2126, 2127, 2128, 2129, 2130, 2131, 2132, 2166, 2167, 2168, 2169, 2170, + 2171, 2172, 2173, 2174, 2175, 2176, 2177, 2178, 2179, 2180, 2193, 2199, + 2200, 2206, 2207, 2208, 2209, 2210, 2211, 2212, 2319, 2320, 2321, 2322, + 2323, 2324, 2325, 2326, 2327, 2328, 2329, 2330, 2331, 2332, 2333, 2334, + 2335, 2336, 2337, 2338, 2339, 2340, 2341, 2342, 2343, 2344, 2345, 2346, + 2347, 2348, 2349, 2350, 2351, 2352, 2353, 2354, 2355, 2356, 2357, 2358, + 2359, 2360, 2361, 2362, 2363, 2364, 2365, 2366, 2367, 2368, 2369, 2370, + 2371, 2372, 2373, 2374, 2375, 2376, 2377, 2378, 2379, 2380, 2381, 2382, + 2383, 2384, 2385, 2386, 2387, 2388, 2389, 2390, 2391, 2392, 2393, 2394, + 2395, 2396, 2397, 2398, 2399, 2400, 2401, 2402, 2403, 2404, 2405, 2406, + 2407, 2408, 2409, 2410, 2411, 2412, 2413, 2414, 2415, 2416, 2417, 2418, + 2419, 2420, 2421, 2422, 2423, 2424, 2425, 2426, 2427, 2428, 2429, 2430, + 2431, 2432, 2433, 2434, 2435, 2436, 2437, 2438, 2439, 2440, 2441, 2442, + 2443, 2444, 2445, 2446, 2447, 2448, 2449, 2450, 2451, 2452, 2453, 2454, + 2455, 2456, 2457, 2458, 2459, 2460, 2461, 2462, 2463, 2464, 2465, 2466, + 2467, 2468, 2469, 2470, 2471, 2472, 2473, 2474, 2475, 2476, 2477, 2478, + 2479, 2480, 2481, 2482, 2483, 2484, 2485, 2486, 2487, 2488, 2489, 2490, + 2491, 2492, 2493, 2494, 2495, 2496, 2497, 2498, 2499, 2500, 2501, 2502, + 2503, 2504, 2505, 2506, 2507, 2508, 2509, 2510, 2511, 2512, 2513, 2514, + 2515, 2516, 2517, 2518, 2519, 2520, 2521, 2522, 2523, 2524, 2525, 2526, + 2527, 2528, 2529, 2530, 2531, 2532, 2533, 2534, 2535, 2536, 2537, 2538, + 2539, 2540, 2541, 2542, 2543, 2544, 2545, 2546, 2547, 2548, 2549, 2551, + 2552, 2553, 2554, 2555, 2556, 2557, 2558, 2559, 2560, 2561, 2562, 2563, + 2564, 2565, 2566, 2567, 2568, 2569, 2570, 2571, 2572, 2573, 2574, 2575, + 2576, 2577, 2578, 2579, 2580, 2581, 2582, 2583, 2584, 2585, 2586, 2587, + 2588, 2589, 2590, 2591, 2592, 2593, 2594, 2595, 2596, 2597, 2598, 2599, + 2600, 2601, 2602, 2603, 2604, 2605, 2606, 2607, 2608, 2609, 2610, 2611, + 2612, 2613, 2614, 2615, 2616, 2617, 2618, 2619, 2620, 2621, 2622, 2623, + 2624, 2625, 2626, 2627, 2628, 2629, 2630, 2631, 2632, 2633, 2634, 2635, + 2636, 2637, 2638, 2639, 2640, 2641, 2642, 2643, 2644, 2645, 2646, 2647, + 2648, 2649, 2650, 2651, 2652, 2653, 2654, 2655, 2656, 2657, 2658, 2659, + 2660, 2661, 2662, 2663, 2664, 2665, 2666, 2667, 2668, 2669, 2670, 2671, + 2672, 2673, 2674, 2675, 2676, 2677, 2678, 2679, 2680, 2681, 2682, 2683, + 2684, 2685, 2686, 2687, 2688, 2689, 2690, 2691, 2692, 2693, 2694, 2695, + 2696, 2697, 2698, 2699, 2700, 2701, 2702, 2703, 2704, 2705, 2706, 2707, + 2708, 2709, 2710, 2711, 2712, 2713, 2714, 2715, 2716, 2717, 2718, 2719, + 2720, 2721, 2722, 2723, 2724, 2725, 2726, 2727, 2728, 2729, 2730, 2731, + 2732, 2733, 2734, 2735, 2738, 2739, 2740, 2741, 2742, 2743, 2744, 2745, + 2746, 2747, 2748, 2749, 2750, 2751, 2752, 2753, 2754, 2755, 2756, 2757, + 2758, 2935, 2936, 2937, 2938, 2939, 2940, 2941, 2953, 2963, 3006, 3007, + 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3015, 3016, 3017, 3018, 3019, + 3020, 3021, 3022, 3023, 3024, 3025, 3026, 3027, 3028, 3029, 3030, 3034, + 3035, 3038, 3039, 3040, 3041, 3042, 3043, 3044, 3045, 3046, 3047, 3048, + 3049, 3050, 3051, 3058, 3059, 3068, 3114, 3115, 3116, 3117, 3118, 3120, + 3126, 3127, 3128, 3129, 3130, 3131, 3132, 3133, 3134, 3135, 3136, 3137, + 3138, 3139, 3140, 3146, 3147, 3150, 3151, 3152, 3300, 3301, 3328, 3329, + 3330, 3331, 3332, 3333, 3334, 3335, 3346, 3350, 3351, 3352, 3366, 3386, + 3387, 3388, 3389, 3390, 3396, 3397, 3398, 3399, 3407, 3414, 3416, 3764, + 3788, 3789, 3790, 3791, 3793, 3795, 3796, 3819, 3821, 3823, 3824, 3833, + 3834, 3835, 3836, 3837, 3838, 3839, 3840, 3841, 3842, 3843, 3844, 3845, + 3846, 3847, 3848, 3849, 3850, 3851, 3852, 3854, 3873, 3874, 3875, 3876, + 3877, 3878, 3879, 3880, 3881, 3882, 3883, 3884, 3885, 3888, 3889, 3906, + 3907, 3908, 3909, 3910, 3911, 4001, 4002, 4003, 4004, 4005, 4006, 4007, + 4008, 4009, 4010, 4011, 4012, 4013, 4014, 4015, 4016, 4017, 4018, 4019, + 4020, 4021, 4022, 4023, 4024, 4025, 4026, 4027, 4028, 4029, 4030, 4031, + 4032, 4033, 4034, 4035, 4036, 4037, 4038, 4040, 4041, 4042, 4043, 4044, + 4045, 4046, 4047, 4052, 4053, 4054, 4055, 4074, 4075, 4080, 4081, 4120, + 4121, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131, 4132, + 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4142, 4143, 4144, + 4145, 4146, 4147, 4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, 4156, + 4157, 4158, 4159, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4167, 4168, + 4169, 4170, 4171, 4172, 4173, 4174, 4175, 4176, 4178, 4179, 4180, 4181, + 4182, 4183, 4184, 4185, 4188, 4189, 4190, 4191, 4192, 4193, 4194, 4195, + 4196, 4197, 4198, 4199, 4200, 4201, 4202, 4203, 4204, 4205, 4206, 4207, + 4208, 4209, 4210, 4211, 4212, 4213, 4214, 4215, 4216, 4218, 4219, 4220, + 4221, 4222, 4223, 4224, 4225, 4226, 4227, 4228, 4229, 4230, 4231, 4232, + 4233, 4234, 4235, 4236, 4237, 4238, 4239, 4240, 4241, 4242, 4243, 4244, + 4245, 4246, 4247, 4248, 4249, 4250, 4251, 4252, 4253, 4254, 4255, 4256, + 4257, 4258, 4259, 4260, 4261, 4262, 4263, 4264, 4265, 4266, 4267, 4268, + 4269, 4270, 4271, 4272, 4273, 4274, 4275, 4276, 4277, 4278, 4279, 4280, + 4281, 4282, 4283, 4284, 4285, 4286, 4287, 4288, 4289, 4291, 4292, 4293, + 4294, 4295, 4296, 4297, 4298, 4299, 4300, 4301, 4302, 4303, 4304, 4306, + 4307, 4308, 4309, 4310, 4311, 4312, 4313, 4314, 4315, 4316, 4317, 4318, + 4319, 4322, 4324, 4326, 4327, 4329, 4339, 4341, 4343, 4345, 4347, 4349, + 4351, 4353, 4355, 4357, 4359, 4361, 4363, 4365, 4367, 4369, 4371, 4373, + 4375, 4377, 4379, 4381, 4383, 4386, 4388, 4417, 4434, 4463, 4466, 4469, + 4470, 4472, 4475, 4480, 4482, 4483, 4490, 4491, 4492, 4493, 4494, 4495, + 4496, 4497, 4498, 4499, 4500, 4501, 4502, 4503, 4504, 4505, 4506, 4507, + 4508, 4509, 4510, 4511, 4512, 4513, 4514, 4515, 4516, 4517, 4518, 4519, + 4520, 4521, 4522, 4523, 4524, 4525, 4526, 4527, 4528, 4529, 4530, 4531, + 4532, 4533, 4534, 4535, 4536, 4537, 4538, 4539, 4540, 4541, 4542, 4543, + 4544, 4545, 4546, 4547, 4548, 4549, 4550, 4551, 4552, 4553, 4554, 4555, + 4557, 4558, 4568, 4569, 4570, 4571, 4572, 4573, 4574, 4575, 4576, 4577, + 4578, 4579, 4580, 4581, 4582, 4583, 4584, 4585, 4586, 4587, 4588, 4589, + 4600, 4601, 4602, 4603, 4604, 4605, 4606, 4607, 4608, 4609, 4610, 4611, + 4612, 4613, 4614, 4615, 4616, 4617, 4618, 4619, 4620, 4621, 4622, 4623, + 4624, 4625, 4626, 4627, 4628, 4629, 4630, 4631, 4632, 4633, 4634, 4635, + 4636, 4637, 4638, 4639, 4640, 4641, 4642, 4643, 4644, 4645, 4646, 4652, + 4653, 4654, 4655, 4656, 4657, 4658, 4659, 4660, 4661, 4662, 4663, 4664, + 4665, 4666, 4667, 4668, 4669, 4670, 4671, 4672, 4673, 4674, 4675, 4676, + 4677, 4678, 4679, 4680, 4681, 4682, 4683, 4684, 4685, 4686, 4687, 4688, + 4689, 4690, 4691, 4692, 4693, 4694, 4695, 4696, 4697, 4698, 4699, 4700, + 4701, 4702, 4703, 4704, 4705, 4706, 4707, 4708, 4709, 4710, 4711, 4712, + 4713, 4714, 4715, 4716, 4717, 4718, 4719, 4720, 4721, 4722, 4723, 4724, + 4725, 4726, 4727, 4728, 4729, 4730, 4731, 4732, 4733, 4734, 4735, 4736, + 4737, 4738, 4739, 4740, 4741, 4742, 4743, 4744, 4745, 4746, 4747, 4748, + 4749, 4750, 4751, 4752, 4753, 4754, 4755, 4756, 4757, 4758, 4759, 4760, + 4761, 4762, 4763, 4764, 4765, 4766, 4767, 4768, 4769, 4770, 4771, 4772, + 4773, 4774, 4775, 4776, 4777, 4778, 4779, 4780, 4781, 4782, 4783, 4784, + 4785, 4786, 4787, 4788, 4789, 4790, 4791, 4792, 4793, 4794, 4795, 4796, + 4797, 4798, 4799, 4800, 4801, 4802, 4803, 4804, 4805, 4806, 4807, 4808, + 4809, 4810, 4811, 4812, 4813, 4814, 4815, 4816, 4817, 4818, 4819, 4820, + 4821, 4822, 4823, 4824, 4839, 4855, 4856, 4857, 4858, 4859, 4860, 4861, + 4862, 4863, 4864, 4865, 4866, 4867, 4868, 4869, 4870, 4871, 4872, 4873, + 4874, 4875, 4876, 4877, 4878, 4879, 4880, 4883, 4885, 4887, 4889, 4891, + 4893, 4895, 4898, 4900, 4901, 4902, 4903, 4904, 4907, 4909, 4921, 4923, + 4925, 4927, 4929, 4931, 4933, 4935, 4937, 4939, 4941, 4943, 4945, 4947, + 4949, 4951, 4953, 4955, 4957, 4959, 4961, 4963, 4965, 4967, 4969, 4971, + 4973, 4975, 4977, 4979, 4981, 4983, 4985, 4987, 4989, 4991, 4993, 4995, + 4997, 4999, 5012, 5013, 5017, 5048, 5105, 5106, 5107, 5108, 5109, 5110, + 5111, 5112, 5113, 5114, 5115, 5116, 5117, 5118, 5119, 5120, 5121, 5122, + 5123, 5124, 5125, 5126, 5127, 5128, 5129, 5130, 5132, 5167, 5168, 5169, + 5170, 5171, 5172, 5173, 5174, 5175, 5176, 5177, 5178, 5179, 5180, 5181, + 5182, 5183, 5184, 5185, 5186, 5187, 5188, 5224, 5228, 5229, 5233, 5245, + 5246, 5251, 5252, 5253, 5254, 5255, 5256, 5257, 5258, 5259, 5263, 5264, + 5269, 5270, 5271, 5272, 5273, 5274, 5275, 5801, 5802, 5803, 5804, 5808, + 5809, 5810, 5811, 5812, 5813, 5814, 5815, 5816, + 20004, 20005, 20006, 20007, 20008, 20009, 20010, 20011, 20012, 20013, + 20014, 20015, 20016, 20017, 20018, 20019, 20020, 20021, 20022, 20023, + 20024, 20025, 20026, 20027, 20028, 20029, 20030, 20031, 20032, 20064, + 20065, 20066, 20067, 20068, 20069, 20070, 20071, 20072, 20073, 20074, + 20075, 20076, 20077, 20078, 20079, 20080, 20081, 20082, 20083, 20084, + 20085, 20086, 20087, 20088, 20089, 20090, 20091, 20092, 21413, 21414, + 21415, 21416, 21417, 21418, 21419, 21420, 21421, 21422, 21423, 21453, + 21454, 21455, 21456, 21457, 21458, 21459, 21460, 21461, 21462, 21463, + 21473, 21474, 21475, 21476, 21477, 21478, 21479, 21480, 21481, 21482, + 21483, 21896, 21897, 21898, 21899, 22171, 22172, 22173, 22174, 22175, + 22176, 22177, 22181, 22182, 22183, 22184, 22185, 22186, 22187, 22191, + 22192, 22193, 22194, 22195, 22196, 22197, 25884, 27205, 27206, 27207, + 27208, 27209, 27210, 27211, 27212, 27213, 27214, 27215, 27216, 27217, + 27218, 27219, 27220, 27221, 27222, 27223, 27224, 27225, 27226, 27227, + 27228, 27229, 27230, 27231, 27232, 27391, 27392, 27393, 27394, 27395, + 27396, 27397, 27398, 27492, 28402, 28403, 28404, 28405, 28406, 28407, + 28408, 28409, 28410, 28411, 28412, 28413, 28414, 28415, 28416, 28417, + 28418, 28419, 28420, 28421, 28422, 28423, 28424, 28425, 28426, 28427, + 28428, 28429, 28430, 28431, 28432, 28462, 28463, 28464, 28465, 28466, + 28467, 28468, 28469, 28470, 28471, 28472, 28473, 28474, 28475, 28476, + 28477, 28478, 28479, 28480, 28481, 28482, 28483, 28484, 28485, 28486, + 28487, 28488, 28489, 28490, 28491, 28492, 29701, 29702, 30161, 30162, + 30163, 30164, 30165, 30166, 30167, 30168, 30169, 30170, 30171, 30172, + 30173, 30174, 30175, 30176, 30177, 30178, 30179, 30800, 31251, 31252, + 31253, 31254, 31255, 31256, 31257, 31258, 31259, 31275, 31276, 31277, + 31278, 31279, 31281, 31282, 31283, 31284, 31285, 31286, 31287, 31288, + 31289, 31290, 31466, 31467, 31468, 31469, 31700 ]; // Module pattern to hide internal vars (function() { + "use strict"; + // Module scope var format_geojson = new OpenLayers.Format.GeoJSON(); // Silently ignore 3rd dimension (e.g. USGS Quakes feed) format_geojson.ignoreExtraDims = true; - var marker_url_path = S3.Ap.concat('/static/img/markers/'); - var proj4326 = S3.gis.proj4326; + + var marker_url_path = S3.Ap.concat('/static/img/markers/'), + proj4326 = S3.gis.proj4326; // Default values if not set by the layer // Also in modules/s3/s3gis.py // http://dev.openlayers.org/docs/files/OpenLayers/Strategy/Cluster-js.html //var cluster_attribute_default = 'colour'; - var cluster_distance_default = 20; // pixels - var cluster_threshold_default = 2; // minimum # of features to form a cluster + var cluster_distance_default = 20, // pixels + cluster_threshold_default = 2; // minimum # of features to form a cluster + // Default values if not set by the map // Also in modules/s3/s3gis.py - var fill_default = '#f5902e'; // fill colour for unclustered Point - var cluster_fill_default = '8087ff'; // fill colour for clustered Point - var cluster_stroke_default = '2b2f76'; // stroke colour for clustered Point - var select_fill_default = 'ffdc33'; // fill colour for selected Point - var select_stroke_default = 'ff9933'; // stroke colour for selected Point + var fill_default = '#f5902e', // fill colour for unclustered Point + cluster_fill_default = '8087ff', // fill colour for clustered Point + cluster_stroke_default = '2b2f76', // stroke colour for clustered Point + select_fill_default = 'ffdc33', // fill colour for selected Point + select_stroke_default = 'ff9933'; // stroke colour for selected Point /** * Main Start Function @@ -1772,16 +220,17 @@ S3.gis.yx = [ * {OpenLayers.Map} The openlayers map. */ S3.gis.show_map = function(map_id, options) { + if (!map_id) { map_id = 'default_map'; } - if (undefined == options) { + if (undefined === options) { // Lookup options options = S3.gis.options[map_id]; } - var projection = options.projection; - var projection_current = new OpenLayers.Projection('EPSG:' + projection); + var projection = options.projection, + projection_current = new OpenLayers.Projection('EPSG:' + projection); options.projection_current = projection_current; if (projection == 900913) { options.maxExtent = new OpenLayers.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34); @@ -1802,7 +251,7 @@ S3.gis.yx = [ lon = options.lon, bounds, center; - if ((lat != undefined) && (lon != undefined)) { + if ((lat !== undefined) && (lon !== undefined)) { center = new OpenLayers.LonLat(lon, lat); center.transform(proj4326, projection_current); } else { @@ -1913,6 +362,7 @@ S3.gis.yx = [ * Check that all Map layers are Loaded */ var layersLoaded = function(map_id) { + var dfd = new jQuery.Deferred(); // Test every half-second @@ -1939,7 +389,7 @@ S3.gis.yx = [ */ var tilesLoaded = function(layer) { - if (undefined == layer.numLoadingTiles) { + if (undefined === layer.numLoadingTiles) { return true; } @@ -1969,14 +419,16 @@ S3.gis.yx = [ * - ensuring a minimal bbox & a little padding */ var zoomBounds = function(map, bounds, minBBOX) { - var bbox = bounds.toArray(); - var lon_min = bbox[0], + + var bbox = bounds.toArray(), + lon_min = bbox[0], lat_min = bbox[1], lon_max = bbox[2], lat_max = bbox[3]; + // Ensure a minimal BBOX in case we just have a single data point - var min_size = minBBOX || 0.05; - var delta = (min_size - Math.abs(lon_max - lon_min)) / 2; + var min_size = minBBOX || 0.05, + delta = (min_size - Math.abs(lon_max - lon_min)) / 2; if (delta > 0) { lon_min -= delta; lon_max += delta; @@ -1986,6 +438,7 @@ S3.gis.yx = [ lat_min -= delta; lat_max += delta; } + // Add an Inset in order to not have points right at the edges of the map var inset = min_size / 7; lon_min -= inset; @@ -2007,10 +460,13 @@ S3.gis.yx = [ * - to ensure that bounds are set correctly */ var search_layer_loadend = function(event) { + // Search results have Loaded var layer = event.object; + // Read Bounds for Zoom var bounds = layer.getDataExtent(); + // Zoom Out to Cluster //layer.map.zoomTo(0) if (bounds) { @@ -2019,6 +475,7 @@ S3.gis.yx = [ bounds.transform(map.getProjectionObject(), proj4326); zoomBounds(map, bounds); } + var strategy, strategies = layer.strategies; for (var i=0, len=strategies.length; i < len; i++) { @@ -2139,7 +596,7 @@ S3.gis.yx = [ } } // Call Custom Call-back - if (undefined != map.s3.layerRefreshed) { + if (undefined !== map.s3.layerRefreshed) { map.s3.layerRefreshed(layer); } }, @@ -2166,7 +623,9 @@ S3.gis.yx = [ } }; - // Build the OpenLayers map + /** + * Build the OpenLayers map + */ var addMap = function(map_id, options) { var fallThrough; @@ -2229,10 +688,13 @@ S3.gis.yx = [ return map; }; - // Add the GeoExt UI + /** + * Add the GeoExt UI + */ var addMapUI = function(map) { - var s3 = map.s3; - var options = s3.options; + + var s3 = map.s3, + options = s3.options; var mapPanel = new GeoExt.MapPanel({ //cls: 'mappanel', @@ -2255,8 +717,10 @@ S3.gis.yx = [ s3.portal = portal; if (options.legend || options.layers_wms) { - var layers = map.layers; - var mp_items = mapPanel.layers.data.items; + + var layers = map.layers, + mp_items = mapPanel.layers.data.items; + for (var i = 0; i < layers.length; i++) { // Ensure that legendPanel knows about the Markers for our Feature layers if (layers[i].legendURL) { @@ -2374,11 +838,14 @@ S3.gis.yx = [ Ext.QuickTips.init(); }; - // Create an embedded Map Panel - // This is also called when a fullscreen map is made to go embedded + /** + * Create an embedded Map Panel + * - this is also called when a fullscreen map is made to go embedded + */ var addMapPanel = function(map) { - var s3 = map.s3; - var options = s3.options; + + var s3 = map.s3, + options = s3.options; var westPanelContainer = addWestPanel(map); var mapPanelContainer = addMapPanelContainer(map); @@ -2406,10 +873,13 @@ S3.gis.yx = [ // Pass to global scope so that s3.gis.fullscreen.js can call it to return from fullscreen S3.gis.addMapPanel = addMapPanel; - // Create a floating Map Window + /** + * Create a floating Map Window + */ var addMapWindow = function(map) { - var s3 = map.s3; - var options = s3.options; + + var s3 = map.s3, + options = s3.options; var westPanelContainer = addWestPanel(map); var mapPanelContainer = addMapPanelContainer(map); @@ -2476,10 +946,13 @@ S3.gis.yx = [ // Pass to global scope so that s3.gis.fullscreen.js can call it to go fullscreen S3.gis.addMapWindow = addMapWindow; - // Put into a Container to allow going fullscreen from a BorderLayout + /** + * Put into a Container to allow going fullscreen from a BorderLayout + */ var addWestPanel = function(map) { - var s3 = map.s3; - var west_collapsed = s3.options.west_collapsed || false; + + var s3 = map.s3, + west_collapsed = s3.options.west_collapsed || false; var mapWestPanel = new Ext.Panel({ //cls: 'gis_west', @@ -2528,10 +1001,13 @@ S3.gis.yx = [ return westPanelContainer; }; - // Put into a Container to allow going fullscreen from a BorderLayout - // We had to put the mapPanel inside a 'card' container for the Google Earth Panel - // - since this is deprecated, we are free to redesign this + /** + * Put into a Container to allow going fullscreen from a BorderLayout + * - we had to put the mapPanel inside a 'card' container for the Google Earth Panel + * - since this is deprecated, we are free to redesign this + */ var addMapPanelContainer = function(map) { + var s3 = map.s3, options = s3.options, toolbar; @@ -2600,7 +1076,9 @@ S3.gis.yx = [ return mapPanelContainer; }; - // Add LayerTree (to be called after the layers are added) + /** + * Add LayerTree (to be called after the layers are added) + */ var addLayerTree = function(map) { // Extend LayerNodeUI to not force a folder with Radio buttons to have one active @@ -2609,9 +1087,11 @@ S3.gis.yx = [ GeoExt.tree.LayerNodeUIS3 = Ext.extend(GeoExt.tree.LayerNodeUI, { onClick: function(e) { if (e.getTarget('.x-tree-node-cb', 1)) { - var node = this.node; - var attributes = this.node.attributes; - var group = attributes.checkedGroup; + + var node = this.node, + attributes = node.attributes, + group = attributes.checkedGroup; + if (group && group !== 'baselayer') { // Radio button folders need different behaviour var checked = !attributes.checked; @@ -2628,13 +1108,17 @@ S3.gis.yx = [ } }, enforceOneVisible: function() { - var attributes = this.node.attributes; - var group = attributes.checkedGroup; + + var attributes = this.node.attributes, + group = attributes.checkedGroup; + // If we are in the baselayer group, the map will take care of // enforcing visibility. if (group && group !== 'baselayer') { - var layer = this.node.layer; - var checkedNodes = this.node.getOwnerTree().getChecked(); + + var layer = this.node.layer, + checkedNodes = this.node.getOwnerTree().getChecked(); + //var checkedCount = 0; // enforce "not more than one visible" Ext.each(checkedNodes, function(n){ @@ -2672,6 +1156,7 @@ S3.gis.yx = [ expanded, folders_radio, collapsible; + if (options.hide_base) { base = false; } else { @@ -2821,8 +1306,8 @@ S3.gis.yx = [ this.addStoreHandlers(node); // Add Folders - var children = node.attributes.children; - var len = children.length; + var children = node.attributes.children, + len = children.length; if (len) { var child, dir, @@ -2864,6 +1349,7 @@ S3.gis.yx = [ loader, parent, sub; + // Place folders into subfolders for (i = 0; i < len; i++) { dir = dirs[i]; @@ -3015,9 +1501,13 @@ S3.gis.yx = [ return layerTree; }; - // Add WMS Browser + /** + * Add WMS Browser + */ var addWMSBrowser = function(map) { + var options = map.s3.options; + var root = new Ext.tree.AsyncTreeNode({ expanded: true, loader: new GeoExt.tree.WMSCapabilitiesLoader({ @@ -3066,6 +1556,7 @@ S3.gis.yx = [ * Show the Throbber */ var showThrobber = function(map_id) { + $('#' + map_id + ' .layer_throbber').show().removeClass('hide'); }; @@ -3074,8 +1565,10 @@ S3.gis.yx = [ * - if all layers have removed their link to it */ var hideThrobber = function(layer, map) { + var s3, layers_loading; + if (layer) { s3 = layer.map.s3; layers_loading = s3.layers_loading; @@ -3094,10 +1587,14 @@ S3.gis.yx = [ * - show Throbber */ var layer_loadstart = function(event) { - var layer = event.object; - var s3 = layer.map.s3; + + var layer = event.object, + s3 = layer.map.s3; + showThrobber(s3.id); + var layer_id = layer.s3_layer_id; + //s3_debug('Loading Layer ' + layer_id); var layers_loading = s3.layers_loading; if (layers_loading.indexOf(layer_id) == -1) { // we never want 2 pushed @@ -3112,10 +1609,12 @@ S3.gis.yx = [ * - parse S3 custom parameters */ var layer_loadend = function(event) { + var layer = event.object, response = event.response, s3, priv; + if (response && response.priv) { priv = response.priv; try { @@ -3134,7 +1633,7 @@ S3.gis.yx = [ marker_url, i; - if (undefined != s3) { + if (undefined !== s3) { var strategies = layer.strategies, numStrategies = strategies.length, @@ -3142,7 +1641,7 @@ S3.gis.yx = [ // Read custom data in GeoJSON response //s3_debug(s3); - if (undefined != s3.level) { + if (undefined !== s3.level) { // We are displaying aggregated data // - update the strategy with the level of aggregation for (i = 0; i < numStrategies; i++) { @@ -3153,11 +1652,13 @@ S3.gis.yx = [ } } } - if (undefined != s3.style) { + if (undefined !== s3.style) { // Apply the style to the layer restyle = true; - var style = s3.style; - var result = createStyleMap(layer.map, style); + + var style = s3.style, + result = createStyleMap(layer.map, style); + marker_url = result[1]; layer.legendURL = marker_url; layer.styleMap = result[0]; // featureStyleMap @@ -3232,7 +1733,7 @@ S3.gis.yx = [ } } - if (undefined != restyle) { + if (undefined !== restyle) { // Redraw the features with the new styleMap features = layer.features; features_len = features.length; @@ -3243,17 +1744,19 @@ S3.gis.yx = [ // Redraw the Legend var legendPanel = layer.map.s3.mapPanel.legendPanel; if (legendPanel) { + // Find the right layerLegend var layerLegend, layerLegends = legendPanel.items.items, + layerLegends_len = layerLegends.length, s3_layer_id = layer.s3_layer_id; - var layerLegends_len = layerLegends.length; + for (i=0; i < layerLegends_len; i++) { layerLegend = layerLegends[i]; if ((layerLegend.layer) && (layerLegend.layer.s3_layer_id == s3_layer_id)) { // @ToDo: Fix this - not currently working var record; - if (undefined != rules) { + if (undefined !== rules) { if (layerLegend.xtype == 'gx_vectorlegend') { layerLegend.rules = rules; layerLegend.update(); @@ -3263,7 +1766,7 @@ S3.gis.yx = [ legendPanel.removeLegend(record); legendPanel.addLegend(record, i); } - } else if (undefined != marker_url) { + } else if (undefined !== marker_url) { if (layerLegend.xtype == 'gx_urllegend') { layerLegend.layerRecord.data.legendURL = marker_url; layerLegend.update(); @@ -3286,14 +1789,16 @@ S3.gis.yx = [ * Re-style a layer dynamically based on the data contents */ var reStyle = function(layer) { + var defaults = layer.s3_style[0]; // Read the data - var prop = defaults.prop; - var features = layer.features; - var i, + var prop = defaults.prop, + features = layer.features, features_len = features.length, + i, data = []; + for (i = 0; i < features_len; i++) { data.push(features[i].attributes[prop]); } @@ -3312,28 +1817,28 @@ S3.gis.yx = [ // How many classes should we use? // Color schemes from ColorBrewer2.org // YlOrRd sequential schemes from ColorBrewer which are colorblind-safe, print-friendly and photocopy-safe - var classes = Object.keys(seen).length, + var numClasses = Object.keys(seen).length, colors; - if (classes >= 5) { - classes = 5; + if (numClasses >= 5) { + numClasses = 5; colors = ['ffffb2', 'fecc5c', 'fd8d3c', 'f03b20', 'bd0026']; - } else if (classes == 4) { + } else if (numClasses == 4) { colors = ['ffffb2', 'fecc5c', 'fd8d3c', 'e31a1c']; - } else if (classes == 3) { + } else if (numClasses == 3) { colors = ['ffeda0', 'feb24c', 'f03b20']; - } else if (classes == 2) { + } else if (numClasses == 2) { colors = ['ffeda0', 'f03b20']; - } else if (classes == 1) { + } else if (numClasses == 1) { colors = ['feb24c']; } // What is the size of each class? - var classSize = Math.round(features_len / classes); - var step = classSize; + var classSize = Math.round(features_len / numClasses), + step = classSize; // Set first value var breaks = [data[0]]; - for (i = 1; i < classes; i++) { + for (i = 1; i < numClasses; i++) { breaks[i] = data[step] || data[features_len - 1]; step += classSize; } @@ -3344,7 +1849,8 @@ S3.gis.yx = [ high, _style, style = []; - for (i=0; i < classes; i++) { + + for (i=0; i < numClasses; i++) { low = breaks[i]; high = breaks[i + 1]; _style = $.extend({}, defaults); // Make a copy @@ -3375,6 +1881,7 @@ S3.gis.yx = [ * - show legendPanel if not displayed */ var layer_visibilitychanged = function(event) { + showLegend(event.object.map); }; @@ -3390,8 +1897,8 @@ S3.gis.yx = [ */ var addLayers = function(map) { - var s3 = map.s3; - var options = s3.options; + var s3 = map.s3, + options = s3.options; // List of folders for the LayerTree s3.dirs = []; @@ -3569,8 +2076,10 @@ S3.gis.yx = [ // Simple Features // e.g. S3LocationSelectorWidget if (options.features) { - var features = options.features; - var current_projection = map.getProjectionObject(); + + var features = options.features, + current_projection = map.getProjectionObject(); + //var parseFeature = format_geojson.parseFeature; //var parseGeometry = format_geojson.parseGeometry; for (i = 0; i < features.length; i++) { @@ -3597,6 +2106,7 @@ S3.gis.yx = [ * - Append ?token=result to the URL */ var addArcRESTLayer = function(map, layer) { + var name = layer.name, url = [layer.url], layers, @@ -3605,13 +2115,14 @@ S3.gis.yx = [ format, transparent, visibility; - if (undefined != layer.layers) { + + if (undefined !== layer.layers) { layers = layer.layers.join(); } else { // Default layer layers = 0; } - if (undefined != layer.dir) { + if (undefined !== layer.dir) { dir = layer.dir; if ($.inArray(dir, map.s3.dirs) == -1) { // Add this folder to the list of folders @@ -3621,22 +2132,22 @@ S3.gis.yx = [ // Default folder dir = ''; } - if (undefined != layer.base) { + if (undefined !== layer.base) { isBaseLayer = layer.base; } else { isBaseLayer = false; } - if (undefined != layer.format) { + if (undefined !== layer.format) { format = layer.format; } else { format = 'png'; } - if (undefined != layer.transparent) { + if (undefined !== layer.transparent) { transparent = layer.transparent; } else { transparent = true; } - if (undefined != layer.visibility) { + if (undefined !== layer.visibility) { visibility = layer.visibility; } else { // Default to visible @@ -3667,9 +2178,11 @@ S3.gis.yx = [ // Bing var addBingLayers = function(map) { - var bing = map.s3.options.Bing; - var ApiKey = bing.ApiKey; - var layer; + + var bing = map.s3.options.Bing, + ApiKey = bing.ApiKey, + layer; + if (bing.Aerial) { layer = new OpenLayers.Layer.Bing({ key: ApiKey, @@ -3716,7 +2229,9 @@ S3.gis.yx = [ // CoordinateGrid var addCoordinateGrid = function(map) { + var CoordinateGrid = map.s3.options.CoordinateGrid; + var graticule = new OpenLayers.Control.Graticule({ //labelFormat: 'dm', layerName: CoordinateGrid.name, @@ -3728,10 +2243,12 @@ S3.gis.yx = [ // DraftLayer // Used for drawing Points/Polygons & for HTML5 GeoLocation var addDraftLayer = function(map) { + // Styling var marker, style, options = map.s3.options; + if (options.draw_feature) { // Marker for Points marker = options.marker_default; @@ -3745,9 +2262,10 @@ S3.gis.yx = [ 'style': style, 'opacity': 0.9 // Trigger the select renderIntent -> Opaque }; - var response = createStyleMap(map, layer); - var featureStyleMap = response[0]; - var marker_url = response[1]; + + var response = createStyleMap(map, layer), + featureStyleMap = response[0], + marker_url = response[1]; var draftLayer = new OpenLayers.Layer.Vector( i18n.gis_draft_layer, { @@ -3768,11 +2286,13 @@ S3.gis.yx = [ // Used also by internal Feature Layers, Feature Queries, Feature Resources // & GeoRSS feeds var addGeoJSONLayer = function(map, layer) { - var s3 = map.s3; - var name = layer.name; + + var s3 = map.s3, + name = layer.name; if (layer.no_popups) { s3.layers_nopopups.push(name); } + var url = layer.url, readWithPOST, refresh, @@ -3783,18 +2303,20 @@ S3.gis.yx = [ cluster_threshold, projection, layer_type; + if (url.indexOf('$search') !== -1) { - // Use POSTs to retrieve data allowing arbitrary length of filter options as well as TLS encryption of filters + // Use POSTs to retrieve data allowing arbitrary l + // ength of filter options as well as TLS encryption of filters readWithPOST = true; } else { readWithPOST = false; } - if (undefined != layer.refresh) { + if (undefined !== layer.refresh) { refresh = layer.refresh; } else { refresh = 900; // seconds (so 15 mins) } - if (undefined != layer.dir) { + if (undefined !== layer.dir) { dir = layer.dir; if ($.inArray(dir, s3.dirs) == -1) { // Add this folder to the list of folders @@ -3804,34 +2326,34 @@ S3.gis.yx = [ // Default folder dir = ''; } - if (undefined != layer.visibility) { + if (undefined !== layer.visibility) { visibility = layer.visibility; } else { // Default to visible visibility = true; } - if (undefined != layer.cluster_attribute) { + if (undefined !== layer.cluster_attribute) { cluster_attribute = layer.cluster_attribute; } else { // Default to global settings //var cluster_attribute = cluster_attribute_default; cluster_attribute = 'colour'; } - if (undefined != layer.cluster_distance) { + if (undefined !== layer.cluster_distance) { cluster_distance = layer.cluster_distance; } else { // Default to global settings cluster_distance = cluster_distance_default; } - if (undefined != layer.cluster_threshold) { + if (undefined !== layer.cluster_threshold) { cluster_threshold = layer.cluster_threshold; } else { // Default to global settings cluster_threshold = cluster_threshold_default; } - if (undefined != layer.projection) { + if (undefined !== layer.projection) { projection = layer.projection; } else { // Feature Layers, GeoRSS & KML are always in 4326 @@ -3842,21 +2364,21 @@ S3.gis.yx = [ } else { projection = new OpenLayers.Projection('EPSG:' + projection); } - if (undefined != layer.type) { + if (undefined !== layer.type) { layer_type = layer.type; } else { // Feature Layers layer_type = 'feature'; } var legendTitle = '
' + name + '
'; - if (undefined != layer.desc) { + if (undefined !== layer.desc) { legendTitle += '
' + layer.desc + '
'; } - if ((undefined != layer.src) || (undefined != layer.src_url)) { + if ((undefined !== layer.src) || (undefined !== layer.src_url)) { var source = ''; // Styling - var response = createStyleMap(map, layer); - var featureStyleMap = response[0]; - var marker_url = response[1]; + var response = createStyleMap(map, layer), + featureStyleMap = response[0], + marker_url = response[1]; // Strategies var strategies = [ @@ -3939,7 +2461,7 @@ S3.gis.yx = [ geojsonLayer.legendTitle = legendTitle; // Set the popup_format, even if empty // - leave if not set (e.g. Feature Queries) - if (undefined != layer.popup_format) { + if (undefined !== layer.popup_format) { geojsonLayer.s3_popup_format = layer.popup_format; } geojsonLayer.setVisibility(visibility); @@ -3958,8 +2480,10 @@ S3.gis.yx = [ // Google var addGoogleLayers = function(map) { - var google = map.s3.options.Google; - var layer; + + var google = map.s3.options.Google, + layer; + if (google.MapMaker || google.MapMakerHybrid) { // v2 API if (google.Satellite) { @@ -4117,6 +2641,7 @@ S3.gis.yx = [ // GPX var addGPXLayer = function(map, layer) { + var cluster_distance, cluster_threshold, dir, @@ -4127,31 +2652,32 @@ S3.gis.yx = [ tracks, url = layer.url, visibility, - waypoints; - var marker_height = marker.h, + waypoints, + marker_height = marker.h, marker_url = marker_url_path + marker.i, marker_width = marker.w; - if (undefined != layer.waypoints) { + + if (undefined !== layer.waypoints) { waypoints = layer.waypoints; } else { waypoints = true; } - if (undefined != layer.tracks) { + if (undefined !== layer.tracks) { tracks = layer.tracks; } else { tracks = true; } - if (undefined != layer.routes) { + if (undefined !== layer.routes) { routes = layer.routes; } else { routes = true; } - if (undefined != layer.visibility) { + if (undefined !== layer.visibility) { visibility = layer.visibility; } else { visibility = true; } - if (undefined != layer.dir) { + if (undefined !== layer.dir) { dir = layer.dir; if ($.inArray(dir, map.s3.dirs) == -1) { // Add this folder to the list of folders @@ -4161,17 +2687,17 @@ S3.gis.yx = [ // Default folder dir = ''; } - if (undefined != layer.opacity) { + if (undefined !== layer.opacity) { opacity = layer.opacity; } else { opacity = 1; } - if (undefined != layer.cluster_distance) { + if (undefined !== layer.cluster_distance) { cluster_distance = layer.cluster_distance; } else { cluster_distance = cluster_distance_default; } - if (undefined != layer.cluster_threshold) { + if (undefined !== layer.cluster_threshold) { cluster_threshold = layer.cluster_threshold; } else { cluster_threshold = cluster_threshold_default; @@ -4236,6 +2762,7 @@ S3.gis.yx = [ // KML var addKMLLayer = function(map, layer) { + var body, cluster_distance, cluster_threshold, @@ -4246,28 +2773,29 @@ S3.gis.yx = [ title, url = layer.url, visibility; - if (undefined != layer.title) { + + if (undefined !== layer.title) { title = layer.title; } else { title = 'name'; } - if (undefined != layer.body) { + if (undefined !== layer.body) { body = layer.body; } else { body = 'description'; } - if (undefined != layer.refresh) { + if (undefined !== layer.refresh) { refresh = layer.refresh; } else { refresh = 900; // seconds (so 15 mins) } - if (undefined != layer.visibility) { + if (undefined !== layer.visibility) { visibility = layer.visibility; } else { // Default to visible visibility = true; } - if (undefined != layer.dir) { + if (undefined !== layer.dir) { dir = layer.dir; if ($.inArray(dir, s3.dirs) == -1) { // Add this folder to the list of folders @@ -4277,20 +2805,20 @@ S3.gis.yx = [ // Default folder dir = ''; } - if (undefined != layer.cluster_distance) { + if (undefined !== layer.cluster_distance) { cluster_distance = layer.cluster_distance; } else { cluster_distance = cluster_distance_default; } - if (undefined != layer.cluster_threshold) { + if (undefined !== layer.cluster_threshold) { cluster_threshold = layer.cluster_threshold; } else { cluster_threshold = cluster_threshold_default; } // Styling: Base - var response = createStyleMap(map, layer); - var featureStyleMap = response[0]; + var response = createStyleMap(map, layer), + featureStyleMap = response[0]; //var marker_url = response[1]; // Needs to be uniquely instantiated @@ -4369,25 +2897,27 @@ S3.gis.yx = [ // OpenStreetMap var addOSMLayer = function(map, layer) { + var dir, isBaseLayer, name = layer.name, numZoomLevels, url = [layer.url1], visibility; - if (undefined != layer.url2) { + + if (undefined !== layer.url2) { url.push(layer.url2); } - if (undefined != layer.url3) { + if (undefined !== layer.url3) { url.push(layer.url3); } - if (undefined != layer.visibility) { + if (undefined !== layer.visibility) { visibility = layer.visibility; } else { // Default to visible visibility = true; } - if (undefined != layer.dir) { + if (undefined !== layer.dir) { dir = layer.dir; if ($.inArray(dir, map.s3.dirs) == -1) { // Add this folder to the list of folders @@ -4397,12 +2927,12 @@ S3.gis.yx = [ // Default folder dir = ''; } - if (undefined != layer.base) { + if (undefined !== layer.base) { isBaseLayer = layer.base; } else { isBaseLayer = true; } - if (undefined != layer.zoomLevels) { + if (undefined !== layer.zoomLevels) { numZoomLevels = layer.zoomLevels; } else { numZoomLevels = 19; @@ -4422,7 +2952,7 @@ S3.gis.yx = [ s3_layer_type: 'openstreetmap' } ); - if (undefined != layer.attribution) { + if (undefined !== layer.attribution) { osmLayer.attribution = layer.attribution; } osmLayer.setVisibility(visibility); @@ -4438,17 +2968,20 @@ S3.gis.yx = [ // Supports OpenStreetMap TMS Layers var osm_getTileURL = function(bounds) { - var res = this.map.getResolution(); - var x = Math.round((bounds.left - this.maxExtent.left) / (res * this.tileSize.w)), + + var res = this.map.getResolution(), + x = Math.round((bounds.left - this.maxExtent.left) / (res * this.tileSize.w)), y = Math.round((this.maxExtent.top - bounds.top) / (res * this.tileSize.h)), - z = this.map.getZoom(); - var limit = Math.pow(2, z); + z = this.map.getZoom(), + limit = Math.pow(2, z); + if (y < 0 || y >= limit) { return OpenLayers.Util.getImagesLocation() + '404.png'; } else { x = ((x % limit) + limit) % limit; - var path = z + '/' + x + '/' + y + '.' + this.type; - var url = this.url; + var path = z + '/' + x + '/' + y + '.' + this.type, + url = this.url; + if (url instanceof Array) { url = this.selectUrl(path, url); } @@ -4458,9 +2991,11 @@ S3.gis.yx = [ // OpenWeatherMap var addOWMLayers = function(map) { + var apikey = S3.gis.openweathermap, layer, layers = map.s3.options.layers_openweathermap; + for (var l in layers){ layer = new OpenLayers.Layer.XYZ( layers[l].name, @@ -4484,24 +3019,26 @@ S3.gis.yx = [ // TMS var addTMSLayer = function(map, layer) { + var dir, format, name = layer.name, numZoomLevels, url = [layer.url]; - if (undefined != layer.url2) { + + if (undefined !== layer.url2) { url.push(layer.url2); } - if (undefined != layer.url3) { + if (undefined !== layer.url3) { url.push(layer.url3); } var layername = layer.layername; - if (undefined != layer.zoomLevels) { + if (undefined !== layer.zoomLevels) { numZoomLevels = layer.zoomLevels; } else { numZoomLevels = 19; } - if (undefined != layer.dir) { + if (undefined !== layer.dir) { dir = layer.dir; if ($.inArray(dir, map.s3.dirs) == -1) { // Add this folder to the list of folders @@ -4511,7 +3048,7 @@ S3.gis.yx = [ // Default folder dir = ''; } - if (undefined != layer.format) { + if (undefined !== layer.format) { format = layer.format; } else { format = 'png'; @@ -4529,7 +3066,7 @@ S3.gis.yx = [ } ); - if (undefined != layer.attribution) { + if (undefined !== layer.attribution) { tmsLayer.attribution = layer.attribution; } tmsLayer.events.on({ @@ -4545,6 +3082,7 @@ S3.gis.yx = [ // WFS // @ToDo: WFS-T Editing: http://www.gistutor.com/openlayers/22-advanced-openlayers-tutorials/47-openlayers-wfs-t-using-a-geoserver-hosted-postgis-layer.html var addWFSLayer = function(map, layer) { + var cluster_attribute, cluster_distance, cluster_threshold, @@ -4563,36 +3101,37 @@ S3.gis.yx = [ url = layer.url, version, visibility; - if ((undefined != layer.username) && (undefined != layer.password)) { + + if ((undefined !== layer.username) && (undefined !== layer.password)) { var username = layer.username, password = layer.password; url = url.replace('://', '://' + username + ':' + password + '@'); } - if (undefined != layer.featureNS) { + if (undefined !== layer.featureNS) { featureNS = layer.featureNS; } else { featureNS = null; } - if (undefined != layer.schema) { + if (undefined !== layer.schema) { schema = layer.schema; } - if (undefined != layer.version) { + if (undefined !== layer.version) { version = layer.version; } else { version = '1.1.0'; } - if (undefined != layer.geometryName) { + if (undefined !== layer.geometryName) { geometryName = layer.geometryName; } else { geometryName = 'the_geom'; } - if (undefined != layer.visibility) { + if (undefined !== layer.visibility) { visibility = layer.visibility; } else { // Default to visible visibility = true; } - if (undefined != layer.dir) { + if (undefined !== layer.dir) { dir = layer.dir; if ($.inArray(dir, map.s3.dirs) == -1) { // Add this folder to the list of folders @@ -4602,24 +3141,24 @@ S3.gis.yx = [ // Default folder dir = ''; } - if (undefined != layer.cluster_attribute) { + if (undefined !== layer.cluster_attribute) { cluster_attribute = layer.cluster_attribute; } else { // Default to global settings //cluster_attribute = cluster_attribute_default; cluster_attribute = 'colour'; } - if (undefined != layer.cluster_distance) { + if (undefined !== layer.cluster_distance) { cluster_distance = layer.cluster_distance; } else { cluster_distance = cluster_distance_default; } - if (undefined != layer.cluster_threshold) { + if (undefined !== layer.cluster_threshold) { cluster_threshold = layer.cluster_threshold; } else { cluster_threshold = cluster_threshold_default; } - if (undefined != layer.refresh) { + if (undefined !== layer.refresh) { refresh = layer.refresh; } else { // Default to Off as 'External Source' which is uneditable @@ -4663,7 +3202,7 @@ S3.gis.yx = [ // @ToDo: if Editable //strategies.push(saveStrategy); - if (undefined != layer.projection) { + if (undefined !== layer.projection) { projection = layer.projection; } else { projection = 4326; @@ -4699,14 +3238,14 @@ S3.gis.yx = [ }); var legendTitle = '
' + name + '
'; - if (undefined != layer.desc) { + if (undefined !== layer.desc) { legendTitle += '
' + layer.desc + '
'; } - if ((undefined != layer.src) || (undefined != layer.src_url)) { + if ((undefined !== layer.src) || (undefined !== layer.src_url)) { var source = '
'; // Styling - var response = createStyleMap(map, layer); - var featureStyleMap = response[0], + var response = createStyleMap(map, layer), + featureStyleMap = response[0], marker_url = response[1]; if (4326 == projection) { @@ -4780,6 +3319,7 @@ S3.gis.yx = [ // WMS var addWMSLayer = function(map, layer) { + var bgcolor, buffer, dir, @@ -4797,18 +3337,19 @@ S3.gis.yx = [ version, visibility, wms_map; + if (layer.username && layer.password) { var username = layer.username, password = layer.password; url = url.replace('://', '://' + username + ':' + password + '@'); } var layers = layer.layers; - if (undefined != layer.visibility) { + if (undefined !== layer.visibility) { visibility = layer.visibility; } else { visibility = true; } - if (undefined != layer.dir) { + if (undefined !== layer.dir) { dir = layer.dir; if ($.inArray(dir, map.s3.dirs) == -1) { // Add this folder to the list of folders @@ -4818,22 +3359,22 @@ S3.gis.yx = [ // Default folder dir = ''; } - if (undefined != layer.base) { + if (undefined !== layer.base) { isBaseLayer = layer.base; } else { isBaseLayer = false; } - if (undefined != layer.transparent) { + if (undefined !== layer.transparent) { transparent = layer.transparent; } else { transparent = true; } - if (undefined != layer.format) { + if (undefined !== layer.format) { format = layer.format; } else { format = 'image/png'; } - if (undefined != layer.version) { + if (undefined !== layer.version) { version = layer.version; } else { version = '1.1.1'; @@ -4849,38 +3390,38 @@ S3.gis.yx = [ } else { style = ''; } - if (undefined != layer.bgcolor) { + if (undefined !== layer.bgcolor) { bgcolor = '0x' + layer.bgcolor; } else { bgcolor = ''; } - if (undefined != layer.buffer) { + if (undefined !== layer.buffer) { buffer = layer.buffer; } else { buffer = 0; } - if (undefined != layer.tiled) { + if (undefined !== layer.tiled) { tiled = layer.tiled; } else { tiled = false; } - if (undefined != layer.singleTile) { + if (undefined !== layer.singleTile) { singleTile = layer.singleTile; } else { singleTile = false; } - if (undefined != layer.opacity) { + if (undefined !== layer.opacity) { opacity = layer.opacity; } else { opacity = 1; } - if (undefined != layer.queryable) { + if (undefined !== layer.queryable) { queryable = layer.queryable; } else { queryable = 1; } var legendTitle = '
' + name + '
'; - if (undefined != layer.desc) { + if (undefined !== layer.desc) { legendTitle += '
' + layer.desc + '
'; } var source, @@ -4888,7 +3429,7 @@ S3.gis.yx = [ if (map.s3.options.metadata) { // Use CMS to display Metadata var murl; - if (undefined != layer.post_id) { + if (undefined !== layer.post_id) { // Link to the existing page if (i18n.gis_metadata) { // Read-only view for end-users @@ -4908,12 +3449,12 @@ S3.gis.yx = [ source = '
'; legendTitle += source; } - } else if ((undefined != layer.src) || (undefined != layer.src_url)) { + } else if ((undefined !== layer.src) || (undefined !== layer.src_url)) { // Link to external source direct source = ''; - if (undefined != layer.legendURL) { + if (undefined !== layer.legendURL) { legendURL = layer.legendURL; } @@ -4993,24 +3534,26 @@ S3.gis.yx = [ // XYZ var addXYZLayer = function(map, layer) { + var dir, format, name = layer.name, numZoomLevels, url = [layer.url]; - if (undefined != layer.url2) { + + if (undefined !== layer.url2) { url.push(layer.url2); } - if (undefined != layer.url3) { + if (undefined !== layer.url3) { url.push(layer.url3); } var layername = layer.layername; - if (undefined != layer.zoomLevels) { + if (undefined !== layer.zoomLevels) { numZoomLevels = layer.zoomLevels; } else { numZoomLevels = 19; } - if (undefined != layer.dir) { + if (undefined !== layer.dir) { dir = layer.dir; if ($.inArray(dir, map.s3.dirs) == -1) { // Add this folder to the list of folders @@ -5020,7 +3563,7 @@ S3.gis.yx = [ // Default folder dir = ''; } - if (undefined != layer.format) { + if (undefined !== layer.format) { format = layer.format; } else { format = 'png'; @@ -5038,7 +3581,7 @@ S3.gis.yx = [ } ); - if (undefined != layer.attribution) { + if (undefined !== layer.attribution) { xyzLayer.attribution = layer.attribution; } xyzLayer.events.on({ @@ -5063,6 +3606,7 @@ S3.gis.yx = [ * {null} */ var addControls = function(map) { + var options = map.s3.options; // The default controls (normally added in OpenLayers.Map, but brought here for greater control) @@ -5076,7 +3620,7 @@ S3.gis.yx = [ //} else if (OpenLayers.Control.TouchNavigation) { // map.addControl(new OpenLayers.Control.TouchNavigation()); //} - if (options.zoomcontrol == undefined) { + if (options.zoomcontrol === undefined) { //if (OpenLayers.Control.Zoom) { map.addControl(new OpenLayers.Control.Zoom()); //} else if (OpenLayers.Control.PanZoom) { @@ -5092,7 +3636,7 @@ S3.gis.yx = [ // Additional Controls // (since the default is enabled, we provide no config in the enabled case) - if (options.scaleline == undefined) { + if (options.scaleline === undefined) { map.addControl(new OpenLayers.Control.ScaleLine()); } if (options.mouse_position == 'mgrs') { @@ -5100,14 +3644,15 @@ S3.gis.yx = [ } else if (options.mouse_position) { map.addControl(new OpenLayers.Control.MousePosition()); } - if (options.permalink == undefined) { + if (options.permalink === undefined) { map.addControl(new OpenLayers.Control.Permalink()); } - if (options.overview == undefined) { + if (options.overview === undefined) { // Copy all map options to the overview map, other than the controls - var ov_options = {}; - var map_options = map.options; - var prop; + var ov_options = {}, + map_options = map.options, + prop; + for (prop in map_options) { if (prop != 'controls') { ov_options[prop] = map_options[prop]; @@ -5120,18 +3665,25 @@ S3.gis.yx = [ addPopupControls(map); }; - /* Popups */ + /** + * Popups + */ var addPopupControls = function(map) { + map.events.register('featureover', this, tooltipSelect); map.events.register('featureout', this, tooltipUnselect); map.events.register('featureclick', this, onFeatureSelect); }; - // A Feature has been hovered over + /** + * A Feature has been hovered over + */ var tooltipSelect = function(event) { + var feature = event.feature, layer = feature.layer, j; + // Style the feature as highlighted feature.renderIntent = 'select'; layer.drawFeature(feature); @@ -5161,31 +3713,35 @@ S3.gis.yx = [ var attributes = feature.attributes, centerPoint = feature.geometry.getBounds().getCenterLonLat(), tooltip; - if (undefined != layer.s3_popup_format) { + + if (undefined !== layer.s3_popup_format) { // GeoJSON Feature Layers _.templateSettings = {interpolate: /\{(.+?)\}/g}; var s3_popup_format = layer.s3_popup_format, template = _.template(s3_popup_format); + // Ensure we have all keys (we don't transmit empty attr) var defaults = {}, key, keys = s3_popup_format.split('{'); + for (j = 0; j < keys.length; j++) { key = keys[j].split('}')[0]; defaults[key] = ''; } _.defaults(attributes, defaults); tooltip = template(attributes); - } else if (undefined != attributes.popup) { + } else if (undefined !== attributes.popup) { // Feature Queries or Theme Layers tooltip = attributes.popup; - } else if (undefined != attributes.name) { + } else if (undefined !== attributes.name) { // GeoJSON, GeoRSS or Legacy Features tooltip = attributes.name; - } else if (undefined != layer.title) { + } else if (undefined !== layer.title) { // KML or WFS - var a = attributes[layer.title]; - var type = typeof a; + var a = attributes[layer.title], + type = typeof a; + if ('object' == type) { tooltip = a.value; } else { @@ -5218,11 +3774,16 @@ S3.gis.yx = [ } }, 500); }; - /* Similar to onFeatureUnselect */ + + /** + * Similar to onFeatureUnselect + */ var tooltipUnselect = function(event) { - var feature = event.feature; - var layer = feature.layer; - var map = layer.map; + + var feature = event.feature, + layer = feature.layer, + map = layer.map; + if (feature) { // Style the feature normally feature.renderIntent = 'default'; @@ -5257,8 +3818,11 @@ S3.gis.yx = [ } }; - // Add a Popup to map + /** + * Add a Popup to map + */ var addPopup = function(feature, url, contents, iframe) { + var id = feature.id + '_popup'; if (iframe && url) { if (url.indexOf('http://') === 0 ) { @@ -5266,10 +3830,11 @@ S3.gis.yx = [ url = OpenLayers.ProxyHost + encodeURIComponent(url); } contents = ''; - } else if (undefined == contents) { + } else if (undefined === contents) { contents = i18n.gis_loading + '...
'; } var centerPoint = feature.geometry.getBounds().getCenterLonLat(); + var popup = new OpenLayers.Popup.FramedCloud( id, centerPoint, @@ -5288,7 +3853,7 @@ S3.gis.yx = [ var map = feature.layer.map; feature.map = map; // Link to be able to delete all popups as a failsafe map.addPopup(popup); - if (!iframe && undefined != url) { + if (!iframe && undefined !== url) { // use AJAX to get the contentHTML loadDetails(url, id + '_contentDiv', popup); } @@ -5299,11 +3864,19 @@ S3.gis.yx = [ // In Global scope as called from HTML (iframe onLoad) S3.gis.popupLoaded = function(id) { + // Display the hidden contents $('#' + id + '_contentDiv iframe').contents().find('#popup').show(); + // Iterate through all Maps (usually just 1) - var maps = S3.gis.maps; - var map_id, map, popup, popups, i, len; + var maps = S3.gis.maps, + map_id, + map, + popup, + popups, + i, + len; + for (map_id in maps) { map = maps[map_id]; // Iterate through all Popups (usually just 1) @@ -5318,8 +3891,11 @@ S3.gis.yx = [ } }; - // Used by addPopup and onFeatureSelect + /** + * Used by addPopup and onFeatureSelect + */ var loadDetails = function(url, id, popup) { + // Load the Popup Details via AJAX if (url.indexOf('http://') === 0) { // Use Proxy for remote popups @@ -5339,6 +3915,7 @@ S3.gis.yx = [ dataType: 'html', // gets moved to 'done' inside AjaxS3 success: function(data) { + try { // Load response into div $('#' + id).html(data); @@ -5347,15 +3924,17 @@ S3.gis.yx = [ //popup.registerImageListeners(); // Check for links to load in iframe $('#' + id + ' a.btn.iframe').click(function() { + var url = $(this).attr('href'); if (url.indexOf('http://') === 0) { // Use Proxy for remote popups url = OpenLayers.ProxyHost + encodeURIComponent(url); } // Strip the '_contentDiv' - var popup_id = id.slice(0, -11); - var contents = ''; + var popup_id = id.slice(0, -11), + contents = ''; $('#' + id).html(contents); + // Prevent default return false; }); @@ -5366,6 +3945,7 @@ S3.gis.yx = [ }, // gets moved to 'fail' inside AjaxS3 error: function(jqXHR, textStatus, errorThrown) { + var msg; if (errorThrown == 'UNAUTHORIZED') { msg = i18n.gis_requires_login; @@ -5378,8 +3958,11 @@ S3.gis.yx = [ }); }; - // A Feature has been clicked + /** + * A Feature has been clicked + */ var onFeatureSelect = function(event) { + var feature = event.feature, layer = feature.layer, map = layer.map, @@ -5436,7 +4019,7 @@ S3.gis.yx = [ //var popup_id = S3.uid(); var popup_id = feature.id + '_popup', titleField; - if (undefined != layer.title) { + if (undefined !== layer.title) { // KML, WFS titleField = layer.title; } else { @@ -5451,6 +4034,7 @@ S3.gis.yx = [ title, template, j; + if (feature.cluster) { // Cluster var cluster = feature.cluster; @@ -5461,7 +4045,7 @@ S3.gis.yx = [ map_id = s3.id; for (i = 0; i < length; i++) { attributes = cluster[i].attributes; - if (undefined != layer.s3_popup_format) { + if (undefined !== layer.s3_popup_format) { // GeoJSON Feature Layers _.templateSettings = {interpolate: /\{(.+?)\}/g}; var s3_popup_format = layer.s3_popup_format; @@ -5477,15 +4061,15 @@ S3.gis.yx = [ _.defaults(attributes, defaults); // Only display the 1st line of the hover popup name = template(attributes).split('
', 1)[0]; - } else if (undefined != attributes.popup) { + } else if (undefined !== attributes.popup) { // Only display the 1st line of the hover popup name = attributes.popup.split('
', 1)[0]; } else { name = attributes[titleField]; } - if (undefined != attributes.url) { + if (undefined !== attributes.url) { contents += "
  • " + name + "
  • "; - } else if (undefined != layer.s3_url_format) { + } else if (undefined !== layer.s3_url_format) { // Feature Layer or Feature Resource // Popup contents are pulled via AJAX _.templateSettings = {interpolate: /\{(.+?)\}/g}; @@ -5520,7 +4104,7 @@ S3.gis.yx = [ var layerType = layer.s3_layer_type; if (layerType == 'kml') { attributes = feature.attributes; - if (undefined != feature.style.balloonStyle) { + if (undefined !== feature.style.balloonStyle) { // Use the provided BalloonStyle var balloonStyle = feature.style.balloonStyle; // "{name}

    {description}" @@ -5551,7 +4135,7 @@ S3.gis.yx = [ value = attributes[body[j]].value; row = '
    ' + label + ':
    ' + value + '
    '; - } else if (undefined != attributes[body[j]]) { + } else if (undefined !== attributes[body[j]]) { row = '
    ' + attributes[body[j]] + '
    '; } else { // How would we get here? @@ -5591,12 +4175,12 @@ S3.gis.yx = [ }); } else { // @ToDo: disambiguate these by type - if (undefined != feature.attributes.url) { + if (undefined !== feature.attributes.url) { // Feature Query with Popup contents pulled via AJAX popup_url = feature.attributes.url; // Defaulted within addPopup() //contents = i18n.gis_loading + "...
    "; - } else if (undefined != layer.s3_url_format) { + } else if (undefined !== layer.s3_url_format) { // Feature Layer or Feature Resource // Popup contents are pulled via AJAX _.templateSettings = {interpolate: /\{(.+?)\}/g}; @@ -5614,32 +4198,32 @@ S3.gis.yx = [ } _.defaults(attributes, defaults);*/ // Since this is single feature case, feature should have single id - if (feature.attributes.id.constructor === Array) { - feature.attributes.id = feature.attributes.id[0]; - } + if (feature.attributes.id.constructor === Array) { + feature.attributes.id = feature.attributes.id[0]; + } popup_url = template(feature.attributes); } else { // Popup contents are built from the attributes attributes = feature.attributes; - if (undefined == attributes.name) { + if (undefined === attributes.name) { name = ''; } else { name = '

    ' + attributes.name + '

    '; } var description; - if (undefined == attributes.description) { + if (undefined === attributes.description) { description = ''; } else { description = '

    ' + attributes.description + '

    '; } var link; - if (undefined == attributes.link) { + if (undefined === attributes.link) { link = ''; } else { link = '' + attributes.link + ''; } var data; - if (undefined == attributes.data) { + if (undefined === attributes.data) { data = ''; } else if (attributes.data.indexOf('http://') === 0) { data_link = true; @@ -5649,7 +4233,7 @@ S3.gis.yx = [ data = '

    ' + attributes.data + '

    '; } var image; - if (undefined == attributes.image) { + if (undefined === attributes.image) { image = ''; } else if (attributes.image.indexOf('http://') === 0) { image = ''; @@ -5667,9 +4251,13 @@ S3.gis.yx = [ } }; - /* Similar to tooltipUnselect */ + /** + * Similar to tooltipUnselect + */ var onFeatureUnselect = function(event) { + var feature = event.feature; + if (feature) { var layer = feature.layer; /* @@ -5710,18 +4298,27 @@ S3.gis.yx = [ } } }; + + /** + * Unselect the associated feature when popup closes + */ var onPopupClose = function(/* event */) { - // Unselect the associated feature + onFeatureUnselect(this); }; - // Replace Cluster Popup contents with selected Feature Popup + /** + * Replace Cluster Popup contents with selected Feature Popup + */ var loadClusterPopup = function(map_id, url, id) { + // Show Throbber whilst waiting for Popup to show - var selector = '#' + id + '_contentDiv'; - var div = $(selector); - var contents = i18n.gis_loading + "...
    "; + var selector = '#' + id + '_contentDiv', + div = $(selector), + contents = i18n.gis_loading + "...
    "; + div.html(contents); + // Load data into Popup var map = S3.gis.maps[map_id]; $.getS3( @@ -5748,16 +4345,23 @@ S3.gis.yx = [ // Pass to global scope to access from HTML S3.gis.loadClusterPopup = loadClusterPopup; - // Zoom to Selected Feature from within Cluster Popup + /** + * Zoom to Selected Feature from within Cluster Popup + */ var zoomToSelectedFeature = function(map_id, lon, lat, zoomfactor) { - var map = S3.gis.maps[map_id]; - var lonlat = new OpenLayers.LonLat(lon, lat); + + var map = S3.gis.maps[map_id], + lonlat = new OpenLayers.LonLat(lon, lat); + // Get Current Zoom var currZoom = map.getZoom(); + // New Zoom var newZoom = currZoom + zoomfactor; + // Center and Zoom map.setCenter(lonlat, newZoom); + // Remove Popups for (var i = 0; i < map.popups.length; i++) { map.removePopup(map.popups[i]); @@ -5766,10 +4370,13 @@ S3.gis.yx = [ // Pass to global scope to access from HTML S3.gis.zoomToSelectedFeature = zoomToSelectedFeature; - // Toolbar Buttons + /** + * Toolbar Buttons + */ var addToolbar = function(map) { - var s3 = map.s3; - var options = s3.options; + + var s3 = map.s3, + options = s3.options; //var toolbar = map.s3.mapPanelContainer.getTopToolbar(); var toolbar = new Ext.Toolbar({ @@ -5806,41 +4413,22 @@ S3.gis.yx = [ tooltip: i18n.gis_zoomfull }); - var line_pressed, - pan_pressed, - point_pressed, - polygon_pressed, - circle_pressed; + var line_pressed = false, + pan_pressed = false, + point_pressed = false, + polygon_pressed = false, + circle_pressed = false; + if (options.draw_polygon == 'active') { polygon_pressed = true; - line_pressed = false; - pan_pressed = false; - point_pressed = false; - circle_pressed = false; } else if (options.draw_line == 'active') { line_pressed = true; - point_pressed = false; - pan_pressed = false; - polygon_pressed = false; - circle_pressed = false; } else if (options.draw_feature == 'active') { point_pressed = true; - line_pressed = false; - pan_pressed = false; - polygon_pressed = false; - circle_pressed = false; } else if (options.draw_circle == 'active') { - point_pressed = false; - line_pressed = false; - pan_pressed = false; - polygon_pressed = false; circle_pressed = true; }else { pan_pressed = true; - line_pressed = false; - point_pressed = false; - polygon_pressed = false; - circle_pressed = false; } // Controls for Draft Features (unused) @@ -6155,9 +4743,12 @@ S3.gis.yx = [ /* Toolbar Buttons */ - // Geolocate control - // HTML5 GeoLocation: http://dev.w3.org/geo/api/spec-source.html + /** + * Geolocate control + * - HTML5 GeoLocation: http://dev.w3.org/geo/api/spec-source.html + */ var addGeolocateControl = function(toolbar) { + var map = toolbar.map; // Use the Draft Features layer @@ -6225,8 +4816,11 @@ S3.gis.yx = [ toolbar.addButton(geoLocateButton); }; - // Supports GeoLocate control + /** + * Supports GeoLocate control + */ var pulsate = function(map, feature) { + var point = feature.geometry.getCentroid(), bounds = feature.geometry.getBounds(), radius = Math.abs((bounds.right - bounds.left) / 2), @@ -6290,7 +4884,7 @@ S3.gis.yx = [ for (var i = 0; i < layers_feature.length; i++) { var layer = layers_feature[i]; var visibility; - if (undefined != layer.visibility) { + if (undefined !== layer.visibility) { visibility = layer.visibility; } else { // Default to visible @@ -6312,8 +4906,11 @@ S3.gis.yx = [ S3.gis.googleEarthPanel.earth.getFeatures().appendChild(object); }; */ - // Google Streetview control + /** + * Google Streetview control + */ var addGoogleStreetviewControl = function(toolbar) { + var map = toolbar.map; var Clicker = OpenLayers.Class(OpenLayers.Control, { defaults: { @@ -6355,8 +4952,11 @@ S3.gis.yx = [ toolbar.addButton(googleStreetviewButton); }; - // Supports Streetview Control + /** + * Supports Streetview Control + */ var openStreetviewPopup = function(map, location) { + if (!location) { location = map.getCenter(); } @@ -6376,8 +4976,11 @@ S3.gis.yx = [ map.s3.sv_popup.show(); }; - // Measure Controls + /** + * Measure Controls + */ var addMeasureControls = function(toolbar) { + var map = toolbar.map; // Common components var measureSymbolizers = { @@ -6474,8 +5077,11 @@ S3.gis.yx = [ } }; - // Legend Panel as floating DIV + /** + * Legend Panel as floating DIV + */ var addLegendPanel = function(map) { + var map_id = map.s3.id; var div = '
    '; $('#' + map_id).append(div); @@ -6501,7 +5107,12 @@ S3.gis.yx = [ return legendPanel; }; + + /** + * Hide the legend + */ var hideLegend = function(map) { + var map_id = map.s3.id; var outerWidth = $('#' + map_id + ' .map_legend_panel').outerWidth(); $('#' + map_id + ' .map_legend_div').animate({ @@ -6510,7 +5121,12 @@ S3.gis.yx = [ $('#' + map_id + ' .map_legend_tab').removeClass('right') .addClass('left'); }; + + /** + * Show the legend + */ var showLegend = function(map) { + var map_id = map.s3.id; $('#' + map_id + ' .map_legend_div').animate({ marginRight: 0 @@ -6519,8 +5135,11 @@ S3.gis.yx = [ .addClass('right'); }; - // Navigation History + /** + * Navigation History + */ var addNavigationControl = function(toolbar) { + var nav = new OpenLayers.Control.NavigationHistory(); toolbar.map.addControl(nav); nav.activate(); @@ -6539,173 +5158,11 @@ S3.gis.yx = [ toolbar.addButton(navNextButton); }; - // Add button for placing popup on map and selecting a resource - /* - currently unused (@ToDo: Integrate this into the main control) - var resourcePopup; - var urlArray = new Array(), counter = 0; - var addResourcePopupButton = function(map, toolbar, active, resource_array) { - OpenLayers.Handler.PointS3 = OpenLayers.Class(OpenLayers.Handler.Point, { - // Ensure that we propagate Double Clicks (so we can still Zoom) - dblclick: function(evt) { - //OpenLayers.Event.stop(evt); - return true; - }, - CLASS_NAME: 'OpenLayers.Handler.PointS3' - }); - var draftLayer = map.s3.draftLayer; - var control = new OpenLayers.Control.DrawFeature(draftLayer, OpenLayers.Handler.PointS3, { - // custom Callback - 'featureAdded': function(feature) { - // Remove previous point - if (map.s3.lastDraftFeature) { - map.s3.lastDraftFeature.destroy(); - } else if (draftLayer.features.length > 1) { - // Clear the one from the Current Location in S3LocationSelector - draftLayer.features[0].destroy(); - } - // Destroy previously created popups - while (map.popups.length > 0) { - map.removePopup(map.popups[0]); - } - //Generating HTML buttons for adding to the popup - var HTML_inside_popup = '
    '; - for (var i=0; i < resource_array.length ; i++){ - resource = resource_array[i]; - if (resource['location'] == 'popup'){ - var url = map.s3.urlForPopup(feature, resource); - urlArray[counter] = url; - urlArray[counter].feature = feature; - HTML_inside_popup += ' '+resource["label"]+'
    '; - counter++; - } - } - HTML_inside_popup += '
    '; - // Create A popup with buttons of generated HTML - resourcePopup = addPopup(feature, undefined, HTML_inside_popup , undefined); - // Prepare in case user selects a new point - map.s3.lastDraftFeature = feature; - } - }); - - if (toolbar) { - // Toolbar Button - var resourcePopupButton = new GeoExt.Action({ - control: control, - handler: function() { - if (resourcePopupButton.items[0].pressed) { - $('.olMapViewport').addClass('crosshair'); - } else { - $('.olMapViewport').removeClass('crosshair'); - } - }, - map: map, - iconCls: 'drawpoint-off', - allowDepress: true, - enableToggle: true, - toggleGroup: 'controls', - pressed: active - }); - toolbar.add(resourcePopupButton); - // Pass to Global scope for LocationSelectorWidget - map.s3.resourcePopupButton = resourcePopupButton; - } - }; - - var changePopupContent = function (c) { - $('input:radio[name="selectResource"]').change( - function () { - if ($(this).is(':checked')) { - contents = i18n.gis_loading + '...
    '; - resourcePopup.setContentHTML(contents); - loadDetails(urlArray[c], resourcePopup.id + '_contentDiv', resourcePopup); - } - }); - } - S3.gis.changePopupContent = changePopupContent; - */ - - // Add custom point controls to add new markers for different resources to the map - /* - currently unused (@ToDo: Integrate this into the main control) - var addCustomPointControl = function (map, toolbar, active, resource, menu_items) { - OpenLayers.Handler.PointS3 = OpenLayers.Class(OpenLayers.Handler.Point, { - // Ensure that we propagate Double Clicks (so we can still Zoom) - dblclick: function (evt) { - //OpenLayers.Event.stop(evt); - return true; - }, - CLASS_NAME: 'OpenLayers.Handler.PointS3' - }); - var draftLayer = map.s3.draftLayer; - var control = new OpenLayers.Control.DrawFeature(draftLayer, OpenLayers.Handler.PointS3, { - // custom Callback - 'featureAdded': function (feature) { - // Remove previous point - if (map.s3.lastDraftFeature) { - map.s3.lastDraftFeature.destroy(); - } else if (draftLayer.features.length > 1) { - // Clear the one from the Current Location in S3LocationSelector - draftLayer.features[0].destroy(); - } - // Destroy previously created popups - while (map.popups.length > 0) { - map.removePopup(map.popups[0]); - } - if (undefined != map.s3.pointPlaced) { - // Call Custom Call-back - map.s3.pointPlaced(feature, resource) - } - // Prepare in case user selects a new point - map.s3.lastDraftFeature = feature; - } - }); - - if (toolbar) { - // Toolbar Button - if (resource['location'] == 'toolbar') { - var pointButton = new GeoExt.Action({ - control: control, - handler: function () { - if (pointButton.items[0].pressed) { - $('.olMapViewport').addClass('crosshair'); - } else { - $('.olMapViewport').removeClass('crosshair'); - } - }, - map: map, - iconCls: 'drawpoint-off', - tooltip: resource['tooltip'], - allowDepress: true, - enableToggle: true, - toggleGroup: 'controls', - pressed: active - }); - toolbar.add(pointButton); - // Pass to Global scope for LocationSelectorWidget - map.s3.pointButton = pointButton; - } else if (resource['location'] == 'menu') { - var menuButton = new GeoExt.Action({ - control: control, - handler: function () { - if (menuButton.items[0].pressed) { - $('.olMapViewport').addClass('crosshair'); - } else { - $('.olMapViewport').removeClass('crosshair'); - } - }, - iconCls: 'drawpoint-off', - map: map, - text: resource['label'] - }); - var newItem = new Ext.menu.CheckItem(menuButton); - menu_items.push(newItem); - // Pass to Global scope for LocationSelectorWidget - map.s3.menuButton = menuButton; - } - } // ToDo : Add custom right click menu for adding resources when toolbar is not defined - }; */ - - // Point Control to add new Markers to the Map + /** + * Point Control to add new Markers to the Map + */ var addPointControl = function(map, toolbar, active, config) { + OpenLayers.Handler.PointS3 = OpenLayers.Class(OpenLayers.Handler.Point, { // Ensure that we propagate Double Clicks (so we can still Zoom) dblclick: function(/* evt */) { @@ -6730,7 +5187,7 @@ S3.gis.yx = [ while (map.popups.length > 0) { map.removePopup(map.popups[0]); } - if (undefined != map.s3.pointPlaced) { + if (undefined !== map.s3.pointPlaced) { // Call Custom Call-back map.s3.pointPlaced(feature, config); } @@ -6789,8 +5246,11 @@ S3.gis.yx = [ } }; - // Line Control to draw Lines on the Map + /** + * Line Control to draw Lines on the Map + */ var addLineControl = function(map, toolbar, active, config) { + var draftLayer = map.s3.draftLayer; var control = new OpenLayers.Control.DrawFeature(draftLayer, OpenLayers.Handler.Path, { // custom Callback @@ -6806,7 +5266,7 @@ S3.gis.yx = [ while (map.popups.length > 0) { map.removePopup(map.popups[0]); } - if (undefined != map.s3.pointPlaced) { + if (undefined !== map.s3.pointPlaced) { // Call Custom Call-back map.s3.pointPlaced(feature, config); } @@ -6865,8 +5325,11 @@ S3.gis.yx = [ } }; - // Polygon Control to select Areas on the Map + /** + * Polygon Control to select Areas on the Map + */ var addPolygonControl = function(map, toolbar, active, not_regular, config) { + var s3 = map.s3, draftLayer = s3.draftLayer; var control = new OpenLayers.Control.DrawFeature(draftLayer, @@ -6907,7 +5370,7 @@ S3.gis.yx = [ while (map.popups.length > 0) { map.removePopup(map.popups[0]); } - if (undefined != s3.pointPlaced) { + if (undefined !== s3.pointPlaced) { // Call Custom Call-back s3.pointPlaced(feature, config); } @@ -6954,7 +5417,7 @@ S3.gis.yx = [ // Clear the one from the Current Location in S3LocationSelector draftLayer.features[0].destroy(); } - if (undefined != s3.polygonButtonOff) { + if (undefined !== s3.polygonButtonOff) { // Call Custom Call-back (used by S3MapFilter) s3.polygonButtonOff(); } @@ -6984,8 +5447,11 @@ S3.gis.yx = [ } }; - // Floating DIV to explain & control + /** + * Floating DIV to explain & control + */ var addPolygonPanel = function(map_id, control) { + if (undefined === control) { var i, len, @@ -7008,7 +5474,7 @@ S3.gis.yx = [ // Complete the Polygon (which in-turn will call pointPlaced) control.finishSketch(); - if (undefined != s3.polygonPanelFinish) { + if (undefined !== s3.polygonPanelFinish) { // Call Custom Call-back (used by S3MapFilter in WACOP) s3.polygonPanelFinish(); } else { @@ -7032,7 +5498,7 @@ S3.gis.yx = [ } control.deactivate(); $('#' + map_id + '_panel .olMapViewport').removeClass('crosshair'); - if (undefined != s3.polygonPanelClear) { + if (undefined !== s3.polygonPanelClear) { // Call Custom Call-back (used by S3MapFilter in WACOP) s3.polygonPanelClear(); } @@ -7058,8 +5524,11 @@ S3.gis.yx = [ return [lon, lat]; };*/ - // Circle Control to draw circles on the Map + /** + * Circle Control to draw circles on the Map + */ var addCircleControl = function(map, toolbar, active, config) { + var draftLayer = map.s3.draftLayer; var control = new OpenLayers.Control.DrawFeature(draftLayer, OpenLayers.Handler.RegularPolygon, { handlerOptions: { @@ -7132,7 +5601,7 @@ S3.gis.yx = [ while (map.popups.length > 0) { map.removePopup(map.popups[0]); } - if (undefined != map.s3.pointPlaced) { + if (undefined !== map.s3.pointPlaced) { // Call Custom Call-back map.s3.pointPlaced(feature, config); } @@ -7195,12 +5664,13 @@ S3.gis.yx = [ * Check that Map UI is Loaded */ var uiLoaded = function(map_id) { + var dfd = new jQuery.Deferred(); var s3 = S3.gis.maps[map_id].s3; // Test every half-second setTimeout(function working() { - if (s3.mapWin != undefined) { + if (s3.mapWin !== undefined) { dfd.resolve('loaded'); } else if (dfd.state() === 'pending') { // Notify progress @@ -7216,26 +5686,39 @@ S3.gis.yx = [ return dfd.promise(); }; + /** + * Convert hex color into RGB tuple + */ var hex2rgb = function(hex) { - var bigint = parseInt(hex, 16); - var r = (bigint >> 16) & 255; - var g = (bigint >> 8) & 255; - var b = bigint & 255; + + var bigint = parseInt(hex, 16), + r = (bigint >> 16) & 255, + g = (bigint >> 8) & 255, + b = bigint & 255; + return [r, g, b].join(); }; + /** + * Convert RGB tuple into hex color + */ var rgb2hex = function(r, g, b) { + return Number(0x1000000 + Math.round(r)*0x10000 + Math.round(g)*0x100 + Math.round(b)).toString(16).substring(1); }; - // ColorPicker to style Features - // - currently used just by S3LocationSelector - // - need to pickup in postprocess + /** + * ColorPicker to style Features + * - currently used just by S3LocationSelector + * - need to pickup in postprocess + */ var addColorPickerControl = function(map, toolbar) { + var s3 = map.s3, map_id = s3.id, draft_style = s3.options.draft_style, value; + if (draft_style) { if (draft_style.fillOpacity) { value = 'rgba(' + hex2rgb(draft_style.fill) + ',' + draft_style.fillOpacity + ')'; @@ -7302,9 +5785,12 @@ S3.gis.yx = [ ); }; - // Potlatch button for editing OpenStreetMap - // @ToDo: Select a Polygon for editing rather than the whole Viewport + /** + * Potlatch button for editing OpenStreetMap + * @ToDo: Select a Polygon for editing rather than the whole Viewport + */ var addPotlatchButton = function(toolbar) { + var map = toolbar.map; // Toolbar Button var potlatchButton = new Ext.Button({ @@ -7328,11 +5814,15 @@ S3.gis.yx = [ toolbar.addButton(potlatchButton); }; - // Print button on Toolbar to save a screenshot + /** + * Print button on Toolbar to save a screenshot + */ var addPrintButton = function(toolbar) { + + var map = toolbar.map, + map_id = map.s3.id; + // Toolbar Button - var map = toolbar.map; - var map_id = map.s3.id; var printButton = new Ext.Button({ iconCls: 'print', tooltip: i18n.gis_print_tip, @@ -7422,7 +5912,9 @@ S3.gis.yx = [ toolbar.addButton(printButton); }; - // Save button on Toolbar to save the Viewport settings + /** + * Save button on Toolbar to save the Viewport settings + */ var addSaveButton = function(toolbar) { // Toolbar Button var saveButton = new Ext.Button({ @@ -7435,10 +5927,14 @@ S3.gis.yx = [ toolbar.addButton(saveButton); }; - // Save button as floating DIV to save the Viewport settings + /** + * Save button as floating DIV to save the Viewport settings + */ var addSavePanel = function(map) { - var s3 = map.s3; - var map_id = s3.id; + + var s3 = map.s3, + map_id = s3.id; + if ($('#' + map_id + ' .map_save_panel').length) { // We already have a Panel // (this happens when switching between full-screen & embedded) @@ -7462,8 +5958,11 @@ S3.gis.yx = [ }); }; - // Save Click Handler for floating DIV + /** + * Save Click Handler for floating DIV + */ var saveClickHandler = function(map) { + var map_id = map.s3.id; $('#' + map_id + ' .map_save_panel').removeClass('off'); // Remove any 'saved' notification @@ -7475,8 +5974,11 @@ S3.gis.yx = [ nameConfig(map); }; - // Name the Config for floating DIV + /** + * Name the Config for floating DIV + */ var nameConfig = function(map) { + var s3 = map.s3, map_id = s3.id, options = s3.options, @@ -7489,9 +5991,11 @@ S3.gis.yx = [ name = ''; } var save_button = $('#' + map_id + ' .map_save_button'); + // Prompt user for the name - var input_id = map_id + '_save'; - var name_input = $('#' + input_id); + var input_id = map_id + '_save', + name_input = $('#' + input_id); + if (!name_input.length) { //name_input = ''; var hint = ''; @@ -7554,8 +6058,11 @@ S3.gis.yx = [ }); }; - // Save the Config (used by both Toolbar & Floating DIV) + /** + * Save the Config (used by both Toolbar & Floating DIV) + */ var saveConfig = function(map, temp, zoom) { + var s3 = map.s3; var map_id = s3.id; // Show Throbber @@ -7656,8 +6163,10 @@ S3.gis.yx = [ return config_id; }; - // Get the State of the Map - // so that it can be Saved & Reloaded later e.g. for Printing + /** + * Get the State of the Map + * - so that it can be Saved & Reloaded later e.g. for Printing + */ var getState = function(map) { // State stored a a JSON array @@ -7713,15 +6222,20 @@ S3.gis.yx = [ return state; }; - // Throbber as floating DIV to see when map layers are loading + /** + * Throbber as floating DIV to see when map layers are loading + */ var addThrobber = function(map) { - var s3 = map.s3; - var map_id = s3.id; + + var s3 = map.s3, + map_id = s3.id; + if ($('#' + map_id + ' .layer_throbber').length) { // We already have a Throbber // (this happens when switching between full-screen & embedded) return; } + var div = '
    q&&(q=Math.max(0,z+q));if(null==y||y>z)y=z;y=Number(y);0>y&&(y=Math.max(0,z+y));for(q=Number(q||0);qk?$("#"+a.id).removeClass("a0 a1 a2 a3").addClass("a4"):1191>k?$("#"+a.id).removeClass("a0 a1 a2 a4").addClass("a3"):1684>k?$("#"+a.id).removeClass("a0 a1 a3 a4").addClass("a2"):2384>k?$("#"+a.id).removeClass("a0 a2 a3 a4").addClass("a1"): $("#"+a.id).removeClass("a1 a2 a3 a4").addClass("a0")});c.windowHide||(d.show(),d.maximize());a.mapWin=d};S3.gis.addMapWindow=aa;var ca=function(b){b=b.s3;var a=b.options.west_collapsed||!1,c=new Ext.Panel({header:!1,border:!1,split:!0,items:b.west_panel_items});a=new Ext.Panel({cls:"gis_west",region:"west",header:!1,border:!0,autoWidth:Ext.isChrome?!1:!0,width:250,collapsible:!0,collapseMode:"mini",collapsed:a,items:[c]});return b.westPanelContainer=a},da=function(b){var a=b.s3,c=a.options;if(c.toolbar)var d= wa(b);else{if(c.draw_feature){var e="active"==c.draw_feature;U(b,null,e)}c.draw_line&&(e="active"==c.draw_line,V(b,null,e));c.draw_polygon&&(e="active"==c.draw_polygon,W(b,null,e,!0));c.draw_circle&&(e="active"==c.draw_circle,X(b,null,e));e=b.s3;var g=e.id;if(!$("#"+g+" .layer_throbber").length){var f='
    ";void 0!=a.desc&&(r+='
    '+a.desc+"
    ");if(void 0!=a.src||void 0!=a.src_url){var v='
    ';void 0!=a.src_url?(v+='',v=void 0!=a.src?v+a.src:v+a.src_url,v+=""):v+=a.src;r+=v+"
    "}r+="
    ";var A=Q(b,a);v=A[0];A=A[1]; -var C=[new OpenLayers.Strategy.ZoomBBOX({ratio:1.5})];f&&C.push(new OpenLayers.Strategy.Refresh({force:!0,interval:1E3*f}));(h||a.cluster)&&C.push(new OpenLayers.Strategy.AttributeCluster({attribute:l,distance:n,threshold:h}));d=new OpenLayers.Layer.Vector(d,{dir:k,projection:m,protocol:new OpenLayers.Protocol.HTTP({url:e,format:p,readWithPOST:g}),legendURL:A,strategies:C,styleMap:v,s3_layer_id:a.id,s3_layer_type:w,s3_style:a.style,s3_url_format:a.url_format});d.legendTitle=r;void 0!=a.popup_format&& -(d.s3_popup_format=a.popup_format);d.setVisibility(c);d.events.on({loadstart:L,loadend:M,visibilitychanged:S});b.addLayer(d)},Da=function(b){var a=b.s3.options.Google;if(a.MapMaker||a.MapMakerHybrid){if(a.Satellite){var c=new OpenLayers.Layer.Google(a.Satellite.name,{type:G_SATELLITE_MAP,sphericalMercator:!0,s3_layer_id:a.Satellite.id,s3_layer_type:"google"});b.addLayer(c);"satellite"==a.Base&&b.setBaseLayer(c)}a.Maps&&(c=new OpenLayers.Layer.Google(a.Maps.name,{type:G_NORMAL_MAP,sphericalMercator:!0, -s3_layer_id:a.Maps.id,s3_layer_type:"google"}),b.addLayer(c),"maps"==a.Base&&b.setBaseLayer(c));a.Hybrid&&(c=new OpenLayers.Layer.Google(a.Hybrid.name,{type:G_HYBRID_MAP,sphericalMercator:!0,s3_layer_id:a.Hybrid.id,s3_layer_type:"google"}),b.addLayer(c),"maps"==a.Base&&b.setBaseLayer(c));a.Terrain&&(c=new OpenLayers.Layer.Google(a.Terrain.name,{type:G_PHYSICAL_MAP,sphericalMercator:!0,s3_layer_id:a.Terrain.id,s3_layer_type:"google"}),b.addLayer(c),"terrain"==a.Base&&b.setBaseLayer(c));a.MapMaker&& -(c=new OpenLayers.Layer.Google(a.MapMaker.name,{type:G_MAPMAKER_NORMAL_MAP,sphericalMercator:!0,s3_layer_id:c.id,s3_layer_type:"google"}),b.addLayer(c),"mapmaker"==a.Base&&b.setBaseLayer(c));a.MapMakerHybrid&&(c=new OpenLayers.Layer.Google(a.MapMakerHybrid.name,{type:G_MAPMAKER_HYBRID_MAP,sphericalMercator:!0,s3_layer_id:c.id,s3_layer_type:"google"}),b.addLayer(c),"mapmakerhybrid"==a.Base&&b.setBaseLayer(c))}else a.Satellite&&(c=new OpenLayers.Layer.Google(a.Satellite.name,{type:"satellite",numZoomLevels:22, -s3_layer_id:a.Satellite.id,s3_layer_type:"google"}),b.addLayer(c),"satellite"==a.Base&&b.setBaseLayer(c)),a.Maps&&(c=new OpenLayers.Layer.Google(a.Maps.name,{numZoomLevels:20,s3_layer_id:a.Maps.id,s3_layer_type:"google"}),b.addLayer(c),"maps"==a.Base&&b.setBaseLayer(c)),a.Hybrid&&(c=new OpenLayers.Layer.Google(a.Hybrid.name,{type:"hybrid",numZoomLevels:20,s3_layer_id:a.Hybrid.id,s3_layer_type:"google"}),b.addLayer(c),"hybrid"==a.Base&&b.setBaseLayer(c)),a.Terrain&&(c=new OpenLayers.Layer.Google(a.Terrain.name, -{type:"terrain",s3_layer_id:a.Terrain.id,s3_layer_type:"google"}),b.addLayer(c),"terrain"==a.Base&&b.setBaseLayer(c))},Ia=function(b,a){var c=a.marker,d=a.name,e=a.url,g=c.h,f=t+c.i,k=c.w;var l=void 0!=a.waypoints?a.waypoints:!0;var n=void 0!=a.tracks?a.tracks:!0;var h=void 0!=a.routes?a.routes:!0;c=void 0!=a.visibility?a.visibility:!0;if(void 0!=a.dir){var m=a.dir;-1==$.inArray(m,b.s3.dirs)&&b.s3.dirs.push(m)}else m="";var w=void 0!=a.opacity?a.opacity:1;var r=void 0!=a.cluster_distance?a.cluster_distance: -20;var v=void 0!=a.cluster_threshold?a.cluster_threshold:2;var A=OpenLayers.Util.extend({},OpenLayers.Feature.Vector.style["default"]);l?(A.graphicOpacity=w,A.graphicWidth=k,A.graphicHeight=g,A.graphicXOffset=-(k/2),A.graphicYOffset=-g,A.externalGraphic=f):A.externalGraphic="";A.strokeColor="blue";A.strokeWidth=6;A.strokeOpacity=w;a=new OpenLayers.Layer.Vector(d,{dir:m,projection:q,strategies:[new OpenLayers.Strategy.Fixed,new OpenLayers.Strategy.Cluster({distance:r,threshold:v})],legendURL:f,s3_layer_id:a.id, -s3_layer_type:"gpx",style:A,protocol:new OpenLayers.Protocol.HTTP({url:e,format:new OpenLayers.Format.GPX({extractAttributes:!0,extractWaypoints:l,extractTracks:n,extractRoutes:h})})});a.setVisibility(c);a.events.on({loadstart:L,loadend:M,visibilitychanged:S});b.addLayer(a)},La=function(b,a){var c=a.name;var d=b.s3;var e=a.url;var g=void 0!=a.title?a.title:"name";var f=void 0!=a.body?a.body:"description";var k=void 0!=a.refresh?a.refresh:900;var l=void 0!=a.visibility?a.visibility:!0;if(void 0!=a.dir){var n= -a.dir;-1==$.inArray(n,d.dirs)&&d.dirs.push(n)}else n="";d=void 0!=a.cluster_distance?a.cluster_distance:20;var h=void 0!=a.cluster_threshold?a.cluster_threshold:2;var m=Q(b,a)[0],w=new OpenLayers.Format.KML({extractStyles:!0,extractAttributes:!0,maxDepth:2}),r=[new OpenLayers.Strategy.Fixed];k&&r.push(new OpenLayers.Strategy.Refresh({force:!0,interval:1E3*k}));h&&r.push(new OpenLayers.Strategy.Cluster({distance:d,threshold:h}));a=new OpenLayers.Layer.Vector(c,{dir:n,projection:q,protocol:new OpenLayers.Protocol.HTTP({url:e, -format:w}),strategies:r,styleMap:m,s3_layer_id:a.id,s3_layer_type:"kml",s3_style:a.style});a.title=g;a.body=f;a.setVisibility(l);a.events.on({loadstart:L,loadend:M,visibilitychanged:S});b.addLayer(a)},Ca=function(b,a){var c=a.name,d=[a.url1];void 0!=a.url2&&d.push(a.url2);void 0!=a.url3&&d.push(a.url3);var e=void 0!=a.visibility?a.visibility:!0;if(void 0!=a.dir){var g=a.dir;-1==$.inArray(g,b.s3.dirs)&&b.s3.dirs.push(g)}else g="";g=new OpenLayers.Layer.TMS(c,d,{dir:g,type:"png",getURL:Pa,displayOutsideMaxExtent:!0, -numZoomLevels:void 0!=a.zoomLevels?a.zoomLevels:19,isBaseLayer:void 0!=a.base?a.base:!0,s3_layer_id:a.id,s3_layer_type:"openstreetmap"});void 0!=a.attribution&&(g.attribution=a.attribution);g.setVisibility(e);g.events.on({loadstart:L,loadend:M});b.addLayer(g);a._base&&b.setBaseLayer(g)},Pa=function(b){var a=this.map.getResolution(),c=Math.round((b.left-this.maxExtent.left)/(a*this.tileSize.w));b=Math.round((this.maxExtent.top-b.top)/(a*this.tileSize.h));a=this.map.getZoom();var d=Math.pow(2,a);if(0> -b||b>=d)return OpenLayers.Util.getImagesLocation()+"404.png";c=a+"/"+(c%d+d)%d+"/"+b+"."+this.type;b=this.url;b instanceof Array&&(b=this.selectUrl(c,b));return b+c},Ma=function(b){var a=S3.gis.openweathermap,c=b.s3.options.layers_openweathermap,d;for(d in c){var e=new OpenLayers.Layer.XYZ(c[d].name,"https://tile.openweathermap.org/map/"+d+"/${z}/${x}/${y}.png?appid="+a,{dir:c[d].dir,isBaseLayer:!1,s3_layer_id:c[d].id,s3_layer_type:"openweathermap",type:"png"});e.setVisibility(c[d].visibility);e.events.on({loadstart:L, -loadend:M});b.addLayer(e)}},Fa=function(b,a){var c=a.name,d=[a.url];void 0!=a.url2&&d.push(a.url2);void 0!=a.url3&&d.push(a.url3);var e=a.layername;var g=void 0!=a.zoomLevels?a.zoomLevels:19;if(void 0!=a.dir){var f=a.dir;-1==$.inArray(f,b.s3.dirs)&&b.s3.dirs.push(f)}else f="";f=new OpenLayers.Layer.TMS(c,d,{dir:f,s3_layer_id:a.id,s3_layer_type:"tms",layername:e,type:void 0!=a.format?a.format:"png",numZoomLevels:g});void 0!=a.attribution&&(f.attribution=a.attribution);f.events.on({loadstart:L,loadend:M}); -b.addLayer(f);a._base&&b.setBaseLayer(f)},Na=function(b,a){var c=a.featureType,d,e=a.name,g=a.title,f=a.url;void 0!=a.username&&void 0!=a.password&&(f=f.replace("://","://"+a.username+":"+a.password+"@"));var k=void 0!=a.featureNS?a.featureNS:null;if(void 0!=a.schema)var l=a.schema;var n=void 0!=a.version?a.version:"1.1.0";var h=void 0!=a.geometryName?a.geometryName:"the_geom";var m=void 0!=a.visibility?a.visibility:!0;if(void 0!=a.dir){var w=a.dir;-1==$.inArray(w,b.s3.dirs)&&b.s3.dirs.push(w)}else w= -"";var r=void 0!=a.cluster_attribute?a.cluster_attribute:"colour";var v=void 0!=a.cluster_distance?a.cluster_distance:20;var A=void 0!=a.cluster_threshold?a.cluster_threshold:2;var C=void 0!=a.refresh?a.refresh:!1;var u=[new OpenLayers.Strategy.BBOX({ratio:1.5})];C&&u.push(new OpenLayers.Strategy.Refresh({force:!0,interval:1E3*C}));A&&u.push(new OpenLayers.Strategy.AttributeCluster({attribute:r,distance:v,threshold:A}));r=void 0!=a.projection?a.projection:4326;"1.0.0"==n?v="EPSG:"+r:(v="urn:ogc:def:crs:EPSG::"+ -r,S3.gis.yx.includes(r)&&(d=new OpenLayers.Format.WFST({version:n,featureType:c,featureNS:k,featurePrefix:"feature",geometryName:h,srsName:v,schema:l,xy:!1})));c=new OpenLayers.Protocol.WFS({url:f,version:n,featureType:c,featureNS:k,format:d,geometryName:h,srsName:v,schema:l});k='
    '+e+"
    ";void 0!=a.desc&&(k+='
    '+a.desc+"
    ");if(void 0!=a.src||void 0!=a.src_url)d='
    ',void 0!=a.src_url? -(d+='',d=void 0!=a.src?d+a.src:d+a.src_url,d+=""):d+=a.src,k+=d+"
    ";k+="
    ";h=Q(b,a);d=h[0];h=h[1];r=4326==r?q:new OpenLayers.Projection("EPSG:"+r);a=new OpenLayers.Layer.Vector(e,{maxFeatures:1E3,strategies:u,dir:w,legendURL:h,projection:r,protocol:c,styleMap:d,s3_layer_id:a.id,s3_layer_type:"wfs",s3_style:a.style});a.legendTitle=k;a.title=g;a.setVisibility(m);a.events.on({loadstart:L,loadend:M,visibilitychanged:S});b.addLayer(a)},Ga=function(b, -a){var c=a.name,d=a.url;a.username&&a.password&&(d=d.replace("://","://"+a.username+":"+a.password+"@"));var e=a.layers;var g=void 0!=a.visibility?a.visibility:!0;if(void 0!=a.dir){var f=a.dir;-1==$.inArray(f,b.s3.dirs)&&b.s3.dirs.push(f)}else f="";var k=void 0!=a.base?a.base:!1;var l=void 0!=a.transparent?a.transparent:!0;var n=void 0!=a.format?a.format:"image/png";var h=void 0!=a.version?a.version:"1.1.1";var m=a.map?a.map:"";var w=a.style?a.style:"";var r=void 0!=a.bgcolor?"0x"+a.bgcolor:"";var v= -void 0!=a.buffer?a.buffer:0;var A=void 0!=a.tiled?a.tiled:!1;var C=void 0!=a.singleTile?a.singleTile:!1;var u=void 0!=a.opacity?a.opacity:1;var I=void 0!=a.queryable?a.queryable:1;var H='
    '+c+"
    ";void 0!=a.desc&&(H+='
    '+a.desc+"
    ");if(b.s3.options.metadata){if(void 0!=a.post_id)if(i18n.gis_metadata){var B=i18n.gis_metadata;var D=S3.Ap.concat("/cms/page/"+a.post_id)}else B=i18n.gis_metadata_edit,D=S3.Ap.concat("/cms/post/"+ -a.post_id+"/update?layer_id="+a.id);else i18n.gis_metadata_create&&(B=i18n.gis_metadata_create,D=S3.Ap.concat("/cms/post/create?layer_id="+a.id));B&&(H+='")}else if(void 0!=a.src||void 0!=a.src_url)B='
    ',void 0!=a.src_url?(B+='',B=void 0!=a.src?B+a.src:B+a.src_url,B+=""):B+=a.src,H+=B+"
    ";H+="
    ";if(void 0!=a.legendURL)var x=a.legendURL;f=new OpenLayers.Layer.WMS(c, -d,{layers:e,transparent:l},{dir:f,isBaseLayer:k,singleTile:C,wrapDateLine:!0,s3_layer_id:a.id,s3_layer_type:"wms",queryable:I,visibility:g});m&&(f.params.MAP=m);n&&(f.params.FORMAT=n);h&&(f.params.VERSION=h);w&&(f.params.STYLES=w);r&&(f.params.BGCOLOR=r);A&&(f.params.TILESORIGIN=[b.maxExtent.left,b.maxExtent.bottom]);k||(f.opacity=u,f.buffer=v?v:0);f.legendTitle=H;x&&(f.legendURL=x);f.events.on({loadstart:L,loadend:M,visibilitychanged:S});b.addLayer(f);a._base&&b.setBaseLayer(f)},Ha=function(b,a){var c= -a.name,d=[a.url];void 0!=a.url2&&d.push(a.url2);void 0!=a.url3&&d.push(a.url3);var e=a.layername;var g=void 0!=a.zoomLevels?a.zoomLevels:19;if(void 0!=a.dir){var f=a.dir;-1==$.inArray(f,b.s3.dirs)&&b.s3.dirs.push(f)}else f="";f=new OpenLayers.Layer.XYZ(c,d,{dir:f,s3_layer_id:a.id,s3_layer_type:"xyz",layername:e,type:void 0!=a.format?a.format:"png",numZoomLevels:g});void 0!=a.attribution&&(f.attribution=a.attribution);f.events.on({loadstart:L,loadend:M});b.addLayer(f);a._base&&b.setBaseLayer(f)},sa= -function(b){var a=b.s3.options,c=new OpenLayers.Control.Navigation;a.no_zoom_wheel&&(c.zoomWheelEnabled=!1);b.addControl(c);void 0==a.zoomcontrol&&b.addControl(new OpenLayers.Control.Zoom);b.addControl(new OpenLayers.Control.ArgParser);b.addControl(new OpenLayers.Control.Attribution);void 0==a.scaleline&&b.addControl(new OpenLayers.Control.ScaleLine);"mgrs"==a.mouse_position?b.addControl(new OpenLayers.Control.MGRSMousePosition):a.mouse_position&&b.addControl(new OpenLayers.Control.MousePosition); -void 0==a.permalink&&b.addControl(new OpenLayers.Control.Permalink);if(void 0==a.overview){a={};c=b.options;for(var d in c)"controls"!=d&&(a[d]=c[d]);b.addControl(new OpenLayers.Control.OverviewMap({mapOptions:a}))}Qa(b)},Qa=function(b){b.events.register("featureover",this,Ra);b.events.register("featureout",this,ia);b.events.register("featureclick",this,Sa)},Ra=function(b){var a=b.feature,c=a.layer,d;a.renderIntent="select";c.drawFeature(a);if(-1==["OpenLayers.Handler.PointS3","OpenLayers.Handler.Path", -"OpenLayers.Handler.Polygon","OpenLayers.Handler.RegularPolygon"].indexOf(c.name)){var e=c.map;-1==e.s3.layers_nopopups.indexOf(c.name)&&(S3.gis.timeouts[a.id]=setTimeout(function(){if(!a.cluster&&a.geometry){var g=a.attributes,f=a.geometry.getBounds().getCenterLonLat();if(void 0!=c.s3_popup_format){_.templateSettings={interpolate:/\{(.+?)\}/g};var k=c.s3_popup_format;var l=_.template(k);var n={},h=k.split("{");for(d=0;d'):void 0==c&&(c=i18n.gis_loading+'...
    ');var g=b.geometry.getBounds().getCenterLonLat();c=new OpenLayers.Popup.FramedCloud(e,g,new OpenLayers.Size(400,400),c,null,!0,Ta);b.popup=c;c.feature=b;c.maxSize=new OpenLayers.Size(750,660);g=b.layer.map;b.map=g;g.addPopup(c);d||void 0==a||ja(a,e+"_contentDiv",c);return c};S3.gis.addPopup=ka;S3.gis.popupLoaded= -function(b){$("#"+b+"_contentDiv iframe").contents().find("#popup").show();var a=S3.gis.maps,c,d;for(c in a){var e=a[c];var g=e.popups;var f=0;for(d=g.length;f';$("#"+a).html(e);return!1})}catch(e){}},error:function(d,e,g){d="UNAUTHORIZED"==g?i18n.gis_requires_login:d.responseText;$("#"+a+"_contentDiv").html(d);c.updateSize()}})},Sa=function(b){var a=b.feature,c=a.layer,d=c.map,e=d.s3;if(-1==["OpenLayers.Handler.PointS3","OpenLayers.Handler.Path","OpenLayers.Handler.Polygon", -"OpenLayers.Handler.RegularPolygon"].indexOf(c.name)&&-1==e.layers_nopopups.indexOf(c.name))if("openweathermap"==c.s3_layer_type){var g=c.options.getPopupHtml(a.attributes.station);var f=new OpenLayers.Popup("Popup",a.geometry.getBounds().getCenterLonLat(),new OpenLayers.Size(c.options.popupX,c.options.popupY),g,"Station",!1);a.popup=f;f.feature=a;d.addPopup(f,!0)}else{var k=a.geometry,l;if("OpenLayers.Geometry.Point"!=k.CLASS_NAME)for(d=0,l=e.clicking.length;d";var m=n.length,w=e.id;for(d=0;d",1)[0]}else e=void 0!=r.popup?r.popup.split("
    ",1)[0]:r[b];void 0!=r.url?h+="
  • "+e+"
  • ":void 0!=c.s3_url_format?(_.templateSettings={interpolate:/\{(.+?)\}/g},f=_.template(c.s3_url_format),r.id.constructor===Array&&(r.id=r.id[0]),f=f(r),h+="
  • "+e+"
  • "):h+="
  • "+e+"
  • "}f=null;h+="";h+=""}else if(d=c.s3_layer_type,"kml"==d){r=a.attributes;if(void 0!=a.style.balloonStyle)h=a.style.balloonStyle.replace(/{([^{}]*)}/g,function(H,B){B=r[B];return"string"===typeof B||"number"===typeof B?B:H});else for(d=typeof r[b],d="object"==d?r[b].value:r[b],h="

    "+d+"

    ",c=c.body.split(" "),e=0;e
    '+d+"
    "}else u=void 0!=r[c[e]]?'
    '+r[c[e]]+"
    ":"";h+=u}-1!=h.search(""+h.replace(/",$.each(r,function(H,B){"id_orig"==H&&(H="id");u='
    '+H+':
    '+B+"
    ";h+=u}),h+="
    "; -else if("wfs"==d)r=a.attributes,d=r[b],h="

    "+d+"

    ",$.each(r,function(H,B){u='
    '+H+':
    '+B+"
    ";h+=u});else if(void 0!=a.attributes.url)f=a.attributes.url;else if(void 0!=c.s3_url_format)_.templateSettings={interpolate:/\{(.+?)\}/g},f=_.template(c.s3_url_format),a.attributes.id.constructor===Array&&(a.attributes.id=a.attributes.id[0]),f=f(a.attributes);else{r=a.attributes;e=void 0==r.name?"":"

    "+ -r.name+"

    ";c=void 0==r.description?"":"

    "+r.description+"

    ";d=void 0==r.link?"":''+r.link+"";if(void 0==r.data)b="";else if(0===r.data.indexOf("http://")){g=!0;var I=S3.uid();b='
    '+i18n.gis_loading+"...
    "}else b="

    "+r.data+"

    ";k=void 0==r.image?"":0===r.image.indexOf("http://")?'':"";h=e+c+d+b+k}f=ka(a,f,h);g&&ja(a.attributes.data,I,f)}},Ta=function(){var b= -this.feature;if(b){var a=b.layer;b.renderIntent="default";a&&a.drawFeature(b);b.popup&&(a&&a.map.removePopup(b.popup),b.popup.destroy(),delete b.popup);if(b.id)$("#"+b.id+"_popup").remove();else{var c=b.map;if(c)for(var d=c.popups,e=d.length-1;-1
    ");var g=S3.gis.maps[b];$.getS3(a,function(f){e.html(f); -g.popups[0].updateSize();f=$(d+" .dropdown-toggle");f.length&&(e.parent().css("overflow","visible").parent().css("overflow","visible"),f.dropdown())},"html")};S3.gis.zoomToSelectedFeature=function(b,a,c,d){b=S3.gis.maps[b];a=new OpenLayers.LonLat(a,c);c=b.getZoom();b.setCenter(a,c+d);for(d=0;d
    ');var c=new GeoExt.LegendPanel({title:i18n.gis_legend,autoScroll:!0,border:!1}),d=$("#"+a+" .map_legend_panel");d=Ext.get(d[0]);c.render(d);$("#"+a+" .map_legend_tab").click(function(){if($(this).hasClass("right")){var e=b.s3.id,g=$("#"+e+" .map_legend_panel").outerWidth();$("#"+e+" .map_legend_div").animate({marginRight:"-"+ -g+"px"});$("#"+e+" .map_legend_tab").removeClass("right").addClass("left")}else ha(b)});return c},ha=function(b){b=b.s3.id;$("#"+b+" .map_legend_div").animate({marginRight:0});$("#"+b+" .map_legend_tab").removeClass("left").addClass("right")},Va=function(b){var a=new OpenLayers.Control.NavigationHistory;b.map.addControl(a);a.activate();var c=new Ext.Button({iconCls:"back",tooltip:i18n.gis_navPrevious,handler:a.previous.trigger});a=new Ext.Button({iconCls:"next",tooltip:i18n.gis_navNext,handler:a.next.trigger}); -b.addButton(c);b.addButton(a)},U=function(b,a,c,d){OpenLayers.Handler.PointS3=OpenLayers.Class(OpenLayers.Handler.Point,{dblclick:function(){return!0},CLASS_NAME:"OpenLayers.Handler.PointS3"});var e=b.s3.draftLayer,g=new OpenLayers.Control.DrawFeature(e,OpenLayers.Handler.PointS3,{featureAdded:function(l){for(b.s3.lastDraftFeature?b.s3.lastDraftFeature.destroy():1Click anywhere on the map to begin drawing. Double click to complete the area or click the Finish button below.
    '); -var g=S3.gis.maps[b].s3;$("#"+b+" .map_polygon_finish").click(function(){a.finishSketch();void 0!=g.polygonPanelFinish?g.polygonPanelFinish():(g.lastDraftFeature?g.lastDraftFeature.destroy():1";void 0!==a.desc&&(r+='
    '+a.desc+"
    ");if(void 0!==a.src||void 0!==a.src_url){var v='
    ';void 0!==a.src_url?(v+='',v=void 0!==a.src?v+a.src:v+a.src_url,v+=""):v+=a.src;r+=v+"
    "}r+="
    ";var A=Q(b, +a);v=A[0];A=A[1];var C=[new OpenLayers.Strategy.ZoomBBOX({ratio:1.5})];f&&C.push(new OpenLayers.Strategy.Refresh({force:!0,interval:1E3*f}));(h||a.cluster)&&C.push(new OpenLayers.Strategy.AttributeCluster({attribute:l,distance:n,threshold:h}));d=new OpenLayers.Layer.Vector(d,{dir:k,projection:m,protocol:new OpenLayers.Protocol.HTTP({url:e,format:p,readWithPOST:g}),legendURL:A,strategies:C,styleMap:v,s3_layer_id:a.id,s3_layer_type:w,s3_style:a.style,s3_url_format:a.url_format});d.legendTitle=r;void 0!== +a.popup_format&&(d.s3_popup_format=a.popup_format);d.setVisibility(c);d.events.on({loadstart:L,loadend:M,visibilitychanged:S});b.addLayer(d)},Da=function(b){var a=b.s3.options.Google;if(a.MapMaker||a.MapMakerHybrid){if(a.Satellite){var c=new OpenLayers.Layer.Google(a.Satellite.name,{type:G_SATELLITE_MAP,sphericalMercator:!0,s3_layer_id:a.Satellite.id,s3_layer_type:"google"});b.addLayer(c);"satellite"==a.Base&&b.setBaseLayer(c)}a.Maps&&(c=new OpenLayers.Layer.Google(a.Maps.name,{type:G_NORMAL_MAP, +sphericalMercator:!0,s3_layer_id:a.Maps.id,s3_layer_type:"google"}),b.addLayer(c),"maps"==a.Base&&b.setBaseLayer(c));a.Hybrid&&(c=new OpenLayers.Layer.Google(a.Hybrid.name,{type:G_HYBRID_MAP,sphericalMercator:!0,s3_layer_id:a.Hybrid.id,s3_layer_type:"google"}),b.addLayer(c),"maps"==a.Base&&b.setBaseLayer(c));a.Terrain&&(c=new OpenLayers.Layer.Google(a.Terrain.name,{type:G_PHYSICAL_MAP,sphericalMercator:!0,s3_layer_id:a.Terrain.id,s3_layer_type:"google"}),b.addLayer(c),"terrain"==a.Base&&b.setBaseLayer(c)); +a.MapMaker&&(c=new OpenLayers.Layer.Google(a.MapMaker.name,{type:G_MAPMAKER_NORMAL_MAP,sphericalMercator:!0,s3_layer_id:c.id,s3_layer_type:"google"}),b.addLayer(c),"mapmaker"==a.Base&&b.setBaseLayer(c));a.MapMakerHybrid&&(c=new OpenLayers.Layer.Google(a.MapMakerHybrid.name,{type:G_MAPMAKER_HYBRID_MAP,sphericalMercator:!0,s3_layer_id:c.id,s3_layer_type:"google"}),b.addLayer(c),"mapmakerhybrid"==a.Base&&b.setBaseLayer(c))}else a.Satellite&&(c=new OpenLayers.Layer.Google(a.Satellite.name,{type:"satellite", +numZoomLevels:22,s3_layer_id:a.Satellite.id,s3_layer_type:"google"}),b.addLayer(c),"satellite"==a.Base&&b.setBaseLayer(c)),a.Maps&&(c=new OpenLayers.Layer.Google(a.Maps.name,{numZoomLevels:20,s3_layer_id:a.Maps.id,s3_layer_type:"google"}),b.addLayer(c),"maps"==a.Base&&b.setBaseLayer(c)),a.Hybrid&&(c=new OpenLayers.Layer.Google(a.Hybrid.name,{type:"hybrid",numZoomLevels:20,s3_layer_id:a.Hybrid.id,s3_layer_type:"google"}),b.addLayer(c),"hybrid"==a.Base&&b.setBaseLayer(c)),a.Terrain&&(c=new OpenLayers.Layer.Google(a.Terrain.name, +{type:"terrain",s3_layer_id:a.Terrain.id,s3_layer_type:"google"}),b.addLayer(c),"terrain"==a.Base&&b.setBaseLayer(c))},Ia=function(b,a){var c=a.marker,d=a.name,e=a.url,g=c.h,f=t+c.i,k=c.w;var l=void 0!==a.waypoints?a.waypoints:!0;var n=void 0!==a.tracks?a.tracks:!0;var h=void 0!==a.routes?a.routes:!0;c=void 0!==a.visibility?a.visibility:!0;if(void 0!==a.dir){var m=a.dir;-1==$.inArray(m,b.s3.dirs)&&b.s3.dirs.push(m)}else m="";var w=void 0!==a.opacity?a.opacity:1;var r=void 0!==a.cluster_distance?a.cluster_distance: +20;var v=void 0!==a.cluster_threshold?a.cluster_threshold:2;var A=OpenLayers.Util.extend({},OpenLayers.Feature.Vector.style["default"]);l?(A.graphicOpacity=w,A.graphicWidth=k,A.graphicHeight=g,A.graphicXOffset=-(k/2),A.graphicYOffset=-g,A.externalGraphic=f):A.externalGraphic="";A.strokeColor="blue";A.strokeWidth=6;A.strokeOpacity=w;a=new OpenLayers.Layer.Vector(d,{dir:m,projection:q,strategies:[new OpenLayers.Strategy.Fixed,new OpenLayers.Strategy.Cluster({distance:r,threshold:v})],legendURL:f,s3_layer_id:a.id, +s3_layer_type:"gpx",style:A,protocol:new OpenLayers.Protocol.HTTP({url:e,format:new OpenLayers.Format.GPX({extractAttributes:!0,extractWaypoints:l,extractTracks:n,extractRoutes:h})})});a.setVisibility(c);a.events.on({loadstart:L,loadend:M,visibilitychanged:S});b.addLayer(a)},La=function(b,a){var c=a.name;var d=b.s3;var e=a.url;var g=void 0!==a.title?a.title:"name";var f=void 0!==a.body?a.body:"description";var k=void 0!==a.refresh?a.refresh:900;var l=void 0!==a.visibility?a.visibility:!0;if(void 0!== +a.dir){var n=a.dir;-1==$.inArray(n,d.dirs)&&d.dirs.push(n)}else n="";d=void 0!==a.cluster_distance?a.cluster_distance:20;var h=void 0!==a.cluster_threshold?a.cluster_threshold:2;var m=Q(b,a)[0],w=new OpenLayers.Format.KML({extractStyles:!0,extractAttributes:!0,maxDepth:2}),r=[new OpenLayers.Strategy.Fixed];k&&r.push(new OpenLayers.Strategy.Refresh({force:!0,interval:1E3*k}));h&&r.push(new OpenLayers.Strategy.Cluster({distance:d,threshold:h}));a=new OpenLayers.Layer.Vector(c,{dir:n,projection:q,protocol:new OpenLayers.Protocol.HTTP({url:e, +format:w}),strategies:r,styleMap:m,s3_layer_id:a.id,s3_layer_type:"kml",s3_style:a.style});a.title=g;a.body=f;a.setVisibility(l);a.events.on({loadstart:L,loadend:M,visibilitychanged:S});b.addLayer(a)},Ca=function(b,a){var c=a.name,d=[a.url1];void 0!==a.url2&&d.push(a.url2);void 0!==a.url3&&d.push(a.url3);var e=void 0!==a.visibility?a.visibility:!0;if(void 0!==a.dir){var g=a.dir;-1==$.inArray(g,b.s3.dirs)&&b.s3.dirs.push(g)}else g="";g=new OpenLayers.Layer.TMS(c,d,{dir:g,type:"png",getURL:Pa,displayOutsideMaxExtent:!0, +numZoomLevels:void 0!==a.zoomLevels?a.zoomLevels:19,isBaseLayer:void 0!==a.base?a.base:!0,s3_layer_id:a.id,s3_layer_type:"openstreetmap"});void 0!==a.attribution&&(g.attribution=a.attribution);g.setVisibility(e);g.events.on({loadstart:L,loadend:M});b.addLayer(g);a._base&&b.setBaseLayer(g)},Pa=function(b){var a=this.map.getResolution(),c=Math.round((b.left-this.maxExtent.left)/(a*this.tileSize.w));b=Math.round((this.maxExtent.top-b.top)/(a*this.tileSize.h));a=this.map.getZoom();var d=Math.pow(2,a); +if(0>b||b>=d)return OpenLayers.Util.getImagesLocation()+"404.png";c=a+"/"+(c%d+d)%d+"/"+b+"."+this.type;b=this.url;b instanceof Array&&(b=this.selectUrl(c,b));return b+c},Ma=function(b){var a=S3.gis.openweathermap,c=b.s3.options.layers_openweathermap,d;for(d in c){var e=new OpenLayers.Layer.XYZ(c[d].name,"https://tile.openweathermap.org/map/"+d+"/${z}/${x}/${y}.png?appid="+a,{dir:c[d].dir,isBaseLayer:!1,s3_layer_id:c[d].id,s3_layer_type:"openweathermap",type:"png"});e.setVisibility(c[d].visibility); +e.events.on({loadstart:L,loadend:M});b.addLayer(e)}},Fa=function(b,a){var c=a.name,d=[a.url];void 0!==a.url2&&d.push(a.url2);void 0!==a.url3&&d.push(a.url3);var e=a.layername;var g=void 0!==a.zoomLevels?a.zoomLevels:19;if(void 0!==a.dir){var f=a.dir;-1==$.inArray(f,b.s3.dirs)&&b.s3.dirs.push(f)}else f="";f=new OpenLayers.Layer.TMS(c,d,{dir:f,s3_layer_id:a.id,s3_layer_type:"tms",layername:e,type:void 0!==a.format?a.format:"png",numZoomLevels:g});void 0!==a.attribution&&(f.attribution=a.attribution); +f.events.on({loadstart:L,loadend:M});b.addLayer(f);a._base&&b.setBaseLayer(f)},Na=function(b,a){var c=a.featureType,d,e=a.name,g=a.title,f=a.url;void 0!==a.username&&void 0!==a.password&&(f=f.replace("://","://"+a.username+":"+a.password+"@"));var k=void 0!==a.featureNS?a.featureNS:null;if(void 0!==a.schema)var l=a.schema;var n=void 0!==a.version?a.version:"1.1.0";var h=void 0!==a.geometryName?a.geometryName:"the_geom";var m=void 0!==a.visibility?a.visibility:!0;if(void 0!==a.dir){var w=a.dir;-1== +$.inArray(w,b.s3.dirs)&&b.s3.dirs.push(w)}else w="";var r=void 0!==a.cluster_attribute?a.cluster_attribute:"colour";var v=void 0!==a.cluster_distance?a.cluster_distance:20;var A=void 0!==a.cluster_threshold?a.cluster_threshold:2;var C=void 0!==a.refresh?a.refresh:!1;var u=[new OpenLayers.Strategy.BBOX({ratio:1.5})];C&&u.push(new OpenLayers.Strategy.Refresh({force:!0,interval:1E3*C}));A&&u.push(new OpenLayers.Strategy.AttributeCluster({attribute:r,distance:v,threshold:A}));r=void 0!==a.projection? +a.projection:4326;"1.0.0"==n?v="EPSG:"+r:(v="urn:ogc:def:crs:EPSG::"+r,S3.gis.yx.includes(r)&&(d=new OpenLayers.Format.WFST({version:n,featureType:c,featureNS:k,featurePrefix:"feature",geometryName:h,srsName:v,schema:l,xy:!1})));c=new OpenLayers.Protocol.WFS({url:f,version:n,featureType:c,featureNS:k,format:d,geometryName:h,srsName:v,schema:l});k='
    '+e+"
    ";void 0!==a.desc&&(k+='
    '+a.desc+"
    ");if(void 0!== +a.src||void 0!==a.src_url)d='
    ',void 0!==a.src_url?(d+='',d=void 0!==a.src?d+a.src:d+a.src_url,d+=""):d+=a.src,k+=d+"
    ";k+="
    ";h=Q(b,a);d=h[0];h=h[1];r=4326==r?q:new OpenLayers.Projection("EPSG:"+r);a=new OpenLayers.Layer.Vector(e,{maxFeatures:1E3,strategies:u,dir:w,legendURL:h,projection:r,protocol:c,styleMap:d,s3_layer_id:a.id,s3_layer_type:"wfs",s3_style:a.style});a.legendTitle=k;a.title=g;a.setVisibility(m);a.events.on({loadstart:L, +loadend:M,visibilitychanged:S});b.addLayer(a)},Ga=function(b,a){var c=a.name,d=a.url;a.username&&a.password&&(d=d.replace("://","://"+a.username+":"+a.password+"@"));var e=a.layers;var g=void 0!==a.visibility?a.visibility:!0;if(void 0!==a.dir){var f=a.dir;-1==$.inArray(f,b.s3.dirs)&&b.s3.dirs.push(f)}else f="";var k=void 0!==a.base?a.base:!1;var l=void 0!==a.transparent?a.transparent:!0;var n=void 0!==a.format?a.format:"image/png";var h=void 0!==a.version?a.version:"1.1.1";var m=a.map?a.map:"";var w= +a.style?a.style:"";var r=void 0!==a.bgcolor?"0x"+a.bgcolor:"";var v=void 0!==a.buffer?a.buffer:0;var A=void 0!==a.tiled?a.tiled:!1;var C=void 0!==a.singleTile?a.singleTile:!1;var u=void 0!==a.opacity?a.opacity:1;var I=void 0!==a.queryable?a.queryable:1;var H='
    '+c+"
    ";void 0!==a.desc&&(H+='
    '+a.desc+"
    ");if(b.s3.options.metadata){if(void 0!==a.post_id)if(i18n.gis_metadata){var B=i18n.gis_metadata;var E= +S3.Ap.concat("/cms/page/"+a.post_id)}else B=i18n.gis_metadata_edit,E=S3.Ap.concat("/cms/post/"+a.post_id+"/update?layer_id="+a.id);else i18n.gis_metadata_create&&(B=i18n.gis_metadata_create,E=S3.Ap.concat("/cms/post/create?layer_id="+a.id));B&&(H+='")}else if(void 0!==a.src||void 0!==a.src_url)B='
    ',void 0!==a.src_url?(B+='',B=void 0!==a.src?B+a.src:B+a.src_url, +B+=""):B+=a.src,H+=B+"
    ";H+="
    ";if(void 0!==a.legendURL)var x=a.legendURL;f=new OpenLayers.Layer.WMS(c,d,{layers:e,transparent:l},{dir:f,isBaseLayer:k,singleTile:C,wrapDateLine:!0,s3_layer_id:a.id,s3_layer_type:"wms",queryable:I,visibility:g});m&&(f.params.MAP=m);n&&(f.params.FORMAT=n);h&&(f.params.VERSION=h);w&&(f.params.STYLES=w);r&&(f.params.BGCOLOR=r);A&&(f.params.TILESORIGIN=[b.maxExtent.left,b.maxExtent.bottom]);k||(f.opacity=u,f.buffer=v?v:0);f.legendTitle=H;x&&(f.legendURL=x); +f.events.on({loadstart:L,loadend:M,visibilitychanged:S});b.addLayer(f);a._base&&b.setBaseLayer(f)},Ha=function(b,a){var c=a.name,d=[a.url];void 0!==a.url2&&d.push(a.url2);void 0!==a.url3&&d.push(a.url3);var e=a.layername;var g=void 0!==a.zoomLevels?a.zoomLevels:19;if(void 0!==a.dir){var f=a.dir;-1==$.inArray(f,b.s3.dirs)&&b.s3.dirs.push(f)}else f="";f=new OpenLayers.Layer.XYZ(c,d,{dir:f,s3_layer_id:a.id,s3_layer_type:"xyz",layername:e,type:void 0!==a.format?a.format:"png",numZoomLevels:g});void 0!== +a.attribution&&(f.attribution=a.attribution);f.events.on({loadstart:L,loadend:M});b.addLayer(f);a._base&&b.setBaseLayer(f)},sa=function(b){var a=b.s3.options,c=new OpenLayers.Control.Navigation;a.no_zoom_wheel&&(c.zoomWheelEnabled=!1);b.addControl(c);void 0===a.zoomcontrol&&b.addControl(new OpenLayers.Control.Zoom);b.addControl(new OpenLayers.Control.ArgParser);b.addControl(new OpenLayers.Control.Attribution);void 0===a.scaleline&&b.addControl(new OpenLayers.Control.ScaleLine);"mgrs"==a.mouse_position? +b.addControl(new OpenLayers.Control.MGRSMousePosition):a.mouse_position&&b.addControl(new OpenLayers.Control.MousePosition);void 0===a.permalink&&b.addControl(new OpenLayers.Control.Permalink);if(void 0===a.overview){a={};c=b.options;for(var d in c)"controls"!=d&&(a[d]=c[d]);b.addControl(new OpenLayers.Control.OverviewMap({mapOptions:a}))}Qa(b)},Qa=function(b){b.events.register("featureover",this,Ra);b.events.register("featureout",this,ia);b.events.register("featureclick",this,Sa)},Ra=function(b){var a= +b.feature,c=a.layer,d;a.renderIntent="select";c.drawFeature(a);if(-1==["OpenLayers.Handler.PointS3","OpenLayers.Handler.Path","OpenLayers.Handler.Polygon","OpenLayers.Handler.RegularPolygon"].indexOf(c.name)){var e=c.map;-1==e.s3.layers_nopopups.indexOf(c.name)&&(S3.gis.timeouts[a.id]=setTimeout(function(){if(!a.cluster&&a.geometry){var g=a.attributes,f=a.geometry.getBounds().getCenterLonLat();if(void 0!==c.s3_popup_format){_.templateSettings={interpolate:/\{(.+?)\}/g};var k=c.s3_popup_format;var l= +_.template(k);var n={},h=k.split("{");for(d=0;d'):void 0===c&&(c=i18n.gis_loading+'...
    ');var g=b.geometry.getBounds().getCenterLonLat();c=new OpenLayers.Popup.FramedCloud(e,g,new OpenLayers.Size(400,400),c,null,!0,Ta);b.popup=c;c.feature=b;c.maxSize=new OpenLayers.Size(750, +660);g=b.layer.map;b.map=g;g.addPopup(c);d||void 0===a||ja(a,e+"_contentDiv",c);return c};S3.gis.addPopup=ka;S3.gis.popupLoaded=function(b){$("#"+b+"_contentDiv iframe").contents().find("#popup").show();var a=S3.gis.maps,c,d;for(c in a){var e=a[c];var g=e.popups;var f=0;for(d=g.length;f';$("#"+a).html(e);return!1})}catch(e){}},error:function(d,e,g){d="UNAUTHORIZED"==g?i18n.gis_requires_login:d.responseText;$("#"+a+"_contentDiv").html(d);c.updateSize()}})},Sa=function(b){var a=b.feature, +c=a.layer,d=c.map,e=d.s3;if(-1==["OpenLayers.Handler.PointS3","OpenLayers.Handler.Path","OpenLayers.Handler.Polygon","OpenLayers.Handler.RegularPolygon"].indexOf(c.name)&&-1==e.layers_nopopups.indexOf(c.name))if("openweathermap"==c.s3_layer_type){var g=c.options.getPopupHtml(a.attributes.station);var f=new OpenLayers.Popup("Popup",a.geometry.getBounds().getCenterLonLat(),new OpenLayers.Size(c.options.popupX,c.options.popupY),g,"Station",!1);a.popup=f;f.feature=a;d.addPopup(f,!0)}else{var k=a.geometry, +l;if("OpenLayers.Geometry.Point"!=k.CLASS_NAME)for(d=0,l=e.clicking.length;d";var m=n.length,w=e.id;for(d=0;d",1)[0]}else e=void 0!==r.popup?r.popup.split("
    ",1)[0]:r[b];void 0!==r.url?h+="
  • "+e+"
  • ":void 0!==c.s3_url_format?(_.templateSettings={interpolate:/\{(.+?)\}/g},f=_.template(c.s3_url_format),r.id.constructor===Array&&(r.id=r.id[0]),f=f(r),h+="
  • "+e+"
  • "):h+="
  • "+e+"
  • "}f=null;h+="";h+=""}else if(d=c.s3_layer_type,"kml"==d){r=a.attributes;if(void 0!==a.style.balloonStyle)h=a.style.balloonStyle.replace(/{([^{}]*)}/g,function(H,B){B=r[B];return"string"===typeof B||"number"===typeof B?B:H});else for(d=typeof r[b],d="object"==d?r[b].value:r[b],h="

    "+d+"

    ",c=c.body.split(" "), +e=0;e
    '+d+"
    "}else u=void 0!==r[c[e]]?'
    '+r[c[e]]+"
    ":"";h+=u}-1!=h.search(""+h.replace(/",$.each(r, +function(H,B){"id_orig"==H&&(H="id");u='
    '+H+':
    '+B+"
    ";h+=u}),h+="
    ";else if("wfs"==d)r=a.attributes,d=r[b],h="

    "+d+"

    ",$.each(r,function(H,B){u='
    '+H+':
    '+B+"
    ";h+=u});else if(void 0!==a.attributes.url)f=a.attributes.url;else if(void 0!==c.s3_url_format)_.templateSettings={interpolate:/\{(.+?)\}/g}, +f=_.template(c.s3_url_format),a.attributes.id.constructor===Array&&(a.attributes.id=a.attributes.id[0]),f=f(a.attributes);else{r=a.attributes;e=void 0===r.name?"":"

    "+r.name+"

    ";c=void 0===r.description?"":"

    "+r.description+"

    ";d=void 0===r.link?"":''+r.link+"";if(void 0===r.data)b="";else if(0===r.data.indexOf("http://")){g=!0;var I=S3.uid();b='
    '+i18n.gis_loading+"...
    "}else b="

    "+r.data+"

    "; +k=void 0===r.image?"":0===r.image.indexOf("http://")?'':"";h=e+c+d+b+k}f=ka(a,f,h);g&&ja(a.attributes.data,I,f)}},Ta=function(){var b=this.feature;if(b){var a=b.layer;b.renderIntent="default";a&&a.drawFeature(b);b.popup&&(a&&a.map.removePopup(b.popup),b.popup.destroy(),delete b.popup);if(b.id)$("#"+b.id+"_popup").remove();else{var c=b.map;if(c)for(var d=c.popups,e=d.length-1;-1");var g=S3.gis.maps[b];$.getS3(a,function(f){e.html(f);g.popups[0].updateSize();f=$(d+" .dropdown-toggle");f.length&&(e.parent().css("overflow","visible").parent().css("overflow","visible"),f.dropdown())},"html")};S3.gis.zoomToSelectedFeature=function(b,a,c,d){b=S3.gis.maps[b];a=new OpenLayers.LonLat(a,c);c=b.getZoom();b.setCenter(a,c+d);for(d=0;d
    ');var c=new GeoExt.LegendPanel({title:i18n.gis_legend,autoScroll:!0,border:!1}),d=$("#"+a+" .map_legend_panel");d=Ext.get(d[0]);c.render(d);$("#"+a+" .map_legend_tab").click(function(){if($(this).hasClass("right")){var e=b.s3.id,g=$("#"+e+" .map_legend_panel").outerWidth(); +$("#"+e+" .map_legend_div").animate({marginRight:"-"+g+"px"});$("#"+e+" .map_legend_tab").removeClass("right").addClass("left")}else ha(b)});return c},ha=function(b){b=b.s3.id;$("#"+b+" .map_legend_div").animate({marginRight:0});$("#"+b+" .map_legend_tab").removeClass("left").addClass("right")},Va=function(b){var a=new OpenLayers.Control.NavigationHistory;b.map.addControl(a);a.activate();var c=new Ext.Button({iconCls:"back",tooltip:i18n.gis_navPrevious,handler:a.previous.trigger});a=new Ext.Button({iconCls:"next", +tooltip:i18n.gis_navNext,handler:a.next.trigger});b.addButton(c);b.addButton(a)},U=function(b,a,c,d){OpenLayers.Handler.PointS3=OpenLayers.Class(OpenLayers.Handler.Point,{dblclick:function(){return!0},CLASS_NAME:"OpenLayers.Handler.PointS3"});var e=b.s3.draftLayer,g=new OpenLayers.Control.DrawFeature(e,OpenLayers.Handler.PointS3,{featureAdded:function(l){for(b.s3.lastDraftFeature?b.s3.lastDraftFeature.destroy():1Click anywhere on the map to begin drawing. Double click to complete the area or click the Finish button below.'); +var g=S3.gis.maps[b].s3;$("#"+b+" .map_polygon_finish").click(function(){a.finishSketch();void 0!==g.polygonPanelFinish?g.polygonPanelFinish():(g.lastDraftFeature?g.lastDraftFeature.destroy():1>16&255,b>>8&255,b&255].join()},$a=function(b,a){var c= +pressed:c,activateOnEnable:!0,deactivateOnDisable:!0});a.add(l);b.s3.circleButton=l}else b.addControl(g),c&&(g.activate(),$("#"+b.s3.id+"_panel .olMapViewport").addClass("crosshair"))},eb=function(b){var a=new jQuery.Deferred,c=S3.gis.maps[b].s3;setTimeout(function e(){void 0!==c.mapWin?a.resolve("loaded"):"pending"===a.state()&&(a.notify("waiting for JS to load..."),setTimeout(e,500))},1);return a.promise()},ma=function(b){b=parseInt(b,16);return[b>>16&255,b>>8&255,b&255].join()},$a=function(b,a){var c= b.s3,d=c.id,e=c.options.draft_style;e=e?e.fillOpacity?"rgba("+ma(e.fill)+","+e.fillOpacity+")":"rgb("+ma(e.fill)+")":"";e=new Ext.Toolbar.Item({html:''});a.add(e);$.when(eb(d)).then(function(){$("#"+d+"_panel .gis_colorpicker").spectrum({showInput:!0,showInitial:!0,preferredFormat:"rgb",showPaletteOnly:!0,togglePaletteOnly:!0,palette:"rgba(255, 0, 0, .5);rgba(255, 165, 0, .5);rgba(255, 255, 0, .5);rgba(0, 255, 0, .5);rgba(0, 0, 255, .5);rgba(255, 255, 255, .5);rgba(0, 0, 0, .5)".split(";"), showAlpha:!0,cancelText:i18n.gis_cancelText,chooseText:i18n.gis_chooseText,togglePaletteMoreText:i18n.gis_togglePaletteMoreText,togglePaletteLessText:i18n.gis_togglePaletteLessText,clearText:i18n.gis_clearText,noColorSelectedText:i18n.gis_noColorSelectedText,change:function(g){var f={fill:Number(16777216+65536*Math.round(g._r)+256*Math.round(g._g)+Math.round(g._b)).toString(16).substring(1)};1!=g._a&&(f.fillOpacity=g._a);g=Q(b,{style:f,opacity:.9})[0];f=c.draftLayer;f.styleMap=g;f.redraw()}})},function(g){s3_debug(g)}, function(g){s3_debug(g)})},bb=function(b){var a=b.map,c=new Ext.Button({iconCls:"potlatch",tooltip:i18n.gis_potlatch,handler:function(){var d=a.getZoom();if(14>d)alert(i18n.gis_osm_zoom_closer);else{var e=a.getCenter();e.transform(a.getProjectionObject(),q);d=S3.Ap.concat("/gis/potlatch2/potlatch2.html")+"?lat="+e.lat+"&lon="+e.lon+"&zoom="+d;window.open(d)}}});b.addSeparator();b.addButton(c)},Wa=function(b){var a=b.map,c=a.s3.id,d=new Ext.Button({iconCls:"print",tooltip:i18n.gis_print_tip,menu:{xtype:"menu", @@ -153,15 +153,15 @@ c=new gxp.plugins.RemoveLayer({actionTarget:"treepanel.tbar",removeActionTip:"Re Ext.Ajax.request({url:f,method:"GET",success:function(k){c&&c.close();"feature"==g?(k=new Ext.TabPanel({activeTab:0,items:[{title:"Layer Properties",html:k.responseText},{title:"Filter",id:"s3_gis_layer_filter_tab",html:""}]}),k.items.items[1].on("activate",function(){var l;Ext.iterate(b.s3.layers_feature,function(n){n.id==e.layer.s3_layer_id&&(l=n.url.replace(/.geojson.+/,"/search.plain"))});Ext.get("s3_gis_layer_filter_tab").load({url:l,discardUrl:!1,callback:function(){S3.redraw()},text:"Loading...", timeout:30,scripts:!1})})):k=new Ext.Panel({title:"Layer Properties",html:k.responseText});c=new Ext.Window({width:400,layout:"fit",items:[k]});c.show();$("#plain form").submit(function(){var l=$('#plain input[name="id"]').val();l=S3.Ap.concat("/gis/layer_"+g+"/"+l+".plain/update");var n=$("#plain input"),h=[];Ext.iterate(n,function(r,v){v.id&&-1!=v.id.indexOf("gis_layer_")&&h.push(v.id)});n=[];for(var m,w=0;w').addClass('lightbox-bg').appendTo('body').show(); - $('
    ').addClass('modal-loader').html('

    submitting...

    ').appendTo('body'); - } - }).done(function(data) { - if (data.success) { - // @ToDo: i18n - alert('Data Saved'); - win_location = window.location.href; - window.location = win_location.split('upload.pdf')[0] + 'upload.pdf'; - } else { - var size = 0; - for (var key in data.error) { - $('#' + key + '-error').html(data.error[key]); - size++; - } - // @ToDo: i18n - alert('There are ' + size + ' Error(s). Resubmit.'); - } - }).always(function(jqXHR, textStatus) { - $('.lightbox-bg').remove(); - $('.modal-loader').remove(); - }); - return false; - }); -}); diff --git a/tests/travis/requirements.txt b/tests/travis/requirements.txt index eec1b0770a..03ca4a6c5b 100644 --- a/tests/travis/requirements.txt +++ b/tests/travis/requirements.txt @@ -2,13 +2,11 @@ xlwt>=0.7.2 # Warning: S3GIS unresolved dependency: shapely required for GIS support Shapely>=1.2.14 #shapely -# Warning: S3PDF unresolved dependency: Python Imaging required for PDF export -PIL>=1.1.7 #from PIL import Image # Warning: S3GIS unresolved dependency: GDAL required for Shapefile support GDAL>=1.9.0 #from osgeo import ogr # Warning: S3GIS unresolved dependency: geopy required for Geocoder support geopy>=1.18.1 #from geopy import geocoders -# Warning: S3PDF unresolved dependency: reportlab required for PDF export +# Warning: S3RL_PDF unresolved dependency: reportlab required for PDF export reportlab>=2.5 pyserial>=2.6 # Warning: S3Msg unresolved dependency tweepy required for non-Tropo Twitter support diff --git a/views/_create.html b/views/_create.html index 3aac244b80..8f5b538d0e 100755 --- a/views/_create.html +++ b/views/_create.html @@ -1,7 +1,6 @@
    {{try:}}{{=H2(title)}}{{except:}}{{pass}} - {{#try:=s3db.ocr_buttons(r)except:pass}}
    diff --git a/views/_list_filter.html b/views/_list_filter.html index 3d7ad70709..410a49c716 100644 --- a/views/_list_filter.html +++ b/views/_list_filter.html @@ -1,7 +1,6 @@
    {{try:}}{{if title:}}{{=H2(title)}}{{pass}}{{except:}}{{pass}} - {{#try:=s3db.ocr_buttons(r)except:pass}}
    {{try:}}{{rheader=rheader}} diff --git a/views/_ocr_page_upload.html b/views/_ocr_page_upload.html deleted file mode 100644 index fcbc25ca14..0000000000 --- a/views/_ocr_page_upload.html +++ /dev/null @@ -1,13 +0,0 @@ -{{extend "layout.html"}} -{{=H2(T("Scanned Forms Upload"))}} -{{inputBoxList = []}} -{{if uploadformat == "image":}} -{{for eachpage in range(1, numpages + 1):}} -{{inputBoxList.append(TR(TD(T("Page")," %s" % eachpage), TD(INPUT(_type="file", _name="page%s" % eachpage, _class="required")), TD(DIV(_class="tooltip", _title="Comments|Upload pages in ascending order, all fields are required. Pages can be of any image format viz. (png, jpg, gif, bmp). Each image represent a single page."))))}} -{{pass}} -{{elif uploadformat == "pdf":}} -{{inputBoxList.append(TR(TD(T("PDF File")), TD(INPUT(_type="file", _name="pdffile", _class="required")), TD(DIV(_class="tooltip", _title="Comments|Upload a single PDF file which contains all the pages of the form in ascending order."))))}} -{{pass}} -{{=DIV(FORM(INPUT(_type="hidden", _name="formuuid", _value=formuuid), INPUT(_type="hidden", _name="numpages", _value=numpages), INPUT(_type="hidden", _name="uploadformat", _value=uploadformat), TABLE(inputBoxList, TR(TD(),TD(INPUT(_type="submit"), _style="text-align:right"))), _action=posturl, _method="post", _class="cmxform", _id="pageuploadForm"), _id="rheader")}} - - diff --git a/views/_ocr_review.html b/views/_ocr_review.html deleted file mode 100644 index e62b5970b9..0000000000 --- a/views/_ocr_review.html +++ /dev/null @@ -1,53 +0,0 @@ -{{extend "layout.html"}} -{{=H2(T("OCR Form Review"))}} - - -{{=reviewform}} diff --git a/views/_ocr_upload.html b/views/_ocr_upload.html deleted file mode 100644 index 3c852620b2..0000000000 --- a/views/_ocr_upload.html +++ /dev/null @@ -1,11 +0,0 @@ -{{extend "layout.html"}} -{{=H2(T("Scanned Forms Upload"))}} -{{optionList = []}} -{{if len(availForms) == 0:}} -{{=DIV(P(T("No forms to the corresponding resource have been downloaded yet."), " ", A(T("click here"), _href=createurl), " ", T("to download a OCR Form.")), _id="rheader")}} -{{else:}} -{{for eachForm in availForms:}} -{{optionList.append(OPTION("uuid %s [revision %s]" % (eachForm["uuid"], eachForm["revision"]), _value="%s" % str(eachForm["uuid"])))}} -{{pass}} -{{=DIV(FORM(TABLE(TR(TD(T("Available Forms")), TD(SELECT(optionList, _name="formuuid")), TD(DIV(_class="tooltip", _title="Comments|The form uuid and revision number is present at the bottom of each page of the form just close to the black markers.z"))), TR(TD(T("Upload Format")), TD(SELECT(OPTION(T("Image File(s), one image per page"), OPTION(T("Single PDF File"), _value="pdf"), _value="image"), _name="uploadformat"))), TR(TD(),TD(INPUT(_type="submit"), _style="text-align:right"))), _action="", _method="get"), _id="rheader")}} -{{pass}} diff --git a/views/pr/index.html b/views/pr/index.html index acaf96ba05..416b162133 100755 --- a/views/pr/index.html +++ b/views/pr/index.html @@ -1,6 +1,5 @@ {{extend "layout.html"}} {{try:}}{{=H2(title)}}{{except:}}{{pass}} -{{try:}}{{=s3db.ocr_buttons(r)}}{{except:}}{{pass}}
    {{try:}}{{=showadd_btn}}{{hide_listadd = True}} {{except:}}{{hide_listadd = False}}{{pass}} diff --git a/views/scripts_top.html b/views/scripts_top.html index c213c5e554..f108470b3d 100644 --- a/views/scripts_top.html +++ b/views/scripts_top.html @@ -36,4 +36,4 @@ S3.Ap='/{{=appname}}' {{if s3.rtl:}}S3.rtl=true{{else:}}S3.rtl=false{{pass}} {{if s3.interactive:}}S3.interactive=true{{else:}}S3.interactive=false{{pass}} -//]]> \ No newline at end of file +//]]>