لایه های داده را در TypeScript تجسم کنید

پاسخ لایه های داده در یک فایل GeoTIFF می آید. شما می توانید از ابزار خود برای به دست آوردن داده های مورد علاقه خود استفاده کنید. برای مثال، تصور کنید یک تصویر GeoTIFF دارید که مقادیر دما را در سراسر یک منطقه نشان می دهد. با استفاده از TypeScript، می‌توانید دمای پایین را به رنگ‌های آبی و دمای بالا را به قرمز ترسیم کنید تا تصویری رنگارنگ ایجاد کنید که برای تجسم الگوهای دما بلافاصله قابل درک است.

این کد TypeScript برای گرفتن فایل های تصویری خاص به نام GeoTIFF و نمایش آنها در یک وب سایت با استفاده از بوم HTML (مانند یک قاب عکس دیجیتال) طراحی شده است. کد از اجزای زیر استفاده می کند:

  • تصاویر GeoTIFF: GeoTIFF ها می توانند چندین لایه از داده های تصویر را ذخیره کنند، که آنها را برای نقشه ها یا تجزیه و تحلیل علمی مفید می کند.
  • تصاویر RGB: اینها انواع تصاویری هستند که بیشتر با آنها آشنا هستیم (مانند عکس). هر پیکسل دارای مقادیر قرمز، سبز و آبی است که رنگ را تعیین می کند.
  • پالت ها: مانند مجموعه های رنگ هستند. آنها حاوی لیستی از رنگ های از پیش تعریف شده هستند که می توانند برای رنگ آمیزی تصاویر استفاده شوند.

این صفحه نحوه دریافت مقادیر داده پیکسل (اطلاعات ذخیره شده در پیکسل های جداگانه یک تصویر دیجیتال، از جمله مقادیر رنگ و سایر ویژگی ها) را نشان می دهد و طول و عرض جغرافیایی را از GeoTIFF محاسبه می کند و آن را در یک شی TypeScript ذخیره می کند.

قطعه کد زیر تعریف نوع را نشان می دهد که در آن داده های مورد علاقه را در این مثال ذخیره می کنیم. فیلدها و نوع داده ها یک " نوع " در TypeScript است. برای این مثال خاص، ما انتخاب کردیم که امکان بررسی نوع، کاهش خطاهای نوع و افزودن قابلیت اطمینان به کد شما را فراهم کنیم تا نگهداری آن آسان‌تر شود. یک نوع برای ذخیره آن داده ها به منظور برگرداندن مقادیر متعدد مانند مقادیر پیکسل و کادر lat/long تعریف کنید.

export interface GeoTiff {
  width: number;
  height: number;
  rasters: Array<number>[];
  bounds: Bounds;
}

توابع اصلی

کد دارای چندین عملکرد است که با هم کار می کنند:

  • renderRGB : یک تصویر RGB GeoTIFF و در صورت تمایل یک ماسک (برای شفافیت) می گیرد، یک عنصر بوم وب سایت ایجاد می کند، از میان هر پیکسل GeoTIFF حلقه می زند، و پیکسل مربوطه را روی بوم رنگ می کند.
  • renderPalette : یک GeoTIFF با یک لایه داده و یک پالت رنگ می گیرد، مقادیر داده GeoTIFF را به رنگ های موجود در پالت نگاشت می کند، یک تصویر RGB جدید با استفاده از رنگ های پالت ایجاد می کند و renderRGB برای نمایش تصویر روی بوم فراخوانی می کند.

/**
 * Renders an RGB GeoTiff image into an HTML canvas.
 *
 * The GeoTiff image must include 3 rasters (bands) which
 * correspond to [Red, Green, Blue] in that order.
 *
 * @param  {GeoTiff} rgb   GeoTiff with RGB values of the image.
 * @param  {GeoTiff} mask  Optional mask for transparency, defaults to opaque.
 * @return {HTMLCanvasElement}  Canvas element with the rendered image.
 */
