Initial commit
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
||||
var Backbone = require('backbone');
|
||||
var XYZModel = require('./xyz/xyz-model');
|
||||
var mosaicThumbnail = require('builder/components/mosaic/mosaic-thumbnail.tpl');
|
||||
var MapboxModel = require('./mapbox/mapbox-model');
|
||||
var WMSModel = require('./wms/wms-model');
|
||||
var TileJSONModel = require('./tilejson/tilejson-model');
|
||||
var NASAModel = require('./nasa/nasa-model');
|
||||
|
||||
/**
|
||||
* Add basemap model
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
tabs: undefined,
|
||||
contentPane: 'tabs', // [tabs, loading, error]
|
||||
currentTab: 'xyz' // [xyz, wms, nasa, mapbox, tilejson, nasa]
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.layerDefinitionsCollection) throw new Error('layerDefinitionsCollection is required');
|
||||
if (!opts.basemapsCollection) throw new Error('basemapsCollection is required');
|
||||
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
|
||||
|
||||
this._layerDefinitionsCollection = opts.layerDefinitionsCollection;
|
||||
this._basemapsCollection = opts.basemapsCollection;
|
||||
this._customBaselayersCollection = opts.customBaselayersCollection;
|
||||
this._currentTab = opts.currentTab;
|
||||
|
||||
this._initTabs();
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
activeTabModel: function () {
|
||||
return this.get('tabs').findWhere({ name: this.get('currentTab') });
|
||||
},
|
||||
|
||||
canSaveBasemap: function () {
|
||||
return this.get('contentPane') === 'tabs' && this._layerToSave();
|
||||
},
|
||||
|
||||
saveBasemap: function () {
|
||||
var self = this;
|
||||
|
||||
this.set('contentPane', 'addingBasemap');
|
||||
|
||||
var customBaselayerModel = this._layerToSave();
|
||||
var attrs = customBaselayerModel.getAttributes();
|
||||
|
||||
if (this.activeTabModel().hasAlreadyAddedLayer(this._customBaselayersCollection)) {
|
||||
// update selected in basemaps collection
|
||||
this._basemapsCollection.updateSelected(attrs.className);
|
||||
// update category if needed in basemaps collection,
|
||||
// this is the case for mapbox basemaps which were stored without category in the editor
|
||||
this._basemapsCollection.updateCategory(attrs.className, attrs.category);
|
||||
|
||||
// update baselayer
|
||||
this._onBasemapSaved(attrs);
|
||||
} else {
|
||||
// Add to customBaselayersCollection before saving, so save URL resolves to the expected endpoint
|
||||
this._customBaselayersCollection.add(customBaselayerModel);
|
||||
|
||||
customBaselayerModel.save({}, {
|
||||
success: function (mdl, mdlAttrs) {
|
||||
var options = mdlAttrs.options;
|
||||
|
||||
var name = options.name ? options.name : 'Custom basemap ' + mdlAttrs.order;
|
||||
var className = options.className;
|
||||
var urlTemplate = options.urlTemplate;
|
||||
|
||||
// add in basemaps collection
|
||||
self._basemapsCollection.add({
|
||||
id: mdlAttrs.id,
|
||||
urlTemplate: urlTemplate,
|
||||
maxZoom: options.minZoom || 21,
|
||||
minZoom: options.minZoom || 0,
|
||||
name: name,
|
||||
className: className,
|
||||
attribution: options.attribution,
|
||||
category: options.category,
|
||||
tms: options.tms,
|
||||
type: options.type,
|
||||
val: className,
|
||||
label: name,
|
||||
template: function (imgURL) {
|
||||
return mosaicThumbnail({
|
||||
imgURL: imgURL
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// update baselayer
|
||||
self._onBasemapSaved(attrs);
|
||||
},
|
||||
error: function () {
|
||||
// Cleanup, remove layer it could not be saved!
|
||||
self._customBaselayersCollection.remove(customBaselayerModel);
|
||||
self.set('contentPane', 'addBasemapFailed');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_onBasemapSaved: function (layerAttrs) {
|
||||
// Update baseLayer
|
||||
this._layerDefinitionsCollection.setBaseLayer(layerAttrs);
|
||||
|
||||
this.trigger('saveBasemapDone');
|
||||
},
|
||||
|
||||
_initTabs: function () {
|
||||
var tabs = new Backbone.Collection([
|
||||
new XYZModel(),
|
||||
new MapboxModel(),
|
||||
new WMSModel(null, {
|
||||
customBaselayersCollection: this._customBaselayersCollection
|
||||
}),
|
||||
new TileJSONModel(),
|
||||
new NASAModel()
|
||||
]);
|
||||
this.set({
|
||||
tabs: tabs,
|
||||
currentTab: this._currentTab || tabs.first().get('name')
|
||||
});
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.get('tabs').each(function (tabModel) {
|
||||
tabModel.bind('saveBasemap', this.saveBasemap, this);
|
||||
}, this);
|
||||
},
|
||||
|
||||
_layerToSave: function () {
|
||||
return this.activeTabModel().get('layer');
|
||||
}
|
||||
|
||||
});
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./add-basemap.tpl');
|
||||
var TabPaneView = require('builder/components/tab-pane/tab-pane-view');
|
||||
var TabPaneCollection = require('builder/components/tab-pane/tab-pane-collection');
|
||||
var ViewFactory = require('builder/components/view-factory');
|
||||
var renderLoading = require('builder/components/loading/render-loading');
|
||||
var ErrorView = require('builder/components/error/error-view');
|
||||
var _ = require('underscore');
|
||||
var TabsView = require('./tabs-view');
|
||||
|
||||
/**
|
||||
* Add basemap dialog
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'Dialog-content Dialog-content--expanded',
|
||||
|
||||
events: {
|
||||
'click .js-ok': 'ok'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.modalModel) throw new TypeError('modalModel is required');
|
||||
if (!opts.createModel) throw new TypeError('createModel is required');
|
||||
|
||||
this._modalModel = opts.modalModel;
|
||||
this._createModel = opts.createModel;
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(template());
|
||||
this._initViews();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this._createModel.bind('saveBasemapDone', this._modalModel.destroy.bind(this._modalModel), this);
|
||||
this._createModel.bind('change:contentPane', this._onChangeContentView, this);
|
||||
this.add_related_model(this._createModel);
|
||||
},
|
||||
|
||||
_onChangeContentView: function () {
|
||||
var context = this._createModel.get('contentPane');
|
||||
var paneModel = _.first(this._tabPaneCollection.where({ name: context }));
|
||||
paneModel.set('selected', true);
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var self = this;
|
||||
|
||||
this._submitButtom = this.$('.js-ok');
|
||||
this._modalFooter = this.$('.js-Modal-footer');
|
||||
|
||||
this._tabPaneCollection = new TabPaneCollection([
|
||||
{
|
||||
name: 'tabs',
|
||||
selected: this._createModel.get('contentPane') === 'tabs',
|
||||
createContentView: function () {
|
||||
return new TabsView({
|
||||
model: self._createModel,
|
||||
submitButton: self._submitButtom,
|
||||
modalFooter: self._modalFooter
|
||||
});
|
||||
}
|
||||
}, {
|
||||
name: 'addingBasemap',
|
||||
selected: this._createModel.get('contentPane') === 'addingBasemap',
|
||||
createContentView: function () {
|
||||
self._disableModalFooter(true);
|
||||
|
||||
return ViewFactory.createByHTML(
|
||||
renderLoading({
|
||||
title: _t('components.modals.add-basemap.adding-basemap')
|
||||
})
|
||||
);
|
||||
}
|
||||
}, {
|
||||
name: 'addBasemapFailed',
|
||||
selected: this._createModel.get('contentPane') === 'addBasemapFailed',
|
||||
createContentView: function () {
|
||||
return new ErrorView({
|
||||
title: _t('components.modals.add-basemap.add-basemap-error')
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
var tabPaneView = new TabPaneView({
|
||||
collection: this._tabPaneCollection
|
||||
});
|
||||
this.addView(tabPaneView);
|
||||
this.$('.js-content-container').append(tabPaneView.render().el);
|
||||
},
|
||||
|
||||
_disableModalFooter: function (disable) {
|
||||
this._modalFooter.toggleClass('is-disabled', disable);
|
||||
},
|
||||
|
||||
ok: function () {
|
||||
if (this._createModel.canSaveBasemap()) {
|
||||
this._createModel.saveBasemap();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<div class="Modal">
|
||||
<div class="Modal-header">
|
||||
<div class="Modal-headerContainer">
|
||||
<h2 class="CDB-Text CDB-Size-huge is-light u-mainTextColor u-bSpace"><%- _t('components.modals.add-basemap.modal-title') %></h2>
|
||||
<h3 class="CDB-Text CDB-Size-medium u-altTextColor"><%- _t('components.modals.add-basemap.modal-desc') %></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Modal-container">
|
||||
<div class="Tab-pane js-content-container">
|
||||
</div>
|
||||
</div>
|
||||
<div class="Modal-footer js-Modal-footer">
|
||||
<div class="Modal-footerContainer u-flex u-justifyEnd">
|
||||
<button class="CDB-Button CDB-Button--primary is-disabled js-ok ok">
|
||||
<span class="CDB-Button-Text CDB-Text is-semibold CDB-Size-medium u-upperCase"><%- _t('components.modals.add-basemap.add-btn') %></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<div class="u-flex u-alignCenter Modal-basemapContainer">
|
||||
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.mapbox.insert') %></h3>
|
||||
<div class="CDB-Text u-flex u-alignCenter ">
|
||||
<label class="Metadata-label Metadata-label--big CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"><%- _t('components.modals.add-basemap.mapbox.enter') %></label>
|
||||
<div class="Form-rowData Form-rowData--longer">
|
||||
<input type="text" class="CDB-InputText CDB-Text js-url" value="<%- url %>" placeholder="<%- _t('components.modals.add-basemap.xyz.eg') %> https://api.mapbox.com/styles/v1/username/basemap/tiles/256/{z}/{x}/{y}@2x?access_token=access_token">
|
||||
<div class="XYZPanel-error CDB-InfoTooltip CDB-InfoTooltip--left is-error CDB-Text CDB-Size-medium CDB-InfoTooltip-text js-error <%- lastErrorMsg ? 'is-visible' : '' %>"><%- lastErrorMsg %></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex">
|
||||
<label class="Metadata-label Metadata-label--big CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"></label>
|
||||
<div class="Form-rowData Form-rowData--noMinHeight Form-rowData--longer">
|
||||
<p class="CDB-Text CDB-Size-small Form-rowInfoText--block u-altTextColor">
|
||||
Learn how to get your Mapbox Style URL <a href="https://www.mapbox.com/help/carto/" target="_blank">here</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var MapboxView = require('./mapbox-view');
|
||||
var MapboxToTileLayerFactory = require('./mapbox-to-tile-layer-factory');
|
||||
|
||||
/**
|
||||
* View model for Mapbox tab content.
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
name: 'mapbox',
|
||||
label: 'Mapbox',
|
||||
currentView: 'enterURL', //, validatingInputs, valid
|
||||
lastErrorMsg: '', // set if fails to save
|
||||
layer: undefined // will be set when valid
|
||||
},
|
||||
|
||||
createView: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
if (!opts.modalFooter) throw new Error('modalFooter is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
this._modalFooter = opts.modalFooter;
|
||||
|
||||
this.set({
|
||||
currentView: 'enterURL',
|
||||
layer: undefined
|
||||
});
|
||||
|
||||
return new MapboxView({
|
||||
model: this,
|
||||
submitButton: this._submitButton,
|
||||
modalFooter: this._modalFooter
|
||||
});
|
||||
},
|
||||
|
||||
hasAlreadyAddedLayer: function (userLayers) {
|
||||
var urlTemplate = this.get('layer').get('urlTemplate');
|
||||
return _.any(userLayers.isCustomCategory(), function (customLayer) {
|
||||
return customLayer.get('urlTemplate') === urlTemplate;
|
||||
});
|
||||
},
|
||||
|
||||
validateInputs: function (url) {
|
||||
this.set({
|
||||
currentView: 'validatingInputs',
|
||||
url: url
|
||||
});
|
||||
|
||||
var self = this;
|
||||
|
||||
var mf = new MapboxToTileLayerFactory({
|
||||
url: url
|
||||
});
|
||||
mf.createTileLayer({
|
||||
success: function (tileLayer) {
|
||||
self.set('layer', tileLayer);
|
||||
self.trigger('saveBasemap');
|
||||
},
|
||||
error: function (errorMsg) {
|
||||
self.set({
|
||||
currentView: 'enterURL',
|
||||
lastErrorMsg: errorMsg
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
/* global Image, location */
|
||||
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
|
||||
|
||||
/**
|
||||
* Factory to create a CustomBaselayerModel from a given Integration URL for Mapbox.
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
url: ''
|
||||
},
|
||||
|
||||
_MAPBOX: {
|
||||
version: 4,
|
||||
https: 'https://dnv9my2eseobd.cloudfront.net',
|
||||
base: 'https://a.tiles.mapbox.com/'
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} callbacks
|
||||
* success {Function} given a new TileLayer object
|
||||
* error {Function} given an error explanation
|
||||
*/
|
||||
createTileLayer: function (callbacks) {
|
||||
var val = this.get('url');
|
||||
var url = this._lowerXYZ(val);
|
||||
var type = 'json';
|
||||
var subdomains = ['a', 'b', 'c'];
|
||||
var mapbox_id;
|
||||
|
||||
// Detects the URL's type
|
||||
if (url.indexOf('{x}') < 0 && url.indexOf('tiles.mapbox.com') !== -1) {
|
||||
mapbox_id = this._getMapBoxMapID(url);
|
||||
if (mapbox_id) {
|
||||
type = 'mapbox_id';
|
||||
url = mapbox_id;
|
||||
}
|
||||
} else if (url.indexOf('{x}') !== -1) {
|
||||
type = 'xyz';
|
||||
url = url.replace(/\{s\}/g, function () {
|
||||
return subdomains[Math.floor(Math.random() * 3)];
|
||||
})
|
||||
.replace(/\{x\}/g, '0')
|
||||
.replace(/\{y\}/g, '0')
|
||||
.replace(/\{z\}/g, '0');
|
||||
} else if (url && url.indexOf('http') < 0 && url.match(/(.*?)\.(.*)/) != null && url.match(/(.*?)\.(.*)/).length === 3) {
|
||||
type = 'mapbox_id';
|
||||
mapbox_id = val;
|
||||
} else { // If not, check https
|
||||
url = this._fixHTTPS(url);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var image;
|
||||
if (type === 'mapbox') {
|
||||
callbacks.success(this._newTileLayer({ tiles: [url] }));
|
||||
} else if (type === 'xyz') {
|
||||
image = new Image();
|
||||
image.onload = function () {
|
||||
callbacks.success(self._newTileLayer({
|
||||
tiles: [self._lowerXYZ(val)]
|
||||
}));
|
||||
};
|
||||
image.onerror = function () {
|
||||
callbacks.error(self._errorToMsg());
|
||||
};
|
||||
image.src = url;
|
||||
} else if (type === 'mapbox_id') {
|
||||
var base_url = this._MAPBOX.base + 'v' + this._MAPBOX.version + '/' + mapbox_id;
|
||||
var tile_url = base_url + '/{z}/{x}/{y}.png';
|
||||
var json_url = base_url + '.json';
|
||||
|
||||
// JQuery has a faulty implementation of the getJSON method and doesn't return
|
||||
// a 404, so we use a timeout. TODO: replace with CORS
|
||||
var errorTimeout = setTimeout(function () {
|
||||
callbacks.error(self._errorToMsg());
|
||||
}, 5000);
|
||||
|
||||
$.ajax({
|
||||
url: json_url,
|
||||
success: function (data) {
|
||||
clearTimeout(errorTimeout);
|
||||
callbacks.success(self._newTileLayer({
|
||||
tiles: [tile_url],
|
||||
attribution: data.attribution,
|
||||
minzoom: data.minzoom,
|
||||
maxzoom: data.maxzoom,
|
||||
name: data.name
|
||||
}));
|
||||
},
|
||||
error: function (e) {
|
||||
clearTimeout(errorTimeout);
|
||||
callbacks.error(self._errorToMsg(e));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callbacks.error(this._errorToMsg());
|
||||
}
|
||||
},
|
||||
|
||||
_newTileLayer: function (data) {
|
||||
// Check if the respond is an array
|
||||
// In that case, get only the first
|
||||
if (_.isArray(data) && _.size(data) > 0) {
|
||||
data = _.first(data);
|
||||
}
|
||||
|
||||
var url = data.tiles[0];
|
||||
var attribution = data.attribution || null;
|
||||
|
||||
var layer = new CustomBaselayerModel({
|
||||
urlTemplate: url,
|
||||
attribution: attribution,
|
||||
maxZoom: data.maxzoom || 21,
|
||||
minZoom: data.minzoom || 0,
|
||||
name: data.name || '',
|
||||
category: 'Mapbox',
|
||||
type: 'Tiled'
|
||||
});
|
||||
layer.set('className', layer._generateClassName(url));
|
||||
|
||||
return layer;
|
||||
},
|
||||
|
||||
_errorToMsg: function (error) {
|
||||
if (typeof error === 'object' || !error) {
|
||||
if (error && error.status && error.status === 401) {
|
||||
return _t('components.modals.add-basemap.mapbox.error');
|
||||
} else {
|
||||
return _t('components.modals.add-basemap.mapbox.invalid');
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
},
|
||||
|
||||
_lowerXYZ: function (url) {
|
||||
return url.replace(/\{S\}/g, '{s}')
|
||||
.replace(/\{X\}/g, '{x}')
|
||||
.replace(/\{Y\}/g, '{y}')
|
||||
.replace(/\{Z\}/g, '{z}');
|
||||
},
|
||||
|
||||
// Extracts the Mapbox MapId from a Mapbox URL
|
||||
_getMapBoxMapID: function (url) {
|
||||
// http://d.tiles.mapbox.com/v3/{user}.{map}/3/4/3.png
|
||||
// http://a.tiles.mapbox.com/v3/{user}.{map}/page.html
|
||||
// http://a.tiles.mapbox.com/v4/{user}.{map}.*
|
||||
var reg1 = /https?:\/\/[a-z]?\.?tiles\.mapbox.com\/v(\d)\/([^\/.]*)\.([^\/.]*)/;
|
||||
|
||||
// https://tiles.mapbox.com/{user}/edit/{map}?newmap&preset=Streets#3/0.00/-0.09
|
||||
var reg2 = /https?:\/\/tiles\.mapbox\.com\/(.*?)\/edit\/(.*?)(\?|#)/;
|
||||
|
||||
var match = '';
|
||||
|
||||
// Check first expresion
|
||||
match = url.match(reg1);
|
||||
|
||||
if (match && match[1] && match[2]) {
|
||||
return match[2] + '.' + match[3];
|
||||
}
|
||||
|
||||
// Check second expresion
|
||||
match = url.match(reg2);
|
||||
|
||||
if (match && match[1] && match[2]) {
|
||||
return match[1] + '.' + match[2];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* return a https url if the current application is loaded from https
|
||||
*/
|
||||
_fixHTTPS: function (url, loc) {
|
||||
loc = loc || location;
|
||||
|
||||
// fix the url to https or http
|
||||
if (url.indexOf('https') !== 0 && loc.protocol === 'https:') {
|
||||
// search for mapping
|
||||
var i = url.indexOf('mapbox.com');
|
||||
if (i !== -1) {
|
||||
return this._MAPBOX.https + url.substr(i + 'mapbox.com'.length);
|
||||
}
|
||||
return url.replace(/http/, 'https');
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var ViewFactory = require('builder/components/view-factory');
|
||||
var renderLoading = require('builder/components/loading/render-loading');
|
||||
var enterUrl = require('./enter-url.tpl');
|
||||
|
||||
/**
|
||||
* Represents the Mapbox tab content.
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'keydown': '_onKeyDown',
|
||||
'keyup': '_onKeyUp'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
if (!opts.modalFooter) throw new Error('modalFooter is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
this._modalFooter = opts.modalFooter;
|
||||
this._onClickBinded = this._onClickOK.bind(this);
|
||||
|
||||
this._bindSubmitButton();
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
var view;
|
||||
|
||||
switch (this.model.get('currentView')) {
|
||||
case 'validatingInputs':
|
||||
this._disableModalFooter(true);
|
||||
|
||||
view = ViewFactory.createByHTML(
|
||||
renderLoading({
|
||||
title: _t('components.modals.add-basemap.validating')
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'enterURL':
|
||||
default:
|
||||
this._disableModalFooter(false);
|
||||
|
||||
view = ViewFactory.createByHTML(
|
||||
enterUrl({
|
||||
url: this.model.get('url'),
|
||||
lastErrorMsg: this.model.get('lastErrorMsg')
|
||||
})
|
||||
);
|
||||
}
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
|
||||
this._updateOkBtn();
|
||||
this._onKeyUp();
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change', this.render, this);
|
||||
},
|
||||
|
||||
_hasValues: function () {
|
||||
return this._urlVal();
|
||||
},
|
||||
|
||||
_urlVal: function () {
|
||||
return this.$('.js-url').val();
|
||||
},
|
||||
|
||||
_onClickOK: function (e) {
|
||||
this.killEvent(e);
|
||||
|
||||
if (this._hasValues()) {
|
||||
var url = this._urlVal();
|
||||
|
||||
this.model.validateInputs(url);
|
||||
}
|
||||
},
|
||||
|
||||
_disableModalFooter: function (disable) {
|
||||
this._modalFooter.toggleClass('is-disabled', disable);
|
||||
},
|
||||
|
||||
_updateOkBtn: function () {
|
||||
this._submitButton.find('span').text(_t('components.modals.add-basemap.add-btn'));
|
||||
},
|
||||
|
||||
_onKeyDown: function (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
this.$('.js-error').removeClass('is-visible');
|
||||
},
|
||||
|
||||
_onKeyUp: function (e) {
|
||||
e && e.stopPropagation();
|
||||
this._submitButton.toggleClass('is-disabled', !this._hasValues());
|
||||
},
|
||||
|
||||
_bindSubmitButton: function () {
|
||||
this._submitButton.on('click', this._onClickBinded);
|
||||
},
|
||||
|
||||
_unBindSubmitButton: function () {
|
||||
this._submitButton.off('click', this._onClickBinded);
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._unBindSubmitButton();
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var moment = require('moment');
|
||||
var NASAView = require('./nasa-view');
|
||||
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
|
||||
|
||||
var TYPES = {
|
||||
day: {
|
||||
url: 'http://map1.vis.earthdata.nasa.gov/wmts-webmerc/MODIS_Terra_CorrectedReflectance_TrueColor/default/<%- date %>/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpeg',
|
||||
limit: '2012-05-01',
|
||||
default: '2012-05-01',
|
||||
attribution: '<a href="http://earthdata.nasa.gov/gibs" target="_blank">NASA EOSDIS GIBS</a>',
|
||||
name: 'NASA Terra',
|
||||
maxZoom: 9,
|
||||
minZoom: 1
|
||||
},
|
||||
night: {
|
||||
url: 'http://map1.vis.earthdata.nasa.gov/wmts-webmerc/VIIRS_CityLights_2012/default/<%- date %>/GoogleMapsCompatible_Level8/{z}/{y}/{x}.jpeg',
|
||||
limit: '2012-05-01',
|
||||
default: '2012-05-02',
|
||||
attribution: '<a href="http://earthdata.nasa.gov/gibs" target="_blank">NASA EOSDIS GIBS</a>',
|
||||
name: 'NASA Earth at night',
|
||||
maxZoom: 8,
|
||||
minZoom: 1
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* View model for NASA tab content.
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
name: 'nasa',
|
||||
label: 'NASA',
|
||||
layer: undefined, // gets set on dayOrNight/date changes
|
||||
layerType: 'day',
|
||||
date: undefined, // for date picker
|
||||
current: undefined,
|
||||
format: 'Y-m-d' // YYYY-MM-DD
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
createView: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
|
||||
var utc = new Date().getTimezoneOffset();
|
||||
var today = moment(new Date()).utcOffset(utc).format('YYYY-MM-DD');
|
||||
var yesterday = moment(new Date()).utcOffset(utc).subtract(1, 'days').format('YYYY-MM-DD');
|
||||
|
||||
this.set({
|
||||
current: today,
|
||||
date: yesterday
|
||||
});
|
||||
|
||||
return new NASAView({
|
||||
model: this,
|
||||
submitButton: this._submitButton
|
||||
});
|
||||
},
|
||||
|
||||
hasAlreadyAddedLayer: function (userLayers) {
|
||||
var urlTemplate = this.get('layer').get('urlTemplate');
|
||||
return _.any(userLayers.isCustomCategory(), function (customLayer) {
|
||||
return customLayer.get('urlTemplate') === urlTemplate;
|
||||
});
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.bind('change:date change:layerType', this._onChange, this);
|
||||
},
|
||||
|
||||
_onChange: function () {
|
||||
var dateStr = this.get('date');
|
||||
var layerType = this.get('layerType');
|
||||
|
||||
var url = _.template(TYPES[layerType].url)({
|
||||
date: dateStr
|
||||
});
|
||||
|
||||
var name = TYPES[layerType].name;
|
||||
|
||||
if (layerType === 'day') {
|
||||
name = name + ' ' + dateStr;
|
||||
}
|
||||
|
||||
var layer = new CustomBaselayerModel({
|
||||
urlTemplate: url,
|
||||
attribution: TYPES[layerType].attribution,
|
||||
maxZoom: TYPES[layerType].maxZoom,
|
||||
minZoom: TYPES[layerType].minZoom,
|
||||
name: name,
|
||||
category: 'NASA',
|
||||
type: 'Tiled'
|
||||
});
|
||||
layer.set('className', layer._generateClassName(url));
|
||||
|
||||
this.set('layer', layer);
|
||||
}
|
||||
|
||||
});
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
var $ = require('jquery');
|
||||
var moment = require('moment');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var DatePickerView = require('builder/components/date-picker/date-picker-view');
|
||||
var EditFieldModel = require('builder/components/date-picker/edit-field-model');
|
||||
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
|
||||
var template = require('./nasa.tpl');
|
||||
|
||||
/**
|
||||
* Represents the NASA tab content
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
options: {
|
||||
dateFormat: 'YYYY-MM-DD'
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .js-day': '_onChangeToDay',
|
||||
'click .js-night': '_onChangeToNight'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
this.dateModel = new EditFieldModel({
|
||||
value: this.model.get('date'),
|
||||
type: 'date'
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this._updateOkBtn();
|
||||
this._disableOkBtn(false);
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
layerType: this.model.get('layerType')
|
||||
})
|
||||
);
|
||||
|
||||
this._renderDatePicker();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:layerType', function () {
|
||||
this.dateModel.set('readOnly', this.model.get('layerType') === 'night');
|
||||
this.render();
|
||||
}, this);
|
||||
this.dateModel.bind('change:value', function () {
|
||||
var date = moment(this.dateModel.get('value')).format(this.options.dateFormat);
|
||||
this.model.set('date', date);
|
||||
}, this);
|
||||
this.add_related_model(this.dateModel);
|
||||
},
|
||||
|
||||
_renderDatePicker: function () {
|
||||
// Date picker
|
||||
var datepicker = this.datepicker = new DatePickerView({
|
||||
className: 'DatePicker DatePicker--withBorder',
|
||||
model: this.dateModel
|
||||
});
|
||||
this._$datePicker().html(datepicker.render().el);
|
||||
this.addView(datepicker);
|
||||
|
||||
// Disabled tooltip
|
||||
if (this.dateModel.get('readOnly')) {
|
||||
var tooltip = new TipsyTooltipView({
|
||||
el: this._$datePicker(),
|
||||
title: function (e) {
|
||||
return $(this).attr('data-title');
|
||||
}
|
||||
});
|
||||
this.addView(tooltip);
|
||||
}
|
||||
},
|
||||
|
||||
_onChangeToNight: function () {
|
||||
this.model.set('layerType', 'night');
|
||||
},
|
||||
|
||||
_onChangeToDay: function () {
|
||||
this.model.set('layerType', 'day');
|
||||
},
|
||||
|
||||
_$datePicker: function () {
|
||||
return this.$('.js-datePicker');
|
||||
},
|
||||
|
||||
_updateOkBtn: function () {
|
||||
this._submitButton.find('span').text(_t('components.modals.add-basemap.add-btn'));
|
||||
},
|
||||
|
||||
_disableOkBtn: function (disable) {
|
||||
this._submitButton.toggleClass('is-disabled', disable);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
<div class="u-flex u-alignCenter Modal-basemapContainer">
|
||||
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.nasa.select') %></h3>
|
||||
<div class="CDB-Text u-flex u-alignCenter">
|
||||
<div class="Form-rowData Form-rowData--short Form-rowData--alignLeft">
|
||||
<div class="Form-rowData Form-rowData--full">
|
||||
<div class="RadioButton js-day">
|
||||
<button type="button" class="RadioButton-input <% if (layerType === 'day') { %>is-checked<% } %>"></button>
|
||||
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis" for="nasa-type-day"><%- _t('components.modals.add-basemap.nasa.day') %></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Form-rowData Form-rowData--full">
|
||||
<div class="RadioButton js-night">
|
||||
<button type="button" class="RadioButton-input <% if (layerType === 'night') { %>is-checked<% } %>"/></button>
|
||||
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis" for="nasa-type-night"><%- _t('components.modals.add-basemap.nasa.night') %></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="Form-rowData Form-rowData--short">
|
||||
<div class="js-datePicker" data-title="<%- _t('components.modals.add-basemap.nasa.cant-select') %>"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,81 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var ScrollView = require('builder/components/scroll/scroll-view');
|
||||
var $ = require('jquery');
|
||||
var template = require('./tabs.tpl');
|
||||
|
||||
/**
|
||||
* View representing the tabs content of the dialog.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click .js-tabs button': '_onClickTab'
|
||||
},
|
||||
|
||||
attributes: function () {
|
||||
return {
|
||||
class: 'Modal-outer'
|
||||
};
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
if (!opts.modalFooter) throw new Error('modalFooter is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
this._modalFooter = opts.modalFooter;
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.html(template({
|
||||
model: this.model
|
||||
}));
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var scrollView = new ScrollView({
|
||||
className: 'ScrollView ScrollView--withoutMargins',
|
||||
createContentView: function () {
|
||||
return this._createTabContentView();
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
this.addView(scrollView);
|
||||
this.$('.js-tab-content').append(scrollView.render().el);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:currentTab', this.render, this);
|
||||
},
|
||||
|
||||
_createTabContentView: function () {
|
||||
if (this._currentTabView) {
|
||||
this._currentTabView.clean();
|
||||
}
|
||||
this._currentTabView = this.model.activeTabModel().createView({
|
||||
submitButton: this._submitButton,
|
||||
modalFooter: this._modalFooter
|
||||
});
|
||||
this.addView(this._currentTabView);
|
||||
return this._currentTabView.render();
|
||||
},
|
||||
|
||||
_onClickTab: function (e) {
|
||||
this.killEvent(e);
|
||||
var name = $(e.target).closest('button').data('name');
|
||||
if (name) {
|
||||
this.model.set('currentTab', name);
|
||||
} else {
|
||||
throw new Error('tab name was expected but was empty');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="Modal-navigation">
|
||||
<ul class="Modal-navigationInner CDB-Text is-semibold CDB-Size-medium js-tabs">
|
||||
<% model.get('tabs').each(function(tab) { %>
|
||||
<li class="CDB-NavMenu-item <%- model.get('currentTab') === tab.get('name') ? 'is-selected' : '' %>">
|
||||
<button data-name="<%- tab.get('name') %>" class="CDB-NavMenu-link u-upperCase">
|
||||
<%- tab.get('label') %>
|
||||
</button>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="Modal-inner Modal-inner--with-navigation js-tab-content"></div>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<div class="u-flex u-alignCenter Modal-basemapContainer">
|
||||
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.tilejson.insert') %></h3>
|
||||
<div class="CDB-Text u-flex u-alignCenter">
|
||||
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"><%- _t('components.modals.add-basemap.xyz.enter') %></label>
|
||||
<div class="Form-rowData Form-rowData--longer">
|
||||
<input type="text" class="has-icon CDB-InputText CDB-Text js-url" value="" placeholder="<%- _t('components.modals.add-basemap.xyz.eg') %> http://domain.com/tiles.json?foo=bar">
|
||||
<i class="CDB-IconFont CDB-IconFont-dribbble Form-inputIcon js-idle"></i>
|
||||
<i class="Spinner XYZPanel-inputIcon Spinner--formIcon Form-inputIcon js-validating" style="display: none;"></i>
|
||||
<div class="XYZPanel-error CDB-InfoTooltip CDB-InfoTooltip--left is-error CDB-Text CDB-Size-medium CDB-InfoTooltip-text js-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
var Backbone = require('backbone');
|
||||
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
|
||||
|
||||
/**
|
||||
* Model to representing a TileJSON endpoint
|
||||
* See https://github.com/mapbox/tilejson-spec/tree/master/2.1.0 for details
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
url: function () {
|
||||
return this.get('tilejson_url');
|
||||
},
|
||||
|
||||
newTileLayer: function () {
|
||||
if (!this._isFetched()) throw new Error('no tiles, have fetch been called and returned a successful resultset?');
|
||||
|
||||
var url = this._urlTemplate();
|
||||
|
||||
var layer = new CustomBaselayerModel({
|
||||
urlTemplate: url,
|
||||
attribution: this.get('attribution'),
|
||||
maxZoom: this.get('maxzoom'),
|
||||
minZoom: this.get('minzoom'),
|
||||
name: this._name(),
|
||||
bounding_boxes: this.get('bounds'),
|
||||
tms: this.get('scheme') === 'tms',
|
||||
category: 'TileJSON',
|
||||
type: 'Tiled'
|
||||
});
|
||||
layer.set('className', layer._generateClassName(url));
|
||||
|
||||
return layer;
|
||||
},
|
||||
|
||||
setUrl: function (url) {
|
||||
this.set('tilejson_url', url);
|
||||
},
|
||||
|
||||
_isFetched: function () {
|
||||
return this.get('tiles').length > 0;
|
||||
},
|
||||
|
||||
_urlTemplate: function () {
|
||||
return this.get('tiles')[0];
|
||||
},
|
||||
|
||||
_name: function () {
|
||||
return this.get('name') || this.get('description');
|
||||
}
|
||||
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var TileJSONView = require('./tilejson-view');
|
||||
var TileJSONLayerModel = require('./tilejson-layer-model');
|
||||
|
||||
/**
|
||||
* View model for TileJSON tab content.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
name: 'tilejson',
|
||||
label: 'TileJSON',
|
||||
layer: undefined // will be set when valid
|
||||
},
|
||||
|
||||
createView: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
|
||||
var tileJSONLayerModel = new TileJSONLayerModel();
|
||||
|
||||
return new TileJSONView({
|
||||
model: this,
|
||||
submitButton: this._submitButton,
|
||||
tileJSONLayerModel: tileJSONLayerModel
|
||||
});
|
||||
},
|
||||
|
||||
hasAlreadyAddedLayer: function (userLayers) {
|
||||
var urlTemplate = this.get('layer').get('urlTemplate');
|
||||
return _.any(userLayers.isCustomCategory(), function (customLayer) {
|
||||
return customLayer.get('urlTemplate') === urlTemplate;
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./enter-url.tpl');
|
||||
|
||||
/**
|
||||
* Represents the TileJSON tab content.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'keydown .js-url': '_onKeydown',
|
||||
'paste .js-url': '_onPaste'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
if (!opts.tileJSONLayerModel) throw new Error('tileJSONLayerModel is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
this._tileJSONLayerModel = opts.tileJSONLayerModel;
|
||||
this._lastURL = '';
|
||||
this._debouncedUpdate = _.debounce(this._update.bind(this), 150);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this._updateOkBtn();
|
||||
this._disableOkBtn(true);
|
||||
|
||||
this.$el.html(
|
||||
template()
|
||||
);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_onKeydown: function (e) {
|
||||
e.stopPropagation();
|
||||
this._debouncedUpdate();
|
||||
},
|
||||
|
||||
_onPaste: function (e) {
|
||||
e.stopPropagation();
|
||||
this._debouncedUpdate();
|
||||
},
|
||||
|
||||
_update: function () {
|
||||
var self = this;
|
||||
|
||||
this._disableOkBtn(true);
|
||||
this._indicateIsValidating(true);
|
||||
|
||||
var url = this._urlWithHTTP();
|
||||
|
||||
if (url === this._lastURL) {
|
||||
// Even if triggered nothing really changed so just update UI and return early
|
||||
this._indicateIsValidating(false);
|
||||
this._updateError();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastURL = url;
|
||||
|
||||
this._indicateIsValidating(true);
|
||||
|
||||
this._tileJSONLayerModel.setUrl(url);
|
||||
|
||||
this._tileJSONLayerModel.fetch({
|
||||
success: function (mdl) {
|
||||
if (url === self._lastURL) {
|
||||
self.model.set('layer', mdl.newTileLayer());
|
||||
self._disableOkBtn(false);
|
||||
self._indicateIsValidating(false);
|
||||
self._updateError();
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
if (url === self._lastURL) {
|
||||
self._indicateIsValidating(false);
|
||||
// Note that this text can not be longer, or it will exceed available space of the error label.
|
||||
self._updateError(_t('components.modals.add-basemap.tilejson.invalid'));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_updateOkBtn: function () {
|
||||
this._submitButton.find('span').text(_t('components.modals.add-basemap.add-btn'));
|
||||
},
|
||||
|
||||
_disableOkBtn: function (disable) {
|
||||
this._submitButton.toggleClass('is-disabled', disable);
|
||||
},
|
||||
|
||||
_updateError: function (msg) {
|
||||
this.$('.js-error').text(msg)[msg ? 'addClass' : 'removeClass']('is-visible');
|
||||
},
|
||||
|
||||
_indicateIsValidating: function (indicate) {
|
||||
if (indicate) {
|
||||
this.$('.js-idle').hide();
|
||||
this.$('.js-validating').show();
|
||||
} else {
|
||||
this.$('.js-validating').hide();
|
||||
this.$('.js-idle').show();
|
||||
}
|
||||
},
|
||||
|
||||
// So don't try to be fetched relatively to current URL path later
|
||||
_urlWithHTTP: function () {
|
||||
var str = this.$('.js-url').val();
|
||||
|
||||
if (str.indexOf('http://') === -1 && str.indexOf('https://') === -1) {
|
||||
return 'http://' + str;
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<div class="u-flex u-alignCenter Modal-basemapContainer">
|
||||
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.wms.insert') %></h3>
|
||||
<div class="CDB-Text u-flex u-alignCenter">
|
||||
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"><%- _t('components.modals.add-basemap.xyz.enter') %></label>
|
||||
<div class="Form-rowData Form-rowData--longer">
|
||||
<input type="text" class="has-icon CDB-InputText CDB-Text js-url" value="" placeholder="<%- _t('components.modals.add-basemap.xyz.eg') %> http://openlayers.org/en/v3.5.0/examples/data/ogcsample.xml">
|
||||
<i class="CDB-IconFont CDB-IconFont-dribbble Form-inputIcon js-idle"></i>
|
||||
<i class="Spinner XYZPanel-inputIcon XYZPanel-inputIcon--loader Spinner--formIcon Form-inputIcon js-validating" style="display: none;"></i>
|
||||
<div class="XYZPanel-error CDB-InfoTooltip CDB-InfoTooltip--left is-error CDB-Text CDB-Size-medium CDB-InfoTooltip-text js-error <%- (layersFetched && layers.length === 0) ? 'is-visible' : '' %>">
|
||||
<% if (layersFetched && layers.length === 0) { %>
|
||||
<%- _t('components.modals.add-basemap.wms.invalid') %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./select-layer.tpl');
|
||||
var WMSLayersView = require('./wms-layers-view');
|
||||
|
||||
/**
|
||||
* Sub view, to select what layer to use as basemap.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'click .js-back': '_onClickBack'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
|
||||
this._customBaselayersCollection = opts.customBaselayersCollection;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.html(
|
||||
template({
|
||||
searchQuery: this.model.get('searchQuery'),
|
||||
layersFound: this.model.getLayers(),
|
||||
layersAvailableCount: this.model.layersAvailableCount()
|
||||
})
|
||||
);
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
var wmsListView = new WMSLayersView({
|
||||
model: this.model,
|
||||
customBaselayersCollection: this._customBaselayersCollection
|
||||
});
|
||||
|
||||
this.addView(wmsListView);
|
||||
this.$('.js-layers').append(wmsListView.render().el);
|
||||
},
|
||||
|
||||
_onClickBack: function (e) {
|
||||
this.killEvent(e);
|
||||
this.model.set('currentView', 'enterURL');
|
||||
}
|
||||
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<div class="u-inner">
|
||||
<div class="Filters WMSSSelectLayer-Filter is-relative">
|
||||
<div class="Filters-inner">
|
||||
<div class="Filters-row">
|
||||
<div class="Filters-group">
|
||||
<div class="Filters-typeItem Filters-typeItem--searchEnabler">
|
||||
<button class="Filters-searchLink CDB-Text is-semibold u-upperCase CDB-Size-medium js-search-link">
|
||||
<i class="Filters-searchLinkIcon CDB-IconFont CDB-IconFont-lens"></i><%- _t('components.modals.add-layer.navigation.search') %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Filters-typeItem Filters-typeItem--searchField">
|
||||
<form class="Filters-searchForm js-search-form" action="#">
|
||||
<input class="Filters-searchInput CDB-Text CDB-Size-medium js-search-input" type="text" value="<%- searchQuery %>" placeholder="<%- _t('components.modals.add-basemap.wms.placeholder', { layersFoundCount: layersFound.length, layersFoundCountPluralize: _t('components.modals.add-basemap.wms.tables-pluralize', { smart_count: layersFound.length }), layersAvailableCount: layersAvailableCount, layersAvailableCountPluralize: _t('components.modals.add-basemap.wms.tables-pluralize', { smart_count: layersAvailableCount }) }) %>" />
|
||||
<button type="button" class="Filters-cleanSearch js-clean-search u-actionTextColor">
|
||||
<i class="CDB-IconFont CDB-IconFont-close"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<span class="Filters-separator"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (searchQuery && layersFound.length == 0) { %>
|
||||
<div class="IntermediateInfo">
|
||||
<div class="LayoutIcon">
|
||||
<i class="CDB-IconFont CDB-IconFont-defaultUser"></i>
|
||||
</div>
|
||||
<h4 class="CDB-Text CDB-Size-large u-mainTextColor u-bSpace u-secondaryTextColor u-tSpace-xl"><%- _t('components.modals.add-basemap.wms.oh-no') %></h4>
|
||||
<p class="CDB-Text CDB-Size-medium u-altTextColor"><%- _t('components.modals.add-basemap.wms.unfortunately') %></p>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="js-layers"></div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<button class="NavButton Dialog-backBtn js-back">
|
||||
<i class="CDB-IconFont CDB-IconFont-arrowPrev"></i>
|
||||
</button>
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
|
||||
|
||||
/**
|
||||
* Model for an individual WMS/WMTS layer.
|
||||
*/
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
state: 'idle' // idle, saving, saveDone, saveFail
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return this._wmsService.saveLayerURL({
|
||||
title: this.get('title'),
|
||||
name: this.get('name'),
|
||||
layer: this.get('name'),
|
||||
srs: this.get('srs'),
|
||||
bounding_boxes: this.get('llbbox'),
|
||||
type: this.get('type'), // wms/wmts
|
||||
matrix_sets: this.get('matrix_sets')
|
||||
});
|
||||
},
|
||||
|
||||
sync: function (method, model, options) {
|
||||
options = options || {};
|
||||
options.url = this.url(method.toLowerCase());
|
||||
options.dataType = 'jsonp';
|
||||
options.attrs = '_';
|
||||
method = 'READ';
|
||||
|
||||
return Backbone.sync.apply(this, arguments);
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.wmsService) throw new Error('wmsService is required');
|
||||
|
||||
this._wmsService = opts.wmsService;
|
||||
},
|
||||
|
||||
canSave: function (customBaselayersCollection) {
|
||||
return !_.any(customBaselayersCollection.isCustomCategory(), function (customLayer) {
|
||||
return customLayer.get('name') === this.get('title');
|
||||
}, this);
|
||||
},
|
||||
|
||||
createProxiedLayerOrCustomBaselayerModel: function () {
|
||||
this.set('state', 'saving');
|
||||
this._shouldBeProxied() ? this._createProxiedLayer() : this._newCustomBaselayerModel();
|
||||
},
|
||||
|
||||
_shouldBeProxied: function () {
|
||||
if (this.get('type') === 'wmts') {
|
||||
var supportedMatrixSets = this._wmsService.supportedMatrixSets(this.get('matrix_sets') || []);
|
||||
|
||||
return supportedMatrixSets.length > 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
_createProxiedLayer: function () {
|
||||
var self = this;
|
||||
|
||||
this.save({}, {
|
||||
success: function () {
|
||||
var proxiedBaselayerModel;
|
||||
|
||||
try {
|
||||
proxiedBaselayerModel = self._newProxiedBaselayerModel();
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (proxiedBaselayerModel) {
|
||||
self._setCustomBaselayerModel(proxiedBaselayerModel);
|
||||
} else {
|
||||
self.set('state', 'saveFail');
|
||||
}
|
||||
},
|
||||
error: function (e) {
|
||||
self.set('state', 'saveFail');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_newCustomBaselayerModel: function () {
|
||||
var customBaselayerModel = this._byCustomURL(this._xyzURLTemplate());
|
||||
|
||||
customBaselayerModel.set({
|
||||
name: this.get('title') || this.get('name'),
|
||||
attribution: this.get('attribution'),
|
||||
bounding_boxes: this.get('llbbox')
|
||||
});
|
||||
this._setCustomBaselayerModel(customBaselayerModel);
|
||||
|
||||
return customBaselayerModel;
|
||||
},
|
||||
|
||||
_newProxiedBaselayerModel: function () {
|
||||
if (!this.get('mapproxy_id')) {
|
||||
throw new Error('mapproxy_id must be set');
|
||||
}
|
||||
|
||||
var url = this._wmsService.getProxyTilesURL() + '/' + this.get('mapproxy_id') + '/wmts/map/webmercator/{z}/{x}/{y}.png';
|
||||
var proxiedBaselayerModel = this._generateCustomBaselayerModel(url);
|
||||
|
||||
proxiedBaselayerModel.set({
|
||||
name: this.get('title') || this.get('name'),
|
||||
attribution: this.get('attribution'),
|
||||
proxy: true,
|
||||
bounding_boxes: this.get('bounding_boxes')
|
||||
});
|
||||
|
||||
return proxiedBaselayerModel;
|
||||
},
|
||||
|
||||
_generateCustomBaselayerModel: function (url) {
|
||||
var layer = new CustomBaselayerModel({
|
||||
urlTemplate: url,
|
||||
attribution: null,
|
||||
maxZoom: 21,
|
||||
minZoom: 0,
|
||||
name: '',
|
||||
category: 'WMS',
|
||||
tms: false,
|
||||
type: 'Tiled'
|
||||
});
|
||||
layer.set('className', layer._generateClassName(url));
|
||||
|
||||
return layer;
|
||||
},
|
||||
|
||||
_byCustomURL: function (url) {
|
||||
// Minimal test for "valid URL" w/o having to complicate it with regex
|
||||
if (url && url.indexOf('/') === -1) throw new TypeError('invalid URL');
|
||||
|
||||
// Only lowercase the placeholder variables, since the URL may contain case-sensitive data (e.g. API keys and such)
|
||||
url = url.replace(/\{S\}/g, '{s}')
|
||||
.replace(/\{X\}/g, '{x}')
|
||||
.replace(/\{Y\}/g, '{y}')
|
||||
.replace(/\{Z\}/g, '{z}');
|
||||
|
||||
var layer = this._generateCustomBaselayerModel(url);
|
||||
|
||||
return layer;
|
||||
},
|
||||
|
||||
_xyzURLTemplate: function () {
|
||||
var urlTemplate = this.get('url_template') || '';
|
||||
// Convert the proxy template variables to XYZ format, http://foo.com/bar/%%(z)s/%%(x)s/%%(y)s.png"
|
||||
return urlTemplate.replace(/%%\((\w)\)s/g, '{$1}');
|
||||
},
|
||||
|
||||
_setCustomBaselayerModel: function (customBaselayerModel) {
|
||||
this.set({
|
||||
state: 'saveDone',
|
||||
customBaselayerModel: customBaselayerModel
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./wms-layer.tpl');
|
||||
|
||||
/**
|
||||
* View for an individual layer item.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
tagName: 'li',
|
||||
|
||||
className: 'List-row',
|
||||
|
||||
events: {
|
||||
'click .js-add': '_onClickAdd'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
|
||||
|
||||
this._customBaselayersCollection = opts.customBaselayersCollection;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.html(
|
||||
template({
|
||||
model: this.model,
|
||||
canSave: this.model.canSave(this._customBaselayersCollection)
|
||||
})
|
||||
);
|
||||
return this;
|
||||
},
|
||||
|
||||
_onClickAdd: function (e) {
|
||||
this.killEvent(e);
|
||||
|
||||
if (this.model.canSave(this._customBaselayersCollection)) {
|
||||
this.model.createProxiedLayerOrCustomBaselayerModel();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<div class="List-rowItem">
|
||||
<div class="DefaultTitle <%- canSave ? '': 'is-disabled' %>"><%- model.get('title') || model.get('name') %></div>
|
||||
<button class="js-add Button Button--secondary Button--secondaryTransparentBkg <%- canSave ? '' : 'is-disabled' %>">
|
||||
<span>Add this</span>
|
||||
</button>
|
||||
</div>
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var WMSLayerModel = require('./wms-layer-model');
|
||||
|
||||
module.exports = Backbone.Collection.extend({
|
||||
|
||||
model: function (attrs, opts) {
|
||||
var self = opts.collection;
|
||||
|
||||
return new WMSLayerModel(attrs, {
|
||||
wmsService: self._wmsService
|
||||
});
|
||||
},
|
||||
|
||||
parse: function (r) {
|
||||
var layers = [];
|
||||
|
||||
if (r.layers) {
|
||||
layers = _.map(r.layers, function (layer) {
|
||||
return _.extend({ type: r.type || 'wms' }, layer);
|
||||
});
|
||||
}
|
||||
|
||||
return layers;
|
||||
},
|
||||
|
||||
sync: function (method, model, options) {
|
||||
options = options || {};
|
||||
options.url = this.url(method.toLowerCase());
|
||||
options.dataType = 'jsonp';
|
||||
method = 'READ';
|
||||
|
||||
return Backbone.sync.apply(this, arguments);
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return this._wmsService.getFetchLayersURL();
|
||||
},
|
||||
|
||||
initialize: function (models, opts) {
|
||||
if (!opts.wmsService) throw new Error('wmsService is required');
|
||||
|
||||
this._wmsService = opts.wmsService;
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
var WMSLayerView = require('./wms-layer-view');
|
||||
var CoreView = require('backbone/core-view');
|
||||
|
||||
/**
|
||||
* Sub view, to select what layer to use as basemap.
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'List',
|
||||
tagName: 'ul',
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
|
||||
if (!this.model) throw new Error('model is required');
|
||||
|
||||
this._customBaselayersCollection = opts.customBaselayersCollection;
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.append.apply(this.$el, this._renderedLayers());
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderedLayers: function () {
|
||||
return this.model.getLayers().map(function (layer) {
|
||||
var view = new WMSLayerView({
|
||||
model: layer,
|
||||
customBaselayersCollection: this._customBaselayersCollection
|
||||
});
|
||||
|
||||
this.addView(view);
|
||||
|
||||
return view.render().el;
|
||||
}, this);
|
||||
}
|
||||
|
||||
});
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var WMSView = require('./wms-view');
|
||||
var WMSLayersCollection = require('./wms-layers-collection');
|
||||
var WMSService = require('builder/data/wms-service');
|
||||
|
||||
/**
|
||||
* View model for WMS/WMTS tab content.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
name: 'wms',
|
||||
label: 'WMS/WMTS',
|
||||
currentView: 'enterURL', // [fetchingLayers, selectLayer, savingLayer]
|
||||
layersFetched: false,
|
||||
layer: undefined // will be set when selected
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
|
||||
|
||||
this._customBaselayersCollection = opts.customBaselayersCollection;
|
||||
|
||||
this.wmsService = new WMSService();
|
||||
this.wmsLayersCollection = new WMSLayersCollection(null, {
|
||||
wmsService: this.wmsService
|
||||
});
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.wmsLayersCollection.bind('change:state', this._onLayerStateChange, this);
|
||||
this.wmsLayersCollection.bind('reset', function () {
|
||||
this.set({
|
||||
currentView: this.wmsLayersCollection.length > 0 ? 'selectLayer' : 'enterURL',
|
||||
layersFetched: true
|
||||
});
|
||||
|
||||
this.trigger('layersFetched');
|
||||
}, this);
|
||||
},
|
||||
|
||||
createView: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
if (!opts.modalFooter) throw new Error('modalFooter is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
this._modalFooter = opts.modalFooter;
|
||||
|
||||
this.set({
|
||||
currentView: 'enterURL',
|
||||
layersFetched: false
|
||||
});
|
||||
|
||||
return new WMSView({
|
||||
model: this,
|
||||
customBaselayersCollection: this._customBaselayersCollection,
|
||||
submitButton: this._submitButton,
|
||||
modalFooter: this._modalFooter
|
||||
});
|
||||
},
|
||||
|
||||
fetchLayers: function (url) {
|
||||
this.set('currentView', 'fetchingLayers');
|
||||
|
||||
this.wmsService.setUrl(url);
|
||||
|
||||
this.wmsLayersCollection.fetch({
|
||||
reset: true
|
||||
});
|
||||
},
|
||||
|
||||
layersAvailableCount: function () {
|
||||
return _.difference(
|
||||
this.wmsLayersCollection.pluck('title'),
|
||||
this._customBaselayersCollection.pluck('name')
|
||||
).length;
|
||||
},
|
||||
|
||||
get: function (name) {
|
||||
if (name === 'layer') {
|
||||
var customBaselayerModel = this.wmsLayersCollection.find(function (mdl) {
|
||||
return mdl.get('state') === 'saveDone';
|
||||
});
|
||||
|
||||
return customBaselayerModel && customBaselayerModel.get('customBaselayerModel');
|
||||
} else {
|
||||
return Backbone.Model.prototype.get.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
getLayers: function () {
|
||||
if (this.get('searchQuery')) {
|
||||
var regExp = new RegExp(this.get('searchQuery'), 'i');
|
||||
|
||||
return this.wmsLayersCollection.filter(function (layer) {
|
||||
return layer.get('name').match(regExp);
|
||||
}, this);
|
||||
} else {
|
||||
return this.wmsLayersCollection;
|
||||
}
|
||||
},
|
||||
|
||||
hasAlreadyAddedLayer: function () {
|
||||
// Already added layers are disabled to be saved for each layer
|
||||
return false;
|
||||
},
|
||||
|
||||
_onLayerStateChange: function (mdl, newState) {
|
||||
switch (newState) {
|
||||
case 'saving':
|
||||
this.set('currentView', 'savingLayer');
|
||||
break;
|
||||
case 'saveDone':
|
||||
this.set('layer', mdl.get('customBaselayerModel'));
|
||||
this.trigger('saveBasemap');
|
||||
break;
|
||||
case 'saveFail':
|
||||
this.set('currentView', 'saveFail');
|
||||
break;
|
||||
default:
|
||||
this.set('currentView', 'selectLayer');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
var CoreView = require('backbone/core-view');
|
||||
var _ = require('underscore');
|
||||
var SelectLayerView = require('./select-layer-view');
|
||||
var ViewFactory = require('builder/components/view-factory');
|
||||
var ErrorView = require('builder/components/error/error-view');
|
||||
var renderLoading = require('builder/components/loading/render-loading');
|
||||
var enterUrl = require('./enter-url.tpl');
|
||||
var $ = require('jquery');
|
||||
|
||||
/**
|
||||
* Represents the WMS/WMTS tab category.
|
||||
* Current state is defined by presence (or lack of) layers
|
||||
*/
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
events: {
|
||||
'keydown .js-search-input': '_onKeyDown',
|
||||
'submit .js-search-form': 'killEvent',
|
||||
'keydown .js-url': '_onKeydown',
|
||||
'paste .js-url': '_onPaste',
|
||||
'click .js-clean-search': '_onCleanSearchClick',
|
||||
'click .js-search-link': '_submitSearch'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.customBaselayersCollection) throw new Error('customBaselayersCollection is required');
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
if (!opts.modalFooter) throw new Error('modalFooter is required');
|
||||
|
||||
this._customBaselayersCollection = opts.customBaselayersCollection;
|
||||
this._submitButton = opts.submitButton;
|
||||
this._modalFooter = opts.modalFooter;
|
||||
this._debouncedUpdate = _.debounce(this._update.bind(this), 150);
|
||||
this._onClickBinded = this._onClickOK.bind(this);
|
||||
|
||||
this._bindSubmitButton();
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this._updateOkBtn();
|
||||
this._disableOkBtn(true);
|
||||
|
||||
var view;
|
||||
|
||||
switch (this.model.get('currentView')) {
|
||||
case 'savingLayer':
|
||||
this._disableModalFooter(true);
|
||||
|
||||
view = ViewFactory.createByHTML(
|
||||
renderLoading({
|
||||
title: _t('components.modals.add-basemap.saving')
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'selectLayer':
|
||||
this._disableModalFooter(true);
|
||||
|
||||
view = new SelectLayerView({
|
||||
model: this.model,
|
||||
customBaselayersCollection: this._customBaselayersCollection
|
||||
});
|
||||
break;
|
||||
case 'saveFail':
|
||||
this._disableModalFooter(true);
|
||||
|
||||
view = new ErrorView({
|
||||
title: _t('components.modals.add-basemap.add-basemap-error')
|
||||
});
|
||||
break;
|
||||
case 'fetchingLayers':
|
||||
this._disableModalFooter(true);
|
||||
|
||||
view = ViewFactory.createByHTML(
|
||||
renderLoading({
|
||||
title: _t('components.modals.add-basemap.fetching')
|
||||
})
|
||||
);
|
||||
break;
|
||||
case 'enterURL':
|
||||
default:
|
||||
this._disableModalFooter(false);
|
||||
|
||||
view = ViewFactory.createByHTML(
|
||||
enterUrl({
|
||||
layersFetched: this.model.get('layersFetched'),
|
||||
layers: this.model.wmsLayersCollection
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
this.addView(view);
|
||||
this.$el.append(view.render().el);
|
||||
|
||||
this.$('.js-search-input').focus();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_showCleanSearchButton: function () {
|
||||
this.$('.js-clean-search').show();
|
||||
},
|
||||
|
||||
_hideCleanSearchButton: function () {
|
||||
this.$('.js-clean-search').hide();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change', this.render, this);
|
||||
this.model.bind('change', this._onChangeSearchQuery, this);
|
||||
this.model.bind('layersFetched', this.render, this);
|
||||
},
|
||||
|
||||
_onKeydown: function (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
this._debouncedUpdate();
|
||||
},
|
||||
|
||||
_onPaste: function (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
this._debouncedUpdate();
|
||||
},
|
||||
|
||||
_update: function (e) {
|
||||
this._disableOkBtn(!this.$('.js-url').val());
|
||||
this.$('.js-error').removeClass('is-visible'); // resets error state when changed
|
||||
},
|
||||
|
||||
_disableOkBtn: function (disable) {
|
||||
this._submitButton.toggleClass('is-disabled', disable);
|
||||
},
|
||||
|
||||
_onKeyDown: function (e) {
|
||||
var enterPressed = (e.keyCode === $.ui.keyCode.ENTER);
|
||||
|
||||
if (enterPressed) {
|
||||
this.killEvent(e);
|
||||
|
||||
this._submitSearch();
|
||||
}
|
||||
},
|
||||
|
||||
_submitSearch: function (e) {
|
||||
this.killEvent(e);
|
||||
|
||||
this.model.set('searchQuery', this.$('.js-search-input').val());
|
||||
},
|
||||
|
||||
_onChangeSearchQuery: function () {
|
||||
var searchQuery = this.model.get('searchQuery');
|
||||
|
||||
if (!searchQuery) {
|
||||
this._hideCleanSearchButton();
|
||||
}
|
||||
},
|
||||
|
||||
_onCleanSearchClick: function (e) {
|
||||
this.killEvent(e);
|
||||
|
||||
this.model.set('searchQuery', '');
|
||||
},
|
||||
|
||||
_onClickOK: function (e) {
|
||||
this.killEvent(e);
|
||||
|
||||
var url = this.$('.js-url').val();
|
||||
|
||||
if (url) {
|
||||
this.model.fetchLayers(url);
|
||||
}
|
||||
},
|
||||
|
||||
_disableModalFooter: function (disable) {
|
||||
this._modalFooter.toggleClass('is-disabled', disable);
|
||||
},
|
||||
|
||||
_updateOkBtn: function () {
|
||||
this._submitButton.find('span').text(_t('components.modals.add-basemap.get-layers'));
|
||||
},
|
||||
|
||||
_bindSubmitButton: function () {
|
||||
this._submitButton.on('click', this._onClickBinded);
|
||||
},
|
||||
|
||||
_unBindSubmitButton: function () {
|
||||
this._submitButton.off('click', this._onClickBinded);
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this._unBindSubmitButton();
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<div class="u-flex u-alignCenter Modal-basemapContainer">
|
||||
<h3 class="CDB-Text CDB-Size-large u-mainTextColor Modal-titleBasemap"><%- _t('components.modals.add-basemap.xyz.insert') %></h3>
|
||||
<div class="CDB-Text u-flex u-alignCenter">
|
||||
<label class="Metadata-label Metadata-label--auto CDB-Text CDB-Size-small is-semibold u-upperCase u-ellipsis"><%- _t('components.modals.add-basemap.xyz.enter') %></label>
|
||||
<div class="Form-rowData Form-rowData--longer">
|
||||
<input type="text" class="has-icon CDB-InputText CDB-Text js-url" value="" placeholder="E.g. https://{s}.carto.com/foobar/{z}/{x}/{y}.png">
|
||||
<i class="Spinner XYZPanel-inputIcon XYZPanel-inputIcon--loader Spinner--formIcon Form-inputIcon js-validating" style="display: none;"></i>
|
||||
<div class="Checkbox XYZPanel-inputCheckbox js-tms" data-title="Inverts Y axis numbering for tiles">
|
||||
<button class="Checkbox-input u-rSpace--m"></button>
|
||||
<label class="CDB-Text CDB-Size-small is-semibold u-upperCase"><%- _t('components.modals.add-basemap.xyz.tms') %></label>
|
||||
</div>
|
||||
<div class="XYZPanel-error CDB-InfoTooltip CDB-InfoTooltip--left is-error CDB-Text CDB-Size-medium CDB-InfoTooltip-text js-error"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
var _ = require('underscore');
|
||||
var Backbone = require('backbone');
|
||||
var XYZView = require('./xyz-view');
|
||||
|
||||
/**
|
||||
* View model for XYZ tab content.
|
||||
*/
|
||||
module.exports = Backbone.Model.extend({
|
||||
|
||||
defaults: {
|
||||
name: 'xyz',
|
||||
label: 'XYZ',
|
||||
tms: false,
|
||||
layer: undefined // will be set when valid
|
||||
},
|
||||
|
||||
createView: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
if (!opts.modalFooter) throw new Error('modalFooter is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
this._modalFooter = opts.modalFooter;
|
||||
|
||||
return new XYZView({
|
||||
model: this,
|
||||
submitButton: this._submitButton,
|
||||
modalFooter: this._modalFooter
|
||||
});
|
||||
},
|
||||
|
||||
hasAlreadyAddedLayer: function (userLayers) {
|
||||
var urlTemplate = this.get('layer').get('urlTemplate');
|
||||
return _.any(userLayers.isCustomCategory(), function (customLayer) {
|
||||
return customLayer.get('urlTemplate') === urlTemplate;
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
var $ = require('jquery');
|
||||
var _ = require('underscore');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./enter-url.tpl');
|
||||
var CustomBaselayerModel = require('builder/data/custom-baselayer-model');
|
||||
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
|
||||
|
||||
/**
|
||||
* Represents the XYZ tab content.
|
||||
*/
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
|
||||
className: 'XYZPanel',
|
||||
|
||||
events: {
|
||||
'click .js-tms': '_changeTMS',
|
||||
'keydown .js-url': '_onKeydown',
|
||||
'paste .js-url': '_onPaste'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
if (!opts.submitButton) throw new Error('submitButton is required');
|
||||
|
||||
this._submitButton = opts.submitButton;
|
||||
this._lastCallSeq = 0;
|
||||
this._debouncedUpdate = _.debounce(this._update.bind(this), 150);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this._updateOkBtn();
|
||||
this._disableOkBtn(true);
|
||||
|
||||
this.$el.html(
|
||||
template()
|
||||
);
|
||||
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
// Add TMS tooltip
|
||||
var tooltip = new TipsyTooltipView({
|
||||
el: this.$('.js-tms'),
|
||||
title: function () {
|
||||
return $(this).data('title');
|
||||
}
|
||||
});
|
||||
this.addView(tooltip);
|
||||
},
|
||||
|
||||
_onKeydown: function (e) {
|
||||
e.stopPropagation();
|
||||
this._debouncedUpdate();
|
||||
},
|
||||
|
||||
_onPaste: function (e) {
|
||||
e.stopPropagation();
|
||||
this._debouncedUpdate();
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.model.bind('change:tms', this._setTMSCheckbox, this);
|
||||
},
|
||||
|
||||
_update: function () {
|
||||
this._disableOkBtn(true);
|
||||
this._indicateIsValidating(true);
|
||||
var layer;
|
||||
var urlErrorMsg;
|
||||
|
||||
var url = this.$('.js-url').val();
|
||||
var tms = this.model.get('tms');
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
layer = this._byCustomURL(url, tms);
|
||||
} catch (e) {
|
||||
urlErrorMsg = _t('components.modals.add-basemap.xyz.not-valid');
|
||||
}
|
||||
}
|
||||
|
||||
this.model.set('layer', layer);
|
||||
|
||||
if (layer) {
|
||||
var self = this;
|
||||
// Make sure only the last call made is the one that defines view change,
|
||||
// avoids laggy responses to indicate wrong state
|
||||
var thisCallSeq = ++this._lastCallSeq;
|
||||
layer.validateTemplateURL({
|
||||
success: function () {
|
||||
if (thisCallSeq === self._lastCallSeq) {
|
||||
self._disableOkBtn(false);
|
||||
self._indicateIsValidating(false);
|
||||
self._updateError();
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
if (thisCallSeq === self._lastCallSeq) {
|
||||
self._disableOkBtn(false);
|
||||
self._indicateIsValidating(false);
|
||||
self._updateError(_t('components.modals.add-basemap.xyz.couldnt-validate'));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (url) {
|
||||
this._indicateIsValidating(false);
|
||||
this._updateError(urlErrorMsg);
|
||||
} else {
|
||||
this._indicateIsValidating(false);
|
||||
this._updateError();
|
||||
}
|
||||
},
|
||||
|
||||
_changeTMS: function (e) {
|
||||
this.model.set('tms', !this.model.get('tms'));
|
||||
this._onKeydown(e);
|
||||
},
|
||||
|
||||
_setTMSCheckbox: function (e) {
|
||||
this.$('.js-tms .Checkbox-input').toggleClass('is-checked', this.model.get('tms'));
|
||||
},
|
||||
|
||||
_byCustomURL: function (url, tms) {
|
||||
// Minimal test for "valid URL" w/o having to complicate it with regex
|
||||
if (url && url.indexOf('/') === -1) throw new TypeError('invalid URL');
|
||||
|
||||
// Only lowercase the placeholder variables, since the URL may contain case-sensitive data (e.g. API keys and such)
|
||||
url = url.replace(/\{S\}/g, '{s}')
|
||||
.replace(/\{X\}/g, '{x}')
|
||||
.replace(/\{Y\}/g, '{y}')
|
||||
.replace(/\{Z\}/g, '{z}');
|
||||
|
||||
var layer = new CustomBaselayerModel({
|
||||
urlTemplate: url,
|
||||
attribution: null,
|
||||
maxZoom: 21,
|
||||
minZoom: 0,
|
||||
name: '',
|
||||
tms: tms,
|
||||
category: 'Custom',
|
||||
type: 'Tiled'
|
||||
});
|
||||
layer.set('className', layer._generateClassName(url));
|
||||
|
||||
return layer;
|
||||
},
|
||||
|
||||
_setTMS: function (ev) {
|
||||
var $checkbox = $(ev.target).closest('.Checkbox');
|
||||
$checkbox.find('.Checkbox-input').toggleClass('is-checked');
|
||||
this._update(ev);
|
||||
},
|
||||
|
||||
_updateOkBtn: function () {
|
||||
this._submitButton.find('span').text(_t('components.modals.add-basemap.add-btn'));
|
||||
},
|
||||
|
||||
_disableOkBtn: function (disable) {
|
||||
this._submitButton.toggleClass('is-disabled', disable);
|
||||
},
|
||||
|
||||
_updateError: function (msg) {
|
||||
this.$('.js-error').text(msg)[ msg ? 'addClass' : 'removeClass' ]('is-visible');
|
||||
},
|
||||
|
||||
_indicateIsValidating: function (indicate) {
|
||||
this.$('.js-validating').toggle(!!indicate);
|
||||
}
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user