Play and Pause interval rxjs - rxjs

i'm trying to implement play and pause button using Rxjs library.
const play$ = fromEvent(playerImplementation, PLAYER_EVENTS.PLAY).pipe(mapTo(true));
const pause$ = fromEvent(playerImplementation, PLAYER_EVENTS.PAUSE).pipe(mapTo(false));
const waiting$ = fromEvent(playerImplementation, PLAYER_EVENTS.WAITING).pipe(mapTo(false));
let counterTime = 0;
const currentTime$ = interval(30).pipe(
map(()=>counterTime += 30));
const player$ = merge(play$, pause$, waiting$).pipe(
switchMap(value => (value ? currentTime$ : EMPTY)));
// DIFFERENCE IN RESULTS
currentTime$.subscribe((v)=> console.log("Regular Count " + v)); // get correctly 30,60,90,120...
player$.subscribe((v)=>console.log("Condition Count" + v)); // get wrongly 30,150,270, 390
can anyone help in understanding why there is a difference between the results?

It happened because I used several subscribers for one observable (player$ observable). I solve this by using ReplaySubject instead of Observable and by using multicasting in order to handle the event in several subscribers, without changing the value.
const play$ = fromEvent(playerImplementation, PLAYER_EVENTS.PLAY).pipe(mapTo(true));
const pause$ = fromEvent(playerImplementation, PLAYER_EVENTS.PAUSE).pipe(mapTo(false));
const waiting$ = fromEvent(playerImplementation, PLAYER_EVENTS.WAITING).pipe(mapTo(false));
let timeCounter = 0;
const source = Observable.create((obs : Observer<number>)=> {
interval(30).pipe(
map(() => timeCounter += 30)).subscribe(obs);
return () => {};
});
// Cast the observable to subject for distributing to several subscribers
const currentTime$ = source.pipe(multicast(()=> new ReplaySubject(5))).refCount();
const player$ = merge(play$, pause$, waiting$).pipe(
switchMap(value => value ? currentTime$ : EMPTY));

Related

RxJS: How to create an event manager with a buffer that flush based on multiple conditions

