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