Initial commit
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
var Backbone = require('backbone');
|
||||
var _ = require('underscore');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'hasGoogleBasemap'
|
||||
];
|
||||
|
||||
var DIMENSION_LIMIT = 8192;
|
||||
|
||||
var OPTIONS = [{
|
||||
type: 'png',
|
||||
label: '.png',
|
||||
className: 'track-ImageExport track-typePNG'
|
||||
}, {
|
||||
type: 'jpg',
|
||||
label: '.jpg',
|
||||
className: 'track-ImageExport track-typeJPG'
|
||||
}];
|
||||
|
||||
module.exports = Backbone.Model.extend({
|
||||
defaults: {
|
||||
type: 'none',
|
||||
title: ''
|
||||
},
|
||||
|
||||
initialize: function (attrs, opts) {
|
||||
checkAndBuildOpts(attrs, REQUIRED_OPTS, this);
|
||||
|
||||
this.schema = this._generateSchema();
|
||||
this._setSchema();
|
||||
this.on('change:format change:width change:height', this._setSchema, this);
|
||||
},
|
||||
|
||||
_setSchema: function () {
|
||||
this.schema = this._generateSchema();
|
||||
},
|
||||
|
||||
_generateSchema: function () {
|
||||
return {
|
||||
width: {
|
||||
type: 'Number',
|
||||
title: _t('editor.export-image.properties.width') + ' (px)',
|
||||
validators: ['required', this._validateDimension.bind(this)],
|
||||
showSlider: false,
|
||||
editorAttrs: {
|
||||
className: 'track-ImageExport track-inputWidth',
|
||||
placeholder: _t('editor.export-image.properties.width')
|
||||
}
|
||||
},
|
||||
height: {
|
||||
type: 'Number',
|
||||
title: _t('editor.export-image.properties.height') + ' (px)',
|
||||
validators: ['required', this._validateDimension.bind(this)],
|
||||
showSlider: false,
|
||||
editorAttrs: {
|
||||
className: 'track-ImageExport track-inputHeight',
|
||||
placeholder: _t('editor.export-image.properties.height')
|
||||
}
|
||||
},
|
||||
format: {
|
||||
type: 'Radio',
|
||||
title: _t('editor.export-image.properties.format'),
|
||||
options: _.map(OPTIONS, function (d) {
|
||||
return {
|
||||
val: d.type,
|
||||
label: d.label,
|
||||
className: d.className
|
||||
};
|
||||
}, this)
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
_validateDimension: function (value, formValues) {
|
||||
var numericValue = +value;
|
||||
var limit = DIMENSION_LIMIT;
|
||||
|
||||
if (_.isNumber(numericValue) && numericValue >= 1 && numericValue <= limit) {
|
||||
return null; // valid dimension
|
||||
}
|
||||
|
||||
return {
|
||||
message: _t('editor.export-image.invalid-dimension', { limit: limit })
|
||||
};
|
||||
},
|
||||
|
||||
setErrors: function (errors) {
|
||||
// we don't want to set these errors as part as attributes
|
||||
// then we manually check if we should trigger the custom event
|
||||
var shouldTrigger = errors !== this._validationErrors;
|
||||
this._validationErrors = errors;
|
||||
shouldTrigger && this.trigger('change:errors');
|
||||
},
|
||||
|
||||
canExport: function () {
|
||||
return _.isUndefined(this._validationErrors);
|
||||
},
|
||||
|
||||
toJSON: function () {
|
||||
return _.extend(
|
||||
this.attributes,
|
||||
{
|
||||
type: this.get('type')
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./export-image-form.tpl');
|
||||
require('builder/components/form-components/index');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var REQUIRED_OPTS = [
|
||||
'formModel'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this._removeFormView();
|
||||
this._renderForm();
|
||||
this._initBinds();
|
||||
return this;
|
||||
},
|
||||
|
||||
_renderForm: function () {
|
||||
this.clearSubViews();
|
||||
this._removeFormView();
|
||||
|
||||
this._formView = new Backbone.Form({
|
||||
template: template,
|
||||
model: this._formModel
|
||||
});
|
||||
|
||||
this.$('form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
this.$el.append(this._formView.render().$el);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this._formView, 'change', this._updateChanges);
|
||||
this.listenTo(this._formModel, 'change:width change:height', this._updateSchema);
|
||||
},
|
||||
|
||||
_updateSchema: function () {
|
||||
this._formView.fields.width.setValue(this._formModel.get('width'));
|
||||
this._formView.fields.height.setValue(this._formModel.get('height'));
|
||||
this._updateChanges();
|
||||
},
|
||||
|
||||
_updateChanges: function () {
|
||||
var errors = this._formView.commit({validate: true});
|
||||
this._formView.model.setErrors(errors);
|
||||
},
|
||||
|
||||
_removeFormView: function () {
|
||||
this._formView && this._formView.remove();
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
this.$('form').off('submit');
|
||||
this._removeFormView();
|
||||
CoreView.prototype.clean.call(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="u-flex u-bSpace--xl">
|
||||
<div class="Editor-HeaderNumeration CDB-Text is-semibold u-rSpace--m">1</div>
|
||||
<div class="Editor-HeaderInfo-inner CDB-Text" data-fields="width,height">
|
||||
<div class="Editor-HeaderInfo-title u-bSpace--m">
|
||||
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.export-image.properties.size') %></h2>
|
||||
</div>
|
||||
<p class="CDB-Text u-upperCase CDB-FontSize-small u-altTextColor u-bSpace--m"><%- _t('editor.export-image.properties.size-subtitle') %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex u-bSpace--xl">
|
||||
<div class="Editor-HeaderNumeration CDB-Text is-semibold u-rSpace--m">2</div>
|
||||
<div class="Editor-HeaderInfo-inner CDB-Text" data-fields="format">
|
||||
<div class="Editor-HeaderInfo-title u-bSpace--m">
|
||||
<h2 class="CDB-Text CDB-HeaderInfo-titleText CDB-Size-large"><%- _t('editor.export-image.properties.format') %></h2>
|
||||
</div>
|
||||
<p class="CDB-Text u-upperCase CDB-FontSize-small u-altTextColor u-bSpace--m"><%- _t('editor.export-image.properties.format-subtitle') %></p>
|
||||
</div>
|
||||
</div>
|
||||
+502
@@ -0,0 +1,502 @@
|
||||
var $ = require('jquery');
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var FooterView = require('./footer/footer-view');
|
||||
var ExportImageFormView = require('./export-image-form-view.js');
|
||||
var ExportImageFormModel = require('./export-image-form-model');
|
||||
var ExportImageWidget = require('./export-image-widget');
|
||||
var template = require('./export-image-pane.tpl');
|
||||
var Utils = require('builder/helpers/utils');
|
||||
var Notifier = require('builder/components/notifier/notifier');
|
||||
var browser = require('builder/helpers/browser-detect');
|
||||
|
||||
var Infobox = require('builder/components/infobox/infobox-factory');
|
||||
var InfoboxView = require('builder/components/infobox/infobox-view');
|
||||
var InfoboxModel = require('builder/components/infobox/infobox-model');
|
||||
var InfoboxCollection = require('builder/components/infobox/infobox-collection');
|
||||
|
||||
var DEFAULT_EXPORT_FORMAT = 'png';
|
||||
var DEFAULT_EXPORT_WIDTH = 300;
|
||||
var DEFAULT_EXPORT_HEIGHT = 200;
|
||||
|
||||
var CARTO_ATTRIBUTION = '@CARTO';
|
||||
var ATTRIBUTION_WIDTH = 15;
|
||||
var ATTRIBUTION_HEIGHT = 16;
|
||||
|
||||
var NOTIFICATION_ID = 'exportImageNotification';
|
||||
var LOGO_PATH = '/unversioned/images/carto.png';
|
||||
|
||||
var GMAPS_DIMENSION_LIMIT = 640;
|
||||
var GMAPS_DIMENSION_LIMIT_PREMIUM = 2048;
|
||||
|
||||
var MIMETYPES = {
|
||||
'jpg': 'image/jpeg',
|
||||
'png': 'image/png'
|
||||
};
|
||||
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
var REQUIRED_OPTS = [
|
||||
'canvasClassName',
|
||||
'configModel',
|
||||
'stackLayoutModel',
|
||||
'userModel',
|
||||
'visDefinitionModel',
|
||||
'stateDefinitionModel',
|
||||
'mapDefinitionModel',
|
||||
'editorModel',
|
||||
'settingsCollection'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'Editor-content',
|
||||
|
||||
events: {
|
||||
'click .js-back': '_goStepBack'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this._canvasModel = new Backbone.Model();
|
||||
this._$map = $('.' + this._canvasClassName);
|
||||
|
||||
var width = DEFAULT_EXPORT_WIDTH;
|
||||
var height = DEFAULT_EXPORT_HEIGHT;
|
||||
|
||||
var x = Math.ceil(this._$map.width() / 2 - (width / 2));
|
||||
var y = Math.ceil(this._$map.height() / 2 - (height / 2));
|
||||
|
||||
this._exportImageFormModel = new ExportImageFormModel({
|
||||
userModel: this._userModel,
|
||||
hasGoogleBasemap: this._hasGoogleMapsBasemap(),
|
||||
format: DEFAULT_EXPORT_FORMAT,
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height
|
||||
});
|
||||
},
|
||||
|
||||
_isLogoActive: function () {
|
||||
var canRemoveLogo = this._userModel.get('actions').remove_logo;
|
||||
var logoSetting = this._settingsCollection.findWhere({
|
||||
setting: 'logo'
|
||||
});
|
||||
|
||||
var isLogoActive = logoSetting && logoSetting.get('enabler') || false;
|
||||
if (!canRemoveLogo || canRemoveLogo && isLogoActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (canRemoveLogo && !isLogoActive) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_addErrorNotification: function (error) {
|
||||
Notifier.addNotification({
|
||||
id: NOTIFICATION_ID,
|
||||
status: 'error',
|
||||
closable: true,
|
||||
button: false,
|
||||
delay: Notifier.DEFAULT_DELAY,
|
||||
info: error + ' ' + _t('editor.maps.export-image.errors.try-again')
|
||||
});
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
|
||||
this.$el.append(
|
||||
template({
|
||||
mapName: this._getMapName()
|
||||
})
|
||||
);
|
||||
|
||||
this._createWidget();
|
||||
this._createForm();
|
||||
this._createDisclaimer();
|
||||
this._createFooter();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_createForm: function () {
|
||||
this._exportImageFormView = new ExportImageFormView({
|
||||
formModel: this._exportImageFormModel,
|
||||
canvasModel: this._exportImageFormModel
|
||||
});
|
||||
|
||||
this.$('.js-content').append(this._exportImageFormView.render().$el);
|
||||
this.addView(this._exportImageFormView);
|
||||
},
|
||||
|
||||
_createDisclaimer: function () {
|
||||
var infoboxSstates = [{
|
||||
state: 'disclaimer',
|
||||
createContentView: function () {
|
||||
return Infobox.createWithAction({
|
||||
type: 'warning',
|
||||
title: _t('editor.maps.export-image.disclaimer.title'),
|
||||
body: _t('editor.maps.export-image.disclaimer.body')
|
||||
});
|
||||
}
|
||||
}];
|
||||
|
||||
this._infoboxModel = new InfoboxModel({
|
||||
state: 'disclaimer'
|
||||
});
|
||||
|
||||
this._infoboxCollection = new InfoboxCollection(infoboxSstates);
|
||||
|
||||
this._infoboxView = new InfoboxView({
|
||||
infoboxModel: this._infoboxModel,
|
||||
infoboxCollection: this._infoboxCollection
|
||||
});
|
||||
|
||||
this.$('.js-disclaimer').append(this._infoboxView.render().el);
|
||||
this.addView(this._infoboxView);
|
||||
},
|
||||
|
||||
_createFooter: function () {
|
||||
this._footerView = new FooterView({
|
||||
configModel: this._configModel,
|
||||
userModel: this._userModel,
|
||||
formModel: this._exportImageFormModel
|
||||
});
|
||||
|
||||
this.addView(this._footerView);
|
||||
this._footerView.bind('finish', this._exportImage, this);
|
||||
this.$('.js-footer').append(this._footerView.render().el);
|
||||
},
|
||||
|
||||
_createWidget: function () {
|
||||
this._exportImageWidget = new ExportImageWidget({
|
||||
mapViewClass: 'CDB-Map-wrapper',
|
||||
dashboardCanvasClass: 'CDB-Dashboard-canvas',
|
||||
model: this._exportImageFormModel,
|
||||
stateDefinitionModel: this._stateDefinitionModel,
|
||||
mapDefinitionModel: this._mapDefinitionModel
|
||||
});
|
||||
|
||||
this.addView(this._exportImageWidget);
|
||||
this._$map.prepend(this._exportImageWidget.render().$el);
|
||||
},
|
||||
|
||||
_getStaticMapURL: function () {
|
||||
var getStaticImageURL = this._mapDefinitionModel.getStaticImageURLTemplate();
|
||||
var imageMapMetadata = this._mapDefinitionModel.getImageExportMetadata();
|
||||
return getStaticImageURL({
|
||||
zoom: imageMapMetadata.zoom,
|
||||
width: this._exportImageFormModel.get('width'),
|
||||
height: this._exportImageFormModel.get('height'),
|
||||
lat: this._exportImageFormModel.get('lat'),
|
||||
lng: this._exportImageFormModel.get('lng'),
|
||||
format: 'png' // we always use 'png' to allow merging layers
|
||||
});
|
||||
},
|
||||
|
||||
_removeNotification: function () {
|
||||
if (Notifier.getNotification(NOTIFICATION_ID)) {
|
||||
Notifier.removeNotification(NOTIFICATION_ID);
|
||||
}
|
||||
},
|
||||
|
||||
_exportImage: function () {
|
||||
var self = this;
|
||||
|
||||
this._removeNotification();
|
||||
|
||||
var def = $.Deferred();
|
||||
|
||||
var logo = function () {
|
||||
return self._loadLogo();
|
||||
};
|
||||
|
||||
var basemap = function () {
|
||||
if (self._hasGoogleMapsBasemap()) {
|
||||
return self._loadGMapBasemap();
|
||||
}
|
||||
};
|
||||
|
||||
var image = function () {
|
||||
return self._onLoadImage(self._downloadImage.bind(self));
|
||||
};
|
||||
|
||||
var attribution = function () {
|
||||
return self._loadAttribution();
|
||||
};
|
||||
|
||||
def.then(logo)
|
||||
.then(basemap)
|
||||
.then(attribution)
|
||||
.then(image)
|
||||
.fail(this._onFail.bind(this));
|
||||
|
||||
def.resolve();
|
||||
},
|
||||
|
||||
_onFail: function (error) {
|
||||
this._footerView.stopLoader();
|
||||
this._addErrorNotification(error);
|
||||
},
|
||||
|
||||
_getMapName: function () {
|
||||
return this._visDefinitionModel.get('name');
|
||||
},
|
||||
|
||||
_downloadImage: function (canvas) {
|
||||
var browserInfo = browser();
|
||||
var imageFormat = this._exportImageFormModel.get('format');
|
||||
var fileName = this._getMapName() + '.' + imageFormat;
|
||||
|
||||
if (canvas.msToBlob) {
|
||||
var image = canvas.msToBlob();
|
||||
window.navigator.msSaveBlob(
|
||||
new Blob([image], { type: MIMETYPES[imageFormat] }), // eslint-disable-line
|
||||
fileName
|
||||
);
|
||||
} else {
|
||||
var dataUri = canvas.toDataURL(MIMETYPES[imageFormat]);
|
||||
var link = document.createElement('a');
|
||||
document.body.appendChild(link);
|
||||
|
||||
link.download = fileName;
|
||||
link.href = dataUri;
|
||||
|
||||
if (browserInfo.name !== 'Safari') {
|
||||
link.target = '_blank';
|
||||
} else {
|
||||
dataUri.replace(/^data:image\/[^;]/, 'data:application/octet-stream');
|
||||
}
|
||||
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
this._footerView.stopLoader();
|
||||
},
|
||||
|
||||
_hasGoogleMapsBasemap: function () {
|
||||
var imageMapMetadata = this._mapDefinitionModel.getImageExportMetadata();
|
||||
return imageMapMetadata.provider === 'googlemaps';
|
||||
},
|
||||
|
||||
_onLoadImage: function (callback) {
|
||||
var self = this;
|
||||
var url = this._getStaticMapURL();
|
||||
|
||||
var basemap = this._basemap;
|
||||
|
||||
var width = +this._exportImageFormModel.get('width');
|
||||
var height = +this._exportImageFormModel.get('height');
|
||||
|
||||
var image = new Image(); // eslint-disable-line
|
||||
|
||||
image.setAttribute('crossOrigin', 'anonymous');
|
||||
|
||||
image.onload = function () {
|
||||
var canvas = document.createElement('canvas');
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
canvas.width = this.naturalWidth;
|
||||
canvas.height = this.naturalHeight;
|
||||
|
||||
if (basemap) {
|
||||
ctx.drawImage(basemap, 0, 0);
|
||||
}
|
||||
|
||||
ctx.drawImage(this, 0, 0);
|
||||
|
||||
if (self._isLogoActive()) {
|
||||
self._drawLogo(ctx, width, height);
|
||||
}
|
||||
|
||||
self._drawAttribution(ctx, width, height);
|
||||
|
||||
callback(canvas);
|
||||
};
|
||||
|
||||
image.onerror = function (e) {
|
||||
self._onFail(_t('editor.maps.export-image.errors.error-image'));
|
||||
};
|
||||
|
||||
image.src = url;
|
||||
},
|
||||
|
||||
_drawLogo: function (ctx, width, height) {
|
||||
if (this._logo) {
|
||||
var left = (width / 2) - (this._logo.width / 2);
|
||||
var top = height - this._logo.height - 5;
|
||||
|
||||
ctx.drawImage(this._logo, left, top);
|
||||
}
|
||||
},
|
||||
|
||||
_drawAttribution: function (ctx, width, height) {
|
||||
if (this._attribution) {
|
||||
var left = width - this._attribution.width - 3;
|
||||
var top = height - this._attribution.height;
|
||||
|
||||
if (this._hasGoogleMapsBasemap()) {
|
||||
var attributionWidth = this._$map.find('.gm-style-cc').width();
|
||||
|
||||
if (attributionWidth >= width / 2) {
|
||||
top = top - ATTRIBUTION_HEIGHT / 2;
|
||||
}
|
||||
|
||||
top = height - this._attribution.height - ATTRIBUTION_HEIGHT;
|
||||
}
|
||||
|
||||
ctx.drawImage(this._attribution, left, top);
|
||||
}
|
||||
},
|
||||
|
||||
_getDimensionString: function () {
|
||||
var width = this._exportImageFormModel.get('width');
|
||||
var height = this._exportImageFormModel.get('height');
|
||||
|
||||
return width + 'x' + height;
|
||||
},
|
||||
|
||||
_getCenterString: function () {
|
||||
var lat = +this._exportImageFormModel.get('lat').toFixed(6);
|
||||
var lng = +this._exportImageFormModel.get('lng').toFixed(6);
|
||||
|
||||
return lat + ',' + lng;
|
||||
},
|
||||
|
||||
_parseAttribution: function (attribution) {
|
||||
return Utils.stripHTML(
|
||||
attribution
|
||||
.join(' ')
|
||||
.replace(/©/g, '©')
|
||||
);
|
||||
},
|
||||
|
||||
_loadAttribution: function () {
|
||||
var self = this;
|
||||
var visMetadata = this._mapDefinitionModel.getImageExportMetadata();
|
||||
var deferred = $.Deferred();
|
||||
|
||||
var image = new Image(); // eslint-disable-line
|
||||
var canvas = document.createElement('canvas');
|
||||
var ctx = canvas.getContext('2d');
|
||||
|
||||
var text = this._hasGoogleMapsBasemap() ? CARTO_ATTRIBUTION : this._parseAttribution(visMetadata.attribution);
|
||||
var dimension = ctx.measureText(text);
|
||||
var width = dimension.width;
|
||||
|
||||
ctx.font = '11px';
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
||||
ctx.fillRect(-ATTRIBUTION_WIDTH, 0, width + ATTRIBUTION_WIDTH + 2, ATTRIBUTION_HEIGHT - 3);
|
||||
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillText(text, 0, 10);
|
||||
|
||||
image.onload = function () {
|
||||
self._attribution = this;
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
image.onerror = function () {
|
||||
deferred.reject(_t('editor.maps.export-image.errors.error-attribution'));
|
||||
};
|
||||
|
||||
image.width = width;
|
||||
image.height = ATTRIBUTION_HEIGHT - 3;
|
||||
image.src = canvas.toDataURL();
|
||||
|
||||
return deferred.promise();
|
||||
},
|
||||
|
||||
_loadLogo: function () {
|
||||
var self = this;
|
||||
var deferred = $.Deferred();
|
||||
var logoUrl = this._configModel.get('app_assets_base_url') + LOGO_PATH;
|
||||
|
||||
if (this._isLogoActive()) {
|
||||
this._getImageFromUrl(logoUrl)
|
||||
.then(function (image) {
|
||||
self._logo = image;
|
||||
deferred.resolve();
|
||||
})
|
||||
.fail(function () {
|
||||
deferred.reject(_t('editor.maps.export-image.errors.error-image'));
|
||||
});
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
return deferred.promise();
|
||||
},
|
||||
|
||||
_loadGMapBasemap: function () {
|
||||
var self = this;
|
||||
var apiUrl = self._configModel.get('base_url') + '/api/v1/viz/' + self._visDefinitionModel.get('id') + '/google_maps_static_image';
|
||||
var visMetadata = this._mapDefinitionModel.getImageExportMetadata();
|
||||
var deferred = $.Deferred();
|
||||
|
||||
var onError = function () {
|
||||
deferred.reject(_t('editor.maps.export-image.errors.error-basemap'));
|
||||
};
|
||||
|
||||
var onSuccess = function (data) {
|
||||
var limit = data.url.indexOf('signature') !== -1 ? GMAPS_DIMENSION_LIMIT_PREMIUM : GMAPS_DIMENSION_LIMIT;
|
||||
|
||||
if (self._validGMapDimension(data.url, limit)) {
|
||||
self._getImageFromUrl(data.url)
|
||||
.then(function (image) {
|
||||
self._basemap = image;
|
||||
deferred.resolve();
|
||||
})
|
||||
.fail(onError);
|
||||
} else {
|
||||
deferred.reject(_t('editor.export-image.invalid-dimension', { limit: limit }));
|
||||
}
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: apiUrl,
|
||||
data: {
|
||||
center: this._getCenterString(),
|
||||
size: this._getDimensionString(),
|
||||
zoom: visMetadata.zoom
|
||||
},
|
||||
success: onSuccess,
|
||||
error: onError
|
||||
});
|
||||
|
||||
return deferred.promise();
|
||||
},
|
||||
|
||||
_getImageFromUrl: function (url) {
|
||||
var deferred = $.Deferred();
|
||||
var image = new Image(); // eslint-disable-line
|
||||
|
||||
image.setAttribute('crossOrigin', 'anonymous');
|
||||
|
||||
image.onload = function () {
|
||||
deferred.resolve(this);
|
||||
};
|
||||
|
||||
image.onerror = function () {
|
||||
deferred.reject();
|
||||
};
|
||||
|
||||
image.src = url;
|
||||
|
||||
return deferred.promise();
|
||||
},
|
||||
|
||||
_validGMapDimension: function (url, limit) {
|
||||
var width = this._exportImageFormModel.get('width');
|
||||
var height = this._exportImageFormModel.get('height');
|
||||
|
||||
return width <= limit && height <= limit;
|
||||
},
|
||||
|
||||
_goStepBack: function () {
|
||||
this._stackLayoutModel.prevStep();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
<ul class="Editor-breadcrumb">
|
||||
<li class="Editor-breadcrumbItem CDB-Text CDB-Size-medium u-actionTextColor">
|
||||
<button class="js-back">
|
||||
<i class="CDB-IconFont CDB-IconFont-arrowPrev Size-large u-rSpace"></i>
|
||||
|
||||
<span class="Editor-breadcrumbLink"><%- _t('back') %></span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="Editor-breadcrumbItem CDB-Text CDB-Size-medium">
|
||||
<span class="Editor-breadcrumbSep"> / </span> <%- _t('editor.maps.export-image.title') %>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="Editor-HeaderInfoEditor">
|
||||
<div class="Editor-HeaderInfo-inner Editor-HeaderInfo-inner--wide">
|
||||
<div class="Editor-HeaderInfo">
|
||||
<div class="CDB-Text CDB-Size-huge is-light u-ellipsis"><%- mapName %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="Editor-content js-content"></div>
|
||||
<div class="js-disclaimer"></div>
|
||||
<div class="Options-bar Options-bar--right u-flex js-footer"></div>
|
||||
@@ -0,0 +1,217 @@
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./export-image-widget.tpl');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var TRACK_CONTEXT_CLASS = 'track-ImageExport';
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'mapViewClass',
|
||||
'dashboardCanvasClass',
|
||||
'stateDefinitionModel',
|
||||
'mapDefinitionModel'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'ExportImageView',
|
||||
|
||||
events: {
|
||||
'mouseenter .js-canvas': '_onEnter',
|
||||
'mouseleave .js-canvas': '_onLeave'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this._$dashBoardCanvas = $('.' + this._dashboardCanvasClass);
|
||||
|
||||
this._initBinds();
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.$el.append(template(this.model.attributes));
|
||||
this._initViews();
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_initViews: function () {
|
||||
this._setupCanvas();
|
||||
this._updateHelpers();
|
||||
|
||||
var x = +this.model.get('x');
|
||||
var y = +this.model.get('y');
|
||||
var width = +this.model.get('width');
|
||||
var height = +this.model.get('height');
|
||||
|
||||
this._updateCoordinates(x, y, width, height);
|
||||
},
|
||||
|
||||
_initBinds: function () {
|
||||
this.listenTo(this.model, 'change:x change:y change:width change:height', this._updateHelpers);
|
||||
this.listenTo(this._stateDefinitionModel, 'change:json', this._onChangeMap);
|
||||
|
||||
this._resize = _.debounce(this._onResizeWindow.bind(this), 20);
|
||||
$(window).on('resize', this._resize);
|
||||
|
||||
this.listenTo(this._stateDefinitionModel, 'change', this._updateCanvasCoordinates);
|
||||
},
|
||||
|
||||
_onEnter: function () {
|
||||
_.debounce(this.$el.addClass('is-top'), 50);
|
||||
},
|
||||
|
||||
_onLeave: function () {
|
||||
_.debounce(this.$el.removeClass('is-top'), 50);
|
||||
},
|
||||
|
||||
_onResizeWindow: function (e) {
|
||||
if (e.target === window) {
|
||||
var latLngToPixel = this._mapDefinitionModel.latLngToPixel;
|
||||
|
||||
var coordinates = latLngToPixel({
|
||||
lat: this.model.get('lat'),
|
||||
lng: this.model.get('lng')
|
||||
});
|
||||
|
||||
var width = this.model.get('width');
|
||||
var height = this.model.get('height');
|
||||
|
||||
this.model.set({
|
||||
x: coordinates.x - width / 2,
|
||||
y: coordinates.y - height / 2
|
||||
});
|
||||
|
||||
this._updateCanvasCoordinates();
|
||||
this._updateHelpers();
|
||||
}
|
||||
},
|
||||
|
||||
_setupCanvas: function () {
|
||||
this.$('.js-canvas').resizable({
|
||||
resize: this._onResize.bind(this),
|
||||
handles: 'n, e, s, w, ne, se, sw, nw',
|
||||
containment: this._$dashBoardCanvas
|
||||
});
|
||||
|
||||
this.$('.js-canvas').draggable({
|
||||
drag: this._updateCanvasCoordinates.bind(this),
|
||||
stop: this._updateCanvasCoordinates.bind(this),
|
||||
containment: this._$dashBoardCanvas
|
||||
});
|
||||
|
||||
this._setupTracks();
|
||||
|
||||
this.$('.js-canvas').css({
|
||||
top: +this.model.get('x'),
|
||||
left: +this.model.get('y'),
|
||||
width: +this.model.get('width'),
|
||||
height: +this.model.get('height')
|
||||
});
|
||||
},
|
||||
|
||||
_updateHelpers: function () {
|
||||
var x = +this.model.get('x');
|
||||
var y = +this.model.get('y');
|
||||
var width = +this.model.get('width');
|
||||
var height = +this.model.get('height');
|
||||
|
||||
var size = this._mapDefinitionModel.getMapViewSize();
|
||||
var canvasWidth = size && size.x || 0;
|
||||
var canvasHeight = size && size.y || 0;
|
||||
|
||||
var bottom = y + height + 1;
|
||||
var right = x + width + 1;
|
||||
|
||||
this.$('.js-canvas').css({
|
||||
top: y,
|
||||
left: x,
|
||||
height: height,
|
||||
width: width
|
||||
});
|
||||
|
||||
if (x >= 0 && y >= 0) {
|
||||
this.$('.js-helper-north').css({ top: 0, width: right, height: y + 1 });
|
||||
this.$('.js-helper-west').css({ left: 0, top: y + 1, width: x + 1, height: height });
|
||||
this.$('.js-helper-south').css({ top: bottom, left: 0, width: canvasWidth, height: canvasHeight - height - y });
|
||||
this.$('.js-helper-east').css({ left: right, top: 0, width: canvasWidth - width - x - 1, height: bottom });
|
||||
}
|
||||
|
||||
if (right + 1 >= canvasWidth) {
|
||||
this.$('.js-helper-east').css('width', 0);
|
||||
}
|
||||
if (x <= 0) {
|
||||
this.$('.js-helper-west').css('width', 0);
|
||||
}
|
||||
if (bottom + 1 >= canvasHeight) {
|
||||
this.$('.js-helper-south').css('height', 0);
|
||||
}
|
||||
if (y <= 0) {
|
||||
this.$('.js-helper-north').css('height', 0);
|
||||
}
|
||||
},
|
||||
|
||||
_updateCanvasCoordinates: function () {
|
||||
var width = this.$('.js-canvas').width();
|
||||
var height = this.$('.js-canvas').height();
|
||||
var x = this.$('.js-canvas').position().left;
|
||||
var y = this.$('.js-canvas').position().top;
|
||||
|
||||
this._updateCoordinates(x, y, width, height);
|
||||
},
|
||||
|
||||
_onChangeMap: function () {
|
||||
var coordinates = this._calcCenter();
|
||||
|
||||
this.model.set({
|
||||
lat: coordinates.lat,
|
||||
lng: coordinates.lng
|
||||
});
|
||||
},
|
||||
|
||||
_onResize: function (e, ui) {
|
||||
var width = ui.helper.width();
|
||||
var height = ui.helper.height();
|
||||
var x = ui.helper.position().left;
|
||||
var y = ui.helper.position().top;
|
||||
|
||||
this._updateCoordinates(x, y, width, height);
|
||||
},
|
||||
|
||||
_updateCoordinates: function (x, y, width, height) {
|
||||
var coordinates = this._calcCenter();
|
||||
|
||||
this.model.set({
|
||||
x: x,
|
||||
y: y,
|
||||
width: width,
|
||||
height: height,
|
||||
lat: coordinates.lat,
|
||||
lng: coordinates.lng
|
||||
});
|
||||
},
|
||||
|
||||
_calcCenter: function () {
|
||||
var x = this.model.get('x') + this.model.get('width') / 2;
|
||||
var y = this.model.get('y') + this.model.get('height') / 2;
|
||||
var pixelToLatLng = this._mapDefinitionModel.pixelToLatLng;
|
||||
return pixelToLatLng({ x: x, y: y });
|
||||
},
|
||||
|
||||
_setupTracks: function () {
|
||||
this.$('.js-canvas .ui-resizable-nw').addClass(TRACK_CONTEXT_CLASS + ' ' + 'track-nodeTopLeft');
|
||||
this.$('.js-canvas .ui-resizable-n').addClass(TRACK_CONTEXT_CLASS + ' ' + 'track-nodeTopCenter');
|
||||
this.$('.js-canvas .ui-resizable-ne').addClass(TRACK_CONTEXT_CLASS + ' ' + 'track-nodeTopRight');
|
||||
this.$('.js-canvas .ui-resizable-w').addClass(TRACK_CONTEXT_CLASS + ' ' + 'track-nodeMiddleLeft');
|
||||
this.$('.js-canvas .ui-resizable-e').addClass(TRACK_CONTEXT_CLASS + ' ' + 'track-nodeMiddleCenter');
|
||||
this.$('.js-canvas .ui-resizable-sw').addClass(TRACK_CONTEXT_CLASS + ' ' + 'track-nodeBottomLeft');
|
||||
this.$('.js-canvas .ui-resizable-s').addClass(TRACK_CONTEXT_CLASS + ' ' + 'track-nodeBottomCenter');
|
||||
this.$('.js-canvas .ui-resizable-se').addClass(TRACK_CONTEXT_CLASS + ' ' + 'track-nodeBottomRight');
|
||||
},
|
||||
|
||||
clean: function () {
|
||||
$(window).off('resize', this._resize);
|
||||
CoreView.prototype.clean.apply(this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="ExportHelper ExportHelper--north js-export-helper js-helper-north"></div>
|
||||
<div class="ExportHelper ExportHelper--west js-export-helper js-helper-west"></div>
|
||||
<div class="ExportHelper ExportHelper--south js-export-helper js-helper-south"></div>
|
||||
<div class="ExportHelper ExportHelper--east js-export-helper js-helper-east"></div>
|
||||
<div class="CanvasExport js-canvas"></div>
|
||||
@@ -0,0 +1,53 @@
|
||||
var Backbone = require('backbone');
|
||||
var CoreView = require('backbone/core-view');
|
||||
var template = require('./footer.tpl');
|
||||
var checkAndBuildOpts = require('builder/helpers/required-opts');
|
||||
|
||||
var LOADING = 'loading';
|
||||
|
||||
var REQUIRED_OPTS = [
|
||||
'userModel',
|
||||
'configModel',
|
||||
'formModel'
|
||||
];
|
||||
|
||||
module.exports = CoreView.extend({
|
||||
className: 'Editor-FooterInfoEditor',
|
||||
|
||||
events: {
|
||||
'click .js-ok': '_finish'
|
||||
},
|
||||
|
||||
initialize: function (opts) {
|
||||
checkAndBuildOpts(opts, REQUIRED_OPTS, this);
|
||||
|
||||
this.model = new Backbone.Model({ state: '' });
|
||||
|
||||
this.listenTo(this.model, 'change:state', this.render);
|
||||
this.listenTo(this._formModel, 'change:errors', this.render);
|
||||
},
|
||||
|
||||
render: function () {
|
||||
this.clearSubViews();
|
||||
this.$el.html(template({
|
||||
isLoading: this._isLoading(),
|
||||
isDisabled: !(this._formModel.canExport() && !this._isLoading())
|
||||
}));
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
stopLoader: function (e) {
|
||||
this.model.unset('state');
|
||||
},
|
||||
|
||||
_isLoading: function () {
|
||||
return this.model.get('state') === LOADING;
|
||||
},
|
||||
|
||||
_finish: function (e) {
|
||||
this.killEvent(e);
|
||||
this.model.set('state', LOADING);
|
||||
this.trigger('finish', this);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<button class="CDB-Button CDB-Button--loading CDB-Button--primary js-ok track-ImageExport track-export
|
||||
<% if (isLoading || isDisabled) { %> is-disabled <% } %>
|
||||
<% if (isLoading) { %> is-loading <% } %>
|
||||
"
|
||||
<% if (isDisabled) { %> disabled <% } %>>
|
||||
<div class="CDB-Button-Text CDB-Text is-semibold CDB-Size-small u-upperCase u-flex">
|
||||
<%- _t('editor.maps.export-image.export') %>
|
||||
</div>
|
||||
<div class="CDB-Button-loader CDB-LoaderIcon is-white">
|
||||
<svg class="CDB-LoaderIcon-spinner" viewbox="0 0 50 50">
|
||||
<circle class="CDB-LoaderIcon-path" cx="25" cy="25" r="20" fill="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
Reference in New Issue
Block a user