export function renderRGB(rgb: GeoTiff, mask?: GeoTiff): HTMLCanvasElement {
  // Create an HTML canvas to draw the image.
  // https://www.w3schools.com/tags/canvas_createimagedata.asp
  const canvas = document.createElement('canvas');

  // Set the canvas size to the mask size if it's available,
  // otherwise set it to the RGB data layer size.
  canvas.width = mask ? mask.width : rgb.width;
  canvas.height = mask ? mask.height : rgb.height;

  // Since the mask size can be different than the RGB data layer size,
  // we calculate the "delta" between the RGB layer size and the canvas/mask
  // size. For example, if the RGB layer size is the same as the canvas size,
  // the delta is 1. If the RGB layer size is smaller than the canvas size,
  // the delta would be greater than 1.
  // This is used to translate the index from the canvas to the RGB layer.
  const dw = rgb.width / canvas.width;
  const dh = rgb.height / canvas.height;

  // Get the canvas image data buffer.
  const ctx = canvas.getContext('2d')!;
  const img = ctx.getImageData(0, 0, canvas.width, canvas.height);

  // Fill in every pixel in the canvas with the corresponding RGB layer value.
  // Since Javascript doesn't support multidimensional arrays or tensors,
  // everything is stored in flat arrays and we have to keep track of the
  // indices for each row and column ourselves.
  for (let y = 0; y < canvas.height; y++) {
    for (let x = 0; x < canvas.width; x++) {
      // RGB index keeps track of the RGB layer position.
      // This is multiplied by the deltas since it might be a different
      // size than the image size.
      const rgbIdx = Math.floor(y * dh) * rgb.width + Math.floor(x * dw);
      // Mask index keeps track of the mask layer position.
      const maskIdx = y * canvas.width + x;

      // Image index keeps track of the canvas image position.
      // HTML canvas expects a flat array with consecutive RGBA values.
      // Each value in the image buffer must be between 0 and 255.
      // The Alpha value is the transparency of that pixel,
      // if a mask was not provided, we default to 255 which is opaque.
      const imgIdx = y * canvas.width * 4 + x * 4;
      img.data[imgIdx + 0] = rgb.rasters[0][rgbIdx]; // Red
      img.data[imgIdx + 1] = rgb.rasters[1][rgbIdx]; // Green
      img.data[imgIdx + 2] = rgb.rasters[2][rgbIdx]; // Blue
      img.data[imgIdx + 3] = mask // Alpha
        ? mask.rasters[0][maskIdx] * 255
        : 255;
    }
  }

  // Draw the image data buffer into the canvas context.
  ctx.putImageData(img, 0, 0);
  return canvas;
}

توابع کمکی

این کد همچنین شامل چندین توابع کمکی است که عملکردهای اضافی را فعال می کند:

  • createPalette : فهرستی از رنگ ها را برای رنگ آمیزی تصاویر بر اساس لیستی از کدهای رنگ هگزا دسیمال ایجاد می کند.
  • colorToRGB : کد رنگی مانند "#FF00FF" را به اجزای قرمز، سبز و آبی آن تبدیل می کند.
  • normalize , lerp , clamp : توابع کمکی ریاضی برای پردازش تصویر.

/**
 * Renders a single value GeoTiff image into an HTML canvas.
 *
 * The GeoTiff image must include 1 raster (band) which contains
 * the values we want to display.
 *
 * @param  {GeoTiff}  data    GeoTiff with the values of interest.
 * @param  {GeoTiff}  mask    Optional mask for transparency, defaults to opaque.
 * @param  {string[]} colors  Hex color palette, defaults to ['000000', 'ffffff'].
 * @param  {number}   min     Minimum value of the data range, defaults to 0.
 * @param  {number}   max     Maximum value of the data range, defaults to 1.
 * @param  {number}   index   Raster index for the data, defaults to 0.
 * @return {HTMLCanvasElement}  Canvas element with the rendered image.
 */
