cartodb-4.42/lib/assets/javascripts/cartodb/models/map.js

881 lines
22 KiB
JavaScript
Raw Normal View History

2024-04-06 13:25:13 +08:00
/*
* extend infowindow to serialize only the data we need
*/
_.extend(cdb.geo.ui.InfowindowModel.prototype, {
toJSON: function() {
var fields = [];
if (!this.attributes.disabled) {
fields = _.clone(this.attributes.fields);
}
return {
fields: fields,
template_name: this.attributes.template_name,
template: this.attributes.template,
alternative_names: this.attributes.alternative_names,
old_fields: this.attributes.old_fields,
old_template_name: this.attributes.old_template_name,
width: this.attributes.width,
maxHeight: this.attributes.maxHeight
};
},
removeMissingFields: function(columns) {
var columnsSet = {}
for(var i = 0; i < columns.length; ++i) {
var c = columns[i];
columnsSet[c] = true;
}
var fields = this.get('fields');
if (!fields) {
return;
}
for(var i = 0; i < fields.length; ++i) {
var name = fields[i].name;
if (! (name in columnsSet)) {
this.removeField(name);
}
}
},
addMissingFields: function(columns) {
var fieldsSet = {};
var fields = this.get('fields');
for(var i = 0; i < fields.length; ++i) {
var c = fields[i].name;
fieldsSet[c] = true;
}
for(var i = 0; i < columns.length; ++i) {
var name = columns[i];
if (! (name in fieldsSet)) {
this.addField(name);
}
}
},
mergeFields: function(columns) {
// remove fields that no longer exist
this.removeMissingFields(columns);
// add new fields that exists
this.addMissingFields(columns);
},
// return the list of columns involved in the infowindow
// ready to set interactivity in a cartodb layer
getInteractivity: function() {
var fields = this.get('fields') || [];
var columns = [];
for(var i = 0; i < fields.length; ++i) {
columns.push(fields[i].name);
}
return columns;
}
});
/**
* extend gmaps layer for data serialization
*/
cdb.admin.GMapsBaseLayer = cdb.geo.GMapsBaseLayer.extend({
clone: function() {
return new cdb.admin.GMapsBaseLayer(_.clone(this.attributes));
},
parse: function(data) {
var c = {};
_.extend(c, data.options, {
id: data.id,
type: 'GMapsBase',
order: data.order,
parent_id: data.parent_id
});
return c;
},
toJSON: function() {
var c = _.clone(this.attributes);
var d = {
kind: 'gmapsbase',
options: c,
order: c.order
};
if(c.id !== undefined) {
d.id = c.id;
}
return d;
}
});
/**
* extend wms layer for data serialization
*/
cdb.admin.WMSLayer = cdb.geo.WMSLayer.extend({
clone: function() {
return new cdb.admin.WMSLayer(_.clone(this.attributes));
},
/*
* Create className from the urlTemplate of the basemap
*/
_generateClassName: function(urlTemplate) {
if (urlTemplate) {
var className = urlTemplate;
if (className && parseInt(className) && _.isNumber(parseInt(className))) {
className = "w" + className;
}
return className.replace(/\s+/g, '').replace(/[^a-zA-Z_0-9 ]/g, "").toLowerCase();
} else return "";
},
parse: function(data) {
var self = this;
var c = {};
_.extend(c, data.options, {
id: data.id,
className: self._generateClassName(data.options.layers),
type: 'WMS',
order: data.order,
parent_id: data.parent_id
});
return c;
},
toJSON: function() {
var c = _.clone(this.attributes);
var d = {
kind: 'wms',
options: c,
order: c.order
};
if(c.id !== undefined) {
d.id = c.id;
}
return d;
}
});
/**
* extend plain layer for data serialization
*/
cdb.admin.PlainLayer = cdb.geo.PlainLayer.extend({
parse: function(data) {
var c = {};
_.extend(c, data.options, {
id: data.id,
type: 'Plain',
order: data.order,
parent_id: data.parent_id
});
return c;
},
toJSON: function() {
var c = _.clone(this.attributes);
var d = {
kind: 'background',
options: c,
order: c.order
};
if(c.id !== undefined) {
d.id = c.id;
}
return d;
}
});
/**
* extend tiled layer to adapt serialization
*/
cdb.admin.TileLayer = cdb.geo.TileLayer.extend({
clone: function() {
return new cdb.admin.TileLayer(_.clone(this.attributes));
},
/*
* Create className from the urlTemplate of the basemap
*/
_generateClassName: function(urlTemplate) {
if (urlTemplate) {
return urlTemplate.replace(/\s+/g, '').replace(/[^a-zA-Z_0-9 ]/g, "").toLowerCase();
} else return "";
},
parse: function(data) {
var self = this;
var c = {};
_.extend(c, data.options, {
id: data.id,
className: self._generateClassName(data.options.urlTemplate),
type: 'Tiled',
order: data.order,
parent_id: data.parent_id
});
return c;
},
toJSON: function() {
var c = _.clone(this.attributes);
var d = {
kind: 'tiled',
options: c,
order: c.order
};
if(c.id !== undefined) {
d.id = c.id;
}
return d;
},
/**
* validateTemplateURL - Validates current urlTemplate of layer.
*
* @param {Object} callbacks with success and error functions defined to be called depending on validation outcome.
*/
validateTemplateURL: function(callbacks) {
var subdomains = ['a', 'b', 'c'];
var image = new Image();
image.onload = callbacks.success;
image.onerror = callbacks.error;
image.src = this.get('urlTemplate').replace(/\{s\}/g, function() {
return subdomains[Math.floor(Math.random() * 3)];
})
.replace(/\{x\}/g, '0')
.replace(/\{y\}/g, '0')
.replace(/\{z\}/g, '0');
}
}, {
/**
* @param {String} url
* @param {Boolean} tms
* @return {cdb.admin.TileLayer}
*/
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 cdb.admin.TileLayer({
urlTemplate: url,
attribution: null,
maxZoom: 21,
minZoom: 0,
name: '',
tms: tms
});
layer.set('className', layer._generateClassName(url));
return layer;
}
});
cdb.admin.TorqueLayer = cdb.admin.CartoDBLayer.extend({
/*parse: function(data, options) {
var c = cdb.admin.CartoDBLayer.prototype.parse.call(this, data, options);
c.type = 'torque';
return c;
}*/
});
cdb.admin.Layers = cdb.geo.Layers.extend({
_DATA_LAYERS: ['CartoDB', 'torque'],
// the model class works here like a factory
// depending on the kind of layer creates a
// type of layer or other
model: function(attrs, options) {
var typeClass = {
'Tiled': cdb.admin.TileLayer,
'CartoDB': cdb.admin.CartoDBLayer,
'Plain': cdb.admin.PlainLayer,
'GMapsBase': cdb.admin.GMapsBaseLayer,
'WMS': cdb.admin.WMSLayer,
'torque': cdb.admin.CartoDBLayer
};
var typeMap = {
'Layer::Tiled': 'Tiled',
'Layer::Carto': 'CartoDB',
'Layer::Background': 'Plain',
'tiled': 'Tiled',
'carto': 'CartoDB',
'wms': 'WMS',
'background': 'Plain',
'gmapsbase': 'GMapsBase',
'torque': 'torque'
};
return new typeClass[typeMap[attrs.kind]](attrs, options);
},
initialize: function() {
this.bind('change:order', function() {
if (!this._isSorted()) this.sort();
});
cdb.geo.Layers.prototype.initialize.call(this);
},
add: function(models, options) {
return Backbone.Collection.prototype.add.apply(this, arguments);
},
getTorqueLayers: function() {
return this.where({ type: 'torque' });
},
getTiledLayers: function() {
return this.where({ type: 'Tiled' });
},
// given layer model returns the index inside the layer definition
getLayerDefIndex: function(layer) {
var cartodbLayers = this.getLayersByType('CartoDB');
if(!cartodbLayers.length) return -1;
for(var i = 0, c = 0; i < cartodbLayers.length; ++i) {
if(cartodbLayers[i].get('visible')) {
if(cartodbLayers[i].cid === layer.cid) {
return c;
}
++c;
}
}
return -1;
},
getLayerDef: function() {
var cartodbLayers = this.getLayersByType('CartoDB');
var layerDef = {
version:'1.0.1',
layers: []
};
for(var i = 0; i < cartodbLayers.length; ++i) {
if(cartodbLayers[i].get('visible')) {
layerDef.layers.push(cartodbLayers[i].getLayerDef());
}
}
return layerDef;
},
/** return non-base layers */
getDataLayers: function() {
var self = this;
return this.filter(function(lyr) {
return _.contains(self._DATA_LAYERS, lyr.get('type'));
});
},
/** without non-base layers */
getTotalDataLayers: function() {
return this.getDataLayers().length;
},
/** without non-base layers */
getTotalDataLegends: function() {
var self = this;
return this.filter(function(lyr) {
return _.contains(self._DATA_LAYERS, lyr.get('type')) &&
lyr.get('legend') &&
lyr.get('legend').type &&
lyr.get('legend').type.toLowerCase() !== "none";
}).length;
},
getLayersByType: function(type) {
if (!type || type === '' ) {
cdb.log.info("a layer type is necessary to get layers");
return 0;
}
return this.filter(function(lyr) {
return lyr.get('type') === type;
});
},
isLayerOnTopOfDataLayers: function(layer) {
var dataLayerOnTop = this.getDataLayers().splice(-1)[0];
return dataLayerOnTop.cid === layer.cid;
},
url: function(method) {
var version = cdb.config.urlVersion('layer', method);
return '/api/' + version + '/maps/' + this.map.id + '/layers';
},
parse: function(data) {
return data.layers;
},
saveLayers: function(opts) {
opts = opts || {};
this.save(null, opts);
},
save: function(attrs, opts) {
Backbone.sync('update', this, opts);
},
toJSON: function(options) {
// We can't use the default toJSON because it uses this.map(function(){...})
// function within it but we override it using map containing all map stuff there.
// And we have to send all layers data within a variable called layers.
var array = _.map(this.models, function(model) {
return model.toJSON(options);
});
return { layers: array }
},
clone: function(layers) {
layers = layers || new cdb.admin.Layers();
this.each(function(layer) {
if(layer.clone) {
var lyr = layer.clone();
lyr.unset('id');
layers.add(lyr);
} else {
var attrs = _.clone(layer.attributes);
delete attrs.id;
layers.add(attrs);
}
});
return layers;
},
_isSorted: function() {
var sorted = true;
var layers = _(this.models).map(function(m) {
return { cid: m.cid, order: m.get('order')}
});
layers.sort(function(a, b) {
return a.order - b.order;
})
return _.isEqual(
_(layers).map(function(m) { return m.cid; }),
_(this.models).map(function(m) { return m.cid; })
)
},
moveLayer: function(movingLayer, options) {
options = options || {};
var newIndex = options.to;
var layerAtNewIndex = this.at(newIndex);
movingLayer.set('order', layerAtNewIndex.get('order'), { silent: true });
// Remove and add the layer again at the new position
this.remove(movingLayer, { silent: true });
this.add(movingLayer, { at: newIndex, silent: true });
// Update the order of all layers
for (var i = 0; i < this.size(); i++) {
var layer = this.at(i);
layer.set('order', i);
}
this.trigger('reset');
this.saveLayers({
complete: options.complete,
error: function() {
throw 'Error saving layers after moving them'
}
});
}
});
/**
* this is a specialization of generic map prepared to hold two layers:
* - a base layer
* - a data layer which contains the table data
*
* cartodb only supports one data layer per map so this will change when
* that changes
*/
cdb.admin.Map = cdb.geo.Map.extend({
urlRoot: '/api/v1/maps',
initialize: function() {
this.constructor.__super__.initialize.apply(this);
this.sync = Backbone.delayedSaveSync(Backbone.syncAbort, 500);
this.bind('change:id', this._fetchLayers, this);
this.layers = new cdb.admin.Layers();
this.layers.map = this;
this.layers.bind('reset add change', this._layersChanged, this);
this.layers.bind('reset add remove change:attribution', this._updateAttributions, this);
},
saveLayers: function(opts) {
opts = opts || {};
var none = function() {}
this.layers.saveLayers({
success: opts.success || none,
error: opts.error || none
});
},
_layersChanged: function() {
if(this.layers.size() >= 1) {
this._adjustZoomtoLayer(this.layers.at(0));
if(this.layers.size() >= 2) {
this.set({ dataLayer: this.layers.at(1) });
}
}
},
// fetch related layers
_fetchLayers: function() {
this.layers.fetch();
},
/**
* link to a table
*/
relatedTo: function(table) {
this.table = table;
this.table.bind('change:map_id', this._fetchOrCreate, this);
},
parse: function(data) {
data.bounding_box_ne = JSON.parse(data.bounding_box_ne);
data.bounding_box_sw = JSON.parse(data.bounding_box_sw);
data.view_bounds_ne = JSON.parse(data.view_bounds_ne);
data.view_bounds_sw = JSON.parse(data.view_bounds_sw);
data.center = JSON.parse(data.center);
return data;
},
_fetchOrCreate: function() {
var self = this;
var map_id = this.table.get('map_id');
if(!map_id) {
this.create();
} else {
this.set({ id: map_id });
this.fetch({
error: function() {
cdb.log.info("creating map for table");
self.create();
}
});
}
},
/**
* change base layer and save all the layers to preserve the order
*/
setBaseLayer: function(baseLayer) {
this.trigger('savingLayers');
// Check if the selected base layer is already selected
if (this.isBaseLayerAdded(baseLayer)) {
this.trigger('savingLayersFinish');
return false;
}
var self = this;
var newBaseLayer = baseLayer;
var currentBaseLayer = this.layers.at(0);
var newBaseLayerHasLabels = newBaseLayer.get('labels') && newBaseLayer.get('labels').urlTemplate;
// Sets the base layer
var options = {
success: function() {
if (!newBaseLayerHasLabels) {
self.trigger('savingLayersFinish');
}
},
error: function() {
cdb.log.error("error changing the basemap");
self.trigger('savingLayersFinish');
}
}
if (currentBaseLayer) {
if (currentBaseLayer.get('type') === newBaseLayer.get('type')) {
this._updateBaseLayer(currentBaseLayer, newBaseLayer, options);
} else {
this._replaceBaseLayer(currentBaseLayer, newBaseLayer, options);
}
} else {
this._addBaseLayer(newBaseLayer, options);
}
// Adds/updates/removes layer with labels at the top
options.success = function() {
self.trigger('savingLayersFinish');
}
if (newBaseLayerHasLabels) {
if (this._hasLabelsLayer()) {
this._updateLabelsLayer(newBaseLayer, options);
} else {
this._addLabelsLayer(newBaseLayer, options);
}
} else {
if (this._hasLabelsLayer()) {
this._destroyLabelsLayer(options);
}
}
return newBaseLayer;
},
_updateBaseLayer: function(currentBaseLayer, newBaseLayer, opts) {
var newAttributes = _.extend(_.clone(newBaseLayer.attributes), {
id: currentBaseLayer.get('id'),
order: currentBaseLayer.get('order')
});
currentBaseLayer.clear({ silent: true });
currentBaseLayer.set(newAttributes);
currentBaseLayer.save(null, opts);
},
_replaceBaseLayer: function(currentBaseLayer, newBaseLayer, opts) {
this.layers.remove(currentBaseLayer);
newBaseLayer.set({
id: currentBaseLayer.get('id'),
order: currentBaseLayer.get('order')
});
this.layers.add(newBaseLayer, { at: 0 });
newBaseLayer.save(null, opts);
},
_addBaseLayer: function(newBaseLayer, opts) {
this.layers.add(newBaseLayer, { at: 0 });
newBaseLayer.save(null, opts);
},
_hasLabelsLayer: function() {
return this.layers.size() > 1 && this.layers.last().get('type') === 'Tiled';
},
_updateLabelsLayer: function(baseLayer, opts) {
var labelsLayer = this.layers.last();
labelsLayer.set({
name: this._labelsLayerNameFromBaseLayer(baseLayer),
urlTemplate: baseLayer.get('labels').urlTemplate,
attribution: baseLayer.get('attribution'),
minZoom: baseLayer.get('minZoom'),
maxZoom: baseLayer.get('maxZoom'),
subdomains: baseLayer.get('subdomains')
});
labelsLayer.save(null, opts);
},
_addLabelsLayer: function(baseLayer, opts) {
this.layers.add({
name: this._labelsLayerNameFromBaseLayer(baseLayer),
urlTemplate: baseLayer.get('labels').urlTemplate,
attribution: baseLayer.get('attribution'),
minZoom: baseLayer.get('minZoom'),
maxZoom: baseLayer.get('maxZoom'),
subdomains: baseLayer.get('subdomains'),
kind: "tiled"
});
var labelsLayer = this.layers.last();
labelsLayer.save(null, opts);
},
_destroyLabelsLayer: function(opts) {
this.layers.last().destroy(opts);
},
_labelsLayerNameFromBaseLayer: function(baseLayer) {
return baseLayer.get('name') + " Labels";
},
/**
* the first version of cartodb contains one single layer
* per table with information.
*/
addDataLayer: function(lyr) {
this.addLayer(lyr);
this.set({ dataLayer: lyr });
},
/**
* create a new map. this is a helper to use from javascript command line
*/
create: function() {
this.unset('id');
this.set({ table_id: this.table.id });
this.save();
},
/**
* enable save map each time the viewport changes
* not working
*/
autoSave: function() {
this.bind('change:center', this.save);
this.bind('change:zoom', this.save);
},
toJSON: function() {
var c = _.clone(this.attributes);
// data layer is a helper to work in local
delete c.dataLayer;
return c;
},
/**
* change provider and optionally baselayer
*/
changeProvider: function(provider, baselayer) {
var self = this;
if(baselayer && baselayer.get('id')) {
cdb.log.error("the baselayer should not be saved in the server");
return;
}
var _changeBaseLayer = function() {
if(baselayer) {
self.setBaseLayer(baselayer);
}
}
if(this.get('provider') !== provider) {
this.save({ provider: provider }, {
success: function() {
_changeBaseLayer();
self.change();
},
error: function(e, resp) {
self.error(_t('error switching base layer'), resp);
},
silent: true
});
} else {
_changeBaseLayer();
}
},
isProviderGmaps: function() {
var provider = this.get("provider");
return provider && provider.toLowerCase().indexOf("googlemaps") !== -1
},
clone: function(m) {
m = m || new cdb.admin.Map();
var attrs = _.clone(this.attributes)
delete attrs.id;
m.set(attrs);
// clone lists
m.set({
center: _.clone(this.attributes.center),
bounding_box_sw: _.clone(this.attributes.bounding_box_sw),
bounding_box_ne: _.clone(this.attributes.bounding_box_ne),
view_bounds_sw: _.clone(this.attributes.view_bounds_sw),
view_bounds_ne: _.clone(this.attributes.view_bounds_ne),
attribution: _.clone(this.attributes.attribution)
});
// layers
this.layers.clone(m.layers);
m.layers.map = m;
return m;
},
notice: function(msg, type, timeout) {
this.trigger('notice', msg, type, timeout);
},
error: function(msg, resp) {
var err = resp && JSON.parse(resp.responseText).errors[0];
this.trigger('notice', msg + " " + err, 'error');
},
addCartodbLayerFromTable: function(tableName, userName, opts) {
opts = opts || {};
/*var newLayer = cdb.admin.CartoDBLayer.createDefaultLayerForTable(tableName, userName);
this.layers.add(newLayer);
newLayer.save(null, opts);
*/
var self = this;
var table = new cdb.admin.CartoDBTableMetadata({ id: tableName });
table.fetch({
success: function() {
// Get the layers for the map
var map = new cdb.admin.Map({ id: table.get('map_id') });
map.layers.bind('reset', function() {
var newLayer = map.layers.at(1).clone();
newLayer.unset('order');
function layerReady() {
newLayer.table.unbind('change:geometry_types', layerReady);
// when the layer is torque and there are other torque layers in the map, switch it to a
// simple visualization layer
if (newLayer.wizard_properties.get('type') === 'torque' && self.layers.getTorqueLayers().length) {
newLayer.wizard_properties.active('polygon');
}
// wait: true is used to make sure the layer is not added until confirmed it was added successfully
// pass opts for success/error callbacks to be triggered as expected
self.layers.create(newLayer, _.extend({ wait: true }, opts));
}
// Wait until the layer is totally ready in order to add it to the layers and save it
if (newLayer.isTableLoaded()) {
layerReady();
} else {
newLayer.table.bind('change:geometry_types', layerReady);
newLayer.table.data().fetch();
}
});
map.layers.fetch();
}
});
},
// moves the map to interval [-180, 180]
clamp: function() {
var fmod = function (a,b) { return Number((a - (Math.floor(a / b) * b)).toPrecision(8)); };
var latlng = this.get('center');
var lon = latlng[1];
if(lon < -180 || lon > 180) {
lon = fmod(180 + lon, 360) - 180;
this.set('center', [latlng[0], lon]);
}
return this;
}
});