| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585 | import Cartesian4 from "../Core/Cartesian4.js";import CesiumMath from "../Core/Math.js";import Check from "../Core/Check.js";import Color from "../Core/Color.js";import defaultValue from "../Core/defaultValue.js";import defined from "../Core/defined.js";import DeveloperError from "../Core/DeveloperError.js";import mergeSort from "../Core/mergeSort.js";import PixelFormat from "../Core/PixelFormat.js";import PixelDatatype from "../Renderer/PixelDatatype.js";import Sampler from "../Renderer/Sampler.js";import Texture from "../Renderer/Texture.js";import TextureMagnificationFilter from "../Renderer/TextureMagnificationFilter.js";import TextureMinificationFilter from "../Renderer/TextureMinificationFilter.js";import TextureWrap from "../Renderer/TextureWrap.js";import Material from "./Material.js";const scratchColor = new Color();const scratchColorAbove = new Color();const scratchColorBelow = new Color();const scratchColorBlend = new Color();const scratchPackedFloat = new Cartesian4();const scratchColorBytes = new Uint8Array(4);function lerpEntryColor(height, entryBefore, entryAfter, result) {  const lerpFactor =    entryBefore.height === entryAfter.height      ? 0.0      : (height - entryBefore.height) /        (entryAfter.height - entryBefore.height);  return Color.lerp(entryBefore.color, entryAfter.color, lerpFactor, result);}function createNewEntry(height, color) {  return {    height: height,    color: Color.clone(color),  };}function removeDuplicates(entries) {  // This function expects entries to be sorted from lowest to highest.  // Remove entries that have the same height as before and after.  entries = entries.filter(function (entry, index, array) {    const hasPrev = index > 0;    const hasNext = index < array.length - 1;    const sameHeightAsPrev = hasPrev      ? entry.height === array[index - 1].height      : true;    const sameHeightAsNext = hasNext      ? entry.height === array[index + 1].height      : true;    const keep = !sameHeightAsPrev || !sameHeightAsNext;    return keep;  });  // Remove entries that have the same color as before and after.  entries = entries.filter(function (entry, index, array) {    const hasPrev = index > 0;    const hasNext = index < array.length - 1;    const sameColorAsPrev = hasPrev      ? Color.equals(entry.color, array[index - 1].color)      : false;    const sameColorAsNext = hasNext      ? Color.equals(entry.color, array[index + 1].color)      : false;    const keep = !sameColorAsPrev || !sameColorAsNext;    return keep;  });  // Also remove entries that have the same height AND color as the entry before.  entries = entries.filter(function (entry, index, array) {    const hasPrev = index > 0;    const sameColorAsPrev = hasPrev      ? Color.equals(entry.color, array[index - 1].color)      : false;    const sameHeightAsPrev = hasPrev      ? entry.height === array[index - 1].height      : true;    const keep = !sameColorAsPrev || !sameHeightAsPrev;    return keep;  });  return entries;}function preprocess(layers) {  let i, j;  const layeredEntries = [];  const layersLength = layers.length;  for (i = 0; i < layersLength; i++) {    const layer = layers[i];    const entriesOrig = layer.entries;    const entriesLength = entriesOrig.length;    //>>includeStart('debug', pragmas.debug);    if (!Array.isArray(entriesOrig) || entriesLength === 0) {      throw new DeveloperError("entries must be an array with size > 0.");    }    //>>includeEnd('debug');    let entries = [];    for (j = 0; j < entriesLength; j++) {      const entryOrig = entriesOrig[j];      //>>includeStart('debug', pragmas.debug);      if (!defined(entryOrig.height)) {        throw new DeveloperError("entry requires a height.");      }      if (!defined(entryOrig.color)) {        throw new DeveloperError("entry requires a color.");      }      //>>includeEnd('debug');      const height = CesiumMath.clamp(        entryOrig.height,        createElevationBandMaterial._minimumHeight,        createElevationBandMaterial._maximumHeight      );      // premultiplied alpha      const color = Color.clone(entryOrig.color, scratchColor);      color.red *= color.alpha;      color.green *= color.alpha;      color.blue *= color.alpha;      entries.push(createNewEntry(height, color));    }    let sortedAscending = true;    let sortedDescending = true;    for (j = 0; j < entriesLength - 1; j++) {      const currEntry = entries[j + 0];      const nextEntry = entries[j + 1];      sortedAscending = sortedAscending && currEntry.height <= nextEntry.height;      sortedDescending =        sortedDescending && currEntry.height >= nextEntry.height;    }    // When the array is fully descending, reverse it.    if (sortedDescending) {      entries = entries.reverse();    } else if (!sortedAscending) {      // Stable sort from lowest to greatest height.      mergeSort(entries, function (a, b) {        return CesiumMath.sign(a.height - b.height);      });    }    let extendDownwards = defaultValue(layer.extendDownwards, false);    let extendUpwards = defaultValue(layer.extendUpwards, false);    // Interpret a single entry to extend all the way up and down.    if (entries.length === 1 && !extendDownwards && !extendUpwards) {      extendDownwards = true;      extendUpwards = true;    }    if (extendDownwards) {      entries.splice(        0,        0,        createNewEntry(          createElevationBandMaterial._minimumHeight,          entries[0].color        )      );    }    if (extendUpwards) {      entries.splice(        entries.length,        0,        createNewEntry(          createElevationBandMaterial._maximumHeight,          entries[entries.length - 1].color        )      );    }    entries = removeDuplicates(entries);    layeredEntries.push(entries);  }  return layeredEntries;}function createLayeredEntries(layers) {  // clean up the input data and check for errors  const layeredEntries = preprocess(layers);  let entriesAccumNext = [];  let entriesAccumCurr = [];  let i;  function addEntry(height, color) {    entriesAccumNext.push(createNewEntry(height, color));  }  function addBlendEntry(height, a, b) {    let result = Color.multiplyByScalar(b, 1.0 - a.alpha, scratchColorBlend);    result = Color.add(result, a, result);    addEntry(height, result);  }  // alpha blend new layers on top of old ones  const layerLength = layeredEntries.length;  for (i = 0; i < layerLength; i++) {    const entries = layeredEntries[i];    let idx = 0;    let accumIdx = 0;    // swap the arrays    entriesAccumCurr = entriesAccumNext;    entriesAccumNext = [];    const entriesLength = entries.length;    const entriesAccumLength = entriesAccumCurr.length;    while (idx < entriesLength || accumIdx < entriesAccumLength) {      const entry = idx < entriesLength ? entries[idx] : undefined;      const prevEntry = idx > 0 ? entries[idx - 1] : undefined;      const nextEntry = idx < entriesLength - 1 ? entries[idx + 1] : undefined;      const entryAccum =        accumIdx < entriesAccumLength ? entriesAccumCurr[accumIdx] : undefined;      const prevEntryAccum =        accumIdx > 0 ? entriesAccumCurr[accumIdx - 1] : undefined;      const nextEntryAccum =        accumIdx < entriesAccumLength - 1          ? entriesAccumCurr[accumIdx + 1]          : undefined;      if (        defined(entry) &&        defined(entryAccum) &&        entry.height === entryAccum.height      ) {        // New entry directly on top of accum entry        const isSplitAccum =          defined(nextEntryAccum) &&          entryAccum.height === nextEntryAccum.height;        const isStartAccum = !defined(prevEntryAccum);        const isEndAccum = !defined(nextEntryAccum);        const isSplit = defined(nextEntry) && entry.height === nextEntry.height;        const isStart = !defined(prevEntry);        const isEnd = !defined(nextEntry);        if (isSplitAccum) {          if (isSplit) {            addBlendEntry(entry.height, entry.color, entryAccum.color);            addBlendEntry(entry.height, nextEntry.color, nextEntryAccum.color);          } else if (isStart) {            addEntry(entry.height, entryAccum.color);            addBlendEntry(entry.height, entry.color, nextEntryAccum.color);          } else if (isEnd) {            addBlendEntry(entry.height, entry.color, entryAccum.color);            addEntry(entry.height, nextEntryAccum.color);          } else {            addBlendEntry(entry.height, entry.color, entryAccum.color);            addBlendEntry(entry.height, entry.color, nextEntryAccum.color);          }        } else if (isStartAccum) {          if (isSplit) {            addEntry(entry.height, entry.color);            addBlendEntry(entry.height, nextEntry.color, entryAccum.color);          } else if (isEnd) {            addEntry(entry.height, entry.color);            addEntry(entry.height, entryAccum.color);          } else if (isStart) {            addBlendEntry(entry.height, entry.color, entryAccum.color);          } else {            addEntry(entry.height, entry.color);            addBlendEntry(entry.height, entry.color, entryAccum.color);          }        } else if (isEndAccum) {          if (isSplit) {            addBlendEntry(entry.height, entry.color, entryAccum.color);            addEntry(entry.height, nextEntry.color);          } else if (isStart) {            addEntry(entry.height, entryAccum.color);            addEntry(entry.height, entry.color);          } else if (isEnd) {            addBlendEntry(entry.height, entry.color, entryAccum.color);          } else {            addBlendEntry(entry.height, entry.color, entryAccum.color);            addEntry(entry.height, entry.color);          }        } else {          // eslint-disable-next-line no-lonely-if          if (isSplit) {            addBlendEntry(entry.height, entry.color, entryAccum.color);            addBlendEntry(entry.height, nextEntry.color, entryAccum.color);          } else if (isStart) {            addEntry(entry.height, entryAccum.color);            addBlendEntry(entry.height, entry.color, entryAccum.color);          } else if (isEnd) {            addBlendEntry(entry.height, entry.color, entryAccum.color);            addEntry(entry.height, entryAccum.color);          } else {            addBlendEntry(entry.height, entry.color, entryAccum.color);          }        }        idx += isSplit ? 2 : 1;        accumIdx += isSplitAccum ? 2 : 1;      } else if (        defined(entry) &&        defined(entryAccum) &&        defined(prevEntryAccum) &&        entry.height < entryAccum.height      ) {        // New entry between two accum entries        const colorBelow = lerpEntryColor(          entry.height,          prevEntryAccum,          entryAccum,          scratchColorBelow        );        if (!defined(prevEntry)) {          addEntry(entry.height, colorBelow);          addBlendEntry(entry.height, entry.color, colorBelow);        } else if (!defined(nextEntry)) {          addBlendEntry(entry.height, entry.color, colorBelow);          addEntry(entry.height, colorBelow);        } else {          addBlendEntry(entry.height, entry.color, colorBelow);        }        idx++;      } else if (        defined(entryAccum) &&        defined(entry) &&        defined(prevEntry) &&        entryAccum.height < entry.height      ) {        // Accum entry between two new entries        const colorAbove = lerpEntryColor(          entryAccum.height,          prevEntry,          entry,          scratchColorAbove        );        if (!defined(prevEntryAccum)) {          addEntry(entryAccum.height, colorAbove);          addBlendEntry(entryAccum.height, colorAbove, entryAccum.color);        } else if (!defined(nextEntryAccum)) {          addBlendEntry(entryAccum.height, colorAbove, entryAccum.color);          addEntry(entryAccum.height, colorAbove);        } else {          addBlendEntry(entryAccum.height, colorAbove, entryAccum.color);        }        accumIdx++;      } else if (        defined(entry) &&        (!defined(entryAccum) || entry.height < entryAccum.height)      ) {        // New entry completely before or completely after accum entries        if (          defined(entryAccum) &&          !defined(prevEntryAccum) &&          !defined(nextEntry)        ) {          // Insert blank gap between last entry and first accum entry          addEntry(entry.height, entry.color);          addEntry(entry.height, createElevationBandMaterial._emptyColor);          addEntry(entryAccum.height, createElevationBandMaterial._emptyColor);        } else if (          !defined(entryAccum) &&          defined(prevEntryAccum) &&          !defined(prevEntry)        ) {          // Insert blank gap between last accum entry and first entry          addEntry(            prevEntryAccum.height,            createElevationBandMaterial._emptyColor          );          addEntry(entry.height, createElevationBandMaterial._emptyColor);          addEntry(entry.height, entry.color);        } else {          addEntry(entry.height, entry.color);        }        idx++;      } else if (        defined(entryAccum) &&        (!defined(entry) || entryAccum.height < entry.height)      ) {        // Accum entry completely before or completely after new entries        addEntry(entryAccum.height, entryAccum.color);        accumIdx++;      }    }  }  // one final cleanup pass in case duplicate colors show up in the final result  const allEntries = removeDuplicates(entriesAccumNext);  return allEntries;}/** * @typedef createElevationBandMaterialEntry * * @property {Number} height The height. * @property {Color} color The color at this height. *//** * @typedef createElevationBandMaterialBand * * @property {createElevationBandMaterialEntry[]} entries A list of elevation entries. They will automatically be sorted from lowest to highest. If there is only one entry and <code>extendsDownards</code> and <code>extendUpwards</code> are both <code>false</code>, they will both be set to <code>true</code>. * @property {Boolean} [extendDownwards=false] If <code>true</code>, the band's minimum elevation color will extend infinitely downwards. * @property {Boolean} [extendUpwards=false] If <code>true</code>, the band's maximum elevation color will extend infinitely upwards. *//** * Creates a {@link Material} that combines multiple layers of color/gradient bands and maps them to terrain heights. * * The shader does a binary search over all the heights to find out which colors are above and below a given height, and * interpolates between them for the final color. This material supports hundreds of entries relatively cheaply. * * @function createElevationBandMaterial * * @param {Object} options Object with the following properties: * @param {Scene} options.scene The scene where the visualization is taking place. * @param {createElevationBandMaterialBand[]} options.layers A list of bands ordered from lowest to highest precedence. * @returns {Material} A new {@link Material} instance. * * @demo {@link https://sandcastle.cesium.com/index.html?src=Elevation%20Band%20Material.html|Cesium Sandcastle Elevation Band Demo} * * @example * scene.globe.material = Cesium.createElevationBandMaterial({ *     scene : scene, *     layers : [{ *         entries : [{ *             height : 4200.0, *             color : new Cesium.Color(0.0, 0.0, 0.0, 1.0) *         }, { *             height : 8848.0, *             color : new Cesium.Color(1.0, 1.0, 1.0, 1.0) *         }], *         extendDownwards : true, *         extendUpwards : true, *     }, { *         entries : [{ *             height : 7000.0, *             color : new Cesium.Color(1.0, 0.0, 0.0, 0.5) *         }, { *             height : 7100.0, *             color : new Cesium.Color(1.0, 0.0, 0.0, 0.5) *         }] *     }] * }); */function createElevationBandMaterial(options) {  options = defaultValue(options, defaultValue.EMPTY_OBJECT);  const scene = options.scene;  const layers = options.layers;  //>>includeStart('debug', pragmas.debug);  Check.typeOf.object("options.scene", scene);  Check.defined("options.layers", layers);  Check.typeOf.number.greaterThan("options.layers.length", layers.length, 0);  //>>includeEnd('debug');  const entries = createLayeredEntries(layers);  const entriesLength = entries.length;  let i;  let heightTexBuffer;  let heightTexDatatype;  let heightTexFormat;  const isPackedHeight = !createElevationBandMaterial._useFloatTexture(    scene.context  );  if (isPackedHeight) {    heightTexDatatype = PixelDatatype.UNSIGNED_BYTE;    heightTexFormat = PixelFormat.RGBA;    heightTexBuffer = new Uint8Array(entriesLength * 4);    for (i = 0; i < entriesLength; i++) {      Cartesian4.packFloat(entries[i].height, scratchPackedFloat);      Cartesian4.pack(scratchPackedFloat, heightTexBuffer, i * 4);    }  } else {    heightTexDatatype = PixelDatatype.FLOAT;    heightTexFormat = PixelFormat.LUMINANCE;    heightTexBuffer = new Float32Array(entriesLength);    for (i = 0; i < entriesLength; i++) {      heightTexBuffer[i] = entries[i].height;    }  }  const heightsTex = Texture.create({    context: scene.context,    pixelFormat: heightTexFormat,    pixelDatatype: heightTexDatatype,    source: {      arrayBufferView: heightTexBuffer,      width: entriesLength,      height: 1,    },    sampler: new Sampler({      wrapS: TextureWrap.CLAMP_TO_EDGE,      wrapT: TextureWrap.CLAMP_TO_EDGE,      minificationFilter: TextureMinificationFilter.NEAREST,      magnificationFilter: TextureMagnificationFilter.NEAREST,    }),  });  const colorsArray = new Uint8Array(entriesLength * 4);  for (i = 0; i < entriesLength; i++) {    const color = entries[i].color;    color.toBytes(scratchColorBytes);    colorsArray[i * 4 + 0] = scratchColorBytes[0];    colorsArray[i * 4 + 1] = scratchColorBytes[1];    colorsArray[i * 4 + 2] = scratchColorBytes[2];    colorsArray[i * 4 + 3] = scratchColorBytes[3];  }  const colorsTex = Texture.create({    context: scene.context,    pixelFormat: PixelFormat.RGBA,    pixelDatatype: PixelDatatype.UNSIGNED_BYTE,    source: {      arrayBufferView: colorsArray,      width: entriesLength,      height: 1,    },    sampler: new Sampler({      wrapS: TextureWrap.CLAMP_TO_EDGE,      wrapT: TextureWrap.CLAMP_TO_EDGE,      minificationFilter: TextureMinificationFilter.LINEAR,      magnificationFilter: TextureMagnificationFilter.LINEAR,    }),  });  const material = Material.fromType("ElevationBand", {    heights: heightsTex,    colors: colorsTex,  });  return material;}/** * Function for checking if the context will allow floating point textures for heights. * * @param {Context} context The {@link Context}. * @returns {Boolean} <code>true</code> if floating point textures can be used for heights. * @private */createElevationBandMaterial._useFloatTexture = function (context) {  return context.floatingPointTexture;};/** * This is the height that gets stored in the texture when using extendUpwards. * There's nothing special about it, it's just a really big number. * @private */createElevationBandMaterial._maximumHeight = +5906376425472;/** * This is the height that gets stored in the texture when using extendDownwards. * There's nothing special about it, it's just a really big number. * @private */createElevationBandMaterial._minimumHeight = -5906376425472;/** * Color used to create empty space in the color texture * @private */createElevationBandMaterial._emptyColor = new Color(0.0, 0.0, 0.0, 0.0);export default createElevationBandMaterial;
 |