Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Static Image is not cleared when change projection #13397

Closed
yonda-yonda opened this issue Feb 19, 2022 · 1 comment · Fixed by #13398
Closed

Static Image is not cleared when change projection #13397

yonda-yonda opened this issue Feb 19, 2022 · 1 comment · Fixed by #13398
Labels

Comments

@yonda-yonda
Copy link
Contributor

Describe the bug
Please watch this movie.
154806128-df2bc1de-eef7-4bf0-8dee-bfb1dc50b05a

To Reproduce

  1. Set Image Source, projection is EPSG:4326, extent is [0,0,10,10].
  2. First, render with EPSG:3857. Displayed at the image at correct position.
  3. Change map projection to EPSG:3413. At this time, center is [0, 0], zoom is 4 (Avoid including point of [lon, lat]=[10, 10] in screen).
  4. The image remain in the center, should disappear.

Expected behavior
Image is cleared when prepare frame.

sample
I checked ol@v6.12.0.

import Map from 'ol/Map';
import {
  Image as ImageLayer,
  Tile as TileLayer
} from 'ol/layer';
import Static from 'ol/source/ImageStatic';
import View from 'ol/View';
import proj4 from 'proj4';
import {
  OSM,
} from 'ol/source';
import {
  getCenter,
} from 'ol/extent';
import {
  get as getProjection,
} from 'ol/proj';
import {
  register
} from 'ol/proj/proj4';
proj4.defs(
  'EPSG:3413',
  '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 ' +
  '+x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'
);
register(proj4);
const proj3413 = getProjection('EPSG:3413');
proj3413.setExtent([-4194304 * 10, -4194304 * 10, 4194304 * 10, 4194304 * 10]);

const layers = {};
layers['osm'] = new TileLayer({
  source: new OSM(),
});
layers['osm'].setProperties({
  name: "osm"
})

layers['static'] = new ImageLayer({
  source: new Static({
    url: 'https://openlayers.org/assets/theme/img/logo70.png',
    crossOrigin: '',
    projection: 'EPSG:4326',
    imageExtent: [0, 0, 10, 10],
    interpolate: true,
  })
})

const map = new Map({
  layers: [layers['osm'], layers['static']],
  target: 'map',
  view: new View({
    projection: 'EPSG:3857',
    center: [0, 0],
    zoom: 4,
  }),
});
const viewProjSelect = document.getElementById('view-projection');

function updateViewProjection() {
  const newProj = getProjection(viewProjSelect.value);
  const newProjExtent = newProj.getExtent();
  const newView = new View({
    projection: newProj,
    center: getCenter(newProjExtent || [0, 0, 0, 0]),
    zoom: 4,
    extent: newProjExtent || undefined,
  });
  map.setView(newView);
}
/**
 * Handle change event.
 */
viewProjSelect.onchange = function () {
  updateViewProjection();
};

