I need to see the pixel value from a single visible WMS. In my project I've this two WMS:
/// WMS sources and layers
var wms_path = 'https://gis.massimilianomoraca.it/geoserver/MassimilianoMoraca/wms';
var sourceNDVI_20150807 = new ol.source.TileWMS({
url: wms_path,
params: {
'LAYERS': 'NDVI_Campania_20150807',
},
});
var titleNDVI_20150807 = 'NDVI_Campania_20150807';
var layerNDVI_20150807 = new ol.layer.Tile({
title: titleNDVI_20150807,
source: sourceNDVI_20150807,
visible: false
});
var sourceNDVI_20160712 = new ol.source.TileWMS({
url: wms_path,
params: {
'LAYERS': 'NDVI_Campania_20160712',
},
});
var layerNDVI_20160712 = new ol.layer.Tile({
title: 'NDVI_Campania_20160712',
source: sourceNDVI_20160712,
visible: false
});
I'm be able to see on the map this datas. I've created the function below whit the aim to active and deactive the single layer.
<button type="button" class="btn btn-primary"
onclick="NDVI_Campania_20150807()">NDVI_Campania_20150807</button>
<button type="button" class="btn btn-success"
onclick="NDVI_Campania_20160712()">NDVI_Campania_20160712</button>
function NDVI_Campania_20150807() {
console.log('NDVI_Campania_20150807');
map.removeLayer(layerNDVI_20160712);
map.addLayer(layerNDVI_20150807);
layerNDVI_20150807.setVisible(true);
/// Click on pixel
map.on('singleclick', function(evt) {
var coordinate = evt.coordinate;
var resolution = view.getResolution();
var projection = 'EPSG:3857';
var params = {
'INFO_FORMAT': 'application/json',
};
var url_20150807 = sourceNDVI_20150807.getFeatureInfoUrl(
coordinate, resolution, projection, params
);
fetch(url_20150807)
.then(function (response) {
return response.text(); })
.then(function (data) {
json = JSON.parse(data).features[0];
ndvi_20150807 = json.properties.GRAY_INDEX;
date_20150807 = '7 agosto 2015';
index_20150807 = [1,date_20150807,ndvi_20150807]
}).catch((error) => {
console.warn(error)
});
});
};
function NDVI_Campania_20160712() {
console.log('NDVI_Campania_20160712');
map.removeLayer(layerNDVI_20150807);
map.addLayer(layerNDVI_20160712);
layerNDVI_20160712.setVisible(true);
layerNDVI_20150807.setVisible(false);
/// Funzione click pixel
map.on('singleclick', function(evt) {
var coordinate = evt.coordinate;
var resolution = view.getResolution();
var projection = 'EPSG:3857';
var params = {
'INFO_FORMAT': 'application/json',
};
var url_20160712 = sourceNDVI_20160712.getFeatureInfoUrl(
coordinate, resolution, projection, params
);
fetch(url_20160712)
.then(function (response) {
return response.text(); })
.then(function (data) {
var json = JSON.parse(data).features[0];
ndvi_20160712 = json.properties.GRAY_INDEX;
date_20160712 = '12 luglio 2016';
index_20160712 = [2,date_20160712,ndvi_20160712]
console.log(index_20160712);
}).catch((error) => {
console.warn(error)
});
});
};
I can active and deactive the layers but if I click on the pixel I see the datas from both layers. How I can see the pixel value from the active layer?
In your button click handlers only change the layers
function NDVI_Campania_20150807() {
console.log('NDVI_Campania_20150807');
map.removeLayer(layerNDVI_20160712);
map.addLayer(layerNDVI_20150807);
layerNDVI_20150807.setVisible(true);
layerNDVI_20160712.setVisible(false);
};
function NDVI_Campania_20160712() {
console.log('NDVI_Campania_20160712');
map.removeLayer(layerNDVI_20150807);
map.addLayer(layerNDVI_20160712);
layerNDVI_20160712.setVisible(true);
layerNDVI_20150807.setVisible(false);
};
and set a single map click listener which queries whichever layer is visible
map.on('singleclick', function(evt) {
var coordinate = evt.coordinate;
var resolution = view.getResolution();
var projection = 'EPSG:3857';
var params = {
'INFO_FORMAT': 'application/json',
};
if (layerNDVI_20150807.getVisible()) {
var url_20150807 = sourceNDVI_20150807.getFeatureInfoUrl(
coordinate, resolution, projection, params
);
fetch(url_20150807)
.then(function (response) {
return response.text(); })
.then(function (data) {
json = JSON.parse(data).features[0];
ndvi_20150807 = json.properties.GRAY_INDEX;
date_20150807 = '7 agosto 2015';
index_20150807 = [1,date_20150807,ndvi_20150807]
}).catch((error) => {
console.warn(error)
});
} else if (layerNDVI_20160712.getVisible()) {
var url_20160712 = sourceNDVI_20160712.getFeatureInfoUrl(
coordinate, resolution, projection, params
);
fetch(url_20160712)
.then(function (response) {
return response.text(); })
.then(function (data) {
var json = JSON.parse(data).features[0];
ndvi_20160712 = json.properties.GRAY_INDEX;
date_20160712 = '12 luglio 2016';
index_20160712 = [2,date_20160712,ndvi_20160712]
console.log(index_20160712);
}).catch((error) => {
console.warn(error)
});
}
});
Related
I am trying to create a cluster map with multiple locations in the same area. Now, I have this pop-up with data which is coming from my API. I have a click event that toggles the pop-up on click for each location. But the problem is even when I specified the click unique ID to toggle it still opens all the pop-ups. Also, the zoom is not working for the clusters count circles.
This is my code:
import mapboxgl from 'mapbox-gl';
import MapboxLanguage from '#mapbox/mapbox-gl-language';
import apiFetch from '#wordpress/api-fetch';
(() => {
const mapContainer = document.querySelector('[data-gewoon-wonen]');
if (!mapContainer) {
return;
}
mapboxgl.accessToken = process.env.MAP_TOKEN_KEY;
const filterGroup = document.getElementById('map-filter');
const resetButton = filterGroup.querySelector('.hidden');
const center = [4.387733, 51.862419];
const locations = {
type: 'FeatureCollection',
features: []
};
apiFetch({ path: '/wp-json/wp/v2/map?_embed' }).then((maps) => {
maps.forEach((item) => {
const {
id,
title: { rendered: title },
_embedded,
acf
} = item;
const image =
_embedded && _embedded['wp:featuredmedia'][0]?.source_url;
const {
map_location_subtitle: subtitle,
map_location_delivery: delivery,
map_location_project: project,
map_location_content: description,
map_location_coordinates_lat: lat,
map_location_coordinates_lng: lng,
map_location_status: status,
map_location_website: website
} = acf;
const getStatus = (currentStatus) => {
let statusObj = {
bouwfase: 'marker-gray',
planfase: 'marker-bright-pink',
opgeleverd: 'marker-bright-blue',
default: ''
};
let icon = statusObj[currentStatus] || statusObj['default'];
return icon;
};
const object = {
type: 'Feature',
properties: {
id,
status,
image,
icon: getStatus(status),
title,
subtitle,
project,
website,
delivery,
description
},
geometry: {
type: 'Point',
coordinates: [lng, lat]
}
};
locations.features.push(object);
});
});
const map = new mapboxgl.Map({
container: mapContainer,
style: 'mapbox://styles/.../clcz9eocm000p14o3vh42tqfj',
center,
zoom: 10,
minZoom: 10,
maxZoom: 18,
attributionControl: false,
cooperativeGestures: true
});
map.addControl(
new MapboxLanguage({
defaultLanguage: 'mul'
})
);
const resetActiveItems = () => {
document
.querySelectorAll('.active')
.forEach((e) => e.classList.remove('active'));
};
const closePopUps = () => {
const popups = document.getElementsByClassName('mapboxgl-popup');
if (popups.length) {
popups[0].remove();
}
};
map.on('load', () => {
map.addSource('locations', {
type: 'geojson',
data: locations,
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)s
});
locations.features.forEach((location) => {
const {
description,
delivery,
title,
subtitle,
project,
status,
image,
website,
id
} = location.properties;
const { coordinates } = location.geometry;
const layerID = `status-${status}-${id}`;
// Add a layer for this symbol type if it hasn't been added already.
if (!map.getLayer(layerID)) {
map.addLayer({
id: layerID,
type: 'symbol',
source: 'locations',
layout: {
'icon-image': ['get', 'icon'],
'icon-size': 0.9,
'icon-allow-overlap': true
},
// filter: ['==', 'id', id]
filter: ['!', ['has', 'point_count']]
});
map.addLayer({
id: `clusters-${id}`,
type: 'circle',
source: 'locations',
filter: ['has', 'point_count'],
paint: {
// Use step expressions (https://docs.mapbox.com/mapbox-gl-js/style-spec/#expressions-step)
// with three steps to implement three types of circles:
// * Blue, 20px circles when point count is less than 100
// * Yellow, 30px circles when point count is between 100 and 750
// * Pink, 40px circles when point count is greater than or equal to 750
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100,
'#f1f075',
750,
'#f28cb1'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
100,
30,
750,
40
]
}
});
map.addLayer({
id: `cluster-count-${id}`,
type: 'symbol',
source: 'locations',
filter: ['has', 'point_count'],
layout: {
'text-field': ['get', 'point_count_abbreviated'],
'text-font': [
'DIN Offc Pro Medium',
'Arial Unicode MS Bold'
],
'text-size': 12
}
});
}
const hasImage = image
? `<img src=${image} class="map__image" />`
: '';
const statusClass = image ? 'map__status--overlay' : '';
const popup = new mapboxgl.Popup({
offset: 25,
maxWidth: null,
className: image ? 'map__content--image' : ''
});
const content = `
${hasImage}
<span class="map__status map__status--${status} ${statusClass}">${status}</span>
<h4 class="map__title">${title}</h4>
<p class="map__subtitle">${subtitle}</p>
<p class="map__subtitle">${
delivery ? `Oplevering ${delivery}` : ''
}</p>
<p class="map__project">${project}</p>
<p class="map__info">${description}</p>
Meer informatie
`;
map.on('click', layerID, (e) => {
// Copy coordinates array.
const coordinateSlice = coordinates.slice();
resetButton.classList.remove('hidden');
// Ensure that if the map is zoomed out such that multiple
// copies of the feature are visible, the popup appears
// over the copy being pointed to.
while (Math.abs(e.lngLat.lng - coordinateSlice[0]) > 180) {
coordinateSlice[0] +=
e.lngLat.lng > coordinateSlice[0] ? 360 : -360;
}
popup.setLngLat(coordinates).setHTML(content).addTo(map);
});
// Change the cursor to a pointer when the mouse is over the places layer.
map.on('mouseenter', layerID, () => {
map.getCanvas().style.cursor = 'pointer';
});
// Change it back to a pointer when it leaves.
map.on('mouseleave', layerID, () => {
map.getCanvas().style.cursor = '';
});
});
const statusTypes = locations.features.map(
(feature) => feature.properties.status
);
const uniqueStatusTypes = Array.from(new Set(statusTypes));
const newGeoJSON = { ...locations };
resetButton.addEventListener('click', () => {
newGeoJSON.features = [...locations.features];
map.getSource('locations').setData(newGeoJSON);
map.setZoom(10).setCenter(center);
resetButton.classList.add('hidden');
resetActiveItems();
closePopUps();
});
const sortedStatusTypes = [
uniqueStatusTypes[0],
uniqueStatusTypes[2],
uniqueStatusTypes[1]
].filter((status) => status !== undefined && status !== null);
sortedStatusTypes.forEach((statusType) => {
const input = document.createElement('input');
input.value = statusType;
input.name = statusType;
input.type = 'button';
input.className = `status--${statusType}`;
input.innerText = statusType;
filterGroup.appendChild(input);
input.onclick = () => {
const statusType = input.value;
resetActiveItems();
closePopUps();
if (statusType) {
input.classList.add('active');
resetButton.classList.remove('hidden');
newGeoJSON.features = locations.features.filter(
(feature) => feature.properties.status === statusType
);
}
map.getSource('locations').setData(newGeoJSON);
};
});
// disable map rotation using right click + drag
map.dragRotate.disable();
// disable map rotation using touch rotation gesture
map.touchZoomRotate.disableRotation();
// Add zoom and rotation controls to the map.
const nav = new mapboxgl.NavigationControl({
showCompass: false
});
const fullscreen = new mapboxgl.FullscreenControl();
map.addControl(fullscreen, 'top-right');
map.addControl(nav, 'bottom-right');
});
})();
I have 2 main function on my project, 1 to display information, vectors,layers, etc when page loads and another one to display information depending what the user want to see.
I am calling geoserver to display geotiff as imageWMS , also i am displaying band values from this geotiff on geoserver using this function showBand:
eventRaster = (f) => {
rasterWMS = new ol.source.ImageWMS({
url: 'https://url/geoserver/name/wms',
params: {
'LAYERS': 'name:' + f
},
ratio: 1,
serverType: 'geoserver'
})
return rasterWMS;
}
showBand = (map, vista, eventRaster) => {
map.on('singleclick', function (evt) {
/* document.querySelector('featureInfo > tr').classList.add("test") */
const viewResolution = /** #type {number} */ (vista.getResolution());
const url = eventRaster.getFeatureInfoUrl(
evt.coordinate,
viewResolution,
'EPSG:3857',
{ 'INFO_FORMAT': 'text/html' }
);
if (url) {
fetch(url)
.then((response) => response.text())
.then((html) => {
document.getElementById('showBand').innerHTML = html;
const pga = document.querySelector(".featureInfo tbody tr:nth-child(2) td:nth-child(2)").textContent;
const floatPga = parseFloat(pga).toFixed(2);
if (floatPga < 0) {
document.getElementById('showBand').innerHTML = "Da click en la una zona vĂ¡lida";
} else {
document.getElementById('showBand').innerHTML = floatPga + " gal";
}
});
}
});
map.on('pointermove', function (evt) {
if (evt.dragging) {
return;
}
const pixel = map.getEventPixel(evt.originalEvent);
const hit = map.forEachLayerAtPixel(pixel, function () {
return true;
});
/* map.getTargetElement().style.cursor = hit ? 'pointer' : ''; */
});
}
So i have this on my functions:
firstLoad(){
const f = sometiffname;
lastTiffRaster = new ol.layer.Image({
title: 'PGA(gal)',
source: eventRaster(f),
});
map.addLayer(lastTiffRaster);
}
selected(){
const f = sometiffname;
map.removeLayer(lastTiffRaster);
map.removeLayer(eventTiff);
rasterWMS.updateParams({
'LAYERS': 'name:'+f,
});
evetTiff = new ol.layer.Image({
title: 'PGA(gal)',
source: eventRaster(f),
});
map.addLayer(eventTiff);
}
There is not problem when page loads for first time and click the first event i want to see because params update perfectly, and i can see band values without problems, but when i try to call another event (the second one), geotiff changes, but params does not update. i print on console f to see whats the value and it changes per event but i dont know why updateParams doesnt work on this situation.
yellow marker is f
I had to create a new imagewms for selected() then update imagewms from firstload()
selected(){
const f = sometiffname;
const newRasterWMS = new ol.source.ImageWMS({
url: 'https://url.pe/geoserver/somename/wms',
params: {
'LAYERS': 'somename:' + f
},
ratio: 1,
serverType: 'geoserver'
});
rasterWMS.updateParams({
'LAYERS': 'somename:'+f,
});
eventTiff = new ol.layer.Image({
title: 'PGA(gal)',
source: newRasterWMS
});
map.addLayer(lastTiffRaster);
}
According to file account/static/src/js/reconciliation_model.js in Odoo module, there is an object assignment :
var StatementModel = BasicModel.extend({
...
...
...
load: function (context) {
var self = this;
var statement_ids = context.statement_ids;
if (!statement_ids) {
return $.when();
}
this.context = context;
var def_statement = this._rpc({
model: 'account.bank.statement',
method: 'reconciliation_widget_preprocess',
args: [statement_ids],
})
.then(function (statement) {
self.statement = statement;
self.bank_statement_id = statement_ids.length === 1 ? {id: statement_ids[0], display_name: statement.statement_name} : false;
self.valuenow = 0;
self.valuemax = statement.st_lines_ids.length;
self.context.journal_id = statement.journal_id;
_.each(statement.st_lines_ids, function (id) {
self.lines[_.uniqueId('rline')] = {
id: id,
reconciled: false,
mode: 'inactive',
mv_lines: [],
offset: 0,
filter: "",
reconciliation_proposition: [],
reconcileModels: [],
};
});
});
var def_reconcileModel = this._rpc({
model: 'account.reconcile.model',
method: 'search_read',
})
.then(function (reconcileModels) {
self.reconcileModels = reconcileModels;
});
var def_account = this._rpc({
model: 'account.account',
method: 'search_read',
fields: ['code'],
})
.then(function (accounts) {
self.accounts = _.object(_.pluck(accounts, 'id'), _.pluck(accounts, 'code'));
});
return $.when(def_statement, def_reconcileModel, def_account).then(function () {
_.each(self.lines, function (line) {
line.reconcileModels = self.reconcileModels;
});
var ids = _.pluck(self.lines, 'id');
ids = ids.splice(0, self.defaultDisplayQty);
self.pagerIndex = ids.length;
return self.loadData(ids, []);
});
},
...
...
...
});
I want to change the statement :
var def_statement = this._rpc({
model: 'account.bank.statement',
method: 'reconciliation_widget_preprocess',
args: [statement_ids],
})
to
var def_statement = this._rpc({
model: 'account.bank.statement',
method: 'reconciliation_widget_preprocess_with_line',
args: [statement_ids, statement_line_ids],
})
My code is something like this :
odoo.define('my_accounting.ReconciliationModel', function (require) {
"use strict";
var BasicModel = require('web.BasicModel');
var field_utils = require('web.field_utils');
var utils = require('web.utils');
var session = require('web.session');
var CrashManager = require('web.CrashManager');
var core = require('web.core');
var _t = core._t;
var ReconciliationModel = require('account.ReconciliationModel');
var StatementModel = ReconciliationModel.StatementModel;
var MyStatementModel = StatementModel.extend({
load: function (context) {
var self = this;
var statement_ids = context.statement_ids;
if (!statement_ids) {
return $.when();
}
var statement_line_ids = context.statement_line_ids;
this.context = context;
var def_statement = this._rpc({
model: 'account.bank.statement',
method: 'reconciliation_widget_preprocess_with_line',
args: [statement_ids, statement_line_ids],
})
.then(function (statement) {
self.statement = statement;
self.bank_statement_id = statement_ids.length === 1 ? {id: statement_ids[0], display_name: statement.statement_name} : false;
self.valuenow = 0;
self.valuemax = statement.st_lines_ids.length;
self.context.journal_id = statement.journal_id;
_.each(statement.st_lines_ids, function (id) {
self.lines[_.uniqueId('rline')] = {
id: id,
reconciled: false,
mode: 'inactive',
mv_lines: [],
offset: 0,
filter: "",
reconciliation_proposition: [],
reconcileModels: [],
};
});
});
var domainReconcile = [];
if (context && context.company_ids) {
domainReconcile.push(['company_id', 'in', context.company_ids]);
}
if (context && context.active_model === 'account.journal' && context.active_ids) {
domainReconcile.push(['journal_id', 'in', [false].concat(context.active_ids)]);
}
var def_reconcileModel = this._rpc({
model: 'account.reconcile.model',
method: 'search_read',
domain: domainReconcile,
})
.then(function (reconcileModels) {
self.reconcileModels = reconcileModels;
});
var def_account = this._rpc({
model: 'account.account',
method: 'search_read',
fields: ['code'],
})
.then(function (accounts) {
self.accounts = _.object(_.pluck(accounts, 'id'), _.pluck(accounts, 'code'));
});
return $.when(def_statement, def_reconcileModel, def_account).then(function () {
_.each(self.lines, function (line) {
line.reconcileModels = self.reconcileModels;
});
var ids = _.pluck(self.lines, 'id');
ids = ids.splice(0, self.defaultDisplayQty);
self.pagerIndex = ids.length;
return self.loadData(ids, []);
});
}
});
});
It not working well. I've performed upgrade my module and still call reconciliation_widget_preprocess method instead of reconciliation_widget_preprocess_with_line in my Odoo module.
Can someone tell me what I missing? I'm using Odoo 11 community edition. I thanks to you for any clue.
You need to use include method when Patching an existing class.
var Hamster = require('web.Hamster');
Hamster.include({
sleep: function () {
this._super.apply(this, arguments);
console.log('zzzz');
},
});
I am new to Ember and want a Utility class in Ember which does the following, takes in rowItems and returns an object (finalMeta)
var myMeta1 = new Array();
var myMeta2 = new Array();
dojo.forEach(rowItems, dojo.hitch(this, function(rowItem){
var metaData = {
Id: rowItem.Id,
version: rowItem.version
};
if(rowItem.tranMetaData.tpl){
myMeta2.push(metaData);
}else{
myMeta1.push(metaData);
}
}));
if(myMeta1.length == 0){
myMeta1 = null;
}
if(myMeta2.length == 0){
myMeta2 = null;
}
var finalMeta = {
"myMeta1": myMeta1,
"myMeta2": myMeta2
};
return finalMeta;
Where/How do I write this Utility class, such that it can be accessed from a different place (say from a different route) ?
Just to add, I want to use the finalMeta in a child route (part of some workflow) as inputs/request params to some API.
In the child route, I would then make an AJAX call,
Ember.$.ajax({
url: someUrl,
type: "POST",
data: JSON.stringify({
'ids': idKeys,
'metaData': finalMeta
}),
})
Two solutions come to mind. The first is probably the simplest to implement. The second might technically be more object oriented, but introduces another class with a very limited purpose.
The easy way: Include this as a method in your API service object:
function SomeApiService() {
}
SomeApiService.prototype = {
constructor: SomeApiService,
saveSomething: function(rows) {
var finalMeta = this.getMetaData(rows);
var idKeys = // create array of id keys
Ember.$.ajax({
url: someUrl,
type: "POST",
data: JSON.stringify({
'ids': idKeys,
'metaData': finalMeta
}),
});
},
doSomethingElse: function(rows) {
var finalMeta = this.getMetaData(rows);
Ember.$.ajax({
...,
data: JSON.stringify({
metaData: finalMeta
})
});
},
getMetaData: function(rowItems) {
var myMeta1 = [];
var myMeta2 = [];
dojo.forEach(rowItems, dojo.hitch(this, function(rowItem){
var metaData = {
Id: rowItem.Id,
version: rowItem.version
};
if(rowItem.tranMetaData.tpl){
myMeta2.push(metaData);
}else{
myMeta1.push(metaData);
}
}));
if(myMeta1.length == 0){
myMeta1 = null;
}
if(myMeta2.length == 0){
myMeta2 = null;
}
var finalMeta = {
"myMeta1": myMeta1,
"myMeta2": myMeta2
};
return finalMeta;
}
};
Or, roll this into its own helper class and have the API service class use it:
Your API Service class becomes slimmer, but introduces a dependency. You can pass your own metaHelper in the constructor and provide a mock object for testing, but it could default to a new MetaDataHelper object.
function SomeApiService(metaHelper) {
this.metaHelper = metaHelper || new MetaDataHelper();
}
SomeApiService.prototype = {
constructor: SomeApiService,
saveSomething: function(rows) {
var finalMeta = this.metaHelper.getMetaData(rows);
var idKeys = // create array of id keys
Ember.$.ajax({
url: someUrl,
type: "POST",
data: JSON.stringify({
'ids': idKeys,
'metaData': finalMeta
}),
});
},
doSomethingElse: function(rows) {
var finalMeta = this.metaHelper.getMetaData(rows);
Ember.$.ajax({
...,
data: JSON.stringify({
metaData: finalMeta
})
});
}
};
And the MetaDataHelper class doesn't contain much at this point, however you will have separated your concerns and made the meta data helper object testable by itself. This also allows you to write other API service classes that use the MetaDataHelper object to prevent the duplication of this logic.
function MetaDataHelper() {
}
MetaDataHelper.prototype.getMetaData = function(rowItems) {
var myMeta1 = [];
var myMeta2 = [];
dojo.forEach(rowItems, dojo.hitch(this, function(rowItem){
var metaData = {
Id: rowItem.Id,
version: rowItem.version
};
if(rowItem.tranMetaData.tpl){
myMeta2.push(metaData);
}else{
myMeta1.push(metaData);
}
}));
if(myMeta1.length == 0){
myMeta1 = null;
}
if(myMeta2.length == 0){
myMeta2 = null;
}
var finalMeta = {
"myMeta1": myMeta1,
"myMeta2": myMeta2
};
return finalMeta;
};
I've just started using Backbone.js and my test cases are churning up something pretty weird.
In short, what I am experiencing is -- after I call a Backbone Model's constructor, some of the fields in my object seem to come from a previously item. For instance, if I call:
var playlist = new Playlist({
title: playlistTitle,
position: playlists.length,
userId: user.id
});
playlist.get('items').length; //1
however if I do:
var playlist = new Playlist({
title: playlistTitle,
position: playlists.length,
userId: user.id,
items: []
});
playlist.get('items').length; //0
Here's the code:
define(['ytHelper', 'songManager', 'playlistItem'], function (ytHelper, songManager, PlaylistItem) {
'use strict';
var Playlist = Backbone.Model.extend({
defaults: {
id: null,
userId: null,
title: 'New Playlist',
selected: false,
position: 0,
shuffledItems: [],
history: [],
items: []
},
initialize: function () {
//Our playlistItem data was fetched from the server with the playlist. Need to convert the collection to Backbone Model entities.
if (this.get('items').length > 0) {
console.log("Initializing a Playlist object with an item count of:", this.get('items').length);
console.log("items[0]", this.get('items')[0]);
this.set('items', _.map(this.get('items'), function (playlistItemData) {
var returnValue;
//This is a bit more robust. If any items in our playlist weren't Backbone.Models (could be loaded from server data), auto-convert during init.
if (playlistItemData instanceof Backbone.Model) {
returnValue = playlistItemData;
} else {
returnValue = new PlaylistItem(playlistItemData);
}
return returnValue;
}));
//Playlists will remember their length via localStorage w/ their ID.
var savedItemPosition = JSON.parse(localStorage.getItem(this.get('id') + '_selectedItemPosition'));
this.selectItemByPosition(savedItemPosition != null ? parseInt(savedItemPosition) : 0);
var songIds = _.map(this.get('items'), function(item) {
return item.get('songId');
});
songManager.loadSongs(songIds);
this.set('shuffledItems', _.shuffle(this.get('items')));
}
},
//TODO: Reimplemnt using Backbone.sync w/ CRUD operations on backend.
save: function(callback) {
if (this.get('items').length > 0) {
var selectedItem = this.getSelectedItem();
localStorage.setItem(this.get('id') + '_selectedItemPosition', selectedItem.get('position'));
}
var self = this;
console.log("Calling save with:", self);
console.log("my position is:", self.get('position'));
$.ajax({
url: 'http://localhost:61975/Playlist/SavePlaylist',
type: 'POST',
dataType: 'json',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(self),
success: function (data) {
console.log('Saving playlist was successful.', data);
self.set('id', data.id);
if (callback) {
callback();
}
},
error: function (error) {
console.error("Saving playlist was unsuccessful", error);
}
});
},
selectItemByPosition: function(position) {
//Deselect the currently selected item, then select the new item to have selected.
var currentlySelected = this.getSelectedItem();
//currentlySelected is not defined for a brand new playlist since we have no items yet selected.
if (currentlySelected != null && currentlySelected.position != position) {
currentlySelected.set('selected', false);
}
var item = this.getItemByPosition(position);
if (item != null && item.position != position) {
item.set('selected', true);
localStorage.setItem(this.get('id') + '_selectedItemPosition', item.get('position'));
}
return item;
},
getItemByPosition: function (position) {
return _.find(this.get('items'), function(item) {
return item.get('position') == position;
});
},
addItem: function (song, selected) {
console.log("this:", this.get('title'));
var playlistId = this.get('id');
var itemCount = this.get('items').length;
var playlistItem = new PlaylistItem({
playlistId: playlistId,
position: itemCount,
videoId: song.videoId,
title: song.title,
relatedVideos: [],
selected: selected || false
});
this.get('items').push(playlistItem);
this.get('shuffledItems').push(playlistItem);
this.set('shuffledItems', _.shuffle(this.get('shuffledItems')));
console.log("this has finished calling");
//Call save to give it an ID from the server before adding to playlist.
songManager.saveSong(song, function (savedSong) {
song.id = savedSong.id;
playlistItem.set('songId', song.id);
console.log("calling save item");
$.ajax({
type: 'POST',
url: 'http://localhost:61975/Playlist/SaveItem',
dataType: 'json',
data: {
id: playlistItem.get('id'),
playlistId: playlistItem.get('playlistId'),
position: playlistItem.get('position'),
songId: playlistItem.get('songId'),
title: playlistItem.get('title'),
videoId: playlistItem.get('videoId')
},
success: function (data) {
playlistItem.set('id', data.id);
},
error: function (error) {
console.error(error);
}
});
});
return playlistItem;
},
addItemByVideoId: function (videoId, callback) {
var self = this;
ytHelper.getVideoInformation(videoId, function (videoInformation) {
var song = songManager.createSong(videoInformation, self.get('id'));
var addedItem = self.addItem(song);
if (callback) {
callback(addedItem);
}
});
},
//Returns the currently selected playlistItem or null if no item was found.
getSelectedItem: function() {
var selectedItem = _.find(this.get('items'), function (item) {
return item.get('selected');
});
return selectedItem;
}
});
return function (config) {
var playlist = new Playlist(config);
playlist.on('change:title', function () {
this.save();
});
return playlist;
};
});
basically I am seeing the property 'items' is populated inside of initialize when I've passed in a config object that does not specify items at all. If I specify a blank items array in my config object, then there are no items in initialize, but this seems counter-intuitive. Am I doing something wrong?
The problem is with using reference types (arrays) in the defaults object. When a new Playlist model is created without specifying an items value, the default is applied. In case of arrays and objects this is problematic, because essentially what happens is:
newModel.items = defaults.items
And so all models initialized this way refer to the same array. To verify this, you can test:
var a = new Playlist();
var b = new Playlist();
var c = new Playlist({items:[]});
//add an item to a
a.get('items').push('over the rainbow');
console.log(b.get('items')); // -> ['over the rainbow'];
console.log(c.get('items')); // -> []
To get around this problem, Backbone supports defining Model.defaults as a function:
var Playlist = Backbone.Model.extend({
defaults: function() {
return {
id: null,
userId: null,
title: 'New Playlist',
selected: false,
position: 0,
shuffledItems: [],
history: [],
items: []
};
}
});