Back to project page WolframCA.
The source code is released under:
Apache License
If you think the Android project WolframCA listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * WolframCA - an android application to view 1-dimensional cellular automata (CA) * Copyright 2013 Barry O'Neill (http://barryoneill.net/) */*from ww w. j av a 2 s .c o m*/ * Licensed under Apache 2.0 with limited permission from, and no affiliation with Steven * Wolfram, LLC. See the LICENSE file in the root of this project for the full license terms. */ package net.nologin.meep.ca.model; import android.content.Context; import android.graphics.*; import android.util.Log; import net.nologin.meep.ca.R; import net.nologin.meep.ca.WolframUtils; import net.nologin.meep.tbv.GridAnchor; import net.nologin.meep.tbv.Tile; import net.nologin.meep.tbv.TileProvider; import net.nologin.meep.tbv.TileRange; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; /** * The WolframCA provider is a {@link TileProvider} implementation for the {@link net.nologin.meep.ca.view.WolframCAView}. * The grid is made up of tiles: Each tile contains many rows of 'cells', where each successive row (across all tiles in * which it appears) represents successive generations of a * <a href="http://mathworld.wolfram.com/ElementaryCellularAutomaton.html">Wolfram CA Rule</a>. The number of cell * rows (generations) per tile is configured by the view. * <br/><br/> * The implementation of this gets complicated by the tiled nature of the provider. Since each cell's state is dependent * on the previous generations's state and that of its two neighbours, cells that are at the edges of tiles are dependent * on cells that exist in other tiles. * <br/><br/> * For example, consider the following view of a tile, t (populated with <i>a-e</i>): * <pre> * | 54321 | 11111 | 12345 | * -+-------+-------+-------+- * | 5432 | aaaaa | 2345 | * | 543 | bbbbb | 345 | * | 54 | ccccc | 45 | * | 5 | ddddd | 5 | * | | eeeee | | * -+-------+-------+-------+- * | | | | * * </pre> * For cells in row 'a', we need all the cells marked '1'. For row 'b', we need all cells in area 'a', and those * marked '2', and so on. As we go down the rows in the tile, our list of prerequisite cells grows as well, * and results in an inverted triangle of prerequisite cells (and by extension, tiles). * <br/><br/> * Note: For the first row of cells in the first row of tiles, there are no dependencies - We manually set this row/ * generation as all off/false, but leave the center cell in tile x=0 to be true/on). This is our starting generation. * All other cell rows follow the 3-cell dependency. * <br/><br/> * <b>Implementation</b>:<br/> * When the view requests a set of tiles, all prerequisite tiles are added to a processing queue, and a background * task processess these in such an order that the dependencies are met for each tile. In an ideal world, we'd * just store the bitmap, and move on. Unfortnately, this will quickly lead to the exhaustion of heap space. * Rather than resort to local storage, we wipe the bitmaps of tiles that have gone far enough out of view. However, * we maintain a boolean array in each tile containing the state of the last cell row/generation. That way, when a * tile is re-requested, rather than build all the dependent tiles all over again, we only need go one tile row up, * and we have the state required to regenerate the cell data we need. * <br><br/> */ public class WolframTileProvider implements TileProvider { /* Improvements that can be made in future versions: * - Increase heap usage (see OFFSCREEN_TILE_BUFFER) - possibly to the exclusions of older devices * - Add a flag to tiles that result in common patterns (eg all off/on), and when requests for bitmap data * occur, reuse a common copy. * - Many rules result in repeating patters, perhaps add detection for these, cutting off a lot of calculation * - Add hints to many of the rules that don't generate anything on one half of the space (eg rule 110), so we * can shortcircuit processing/prerequisites for those. */ /** * Default rule (110), should a rule not be specified during construction or via {@link #setPixelsPerCell(int)} */ public static final int DEFAULT_RULE = 110; /** * Default zoom level (size of cell width in pixels). If {@link WolframUtils#getNumCores()} reports a single * core device, this will have value 6 (less processor intensive), otherwise value 2 (finer detail). */ public static final int DEFAULT_ZOOMLEVEL = WolframUtils.getNumCores() == 1 ? 6 : 2; // how many tiles wide a tile can be scrolled offscreen before getting cleared (caching performance v memory) private static final int OFFSCREEN_TILE_BUFFER = 3; private int ruleNo; private int pixelsPerCell; private int colorPixelOn, colorPixelOff; // background tasks here will set it, view's rendering thread (via hasFreshData()) will poll it private AtomicBoolean hasFreshData = new AtomicBoolean(false); /* All referenced tiles get cached here, even though their bitmap content will be cleared as necessary (see * OFFSCREEN_TILE_BUFFER). Must be mulithread friendly as it'll be accessed indirectly by view's rendering thread * via getTile(), as well as by tile generation stuff here. */ private final ConcurrentMap<Long, WolframTile> tileCache; private ExecutorService executorService; private Future lastSubmittedTask; /** * Constructor, defaulting the rule number to {@link #DEFAULT_RULE} and zoom level to {@link #DEFAULT_ZOOMLEVEL} * * @param ctx the context */ public WolframTileProvider(Context ctx) { this(ctx, DEFAULT_RULE, DEFAULT_ZOOMLEVEL); } /** * Constructor * * @param ctx The context * @param ruleNo The rule number (0-255). Invalid rule numbers will result in {@link #DEFAULT_RULE}. * @param zoomLevel The zoomLevel (1-16, step of 2). Invalid levels will be adjusted to the nearest valid value. */ public WolframTileProvider(Context ctx, int ruleNo, int zoomLevel) { this.ruleNo = ruleNo < 1 || ruleNo > 255 ? DEFAULT_RULE : ruleNo; this.pixelsPerCell = zoomLevel < 1 ? DEFAULT_ZOOMLEVEL : WolframUtils.sanitizeZoom(zoomLevel); // an easy future feature would be to make this configurable colorPixelOn = ctx.getResources().getColor(R.color.CAView_PixelOn); colorPixelOff = ctx.getResources().getColor(R.color.CAView_PixelOff); // as mentioned in field comment, this should be multi-thread friendly tileCache = new ConcurrentHashMap<Long, WolframTile>(); Log.i(WolframUtils.LOG_TAG, "WolframTileProvider created, rule=" + ruleNo + ", pixelsPerCell=" + pixelsPerCell); } /** * @return The currently set rule number */ public int getRule() { return ruleNo; } /** * Set a new rule number * * @param newRule The new rule number. If not in range 0-255, {@link #DEFAULT_RULE} will be used */ public void setRule(int newRule) { if (newRule < 1 || newRule > 255) { Log.w(WolframUtils.LOG_TAG, "Rule " + newRule + " not in range 0-255, defaulting to " + DEFAULT_RULE); newRule = DEFAULT_RULE; } ruleNo = newRule; tileCache.clear(); } /** * Get the number of pixels wide that each cell will be rendered * * @return The number of pixels */ public int getPixelsPerCell() { return pixelsPerCell; } /** * Set the number of pixels wide that each cell should be rendered * * @param newZoom The number of pixels (1-16, step of 2). Invalid values will be adjusted to the nearest valid one. */ public void setPixelsPerCell(int newZoom) { newZoom = WolframUtils.sanitizeZoom(newZoom); pixelsPerCell = newZoom; tileCache.clear(); } @Override public Integer[] getConfigTileIDLimits() { /* Each CA generation is rendered below the previous, so there's no sense in letting the user scroll upwards * beyond y=0 - there's nothing to generate there. (All other scrolling is unlimited) */ return new Integer[]{null, 0, null, null}; } @Override public GridAnchor getConfigGridAnchor() { // have the (0,0) tile (where our first generation is drawn) be anchored to the top of the screen return GridAnchor.TopCenter; } @Override public int getConfigTileSize() { return Tile.DEFAULT_TILE_SIZE; // default is fine } @Override public WolframTile getTile(int xId, int yId) { // Return cache hits, otherwise create, cache and return. Async processing triggered by onTileIDRangeChange WolframTile t = tileCache.get(Tile.createCacheKey(xId, yId)); if (t != null) { return t; } t = new WolframTile(xId, yId); tileCache.put(t.cacheKey, t); return t; } @Override public boolean hasFreshData() { // Set by WolframQueueProcessorTask on new data. Reset value on poll to prevent pointless re-rendering. return hasFreshData.getAndSet(false); } @Override public void onZoomFactorChange(float newZoom) { // NOP - At the moment, pixelsPerCell is set by a menu item, but pinch-to-zoom is a possibile future feature } @Override public void onTileIDRangeChange(TileRange newRange) { // free up bitmap data of non-rendered tiles (observing OFFSCREEN_TILE_BUFFER). Tile lastCellRow stays. Collection<WolframTile> entries = tileCache.values(); for (WolframTile t : entries) { if (t.lastCellRow != null && t.getBmpData() != null && !newRange.contains(t, OFFSCREEN_TILE_BUFFER)) { t.clearBmpData(); } } List<WolframTile> renderQueue = new LinkedList<WolframTile>(); /* Adding the tiles to the renderQueue in a row-by-row, cell-by-cell manner is not very optimal, given that * the chain of prerequisites (calculated by addPrerequisites) is an inverted triangle. It's better to add * the tiles that are vertically center in newRange first, then work outwards. This results in these more * central tiles appearing first, before boundary tiles that may require a lot of off-screen tile processing. * I.e. - The user sees a constant trickle of tiles, rather than nothing for a while, then all tiles at once. */ int num_tiles_horizontal = newRange.numTilesHorizontal(); int half_tiles_horizontal = num_tiles_horizontal / 2; for (int y = newRange.top; y <= newRange.bottom; y++) { for (int i = 0; i < num_tiles_horizontal; i++) { // an even i results in the next tile to the right, an odd to the left int offset = half_tiles_horizontal + (i % 2 == 0 ? i / 2 : -(i / 2 + 1)); int x = newRange.left + offset; // the impl of getTile adds the tile to the cache! WolframTile t = getTile(x, y); if (t.getBmpData() != null) { continue; } addPrerequisites(t, renderQueue); renderQueue.add(t); } } // init the executor if not ready, and try to kill any previous task, should one exist if (executorService == null || executorService.isShutdown()) { executorService = Executors.newSingleThreadExecutor(); } if (lastSubmittedTask != null) { lastSubmittedTask.cancel(true); } // fire off the async job Log.d(WolframUtils.LOG_TAG, "Starting async queue processing, queue size:" + renderQueue.size()); lastSubmittedTask = executorService.submit(new WolframQueueProcessorTask(renderQueue, newRange)); } /** * Add any (unprocessed) preqrequisite tiles for the specified tile to the queue. To generate any given tile, * we need access to the last generation of cells in the above left, above, and above right tiles. If those * tiles haven't been processed, we keep iterating up the inverse triangle of dependencies (adding all those * found to the queue), until we hit the top row (y=0). * * @param t The tile whose prerequisite tiles we are searching for * @param renderQueue The queue to add any unprocessed prerequisite tiles to */ private void addPrerequisites(WolframTile t, List<WolframTile> renderQueue) { List<WolframTile> deps = new LinkedList<WolframTile>(); if (t.yId <= 0) { // top tile doesn't have prerequisite tiles return; } int curY = t.yId - 1; // start one tile row up int curXMin = t.xId - 1, curXMax = t.xId + 1; // scan from y-1 to y+1 of that parent tile row // keep looping up to the top tile row (y=0) unless we hit a set of already processed prerequisite tiles first while (curY >= 0) { boolean foundMissing = false; for (int x = curXMin; x <= curXMax; x++) { // the impl of getTile() above puts the tile in the cache if it wasn't already there WolframTile preReq = getTile(x, curY); if (!renderQueue.contains(preReq) && preReq.lastCellRow == null) { foundMissing = true; deps.add(preReq); } } // if the current level of prerequisite tiles are already processed, we're done if (!foundMissing) { break; } // move up a tile row, expand left and right by one tile curXMin--; curXMax++; curY--; } // we added deps while working upwards - reverse this so higher up tiles get processed first! Collections.reverse(deps); renderQueue.addAll(deps); } /** * An instance of this task is started every time {@link #onTileIDRangeChange(net.nologin.meep.tbv.TileRange)} * generates a new list of required tiles. This task processes each tile in order, and when done, toggles the * flag that {@link #hasFreshData()} checks when polled. */ class WolframQueueProcessorTask implements Runnable { private List<WolframTile> renderQueue; private TileRange visibleRange; public WolframQueueProcessorTask(List<WolframTile> renderQueue, TileRange visibleRange) { this.renderQueue = renderQueue; this.visibleRange = visibleRange; } @Override public void run() { Log.d(WolframUtils.LOG_TAG, "WolframQueueProcessorTask task starting"); /* this task ends as soon as the queue is finished. If the provider detects a change in the * list of required tiles, it'll start a new task (requesting that this one stop) */ while (!renderQueue.isEmpty()) { // check for interruption every time, we need to stop quickly if requested if (Thread.currentThread().isInterrupted()) { Log.d(WolframUtils.LOG_TAG, "WolframQueueProcessorTask interrupted (new task incoming?)"); return; } WolframTile t = renderQueue.remove(0); // sanity check for null or already processed tiles if (t == null || t.getBmpData() != null) { continue; } Log.d(WolframUtils.LOG_TAG, "WolframQueueProcessorTask processing tile " + t); processTileState(t, visibleRange.contains(t)); // allow the hasFreshData() interface method to report that there's new data available hasFreshData.set(true); } Log.d(WolframUtils.LOG_TAG, "WolframQueueProcessorTask task finished normally"); } /** * Calculate the state of all the cells in the tile. When done, we record the state of the last row of cells * in the tile. If requested, the bitmap data to be rendered for this tile is also generated and stored. * * @param t The tile to process * @param fillBitmap If <code>true</code>, the bitmap data for this cell is generated and stored in the tile, * otherwise just the last cell row will be stored. */ private void processTileState(WolframTile t, boolean fillBitmap) { // how many cells high/wide our square tile measures int cellsPerEdge = Tile.DEFAULT_TILE_SIZE / pixelsPerCell; // bitmap data is stored as an int array updated row by row, then converted to bmp at the end int[] bmpData = null; if (fillBitmap) { bmpData = new int[cellsPerEdge * cellsPerEdge]; // rows * cols } // 'current' generation, across 3 tiles (tile t in center) boolean[] curGenCells = new boolean[cellsPerEdge * 3]; // 'next' generation (same length) boolean[] nextGenCells = new boolean[curGenCells.length]; /* Copy the last cell row in the top-left, top and top-right tiles into each third of the 'curGenCells' * array respectively - this is our 'starting state' (first tile row has no starting state) */ if (t.yId != 0) { int yAbove = t.yId - 1; try { boolean[] stateAL = getLastCellRowState(t.xId - 1, yAbove); boolean[] stateA = getLastCellRowState(t.xId, yAbove); boolean[] stateAR = getLastCellRowState(t.xId + 1, yAbove); System.arraycopy(stateAL, 0, curGenCells, 0, cellsPerEdge); System.arraycopy(stateA, 0, curGenCells, cellsPerEdge, cellsPerEdge); System.arraycopy(stateAR, 0, curGenCells, cellsPerEdge * 2, cellsPerEdge); } catch (IllegalStateException e) { Log.w(WolframUtils.LOG_TAG, "Cannot process tile " + t + ", error:" + e.getMessage()); return; } } // set end pointers to one element in from each side of nextGenCells (see class doc for logic) int leftPtr = 1, rightPtr = curGenCells.length - 2; for (int row = 0; row < cellsPerEdge; row++) { // for each row of cells in tile t if (row == 0 && t.yId == 0) { /* For all our rules, we start our very first row of cells, in every tile on the first row (y=0) * to be false(off), except for one cell right in the middle of tile x=0. This is our CA starting * data. * * If the xId==0, we set the first cell row's middle cell to true. However, we shouldn't forget * that neighbouring tiles x=-1 and x=1 need to see this value (in the right and left segments of * nextGenCells respectively) */ if ((t.xId == -1 || t.xId == 0 || t.xId == 1)) { nextGenCells[nextGenCells.length / 2 - (t.xId * cellsPerEdge)] = true; } } else { // for all other cell rows in all other tiles, simply do a rule lookup for (int col = leftPtr; col <= rightPtr; col++) { nextGenCells[col] = WolframRuleTable.getNextState(ruleNo, curGenCells[col - 1], curGenCells[col], curGenCells[col + 1]); } } // mid segment of nextGenCells holds the data for the respective row of the bitmap content (if needed) if (fillBitmap) { int rowOffset = row * cellsPerEdge; for (int col = cellsPerEdge; col < cellsPerEdge * 2; col++) { int val = nextGenCells[col] ? colorPixelOn : colorPixelOff; bmpData[rowOffset + col - cellsPerEdge] = val; } } /* finally, regardless of whether we want a bitmap or not, we keep a copy of the last cell row * (the mid segment of nextGenCells) */ if (row == cellsPerEdge - 1) { t.lastCellRow = new boolean[cellsPerEdge]; System.arraycopy(nextGenCells, cellsPerEdge, t.lastCellRow, 0, cellsPerEdge); } // the 'next' generation becomes the current, and we loop System.arraycopy(nextGenCells, 0, curGenCells, 0, curGenCells.length); } // finally, if needed, we convert the int array of colours into the desired bitmap if (fillBitmap) { Bitmap bmp = Bitmap.createBitmap(cellsPerEdge, cellsPerEdge, Bitmap.Config.RGB_565); bmp.setPixels(bmpData, 0, cellsPerEdge, 0, 0, cellsPerEdge, cellsPerEdge); t.setBmpData(Bitmap.createScaledBitmap(bmp, Tile.DEFAULT_TILE_SIZE, Tile.DEFAULT_TILE_SIZE, false)); } } // Convenience method to get the last row of cells from the desired tile with some sanity checking private boolean[] getLastCellRowState(int xId, int yId) { WolframTile tile = tileCache.get(Tile.createCacheKey(xId, yId)); if (tile == null) { throw new IllegalStateException("Prerequisite tile (" + xId + "," + yId + ") not in cache"); } if (tile.lastCellRow == null) { throw new IllegalStateException("Prerequisite tile (" + xId + "," + yId + ") not yet processed"); } return tile.lastCellRow; } } @Override public void onSurfaceDestroyed() { // ensure we don't leave any hanging threads if (executorService != null) { executorService.shutdownNow(); } } @Override public String getDebugSummary() { return String.format("WolframProv[r=%d,cache=%d]", ruleNo, tileCache.size()); } }