Related
The google slides API documentation says on shapes/text elements it can accept "AUTOFIT_TEXT" as an autofit type, however this simply doesn't work and the API throws an error saying "autofitType can only be set to NONE", see here
My alternate pathway solution here is to attempt to come up with a method that can recalculate or estimate the font-size needed for the width of the element in question.
How would I go about doing this?
The font size could be anything it doesn't matter, I just need the text I'm replacing in the element to NOT wrap.
Simply just duplicating the slide with the API resets all the auto fit options already set in the google slide template.
Let's say the string length is "Hi-dilly-ho, neighborinos!", which has a length of 26, and let's say the width of the element is say 200PX. I'd need to come up with a method to fit the text on one line!
There could simply be a magic number I come up with here and multiply that by the length of the string which may work, but i was wondering if there's a nicer way with nodejs that anyone can think of
As i couldn't find a good example of this anywhere, i wrote a method to re-calculate the font-size based on the dimensions of the input element from the Google Presentation.
This workaround calculates the size of the element (width and height) and using node canvas, i created a text element and measure the size of the text which works for multiline text as well.
This is NOT as smart as the default autofit inside gslides, as it bases the amount of chars that can fit of the W key as it's the widest key usually in font faces.
There's a few helpers i've created here which are used by the main function:
// An English Metric Unit (EMU) is defined as 1/360,000 of a centimeter and thus there are 914,400 EMUs per inch, and 12,700 EMUs per point.
export const convertEMUToPT = (emu: number): number => emu / 12700;
// convert pixles to PT, there is 0.75pt to a px
export const covertPXtoPT = (px: number): number => px * 0.75;
// convert PT to PX, there is 0.75pt to a px
export const convertPTtoPX = (px: number): number => px / 0.75;
// this is a very simple example of what i have, obviously you'll need error handling if those values don't exist
// The below will return the dimensions in EMU, to convert to PT divide the EMU value by `12700`
export function getElementSize(element: slides_v1.Schema$PageElement) {
const width = element?.size?.width?.magnitude * element.transform?.scaleX;
const height = element?.size?.height?.magnitude * element.transform?.scaleY;
return { width, height };
}
/**
* #name findByKey
* #description This was introduced as the fontWeight key for example could be on a mixture of elements, and we
* want to find them whereever they may be on the element so we can average out the values
* #function
* #param obj - any object to search
* #param kee - representing the needle to search
* #returns any - returns the value by the key if found
*/
export const findByKey = (obj: any, kee: string): any | undefined => {
if (kee in obj) {
return obj[kee];
}
for (const n of Object.values(obj).filter(Boolean).filter(v => typeof v === 'object')) {
const found = findByKey(n, kee);
if (typeof found !== 'undefined') {
return found;
}
}
};
/**
* #name splitter
* #description Based on the maximum allowed characters on a single line, we split the lines
* based on this value so we can calculate multi line text wrapping and adjust the font size
* continually within a while loop
* #function
* #param str - the input string
* #param l - the length of each "line" of text
* #returns string[] - an array of strings representing each new line of text
*/
export function splitter(str: string, l: number): string[] {
const strs = [];
while (str.length > l) {
let pos = str.substring(0, l).lastIndexOf(' ');
pos = pos <= 0 ? l : pos;
strs.push(str.substring(0, pos));
let i = str.indexOf(' ', pos) + 1;
if (i < pos || i > pos + l)
i = pos;
str = str.substring(i);
}
strs.push(str);
return strs;
}
import { createCanvas } from 'canvas';
export function calculateFontSize(element: slides_v1.Schema$PageElement, text: string): number {
// get the dimensions of the element
const size = getElementSize(element, false);
// create a canvas with the same size as the element, this most likely does not matter as we're only measureing a fake
// representation of the text with ctx.measureText
const canvas = createCanvas(convertPTtoPX(size.width), convertPTtoPX(size.height));
const ctx = canvas.getContext('2d');
// try to extract all the font-sizes
const fontSizes = element.shape?.text?.textElements?.map(textElement => textElement.textRun?.style?.fontSize?.magnitude).filter((a): a is number => isNumber(a)) ?? [];
// try to extract all the font-weights
const fontWeights = element.shape?.text?.textElements?.map(textElement => textElement.textRun?.style?.weightedFontFamily?.weight).filter((a): a is number => isNumber(a)) ?? [];
// fallback to arial if not found, if there's more than one fontFamily used in a single element, we just pick the first one, no way i can think of
// to be smart here and not really necessary to create multiple strings with different fonts and calculate those, this seems to work fine
const fontFamily = findByKey(element, 'fontFamily') ?? 'Arial';
// calulate the average as there can be different fonts with different weights within a single text element
const averageFontWeight = fontWeights.reduce((a, n) => a + n, 0) / fontWeights.length;
const averageFontSize = fontSizes.reduce((a, n) => a + n, 0) / fontSizes.length;
// if the average font-weight is not a number, usae the default
const fontWeight = isNaN(averageFontWeight) ? DEFAULT_FONT_WEIGHT : averageFontWeight;
// use the average fontSize if available, else start at an arbitrary default
let fontSize = isNaN(averageFontSize) ? DEFAULT_FONT_SIZE : averageFontSize;
// if the input value is an empty string, don't bother with any calculations
if (text.length === 0) {
return fontSize;
}
// create the initial font value, this is overwritten during the while loop
ctx.font = `${fontWeight} ${fontSize}pt ${fontFamily}`;
// max chars we will fit horizontally based on the char width of W
const getCharWidth = (): number => convertPTtoPX(size.width) / ctx.measureText('W').width;
// used for the while loop, to continually resize the shape and multiline text, until it fits within the bounds
// of the element
const isOutsideBounds = (): boolean => {
// based on the maximum amount of chars available in the horizontal axis for this font size
// we split onto a new line to get the width/height correctly
const multiLineString = splitter(text, getCharWidth()).join('\n');
// get the measurements of the current multiline string
const metrics = ctx.measureText(multiLineString);
// get the width in PT
const width = covertPXtoPT(metrics.width);
// the emAcent/Decent values do exist, it's the types that are wrong from canvas
// #ts-expect-error
const emAcent = metrics.emHeightAscent as number;
// #ts-expect-error
const emDecent = metrics.emHeightDescent as number;
const height = covertPXtoPT(emAcent + emDecent);
return width > size.width || height > size.height;
};
// continually loop over until the size of the text element is less than the intiial size of the element in gslides
while (isOutsideBounds()) {
// decrease by 0.1 incrememnts until it fits within the width
fontSize = fontSize - 0.1;
// update the ctx with the new font style (shrinking the font size)
ctx.font = `${fontWeight} ${fontSize}pt ${fontFamily}`;
}
// returns the font size
return fontSize;
}
Then to use for a request to update the text after the text is inserted/replaced:
{
updateTextStyle: {
objectId: pageElement.objectId,
fields: 'fontSize',
style: {
fontSize: {
magnitude: calculateFontSize(pageElement, STRING_VALUE_HERE),
unit: 'PT'
}
}
}
}
I'm developing an taxi caller app. And I used Bing map
This is my flow:
Get my position
Get 3 near taxis
When I tap 1 in 3 taxis, all in formation of taxis will sent to a new
Variable to used with another function.
This is my code
And how to send a obj from Foreach to taxiIcon_Tap?
Thanks and best regards!
//------ BEGIN get near Driver ------//
private async void GetNearDriver()
{
var uid = userData.content.uid;
var lat = pickupLat;
var lng = pickupLng;
var clvl = taxiType;
var input = string.Format("{{\"uid\":\"{0}\",\"lat\":{1},\"lng\":{2},\"cLvl\":\"{3}\"}}", uid, lat.ToString().Replace(',', '.'), lng.ToString().Replace(',', '.'), clvl);
var output = await GetJsonFromPOSTMethod.GetJsonString(ConstantVariable.tNetRiderGetNerDriverAddress, input);
var nearDriver = JsonConvert.DeserializeObject<RiderGetNearDriver>(output);
if (nearDriver.content.listDriverDTO.Count > 0)
{
foreach (var taxi in nearDriver.content.listDriverDTO)
{
ShowNearDrivers(taxi.lat, taxi.lng, taxi.cName);
}
}
}
//------ END get near Driver ------//
//------ BEGIN show and Design UI 3 taxi near current position ------//
private void ShowNearDrivers(double lat, double lng, string tName)
{
GeoCoordinate TaxiCoordinate = new GeoCoordinate(lat, lng);
//Create taxi icon on map
Image taxiIcon = new Image();
taxiIcon.Source = new BitmapImage(new Uri("/Images/Taxis/img_CarIcon.png", UriKind.Relative));
//Add a tapped event
taxiIcon.Tap += taxiIcon_Tap;
//Create Taxi Name
TextBlock taxiName = new TextBlock();
taxiName.HorizontalAlignment = HorizontalAlignment.Center;
taxiName.Text = tName;
taxiName.FontSize = 12;
taxiName.Foreground = new SolidColorBrush(Color.FromArgb(255, (byte)46, (byte)159, (byte)255)); //RBG color for #2e9fff
//Create Stack Panel to group icon, taxi name, ...
Rectangle taxiNameBackground = new Rectangle();
taxiNameBackground.Height = 18;
taxiNameBackground.Width = taxiName.ToString().Length + 20;
taxiNameBackground.RadiusX = 9;
taxiNameBackground.RadiusY = 7;
//taxiNameBackground.Stroke = new SolidColorBrush(Color.FromArgb(255, (byte)171, (byte)171, (byte)171)); //RBG color for #ababab
taxiNameBackground.Fill = new SolidColorBrush(Color.FromArgb(255, (byte)213, (byte)235, (byte)255)); //RBG color for #d5ebff
Grid taxiNameGrid = new Grid();
taxiNameGrid.Margin = new Thickness(0, 4, 0, 4); //Margin Top and Bottom 4px
taxiNameGrid.HorizontalAlignment = System.Windows.HorizontalAlignment.Center;
taxiNameGrid.VerticalAlignment = System.Windows.VerticalAlignment.Center;
taxiNameGrid.Children.Add(taxiNameBackground);
taxiNameGrid.Children.Add(taxiName);
StackPanel taxiStackPanel = new StackPanel();
//taxiStackPanel.Margin = new Thickness(5, 0, 5, 0);
taxiStackPanel.Children.Add(taxiIcon);
taxiStackPanel.Children.Add(taxiNameGrid);
// Create a MapOverlay to contain the circle.
MapOverlay myTaxiOvelay = new MapOverlay();
//myTaxiOvelay.Content = myCircle;
myTaxiOvelay.Content = taxiStackPanel;
myTaxiOvelay.PositionOrigin = new Point(0.5, 0.5);
myTaxiOvelay.GeoCoordinate = TaxiCoordinate;
//Add to Map's Layer
riderMapLayer = new MapLayer();
riderMapLayer.Add(myTaxiOvelay);
map_RiderMap.Layers.Add(riderMapLayer);
}
//Tapped event
private void taxiIcon_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
//Hide Step 01
this.grv_Step01.Visibility = Visibility.Collapsed;
//Show Step 02
this.grv_Step02.Visibility = Visibility.Visible;
this.grv_Picker.Visibility = Visibility.Collapsed;
//Step 2 info
LoadStep2Info();
}
//------ END show and Design UI 3 taxi near current position ------//
You can use the Tag property to pass such miscellaneous objects around when using events which don't expose the ability to add an additional payload.
My understanding is that in the taxiIcon_Tap event you want to be able to access your own object that is related to whatever was tapped.
To do this set you object to the Tag property of the item being tapped.
i.e. If you want to pass the coordinates do:
taxiIcon.Tag = TaxiCoordinate;
(You could pass anything.)
Then, in the tapped event handler you can get at this object by casting from the sender to get the Tag and then casting that back to your type.
var coords = ((FrameworkElement)sender).Tag as GeoCoordinate;
I'm developing a classifier for text categorisation using Weka java libraries. I've extracted a number of features using Stanfords' CoreNLP package, including a dependency parse of the text which returns a string "(rel, head, mod)".
I was wanting to use the dependency triplets returned from this as features for classification but I cannot figure out how to properly represent them in the ARFF file. Basically, I'm stumped; for each instance, there are an arbitrary number of dependency triplets, so I can't define them explicitly in the attributes, for example:
#attribute entityCount numeric
#attribute depTriple_1 string
#attribute depTriple_2 string
.
.
#attribute depTriple_n string
Is there a particular way to go about this? I've spent the better part of the day searching and have not found anything yet.
Thanks a lot for reading.
Extracted from Weka Wiki:
import weka.core.Attribute;
import weka.core.FastVector;
import weka.core.Instance;
import weka.core.Instances;
/**
* Generates a little ARFF file with different attribute types.
*
* #author FracPete
*/
public class SO_Test {
public static void main(String[] args) throws Exception {
FastVector atts;
FastVector attsRel;
FastVector attVals;
FastVector attValsRel;
Instances data;
Instances dataRel;
double[] vals;
double[] valsRel;
int i;
// 1. set up attributes
atts = new FastVector();
// - numeric
atts.addElement(new Attribute("att1"));
// - nominal
attVals = new FastVector();
for (i = 0; i < 5; i++)
attVals.addElement("val" + (i+1));
atts.addElement(new Attribute("att2", attVals));
// - string
atts.addElement(new Attribute("att3", (FastVector) null));
// - date
atts.addElement(new Attribute("att4", "yyyy-MM-dd"));
// - relational
attsRel = new FastVector();
// -- numeric
attsRel.addElement(new Attribute("att5.1"));
// -- nominal
attValsRel = new FastVector();
for (i = 0; i < 5; i++)
attValsRel.addElement("val5." + (i+1));
attsRel.addElement(new Attribute("att5.2", attValsRel));
dataRel = new Instances("att5", attsRel, 0);
atts.addElement(new Attribute("att5", dataRel, 0));
// 2. create Instances object
data = new Instances("MyRelation", atts, 0);
// 3. fill with data
// first instance
vals = new double[data.numAttributes()];
// - numeric
vals[0] = Math.PI;
// - nominal
vals[1] = attVals.indexOf("val3");
// - string
vals[2] = data.attribute(2).addStringValue("This is a string!");
// - date
vals[3] = data.attribute(3).parseDate("2001-11-09");
// - relational
dataRel = new Instances(data.attribute(4).relation(), 0);
// -- first instance
valsRel = new double[2];
valsRel[0] = Math.PI + 1;
valsRel[1] = attValsRel.indexOf("val5.3");
dataRel.add(new Instance(1.0, valsRel));
// -- second instance
valsRel = new double[2];
valsRel[0] = Math.PI + 2;
valsRel[1] = attValsRel.indexOf("val5.2");
dataRel.add(new Instance(1.0, valsRel));
vals[4] = data.attribute(4).addRelation(dataRel);
// add
data.add(new Instance(1.0, vals));
// second instance
vals = new double[data.numAttributes()]; // important: needs NEW array!
// - numeric
vals[0] = Math.E;
// - nominal
vals[1] = attVals.indexOf("val1");
// - string
vals[2] = data.attribute(2).addStringValue("And another one!");
// - date
vals[3] = data.attribute(3).parseDate("2000-12-01");
// - relational
dataRel = new Instances(data.attribute(4).relation(), 0);
// -- first instance
valsRel = new double[2];
valsRel[0] = Math.E + 1;
valsRel[1] = attValsRel.indexOf("val5.4");
dataRel.add(new Instance(1.0, valsRel));
// -- second instance
valsRel = new double[2];
valsRel[0] = Math.E + 2;
valsRel[1] = attValsRel.indexOf("val5.1");
dataRel.add(new Instance(1.0, valsRel));
vals[4] = data.attribute(4).addRelation(dataRel);
// add
data.add(new Instance(1.0, vals));
// 4. output data
System.out.println(data);
}
}
Your problem is in particular a "relational" attribute. This code segment has dealt with such relational attribute.
Alright, I did it! Just posting this as an answer incase anyone else has a similar problem. Previously I was following the guide found on the Weka Wiki (as posted below by Rushdi), but I was having a lot of trouble following as the guide is creating static instances of the relational attribute, where as I required dynamic declarations of an arbitrary amount. So I decided to re-evaluate how I was generating the attributes, and I managed to get it to work with slight changes to the above guide:
//1. Set up attributes
FastVector atts;
FastVector relAtts;
Instances relData;
atts = new FastVector();
//Entity Count - numeric
atts.addElement(new Attribute("entityCount"));
//Dependencies - Relational (Multi-Instance)
relAtts = new FastVector();
relAtts.addElement(new Attribute("depTriplet", (FastVector) null));
relData = new Instances("depTriples", relAtts, 0);
atts.addElement(new Attribute("depTriples", relData, 0));
atts.addElement(new Attribute("postTxt", (FastVector) null));
//2. Create Instances Object
Instances trainSet = new Instances("MyName", atts, 0);
/* 3. Fill with data:
Loop through text docs to extract features
and generate instance for train set */
//Holds the relational attribute instances
Instances relAttData;
for(Object doc: docList) {
List<String> depTripleList = getDepTriples(doc);
int entCount = getEntityCount(doc);
String pt = getText(doc);
//Create instance to be added to training set
Instance tInst = new Instance(trainSet.numAttributes());
//Entity count
tInst.setValue( (Attribute) atts.elementAt(0), entCount);
//Generate Instances for relational attribute
relAttData = new Instances(trainSet.attribute(1).relation(), 0);
//For each deplist entry, create an instance and add it to dataset
for(String depTriple: depTripleList) {
Instance relAttInst = new Instance(1);
relAttInst.setDataset(relAttData);
relAttInst.setValue(0, depTriple);
relAttData.add(relAttInst);
}
//Add relational attribute (now filled with a number of Instances of attributes) to the main Instance
tInst.setValue( (Attribute) atts.elementAt(1), trainSet.attribute(1).addRelation(relAttData));
//Finally, add the instance to the relational attribute
trainSet.add(tInst)
}
//4. Output data
System.out.println(trainSet);
I realise this could probably be done differently, but this works well with my situation. Please keep in mind this is not my actual code, but an excerpt of multiple parts stitched together to demonstrate the process used to fix the problem.
The page about the PubNub History API states that
The history() function returns a list of up to 100 messages, the start
time token and the ending time token.
Is there a way to retrieve more than the 100 messages?
I'm currently not a paying customer of PubNub.
PubNub Load History More than 100 Messages
Sometimes you want to slice back in time over a linear stream of data. And often you'll want to do this at different levels of granularity. That is why PubNub Storage and Playback APIs provide maximum level of flexibility. However sometimes it ends up being a bit tricky to load data with the preferred result set.
PubNub Real-Time Network Storage and Playback
There are several considerations you may be seeking when loading transaction history over timelines that can potentially span millions of message in the transaction set. There are some great options available to you and we will cover two of them right now. The examples will be coded in JavaScript. The first example loads a summary of the data by grabbing the snapshots for the beginning of each hour for the past 24 hours. The second example shows you how to load all transactions in full detail and maximum granularity.
All Reference Files can be found on this GIST: Loading History from PubNub Mt.Gox Trades
Example PubNub Mt.Gox History JavaScript Usage
<script src="https://cdn.pubnub.com/pubnub.min.js"></script>
<script src="mtgox-history.js"></script>
<script>(function(){
// LOAD HOURLY SUMMARY
MTGOX.history.hourly({
channel : 'd5f06780-30a8-4a48-a2f8-7ed181b4a13f',
data : function(response) { console.log(JSON.stringify(response)) },
error : function() { console.log("NETWORK ERROR") }
});
// LOAD ALL WITH LIMITER OPTION
MTGOX.history.full({
limit : 500, // SET LIMIT AS HIGH AS NEEDED TO LOAD MORE!
channel : 'd5f06780-30a8-4a48-a2f8-7ed181b4a13f',
data : function(messages) { console.log(messages) },
error : function(e) { console.log("NETWORK ERROR") }
});
})();</script>
NOTE: Running MTGOX.history.hourly() method will generate a list of snapshots per hour over the last 24 hours.
NOTE: Running MTGOX.history.full() method will generate maximum resolution detail with a lot of data. You can get a full dump or partial dump as needed; and you should increase the limit parameter in order to grab more data points.
This following JavaScript file will provide you the MTGOX interface.
PubNub Mt.Gox History JavaScript Loader
//
// mtgox-history.js
//
(function(){
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// INITIALIZE PUBNUB
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
var pubnub = PUBNUB.init({
subscribe_key : 'sub-c-50d56e1e-2fd9-11e3-a041-02ee2ddab7fe'
});
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// MTGOX HISTORY INTERFACE
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
window.MTGOX = {
history : {
hourly : hourly,
full : full
}
};
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// GET ALL DATA FOREVER (WITH LIMIT OF COURSE)
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
/*
MTGOX.history.full({
limit : 1000,
channel : 'd5f06780-30a8-4a48-a2f8-7ed181b4a13f',
data : function(messages) { console.log(messages) },
error : function(e) { console.log("NETWORK ERROR") }
});
*/
function full(args) {
var chan = args['channel'] ||'d5f06780-30a8-4a48-a2f8-7ed181b4a13f'
, callback = args['data'] || function(){}
, error = args['error'] || function(){}
, limit = +args['limit'] || 5000
, start = 0
, count = 100
, history = []
, params = {
channel : chan,
count : count,
callback : function(messages) {
var msgs = messages[0];
start = messages[1];
params.start = start;
PUBNUB.each( msgs.reverse(), function(m) {history.push(m)} );
if (history.length >= limit) return callback(history);
if (msgs.length < count) return callback(history);
count = 100;
add_messages();
},
error : function(e) {
callback(history);
error(history);
}
};
add_messages();
function add_messages() { pubnub.history(params) }
}
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
// GET 24 HOURS IN HOURLY INCREMENTS
// -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
/*
MTGOX.history.hourly({
channel : 'd5f06780-30a8-4a48-a2f8-7ed181b4a13f',
data : function(response) { console.log(response) },
error : function() { console.log('ERROR') }
});
*/
function hourly(setup) {
var limit = 24;
var count = 0;
var chan = setup['channel'] ||'d5f06780-30a8-4a48-a2f8-7ed181b4a13f';
var cb = setup['data'] || function(){};
var eb = setup['error'] || function(){};
var now = new Date();
now.setUTCHours(0);
now.setUTCMinutes(0);
now.setUTCSeconds(0);
now.setUTCMilliseconds(0);
var utc_now = now.getTime();
var vectors = [];
PUBNUB.each( (new Array(limit)).join(',').split(','), function( _, d ) {
var day = utc_now - 3600000 * d;
pubnub.history({
limit : 1,
channel : chan,
start : day * 10000,
error : function() { count++; eb(); },
callback : function(messages) {
// DONE?
if (++count == limit) return cb(vectors);
// ADD TIME SLICES
var res = +(((messages[0][0]||{}).ticker||{}).avg||{}).value;
res && vectors.push([ new Date(day).getUTCHours(), res ]);
// KEEP IT SORTED
vectors.sort(function(a,b){ return a[0] > b[0] && -1 || 1 });
}
})
} );
}
})();
Mt.Gox PubNub Channel Listing for Tickers, Depth and Trades
The following is a list of channels provided by Mt.Gox data feed options you can use in the history channel parameter field.
{
"TICKER.ltcgbp": "0102A446-E4D4-4082-8E83-CC02822F9172",
"TICKER.ltccny": "0290378C-E3D7-4836-8CB1-2BFAE20CC492",
"DEPTH.btchkd": "049F65DC-3AF3-4FFD-85A5-AAC102B2A579",
"DEPTH.btceur": "057BDC6B-9F9C-44E4-BC1A-363E4443CE87",
"TICKER.nmcaud": "08C65460-CBD9-492E-8473-8507DFA66AE6",
"TICKER.btceur": "0BB6DA8B-F6C6-4ECF-8F0D-A544AD948C15",
"DEPTH.btckrw": "0C84BDA7-E613-4B19-AE2A-6D26412C9F70",
"DEPTH.btccny": "0D1ECAD8-E20F-459E-8BED-0BDCF927820F",
"TICKER.btccad": "10720792-084D-45BA-92E3-CF44D9477775",
"DEPTH.btcchf": "113FEC5F-294D-4929-86EB-8CA4C3FD1BED",
"TICKER.ltcnok": "13616AE8-9268-4A43-BDF7-6B8D1AC814A2",
"TICKER.ltcusd": "1366A9F3-92EB-4C6C-9CCC-492A959ECA94",
"TICKER.btcbtc": "13EDFF67-CFA0-4D99-AA76-52BD15D6A058",
"TICKER.ltccad": "18B55737-3F5C-4583-AF63-6EB3951EAD72",
"TICKER.nmccny": "249FDEFD-C6EB-4802-9F54-064BC83908AA",
"DEPTH.btcusd": "24E67E0D-1CAD-4CC0-9E7A-F8523EF460FE",
"TICKER.btcchf": "2644C164-3DB7-4475-8B45-C7042EFE3413",
"DEPTH.btcaud": "296EE352-DD5D-46F3-9BEA-5E39DEDE2005",
"TICKER.btcczk": "2A968B7F-6638-40BA-95E7-7284B3196D52",
"TICKER.btcsgd": "2CB73ED1-07F4-45E0-8918-BCBFDA658912",
"TICKER.nmcjpy": "314E2B7A-A9FA-4249-BC46-B7F662ECBC3A",
"TICKER.btcnmc": "36189B8C-CFFA-40D2-B205-FB71420387AE",
"DEPTH.btcinr": "414FDB18-8F70-471C-A9DF-B3C2740727EA",
"DEPTH.btcsgd": "41E5C243-3D44-4FAD-B690-F39E1DBB86A8",
"TICKER.btcltc": "48B6886F-49C0-4614-B647-BA5369B449A9",
"TICKER.ltceur": "491BC9BB-7CD8-4719-A9E8-16DAD802FFAC",
"TICKER.btcinr": "55E5FEB8-FEA5-416B-88FA-40211541DECA",
"TICKER.ltcjpy": "5AD8E40F-6DF3-489F-9CF1-AF28426A50CF",
"DEPTH.btccad": "5B234CC3-A7C1-47CE-854F-27AEE4CDBDA5",
"TICKER.btcnzd": "5DDD27CA-2466-4D1A-8961-615DEDB68BF1",
"DEPTH.btcgbp": "60C3AF1B-5D40-4D0E-B9FC-CCAB433D2E9C",
"DEPTH.btcnok": "66DA7FB4-6B0C-4A10-9CB7-E2944E046EB5",
"DEPTH.btcthb": "67879668-532F-41F9-8EB0-55E7593A5AB8",
"TICKER.btcsek": "6CAF1244-655B-460F-BEAF-5C56D1F4BEA7",
"TICKER.btcnok": "7532E866-3A03-4514-A4B1-6F86E3A8DC11",
"TICKER.btcgbp": "7B842B7D-D1F9-46FA-A49C-C12F1AD5A533",
"TRADE.LAG": "85174711-BE64-4DE1-B783-0628995D7914",
"DEPTH.btcsek": "8F1FEFAA-7C55-4420-ADA0-4DE15C1C38F3",
"DEPTH.btcdkk": "9219ABB0-B50C-4007-B4D2-51D1711AB19C",
"DEPTH.btcjpy": "94483E07-D797-4DD4-BC72-DC98F1FD39E3",
"TICKER.nmcusd": "9AAEFD15-D101-49F3-A2FD-6B63B85B6BED",
"TICKER.ltcaud": "A046600A-A06C-4EBF-9FFB-BDC8157227E8",
"TICKER.btcjpy": "A39AE532-6A3C-4835-AF8C-DDA54CB4874E",
"DEPTH.btcczk": "A7A970CF-4F6C-4D85-A74E-AC0979049B87",
"TICKER.ltcdkk": "B10A706E-E8C7-4EA8-9148-669F86930B36",
"TICKER.btcpln": "B4A02CB3-2E2D-4A88-AEEA-3C66CB604D01",
"TEST": "BAD99F24-FA8B-4938-BFDF-0C1831FC6665",
"TICKER.btcrub": "BD04F720-3C70-4DCE-AE71-2422AB862C65",
"TICKER.nmcgbp": "BF5126BA-5187-456F-8AE6-963678D0607F",
"TICKER.btckrw": "BF85048D-4DB9-4DBE-9CA3-5B83A1A4186E",
"TICKER.btccny": "C251EC35-56F9-40AB-A4F6-13325C349DE4",
"DEPTH.btcnzd": "CEDF8730-BCE6-4278-B6FE-9BEE42930E95",
"TICKER.btchkd": "D3AE78DD-01DD-4074-88A7-B8AA03CD28DD",
"TICKER.btcthb": "D58E3B69-9560-4B9E-8C58-B5C0F3FDA5E1",
"TICKER.btcusd": "D5F06780-30A8-4A48-A2F8-7ED181B4A13F",
"DEPTH.btcrub": "D6412CA0-B686-464C-891A-D1BA3943F3C6",
"TICKER.nmceur": "D8512D04-F262-4A14-82F2-8E5C96C15E68",
"TRADE.btc": "DBF1DEE9-4F2E-4A08-8CB7-748919A71B21",
"TICKER.nmccad": "DC28033E-7506-484C-905D-1C811A613323",
"DEPTH.btcpln": "E4FF055A-F8BF-407E-AF76-676CAD319A21",
"TICKER.btcdkk": "E5CE0604-574A-4059-9493-80AF46C776B3",
"TICKER.btcaud": "EB6AAA11-99D0-4F64-9E8C-1140872A423D"
}
See https://help.pubnub.com/entries/24113341-How-do-I-Page-Through-Stored-Messages-
Contact PubNub support (help#...) if further assistance is needed
The below Java class can be used to easily retrieve and process long time ranges of history messages in an advanced for loop of this form:
PubnubHistoryExcerpt history = new PubnubHistoryExcerpt(pubnub, channel, start, end);
for (Object message : history) {
// do something with the message object
}
The messages are retrieved on the fly, so no memory problem occurs.
Below is the complete code. You can find a fully runnable usage example in the main() method inside of the class.
I haven't yet tested this class extensively. Enhancements are welcome.
/*
* PubnubHistoryExcerpt.java
*
* This file is distributed under the FreeBSD License:
*
* Copyright (c) 2014, Daniel S. (http://stackoverflow.com/users/1838726/daniel-s)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation are those
* of the authors and should not be interpreted as representing official policies,
* either expressed or implied, of the FreeBSD Project.
*/
import java.math.BigDecimal;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.pubnub.api.Callback;
import com.pubnub.api.Pubnub;
import com.pubnub.api.PubnubError;
/**
* You can use this class to iterate over historical PubNub messages. The messages are retrieved transparently while you
* iterate over the history excerpt. This class and the returned iterators are thread-safe.
*
* See {#link #main(String[])} for a usage example.
*/
public class PubnubHistoryExcerpt implements Iterable<Object> {
/**
* This main method contains a usage example for this class. It downloads last 3 hour's messages of the MtGox BTC/USD ticker
* channel from PubNub and outputs the timestamp and USD value which are found in the messages.
*/
public static void main(String[] args) throws JSONException {
String PUBNUB_SUBSCRIBE_KEY_MTGOX = "sub-c-50d56e1e-2fd9-11e3-a041-02ee2ddab7fe";
String PUBNUB_CHANNEL_MTGOX_TICKER_BTCUSD = "d5f06780-30a8-4a48-a2f8-7ed181b4a13f";
Pubnub pubnub = new Pubnub(null, PUBNUB_SUBSCRIBE_KEY_MTGOX);
long ONE_HOUR_IN_MILLIS = 60 * 60 * 1000;
long end = System.currentTimeMillis();
long start = end - 3 * ONE_HOUR_IN_MILLIS;
// convert from milliseconds as time unit (10^-3 seconds) to
// pubnub's better-than-microsecond precision time units (10^-7 seconds)
start *= 10000;
end *= 10000;
PubnubHistoryExcerpt history = new PubnubHistoryExcerpt(pubnub, PUBNUB_CHANNEL_MTGOX_TICKER_BTCUSD, start, end);
DefaultDateFormat dateFormat = DefaultDateFormat.create();
for (Object message : history) {
JSONObject messageJson = (JSONObject) message;
JSONObject ticker = messageJson.getJSONObject("ticker");
long instant = ticker.getLong("now");
BigDecimal value = new BigDecimal(ticker.getJSONObject("last_local").getString("value"));
instant /= 1000; // convert from microseconds to milliseconds
System.out.println(dateFormat.format(instant) + ": " + value);
}
System.exit(0);
}
/**
* This is the maximum number of messages to fetch in one batch. If you fetch many messages, higher numbers improve
* performance. Setting this to a value higher than 100 doesn't have an effect, because Pubnub currently doesn't
* support fetching more than this many messages at once.
*/
private static final int BATCH_SIZE = 100;
private final Pubnub pubnub;
private final String channel;
private final long start;
private final long end;
/**
* Constructs a new excerpt over which you can iterate. Insances represent an excerpt. No retrieval operations are
* started unless you call iterator().next() for the first time.
*
* #param pubnub
* The Pubnub connection to use for retrieving messages.
* #param channel
* The channel for which to retrieve historical messages.
* #param start
* The beginning of the time interval for which to retrieve messages, in pubnub's time units (10^-7
* seconds, so milliseconds * 10000) since 1970-01-01 00:00:00).
* #param end
* The end of the time interval for which to retrieve messages, in pubnub's time units (10^-7 seconds, so
* milliseconds * 10000) since 1970-01-01 00:00:00).
*/
private PubnubHistoryExcerpt(Pubnub pubnub, String channel, long start, long end) {
this.pubnub = pubnub;
this.channel = channel;
this.start = start;
this.end = end;
}
public Iterator<Object> iterator() {
return new Iter();
}
private class Iter implements Iterator<Object> {
/**
* This list is used as a fifo buffer for messages retrieves through this iterator. It also acts as the main
* synchronization lock for synchronizing access between threads accessing this class as an iterator and threads
* calling back from the Pubnub API.
*/
private LinkedList<Object> buffer = new LinkedList<Object>();
/**
* This field stores the end of the time range of the previous batch retrieval, in Pubnub time units (10th of a
* microsecond, so milliseconds*10000). For the following batch retrieval, this is used as the start time for
* retrieving the following messages.
*/
private long prevBatchTimeRangeEnd = PubnubHistoryExcerpt.this.start;
/**
* Retrieval of messages is handled asynchronously. That means that exceptions which are thrown during retrieval
* can't automatically be propagated through to the code which invokes <code>next()</code> or
* <code>hasNext()</code> . Therefor, such an exception is stored temporarily in this field and then re-thrown
* from within <code>next()</code> or <code>hasNext()</code>.
*/
private Exception caughtDuringRetrieval = null;
/**
* This object is used to wait on and to notify about updates of the buffer.
*/
private Object notifier = new Object();
/**
* Because of spurious wakeups that can happen during wait(), this field is necessary to tell the waiting thread
* if retrieval is still running.
*/
private boolean retrieving = false;
/**
* The callback object to use for retrieving messages. This is stored in a field here for re-use. This is a
* compromise between performance and low memory footprint, slightly in favor of performance.
*/
private InternalCallback internalCallback = new InternalCallback();
private void retrieveNextBatch() {
synchronized (notifier) {
this.retrieving = true;
// String startStr = DefaultDateFormat.create().format(prevBatchTimeRangeEnd / 10000);
// String endStr = DefaultDateFormat.create().format(end / 10000);
// System.out.println("fetching from " + startStr + " till " + endStr);
if (Iter.this.prevBatchTimeRangeEnd < PubnubHistoryExcerpt.this.end) {
PubnubHistoryExcerpt.this.pubnub.history( //
PubnubHistoryExcerpt.this.channel, //
Iter.this.prevBatchTimeRangeEnd, //
PubnubHistoryExcerpt.this.end, //
BATCH_SIZE, //
false, //
Iter.this.internalCallback //
);
waitUntilNextBatchRetrievalFinished();
}
}
}
private void waitUntilNextBatchRetrievalFinished() {
while (this.retrieving) {
try {
this.notifier.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private class InternalCallback extends Callback {
#Override
public void successCallback(String channel, Object message) {
synchronized (Iter.this.notifier) {
try {
processSuccessCallback(channel, message);
} catch (Exception e) {
Iter.this.caughtDuringRetrieval = e;
} finally {
Iter.this.retrieving = false;
Iter.this.notifier.notifyAll();
}
}
}
#Override
public void errorCallback(String channel, PubnubError error) {
Iter.this.caughtDuringRetrieval = new Exception("" + //
error.getClass().getName() + ": " + //
error.getErrorString() + //
" (code=" + error.errorCode + "; extendedCode=" + error.errorCodeExtended + ")");
Iter.this.caughtDuringRetrieval.fillInStackTrace();
}
}
private void processSuccessCallback(String channel, Object message) throws JSONException {
if (message == null)
throw new NullPointerException("retrieved message is null");
if (!(message instanceof JSONArray))
throw new RuntimeException("retrieved message is not a " + JSONArray.class.getName());
JSONArray historyMessage = (JSONArray) message;
// System.out.println(historyMessage.toString(2));
JSONArray messageList = extractMessageList(historyMessage);
long batchTimeRangeEnd = extractBatchTimeRangeEnd(historyMessage);
if (batchTimeRangeEnd > 0)
Iter.this.prevBatchTimeRangeEnd = batchTimeRangeEnd;
else
Iter.this.prevBatchTimeRangeEnd = end;
processMessageList(messageList);
}
private void processMessageList(JSONArray messageList) {
int i = 0;
for (; i < messageList.length(); i++) {
JSONObject message;
try {
message = messageList.getJSONObject(i);
} catch (JSONException e) {
String str;
try {
str = messageList.toString(2);
} catch (JSONException secondaryE) {
str = "(couldn't convert messageList to String because of " + secondaryE.toString() + ")";
}
throw new RuntimeException("couldn't extract message at index " + i + " from messageList (messageList:\n" + str
+ "\n(end of messageList)\n)", e);
}
Iter.this.buffer.add(message);
}
}
private long extractBatchTimeRangeEnd(JSONArray historyMessage) {
long batchTimeRangeEnd;
try {
batchTimeRangeEnd = historyMessage.getLong(2);
} catch (JSONException e) {
String str = safeConvertHistoryMessageToString(historyMessage);
throw new RuntimeException("could not extract element 2 (batchTimeRangeEnd) of retrieved historyMessage (historyMessage:\n" + str
+ "\n(end of historyMessage)\n)", e);
}
return batchTimeRangeEnd;
}
private String safeConvertHistoryMessageToString(JSONArray historyMessage) {
String str;
try {
str = historyMessage.toString(2);
} catch (JSONException secondaryE) {
str = "(couldn't convert historyMessage to String because of " + secondaryE.toString() + ")";
}
return str;
}
private JSONArray extractMessageList(JSONArray historyMessage) {
JSONArray messageArJson;
try {
messageArJson = historyMessage.getJSONArray(0);
} catch (JSONException e) {
String str = safeConvertHistoryMessageToString(historyMessage);
throw new RuntimeException("could not extract element 0 (messageList) of retrieved historyMessage (historyMessage:\n" + str
+ "\n(end of historyMessage)\n)", e);
}
return messageArJson;
}
public boolean hasNext() {
synchronized (Iter.this.buffer) {
ensureNotInExceptionState();
if (Iter.this.buffer.isEmpty())
retrieveNextBatch();
return !Iter.this.buffer.isEmpty();
}
}
public Object next() {
synchronized (Iter.this.buffer) {
if (!hasNext()) {
throw new NoSuchElementException("there are no more elements in this iterator");
}
Object result = Iter.this.buffer.removeFirst();
return result;
}
}
private void ensureNotInExceptionState() {
if (caughtDuringRetrieval != null) {
throw new RuntimeException("an exception was caught already by a previous attempt to access this iterator", caughtDuringRetrieval);
}
}
public void remove() {
throw new UnsupportedOperationException(getClass().getName() + " doesn't support remove()");
}
}
}
Update
Since the problem has been found I've also find out that Box2D for web is leaking on every side :/
To show this I made a simple circle moving in a static polygon and here is the result after some time.
Notice how the following items are leaking as I'm not creating any body or changing the world in any way:
b2Vec2
Features
b2ManifoldPoint
b2ContactID
b2Manifold
b2ContactEdge
b2PolyAndCircleContact
Array
...
Original post
I have a problem because I'm profiling my game and the garbage collector doesnt' delete my bodies, contacts and other stuff. Then I've looked at what are they keeping from the GC and was the Box2D itself. This might lead to 2 options: I'm doing it bad or Box2D is leaking. I consider is my cause.
What exactly is keeping it?
contact.m_nodeA.other was appearing to be the most used to keep it from GC.
other times: m_fixtureB in a contact... see image
You can see that the body has a __destroyed property. That is set manually before deleting it with world.DestroyBody(body)
When I destroy a body I call it after I call the step method on the world.
As you can see from the box2d method it doesn't get rid of the other variable nor it changes it to another body and my body is not GC.
Any idea of what I'm missing here?
Now I can fix the problem only if the world.Step is not ran:
var gravity = new Box2D.Vec2(0, 0);
var doSleep = true;
var world = new Box2D.World(gravity, doSleep);
var step = false;
var fixtureDef = new Box2D.FixtureDef();
fixtureDef.density = 1.0;
fixtureDef.friction = 0.5;
fixtureDef.restitution = 0.2;
fixtureDef.shape = new Box2D.PolygonShape();
fixtureDef.shape.SetAsBox(1, 1);
var bodyDef = new Box2D.BodyDef;
bodyDef.type = Box2D.Body.b2_dynamicBody;
bodyDef.position.x = 0.4;
bodyDef.position.y = 0.4;
var bodies = []
var fix = [];
window.c = function(){
for(var i = 0; i < 100; i++){
var body = world.CreateBody(bodyDef);
body._id = i;
fix.push(body.CreateFixture(fixtureDef));
bodies.push(body);
}
if(step){world.Step(1/60, 3, 3); world.ClearForces();}
console.log('Created', bodies)
fixtureDef = null;
bodyDef = null;
}
window.d = function(){
_.each(bodies, function(body, i){
body.DestroyFixture(fix[i]);
world.DestroyBody(body);
fix[i] = null;
bodies[i] = null;
})
if(step){world.Step(1/60, 3, 3); world.ClearForces();}
bodies = null;
fix = null;
}
Change the step to true and the memory leak problem appears again.
Reproduce the memory leak problem:
Code in your file:
var gravity = new Box2D.Vec2(0, 0);
var doSleep = true;
var world = new Box2D.World(gravity, doSleep);
var bodies = []
window.c = function(){
for(var i = 0; i < 100; i++){
var bodyDef = new Box2D.BodyDef();
bodyDef.type = 2;
var shape = new Box2D.PolygonShape();
shape.SetAsBox(1, 1);
var fixtureDef = new Box2D.FixtureDef();
fixtureDef.shape = shape;
var body = world.CreateBody(bodyDef);
body._id = i;
body.CreateFixture(fixtureDef);
bodies.push(body);
}
world.Step(0.3, 3, 3);
console.log('Created', bodies)
}
window.d = function(){
_.each(bodies, function(body, i){
world.DestroyBody(body);
bodies[i] = null;
})
world.Step(0.3, 3, 3);
bodies = null;
}
Open google chrome:
Then open your profile and make a snapshot.
Now run the c() method in your console to create 100 bodies
Now snapshot 2
Search in snapshot for b2Body and you'll find 100 Object count
Now run d() to delete all your bodies;
Force Garbage collection by clicking on the garbage can
Make a snapshot 3
Search for b2Body and you'll also find 100 Object count
At the last step should only be 0 objects as they have been destroyed. Instead of this you'll find this:
Now you can see there are a lot of references from b2ContactEdge. Now if you remove the world.Step part of the code you will only see 2 references to the body.
If you remove this line
body.CreateFixture(fixtureDef);
or making the body static is not leaking anymore.
My game loop
...gameLoop = function(o){
// used a lot here
var world = o.world;
// calculate the new positions
var worldStepSeconds = o.worldStepMs / 1000;
// step world
world.Step(worldStepSeconds, o.velocityIterations, o.positionIterations)
// render debug
if(o.renderDebug){
world.DrawDebugData();
}
// always to not accumulate forces, maybe some bug occurs
world.ClearForces();
// tick all ticking entities
_.each(o.getTickEntitiesFn(), function(actor){
if(!actor) return;
actor.tick(o.worldStepMs, o.lastFrameMs);
})
// update PIXI entities
var body = world.GetBodyList();
var worldScale = world.SCALE;
var destroyBody = world.DestroyBody.bind(world);
while(body){
var actor = null;
var visualEntity = null;
var box2DEntity = o.getBox2DEntityByIdFn(body.GetUserData());
if(box2DEntity){
visualEntity = o.getVisualEntityByIdFn(box2DEntity.getVisualEntityId());
if(box2DEntity.isDestroying()){
// optimization
body.__destroyed = true;
world.DestroyBody(body);
box2DEntity.completeDestroy();
}
}
if(visualEntity){
if(visualEntity.isDestroying()){
visualEntity.completeDestroy();
}else{
var inverseY = true;
var bodyDetails = Utils.getScreenPositionAndRotationOfBody(world, body, inverseY);
visualEntity.updateSprite(bodyDetails.x, bodyDetails.y, bodyDetails.rotation);
}
}
// this delegates out functionality for each body processed
if(o.triggersFn.eachBody) o.triggersFn.eachBody(world, body, visualEntity);
body = body.GetNext();
}
// when a joint is created is then also created it's visual counterpart and then set to userData.
var joint = world.GetJointList();
while(joint){
var pixiGraphics = joint.GetUserData();
if(pixiGraphics){
// In order to draw a distance joint we need to know the start and end positions.
// The joint saves the global (yes) anchor positions for each body.
// After that we need to scale to our screen and invert y axis.
var anchorA = joint.GetAnchorA();
var anchorB = joint.GetAnchorB();
var screenPositionA = anchorA.Copy();
var screenPositionB = anchorB.Copy();
// scale
screenPositionA.Multiply(world.SCALE);
screenPositionB.Multiply(world.SCALE);
// invert y
screenPositionA.y = world.CANVAS_HEIGHT - screenPositionA.y
screenPositionB.y = world.CANVAS_HEIGHT - screenPositionB.y
// draw a black line
pixiGraphics.clear();
pixiGraphics.lineStyle(1, 0x000000, 0.7);
pixiGraphics.moveTo(screenPositionA.x, screenPositionA.y);
pixiGraphics.lineTo(screenPositionB.x, screenPositionB.y);
}
joint = joint.GetNext();
}
// render the PIXI scene
if(o.renderPixi){
o.renderer.render(o.stage)
}
// render next frame
requestAnimFrame(o.requestAnimFrameFn);
}
Code from Box2d:
b2ContactManager.prototype.Destroy = function (c) {
var fixtureA = c.GetFixtureA();
var fixtureB = c.GetFixtureB();
var bodyA = fixtureA.GetBody();
var bodyB = fixtureB.GetBody();
if (c.IsTouching()) {
this.m_contactListener.EndContact(c);
}
if (c.m_prev) {
c.m_prev.m_next = c.m_next;
}
if (c.m_next) {
c.m_next.m_prev = c.m_prev;
}
if (c == this.m_world.m_contactList) {
this.m_world.m_contactList = c.m_next;
}
if (c.m_nodeA.prev) {
c.m_nodeA.prev.next = c.m_nodeA.next;
}
if (c.m_nodeA.next) {
c.m_nodeA.next.prev = c.m_nodeA.prev;
}
if (c.m_nodeA == bodyA.m_contactList) {
bodyA.m_contactList = c.m_nodeA.next;
}
if (c.m_nodeB.prev) {
c.m_nodeB.prev.next = c.m_nodeB.next;
}
if (c.m_nodeB.next) {
c.m_nodeB.next.prev = c.m_nodeB.prev;
}
if (c.m_nodeB == bodyB.m_contactList) {
bodyB.m_contactList = c.m_nodeB.next;
}
this.m_contactFactory.Destroy(c);
--this.m_contactCount;
}
b2ContactFactory.prototype.Destroy = function (contact) {
if (contact.m_manifold.m_pointCount > 0) {
contact.m_fixtureA.m_body.SetAwake(true);
contact.m_fixtureB.m_body.SetAwake(true);
}
var type1 = parseInt(contact.m_fixtureA.GetType());
var type2 = parseInt(contact.m_fixtureB.GetType());
var reg = this.m_registers[type1][type2];
if (true) {
reg.poolCount++;
contact.m_next = reg.pool;
reg.pool = contact;
}
var destroyFcn = reg.destroyFcn;
destroyFcn(contact, this.m_allocator);
}
I have the same problem, but I think I find out from where it comes.
Instead of m_* try functions, like GetFixtureA() instead of m_fixtureA.
Totti did you ever figure this out? It looks like box2dweb requires manual destruction and memory management.
I think I have found your leaks, un-implemented ( static class ) destruction functions:
b2Joint.Destroy = function (joint, allocator) {}
b2CircleContact.Destroy = function (contact, allocator) {}<
b2PolygonContact.Destroy = function (contact, allocator) {}
b2EdgeAndCircleContact.Destroy = function (contact, allocator) {}<
b2PolyAndCircleContact.Destroy = function (contact, allocator) {}
b2PolyAndEdgeContact.Destroy = function (contact, allocator) {}
[UPDATE...]
b2DestructionListener.b2DestructionListener = function () {};
b2DestructionListener.prototype.SayGoodbyeJoint = function (joint) {}
b2DestructionListener.prototype.SayGoodbyeFixture = function (fixture) {}
b2Contact.prototype.Reset(fixtureA, fixtureB)
called with with one/both fixture arguments resets passed in fixture/s BUT ALSO pass in NO arguments and it 'nulls' all the the b2Contact properties! (UNTESTED:) but I suggest set your YOURcontactListener class up to handle all contact callbacks EVERY call with Reset(??) dynamically configureable as logic requies EVERY call (there are more than you'd imagine each and every world step).
Also take Colt McAnlis clever advice and strategically pre allocate all the memory the life of your game will need (by creating game and box2d object pools now you know objects can be reset) so the garbage collector NEVER runs until, you destroy object pools at times of your own convenience.... i.e when you close the tab, or your device needs recharging! ;D [...UPDATE]
// you can define and assign your own contact listener ...via...
YOUR.b2world.b2ContactManager.m_world.m_contactList = new YOURcontactlistener();<br>[edit]...if you dont it actually does have Box2D.Dynamics.b2ContactListener.b2_defaultListener.
// box2d in the worldStep calls YOURcontactlistener.update() via:
this.b2world.b2ContactManager.m_world.m_contactList.Update(this.m_contactListener) // this.m_contactListener being YOURS || b2_defaultListener;
// which instantiates ALL your listed leaking object like so:
{b2Contact which instantiates {b2ContactEdge} and {b2Manifold which instantiates {b2ManifoldPoint{which instantiates m_id.key == ContactID{which instantiates Features}}}} along with {B2Vec2} are instantiated in b2ContactResult ...which I can not actually find but assume it must be instantiated in the Solver.
// There is a Contacts.destroyFcn callback is CREATED in....
b2ContactFactory.prototype.Destroy = function (contact) {...}
// then Contacts.destroyFcn callback(s) are privately REGISTERED in....
b2ContactFactory.prototype.InitializeRegisters() {...}
...via...
this.AddType = function (createFcn, destroyFcn, type1, type2) {...}
...BUT... THOSE privately registered ARE four of the un-implimented static class function from above...
b2PolygonContact.Destroy = function (contact, allocator) {}
b2EdgeAndCircleContact.Destroy = function (contact, allocator) {}
b2PolyAndCircleContact.Destroy = function (contact, allocator) {}
b2PolyAndEdgeContact.Destroy = function (contact, allocator) {}
So I havn't tested it yet but it looks like box2dweb just gives you the Destroy callback/handler functions and you have to read the source to find all the properties you need to null. [Edit] In combination with b2Contact.prototype.Reset(fixtureA, fixtureB)
But either way pretty confident the functions above(possibly incomplete) are callback/handlers, and can be used to null your way back to performance for anyone else who stumbles across this problem. Pretty sure Totti's moved on(dont forget to handle your 'this' scope in callbacks).