I have a requirement to create an event manager with a buffer that flushes if one of 3 criteria is met:
2 seconds pass
50 events received
Flush on demand if requested by user
All criteria will reset when buffer flushes (reset the 2 second timer, reset the 50 events count...etc)
This is what I've implemeted so far and it seems to be working but I'm wondering if there's a better way to achieve this requirement.
import { interval, merge, Subject, Subscription } from "rxjs";
import { bufferWhen, filter, tap } from "rxjs/operators";
class Foo {
private eventListener: Subject < string > = new Subject();
private eventForceFlushListener: Subject < void > = new Subject();
private eventBufferSizeListener: Subject < void > = new Subject();
private maxBufferSize = 50;
private currentBufferSize = 0;
/**
*
* Buffer that will flush if one of the 3 criteria is met:
* - 50 texts are received
* - 2 seconds pass
* - Force flush by user
*/
private eventBufferOperator = () => merge(interval(2 * 1000), this.eventForceFlushListener, this.eventBufferSizeListener);
/**
* Flush buffer if requested by user. (for example flush buffer before app close so we dont lose buffered texts)
*/
public forceFlush() {
this.eventForceFlushListener.next();
}
/**
* Method used by users to emit texts to the listener
*/
public emitText(text: string) {
this.eventListener.next(text);
this.currentBufferSize = this.currentBufferSize + 1;
if (this.currentBufferSize == this.maxBufferSize) {
// flush all evenst when maxBufferSize is reached
this.eventBufferSizeListener.next();
// buffer size is reset below in the function that's inside "subscribe"
}
}
public subscribeToEventListerenr() {
const eventListenerSubscription = this.eventListener
.pipe(
tap((text) => text.trim()),
filter((text) => true),
bufferWhen(this.eventBufferOperator),
filter((events) => !!events.length)
)
.subscribe((x) => {
console.log(x);
this.maxBufferSize = 0; // reset size buffer
});
return eventListenerSubscription;
}
}
Users then can use this event manager as follows:
const eventManager = new Foo();
eventManager.subscribeToEventListerenr();
eventManager.emitText('message1');
eventManager.emitText('message2');
5 seconds pass
5 events received
Flush on demand if requested by user
const { race, Subject, take, share, buffer, tap, bufferCount, bufferTime, startWith, exhaustMap, map } = rxjs;
const observer = (str) => ({
subscribe: () => console.log(`${str} -> subscribe`),
next: () => console.log(`${str} -> next`),
unsubscribe: () => console.log(`${str} -> unsubscribe`),
});
const event$ = new Subject()
const share$ = event$.pipe(map((_, i) => i + 1), share());
const flush$ = new Subject();
const trigger$ = flush$.pipe(tap(observer('flush$')));
const bufferSize$ = share$.pipe(startWith(null), bufferCount(5), tap(observer('BufferSize 5')));
const bufferTime$ = share$.pipe(bufferTime(5000), tap(observer('5 Sec')));
const race$ = race(bufferTime$, trigger$, bufferSize$).pipe(take(1));
const buffer$ = share$.pipe(exhaustMap(() => race$));
share$.pipe(buffer(buffer$)).subscribe((x) => console.log(x));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/7.5.6/rxjs.umd.min.js"></script>
<button onclick="event$.next()">event</button>
<button onclick="flush$.next()">flush</button>
https://stackblitz.com/edit/rxjs-5qgaeq
My final answer
window
bufferTime
const bufferBy$ = new Subject<void>();
const maxBufferSize = 3;
const bufferTimeSpan = 5000;
source$.pipe(
window(bufferBy$),
mergeMap(bufferTime(bufferTimeSpan, null, maxBufferSize)),
).subscribe(...);
https://stackblitz.com/edit/rxjs-hoafkr

How to access the BufferGeometry of IFC items in web-ifc-three

