content: /*
   * Copyright 2013 Hannes Janetzek
   * Copyright 2018 Gustl22
   *
   * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
   *
   * This program is free software: you can redistribute it and/or modify it under the
   * terms of the GNU Lesser General Public License as published by the Free Software
   * Foundation, either version 3 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 Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License along with
   * this program. If not, see <http://www.gnu.org/licenses/>.
   */
  package org.oscim.layers.tile;
  
  import org.oscim.layers.tile.MapTile.TileNode;
  import org.oscim.renderer.BufferObject;
  import org.oscim.renderer.GLViewport;
  import org.oscim.renderer.LayerRenderer;
  import org.oscim.renderer.MapRenderer;
  import org.oscim.renderer.bucket.RenderBuckets;
  import org.oscim.utils.ScanBox;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static org.oscim.layers.tile.MapTile.PROXY_PARENT;
  import static org.oscim.layers.tile.MapTile.State.NEW_DATA;
  import static org.oscim.layers.tile.MapTile.State.READY;
  
  public abstract class TileRenderer extends LayerRenderer {
      static final Logger log = LoggerFactory.getLogger(TileRenderer.class);
  
      /**
       * fade-in time
       */
      protected static final float FADE_TIME = 500;
      protected static final int MAX_TILE_LOAD = 8;
  
      private TileManager mTileManager;
  
      protected final TileSet mDrawTiles;
      protected int mProxyTileCnt;
  
      private int mOverdraw = 0;
      private float mAlpha = 1;
  
      protected int mOverdrawColor;
      protected float mLayerAlpha;
  
      private int mUploadSerial;
  
      public TileRenderer() {
          mUploadSerial = 0;
          mDrawTiles = new TileSet();
      }
  
      protected void setTileManager(TileManager tileManager) {
          mTileManager = tileManager;
      }
  
      /**
       * Threadsafe
       */
      public synchronized void setOverdrawColor(int color) {
          mOverdraw = color;
      }
  
      /**
       * Threadsafe
       */
      public synchronized void setBitmapAlpha(float alpha) {
          mAlpha = alpha;
      }
  
      /**
       * synced with clearTiles, setOverdrawColor and setBitmapAlpha
       */
      @Override
      public synchronized void update(GLViewport v) {
          /* count placeholder tiles */
  
          if (mAlpha == 0) {
              mDrawTiles.releaseTiles();
              setReady(false);
              return;
          }
  
          /* keep constant while rendering frame */
          mLayerAlpha = mAlpha;
          mOverdrawColor = mOverdraw;
  
          /* get current tiles to draw */
          synchronized (tilelock) {
              boolean tilesChanged = mTileManager.getActiveTiles(mDrawTiles);
  
              if (mDrawTiles.cnt == 0) {
                  setReady(false);
                  mProxyTileCnt = 0;
                  return;
              }
  
              /* update isVisible flag true for tiles that intersect view */
              if (tilesChanged || v.changed()) {
  
                  /* lock tiles while updating isVisible state */
                  mProxyTileCnt = 0;
  
                  MapTile[] tiles = mDrawTiles.tiles;
                  int tileZoom = tiles[0].zoomLevel;
  
                  for (int i = 0; i < mDrawTiles.cnt; i++)
                      tiles[i].isVisible = false;
  
                  /* check visibile tiles */
                  mScanBox.scan(v.pos.x, v.pos.y, v.pos.scale, tileZoom, v.plane);
              }
          }
          /* prepare tiles for rendering */
          if (compileTileLayers(mDrawTiles.tiles, mDrawTiles.cnt + mProxyTileCnt) > 0) {
              mUploadSerial++;
              BufferObject.checkBufferUsage(false);
          }
          setReady(true);
      }
  
      public void clearTiles() {
          synchronized (tilelock) {
              /* Clear all references to MapTiles as all current
               * tiles will also be removed from TileManager. */
              mDrawTiles.releaseTiles();
              mDrawTiles.tiles = new MapTile[1];
              mDrawTiles.cnt = 0;
          }
      }
  
      /**
       * compile tile layer data and upload to VBOs
       */
      private static int compileTileLayers(MapTile[] tiles, int tileCnt) {
          int uploadCnt = 0;
  
          for (int i = 0; i < tileCnt; i++) {
              MapTile tile = tiles[i];
  
              if (!tile.isVisible)
                  continue;
  
              if (tile.state(READY))
                  continue;
  
              if (tile.state(NEW_DATA)) {
                  uploadCnt += uploadTileData(tile);
                  continue;
              }
  
              /* load tile that is referenced by this holder */
              MapTile proxy = tile.holder;
              if (proxy != null && (proxy.state(NEW_DATA) || proxy.state(READY))) {
                  tile.state = NEW_DATA; // Change independently of proxy state, as long as it isn't READY
                  //uploadCnt += uploadTileData(proxy); // Should already been done in separate call
                  uploadCnt += uploadTileData(tile); // Actual tile must be loaded immediately
                  continue;
              }
  
              /* check near relatives than can serve as proxy */
              proxy = tile.getProxy(PROXY_PARENT, NEW_DATA);
              if (proxy != null) {
                  uploadCnt += uploadTileData(proxy);
                  /* don't load child proxies */
                  continue;
              }
  
              for (int c = 0; c < 4; c++) {
                  proxy = tile.getProxyChild(c, NEW_DATA);
                  if (proxy != null)
                      uploadCnt += uploadTileData(proxy);
              }
  
              if (uploadCnt >= MAX_TILE_LOAD)
                  break;
          }
          return uploadCnt;
      }
  
      private static int uploadTileData(MapTile tile) {
          tile.setState(READY);
          RenderBuckets buckets = tile.getBuckets();
  
          /* tile might only contain label layers */
          if (buckets == null)
              return 0;
  
          if (!buckets.compile(true)) {
              buckets.clear();
              return 0;
          }
  
          return 1;
      }
  
      private final Object tilelock = new Object();
  
      /**
       * Update tileSet with currently visible tiles to get a TileSet of currently visible tiles.
       */
      public boolean getVisibleTiles(TileSet tileSet) {
          return getVisibleTiles(tileSet, false) != null;
      }
  
      /**
       * Update tileSet with currently visible tiles to get a TileSet of currently visible tiles.
       * Replace tiles with ancestor (parent, etc.) if tiles are not loaded yet.
       *
       * @return original zoom level, otherwise null (if nothing can be loaded)
       */
      public Integer getVisibleTiles(TileSet tileSet, boolean replace) {
          if (tileSet == null)
              return null;
  
          if (mDrawTiles == null) {
              releaseTiles(tileSet);
              return null;
          }
  
          Integer zoom = null;
  
          int prevSerial = tileSet.serial;
  
          /* ensure tiles keep visible state */
          synchronized (tilelock) {
  
              MapTile[] newTiles = mDrawTiles.tiles;
              int cnt = mDrawTiles.cnt;
  
              /* ensure same size */
              if (tileSet.tiles.length != newTiles.length) {
                  tileSet.tiles = new MapTile[newTiles.length];
              }
  
              /* lock tiles to not be removed from cache */
              tileSet.cnt = 0;
              for (int i = 0; i < cnt; i++) {
                  MapTile t = newTiles[i];
                  if (t.isVisible && t.state(READY))
                      t.lock();
              }
  
              // Set main zoom level, even if no tiles are ready
              if (cnt > 0)
                  zoom = (int) newTiles[0].zoomLevel;
  
              /* unlock previous tiles */
              tileSet.releaseTiles();
  
              for (int i = 0; i < cnt; i++) {
                  MapTile t = newTiles[i];
                  if (t.isVisible) {
                      if (t.state(READY))
                          tileSet.tiles[tileSet.cnt++] = t;
                      else if (replace) {
                          // Replace with next available ancestor
                          for (int j = t.zoomLevel - 1; j > mTileManager.mMinZoom; j--) {
                              int diff = t.zoomLevel - j;
                              MapTile parent = mTileManager.getTile(t.tileX >> diff, t.tileY >> diff, j);
                              if (parent != null && parent.state(READY)) {
                                  parent.lock();
                                  tileSet.tiles[tileSet.cnt++] = parent;
                                  break;
                              }
                          }
                      }
                  }
              }
  
              tileSet.serial = mUploadSerial;
          }
  
          return prevSerial != tileSet.serial ? zoom : null;
      }
  
      public void releaseTiles(TileSet tileSet) {
          tileSet.releaseTiles();
      }
  
      /**
       * scanline fill class used to check tile visibility
       */
      private final ScanBox mScanBox = new ScanBox() {
          @Override
          protected void setVisible(int y, int x1, int x2) {
  
              MapTile[] tiles = mDrawTiles.tiles;
              int proxyOffset = mDrawTiles.cnt;
  
              for (int i = 0; i < proxyOffset; i++) {
                  MapTile t = tiles[i];
                  if (t.tileY == y && t.tileX >= x1 && t.tileX < x2)
                      t.isVisible = true;
              }
  
              /* add placeholder tiles to show both sides
               * of date line. a little too complicated... */
              int xmax = 1 << mZoom;
              if (x1 >= 0 && x2 < xmax)
                  return;
  
              O:
              for (int x = x1; x < x2; x++) {
                  if (x >= 0 && x < xmax)
                      continue;
  
                  int xx = x;
                  if (x < 0)
                      xx = xmax + x;
                  else
                      xx = x - xmax;
  
                  if (xx < 0 || xx >= xmax)
                      continue;
  
                  for (int i = proxyOffset; i < proxyOffset + mProxyTileCnt; i++)
                      if (tiles[i].tileX == x && tiles[i].tileY == y)
                          continue O;
  
                  MapTile tile = null;
                  for (int i = 0; i < proxyOffset; i++)
                      if (tiles[i].tileX == xx && tiles[i].tileY == y) {
                          tile = tiles[i];
                          break;
                      }
  
                  if (tile == null)
                      continue;
  
                  if (proxyOffset + mProxyTileCnt >= tiles.length) {
                      //log.error(" + mNumTileHolder");
                      break;
                  }
  
                  MapTile holder = new MapTile(null, x, y, (byte) mZoom);
                  holder.isVisible = true;
                  holder.holder = tile;
                  holder.state = tile.state;
                  tile.isVisible = true;
                  tiles[proxyOffset + mProxyTileCnt++] = holder;
              }
          }
      };
  
      /**
       * @param proxyLevel zoom-level of tile relative to current TileSet
       */
      public static long getMinFade(MapTile tile, int proxyLevel) {
          long minFade = MapRenderer.frametime - 50;
  
          /* check children for grandparent, parent or current */
          if (proxyLevel <= 0) {
              for (int c = 0; c < 4; c++) {
                  MapTile ci = tile.node.child(c);
                  if (ci == null)
                      continue;
  
                  if (ci.fadeTime > 0 && ci.fadeTime < minFade)
                      minFade = ci.fadeTime;
  
                  /* when drawing the parent of the current level
                   * we also check if the children of current level
                   * are visible */
                  if (proxyLevel >= -1) {
                      long m = getMinFade(ci, proxyLevel - 1);
                      if (m < minFade)
                          minFade = m;
                  }
              }
          }
  
          /* check parents for child, current or parent */
          TileNode p = tile.node.parent;
  
          for (int i = proxyLevel; i >= -1; i--) {
              if (p == null)
                  break;
  
              if (p.item != null && p.item.fadeTime > 0 && p.item.fadeTime < minFade)
                  minFade = p.item.fadeTime;
  
              p = p.parent;
          }
  
          return minFade;
      }
  }
  