project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/MapPreviewGenerator.java
author nemo
Tue, 24 Oct 2017 18:55:19 -0400
changeset 12764 df9d9d19406a
parent 10017 de822cd3df3a
permissions -rw-r--r--
w is in pixels, so presumably this needs to be *4

/*
 * Hedgewars for Android. An Android port of Hedgewars, a free turn based strategy game
 * Copyright (C) 2012 Simeon Maxein <smaxein@googlemail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package org.hedgewars.hedgeroid;

import java.io.File;
import java.io.FileNotFoundException;

import org.hedgewars.hedgeroid.Datastructures.MapFile;
import org.hedgewars.hedgeroid.Datastructures.MapRecipe;
import org.hedgewars.hedgeroid.EngineProtocol.PascalExports;
import org.hedgewars.hedgeroid.frontlib.Flib;
import org.hedgewars.hedgeroid.frontlib.Frontlib;
import org.hedgewars.hedgeroid.frontlib.Frontlib.ByteArrayPtr;
import org.hedgewars.hedgeroid.frontlib.Frontlib.MapRecipePtr;
import org.hedgewars.hedgeroid.frontlib.Frontlib.MapconnPtr;
import org.hedgewars.hedgeroid.frontlib.Frontlib.MapimageCallback;
import org.hedgewars.hedgeroid.frontlib.Frontlib.StrCallback;
import org.hedgewars.hedgeroid.util.FileUtils;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import com.sun.jna.Pointer;

/**
 * A class that asynchronously generates a map preview from a MapRecipe.
 *
 * For named maps, this will load the preview image from the filesystem. For others,
 * it will call the Hedgewars engine to generate a preview image. The result is sent
 * back to a listener on the UI thread.
 */
public final class MapPreviewGenerator implements Runnable {
    private static final String TAG = MapPreviewGenerator.class.getSimpleName();
    private static final Handler mainHandler = new Handler(Looper.getMainLooper());
    private static final long TIMEOUT_NS = 20l * 1000 * 1000 * 1000;

    private final Context appContext;
    private final MapRecipe map;
    private final Listener listener;

    private boolean resultAvailable;
    private Drawable result;

    public static interface Listener {
        /**
         * This is called on the UI thread once the preview is ready or failed.
         * In case of failure, null is passed.
         */
        void onMapPreviewResult(Drawable preview);
    }

    private MapPreviewGenerator(Context appContext, MapRecipe map, Listener listener) {
        this.appContext = appContext;
        this.map = map;
        this.listener = listener;
    }