I'm trying to get the geometry of an element
i.e. a BufferGeometry object corresponding to an expressId I have (not through picking).
Basically I'm asking how to traverse the IFC model and export each object as a separate OBJ.
I'll note I have reverse engineered code to achieve that for some version of the package, but it uses undocumented functionality, so naturally it broke in later versions (the code also colors the geometry according to the material's color so I don't need an mtl):
Don't copy this code it won't work
Object.values(bimModel.ifcManager.state.models[bimModel.modelID].items).forEach(type => {
Object.entries(type.geometries).forEach(([id, geometry]) => {
const properties = bimModel.getItemProperties(Number(id))
const numVertices = geometry.getAttribute('position').count
const color = type.material.color.toArray().map(x => x * 255)
const vertexColors = new Uint8Array(Array.from({ length: numVertices }, () => color).flat())
geometry.setAttribute('color', new BufferAttribute(vertexColors, 3, true))
})
})
This is exactly what we do to export models to glTF. The basic workflow is:
Decide what IFC categories you would like to export.
Get all the items of each category.
Reconstruct the mesh for each item.
Export the mesh using the Three.js exporter of your choice.
Let's see a basic example to get all the meshes from the walls. The process is not as straightforward as having each IFC item as a separate mesh, but that's the price for having the draw calls at minimum (otherwise, a browser wouldn't stand even medium-sized IFC files):
import { IFCWALLSTANDARDCASE } from 'web-ifc';
async function getAllWallMeshes() {
// Get all the IDs of the walls
const wallsIDs = manager.getAllItemsOfType(0, IFCWALL, false);
const meshes = [];
const customID = 'temp-gltf-subset';
for (const wallID of wallsIDs) {
const coordinates = [];
const expressIDs = [];
const newIndices = [];
const alreadySaved = new Map();
// Get the subset for the wall
const subset = viewer.IFC.loader.ifcManager.createSubset({
ids: [wallID],
modelID,
removePrevious: true,
customID
});
// Subsets have their own index, but share the BufferAttributes
// with the original geometry, so we need to rebuild a new
// geometry with this index
const positionAttr = subset.geometry.attributes.position;
const expressIDAttr = subset.geometry.attributes.expressID;
const newGroups = subset.geometry.groups
.filter((group) => group.count !== 0);
const newMaterials = [];
const prevMaterials = subset.material;
let newMaterialIndex = 0;
newGroups.forEach((group) => {
newMaterials.push(prevMaterials[group.materialIndex]);
group.materialIndex = newMaterialIndex++;
});
let newIndex = 0;
for (let i = 0; i < subset.geometry.index.count; i++) {
const index = subset.geometry.index.array[i];
if (!alreadySaved.has(index)) {
coordinates.push(positionAttr.array[3 * index]);
coordinates.push(positionAttr.array[3 * index + 1]);
coordinates.push(positionAttr.array[3 * index + 2]);
expressIDs.push(expressIDAttr.getX(index));
alreadySaved.set(index, newIndex++);
}
const saved = alreadySaved.get(index);
newIndices.push(saved);
}
const geometryToExport = new BufferGeometry();
const newVerticesAttr = new BufferAttribute(Float32Array.from(coordinates), 3);
const newExpressIDAttr = new BufferAttribute(Uint32Array.from(expressIDs), 1);
geometryToExport.setAttribute('position', newVerticesAttr);
geometryToExport.setAttribute('expressID', newExpressIDAttr);
geometryToExport.setIndex(newIndices);
geometryToExport.groups = newGroups;
geometryToExport.computeVertexNormals();
const mesh = new Mesh(geometryToExport, newMaterials);
meshes.push(mesh);
}
viewer.IFC.loader.ifcManager.removeSubset(modelID, undefined, customID);
return meshes;
}

Google Users: List users data of a specific group

I am in the need of listing the users data belonging to a specific group within the organization. The documentation does not specify if this is possible. I was really hoping there could be some kind of query that would allow this. For example email in (1#domain.com,2#domain.com). However, I don't see that being possible. The only way I could think to accomplish this would be:
Get a list of all the members in the group (https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/list)
Get each user data by email (https://developers.google.com/admin-sdk/directory/reference/rest/v1/users/get)
The problem with the above approach is that if a group contains 50+ members, this means that I have to make all that amount of requests, which is counter productive. Imagine how long that would take.
Any ideas? Greatly appreciate it.
Unfortunately I don’t think you can skip this two step process, but you can speed it up using batch requests. This
allows you to request up to 1000 calls in a single request. The steps would be:
Make a batch request to get all the members of all the groups you want (using members.list).
Make a batch request to get all the user info that you need using their id (using user.get).
Notice that the data in the result won’t be sorted, but they will be tagged by Content-ID.
References
Sending Batch Requests (Directory API)
Method: members.list (Directory API)
Method: users.get (Directory API)
I thought about the batching request a couple of hours after I posted the question. The problem with Node JS is that it does not has built in support for batch requests, unlike the php client library for example; Therefore, I had to spent some time implementing support for it on my own since I was not able to find any example. I'll share the solution in case it helps someone else or for my future reference.
async function getGroupMembersData(){
const groupEmail = "group#domain.com"; //google group email
const groupMembers = await getGroupMembers(groupEmail).catch(error=>{
console.error(`Error querying group members: ${error.toString()}`);
});
if(!groupMembers){ return; }
const url = "https://www.googleapis.com/batch/admin/directory_v1";
const scopes = ["https://www.googleapis.com/auth/admin.directory.user.readonly"];
const requests = [];
for(let i=0; i<groupMembers.length; ++i){
const user = groupMembers[i];
const request = {
email: user,
endpoint: `GET directory_v1/admin/directory/v1/users/${user}?fields=*`
};
requests.push(request);
}
const batchRequestData = await batchProcess(url, scopes, requests).catch(error=>{
console.error(`Error processing batch request: ${error.toString()}`);
});
if(!batchRequestData){ return; }
const usersList = batchRequestData.map(i=>{
return i.responseBody;
});
console.log(usersList);
}
//get group members using group email address
async function getGroupMembers(groupKey){
const client = await getClient(scopes); //function to get an authorized client, you have to implement on your own
const service = google.admin({version: "directory_v1", auth: client});
const request = await service.members.list({
groupKey,
fields: "members(email)",
maxResults: 200
});
const members = !!request.data.members ? request.data.members.map(i=>i.email) : [];
return members;
}
//batch request processing in groups of 100
async function batchProcess(batchUrl, scopes, requests){
const client = await getClient(scopes); //function to get an authorized client, you have to implement on your own
let results = [];
const boundary = "foobar99998888"; //boundary line definition
let batchBody = ""; const nl = "\n";
const batchLimit = 100; //define batch limit (max supported = 100)
const totalRounds = Math.ceil(requests.length / batchLimit);
let batchRound = 1;
let batchItem = 0;
let roundLimit = batchLimit;
do{
roundLimit = roundLimit < requests.length ? roundLimit : requests.length;
//build the batch request body
for(batchItem; batchItem<roundLimit; batchItem++){
const requestData = requests[batchItem];
batchBody += `--${boundary}${nl}`;
batchBody += `Content-Type: application/http${nl}`;
batchBody += `Content-Id: <myapprequest-${requestData.email}>${nl}${nl}`;
batchBody += `${requestData.endpoint}${nl}`;
}
batchBody += `--${boundary}--`;
//send the batch request
const batchRequest = await client.request({
url: batchUrl,
method: "POST",
headers: {
"Content-Type": `multipart/mixed; boundary=${boundary}`
},
body: batchBody
}).catch(error=>{
console.log("Error processing batch request: " + error);
});
//parse the batch request response
if(!!batchRequest){
const batchResponseData = batchRequest.data;
const responseBoundary = batchRequest.headers["content-type"].split("; ")[1].replace("boundary=", "");
const httpResponses = batchResponseParser(batchResponseData, responseBoundary);
results.push(...httpResponses);
}
batchRound++;
roundLimit += batchLimit;
} while(batchRound <= totalRounds);
return results;
};
//batch response parser
function batchResponseParser(data, boundary){
const nl = "\r\n";
data = data.replace(`--${boundary}--`,"");
const responses = data.split(`--${boundary}`);
responses.shift();
const formattedResponses = responses.map(i=>{
const parts = i.split(`${nl}${nl}`);
const responseMetaParts = (parts[0].replace(nl, "")).split(nl);
let responseMeta = {};
responseMetaParts.forEach(part=>{
const objectParts = part.split(":");
responseMeta[objectParts[0].trim()] = objectParts[1].trim();
});
const responseHeadersParts = parts[1].split(nl);
let responseHeaders = {};
responseHeadersParts.forEach(part=>{
if(part.indexOf("HTTP/1.1") > -1){
responseHeaders.status = part;
} else {
const objectParts = part.split(":");
responseHeaders[objectParts[0].trim()] = objectParts[1].trim();
}
});
const reg = new RegExp(`${nl}`, "g");
const responseBody = JSON.parse(parts[2].replace(reg, ""));
const formatted = {
responseMeta: responseMeta,
responseHeaders: responseHeaders,
responseBody: responseBody
};
return formatted;
});
return formattedResponses;
}

Can you use if statements in .nest(). If not what is the best way to assign labels to array elements so they fit in several groups

Is it possible to use if statements within .nest()? I'm trying to populate a column with several keys, but instead of looking for a name I want to assign a name/value to individual rows based on whether the value in the column is within a certain range of numbers. I have looked at the D3 Nest Tutorial and examples and I'm still having a hard time. This is the code I have at the moment, which isn't working. Any help would be greatly appreciated!
fireballData.forEach( d => {
years.forEach(year => {
const latitude = d['Latitude (deg.)'];
const impactEnergy = d['Calculated Total Impact Energy (kt)'];
const impactTest = +impactEnergy;
const impactLevel = nest('impactTest')
.key(function(d) { if (d.impactTest < .5) {return "impactA" }});
// .sortKeys(d3.ascending)
// .key(function(d) { return d.impactEnergy; })
// .sortKeys(function(a,b) {
// return impactEnergy.indexOf(a) - impactEnergy.indexOf(b); })
// .sortValues(function(a,b) { return ((a.who < b.who)
// ? -1
// : 1);;
const longitude = d['Longitude (deg.)'];
const impactYear = d['PeakBrightnessDate_TimeUT'];
const row = {
//year,
impactLevel,
impactEnergy,
latitude,
longitude,
impactYear
};
console.log(row);
});
});
Just to be a bit more clear, I want to assign five or six impact levels to the column impactLevel based on how big of a value impactEnergy is. This would then leave each row with the impactLevel id (ie impactLevel1) followed by the rest of data of that object. Any solustions or suggestions are great appreciated! I am still very new to d3 and I am still learning the in's and out's.
So for what I was trying to accomplish using .nest wasn't a good solution. The better way of going about creating the new array was to use .map.
export const loadAndProcessData = () =>
Promise
.all([
csv('data.csv')
])
.then(([fireballData]) => {
const minYear = 2015;
const maxYear = 2100;
const years = range(minYear, maxYear + 1);
const data = [];
// Special thanks to Darshit Shah for helping me figure this bit out
const showData = fireballData.map(d =>{
const impactYear = d['PeakBrightnessDate_TimeUT'];
const latitude = d['Latitude (deg.)'];
const longitude = d['Longitude (deg.)'];
const impactEnergy = +d['Calculated Total Impact Energy (kt)'];
const target = impactEnergy;
const type = 'Impact Year: ' + impactYear + ', Latitude: ' + latitude + ', Longitude: ' + longitude;
return {
sorce: (impactEnergy < 0.15 ? "0-0.15" : (impactEnergy < 0.25 ? "0.15-0.25" : (impactEnergy < 0.5 ? "0.25-0.5" : ">0.5"))),
target,
type
}
});
data.push(showData);
return data;
`

RxJS several input streams. Proper way to deal with them?

I have five inputs on my app and I want to calculate the sum of each of them in a final number.
So far I did this:
const input1 = document.querySelector("#text1");
const input2 = document.querySelector("#text2");
const input3 = document.querySelector("#text3");
const input4 = document.querySelector("#text4");
const input5 = document.querySelector("#text5");
function setObservable(selector) {
return Rx.Observable
.fromEvent(selector, "input")
.map(event => parseInt(event.target.value))
.startWith(0)
}
const input$ = setObservable(input1)
const input2$ = setObservable(input2)
const input3$ = setObservable(input3)
const input4$ = setObservable(input4)
const input5$ = setObservable(input5)
const source = Rx.Observable.combineLatest(input$, input2$, input3$, input4$, input5$)
source.subscribe(value => {
const totalHTML = document.querySelector(".total");
const total = value.reduce((acc, curr) => acc + curr, 0);
totalHTML.textContent = total;
});
Which is working fine... but is there any more elegant way to get the values of each input without specifying all of them?
For sure! Let's store all your selectors in an array:
const selectors = ['#text1', '#text2', '#text3', '#text4', '#text5'];
And let's map over these to get the actual elements:
const elements = selectors.map(s => document.querySelector(s));
And now let's map over these elements and get an array of Observables:
const clickStreams = elements.map(setObservable);
And finally we can combine latest (like you did before):
const click$ = Rx.Observable.combineLatest(clickStreams);

Resources