diff --git a/arches/app/models/models.py b/arches/app/models/models.py index 1f4cf9698c..1f4a640c13 100644 --- a/arches/app/models/models.py +++ b/arches/app/models/models.py @@ -50,6 +50,7 @@ class AuthGroup(models.Model): id = models.IntegerField(primary_key=True) name = models.CharField(unique=True, max_length=80) + geom = models.GeometryField() class Meta: db_table = u'auth_group' diff --git a/arches/app/models/resource.py b/arches/app/models/resource.py index 53da4fa020..e4f521bf38 100644 --- a/arches/app/models/resource.py +++ b/arches/app/models/resource.py @@ -992,6 +992,29 @@ def prepare_term_index(self, create=False): return index_settings + def prepare_maplayer_index(self, create=False): + """ + Creates the mappings for the maplayers index + """ + index_settings = { + "mappings": { + "HERITAGE_RESOURCE_GROUP.E27": { + "properties": { + "geometry": { + "type": "geo_shape" + } + } + } + } + } + + if create: + se = SearchEngineFactory().create() + se.create_index(index='maplayers', body=index_settings, ignore=400) + + return index_settings + + def prepare_resource_relations_index(self, create=False): """ Creates the settings and mappings in Elasticsearch to support related resources diff --git a/arches/app/utils/index_database.py b/arches/app/utils/index_database.py index 3ae970fc20..85b67a45d4 100644 --- a/arches/app/utils/index_database.py +++ b/arches/app/utils/index_database.py @@ -42,6 +42,7 @@ def index_resources(): se.delete_index(index=index_type) se.delete(index='term', body='{"query":{"bool":{"must":[{"constant_score":{"filter":{"missing":{"field":"value.options.conceptid"}}}}],"must_not":[],"should":[]}}}') + Resource().prepare_maplayer_index(create=True) Resource().prepare_term_index(create=True) cursor = connection.cursor() diff --git a/arches/app/utils/index_database_.py b/arches/app/utils/index_database_.py index 9fc4563e43..27da43955d 100644 --- a/arches/app/utils/index_database_.py +++ b/arches/app/utils/index_database_.py @@ -42,6 +42,7 @@ def index_resources(): se.delete_index(index=index_type) se.delete(index='term', body='{"query":{"bool":{"must":[{"constant_score":{"filter":{"missing":{"field":"value.options.conceptid"}}}}],"must_not":[],"should":[]}}}') + Resource().prepare_maplayer_index(create=True) Resource().prepare_term_index(create=True) cursor = connection.cursor() diff --git a/arches/app/utils/set_anonymous_user.py b/arches/app/utils/set_anonymous_user.py index a3feafbd23..96dc642fb2 100644 --- a/arches/app/utils/set_anonymous_user.py +++ b/arches/app/utils/set_anonymous_user.py @@ -8,5 +8,4 @@ def process_request(self, request): request.user = User.objects.get(username='anonymous') except: pass - - request.user.user_groups = [group.name for group in request.user.groups.all()] + request.user.user_groups = ", ".join([group.name for group in request.user.groups.all()]) diff --git a/arches/app/views/concept.py b/arches/app/views/concept.py index cc94e56b79..7a6d831bc2 100644 --- a/arches/app/views/concept.py +++ b/arches/app/views/concept.py @@ -24,7 +24,7 @@ from django.views.decorators.csrf import csrf_exempt from django.template import RequestContext from django.shortcuts import render_to_response -from django.contrib.auth.decorators import permission_required +from django.contrib.auth.decorators import user_passes_test from arches.app.models import models from arches.app.models.concept import Concept, ConceptValue, CORE_CONCEPTS from arches.app.search.search_engine_factory import SearchEngineFactory @@ -33,6 +33,7 @@ from arches.app.utils.JSONResponse import JSONResponse from arches.app.utils.skos import SKOSWriter, SKOSReader from django.utils.module_loading import import_by_path +from eamena.models.group import edit_group_check sparql_providers = {} @@ -40,7 +41,7 @@ Provider = import_by_path(provider)() sparql_providers[Provider.endpoint] = Provider -@permission_required('edit') +@user_passes_test(edit_group_check) def rdm(request, conceptid): lang = request.GET.get('lang', request.LANGUAGE_CODE) languages = models.DLanguages.objects.all() @@ -60,7 +61,7 @@ def rdm(request, conceptid): -@permission_required('edit') +@user_passes_test(edit_group_check) @csrf_exempt def concept(request, conceptid): f = request.GET.get('f', 'json') diff --git a/arches/app/views/resources.py b/arches/app/views/resources.py index 11e27bc32f..4697bd1c33 100644 --- a/arches/app/views/resources.py +++ b/arches/app/views/resources.py @@ -20,7 +20,6 @@ from django.template import RequestContext from django.shortcuts import render_to_response, redirect from django.views.decorators.csrf import csrf_exempt -from django.contrib.auth.decorators import permission_required from django.conf import settings from django.db import transaction from arches.app.models import models @@ -28,20 +27,26 @@ from arches.app.utils.betterJSONSerializer import JSONSerializer, JSONDeserializer from arches.app.utils.JSONResponse import JSONResponse from arches.app.search.search_engine_factory import SearchEngineFactory -from arches.app.search.elasticsearch_dsl_builder import Query, Terms +from arches.app.search.elasticsearch_dsl_builder import Query, Terms, Bool, GeoShape from arches.app.views.concept import get_preflabel_from_valueid from arches.app.models.concept import Concept from django.http import HttpResponseNotFound from django.contrib.gis.geos import GEOSGeometry from django.db.models import Max, Min from django.contrib.auth.decorators import user_passes_test +from eamena.models import forms +from eamena.models.group import canUserAccessResource, edit_group_check, canUserCreateResource +from django.core.exceptions import PermissionDenied def report(request, resourceid): raise NotImplementedError('Reports are not yet implemented.') -@permission_required('edit') +@user_passes_test(edit_group_check) @csrf_exempt def resource_manager(request, resourcetypeid='', form_id='default', resourceid=''): + can_edit = canUserAccessResource(request.user, resourceid, 'edit'); + if not can_edit: + raise PermissionDenied if resourceid != '': resource = Resource(resourceid) @@ -51,9 +56,18 @@ def resource_manager(request, resourcetypeid='', form_id='default', resourceid=' if form_id == 'default': form_id = resource.form_groups[0]['forms'][0]['id'] + if canUserAccessResource(request.user, resourceid, 'delete'): + # add the delete form + manage_groups = [x for x in resource.form_groups if x['id'] == 'manage-resource'] + if len(manage_groups) > 0: + manage_group = manage_groups[0] + manage_group['forms'].append(forms.DeleteResourceForm.get_info()) + form = resource.get_form(form_id) if request.method == 'DELETE': + if not canUserAccessResource(request.user, resourceid, 'delete'): + raise PermissionDenied resource.delete_index() se = SearchEngineFactory().create() realtionships = resource.get_related_resources(return_entities=False) @@ -68,6 +82,10 @@ def resource_manager(request, resourcetypeid='', form_id='default', resourceid=' form.set_user(request.user) form.update(data, request.FILES) + # Before saving the resource, need to check the location is within the group boundary (Can't use SE for this) + if not canUserCreateResource(request.user, resource): + raise PermissionDenied + with transaction.atomic(): if resourceid != '': resource.delete_index() @@ -175,12 +193,44 @@ def map_layers(request, entitytypeid='all', get_centroids=False): se = SearchEngineFactory().create() query = Query(se, limit=limit) + boolfilter = Bool() + + # filter based on user's group geometries + locationfilter = Bool() + for group in request.user.groups.all(): + if group.name.startswith('restrict'): + continue + if group.geom: + geojson = group.geom.geojson + geojson_as_dict = JSONDeserializer().deserialize(geojson) + geoshape = GeoShape(field='geometry', type=geojson_as_dict['type'], + coordinates=geojson_as_dict['coordinates']) + locationfilter.should(geoshape) + boolfilter.must(locationfilter) + + if request.user.groups.filter(name__startswith='restrict').all(): + restrictedfilter = Bool() + for restrictedGroup in request.user.groups.filter(name__startswith='restrict').all(): + if restrictedGroup.geom: + geojson = restrictedGroup.geom.geojson + geojson_as_dict = JSONDeserializer().deserialize(geojson) + geoshape = GeoShape(field='geometry', type=geojson_as_dict['type'], + coordinates=geojson_as_dict['coordinates']) + restrictedfilter.should(geoshape) + boolfilter.must_not(restrictedfilter) + + query.add_query(boolfilter) + args = { 'index': 'maplayers' } if entitytypeid != 'all': args['doc_type'] = entitytypeid if entityids != '': for entityid in entityids.split(','): - geojson_collection['features'].append(se.search(index='maplayers', id=entityid)['_source']) + record = se.search(index='maplayers', id=entityid)['_source'] + # Set a param to indicate the user's permissions for this resource + record['properties']['can_edit'] = canUserAccessResource(request.user, record['id'], 'edit') + + geojson_collection['features'].append(record) return JSONResponse(geojson_collection) @@ -212,9 +262,19 @@ def map_layers(request, entitytypeid='all', get_centroids=False): item['_source']['geometry'] = item['_source']['properties'][geom_param] item['_source']['properties'].pop('extent', None) item['_source']['properties'].pop(geom_param, None) + + # Set a param to indicate the user's permissions for this resource + # This may be expensive, but we are assuming there won't be large numbers of results here + can_edit = canUserAccessResource(request.user, item['_source']['id'], 'edit') + item['_source']['properties']['can_edit'] = can_edit else: item['_source']['properties'].pop('extent', None) item['_source']['properties'].pop('centroid', None) + + # Set a param to indicate the user's permissions for this resource + # This may be expensive, but we are assuming there won't be large numbers of results here + can_edit = canUserAccessResource(request.user, item['_source']['id'], 'edit') + item['_source']['properties']['can_edit'] = can_edit geojson_collection['features'].append(item['_source']) return JSONResponse(geojson_collection) diff --git a/arches/install/django_overrides/admin.py b/arches/install/django_overrides/admin.py index 7d9a4ec045..aa62f620df 100644 --- a/arches/install/django_overrides/admin.py +++ b/arches/install/django_overrides/admin.py @@ -15,12 +15,35 @@ from django.utils.translation import ugettext, ugettext_lazy as _ from django.views.decorators.csrf import csrf_protect from django.views.decorators.debug import sensitive_post_parameters +from olwidget.admin import GeoModelAdmin +from django import forms +from olwidget.fields import MapField, EditableLayerField csrf_protect_m = method_decorator(csrf_protect) sensitive_post_parameters_m = method_decorator(sensitive_post_parameters()) -class GroupAdmin(admin.ModelAdmin): +class GroupAdminForm(forms.ModelForm): + geom = MapField([ + EditableLayerField({'geometry': 'polygon', 'name': 'geom', 'isCollection': True}), + ], { + 'layers': ['bing.AerialWithLabels', 'bing.Aerial', 'osm.mapnik'], + 'default_lat': 29, + 'default_lon': 15, + }, template="olwidget/admin_olwidget.html") + + def clean(self): + self.cleaned_data['geom'] = self.cleaned_data['geom'][0] + return self.cleaned_data + + class Meta: + model = Group + + +class GroupAdmin(GeoModelAdmin): + + form = GroupAdminForm + search_fields = ('name',) ordering = ('name',) filter_vertical = ('permissions',) @@ -35,6 +58,17 @@ def formfield_for_manytomany(self, db_field, request=None, **kwargs): db_field, request=request, **kwargs) + def add_view(self, request, form_url='', extra_context=None): + extra_context = extra_context or {} + extra_context['bingkey'] = settings.BING_KEY + return super(GroupAdmin, self).add_view(request, form_url, extra_context=extra_context) + + def change_view(self, request, object_id, form_url='', extra_context=None): + extra_context = extra_context or {} + extra_context['bingkey'] = settings.BING_KEY + return super(GroupAdmin, self).change_view(request, object_id, form_url, extra_context=extra_context) + + class UserAdmin(admin.ModelAdmin): add_form_template = 'admin/auth/user/add_form.html' change_user_password_template = None diff --git a/eamena/eamena/media/js/admin_group.js b/eamena/eamena/media/js/admin_group.js new file mode 100644 index 0000000000..112156fe58 --- /dev/null +++ b/eamena/eamena/media/js/admin_group.js @@ -0,0 +1,102 @@ +var WGS84 = new OpenLayers.Projection("EPSG:4326"); +var Mercator = new OpenLayers.Projection("EPSG:900913"); +//enable drag and drop of kml files onto django admin map view +django.jQuery( document ).ready(function() { + console.log( "ready!"); + + var mapEl = django.jQuery('#id_geom')[0]; + + if(mapEl) { + mapEl.ondrop = function (evt) { + evt.stopPropagation(); + evt.preventDefault(); + if(evt.dataTransfer.files[0]) { + handleFile(evt.dataTransfer.files[0]); + } + }; + mapEl.ondragenter = function (evt) { + evt.stopPropagation(); + evt.preventDefault(); + }; + mapEl.ondragover = function (evt) { + evt.stopPropagation(); + evt.preventDefault(); + }; + } + + + var allfeatures = []; + + var existingfeatures = olwidget_id_geom.vectorLayers[0].features; + for (var i =0; i < existingfeatures.length; i++){ + if (existingfeatures[i].geometry) { + existingfeatures[i].geometry.transform(Mercator, WGS84); + allfeatures.push(existingfeatures[i]); + } + } + + + var handleFile = function(file) { + + var reader = new FileReader(); + reader.onload = function (evt) { + if (evt.error) { + readerror(); + return; + } + + var results = null; + var content = evt.target.result; + var engine; + var formats = ['KML', 'GPX', 'OSM']; + for (var i = 0; i < formats.length; i++) { + engine = new OpenLayers.Format[formats[i]]({ + internalProjection: WGS84, + externalProjection: WGS84, + extractStyles: true + }); + try { + results = engine.read(content); + } catch (e) {} + if (results && results.length) { + break; + } + } + if (!results || !results.length) { + readerror(); + return; + } + + for (var i = 0; i < results.length; i++){ + if (results[i].geometry.components) { + allfeatures.push(results[i]); + } + } + + + var options = { + 'internalProjection': new OpenLayers.Projection("EPSG:4326"), + 'externalProjection': new OpenLayers.Projection("EPSG:4326") + }; + var wktFormat = new OpenLayers.Format.WKT(options); + var wkttext = wktFormat.write(allfeatures); + + for (var i =0; i < olwidget_id_geom.layers.length; i++){ + if (olwidget_id_geom.layers[i].CLASS_NAME == "olwidget.EditableLayer"){ + var mylayr = olwidget_id_geom.layers[i]; + mylayr.clearFeatures(); + if (mylayr.textarea.value.length > 0) { + mylayr.textarea.value = mylayr.textarea.value + ',' + wkttext; + }else{ + mylayr.textarea.value = wkttext; + } + mylayr.readWKT(); + } + } + olwidget_id_geom.initCenter(); + + }; + reader.readAsText(file); + }; + +}) \ No newline at end of file diff --git a/eamena/eamena/media/js/views/search/search-results.js b/eamena/eamena/media/js/views/search/search-results.js index 19a100357b..c3e3836a47 100644 --- a/eamena/eamena/media/js/views/search/search-results.js +++ b/eamena/eamena/media/js/views/search/search-results.js @@ -150,7 +150,8 @@ define(['jquery', description: description, geometries: ko.observableArray(this._source.geometries), typeIcon: resourceTypes[this._source.entitytypeid].icon, - typeName: resourceTypes[this._source.entitytypeid].name + typeName: resourceTypes[this._source.entitytypeid].name, + can_edit: this.can_edit }); }); return data; diff --git a/eamena/eamena/media/olwidget/css/olwidget.css b/eamena/eamena/media/olwidget/css/olwidget.css new file mode 100644 index 0000000000..22cff6235e --- /dev/null +++ b/eamena/eamena/media/olwidget/css/olwidget.css @@ -0,0 +1,188 @@ +/* + * Extra editing icons + */ +.olControlNoSelect { + cursor: auto; +} +.olControlEditingToolbar .olControlModifyFeatureItemInactive { + background: url("../img/extra_edit_icons.png") no-repeat scroll -48px 0px; +} +.olControlEditingToolbar .olControlModifyFeatureItemActive { + background: url("../img/extra_edit_icons.png") no-repeat scroll -24px 0px; +} +.olControlEditingToolbar .olControlClearFeaturesItemInactive, +.olControlEditingToolbar .olControlClearFeaturesItemActive { + background: url("../img/extra_edit_icons.png") no-repeat scroll -0px 0px; +} +.olControlEditingToolbar .olControlUndoItemActive { + background: url("../img/extra_edit_icons.png") no-repeat scroll -72px 0px; +} +.olControlEditingToolbar .olControlUndoItemInactive { + background: url("../img/extra_edit_icons.png") no-repeat scroll -96px 0px; +} +.olControlEditingToolbar .olControlRedoItemActive { + background: url("../img/extra_edit_icons.png") no-repeat scroll -120px 0px; +} +.olControlEditingToolbar .olControlRedoItemInactive { + background: url("../img/extra_edit_icons.png") no-repeat scroll -144px 0px; +} +.olControlEditingToolbar .olwidgetDeleteVertexItemActive { + background: url("../img/extra_edit_icons.png") no-repeat scroll -168px 0px; +} +.olControlEditingToolbar .olwidgetDeleteVertexItemInactive { + background: url("../img/extra_edit_icons.png") no-repeat scroll -192px 0px; +} +.olwidgetDeleteVertexOver { + cursor: pointer; +} + +.olwidgetEditableLayerSwitcher { + right: 0; + cursor: auto; +} +.olwidgetEditableLayerSwitcher .olwidgetMaxMin, .olwidgetEditableLayerSwitcher .olwidgetLabel { + color: white; + font-weight: bold; + font-size: smaller; +} +.olwidgetEditableLayerSwitcher .olwidgetContainer { + background-color: darkblue; +} +.olwidgetEditableLayerSwitcher .olwidgetMaxMin { + cursor: pointer; + padding: 0.5em; + float: left; +} +.olwidgetEditableLayerSwitcher .editingControls { + float: left; +} +.olwidgetEditableLayerSwitcher .olControlEditingToolbar { + position: relative !important; + float: left; + width: auto !important; +} +.olwidgetEditableLayerSwitcher .layersDiv { + clear: both; +} +.olwidgetEditableLayerSwitcher input { + padding-left: 0.5em; + padding-bottom: 0.5em; +} +.olwidgetEditableLayerSwitcher .olwidgetLabel { + padding-right: 2em; +} +.olwidgetEditableLayerSwitcher .olwidgetDisabled { + font-style: italic; + color: gray; +} + +/* Move layer switcher out of the way of editableLayerSwitcher */ +.olControlLayerSwitcher { + top: 30px !important; +} + +/* Override padding to avoid position computation errors */ +.olPopupContent { + padding: 0px !important; + overflow: visible !important; +} +/* Show popups over other controls */ +.olPopup { + z-index: 1005 !important; +} + +/* Div containing all popup elements */ +.olwidgetPopupContent { + overflow: visible; + padding: 2px; + border: 2px solid #777; + background-color: #777; +} + +.olwidgetPopupCloseBox { + cursor: pointer; + position: absolute; + right: 10px; + top: 10px; + background: #fff url("../img/popup_icons.png") no-repeat scroll -80px 0px; + padding-top: 16px; + width: 16px; + height: 0px; + overflow: hidden; +} +.olwidgetPopupCloseBox:hover { + background-position: -64px 0px; +} +/* Div containing content user HTML */ +.olwidgetPopupPage { + overflow: auto; + background: #fff; + padding: 8px; + padding-top: 23px; + + /* IE6 fix */ + zoom: 1; +} + +.olwidgetPopupPagination { + clear: both; + float: right; + background: #fff; + padding-left: 5px; + padding-right: 5px; +} +.olwidgetPaginationPrevious { + float: left; + cursor: pointer; + /* Replace text with background image trick */ + background: url("../img/popup_icons.png") no-repeat scroll -48px 0px; + padding-top: 16px; + width: 16px; + height: 0px; + overflow: hidden; +} +.olwidgetPaginationPrevious:hover { + background-position: -32px 0px; +} + +/* Div containing page count, e.g. "1 of 5". */ +.olwidgetPaginationCount { + float: left; + margin-left: 0.5em; + margin-right: 0.5em; +} +.olwidgetPaginationNext { + float: left; + cursor: pointer; + /* Replace text with background image trick */ + background: url("../img/popup_icons.png") no-repeat scroll -16px 0px; + padding-top: 16px; + width: 16px; + height: 0px; + overflow: hidden; +} +.olwidgetPaginationNext:hover { + background-position: 0px 0px; +} +.olwidgetClusterList { + margin: 0; + padding-left: 1em; +} + +/* position blocks */ +.olwidgetPopupStemTL, .olwidgetPopupStemTR { + background: url("../img/popup_icons.png") no-repeat scroll -124px 0px; +} +.olwidgetPopupStemBL, .olwidgetPopupStemBR { + background: url("../img/popup_icons.png") no-repeat scroll -96px 0px; +} +/* Put the attribution in a less obtrusive position. */ +.olControlAttribution { + bottom: 0em !important; + right: 0; +} + +/* Hide the erroneous OpenLayers Google copyright popup */ +.olLayerGoogleCopyright { + display: none; +} diff --git a/eamena/eamena/media/olwidget/img/extra_edit_icons.png b/eamena/eamena/media/olwidget/img/extra_edit_icons.png new file mode 100644 index 0000000000..eb0341f610 Binary files /dev/null and b/eamena/eamena/media/olwidget/img/extra_edit_icons.png differ diff --git a/eamena/eamena/media/olwidget/img/jquery_ui_license.txt b/eamena/eamena/media/olwidget/img/jquery_ui_license.txt new file mode 100644 index 0000000000..1857336ec9 --- /dev/null +++ b/eamena/eamena/media/olwidget/img/jquery_ui_license.txt @@ -0,0 +1,29 @@ +The file "popup_icons.png" contain images taken from the jquery-ui library, +under the following license: + +Copyright (c) 2009 Paul Bakaus, http://jqueryui.com/ + +This software consists of voluntary contributions made by many +individuals (AUTHORS.txt, http://jqueryui.com/about) For exact +contribution history, see the revision history and logs, available +at http://jquery-ui.googlecode.com/svn/ + +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. + diff --git a/eamena/eamena/media/olwidget/img/popup_icons.png b/eamena/eamena/media/olwidget/img/popup_icons.png new file mode 100644 index 0000000000..c7af5fd8e9 Binary files /dev/null and b/eamena/eamena/media/olwidget/img/popup_icons.png differ diff --git a/eamena/eamena/media/olwidget/js/cloudmade.js b/eamena/eamena/media/olwidget/js/cloudmade.js new file mode 100644 index 0000000000..eb9d2dc3a0 --- /dev/null +++ b/eamena/eamena/media/olwidget/js/cloudmade.js @@ -0,0 +1,65 @@ +// Get path of currently executing file. +// source: http://stackoverflow.com/questions/2255689/how-to-get-the-file-path-of-the-currenctly-executing-javascript-code +(function() { + var scripts = document.getElementsByTagName("script"); + var __FILE__ = scripts[scripts.length - 1].src; + window.CLOUDMADE_API_KEY = __FILE__.split("#")[1]; +})(); + +// Adapted from example provided by CloudMade wiki: +// http://developers.cloudmade.com/wiki/openlayers-api/CloudMade_Tiles +OpenLayers.Layer.CloudMade = OpenLayers.Class(OpenLayers.Layer.TMS, { + initialize: function(name, options) { + var key = CLOUDMADE_API_KEY; + + options = OpenLayers.Util.extend({ + attribution: "Data © 2009 OpenStreetMap. Rendering © 2009 CloudMade.", + maxExtent: new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34), + maxResolution: 156543.0339, + units: "m", + projection: "EPSG:900913", + isBaseLayer: true, + numZoomLevels: 19, + displayOutsideMaxExtent: true, + wrapDateLine: true, + styleId: 1 + }, options); + var prefix = [key, options.styleId, 256].join('/') + '/'; + var url = [ + "http://a.tile.cloudmade.com/" + prefix, + "http://b.tile.cloudmade.com/" + prefix, + "http://c.tile.cloudmade.com/" + prefix + ]; + var newArguments = [name, url, options]; + OpenLayers.Layer.TMS.prototype.initialize.apply(this, newArguments); + }, + + getURL: function (bounds) { + var res = this.map.getResolution(); + var x = Math.round((bounds.left - this.maxExtent.left) / (res * this.tileSize.w)); + var y = Math.round((this.maxExtent.top - bounds.top) / (res * this.tileSize.h)); + var z = this.map.getZoom(); + var limit = Math.pow(2, z); + + if (y < 0 || y >= limit) + { + return "http://cloudmade.com/js-api/images/empty-tile.png"; + } + else + { + x = ((x % limit) + limit) % limit; + + var url = this.url; + var path = z + "/" + x + "/" + y + ".png"; + + if (url instanceof Array) + { + url = this.selectUrl(path, url); + } + + return url + path; + } + }, + + CLASS_NAME: "OpenLayers.Layer.CloudMade" +}); diff --git a/eamena/eamena/media/olwidget/js/olwidget.js b/eamena/eamena/media/olwidget/js/olwidget.js new file mode 100644 index 0000000000..d355456720 --- /dev/null +++ b/eamena/eamena/media/olwidget/js/olwidget.js @@ -0,0 +1,1651 @@ +(function() { + +var olwidget = { + /* + * WKT transformations + */ + wktFormat: new OpenLayers.Format.WKT(), + featureToEWKT: function(feature, proj) { + // convert "EPSG:" in projCode to 'SRID=' + var srid = 'SRID=' + proj.projCode.substring(5) + ';'; + return srid + this.wktFormat.write(feature); + }, + stripSRIDre: new RegExp("^SRID=\\d+;(.+)", "i"), + ewktToFeature: function(wkt) { + var match = this.stripSRIDre.exec(wkt); + if (match) { + wkt = match[1]; + } + return this.wktFormat.read(wkt); + }, + multiGeometryClasses: { + 'linestring': OpenLayers.Geometry.MultiLineString, + 'point': OpenLayers.Geometry.MultiPoint, + 'polygon': OpenLayers.Geometry.MultiPolygon, + 'collection': OpenLayers.Geometry.Collection + }, + /* + * Projection transformation + */ + transformVector: function(vector, fromProj, toProj) { + // Transform the projection of a feature vector or an array of feature + // vectors (as used in a collection) between the given projections. + if (fromProj.projCode == toProj.projCode) { + return vector; + } + var transformed; + if (vector.constructor == Array) { + transformed = []; + for (var i = 0; i < vector.length; i++) { + transformed.push(this.transformVector(vector[i], fromProj, toProj)); + } + } else { + var cloned = vector.geometry.clone(); + transformed = new OpenLayers.Feature.Vector(cloned.transform(fromProj, toProj)); + } + return transformed; + }, + /* + * Constructors for base (tile) layers. + */ + wms: { + map: function(type) { + if (type === "map") { + return new OpenLayers.Layer.WMS( + "OpenLayers WMS", + "http://labs.metacarta.com/wms/vmap0", + {layers: 'basic'} + ); + } else if (type === "nasa") { + return new OpenLayers.Layer.WMS( + "NASA Global Mosaic", + "http://t1.hypercube.telascience.org/cgi-bin/landsat7", + {layers: "landsat7"} + ); + } else if (type === "blank") { + return new OpenLayers.Layer("", {isBaseLayer: true}); + } + return false; + } + }, + osm: { + map: function(type) { + return this[type](); + }, + mapnik: function() { + // Not using OpenLayers.Layer.OSM.Mapnik constructor because of + // an IE6 bug. This duplicates that constructor. + return new OpenLayers.Layer.OSM("OpenStreetMap (Mapnik)", + [ + "http://a.tile.openstreetmap.org/${z}/${x}/${y}.png", + "http://b.tile.openstreetmap.org/${z}/${x}/${y}.png", + "http://c.tile.openstreetmap.org/${z}/${x}/${y}.png" + ], + { numZoomLevels: 19 }); + } + }, + google: { + map: function(type) { + return this[type](); + }, + streets: function() { + return new OpenLayers.Layer.Google("Google Streets", + {sphericalMercator: true, numZoomLevels: 20}); + }, + physical: function() { + return this.terrain(); + }, + terrain: function() { + return new OpenLayers.Layer.Google("Google Terrain", + {sphericalMercator: true, type: google.maps.MapTypeId.TERRAIN}); + }, + satellite: function() { + return new OpenLayers.Layer.Google("Google Satellite", + {sphericalMercator: true, type: google.maps.MapTypeId.SATELLITE, + numZoomLevels: 22}); + }, + hybrid: function() { + return new OpenLayers.Layer.Google("Google Hybrid", + {sphericalMercator: true, type: google.maps.MapTypeId.HYBRID, numZoomLevels: 20}); + } + }, + yahoo: { + map: function(type) { + if (type != 'map') { + return this[type](); + } + return new OpenLayers.Layer.Yahoo("Yahoo", + {sphericalMercator: true, numZoomLevels: 20}); + }, + satellite: function(type) { + return new OpenLayers.Layer.Yahoo("Yahoo", + {type: YAHOO_MAP_SAT, sphericalMercator: true, numZoomLevels: 20}); + }, + hybrid: function(type) { + return new OpenLayers.Layer.Yahoo("Yahoo", + {type: YAHOO_MAP_HYB, sphericalMercator: true, numZoomLevels: 20}); + } + }, + ve: { + map: function(type) { + /* + VE does not play nice with vector layers at zoom level 1. + Also, map may need "panMethod: OpenLayers.Easing.Linear.easeOut" + to avoid drift. See: + + http://openlayers.org/dev/examples/ve-novibrate.html + + */ + + //var typeCode = this.types[type](); + return new OpenLayers.Layer.VirtualEarth("Bing Maps (" + type + ")", + {sphericalMercator: true, minZoomLevel: 4, type: 'AerialWithLabels' }); + }, + types: { + road: function() { return VEMapStyle.Road; }, + shaded: function() { return VEMapStyle.Shaded; }, + aerial: function() { return VEMapStyle.Aerial; }, + hybrid: function() { return VEMapStyle.Hybrid; } + } + }, + cloudmade: { + map: function(type) { + return new OpenLayers.Layer.CloudMade("CloudMade " + type, { + styleId: type + }); + } + }, + bing: { + map: function(type) { + console.log(type); + return new OpenLayers.Layer.Bing({ + key: bingkey, + type: type + }); + } + }, + /* + * Utilities + */ + + // Takes any number of objects as arguments. Working through its arguments + // from left to right, deep-copies all properties of each argument onto the + // left-most object, from left to right (so properties on objects to the + // right will override properties on objects to the left). Returns the + // left-most object. + // + // Useful for nested preferences, e.g.: + // deepJoinOptions({}, defaults, overrides, superoverrides); + deepJoinOptions: function() { + var destination = arguments[0]; + if (destination === undefined) { + destination = {}; + } + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + if (source) { + for (var a in source) { + if (source[a] !== undefined && source[a] !== null) { + if (typeof source[a] === 'object' && source[a].constructor != Array) { + destination[a] = this.deepJoinOptions(destination[a], source[a]); + } else { + destination[a] = source[a]; + } + } + } + } + } + return destination; + }, + isCollectionEmpty: function(geom) { + /* Is the provided collection empty? */ + return !(geom && (geom.constructor != Array || geom[0] != undefined)); + }, + _customBaseLayers: {}, + registerCustomBaseLayers: function(layer_descriptions) { + OpenLayers.Util.extend(this._customBaseLayers, layer_descriptions); + } +}; + +/* + * The Map. Extends an OpenLayers map. + */ +olwidget.Map = OpenLayers.Class(OpenLayers.Map, { + initialize: function(mapDivID, vectorLayers, options) { + this.vectorLayers = vectorLayers; + this.opts = this.initOptions(options); + this.initMap(mapDivID, this.opts); + }, + /* + * Extend the passed in options with defaults, and create unserialized + * objects for serialized options (such as projections). + */ + initOptions: function(options) { + var defaults = { + // Constructor options + mapOptions: { + units: 'm', + projection: "EPSG:900913", + displayProjection: "EPSG:4326", + maxExtent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34], + controls: ['LayerSwitcher', 'Navigation', 'PanZoom', 'Attribution'] + }, + // Map div stuff + mapDivClass: '', + mapDivStyle: { + width: '600px', + height: '400px' + }, + layers: ['osm.mapnik'], + defaultLon: 0, + defaultLat: 0, + defaultZoom: 4, + zoomToDataExtent: true, + zoomToDataExtentMin: 17 + }; + + // deep copy all options into "defaults". + var opts = olwidget.deepJoinOptions(defaults, options); + + // construct objects for serialized options + var me = opts.mapOptions.maxExtent; + opts.mapOptions.maxExtent = new OpenLayers.Bounds(me[0], me[1], me[2], me[3]); + if (opts.mapOptions.restrictedExtent) { + var re = opts.mapOptions.restrictedExtent; + opts.mapOptions.restrictedExtent = new OpenLayers.Bounds(re[0], re[1], re[2], re[3]); + } + opts.mapOptions.projection = new OpenLayers.Projection(opts.mapOptions.projection); + opts.mapOptions.displayProjection = new OpenLayers.Projection( + opts.mapOptions.displayProjection); + + for (var i = 0; i < opts.mapOptions.controls.length; i++) { + opts.mapOptions.controls[i] = new OpenLayers.Control[opts.mapOptions.controls[i]](); + } + return opts; + }, + /* + * Initialize the OpenLayers Map and add base layers + */ + initMap: function(mapDivId, opts) { + var mapDiv = document.getElementById(mapDivId); + OpenLayers.Util.extend(mapDiv.style, opts.mapDivStyle); + if (opts.mapDivClass) { + mapDiv.className = opts.mapDivClass; + } + + // Must have explicitly specified position for popups to work properly. + if (!mapDiv.style.position) { + mapDiv.style.position = 'relative'; + } + + var layers = []; + for (var i = 0; i < opts.layers.length; i++) { + var parts = opts.layers[i].split("."); + var map_service = olwidget[parts[0]]; + var map_type = parts[1]; + + layers.push(map_service.map(map_type)); + + // workaround for problems with Microsoft layers and vector layer + // drift (see http://openlayers.com/dev/examples/ve-novibrate.html) + if (parts[0] == "ve") { + if (opts.mapOptions.panMethod === undefined) { + opts.mapOptions.panMethod = OpenLayers.Easing.Linear.easeOut; + } + } + } + + // Map super constructor + OpenLayers.Map.prototype.initialize.apply(this, [mapDiv.id, opts.mapOptions]); + + if (this.vectorLayers) { + for (var i = 0; i < this.vectorLayers.length; i++) { + layers.push(this.vectorLayers[i]); + } + } else { + this.vectorLayers = []; + } + if (layers.length > 0) { + this.addLayers(layers); + if (this.baseLayer) { + // Only initCenter if we have base layers -- otherwise, user is + // responsible for adding and then calling initCenter. + this.initCenter(); + } + } + this.selectControl = new OpenLayers.Control.SelectFeature( + this.vectorLayers); + this.selectControl.events.on({ + featurehighlighted: this.featureHighlighted, + featureunhighlighted: this.featureUnhighlighted, + scope: this + }); + // Allow dragging when over features. + this.selectControl.handlers.feature.stopDown = false; + this.events.on({ + zoomend: this.zoomEnd, + scope: this + }); + this.addControl(this.selectControl); + this.selectControl.activate(); + this.addControl(new olwidget.EditableLayerSwitcher()); + }, + initCenter: function() { + if (this.opts.zoomToDataExtent) { + var extent = new OpenLayers.Bounds(); + for (var i = 0; i < this.vectorLayers.length; i++) { + var vl = this.vectorLayers[i]; + if (vl.opts.cluster) { + for (var j = 0; j < vl.features.length; j++) { + for (var k = 0; k < vl.features[j].cluster.length; k++) { + extent.extend(vl.features[j].cluster[k].geometry.getBounds()); + } + } + } else { + extent.extend(vl.getDataExtent()); + } + } + if (!extent.equals(new OpenLayers.Bounds())) { + this.zoomToExtent(extent); + this.zoomTo(Math.min(this.getZoom(), this.opts.zoomToDataExtentMin)); + return; + } + } + // zoomToDataExtent == false, or there is no data on any layer + var center = new OpenLayers.LonLat( + this.opts.defaultLon, this.opts.defaultLat); + center = center.transform(this.displayProjection, this.projection); + this.setCenter(center, this.opts.defaultZoom); + }, + featureHighlighted: function(evt) { + this.createPopup(evt); + }, + featureUnhighlighted: function(evt) { + this.deleteAllPopups(); + }, + zoomEnd: function(evt) { + this.deleteAllPopups(); + }, + + /** + * Override parent to allow placement of popups outside viewport + */ + addPopup: function(popup, exclusive) { + if (exclusive) { + //remove all other popups from screen + for (var i = this.popups.length - 1; i >= 0; --i) { + this.removePopup(this.popups[i]); + } + } + + popup.map = this; + this.popups.push(popup); + var popupDiv = popup.draw(); + if (popupDiv) { + popupDiv.style.zIndex = this.Z_INDEX_BASE.Popup + + this.popups.length; + this.div.appendChild(popupDiv); + // store a reference to this function so we can unregister on + // removal + this.popupMoveFunc = function(event) { + var px = this.getPixelFromLonLat(popup.lonlat); + popup.moveTo(px); + }; + this.events.register("move", this, this.popupMoveFunc); + this.popupMoveFunc(); + } + }, + /** + * Override parent to allow placement of popups outside viewport + */ + removePopup: function(popup) { + OpenLayers.Util.removeItem(this.popups, popup); + if (popup.div) { + try { + this.div.removeChild(popup.div); + this.events.unregister("move", this, this.popupMoveFunc); + } catch (e) { } + } + popup.map = null; + }, + /** + * Build a paginated popup + */ + createPopup: function(evt) { + var feature = evt.feature; + var lonlat; + if (feature.geometry.CLASS_NAME == "OpenLayers.Geometry.Point") { + lonlat = feature.geometry.getBounds().getCenterLonLat(); + } else { + lonlat = this.getLonLatFromViewPortPx(evt.object.handlers.feature.evt.xy); + } + + var popupHTML = []; + if (feature.cluster) { + if (feature.layer && feature.layer.opts && + feature.layer.opts.clusterDisplay == 'list') { + if (feature.cluster.length > 1) { + var html = ""; + popupHTML.push(html); + } else { + popupHTML.push(feature.cluster[0].attributes.html); + } + } else { + for (var i = 0; i < feature.cluster.length; i++) { + popupHTML.push(feature.cluster[i].attributes.html); + } + } + } else { + if (feature.attributes.html) { + popupHTML.push(feature.attributes.html); + } + } + if (popupHTML.length > 0) { + var infomap = this; + var popup = new olwidget.Popup(null, + lonlat, null, popupHTML, null, true, + function() { infomap.selectControl.unselect(feature); }, + this.opts.popupDirection, + this.opts.popupPaginationSeparator); + if (this.opts.popupsOutside) { + popup.panMapIfOutOfView = false; + } + this.addPopup(popup); + } + }, + deleteAllPopups: function() { + // must clone this.popups array first; it's modified during iteration + var popups = []; + var i; + for (i = 0; i < this.popups.length; i++) { + popups.push(this.popups[i]); + } + for (i = 0; i < popups.length; i++) { + this.removePopup(popups[i]); + } + this.popups = []; + }, + CLASS_NAME: "olwidget.Map" +}); + +olwidget.BaseVectorLayer = OpenLayers.Class(OpenLayers.Layer.Vector, { + initialize: function(options) { + if (!options) { + options = {}; + } + this.opts = options; + this.defaultOpts = {}; + OpenLayers.Layer.Vector.prototype.initialize.apply(this); + }, + setMap: function(map) { + OpenLayers.Layer.Vector.prototype.setMap.apply(this, [map]); + // If we are in an olwidget Map, inherit the olwidget Map's properties. + if (map.CLASS_NAME == "olwidget.Map") { + this.opts = olwidget.deepJoinOptions({ + name: "data", + overlayStyle: { + fillColor: '#ff00ff', + strokeColor: '#ff00ff', + pointRadius: 6, + fillOpacity: 0.5, + strokeWidth: 2 + }, + selectOverlayStyle: { + fillColor: '#9999ff', + strokeColor: '#9999ff', + pointRadius: 6, + fillOpacity: 0.5, + strokeWidth: 2 + } + }, this.defaultOpts, map.opts, this.opts); + this.name = this.opts.name; + + this.styleMap = new OpenLayers.StyleMap({ + "default": new OpenLayers.Style(this.opts.overlayStyle, + {context: this.opts.overlayStyleContext}), + "select": new OpenLayers.Style(this.opts.selectOverlayStyle, + {context: this.opts.overlayStyleContext}) + }); + } + if (this.opts.paging === true) { + if (this.strategies === null) { + this.strategies = []; + } + var paging = new OpenLayers.Strategy.Paging(); + paging.setLayer(this); + this.strategies.push(paging); + paging.activate(); + } + }, + CLASS_NAME: "olwidget.BaseVectorLayer" +}); + +olwidget.InfoLayer = OpenLayers.Class(olwidget.BaseVectorLayer, { + initialize: function(info, options) { + olwidget.BaseVectorLayer.prototype.initialize.apply(this, [options]); + this.info = info; + }, + setMap: function(map) { + if (this.opts.cluster || map.opts.cluster) { + // Use a different default style if we are clustering. + var clusterStyle = { + pointRadius: "${radius}", + strokeWidth: "${width}", + label: "${label}", + labelSelect: true, + fontSize: "11px", + fontFamily: "Helvetica, sans-serif", + fontColor: "#ffffff" + }; + this.defaultOpts.overlayStyle = olwidget.deepJoinOptions( + {}, clusterStyle); + this.defaultOpts.selectOverlayStyle = olwidget.deepJoinOptions( + {}, clusterStyle); + this.defaultOpts.overlayStyleContext = { + width: function(feature) { + return (feature.cluster) ? 2 : 1; + }, + radius: function(feature) { + var n = feature.attributes.count; + var pix; + if (n == 1) { + pix = 6; + } else if (n <= 5) { + pix = 8; + } else if (n <= 25) { + pix = 10; + } else if (n <= 50) { + pix = 12; + } else { + pix = 14; + } + return pix; + }, + label: function(feature) { + if (feature.cluster && feature.cluster.length > 1) { + return feature.cluster.length; + } + return ''; + } + }; + } + // Merge our options with the map's. + olwidget.BaseVectorLayer.prototype.setMap.apply(this, arguments); + + // Add cluster strategy if needed. + if (this.opts.cluster === true) { + if (!this.strategies) { + this.strategies = []; + } + var cluster = new OpenLayers.Strategy.Cluster(); + cluster.setLayer(this); + this.strategies.push(cluster); + cluster.activate(); + } + }, + afterAdd: function() { + olwidget.BaseVectorLayer.prototype.afterAdd.apply(this); + var features = []; + for (var i = 0; i < this.info.length; i++) { + var feature = olwidget.ewktToFeature(this.info[i][0]); + if (olwidget.isCollectionEmpty(feature)) { + continue; + } + feature = olwidget.transformVector(feature, + this.map.displayProjection, this.map.projection); + + if (feature.constructor != Array) { + feature = [feature]; + } + var htmlInfo = this.info[i][1]; + for (var k = 0; k < feature.length; k++) { + if (typeof htmlInfo === "object") { + feature[k].attributes = htmlInfo; + if (typeof htmlInfo.style !== "undefined") { + feature[k].style = OpenLayers.Util.applyDefaults( + htmlInfo.style, this.opts.overlayStyle + ); + } + } else { + feature[k].attributes = { html: htmlInfo }; + } + features.push(feature[k]); + } + } + this.addFeatures(features); + }, + CLASS_NAME: "olwidget.InfoLayer" +}); + +olwidget.EditableLayer = OpenLayers.Class(olwidget.BaseVectorLayer, { + undoStack: null, + undoStackPos: null, + undoStackLength: 1000, + + initialize: function(textareaId, options) { + olwidget.BaseVectorLayer.prototype.initialize.apply(this, + [options]); + this.undoStack = []; + this.textarea = document.getElementById(textareaId); + }, + setMap: function(map) { + this.defaultOpts = { + editable: true, + geometry: 'point', + hideTextarea: true, + isCollection: false + }; + olwidget.BaseVectorLayer.prototype.setMap.apply(this, arguments); + // force non-clustering, it doesn't make sense for editable maps. + this.opts.cluster = false; + if (this.opts.hideTextarea) { + this.textarea.style.display = 'none'; + } + this.buildControls(); + this.readWKT(); + // init undo stack + this.addUndoState(); + }, + _addDrawFeature: function(obj_type, obj_options, controls) { + var drawControl = new OpenLayers.Control.DrawFeature( + this, obj_type, obj_options); + drawControl.activate = function() { + OpenLayers.Control.prototype.activate.apply(this, []); + this.map.div.style.cursor = "crosshair"; + }; + drawControl.deactivate = function() { + OpenLayers.Control.prototype.deactivate.apply(this, []); + this.map.div.style.cursor = "auto"; + }; + controls.push(drawControl); + if (!this.defaultControl) { + this.defaultControl = drawControl; + } + }, + + buildControls: function() { + var controls = []; + var context = this; + + // + // Custom controls: + // + + // Clear all + controls.push(new OpenLayers.Control.Button({ + displayClass: 'olControlClearFeatures', + trigger: function() { + context.clearFeatures(); + }, + title: "Clear all" + })); + + // redo + this.redoButton = new OpenLayers.Control.Button({ + displayClass: 'olControlRedo', + trigger: function() { + context.redo(); + }, + title: "Redo" + }); + controls.push(this.redoButton); + + // undo + this.undoButton = new OpenLayers.Control.Button({ + displayClass: 'olControlUndo', + trigger: function() { + context.undo(); + }, + title: "Undo" + }); + controls.push(this.undoButton); + + // don't add duplicate functionality from single point maps. + if (this.opts.geometry != 'point' || this.opts.isCollection) { + // Delete vertex + controls.push(new olwidget.DeleteVertex(this, { + title: "Delete vertices" + })); + } + + + var nav = new OpenLayers.Control.Navigation({ + "title": "Move the map" + }); + controls.push(nav); + + // Drawing control(s) + var geometries; + if (this.opts.geometry.constructor == Array) { + geometries = this.opts.geometry; + } else { + geometries = [this.opts.geometry]; + } + this.defaultControl = null; + + var has_point = false; + var has_linestring = false; + var has_polygon = false; + for (var i = 0; i < geometries.length; i++) { + if (geometries[i] == 'linestring') + has_linestring = true; + else if (geometries[i] == 'polygon') + has_polygon = true; + else if (geometries[i] == 'point') + has_point = true; + } + if (has_polygon) { + this._addDrawFeature(OpenLayers.Handler.Polygon, { + 'displayClass': 'olControlDrawFeaturePolygon', + "title": "Draw polygons" + }, controls); + } + if (has_linestring) { + this._addDrawFeature(OpenLayers.Handler.Path, { + 'displayClass': 'olControlDrawFeaturePath', + 'title': "Draw lines" + }, controls); + } + if (has_point) { + this._addDrawFeature(OpenLayers.Handler.Point, { + 'displayClass': 'olControlDrawFeaturePoint', + "title": "Draw points" + }, controls); + } + + // don't add duplicate functionality from single point maps. + if (this.opts.geometry != 'point' || this.opts.isCollection) { + // Modify feature control + var mod = new OpenLayers.Control.ModifyFeature(this, { + clickout: true, + title: "Modify features" + }); + controls.push(mod); + } + + this.controls = controls; + }, + clearFeatures: function() { + this.removeFeatures(this.features); + this.destroyFeatures(); + if (this.textarea.value !== "") { + this.textarea.value = ""; + this.addUndoState(); + } + }, + addUndoState: function() { + // Put the current value of the textarea in the undo stack. + var value = this.textarea.value; + if (this.undoStack.length > this.undoStackPos) { + this.undoStack = this.undoStack.slice(0, this.undoStackPos + 1); + } + this.undoStack.push(value); + if (this.undoStack.length > this.undoStackLength) { + this.undoStack.shift(); + } + this.undoStackPos = this.undoStack.length - 1; + this.setUndoButtonStates(); + }, + undo: function() { + // Move to previous undo stack position. + if (this.undoStackPos > 0) { + this.undoStackPos--; + if (this.undoStackPos < this.undoStack.length) { + this.textarea.value = this.undoStack[this.undoStackPos]; + this.readWKT(); + } + } + this.setUndoButtonStates(); + }, + redo: function() { + // Move to next undo stack position. + if (this.undoStackPos < this.undoStack.length - 1) { + this.undoStackPos++; + this.textarea.value = this.undoStack[this.undoStackPos]; + this.readWKT(); + } + this.setUndoButtonStates(); + }, + setUndoButtonStates: function() { + if (this.undoButton.map) { + if (this.undoStackPos > 0) { + this.undoButton.activate(); + } else { + this.undoButton.deactivate(); + } + if (this.undoStackPos < this.undoStack.length - 1) { + this.redoButton.activate(); + } else { + this.redoButton.deactivate(); + } + } + }, + readWKT: function() { + // Read WKT from the text field. We assume that the WKT uses the + // projection given in "displayProjection", and ignore any initial + // SRID. + var wkt = this.textarea.value; + if (this.features) { + this.removeFeatures(this.features); + } + if (wkt) { + var geom = olwidget.ewktToFeature(wkt); + if (!olwidget.isCollectionEmpty(geom)) { + geom = olwidget.transformVector(geom, + this.map.displayProjection, + this.map.projection); + if (geom.constructor == Array || + geom.geometry.CLASS_NAME === + "OpenLayers.Geometry.MultiLineString" || + geom.geometry.CLASS_NAME === + "OpenLayers.Geometry.MultiPoint" || + geom.geometry.CLASS_NAME === + "OpenLayers.Geometry.MultiPolygon") { + // extract geometries from MULTI types into + // individual components (keeps the vector layer flat) + if (geom.geometry != undefined) { + var geoms = []; + var n = geom.geometry.components.length; + for (var i = 0; i < n; i++) { + geoms.push( + new OpenLayers.Feature.Vector( + geom.geometry.components[i]) + ); + } + this.addFeatures(geoms, {silent: true}); + } else { + this.addFeatures(geom, {silent: true}); + } + } else { + this.addFeatures([geom], {silent: true}); + } + this.numGeom = this.features.length; + } else { + this.numGeom = 0; + } + } + }, + // Callback for openlayers "featureadded" + addWKT: function(event) { + // This function will sync the contents of the `vector` layer with the + // WKT in the text field. + if (this.opts.isCollection) { + this.featureToTextarea(this.features); + } else { + // Make sure to remove any previously added features. + if (this.features.length > 1) { + var old_feats = [this.features[0]]; + this.removeFeatures(old_feats); + this.destroyFeatures(old_feats); + } + this.featureToTextarea(event.feature); + } + this.addUndoState(); + }, + // Callback for openlayers "featuremodified" + modifyWKT: function(event) { + if (this.opts.isCollection){ + // OpenLayers adds points around the modified feature that we want + // to strip. So only take the features up to "numGeom", the number + // of features counted when we last added. + var feat = []; + for (var i = 0; i < Math.min(this.numGeom, this.features.length); i++) { + feat.push(this.features[i].clone()); + } + this.featureToTextarea(feat); + } else { + if (event.feature) { + this.featureToTextarea(event.feature); + } else { + this.textarea.value = ""; + } + } + this.addUndoState(); + }, + featureToTextarea: function(feature) { + if (this.opts.isCollection) { + this.numGeom = feature.length; + } else { + this.numGeom = 1; + } + feature = olwidget.transformVector(feature, + this.map.projection, this.map.displayProjection); + if (this.opts.isCollection) { + // Convert to multi-geometry types if we are a collection. Passing + // arrays to the WKT formatter results in a "GEOMETRYCOLLECTION" + // type, but if we have only one geometry, we should use a + // "MULTI" type. + if (this.opts.geometry.constructor != Array) { + var geoms = []; + for (var i = 0; i < feature.length; i++) { + geoms.push(feature[i].geometry); + } + var GeoClass = olwidget.multiGeometryClasses[this.opts.geometry]; + feature = new OpenLayers.Feature.Vector(new GeoClass(geoms)); + } + } + this.textarea.value = olwidget.featureToEWKT( + feature, this.map.displayProjection); + }, + CLASS_NAME: "olwidget.EditableLayer" +}); + +/** + * Convenience and backwards-compatibility single-layer EditableMap. + */ +olwidget.EditableMap = OpenLayers.Class(olwidget.Map, { + initialize: function(textareaId, options) { + // Create div element. + var textarea = document.getElementById(textareaId); + var div = document.createElement("div"); + div.setAttribute('id', textareaId + "_map"); + textarea.parentNode.insertBefore(div, textarea); + olwidget.Map.prototype.initialize.apply(this, [ + div.id, [new olwidget.EditableLayer(textareaId)], options + ]); + } +}); + +/** + * Convenience and backwards-compatibility single-layer InfoMap. + */ +olwidget.InfoMap = OpenLayers.Class(olwidget.Map, { + initialize: function(divId, info, options) { + olwidget.Map.prototype.initialize.apply(this, [ + divId, [new olwidget.InfoLayer(info)], options + ]); + } +}); + + +olwidget.EditableLayerSwitcher = OpenLayers.Class(OpenLayers.Control.LayerSwitcher, { + // The layer we are currently editing + currentlyEditing: null, + /** A list of all editable layers, including none. Contains an object: + * { + * layer: the layer, + * inputElem: the rado button, + * labelSpan: the name span next to the button + * } + */ + editableLayers: [], + // Panel for editing controls + panel: null, + + initialize: function(options) { + OpenLayers.Control.prototype.initialize.apply(this, arguments); + }, + setMap: function() { + OpenLayers.Control.prototype.setMap.apply(this, arguments); + this.map.events.on({ + "addlayer": this.redraw, + "changelayer": this.redraw, + "removelayer": this.redraw, + scope: this + }); + }, + onInputClick: function(e) { + if (!this.inputElem.disabled) { + if (this.layer) { + this.layerSwitcher.setEditing(this.layer); + } else { + this.layerSwitcher.stopEditing(); + } + this.layerSwitcher.minimizeControl(); + } + }, + stopEditing: function() { + if (this.currentlyEditing) { + var layer = this.currentlyEditing; + layer.events.un({ + "featuremodified": layer.modifyWKT, + "featureadded": layer.addWKT, + scope: layer + }); + } + if (this.panel) { + this.panel.deactivate(); + this.panel.destroy(); + this.panel = null; + } + // TODO: i18n + this.maximize.innerHTML = "Edit"; + this.minimize.innerHTML = "(-) Edit"; + this.currentlyEditing = null; + if (this.map.selectControl) { + this.map.selectControl.activate(); + } + this.map.removeControl(this.panel); + this.setChecked(null); + }, + setEditing: function(layer) { + if (this.currentlyEditing) { + this.stopEditing(); + } + this.panel = new olwidget.EditingToolbar({ + defaultControl: layer.defaultControl, + displayClass: 'olControlEditingToolbar' + }); + this.panel.layer = layer; + this.panel.addControls(layer.controls); + this.map.addControl(this.panel); + + this.currentlyEditing = layer; + // TODO: i18n + this.maximize.innerHTML = "Editing \"" + layer.name + "\""; + this.minimize.innerHTML = "(-) Editing \"" + layer.name + "\""; + layer.events.on({ + "featuremodified": layer.modifyWKT, + "featureadded": layer.addWKT, + scope: layer + }); + layer.setUndoButtonStates(); + if (this.map.selectControl) { + this.map.selectControl.deactivate(); + } + + // fix ugly MSIE errors on editing toolbar disappear: + // bind event listeners to drawFeature controls activation event + for(var i=0; i < this.panel.controls.length; i++){ + this.panel.controls[i].events.register('activate', this, function(evt){ + this.panel.redraw(); + }); + } + }, + setChecked: function(layer) { + for (var i = 0; i < this.editableLayers.length; i++) { + if (layer == this.editableLayers[i].layer) { + this.editableLayers[i].inputElem.checked = true; + break; + } + } + }, + destroy: function() { + OpenLayers.Event.stopObservingElement(this.div); + this.map.events.un({ + "addlayer": this.redraw, + "changelayer": this.redraw, + "removelayer": this.redraw, + "changebaselayer": this.redraw, + scope: this + }); + clearLayersArray(); + OpenLayers.Control.prototype.destroy.apply(this, arguments); + }, + clearLayersArray: function() { + for (var i = 0; i < this.editableLayers.length; i++) { + var layer = this.editableLayers[i]; + OpenLayers.Event.stopObservingElement(layer.inputElem); + OpenLayers.Event.stopObservingElement(layer.labelSpan); + } + this.editableLayers = []; + }, + draw: function() { + OpenLayers.Control.prototype.draw.apply(this); + this.loadContents(); + this.redraw(); + this.stopEditing(); + return this.div; + }, + buildInput: function(name, checked, layer) { + var input = document.createElement("input"); + input.id = this.id + "_edit_switcher_" + this.editableLayers.length; + input.name = this.id + "_edit"; + input.type = "radio"; + input.value = name; + input.checked = checked; + var span = document.createElement("span"); + OpenLayers.Element.addClass(span, "olwidgetLabel"); + span.innerHTML = name; + + if (layer && (!layer.inRange || !layer.visibility)) { + input.disabled = true; + OpenLayers.Element.addClass(span, "olwidgetDisabled"); + } + + var context = { + "inputElem": input, + "layer": layer, + "layerSwitcher": this + }; + OpenLayers.Event.observe(input, "mouseup", + OpenLayers.Function.bindAsEventListener(this.onInputClick, + context) + ); + OpenLayers.Event.observe(span, "mouseup", + OpenLayers.Function.bindAsEventListener(this.onInputClick, + context) + ); + + this.layersDiv.appendChild(input); + this.layersDiv.appendChild(span); + this.layersDiv.appendChild(document.createElement("br")); + + this.editableLayers.push({ + 'layer': layer, + 'inputElem': input, + 'labelSpan': span + }); + }, + redraw: function() { + this.clearLayersArray(); + this.layersDiv.innerHTML = ""; + this.editingControls.innerHTML = ""; + if (this.panel && this.panel.div) { + this.editingControls.appendChild(this.panel.div); + var clr = document.createElement("div"); + clr.style.clear = "both"; + this.editingControls.appendChild(clr); + } + + // TODO: i18n + this.buildInput("none", !this.currentlyEditing, null); + + for (var i = 0; i < this.map.layers.length; i++) { + var layer = this.map.layers[i]; + if (layer.opts && layer.opts.editable) { + this.buildInput(layer.name, + this.currentlyEditing && + this.currentlyEditing.name == layer.name, + layer); + } + } + if (this.editableLayers.length == 1) { + this.div.style.display = "none"; + } else { + this.div.style.display = ""; + } + }, + loadContents: function() { + // Set event handlers such that we don't get spurious clicks or mouse + // ups for events not initiated in our control. + OpenLayers.Event.observe(this.div, "mouseup", + OpenLayers.Function.bindAsEventListener(this.mouseUp, this)); + OpenLayers.Event.observe(this.div, "click", this.ignoreEvent); + OpenLayers.Event.observe(this.div, "mousedown", + OpenLayers.Function.bindAsEventListener(this.mouseDown, this)); + OpenLayers.Event.observe(this.div, "dblclick", this.ignoreEvent); + + // Optionally create rounded corners + this.container = document.createElement("div"); + OpenLayers.Element.addClass(this.container, "olwidgetContainer"); + this.div.appendChild(this.container); + if (this.roundedCorner) { + OpenLayers.Rico.Corner.round(this.div, { + corners: "bl br", + bgColor: "transparent", + color: this.roundedCornerColor, + blend: false + }); + OpenLayers.Rico.Corner.changeOpacity(this.container, 0.75); + } + + // Container for layers + this.layersDiv = document.createElement("div"); + this.layersDiv.id = this.id + "_layersDiv"; + this.layersDiv.style.display = "none"; + OpenLayers.Element.addClass(this.layersDiv, "layersDiv"); + + // Container for editing controls + this.editingControls = document.createElement("div"); + this.editingControls.id = this.id + "_editingControls"; + OpenLayers.Element.addClass(this.editingControls, "editingControls"); + + // Heading + this.maximize = document.createElement("div"); + OpenLayers.Element.addClass(this.maximize, "olwidgetMaxMin olwidgetMax"); + this.minimize = document.createElement("div"); + OpenLayers.Element.addClass(this.minimize, "olwidgetMaxMin olwidgetMin"); + this.minimize.style.display = "none"; + OpenLayers.Event.observe(this.maximize, "click", + OpenLayers.Function.bindAsEventListener(this.maximizeControl, this) + ); + OpenLayers.Event.observe(this.minimize, "click", + OpenLayers.Function.bindAsEventListener(this.minimizeControl, this) + ); + + this.container.appendChild(this.maximize); + this.container.appendChild(this.minimize); + this.container.appendChild(this.editingControls); + var clr = document.createElement("div"); + clr.style.clear = "both"; + this.container.appendChild(clr); + this.container.appendChild(this.layersDiv); + }, + maximizeControl: function(e) { + // Only show control if there's more than one thing to choose + if (this.editableLayers.length == 2 && + this.editableLayers[1].layer.visibility) { + if (this.currentlyEditing) { + this.stopEditing(); + } else { + this.setEditing(this.editableLayers[1].layer); + } + } else { + this.showControls(false); + } + if (e != null) { + OpenLayers.Event.stop(e); + } + }, + minimizeControl: function(e) { + this.showControls(true); + if (e != null) { + OpenLayers.Event.stop(e); + } + }, + showControls: function(minimize) { + this.maximize.style.display = minimize ? "" : "none"; + this.minimize.style.display = minimize ? "none" : ""; + this.layersDiv.style.display = minimize ? "none" : ""; + }, + CLASS_NAME: "olwidget.EditableLayerSwitcher" +}); + +/* + * Our editing toolbar. + */ +olwidget.EditingToolbar = OpenLayers.Class(OpenLayers.Control.Panel, { + onClick: function(ctrl, evt) { + // Keep undo button states + OpenLayers.Event.stop(evt ? evt : window.event); + this.activateControl(ctrl); + this.layer.setUndoButtonStates(); + } +}); + +/* + * Paginated, framed popup type, CSS stylable. + */ +olwidget.Popup = OpenLayers.Class(OpenLayers.Popup.Framed, { + autoSize: true, + panMapIfOutOfView: true, + fixedRelativePosition: false, + // Position blocks. Overriden to include additional "className" parameter, + // allowing image paths relative to css rather than relative to the html + // file (as paths included in a JS file are computed). + positionBlocks: { + "tl": { + 'offset': new OpenLayers.Pixel(44, -6), + 'padding': new OpenLayers.Bounds(5, 14, 5, 5), + 'blocks': [ + { // stem + className: 'olwidgetPopupStemTL', + size: new OpenLayers.Size(24, 14), + anchor: new OpenLayers.Bounds(null, 0, 32, null), + position: new OpenLayers.Pixel(0, -28) + } + ] + }, + "tr": { + 'offset': new OpenLayers.Pixel(-44, -6), + 'padding': new OpenLayers.Bounds(5, 14, 5, 5), + 'blocks': [ + { // stem + className: "olwidgetPopupStemTR", + size: new OpenLayers.Size(24, 14), + anchor: new OpenLayers.Bounds(32, 0, null, null), + position: new OpenLayers.Pixel(0, -28) + } + ] + }, + "bl": { + 'offset': new OpenLayers.Pixel(44, 6), + 'padding': new OpenLayers.Bounds(5, 5, 5, 14), + 'blocks': [ + { // stem + className: "olwidgetPopupStemBL", + size: new OpenLayers.Size(24, 14), + anchor: new OpenLayers.Bounds(null, null, 32, 0), + position: new OpenLayers.Pixel(0, 0) + } + ] + }, + "br": { + 'offset': new OpenLayers.Pixel(-44, 6), + 'padding': new OpenLayers.Bounds(5, 5, 5, 14), + 'blocks': [ + { // stem + className: "olwidgetPopupStemBR", + size: new OpenLayers.Size(24, 14), + anchor: new OpenLayers.Bounds(32, null, null, 0), + position: new OpenLayers.Pixel(0, 0) + } + ] + } + }, + + initialize: function(id, lonlat, contentSize, contentHTML, anchor, closeBox, + closeBoxCallback, relativePosition, separator) { + if (relativePosition && relativePosition != 'auto') { + this.fixedRelativePosition = true; + this.relativePosition = relativePosition; + } + if (separator === undefined) { + this.separator = ' of '; + } else { + this.separator = separator; + } + // we don't use the default close box because we want it to appear in + // the content div for easier CSS control. + this.olwidgetCloseBox = closeBox; + this.olwidgetCloseBoxCallback = closeBoxCallback; + this.page = 0; + OpenLayers.Popup.Framed.prototype.initialize.apply(this, [id, lonlat, + contentSize, contentHTML, anchor, false, null]); + }, + + /* + * Construct the interior of a popup. If contentHTML is an Array, display + * the array element specified by this.page. + */ + setContentHTML: function(contentHTML) { + if (contentHTML !== null && contentHTML !== undefined) { + this.contentHTML = contentHTML; + } + + var pageHTML; + var showPagination; + if (this.contentHTML.constructor != Array) { + pageHTML = this.contentHTML; + showPagination = false; + } else { + pageHTML = this.contentHTML[this.page]; + showPagination = this.contentHTML.length > 1; + } + + if ((this.contentDiv !== null) && (pageHTML !== null)) { + var popup = this; // for closures + + // Clear old contents + this.contentDiv.innerHTML = ""; + + // Build container div + var containerDiv = document.createElement("div"); + containerDiv.className = 'olwidgetPopupContent'; + this.contentDiv.appendChild(containerDiv); + + // Build close box + if (this.olwidgetCloseBox) { + var closeDiv = document.createElement("div"); + closeDiv.className = "olwidgetPopupCloseBox"; + closeDiv.innerHTML = "close"; + closeDiv.onclick = function(event) { + popup.olwidgetCloseBoxCallback.apply(popup, arguments); + }; + containerDiv.appendChild(closeDiv); + } + + var pageDiv = document.createElement("div"); + pageDiv.innerHTML = pageHTML; + pageDiv.className = "olwidgetPopupPage"; + containerDiv.appendChild(pageDiv); + + if (showPagination) { + // Build pagination control + + var paginationDiv = document.createElement("div"); + paginationDiv.className = "olwidgetPopupPagination"; + var prev = document.createElement("div"); + prev.className = "olwidgetPaginationPrevious"; + prev.innerHTML = "prev"; + prev.onclick = function(event) { + popup.page = (popup.page - 1 + popup.contentHTML.length) % + popup.contentHTML.length; + popup.setContentHTML(); + popup.map.events.triggerEvent("move"); + }; + + var count = document.createElement("div"); + count.className = "olwidgetPaginationCount"; + count.innerHTML = (this.page + 1) + this.separator + this.contentHTML.length; + var next = document.createElement("div"); + next.className = "olwidgetPaginationNext"; + next.innerHTML = "next"; + next.onclick = function(event) { + popup.page = (popup.page + 1) % popup.contentHTML.length; + popup.setContentHTML(); + popup.map.events.triggerEvent("move"); + }; + + paginationDiv.appendChild(prev); + paginationDiv.appendChild(count); + paginationDiv.appendChild(next); + containerDiv.appendChild(paginationDiv); + + } + var clearFloat = document.createElement("div"); + clearFloat.style.clear = "both"; + containerDiv.appendChild(clearFloat); + + if (this.autoSize) { + this.registerImageListeners(); + this.updateSize(); + } + } + }, + + /* + * Override parent to make the popup more CSS-friendly. Rather than + * specifying img paths in javascript, give position blocks CSS classes + * that can be used to apply background images to the divs. + */ + createBlocks: function() { + this.blocks = []; + + // since all positions contain the same number of blocks, we can + // just pick the first position and use its blocks array to create + // our blocks array + var firstPosition = null; + for(var key in this.positionBlocks) { + firstPosition = key; + break; + } + + var position = this.positionBlocks[firstPosition]; + for (var i = 0; i < position.blocks.length; i++) { + + var block = {}; + this.blocks.push(block); + + var divId = this.id + '_FrameDecorationDiv_' + i; + block.div = OpenLayers.Util.createDiv(divId, + null, null, null, "absolute", null, "hidden", null + ); + this.groupDiv.appendChild(block.div); + } + }, + /* + * Override parent to make the popup more CSS-friendly, reflecting + * modifications to createBlocks. + */ + updateBlocks: function() { + if (!this.blocks) { + this.createBlocks(); + } + if (this.size && this.relativePosition) { + var position = this.positionBlocks[this.relativePosition]; + for (var i = 0; i < position.blocks.length; i++) { + + var positionBlock = position.blocks[i]; + var block = this.blocks[i]; + + // adjust sizes + var l = positionBlock.anchor.left; + var b = positionBlock.anchor.bottom; + var r = positionBlock.anchor.right; + var t = positionBlock.anchor.top; + + // note that we use the isNaN() test here because if the + // size object is initialized with a "auto" parameter, the + // size constructor calls parseFloat() on the string, + // which will turn it into NaN + // + var w = (isNaN(positionBlock.size.w)) ? this.size.w - (r + l) + : positionBlock.size.w; + + var h = (isNaN(positionBlock.size.h)) ? this.size.h - (b + t) + : positionBlock.size.h; + + block.div.style.width = (w < 0 ? 0 : w) + 'px'; + block.div.style.height = (h < 0 ? 0 : h) + 'px'; + + block.div.style.left = (l !== null) ? l + 'px' : ''; + block.div.style.bottom = (b !== null) ? b + 'px' : ''; + block.div.style.right = (r !== null) ? r + 'px' : ''; + block.div.style.top = (t !== null) ? t + 'px' : ''; + + block.div.className = positionBlock.className; + } + + this.contentDiv.style.left = this.padding.left + "px"; + this.contentDiv.style.top = this.padding.top + "px"; + } + }, + updateSize: function() { + if (this.map.opts.popupsOutside === true) { + var preparedHTML = "
" + + this.contentDiv.innerHTML + + "
"; + + var containerElement = document.body; + var realSize = OpenLayers.Util.getRenderedDimensions( + preparedHTML, null, { + displayClass: this.displayClass, + containerElement: containerElement + } + ); + return this.setSize(realSize); + } else { + return OpenLayers.Popup.prototype.updateSize.apply(this, arguments); + } + }, + + CLASS_NAME: "olwidget.Popup" +}); + +olwidget.DeleteVertex = OpenLayers.Class(OpenLayers.Control.ModifyFeature, { + initialize: function(layer, options) { + options['toggle'] = false; + OpenLayers.Control.ModifyFeature.prototype.initialize.apply(this, + [layer, options]); + this.selectControl.onUnselect = this.unselectFeature; + var control = this; + this.selectControl.callbacks.clickout = function(feature) { + control.layer.removeFeatures(control.vertices, {silent: true}); + control.editingFeature = null; + if (!this.hover && this.clickout) { + this.unselectAll(); + } + }; + this.editingFeature = null; + this.handlers.feature = new OpenLayers.Handler.Feature( + this, this.layer, { + over: this.overVertex, + out: this.outVertex + } + ); + }, + overVertex: function(feature) { + if (feature.geometry.CLASS_NAME === "OpenLayers.Geometry.Point") { + OpenLayers.Element.addClass(this.map.viewPortDiv, this.displayClass + "Over"); + } + }, + outVertex: function(feature) { + OpenLayers.Element.removeClass(this.map.viewPortDiv, this.displayClass + "Over"); + }, + activate: function() { + this.handlers.feature.map = this.map; + this.handlers.feature.activate(); + return OpenLayers.Control.ModifyFeature.prototype.activate.apply(this, arguments); + + }, + deactivate: function() { + return OpenLayers.Control.ModifyFeature.prototype.deactivate.apply(this, arguments); + }, + selectFeature: function(feature) { + this.outVertex(); + if (feature.geometry.CLASS_NAME === "OpenLayers.Geometry.Point") { + if (this.editingFeature && feature.geometry.parent) { + if (feature.geometry.parent) { + var n = feature.geometry.parent.components.length; + feature.geometry.parent.removeComponent(feature.geometry); + if (feature.geometry.parent.components.length == n) { + // Delete the feature -- we are at min vertices + this.layer.removeFeatures([this.editingFeature], {silent: true}); + this.layer.removeFeatures(this.vertices, {silent: true}); + this.editingFeature = null; + this.layer.events.triggerEvent("featuremodified"); + } else { + // Remove a vertex. + this.layer.events.triggerEvent( + "featuremodified", {feature: this.editingFeature}); + this.selectControl.select(this.editingFeature); + } + } + } else { + // We're just a single point. Delete it! + this.layer.removeFeatures([feature], {silent: true}); + this.layer.events.triggerEvent("featuremodified"); + } + } else { + // We must be selecting a non-point for the first time. Show + // vertices and set this as the figure to be edited. + this.editingFeature = feature; + this.feature = feature; + this.resetVertices(); + } + }, + collectVertices: function() { + this.vertices = []; + this.virtualVertices = []; + var control = this; + function collectComponentVertices(geometry) { + var i, vertex, component; + if(geometry.CLASS_NAME == "OpenLayers.Geometry.Point") { + vertex = new OpenLayers.Feature.Vector(geometry); + vertex._sketch = true; + control.vertices.push(vertex); + } else { + var numVert = geometry.components.length; + if(geometry.CLASS_NAME == "OpenLayers.Geometry.LinearRing") { + numVert -= 1; + } + for(i=0; i +{{ media }} + + +{% endblock %} + + {% block object-tools-items %}
  • {% trans "History" %} diff --git a/eamena/eamena/templates/admin/olwidget_change_list.html b/eamena/eamena/templates/admin/olwidget_change_list.html new file mode 100644 index 0000000000..126b2990d9 --- /dev/null +++ b/eamena/eamena/templates/admin/olwidget_change_list.html @@ -0,0 +1,9 @@ +{% extends "admin/change_list.html" %} + +{% block content %} + {% if not is_popup %} + {{ map }} + {% endif %} + + {{ block.super }} +{% endblock %} diff --git a/eamena/eamena/templates/map.htm b/eamena/eamena/templates/map.htm index f6cdf75647..4576e7056e 100644 --- a/eamena/eamena/templates/map.htm +++ b/eamena/eamena/templates/map.htm @@ -316,9 +316,9 @@

    Boogah!

    diff --git a/eamena/eamena/templates/search.htm b/eamena/eamena/templates/search.htm index 613d6a5d2f..5f7e824b91 100755 --- a/eamena/eamena/templates/search.htm +++ b/eamena/eamena/templates/search.htm @@ -115,8 +115,8 @@