| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703 | import Cartesian3 from "../Core/Cartesian3.js";import defined from "../Core/defined.js";import Cartographic from "../Core/Cartographic.js";import Ellipsoid from "../Core/Ellipsoid.js";import Intersect from "../Core/Intersect.js";import Matrix3 from "../Core/Matrix3.js";import Plane from "../Core/Plane.js";import CoplanarPolygonOutlineGeometry from "../Core/CoplanarPolygonOutlineGeometry.js";import BoundingSphere from "../Core/BoundingSphere.js";import Check from "../Core/Check.js";import ColorGeometryInstanceAttribute from "../Core/ColorGeometryInstanceAttribute.js";import defaultValue from "../Core/defaultValue.js";import GeometryInstance from "../Core/GeometryInstance.js";import Matrix4 from "../Core/Matrix4.js";import PerInstanceColorAppearance from "./PerInstanceColorAppearance.js";import Primitive from "./Primitive.js";import S2Cell from "../Core/S2Cell.js";let centerCartographicScratch = new Cartographic();/** * A tile bounding volume specified as an S2 cell token with minimum and maximum heights. * The bounding volume is a k DOP. A k-DOP is the Boolean intersection of extents along k directions. * * @alias TileBoundingS2Cell * @constructor * * @param {Object} options Object with the following properties: * @param {String} options.token The token of the S2 cell. * @param {Number} [options.minimumHeight=0.0] The minimum height of the bounding volume. * @param {Number} [options.maximumHeight=0.0] The maximum height of the bounding volume. * @param {Ellipsoid} [options.ellipsoid=Ellipsoid.WGS84] The ellipsoid. * @param {Boolean} [options.computeBoundingVolumes=true] True to compute the {@link TileBoundingS2Cell#boundingVolume} and *                  {@link TileBoundingS2Cell#boundingSphere}. If false, these properties will be undefined. * * @private */function TileBoundingS2Cell(options) {  //>>includeStart('debug', pragmas.debug);  Check.typeOf.object("options", options);  Check.typeOf.string("options.token", options.token);  //>>includeEnd('debug');  const s2Cell = S2Cell.fromToken(options.token);  const minimumHeight = defaultValue(options.minimumHeight, 0.0);  const maximumHeight = defaultValue(options.maximumHeight, 0.0);  const ellipsoid = defaultValue(options.ellipsoid, Ellipsoid.WGS84);  this.s2Cell = s2Cell;  this.minimumHeight = minimumHeight;  this.maximumHeight = maximumHeight;  this.ellipsoid = ellipsoid;  const boundingPlanes = computeBoundingPlanes(    s2Cell,    minimumHeight,    maximumHeight,    ellipsoid  );  this._boundingPlanes = boundingPlanes;  // Pre-compute vertices to speed up the plane intersection test.  const vertices = computeVertices(boundingPlanes);  this._vertices = vertices;  // Pre-compute edge normals to speed up the point-polygon distance check in distanceToCamera.  this._edgeNormals = new Array(6);  this._edgeNormals[0] = computeEdgeNormals(    boundingPlanes[0],    vertices.slice(0, 4)  );  let i;  // Based on the way the edge normals are computed, the edge normals all point away from the "face"  // of the polyhedron they surround, except the plane for the top plane. Therefore, we negate the normals  // for the top plane.  for (i = 0; i < 4; i++) {    this._edgeNormals[0][i] = Cartesian3.negate(      this._edgeNormals[0][i],      this._edgeNormals[0][i]    );  }  this._edgeNormals[1] = computeEdgeNormals(    boundingPlanes[1],    vertices.slice(4, 8)  );  for (i = 0; i < 4; i++) {    // For each plane, iterate through the vertices in CCW order.    this._edgeNormals[2 + i] = computeEdgeNormals(boundingPlanes[2 + i], [      vertices[i % 4],      vertices[(i + 1) % 4],      vertices[4 + ((i + 1) % 4)],      vertices[4 + i],    ]);  }  this._planeVertices = [    this._vertices.slice(0, 4),    this._vertices.slice(4, 8),  ];  for (i = 0; i < 4; i++) {    this._planeVertices.push([      this._vertices[i % 4],      this._vertices[(i + 1) % 4],      this._vertices[4 + ((i + 1) % 4)],      this._vertices[4 + i],    ]);  }  const center = s2Cell.getCenter();  centerCartographicScratch = ellipsoid.cartesianToCartographic(    center,    centerCartographicScratch  );  centerCartographicScratch.height = (maximumHeight + minimumHeight) / 2;  this.center = ellipsoid.cartographicToCartesian(    centerCartographicScratch,    center  );  this._boundingSphere = BoundingSphere.fromPoints(vertices);}const centerGeodeticNormalScratch = new Cartesian3();const topCartographicScratch = new Cartographic();const topScratch = new Cartesian3();const vertexCartographicScratch = new Cartographic();const vertexScratch = new Cartesian3();const vertexGeodeticNormalScratch = new Cartesian3();const sideNormalScratch = new Cartesian3();const sideScratch = new Cartesian3();/** * Computes bounding planes of the kDOP. * @private */function computeBoundingPlanes(  s2Cell,  minimumHeight,  maximumHeight,  ellipsoid) {  const planes = new Array(6);  const centerPoint = s2Cell.getCenter();  // Compute top plane.  // - Get geodetic surface normal at the center of the S2 cell.  // - Get center point at maximum height of bounding volume.  // - Create top plane from surface normal and top point.  const centerSurfaceNormal = ellipsoid.geodeticSurfaceNormal(    centerPoint,    centerGeodeticNormalScratch  );  const topCartographic = ellipsoid.cartesianToCartographic(    centerPoint,    topCartographicScratch  );  topCartographic.height = maximumHeight;  const top = ellipsoid.cartographicToCartesian(topCartographic, topScratch);  const topPlane = Plane.fromPointNormal(top, centerSurfaceNormal);  planes[0] = topPlane;  // Compute bottom plane.  // - Iterate through bottom vertices  //   - Get distance from vertex to top plane  // - Find longest distance from vertex to top plane  // - Translate top plane by the distance  let maxDistance = 0;  let i;  const vertices = [];  let vertex, vertexCartographic;  for (i = 0; i < 4; i++) {    vertex = s2Cell.getVertex(i);    vertices[i] = vertex;    vertexCartographic = ellipsoid.cartesianToCartographic(      vertex,      vertexCartographicScratch    );    vertexCartographic.height = minimumHeight;    const distance = Plane.getPointDistance(      topPlane,      ellipsoid.cartographicToCartesian(vertexCartographic, vertexScratch)    );    if (distance < maxDistance) {      maxDistance = distance;    }  }  const bottomPlane = Plane.clone(topPlane);  // Negate the normal of the bottom plane since we want all normals to point "outwards".  bottomPlane.normal = Cartesian3.negate(    bottomPlane.normal,    bottomPlane.normal  );  bottomPlane.distance = bottomPlane.distance * -1 + maxDistance;  planes[1] = bottomPlane;  // Compute side planes.  // - Iterate through vertices (in CCW order, by default)  //   - Get a vertex and another vertex adjacent to it.  //   - Compute geodetic surface normal at one vertex.  //   - Compute vector between vertices.  //   - Compute normal of side plane. (cross product of top dir and side dir)  for (i = 0; i < 4; i++) {    vertex = vertices[i];    const adjacentVertex = vertices[(i + 1) % 4];    const geodeticNormal = ellipsoid.geodeticSurfaceNormal(      vertex,      vertexGeodeticNormalScratch    );    const side = Cartesian3.subtract(adjacentVertex, vertex, sideScratch);    let sideNormal = Cartesian3.cross(side, geodeticNormal, sideNormalScratch);    sideNormal = Cartesian3.normalize(sideNormal, sideNormal);    planes[2 + i] = Plane.fromPointNormal(vertex, sideNormal);  }  return planes;}let n0Scratch = new Cartesian3();let n1Scratch = new Cartesian3();let n2Scratch = new Cartesian3();let x0Scratch = new Cartesian3();let x1Scratch = new Cartesian3();let x2Scratch = new Cartesian3();const t0Scratch = new Cartesian3();const t1Scratch = new Cartesian3();const t2Scratch = new Cartesian3();let f0Scratch = new Cartesian3();let f1Scratch = new Cartesian3();let f2Scratch = new Cartesian3();let sScratch = new Cartesian3();const matrixScratch = new Matrix3();/** * Computes intersection of 3 planes. * @private */function computeIntersection(p0, p1, p2) {  n0Scratch = p0.normal;  n1Scratch = p1.normal;  n2Scratch = p2.normal;  x0Scratch = Cartesian3.multiplyByScalar(p0.normal, -p0.distance, x0Scratch);  x1Scratch = Cartesian3.multiplyByScalar(p1.normal, -p1.distance, x1Scratch);  x2Scratch = Cartesian3.multiplyByScalar(p2.normal, -p2.distance, x2Scratch);  f0Scratch = Cartesian3.multiplyByScalar(    Cartesian3.cross(n1Scratch, n2Scratch, t0Scratch),    Cartesian3.dot(x0Scratch, n0Scratch),    f0Scratch  );  f1Scratch = Cartesian3.multiplyByScalar(    Cartesian3.cross(n2Scratch, n0Scratch, t1Scratch),    Cartesian3.dot(x1Scratch, n1Scratch),    f1Scratch  );  f2Scratch = Cartesian3.multiplyByScalar(    Cartesian3.cross(n0Scratch, n1Scratch, t2Scratch),    Cartesian3.dot(x2Scratch, n2Scratch),    f2Scratch  );  matrixScratch[0] = n0Scratch.x;  matrixScratch[1] = n1Scratch.x;  matrixScratch[2] = n2Scratch.x;  matrixScratch[3] = n0Scratch.y;  matrixScratch[4] = n1Scratch.y;  matrixScratch[5] = n2Scratch.y;  matrixScratch[6] = n0Scratch.z;  matrixScratch[7] = n1Scratch.z;  matrixScratch[8] = n2Scratch.z;  const determinant = Matrix3.determinant(matrixScratch);  sScratch = Cartesian3.add(f0Scratch, f1Scratch, sScratch);  sScratch = Cartesian3.add(sScratch, f2Scratch, sScratch);  return new Cartesian3(    sScratch.x / determinant,    sScratch.y / determinant,    sScratch.z / determinant  );}/** * Compute the vertices of the kDOP. * @private */function computeVertices(boundingPlanes) {  const vertices = new Array(8);  for (let i = 0; i < 4; i++) {    // Vertices on the top plane.    vertices[i] = computeIntersection(      boundingPlanes[0],      boundingPlanes[2 + ((i + 3) % 4)],      boundingPlanes[2 + (i % 4)]    );    // Vertices on the bottom plane.    vertices[i + 4] = computeIntersection(      boundingPlanes[1],      boundingPlanes[2 + ((i + 3) % 4)],      boundingPlanes[2 + (i % 4)]    );  }  return vertices;}let edgeScratch = new Cartesian3();let edgeNormalScratch = new Cartesian3();/** * Compute edge normals on a plane. * @private */function computeEdgeNormals(plane, vertices) {  const edgeNormals = [];  for (let i = 0; i < 4; i++) {    edgeScratch = Cartesian3.subtract(      vertices[(i + 1) % 4],      vertices[i],      edgeScratch    );    edgeNormalScratch = Cartesian3.cross(      plane.normal,      edgeScratch,      edgeNormalScratch    );    edgeNormalScratch = Cartesian3.normalize(      edgeNormalScratch,      edgeNormalScratch    );    edgeNormals[i] = Cartesian3.clone(edgeNormalScratch);  }  return edgeNormals;}Object.defineProperties(TileBoundingS2Cell.prototype, {  /**   * The underlying bounding volume.   *   * @memberof TileOrientedBoundingBox.prototype   *   * @type {Object}   * @readonly   */  boundingVolume: {    get: function () {      return this;    },  },  /**   * The underlying bounding sphere.   *   * @memberof TileOrientedBoundingBox.prototype   *   * @type {BoundingSphere}   * @readonly   */  boundingSphere: {    get: function () {      return this._boundingSphere;    },  },});const facePointScratch = new Cartesian3();/** * The distance to point check for this kDOP involves checking the signed distance of the point to each bounding * plane. A plane qualifies for a distance check if the point being tested against is in the half-space in the direction * of the normal i.e. if the signed distance of the point from the plane is greater than 0. * * There are 4 possible cases for a point if it is outside the polyhedron: * *   \     X     /     X \           /       \           /       \           / * ---\---------/---   ---\---------/---   ---X---------/---   ---\---------/--- *     \       /           \       /           \       /           \       / *   ---\-----/---       ---\-----/---       ---\-----/---       ---\-----/--- *       \   /               \   /               \   /               \   / *                                                                    \ / *                                                                     \ *                                                                    / \ *                                                                   / X \ * *         I                  II                  III                 IV * * Case I: There is only one plane selected. * In this case, we project the point onto the plane and do a point polygon distance check to find the closest point on the polygon. * The point may lie inside the "face" of the polygon or outside. If it is outside, we need to determine which edges to test against. * * Case II: There are two planes selected. * In this case, the point will lie somewhere on the line created at the intersection of the selected planes or one of the planes. * * Case III: There are three planes selected. * In this case, the point will lie on the vertex, at the intersection of the selected planes. * * Case IV: There are more than three planes selected. * Since we are on an ellipsoid, this will only happen in the bottom plane, which is what we will use for the distance test. */TileBoundingS2Cell.prototype.distanceToCamera = function (frameState) {  //>>includeStart('debug', pragmas.debug);  Check.defined("frameState", frameState);  //>>includeEnd('debug');  const point = frameState.camera.positionWC;  const selectedPlaneIndices = [];  const vertices = [];  let edgeNormals;  if (Plane.getPointDistance(this._boundingPlanes[0], point) > 0) {    selectedPlaneIndices.push(0);    vertices.push(this._planeVertices[0]);    edgeNormals = this._edgeNormals[0];  } else if (Plane.getPointDistance(this._boundingPlanes[1], point) > 0) {    selectedPlaneIndices.push(1);    vertices.push(this._planeVertices[1]);    edgeNormals = this._edgeNormals[1];  }  let i;  let sidePlaneIndex;  for (i = 0; i < 4; i++) {    sidePlaneIndex = 2 + i;    if (      Plane.getPointDistance(this._boundingPlanes[sidePlaneIndex], point) > 0    ) {      selectedPlaneIndices.push(sidePlaneIndex);      // Store vertices in CCW order.      vertices.push(this._planeVertices[sidePlaneIndex]);      edgeNormals = this._edgeNormals[sidePlaneIndex];    }  }  // Check if inside all planes.  if (selectedPlaneIndices.length === 0) {    return 0.0;  }  // We use the skip variable when the side plane indices are non-consecutive.  let facePoint;  let selectedPlane;  if (selectedPlaneIndices.length === 1) {    // Handles Case I    selectedPlane = this._boundingPlanes[selectedPlaneIndices[0]];    facePoint = closestPointPolygon(      Plane.projectPointOntoPlane(selectedPlane, point, facePointScratch),      vertices[0],      selectedPlane,      edgeNormals    );    return Cartesian3.distance(facePoint, point);  } else if (selectedPlaneIndices.length === 2) {    // Handles Case II    // Since we are on the ellipsoid, the dihedral angle between a top plane and a side plane    // will always be acute, so we can do a faster check there.    if (selectedPlaneIndices[0] === 0) {      const edge = [        this._vertices[          4 * selectedPlaneIndices[0] + (selectedPlaneIndices[1] - 2)        ],        this._vertices[          4 * selectedPlaneIndices[0] + ((selectedPlaneIndices[1] - 2 + 1) % 4)        ],      ];      facePoint = closestPointLineSegment(point, edge[0], edge[1]);      return Cartesian3.distance(facePoint, point);    }    let minimumDistance = Number.MAX_VALUE;    let distance;    for (i = 0; i < 2; i++) {      selectedPlane = this._boundingPlanes[selectedPlaneIndices[i]];      facePoint = closestPointPolygon(        Plane.projectPointOntoPlane(selectedPlane, point, facePointScratch),        vertices[i],        selectedPlane,        this._edgeNormals[selectedPlaneIndices[i]]      );      distance = Cartesian3.distanceSquared(facePoint, point);      if (distance < minimumDistance) {        minimumDistance = distance;      }    }    return Math.sqrt(minimumDistance);  } else if (selectedPlaneIndices.length > 3) {    // Handles Case IV    facePoint = closestPointPolygon(      Plane.projectPointOntoPlane(        this._boundingPlanes[1],        point,        facePointScratch      ),      this._planeVertices[1],      this._boundingPlanes[1],      this._edgeNormals[1]    );    return Cartesian3.distance(facePoint, point);  }  // Handles Case III  const skip =    selectedPlaneIndices[1] === 2 && selectedPlaneIndices[2] === 5 ? 0 : 1;  // Vertex is on top plane.  if (selectedPlaneIndices[0] === 0) {    return Cartesian3.distance(      point,      this._vertices[(selectedPlaneIndices[1] - 2 + skip) % 4]    );  }  // Vertex is on bottom plane.  return Cartesian3.distance(    point,    this._vertices[4 + ((selectedPlaneIndices[1] - 2 + skip) % 4)]  );};const dScratch = new Cartesian3();const pL0Scratch = new Cartesian3();/** * Finds point on a line segment closest to a given point. * @private */function closestPointLineSegment(p, l0, l1) {  const d = Cartesian3.subtract(l1, l0, dScratch);  const pL0 = Cartesian3.subtract(p, l0, pL0Scratch);  let t = Cartesian3.dot(d, pL0);  if (t <= 0) {    return l0;  }  const dMag = Cartesian3.dot(d, d);  if (t >= dMag) {    return l1;  }  t = t / dMag;  return new Cartesian3(    (1 - t) * l0.x + t * l1.x,    (1 - t) * l0.y + t * l1.y,    (1 - t) * l0.z + t * l1.z  );}const edgePlaneScratch = new Plane(Cartesian3.UNIT_X, 0.0);/** * Finds closes point on the polygon, created by the given vertices, from * a point. The test point and the polygon are all on the same plane. * @private */function closestPointPolygon(p, vertices, plane, edgeNormals) {  let minDistance = Number.MAX_VALUE;  let distance;  let closestPoint;  let closestPointOnEdge;  for (let i = 0; i < vertices.length; i++) {    const edgePlane = Plane.fromPointNormal(      vertices[i],      edgeNormals[i],      edgePlaneScratch    );    const edgePlaneDistance = Plane.getPointDistance(edgePlane, p);    // Skip checking against the edge if the point is not in the half-space that the    // edgePlane's normal points towards i.e. if the edgePlane is facing away from the point.    if (edgePlaneDistance < 0) {      continue;    }    closestPointOnEdge = closestPointLineSegment(      p,      vertices[i],      vertices[(i + 1) % 4]    );    distance = Cartesian3.distance(p, closestPointOnEdge);    if (distance < minDistance) {      minDistance = distance;      closestPoint = closestPointOnEdge;    }  }  if (!defined(closestPoint)) {    return p;  }  return closestPoint;}/** * Determines which side of a plane this volume is located. * * @param {Plane} plane The plane to test against. * @returns {Intersect} {@link Intersect.INSIDE} if the entire volume is on the side of the plane *                      the normal is pointing, {@link Intersect.OUTSIDE} if the entire volume is *                      on the opposite side, and {@link Intersect.INTERSECTING} if the volume *                      intersects the plane. */TileBoundingS2Cell.prototype.intersectPlane = function (plane) {  //>>includeStart('debug', pragmas.debug);  Check.defined("plane", plane);  //>>includeEnd('debug');  let plusCount = 0;  let negCount = 0;  for (let i = 0; i < this._vertices.length; i++) {    const distanceToPlane =      Cartesian3.dot(plane.normal, this._vertices[i]) + plane.distance;    if (distanceToPlane < 0) {      negCount++;    } else {      plusCount++;    }  }  if (plusCount === this._vertices.length) {    return Intersect.INSIDE;  } else if (negCount === this._vertices.length) {    return Intersect.OUTSIDE;  }  return Intersect.INTERSECTING;};/** * Creates a debug primitive that shows the outline of the tile bounding * volume. * * @param {Color} color The desired color of the primitive's mesh * @return {Primitive} */TileBoundingS2Cell.prototype.createDebugVolume = function (color) {  //>>includeStart('debug', pragmas.debug);  Check.defined("color", color);  //>>includeEnd('debug');  const modelMatrix = new Matrix4.clone(Matrix4.IDENTITY);  const topPlanePolygon = new CoplanarPolygonOutlineGeometry({    polygonHierarchy: {      positions: this._planeVertices[0],    },  });  const topPlaneGeometry = CoplanarPolygonOutlineGeometry.createGeometry(    topPlanePolygon  );  const topPlaneInstance = new GeometryInstance({    geometry: topPlaneGeometry,    id: "outline",    modelMatrix: modelMatrix,    attributes: {      color: ColorGeometryInstanceAttribute.fromColor(color),    },  });  const bottomPlanePolygon = new CoplanarPolygonOutlineGeometry({    polygonHierarchy: {      positions: this._planeVertices[1],    },  });  const bottomPlaneGeometry = CoplanarPolygonOutlineGeometry.createGeometry(    bottomPlanePolygon  );  const bottomPlaneInstance = new GeometryInstance({    geometry: bottomPlaneGeometry,    id: "outline",    modelMatrix: modelMatrix,    attributes: {      color: ColorGeometryInstanceAttribute.fromColor(color),    },  });  const sideInstances = [];  for (let i = 0; i < 4; i++) {    const sidePlanePolygon = new CoplanarPolygonOutlineGeometry({      polygonHierarchy: {        positions: this._planeVertices[2 + i],      },    });    const sidePlaneGeometry = CoplanarPolygonOutlineGeometry.createGeometry(      sidePlanePolygon    );    sideInstances[i] = new GeometryInstance({      geometry: sidePlaneGeometry,      id: "outline",      modelMatrix: modelMatrix,      attributes: {        color: ColorGeometryInstanceAttribute.fromColor(color),      },    });  }  return new Primitive({    geometryInstances: [      sideInstances[0],      sideInstances[1],      sideInstances[2],      sideInstances[3],      bottomPlaneInstance,      topPlaneInstance,    ],    appearance: new PerInstanceColorAppearance({      translucent: false,      flat: true,    }),    asynchronous: false,  });};export default TileBoundingS2Cell;
 |