updateViewProjection();
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Raster Reprojection</title>
    <!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
    <script src="https://unpkg.com/elm-pep"></script>
    <!-- The lines below are only needed for old environments like Internet Explorer and Android 4.x -->
    <script src="https://cdn.polyfill.io/v3/polyfill.min.js?features=fetch,requestAnimationFrame,Element.prototype.classList,TextDecoder"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/3.18.3/minified.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.8.1/css/ol.css" type="text/css">
    <style>
      .map {
        width: 100%;
        height:400px;
      }
      .form-inline label {
        justify-content: left;
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <form class="form-inline">
      <label for="view-projection">View projection:</label>
      <select id="view-projection">
        <option value="EPSG:3857">Spherical Mercator (EPSG:3857)</option>
        <option value="EPSG:3413">NSIDC Polar Stereographic North (EPSG:3413)</option>
      </select>
    </form>
    <script src="test.js" type="module"></script>
  </body>
</html>
parcel test.html
@yonda-yonda
Copy link
Contributor Author

yonda-yonda commented Mar 19, 2022

Thanks @ahocevar for your care for #13459 .
I've been thinking about that test case, and realized that there are a few other problems with ImageStatic re-projection.

For example, in the case of source projection is EPSG:4326, view projection is EPSG:3995,

  1. For some z, a part of the image is missing (See pictures below).
  2. Extent crushed, ImageStatic is not displayed. When [140, 0, 150, 10] at EPSG:4326, extent transform to EPSG:3995 failed.

correct
correct

missing part.
missing

Projection is so difficult!!
I learned ImageStatic should be used without re-projection.
I've given up fixing ImageStatic, sorry.

Instead, I found a solution with TileImage, though it is limited to EPSG:4326. Look at the code below.

const layer = new TileLayer({
  source: new ImageWGS84({
    url: '~',
    crossOrigin: "Anonymous",
  }),
});
class ImageWGS84 extends TileImage {
  constructor(opt_options) {
    const options = opt_options || {};
    const tileSize = options.tileSize ? options.size : DEFAULT_TILE_SIZE;

    const gridExtent = [-180, -90, 180, 90];
    const tileGrid = new TileGrid({
      extent: gridExtent,
      tileSize: tileSize,
      resolutions: resolutionsFromExtent(gridExtent, opt_options.maxZoom, tileSize, 360 / tileSize),
    });

    const tileLoadFunction =
      (imageTile, coordString) => {
        const [z, x, y] = coordString.split(",").map(Number);

        const canvas = document.createElement("canvas");
        canvas.width = tileSize;
        canvas.height = tileSize;
        const context = canvas.getContext("2d");
        if (!context || !this._context) {
          imageTile.setState(TileState.ERROR);
          return;
        }

        const [leftTopLon, leftTopLat, size] = toLonLat(z, x, y);
        const [
          imageExtentLeft, imageExtentBottom, imageExtentRight, imageExtentTop
        ] = this._imageExtent;

        let [
          tileLeft, tileBottom, tileRight, tileTop
        ] = [
          leftTopLon, leftTopLat - size, leftTopLon + size, leftTopLat
        ];
        if (imageExtentLeft < -180 && 0 < tileRight) {
          tileLeft -= 360;
          tileRight -= 360;
        } else if (180 < imageExtentRight && tileLeft < 0) {
          tileLeft += 360;
          tileRight += 360;
        }

        if (!crossing([
            imageExtentLeft, imageExtentBottom, imageExtentRight, imageExtentTop
          ], [
            tileLeft, tileBottom, tileRight, tileTop
          ])) {
          imageTile.setState(TileState.EMPTY);
        }

        const sourcePerPixel = [(imageExtentRight - imageExtentLeft) / this._context.canvas.width, (imageExtentTop - imageExtentBottom) / this._context.canvas.height];
        const tilePerPixel = [(tileRight - tileLeft) / tileSize, (tileTop - tileBottom) / tileSize];
        const leftBottom = [Math.max(tileLeft, imageExtentLeft), Math.max(imageExtentBottom, tileBottom)];
        const rightTop = [Math.min(tileRight, imageExtentRight), Math.min(imageExtentTop, tileTop)];
        const tileRect = [
          Math.round((leftBottom[0] - tileLeft) / tilePerPixel[0]), Math.round((tileTop - rightTop[1]) / tilePerPixel[1]),
          Math.round((rightTop[0] - tileLeft) / tilePerPixel[0]), Math.round((tileTop - leftBottom[1]) / tilePerPixel[1])
        ];
        const sourceRect = [
          Math.round((leftBottom[0] - imageExtentLeft) / sourcePerPixel[0]), Math.round((imageExtentTop - rightTop[1]) / sourcePerPixel[1]),
          Math.round((rightTop[0] - imageExtentLeft) / sourcePerPixel[0]), Math.round((imageExtentTop - leftBottom[1]) / sourcePerPixel[1])
        ];
        const sourceRectSize = [sourceRect[2] - sourceRect[0], sourceRect[3] - sourceRect[1]];
        const tileRectSize = [tileRect[2] - tileRect[0], tileRect[3] - tileRect[1]];

        if (Math.min(...sourceRectSize, ...tileRectSize) <= 0) {
          imageTile.setState(TileState.EMPTY);
          return
        }

        const tempCanvas = document.createElement("canvas");
        tempCanvas.width = sourceRectSize[0];
        tempCanvas.height = sourceRectSize[1];
        const tempContext = tempCanvas.getContext("2d");
        if (tempContext) {
          tempContext.clearRect(0, 0, sourceRectSize[0], sourceRectSize[1]);
          tempContext.putImageData(this._context.getImageData(sourceRect[0], sourceRect[1], sourceRectSize[0], sourceRectSize[1]), 0, 0);
          context.drawImage(tempCanvas, 0, 0, sourceRectSize[0], sourceRectSize[1], tileRect[0], tileRect[1], tileRectSize[0], tileRectSize[1]);
        }

        imageTile.getImage().src =
          canvas.toDataURL();

      };

    let interpolate =
      options.imageSmoothing !== undefined ? options.imageSmoothing : true;
    if (options.interpolate !== undefined) {
      interpolate = options.interpolate;
    }
    super({
      state: options.url ? SourceState.LOADING : SourceState.UNDEFINED,
      attributions: options.attributions,
      cacheSize: options.cacheSize,
      crossOrigin: options.crossOrigin,
      interpolate: interpolate,
      opaque: options.opaque,
      projection: "EPSG:4326",
      reprojectionErrorThreshold: options.reprojectionErrorThreshold,
      tileGrid: tileGrid,
      tileLoadFunction,
      tilePixelRatio: options.tilePixelRatio,
      url: "{z},{x},{y}",
      wrapX: options.wrapX !== undefined ? options.wrapX : true,
      transition: options.transition,
      attributionsCollapsible: options.attributionsCollapsible,
    });
    this._imageExtent = [Infinity, Infinity, -Infinity, -Infinity];
    if (Array.isArray(options.imageExtent) && options.imageExtent.length > 3) {
      this._imageExtent = options.imageExtent;
    }
    if (
      this._imageExtent[0] < -360 || 360 < this._imageExtent[0] ||
      this._imageExtent[1] < -90 || 90 < this._imageExtent[1] ||
      this._imageExtent[2] < -360 || 360 < this._imageExtent[2] ||
      this._imageExtent[3] < -90 || 90 < this._imageExtent[3] ||
      this._imageExtent[2] <= this._imageExtent[0] ||
      this._imageExtent[0] - this._imageExtent[2] > 360
    ) throw new Error("invalid extent.");

    let rad = 0;
    if (options.rotate) {
      rad = options.rotate;
      this._imageExtent = rotateExtent(this._imageExtent, rad)
    }

    const canvas = document.createElement("canvas");
    const context = canvas.getContext("2d");

    if (!context) return;
    context.imageSmoothingEnabled = interpolate !== undefined ? interpolate : true;
    this._context = context;

    const image = new Image();
    image.crossOrigin = options.crossOrigin;

    image.addEventListener("load", () => {
      const rotatedCoordinates = rotateExtent([0, 0, image.width, image.height], rad);
      const rotatedWidth = rotatedCoordinates[2] - rotatedCoordinates[0];
      const rotatedHeight = rotatedCoordinates[3] - rotatedCoordinates[1];
      this._context.canvas.width = rotatedWidth;
      this._context.canvas.height = rotatedHeight;

      context.save();
      context.translate(rotatedWidth / 2, rotatedHeight / 2);
      context.rotate(-rad);
      this._context.drawImage(image, -(image.width / 2), -(image.height / 2));
      context.restore();
      this.setState(SourceState.READY);
    });
    image.addEventListener("error", () => {
      this.setState(SourceState.ERROR);
    });

    image.src = options.url;
  }
}

function rotateExtent(extent, rad) {
  const center = [(extent[2] - extent[0]) / 2 + extent[0], (extent[3] - extent[1]) / 2 + extent[1]];
  const leftTop = [
    (extent[0] - center[0]) * Math.cos(rad) - (extent[3] - center[1]) * Math.sin(rad),
    (extent[0] - center[0]) * Math.sin(rad) + (extent[3] - center[1]) * Math.cos(rad),
  ];
  const leftBottom = [
    (extent[0] - center[0]) * Math.cos(rad) - (extent[1] - center[1]) * Math.sin(rad),
    (extent[0] - center[0]) * Math.sin(rad) + (extent[1] - center[1]) * Math.cos(rad),
  ];
  const rightBottom = [
    (extent[2] - center[0]) * Math.cos(rad) - (extent[1] - center[1]) * Math.sin(rad),
    (extent[2] - center[0]) * Math.sin(rad) + (extent[1] - center[1]) * Math.cos(rad),
  ];
  const rightTop = [
    (extent[2] - center[0]) * Math.cos(rad) - (extent[3] - center[1]) * Math.sin(rad),
    (extent[2] - center[0]) * Math.sin(rad) + (extent[3] - center[1]) * Math.cos(rad),
  ];

  return [
    Math.min(leftTop[0], leftBottom[0], rightBottom[0], rightTop[0]) + center[0],
    Math.min(leftTop[1], leftBottom[1], rightBottom[1], rightTop[1]) + center[1],
    Math.max(leftTop[0], leftBottom[0], rightBottom[0], rightTop[0]) + center[0],
    Math.max(leftTop[1], leftBottom[1], rightBottom[1], rightTop[1]) + center[1],
  ];
}

function resolutionsFromExtent(
  extent,
  opt_maxZoom,
  opt_tileSize,
  opt_maxResolution
) {
  const maxZoom = opt_maxZoom !== undefined ? opt_maxZoom : DEFAULT_MAX_ZOOM;

  const height = getHeight(extent);
  const width = getWidth(extent);

  const tileSize = toSize(
    opt_tileSize !== undefined ? opt_tileSize : DEFAULT_TILE_SIZE
  );
  const maxResolution =
    opt_maxResolution > 0 ?
    opt_maxResolution :
    Math.max(width / tileSize[0], height / tileSize[1]);

  const length = maxZoom + 1;
  const resolutions = new Array(length);
  for (let z = 0; z < length; ++z) {
    resolutions[z] = maxResolution / Math.pow(2, z + 1);
  }
  return resolutions;
}
function toLonLat(z, x, y) {
  const size = 180 / (2 ** z);
  const lon = -180 + x * size;
  const lat = 90 - y * size;
  return [lon, lat, size]
}
function crossing(extentLeft, extentRight) {
  return Math.max(extentLeft[0], extentRight[0]) <= Math.min(extentLeft[2], extentRight[2]) && Math.max(extentLeft[1], extentRight[1]) <= Math.min(extentLeft[3], extentRight[3])
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant