Cesium for Unity 1.26.0
Loading...
Searching...
No Matches
CesiumCartographicPolygon.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using Unity.Mathematics;
4using UnityEngine;
5
6#if SUPPORTS_SPLINES
7using UnityEngine.Splines;
8#endif
9
10namespace CesiumForUnity
11{
40
51 [ExecuteInEditMode]
52#if SUPPORTS_SPLINES
53 [RequireComponent(typeof(SplineContainer))]
54 [RequireComponent(typeof(CesiumGlobeAnchor))]
55 [AddComponentMenu("Cesium/Cesium Cartographic Polygon")]
56#else
57 [AddComponentMenu("")]
58#endif
59 [IconAttribute("Packages/com.cesium.unity/Editor/Resources/Cesium-24x24.png")]
60 public partial class CesiumCartographicPolygon : MonoBehaviour
61 {
62 #region Events and Delegates
63
70
76
77 internal static void BroadcastCesiumCartographicPolygonLoadFailure(
79 {
81 {
83 }
84 }
85
86 #endregion
87
88 internal static readonly List<double2> emptyList = new List<double2>();
89
90#if SUPPORTS_SPLINES
91 private SplineContainer _splineContainer;
92 private CesiumGlobeAnchor _globeAnchor;
93#endif
94
95 [SerializeField]
97
110 {
111 get => this._source;
112 set
113 {
114 this._source = value;
115 this.LoadFromSource();
116 }
117 }
118
119 [SerializeField]
120 private string _url = "";
121
129 public string url
130 {
131 get => this._url;
132 set
133 {
134 this._url = value;
135 if (this._source == CesiumCartographicPolygonSource.FromUrl)
136 {
137 this.LoadFromSource();
138 }
139 }
140 }
141
142 [SerializeField]
143 private int _polygonIndex = 0;
144
154 public int polygonIndex
155 {
156 get => this._polygonIndex;
157 set
158 {
159 this._polygonIndex = value;
160 this.LoadFromSource();
161 }
162 }
163
164 [SerializeField]
165 private int _featureIndex = 0;
166
176 public int featureIndex
177 {
178 get => this._featureIndex;
179 set
180 {
181 this._featureIndex = value;
182 this.LoadFromSource();
183 }
184 }
185
186 [SerializeField]
187 private long _ionAssetID = 0;
188
196 public long ionAssetID
197 {
198 get => this._ionAssetID;
199 set
200 {
201 this._ionAssetID = value;
202 if (this._source == CesiumCartographicPolygonSource.FromCesiumIon)
203 {
204 this.LoadFromSource();
205 }
206 }
207 }
208
209 [SerializeField]
210 private string _ionAccessToken = "";
211
220 public string ionAccessToken
221 {
222 get => this._ionAccessToken;
223 set
224 {
225 this._ionAccessToken = value;
226 if (this._source == CesiumCartographicPolygonSource.FromCesiumIon)
227 {
228 this.LoadFromSource();
229 }
230 }
231 }
232
233 [SerializeField]
234 private CesiumIonServer _ionServer = null;
235
244 {
245 get
246 {
247 if (this._ionServer == null)
248 this._ionServer = CesiumIonServer.serverForNewObjects;
249 return this._ionServer;
250 }
251 set
252 {
253 if (value == null) value = CesiumIonServer.serverForNewObjects;
254 this._ionServer = value;
255 if (this._source == CesiumCartographicPolygonSource.FromCesiumIon)
256 {
257 this.LoadFromSource();
258 }
259 }
260 }
261
262 [NonSerialized] private CesiumGeoJsonDocument _document = null;
263
272 {
273 get => this._document;
274 set
275 {
276 this._document = value;
277 this._source = CesiumCartographicPolygonSource.FromGeoJsonDocument;
278 this.ApplyDocument(value);
279 }
280 }
281
282 // Set to true while this component is writing to the spline itself, so that
283 // the resulting Spline.Changed event isn't misinterpreted as a manual edit.
284 // Exposed as internal so the editor can skip its own spline-modified handling
285 // while a reload from the configured source is in progress.
286 [NonSerialized] internal bool _isUpdatingSplineInternally = false;
287
288#if SUPPORTS_SPLINES
289 // Exposed for the editor so it can verify a modified spline belongs to this
290 // polygon before reacting to the global AfterSplineWasModified callback.
291 internal bool IsSplineOwned(Spline spline)
292 {
293 if (this._splineContainer == null)
294 {
295 return false;
296 }
297
298 IReadOnlyList<Spline> splines = this._splineContainer.Splines;
299 for (int i = 0; i < splines.Count; i++)
300 {
301 if (splines[i] == spline)
302 {
303 return true;
304 }
305 }
306
307 return false;
308 }
309#endif
310
311 void OnEnable()
312 {
313#if SUPPORTS_SPLINES
314 this._splineContainer = this.GetComponent<SplineContainer>();
315 this._globeAnchor = this.GetComponent<CesiumGlobeAnchor>();
316
317 // If this component is created before the Splines package is added, the
318 // "RequireComponent" attributes won't automatically apply. This extra check
319 // should ensure the required components exist.
320 if (this._splineContainer == null)
321 {
322 this._splineContainer = this.gameObject.AddComponent<SplineContainer>();
323#if UNITY_EDITOR
324 this.Reset();
325#endif
326 }
327 if (this._globeAnchor == null)
328 {
329 this._globeAnchor = this.gameObject.AddComponent<CesiumGlobeAnchor>();
330 }
331
332 if (this._source != CesiumCartographicPolygonSource.Manual)
333 {
334 this.LoadFromSource();
335 }
336
337#elif UNITY_2022_2_OR_NEWER
338 Debug.LogError("CesiumCartographicPolygon requires the Splines package, which is currently not installed " +
339 "in the project. Install the Splines package using the Package Manager.");
340#else
341 Debug.LogError("CesiumCartographicPolygon requires the Splines package, which is not available " +
342 "in this version of Unity.");
343#endif
344 }
345
346#if SUPPORTS_SPLINES && UNITY_EDITOR
347 void Reset()
348 {
349 IReadOnlyList<Spline> splines = this._splineContainer.Splines;
350 for (int i = splines.Count - 1; i >= 0; i--)
351 {
352 this._splineContainer.RemoveSpline(splines[i]);
353 }
354
355 Spline defaultSpline = new Spline();
356
357 BezierKnot[] knots = new BezierKnot[] {
358 new BezierKnot(new float3(-100.0f, 0f, -100.0f)),
359 new BezierKnot(new float3(100.0f, 0f, -100.0f)),
360 new BezierKnot(new float3(100.0f, 0f, 100.0f)),
361 new BezierKnot(new float3(-100.0f, 0f, 100.0f)),
362 };
363
364 defaultSpline.Knots = knots;
365 defaultSpline.Closed = true;
366 defaultSpline.SetTangentMode(TangentMode.Linear);
367
368 this._splineContainer.AddSpline(defaultSpline);
369 }
370#endif
371
383 public void Refresh()
384 {
385 if (this._source == CesiumCartographicPolygonSource.Manual)
386 {
387 this.RefreshDependentOverlays();
388 }
389 else
390 {
391 // For non-Manual sources, "LoadFromSource" reloads the spline asynchronously
392 // and refreshes the dependent raster overlays itself once the load completes.
393 LoadFromSource();
394 }
395 }
396
397 private void RefreshDependentOverlays()
398 {
399 CesiumPolygonRasterOverlay[] overlays = FindObjectsByType<CesiumPolygonRasterOverlay>(FindObjectsSortMode.None);
400 for (int i = 0; i < overlays.Length; i++)
401 {
402 CesiumPolygonRasterOverlay overlay = overlays[i];
403 if (overlay == null || overlay.polygons == null)
404 continue;
405 if (overlay.polygons.Contains(this))
406 {
407 overlay.Refresh();
408 }
409 }
410 }
411
412#if !SUPPORTS_SPLINES
413 private void LoadFromSource(){} // Defined as no-op to supress CS1998, since Splines Package is not available during build.
414#else
415 private async void LoadFromSource()
416 {
417 CesiumGeoJsonDocument loaded = null;
418 bool hasEmptySource = false;
419 switch (this._source)
420 {
422 return;
423 case CesiumCartographicPolygonSource.FromGeoJsonDocument:
424 loaded = this._document;
425 break;
427 if (!string.IsNullOrEmpty(this._url))
428 loaded = await CesiumGeoJsonDocument.LoadFromUrlAsync(this._url);
429 else
430 hasEmptySource = true;
431 break;
432 case CesiumCartographicPolygonSource.FromCesiumIon:
433 if (this._ionAssetID > 0)
434 loaded = await CesiumGeoJsonDocument.LoadFromCesiumIonAsync(
435 this._ionAssetID, this._ionAccessToken, this.ionServer);
436 else
437 hasEmptySource = true;
438 break;
439 }
440
441 if (loaded == null && this._source != CesiumCartographicPolygonSource.Manual && !hasEmptySource)
442 {
444 this._source == CesiumCartographicPolygonSource.FromCesiumIon
445 ? CesiumCartographicPolygonLoadType.CesiumIon
447
448 BroadcastCesiumCartographicPolygonLoadFailure(
449 new CesiumCartographicPolygonLoadFailureDetails(
450 this,
451 loadType,
452 "CesiumCartographicPolygon: failed to load the GeoJSON."));
453 }
454
455 // If this component was disabled or destroyed while the load was in flight, skip applying the result.
456 if (!this.isActiveAndEnabled)
457 {
458 return;
459 }
460
461 this.ApplyDocument(loaded);
462
463 // The spline has now been updated from the source, so rebuild any dependent
464 // raster overlays to re-bake the cutout using the new shape.
465 this.RefreshDependentOverlays();
466 }
467#endif
468
487 private static List<double2> GetPolygonRingPoints(CesiumGeoJsonDocument geoJsonDocument, int featureIndex, int polygonIndex)
488 {
489#if SUPPORTS_SPLINES
490 if (geoJsonDocument == null)
491 return null;
492
493 CesiumGeoJsonObject obj = geoJsonDocument.GetRootObject();
494 if (obj == null)
495 return null;
496
497 CesiumGeoJsonObjectType objType = obj.GetObjectType();
498
499 CesiumGeoJsonObject geometry = obj;
500 if (objType == CesiumGeoJsonObjectType.Feature)
501 {
502 CesiumGeoJsonFeature feature = obj.GetObjectAsFeature();
503 geometry = feature?.GetGeometry();
504 }
505 else if (objType == CesiumGeoJsonObjectType.FeatureCollection)
506 {
507 CesiumGeoJsonFeature[] features = obj.GetObjectAsFeatureCollection();
508
509 // Bounds check: if featureIndex is out of range, return null (no cutout).
510 if (features == null || features.Length == 0 || featureIndex < 0 || featureIndex >= features.Length)
511 return null;
512
513 geometry = features[featureIndex].GetGeometry();
514 }
515
516 if (geometry == null)
517 return null;
518
519 CesiumGeoJsonLineString[] rings = null;
520
521 CesiumGeoJsonObjectType geometryType = geometry.GetObjectType();
522 if (geometryType == CesiumGeoJsonObjectType.Polygon)
523 {
524 rings = geometry.GetObjectAsPolygon()?.rings;
525 }
526 else if (geometryType == CesiumGeoJsonObjectType.MultiPolygon)
527 {
528 CesiumGeoJsonPolygon[] polys = geometry.GetObjectAsMultiPolygon();
529
530 // Bounds check: if polygonIndex is out of range, return null (no cutout).
531 if (polys == null || polys.Length == 0 || polygonIndex < 0 || polygonIndex >= polys.Length)
532 return null;
533
534 rings = polys[polygonIndex]?.rings;
535 }
536
537 if (rings == null || rings.Length == 0 || rings[0] == null || rings[0].points.Length < 3)
538 return null;
539
540 List<double2> result = new List<double2>(rings[0].points.Length);
541 foreach (double3 p in rings[0].points)
542 result.Add(new double2(p.x, p.y));
543 return result;
544#else
545 return null;
546#endif
547 }
548
549 private void ApplyDocument(CesiumGeoJsonDocument geoJsonDocument)
550 {
551#if SUPPORTS_SPLINES
552 List<double2> points = GetPolygonRingPoints(geoJsonDocument, this._featureIndex, this._polygonIndex);
553 if (points == null || points.Count < 3)
554 {
555 // Clear any existing spline so that no cutout is applied.
556 this._isUpdatingSplineInternally = true;
557 try
558 {
559 IReadOnlyList<Spline> splines = this._splineContainer.Splines;
560 for (int i = splines.Count - 1; i >= 0; i--)
561 {
562 this._splineContainer.RemoveSpline(splines[i]);
563 }
564 }
565 finally
566 {
567 this._isUpdatingSplineInternally = false;
568 }
569
570 return;
571 }
572
573 CesiumGeoreference georeference = this._globeAnchor.GetComponentInParent<CesiumGeoreference>();
574 if (georeference == null)
575 {
576 Debug.LogError("CesiumCartographicPolygon could not find a CesiumGeoreference in its " +
577 "parent hierarchy, so the GeoJSON source could not be applied.");
578 return;
579 }
580
581 georeference.Initialize();
582
583 // Remove the last point if it duplicates the first (GeoJson Spec.)
584 if (points.Count > 3)
585 {
586 double2 first = points[0];
587 double2 last = points[points.Count - 1];
588 if (math.abs(first.x - last.x) < 1e-9 && math.abs(first.y - last.y) < 1e-9)
589 {
590 points.RemoveAt(points.Count - 1);
591 }
592 }
593
594 Matrix4x4 worldToLocal = this.transform.worldToLocalMatrix;
595
596 BezierKnot[] knots = new BezierKnot[points.Count];
597 for (int i = 0; i < points.Count; i++)
598 {
599 double3 cartographic = new double3(points[i].x, points[i].y, 0.0);
600 double3 ecef = georeference.ellipsoid.LongitudeLatitudeHeightToCenteredFixed(cartographic);
601 double3 unityPosition = georeference.TransformEarthCenteredEarthFixedPositionToUnity(ecef);
602 Vector3 localPosition = worldToLocal.MultiplyPoint3x4(
603 new Vector3((float)unityPosition.x, (float)unityPosition.y, (float)unityPosition.z));
604 knots[i] = new BezierKnot(new float3(localPosition.x, localPosition.y, localPosition.z));
605 }
606
607 Spline newSpline = new Spline();
608 newSpline.Knots = knots;
609 newSpline.Closed = true;
610 newSpline.SetTangentMode(TangentMode.Linear);
611
612 this._isUpdatingSplineInternally = true;
613 try
614 {
615 IReadOnlyList<Spline> splines = this._splineContainer.Splines;
616 for (int i = splines.Count - 1; i >= 0; i--)
617 {
618 this._splineContainer.RemoveSpline(splines[i]);
619 }
620
621 this._splineContainer.AddSpline(newSpline);
622 }
623 finally
624 {
625 this._isUpdatingSplineInternally = false;
626 }
627#endif
628 }
629
630 internal List<double2> GetCartographicPoints(Matrix4x4 worldToTileset)
631 {
632#if SUPPORTS_SPLINES
633 CesiumGeoreference georeference = this._globeAnchor.GetComponentInParent<CesiumGeoreference>();
634 if (georeference == null)
635 {
636 return emptyList;
637 }
638
639 IReadOnlyList<Spline> splines = this._splineContainer.Splines;
640 if (splines.Count == 0)
641 {
642 return emptyList;
643 }
644
645 if (splines.Count > 1)
646 {
647 Debug.LogWarning("CesiumCartographicPolygon has multiple splines in its Spline component, " +
648 "but can only support one at a time. Only the first spline will be rasterized.");
649 }
650
651 Spline spline = splines[0];
652 if (!spline.Closed)
653 {
654 Debug.LogError("Spline must be closed to be used as a cartographic polygon.");
655 return emptyList;
656 }
657
658 BezierKnot[] knots = spline.ToArray();
659 List<double2> cartographicPoints = new List<double2>(knots.Length);
660
661 float4x4 localToWorld = this.transform.localToWorldMatrix;
662
663 for (int i = 0; i < knots.Length; i++)
664 {
665 if (spline.GetTangentMode(i) != TangentMode.Linear)
666 {
667 Debug.LogError("CesiumCartographicPolygon only supports linear splines.");
668 return emptyList;
669 }
670
671 BezierKnot knot = knots[i];
672
673 // The spline points should be located in the tileset *exactly where they
674 // appear to be*. The way we do that is by getting their world position, and
675 // then transforming that world position to a Cesium3DTileset local position.
676 // That way if the tileset is transformed relative to the globe, the polygon
677 // will still affect the tileset where the user thinks it should.
678
679 float3 worldPosition = knot.Transform(localToWorld).Position;
680 float3 unityPosition = worldToTileset.MultiplyPoint3x4(worldPosition);
681 double3 ecefPosition = georeference.TransformUnityPositionToEarthCenteredEarthFixed(unityPosition);
682 double3 cartographicPosition = georeference.ellipsoid.CenteredFixedToLongitudeLatitudeHeight(ecefPosition);
683
684 cartographicPoints.Add(cartographicPosition.xy);
685 }
686
687 return cartographicPoints;
688#else
689 return emptyList;
690#endif
691 }
692 }
693}
A spline-based polygon used to rasterize 2D polygons on top of Cesium3DTilesets.
string url
The URL from which to load the GeoJSON document.
CesiumIonServer ionServer
The Cesium ion server from which this polygon's GeoJSON is loaded.
long ionAssetID
The ID of the Cesium ion asset to use.
static CartographicPolygonLoadFailureDelegate OnCesiumCartographicPolygonLoadFailure
An event that is raised when the polygon encounters an error that prevents it from loading.
CesiumGeoJsonDocument document
Gets or sets the GeoJSON document used by this polygon.
int polygonIndex
The zero-based index of the polygon to use when the source geometry is a MultiPolygon.
void Refresh()
Reloads the polygon's spline from the configured source and rebuilds any CesiumPolygonRasterOverlay t...
CesiumCartographicPolygonSource source
The source from which this polygon's shape is derived.
int featureIndex
The zero-based index of the feature to use when the GeoJSON root is a FeatureCollection.
string ionAccessToken
The access token to use to access the Cesium ion resource.
delegate void CartographicPolygonLoadFailureDelegate(CesiumCartographicPolygonLoadFailureDetails details)
Encapsulates a method that receives details of a cartographic polygon load failure.
Represents a parsed GeoJSON document.
Anchors this game object to the globe.
Defines a Cesium ion Server.
static CesiumIonServer serverForNewObjects
Gets the current Cesium ion server that should be assigned to newly-created objects.
A raster overlay that rasterizes polygons and drapes them over the tileset.
List< CesiumCartographicPolygon > polygons
The polygons to rasterize for this overlay.
void Refresh()
Refreshes this overlay by calling RemoveFromTileset followed by AddToTileset.
CesiumCartographicPolygonSource
Configures where a CesiumCartographicPolygon gets the vertices that define its shape.
@ FromGeoJsonDocument
The polygon's shape is loaded from a GeoJSON document that has been parsed and assigned in code using...
@ Manual
The polygon's shape is defined manually by editing the spline directly in the Editor (or at runtime).
CesiumCartographicPolygonLoadType
The type of CesiumCartographicPolygon load that encountered an error.
CesiumGeoJsonObjectType
The type of a GeoJSON object.
@ FromCesiumIon
The dataset is from Cesium ion.
@ FromUrl
The dataset is from a regular web URL.