0

I'm struggling to wrap my head around the javascript async/sync concepts.

I've subclassed an openlayers Layer class for some custom rendering in a map. After instantiating an object of the class, I want to load data (a png file from an url) and save the data into a class variable before I go on and do some custom webgl rendering based on the data. I've tried all sorts of stuff now with awaits and thens, but unable to get something to work. I was hoping someone could give me some pointers on how I should structure code to achieve this.

I'm able to do it when I load the data outside the class, but then only with a fixed url, and I need to be able to change the url on certain events.

Code for instantiating object and trying to run the function that sets the data in a class variable:

function update_map(time_change_event) {
    const new_url = 'www.example.com/' + time_change_event.detail.key
    const canvasLayer = new CanvasLayer({})
    canvasLayer.getData(new_url)
    map.addLayer(canvasLayer)
    map.removeLayer(*oldlayer*)
}

Code for custom class:

export class CanvasLayer extends Layer {

    data = null;

    getData(url){

        async data_promise = new Promise((resolve, reject) => {
            const image = new Image();
            image.onload = () => {
                const canvas = document.createElement('canvas');
                canvas.width = image.width;
                canvas.height = image.height;
                const context = canvas.getContext('2d');
                context.drawImage(image, 0, 0);
                const data = context.getImageData(0, 0, width, height);
                resolve(data);
            
            };
            image.onerror = () => {
                reject(new Error('failed to load'));
            };

            image.src = url;
        })
    
        this.data = await data_promise;

    }

    render(frameState, target){
        // This is called when layer is added to map
        // Doing stuff with this.data does not work because it is null
        console.log(this.data) returns null
    }
}
5
  • The URL should begin with https:// or http://. Otherwise, www.example.com/ is treated as a folder name, not hostname. Commented Sep 18, 2024 at 20:48
  • 1
    async data_promise = new Promise - Is this some new syntax I'm not familiar with? this.data = await data_promise; - How can this work if getData isn't async? Overall it sounds like you want getData to be async and to await it when you invoke it? Commented Sep 18, 2024 at 20:49
  • @David Yes, but when I move "async" on the "get_data" function and use "await" directly in the "update_map" function, i get an error stating: 'await' expressions are only allowed within async functions and at the top levels of modules.ts(1308) Commented Sep 18, 2024 at 21:09
  • 1
    @Drublic: And do you make update_map also be async? The error means exactly what it says, await can only be used in an async function. (Which is why I'm surprised the code shown wasn't producing the same or a similar error...) Commented Sep 18, 2024 at 21:13
  • @David: You are right, if I add async to "update_map" and "get_data", and remove it from "data_promise", then I am allowed to use "await canvasLayer.getData(new_url)". And when I log "canvasLayer.data" immediately after, it prints the data correctly. So this actually solves the problem - thank you very much! I still don't understand what's happening conceptually - but I guess that is for me to figure out. Commented Sep 18, 2024 at 21:28

1 Answer 1

1

To expound on what @David was saying, there's a few things at play here with promises and async functions. Here's one option using just plain promises:

export class CanvasLayer extends Layer {
  data = new Promise()

  getData(url) {
    this.data = new Promise((resolve, reject) => {
      const image = new Image()
      image.onload = () => {
        const canvas = document.createElement('canvas')
        canvas.width = image.width
        canvas.height = image.height
        const context = canvas.getContext('2d')
        context.drawImage(image, 0, 0)
        const data = context.getImageData(0, 0, width, height)
        resolve(data)
      }
      image.onerror = () => {
        reject(new Error('failed to load'))
      }

      image.src = url
    })
  }

  render(frameState, target) {
    this.data.then((imageData) => {
      console.log('Image data:', imageData)
    })
  }
}

What this does is creates a new Promise and stores it in the class for later use. When you call render, you essentially just hook into that promise and tell it to run a function when the promise resolves.

You can also make the function async and have something like this:

async render(frameState, target) {
  let imageData = await this.data
  console.log('Image data:', imageData)
}

Explanation

Promises and async stuff can get really confusing in js. But here are some rules of thumb:

  1. await can only be used in an async function (as of right now in 2024, though this might change with some new js features like top level awaits)
  2. Promises are an object like anything else, its essentially your way of saying in the code "this won't have a value immediately, but I promise it will eventually arrive".
  3. You can either get a value from a promise by awaiting it, or by chaining a then to it; but either way you need to tell the code to "just wait until this value comes, you don't need to do anything else until then."

Here's some more info on async functions: async function - developer.mozilla.org

And some info on promises: Promise - developer.mozilla.org

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.