export function renderPalette({
  data,
  mask,
  colors,
  min,
  max,
  index,
}: {
  data: GeoTiff;
  mask?: GeoTiff;
  colors?: string[];
  min?: number;
  max?: number;
  index?: number;
}): HTMLCanvasElement {
  // First create a palette from a list of hex colors.
  const palette = createPalette(colors ?? ['000000', 'ffffff']);
  // Normalize each value of our raster/band of interest into indices,
  // such that they always map into a value within the palette.
  const indices = data.rasters[index ?? 0]
    .map((x) => normalize(x, max ?? 1, min ?? 0))
    .map((x) => Math.round(x * (palette.length - 1)));
  return renderRGB(
    {
      ...data,
      // Map each index into the corresponding RGB values.
      rasters: [
        indices.map((i: number) => palette[i].r),
        indices.map((i: number) => palette[i].g),
        indices.map((i: number) => palette[i].b),
      ],
    },
    mask,
  );
}

/**
 * Creates an {r, g, b} color palette from a hex list of colors.
 *
 * Each {r, g, b} value is a number between 0 and 255.
 * The created palette is always of size 256, regardless of the number of
 * hex colors passed in. Inbetween values are interpolated.
 *
 * @param  {string[]} hexColors  List of hex colors for the palette.
 * @return {{r, g, b}[]}         RGB values for the color palette.
 */
export function createPalette(hexColors: string[]): { r: number; g: number; b: number }[] {
  // Map each hex color into an RGB value.
  const rgb = hexColors.map(colorToRGB);
  // Create a palette with 256 colors derived from our rgb colors.
  const size = 256;
  const step = (rgb.length - 1) / (size - 1);
  return Array(size)
    .fill(0)
    .map((_, i) => {
      // Get the lower and upper indices for each color.
      const index = i * step;
      const lower = Math.floor(index);
      const upper = Math.ceil(index);
      // Interpolate between the colors to get the shades.
      return {
        r: lerp(rgb[lower].r, rgb[upper].r, index - lower),
        g: lerp(rgb[lower].g, rgb[upper].g, index - lower),
        b: lerp(rgb[lower].b, rgb[upper].b, index - lower),
      };
    });
}

/**
 * Convert a hex color into an {r, g, b} color.
 *
 * @param  {string} color  Hex color like 0099FF or #0099FF.
 * @return {{r, g, b}}     RGB values for that color.
 */
export function colorToRGB(color: string): { r: number; g: number; b: number } {
  const hex = color.startsWith('#') ? color.slice(1) : color;
  return {
    r: parseInt(hex.substring(0, 2), 16),
    g: parseInt(hex.substring(2, 4), 16),
    b: parseInt(hex.substring(4, 6), 16),
  };
}

/**
 * Normalizes a number to a given data range.
 *
 * @param  {number} x    Value of interest.
 * @param  {number} max  Maximum value in data range, defaults to 1.
 * @param  {number} min  Minimum value in data range, defaults to 0.
 * @return {number}      Normalized value.
 */
export function normalize(x: number, max: number = 1, min: number = 0): number {
  const y = (x - min) / (max - min);
  return clamp(y, 0, 1);
}

/**
 * Calculates the linear interpolation for a value within a range.
 *
 * @param  {number} x  Lower value in the range, when `t` is 0.
 * @param  {number} y  Upper value in the range, when `t` is 1.
 * @param  {number} t  "Time" between 0 and 1.
 * @return {number}    Inbetween value for that "time".
 */
export function lerp(x: number, y: number, t: number): number {
  return x + t * (y - x);
}

/**
 * Clamps a value to always be within a range.
 *
 * @param  {number} x    Value to clamp.
 * @param  {number} min  Minimum value in the range.
 * @param  {number} max  Maximum value in the range.
 * @return {number}      Clamped value.
 */
export function clamp(x: number, min: number, max: number): number {
  return Math.min(Math.max(x, min), max);
}