I'm currently trying to reactively show markers on a Mapbox map. My approach was to observe a collection and while doing so, create a GeoJSON object. Changes in that particular object do not reflect on the map however.
var featureArray = [];
CollectionName.find({}).observe({
added: function(item) {
featureArray.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: [+item.location.coordinates[0], +item.location.coordinates[1]]
},
properties: {
_id: item._id,
}
});
},
changedAt: function(newDocument, oldDocument, atIndex) {
//..
},
removed: function(oldDocument, atIndex) {
//..
}
});
var CollectionGeoJSON = {
'type': 'FeatureCollection',
'features': testmarkers
};
var markers = L.mapbox.featureLayer().setGeoJSON(CollectionGeoJSON);
// add cluster markers and add them to map
My idea was to manually add/remove/change the markers on the client (as changes are synced to the server anyway), however also no success here as I'm not sure how to do that using the Mapbox API.
Any hints are greatly appreciated.
I've created a meteorpad example, showing this.
The reactivity is created by calling template.autorun in the onRendered callback. The template.autorun callback is triggered by any changes to the results of Cities.find(), and updates the map with .setGeoJSON
this.autorun(function () {
if (Mapbox.loaded()) {
geojson = Cities.find().fetch()
view.featureLayer.setGeoJSON(geojson);
}
});
In this example the contents of the Cities collection are already in the correct format to be passed to .setGeoJSON, but if you prefer you could have a different Colleciton schema, and create the list in this format within the template.autorun callback.
Related
I have a chart displayed in a canvas that I want to destroy and redraw from a Vue 3 watcher. The watcher works and the function that fires on the change works up to the redraw step. When it reaches the redraw step, I get:
TypeError: Argument 1 ('element') to Window.getComputedStyle must be an instance of Element
The chart object is loaded as a component and is rendered from a method when initially mounted. I am using vanilla chart.js (not vue-chartjs).
Mount:
mounted() {
this.renderChart()
},
Watch:
watch: {
'$store.state.weather': {
handler(newValue, oldValue) {
this.forecastChart.destroy()
this.animation = 0
this.renderChart() // works fine until this line
},
deep: true
}
}
Method:
methods: {
renderChart() {
let ctx = document.getElementById(this.id).getContext('2d');
this.forecastChart = new Chart(ctx, {
// source: https://stackoverflow.com/a/69047139/2827397
type: 'doughnut',
plugins: [{
beforeDraw: (chart) => {
const {ctx} = chart;
// etc.
Similar questions seem to have solutions that are outdated and have not worked for me.
Ideally, I'd like to make the chart reactive to the change in the Vuex store state value, but destroying and redrawing the chart would be an acceptable outcome and that is what my question here is regarding.
chart.js 3.9.1, vue 3.0.0, vuex 4.0.2
Edit 1:
Trying to .update() rather than .destroy() the chart object didn't yield results, either.
updateChart() {
this.forecastChart.data.datasets[0].needleValue = this.weather.airQuality - 0.5
this.forecastChart.update()
},
Results in:
Unhandled Promise Rejection: TypeError: undefined is not an object (evaluating 'item.fullSize = options.fullSize')
I'm not smart enough to be able to explain why, but the following revision solves the issue I was facing. Above, I was referencing the chart object with the following destroy() command:
this.forecastChart.destroy()
While this command didn't cause any error to be displayed in the console, it was clearly not working properly in this context (again--very important to note here that this is being done in Vue 3).
I replaced the above line with:
let chartStatus = Chart.getChart(this.forecastChart)
chartStatus.destroy()
And now the original chart is properly destroyed and the new chart is drawn in its place. Here is the relevant code all together:
watch: {
'$store.state.weather.airQuality': {
handler() {
this.updateChart()
},
deep: true
}
},
methods: {
updateChart() {
let chartStatus = Chart.getChart(this.forecastChart) // key change
chartStatus.destroy() // key change
this.animation = 0
this.renderChart()
},
renderChart() {
let ctx = document.getElementById(this.id).getContext('2d');
this.forecastChart = new Chart(ctx, {
type: 'doughnut',
plugins: [{
beforeDraw: (chart) => {
const {ctx} = chart;
// etc.
I'm trying to create a simple Ractive adaptor to parse a value from the Color Thief (http://lokeshdhakar.com/projects/color-thief/) into a template with a defined mustache. (I know there may be better ways to achieve this, but there is a reason for why I'm using the adaptor route!)
I've set up a demo of what I have so far here - this the Ractive code part:
var colorThief = new ColorThief();
var img2 = document.getElementById('ctimage');
var imgColor;
Ractive.adapt.CTImg = {
filter: function ( object ) {
return object instanceof img2;
},
wrap: function ( ractive, img2, keypath, prefixer ) {
// Setup
return {
teardown: function(){
colorThief.destroy();
},
get: function(){
imgColor = colorThief.getColor(img);
},
set: function(property, value){
ractive.set('mainColor', imgColor);
},
reset: function(value){
}
}
}
};
var ractive = new Ractive({
target: '#container',
template: '#template',
adapt: [ 'CTImg' ],
data: {
mainColor: "rgb(97, 79, 112)" // this is what should be returned
}
});
My aim is to get the prominent color from the image given in the Codepen (above), pass it into Ractive (and to Color Thief by the adaptor), then output the resulting color on screen in the relevant mustache.
I can display a hard coded color OK in the template, so I know that the data keypath / reference is OK. However, my issue is getting the color back from Color Thief via the adaptor - the error I'm getting is Uncaught "TypeError: Cannot set property 'CTImg' of undefined".
I've checked through SO and the Ractive Github site to see if I can figure out what is going wrong, but my head is starting to spin!
Can anyone please help me to at least get the color to come back from Color Thief via the adaptor?
So adapt and adaptors are two different config objects. adaptors is a registry of adaptor definitions and adapt tells the component/instance what adaptors to use. There's no global adapt property.
For global registration of an adaptor, you need Ractive.adaptors.
Ractive.adaptors.CTImg = {...}
The next problem is actually how you use the adaptor. Adaptors require you to put the non-POJO data into the instance. The filter is run on the data and determines if the data needs to be adapted, and if so, does the setup. Then, it's the usual adaptor setup. get returns the value to Ractive, set sets the value to your custom object, etc.
Here's an updated example:
Ractive.adaptors.CTImg = {
filter: function ( object ) {
// Detect if the data is an image element
return object instanceof HTMLImageElement;
},
wrap: function ( ractive, object, keypath, prefixer ) {
// Set up color thief for this piece of data because it's an image
var colorThief = new ColorThief();
return {
teardown: function(){
colorThief.destroy();
},
get: function(){
// Return the replacement data
return colorThief.getColor(object);
},
set: function(property, value){
// We're not setting to color thief, leave empty
},
reset: function(value){
// Always replace the data when the data is changed
return false;
}
}
}
};
var ractive = new Ractive({
target: '#container',
template: '#template',
adapt: [ 'CTImg' ],
data: {
dominant: null
},
onrender: function(){
// set image on data. adaptor will capture it.
this.set('dominant', this.find('#ctimage'))
}
});
This is just a snippet of my code:
function removeFilter(a){
if (a === "100-Year Rainfall"){
bldg100yearCBR.filter = null;
bldg100yearCBR.refresh({
force: true
});
}
...
Above is my function to remove filter for WFS vector layer. It works if I won't add another layer. I am wondering why? Here's how I add my layer:
bldg100yearCBR = new OpenLayers.Layer.Vector("100-Year Rainfall Affected Buildings", {
strategies: [new OpenLayers.Strategy.Fixed()],
eventListeners: {
'loadend': function (evt) {
map.zoomToExtent(bldg100yearCBR.getDataExtent());
//map.getExtent().containsBounds(bldg100yearCBR, true);
$("#load_table").removeAttr("disabled", "disabled");
$("#load_table").val("Go");
$("#locateMe").hide();
},
'loadstart': function (evt) {
$("#load_table").attr("disabled", "disabled");
$("#load_table").val("Loading...");
$("#locateMe").show();
}
},
projection: new OpenLayers.Projection("EPSG:4326"),
protocol: new OpenLayers.Protocol.WFS({
version: "1.1.0",
url: a,
featureType: "bldg_100yr",
featureNS: b,
geometryName: "geom"
}),
renderers: renderer,
styleMap: new OpenLayers.StyleMap(style),
displayInLayerSwitcher: !0
});
var selFloodEvent = $('#affectedLayer').val();
var layerName = map.getLayersByName(selFloodEvent);
var len = layerName.length;
if (len < 1){
map.addLayer(bldg100yearCBR);
activateControls(c);
}else{
removeFilter("100-Year Rainfall");
layerName[0].setVisibility(true);
console.log("reset me");
}
myflag1 = true;
toggleStatic1("100-Year Rainfall Affected Buildings");
After adding/loading another vector layer, filter won't work. The scenario is if the layer is already loaded, I want to display it without adding it again but if it is filtered before, I wanted to reset it. That's the problem, the removeFilter function won't work after loading another vector layer.
So, basically filter only works the first time I loaded the layer. I also noticed in console log in my browser, it does not query to the GeoServer like this:
XHR finished loading: POST "http://127.0.0.1:8080/geoserver/wfs".
after loading another layer.
I need to format the response I get from Analytics before showing it inside a Google Chart, I tried editing the response when the on("success"... method gets fired but I found that it gets called after the .execute().
Is there any way to edit the response after receiving it and before it populates the chart?
This is my function:
var dataChart5 = new gapi.analytics.googleCharts.DataChart({
reportType: 'ga',
query: {
'ids': 'ga:***', // My ID
'start-date': '31daysAgo',
'end-date': 'yesterday',
'metrics': 'ga:users,ga:percentNewSessions,ga:sessions,ga:bounceRate,ga:avgSessionDuration,ga:pageviews,ga:pageviewsPerSession',
'prettyPrint':'true',
},
chart: {
'container': 'chart-5-container',
'type': 'TABLE',
'options': {
'width': '100%',
'title': 'test'
}
}
});
dataChart5.on('success', function(response) {
response.data.cols[0].label = "test1"; //here I edit the response
console.log(response);
});
dataChart5.execute();
Using the console.log(response); I can see that the record label gets modified but the chart gets populated before the edit.
I think a have a workaround. It has problems, but might be useful. While handling the success event, call a function that will recursively walk through the child elements of $('#chart-5-container') and apply your formatting there.
One problem with that approach is that the positions of the elements won't be recalculated. Therefore, with different string sizes you might get overlapping strings. Moreover, it seems not to be affecting the tooltip.
I'm using this approach to translate to Portuguese.
function recursiveTranslate(e) {
var key = e.html(),
dict = {};
dict['Date'] = 'Data';
dict['Users'] = 'Visitantes';
dict['Sessions'] = 'Visitas';
dict['Pageviews'] = 'Visualizações';
if (key in dict) {
e.html(dict[key]);
}
for (var i = 0; i < e.children().length; i++) {
recursiveTranslate($(e.children()[i]));
}
}
Then I call recursiveTranslate inside the success event:
dataChart5.on('success', function h(obj) {
recursiveTranslate($('#chart-5-container'));
});
It is not elegant and has a lot of issues. I would really like to get my hands on the proper solution.
The current method I'm using is to filter a collection, which returns an array, and use
collection.reset(array)
to re-populate it. However, this modifies the original collection, so I added an array called "originalCollectionArray" which keeps track of the initial array state of the collection. When no filtering is active I simply use
collection.reset(originalCollectionArray)
But then, I need to keep track of adding and removing models from the real collection, so I did this:
// inside collection
initialize: function(params){
this.originalCollectionArray = params;
this.on('add', this.addInOriginal, this);
this.on('remove', this.removeInOriginal, this);
},
addInOriginal: function(model){
this.originalCollectionArray.push(model.attributes);
},
removeInOriginal: function(model){
this.originalTasks = _(this.originalTasks).reject(function(val){
return val.id == model.get('id');
});
},
filterBy: function(params){
this.reset(this.originalCollectionArray, {silent: true});
var filteredColl = this.filter(function(item){
// filter code...
});
this.reset(filteredColl);
}
This is quickly becoming cumbersome as I try to implement other tricks related to the manipulation of the collection, such as sorting. And frankly, my code looks a bit hacky. Is there an elegant way of doing this?
Thanks
You could create a collection as a property of the main collection reflecting the state of the filters:
var C = Backbone.Collection.extend({
initialize: function (models) {
this.filtered = new Backbone.Collection(models);
this.on('add', this.refilter);
this.on('remove', this.refilter);
},
filterBy: function (params){
var filteredColl = this.filter(function(item){
// ...
});
this.filtered.params = params;
this.filtered.reset(filteredColl);
},
refilter: function() {
this.filterBy(this.filtered.params);
}
});
The parent collection keeps its models whatever filters you applied, and you bind to the filtered collection to know when a change has occurred. Binding internally on the add and remove events lets you reapply the filter. See
http://jsfiddle.net/dQr7X/ for a demo.
The major problem on your code is that you are using a raw array as original, instead of a Collection. My code is close to the yours but use only Collections, so methods like add, remove and filter works on the original:
var OriginalCollection = Backbone.Collection.extend({
});
var FilteredCollection = Backbone.Collection.extend({
initialize: function(originalCol){
this.originalCol = originalCol;
this.on('add', this.addInOriginal, this);
this.on('remove', this.removeInOriginal, this);
},
addInOriginal: function(model){
this.originalCol.add(model);
},
removeInOriginal: function(model){
this.originalCol.remove(model);
},
filterBy: function(params){
var filteredColl = this.originalCol.filter(function(item){
// filter code...
});
this.reset(filteredColl);
}
});