    public void run() {
        if (map.mapgen == Frontlib.MAPGEN_NAMED) {
            postToListener(loadPreviewFromFile(appContext, map.name));
        } else {
            resultAvailable = false;
            result = null;
            MapconnPtr conn = Flib.INSTANCE.flib_mapconn_create(MapRecipePtr.createJavaOwned(map));
            if (conn == null) {
                postToListener(null);
                return;
            }
            try {
                int port = Flib.INSTANCE.flib_mapconn_getport(conn);
                Flib.INSTANCE.flib_mapconn_onSuccess(conn, successCb, null);
                Flib.INSTANCE.flib_mapconn_onFailure(conn, failureCb, null);

                String configPath;
                try {
                    configPath = FileUtils.getCachePath(appContext).getAbsolutePath();
                } catch(FileNotFoundException e) {
                    return;
                }

                startEngine(configPath, port);
                long startTime = System.nanoTime();
                do {
                    Flib.INSTANCE.flib_mapconn_tick(conn);
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // ignore
                    }
                    if(System.nanoTime()-startTime > TIMEOUT_NS) {
                        Log.w(TAG, "Error generating map preview: timeout");
                        resultAvailable = true;
                    }
                } while(!resultAvailable);
            } finally {
                Flib.INSTANCE.flib_mapconn_destroy(conn);
                postToListener(result);
            }
        }
    }

    public static void startPreviewGeneration(Context appContext, MapRecipe map, Listener listener) {
        new Thread(new MapPreviewGenerator(appContext, map, listener)).start();
    }

    private static Drawable loadPreviewFromFile(Context appContext, String mapName) {
        if(!mapName.startsWith("+")) {
            try {
                File previewFile = MapFile.getPreviewFile(appContext, mapName);
                return Drawable.createFromPath(previewFile.getAbsolutePath());
            } catch (FileNotFoundException e) {
                Log.w("MapPreviewGenerator", "Preview for map "+mapName+" not found.");
            }
        }
        return null;
    }

    private static void startEngine(final String configPath, final int port) {
        new Thread(new Runnable() {
            public void run() {
                Log.d(TAG, "Starting engine "+port);
                PascalExports.synchronizedGenLandPreview(port);
                Log.d(TAG, "Engine finished");
            }
        }).start();
    }

    private void postToListener(final Drawable result) {
        mainHandler.post(new Runnable() {
            public void run() {
                listener.onMapPreviewResult(result);
            }
        });
    }

    /**
     * Let's be extra nice here and clip off the left and right sides, so the preview is centered...
     * Since the image is present in bytes, we can save some effort by checking entire byte-columns first.
     */
    private final MapimageCallback successCb = new MapimageCallback() {
        public void callback(Pointer context, ByteArrayPtr buffer, int hedgehogCount) {
            byte[] mapdata = buffer.deref(Frontlib.MAPIMAGE_BYTES);

            int leftmostPixel = Frontlib.MAPIMAGE_WIDTH;
            int rightmostPixel = -1;
            int bytesPerLine = Frontlib.MAPIMAGE_WIDTH/8;

            // Find the leftmost pixel
            for(int xbyte=0; xbyte<bytesPerLine; xbyte++) {
                for(int y=0; y<Frontlib.MAPIMAGE_HEIGHT; y++) {
                    int b = 0xff&mapdata[xbyte+y*bytesPerLine];
                    if(b != 0) {
                        leftmostPixel = Math.min(leftmostPixel, Integer.numberOfLeadingZeros(b)-24+xbyte*8);
                    }
                }
                if(leftmostPixel!=Frontlib.MAPIMAGE_WIDTH) break;
            }

            // Find the rightmost pixel
            for(int xbyte=bytesPerLine-1; xbyte>=0; xbyte--) {
                for(int y=0; y<Frontlib.MAPIMAGE_HEIGHT; y++) {
                    int b = mapdata[xbyte+y*bytesPerLine];
                    if(b != 0) {
                        rightmostPixel = Math.max(rightmostPixel, xbyte*8+7-Integer.numberOfTrailingZeros(b));
                    }
                }
                if(rightmostPixel!=-1) break;
            }

            // No pixel was set at all -> use default width
            if(rightmostPixel==-1) {
                leftmostPixel = 0;
                rightmostPixel = Frontlib.MAPIMAGE_WIDTH-1;
            }

            Bitmap bitmap = Bitmap.createBitmap(rightmostPixel-leftmostPixel+1, Frontlib.MAPIMAGE_HEIGHT, Config.ARGB_8888);
            for(int y=0; y<Frontlib.MAPIMAGE_HEIGHT; y++) {
                for(int x=0; x<bitmap.getWidth(); x++) {
                    bitmap.setPixel(x, y, isPixelSet(mapdata, x+leftmostPixel, y) ? Color.YELLOW : Color.TRANSPARENT);
                }
            }
            result = new BitmapDrawable(bitmap);
            resultAvailable = true;
        }
    };

    private static boolean isPixelSet(byte[] imgdata, int x, int y) {
        int pixelnum = x+Frontlib.MAPIMAGE_WIDTH*y;
        return (imgdata[pixelnum>>3] & (128>>(pixelnum&7))) != 0;
    }

    private final StrCallback failureCb = new StrCallback() {
        public void callback(Pointer context, String reason) {
            Log.w(TAG, "Error generating map preview: "+reason);
            result = null;
            resultAvailable = true;
        }
    };
}