Initial commit

This commit is contained in:
zhongjin
2020-06-15 10:58:47 +08:00
commit 4f1dfe7564
8590 changed files with 1516878 additions and 0 deletions
@@ -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');
}
});
@@ -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();
}
}
});
@@ -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>
@@ -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>
@@ -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
});
}
});
}
});
@@ -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);
}
});
@@ -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);
}
});
@@ -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>
@@ -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>
@@ -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');
}
});
@@ -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;
});
}
});
@@ -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;
}
}
});
@@ -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>
@@ -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');
}
});
@@ -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>
@@ -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
});
}
});
@@ -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();
}
}
});
@@ -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>
@@ -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);
}
});
@@ -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');
}
}
});
@@ -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);
}
});
@@ -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>
@@ -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;
});
}
});
@@ -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);
}
});