Threaded map tile (ofImage) loader crashes after some time

Hello everyone,

I’ve been using the modestMaps extension for Open Frameworks, but had trouble with the program crashing while trying to load tiles. After trying to find the problem, I decided to write my own, very basic code to load the tiles so I could simplify everything and hopefully figure out the problem. The software has to run in a public setting, so any crashes while it’s being used would be unpleasant.

For testing purposes, I’m having the map zoom in and out and move around automatically. It runs fine for about 20-40 minutes, then crashes with a EXC_BAD_ACCESS.

If I’m able to solve these crashes, I’ll try to publish my code in a neatly packaged add-on, so other people can use a well running map for their applications as well.

Here is the code I’m using to load and draw images, as well as a screenshot of the crash I’m getting.

I’ve tried the thread both with blocking on and blocking off, both seem to crash with the same errors.

Any help would be greatly appreciated.



hvMap.cpp

hvMap::hvMap() {
    imageLoader.startThread(true, false);
}

hvMap::~hvMap() {
    imageLoader.stopThread();
}

void hvMap::update() {

    //figure out which tiles to load
	int baseZoom = bestZoomForScale((float)sc);
	
	hvCoordinate startCoord = pointCoordinate(hvPixel(0,0)).zoomTo(baseZoom).container();
	hvCoordinate endCoord = pointCoordinate(hvPixel(width,height)).zoomTo(baseZoom).container().right().down();
    
	int minCol = startCoord.column;
	int maxCol = endCoord.column;
	int minRow = startCoord.row;
	int maxRow = endCoord.row;
    
	minCol -= GRID_PADDING;
	minRow -= GRID_PADDING;
	maxCol += GRID_PADDING;
	maxRow += GRID_PADDING;
    
    //ask the loader to load the tiles (loader checks if they are already loaded)
	for(int column = minCol; column <= maxCol; column++) {
		for(int row = minRow; row <= maxRow; row++) {
			hvCoordinate coordinate = hvCoordinate(row, column, baseZoom);
            loadTile(coordinate);
		}
	}
    
    //if the image is loaded, turn on the texture and update so we can draw the image
    if(imageLoader.lock()) {
        for(map<hvCoordinate, ofImage>::iterator it = imageLoader.loadedFiles.begin(); it != imageLoader.loadedFiles.end(); it++) {
            if(it->second.getWidth() > 0 && !it->second.isUsingTexture()) {
                it->second.setUseTexture(true);
                it->second.update();
            }
        }
        imageLoader.unlock();
    }
    
    //put all images outside of the screen and from other zoom levels in a vector to remove
    //TO DO: draw bigger images first, if not all 4 smaller images are loaded.
    remove.clear();
    if(imageLoader.lock()) {
        for(map<hvCoordinate, ofImage>::iterator it = imageLoader.loadedFiles.begin(); it != imageLoader.loadedFiles.end(); it++) {
            
            bool removing = false;
            
            if(
               it->first.column < minCol ||
               it->first.column > maxCol ||
               it->first.row < minRow ||
               it->first.row > maxRow
               ) {
                remove.push_back(hvCoordinate(it->first));
                removing = true;
            }
            
            if(!removing && it->first.zoom != baseZoom) {
                remove.push_back(hvCoordinate(it->first));
            }
            
        }
        imageLoader.unlock();
    }
    
    //remove the unneeded images
    for(int i = 0; i < remove.size(); i++) {
        removeTile(remove.at(i));
    }
    
}

void hvMap::draw() {

    //scale and move the tiles accordingly
    double correction = 1.0 / pow(2.0, bestZoomForScale((float)sc));
    ofPushMatrix();
	ofTranslate(width / 2.0, height / 2.0);
	ofScale(sc, sc, 1);
	ofTranslate(tx, ty);
    ofScale(correction, correction, 1);
    
    //draw the tiles
    if(imageLoader.lock()) {
        for(map<hvCoordinate, ofImage>::iterator it = imageLoader.loadedFiles.begin(); it != imageLoader.loadedFiles.end(); it++) {
            if(it->second.isAllocated() && it->second.isUsingTexture()) {
                it->second.draw(it->first.column * TILE_SIZE, it->first.row * TILE_SIZE, TILE_SIZE, TILE_SIZE);
            }
        }
        imageLoader.unlock();
    }
    
    ofPopMatrix();
    
}

void hvMap::loadTile(hvCoordinate coordinate) {
        
    if (coordinate.row >= 0 && coordinate.row < pow(2, coordinate.zoom)) {
        
        hvCoordinate c = sourceCoordinate(coordinate);
        
        string url = string("~/tiles/");
        url.append(ofToString((int)c.zoom));
        url.append("/");
        url.append(ofToString((int)c.column));
        url.append("/");
        url.append(ofToString((int)c.row));
        url.append(".png");

        imageLoader.loadCoordinate(coordinate, url);
        
    }
    
}

void hvMap::removeTile(hvCoordinate coordinate) {
    imageLoader.deleteCoordinate(coordinate);
}



hvImageLoader.h

#include "ofMain.h"
#include "hvCoordinate.h"

class hvImageLoader : public ofThread {
        
public:

    hvImageLoader();
    ~hvImageLoader();
    
    void setup();
    void update();
    
    void threadedFunction();

    void loadCoordinate(hvCoordinate coordinate, string url);
    void deleteCoordinate(hvCoordinate coordinate);
    
    map<hvCoordinate, string> filenames;
    map<hvCoordinate, ofImage> loadedFiles;   
    
};



hvImageLoader.cpp

void hvImageLoader::threadedFunction() {
    
    while(isThreadRunning()) {
        
        while(filenames.size() > 0) {
            
            ofImage image;
            image.setUseTexture(false);
            image.loadImage(ofFile(filenames.begin()->second));
            
            if(lock()) {
                loadedFiles[filenames.begin()->first] = image;
                filenames.erase(filenames.begin());
                unlock();
            } else {
                cout << "hvImageLoader::threadedFunction() --> lock() == false" << endl;
            }
            
        }
        
    }

}

void hvImageLoader::loadCoordinate(hvCoordinate coordinate, string url) {

    if(lock()) {
        if(
           filenames.count(coordinate) == 0 &&
           loadedFiles.count(coordinate) == 0
           ) {
            filenames[coordinate] = url;
        }
        unlock();
    } else {
        cout << "hvImageLoader::loadCoordinate() --> lock() == false" << endl;
    }
    
}

void hvImageLoader::deleteCoordinate(hvCoordinate coordinate) {
    
    if(lock()) {
        if(loadedFiles.count(coordinate) > 0) {
            //if(loadedFiles[coordinate].isUsingTexture()) {
                loadedFiles.erase(loadedFiles.find(coordinate));
            //}
        }
        unlock();
    } else {
        cout << "hvImageLoader::deleteCoordinate() --> lock() == false" << endl;
    }

}

I recently updated https://github.com/bakercp/ofxSlippyMaps to use the native tile loaders. It’s an unfinished addon, but might be a helpful solution or reference.